Skip to content
Go back

Singleton Design Pattern in C#: Thread-Safe Implementation and Modern Alternatives

Published:

The Singleton design pattern ensures that a class has only one instance throughout the application’s lifetime and provides global access to that instance. While it’s one of the most well-known design patterns, it’s also one of the most controversial due to potential issues with testability, thread safety, and coupling.

In this comprehensive guide, we’ll explore various implementations of the Singleton pattern in C#, from basic approaches to thread-safe versions, and discuss modern alternatives using dependency injection containers.

Table of Contents

Open Table of Contents

What is the Singleton Pattern?

The Singleton pattern restricts the instantiation of a class to a single instance and provides a global point of access to that instance. It’s useful when you need exactly one instance of a class to coordinate actions across your application.

Common Use Cases

Key Characteristics

Basic Singleton Implementation

Let’s start with the simplest implementation and progressively improve it:

1. Naive Implementation (Not Thread-Safe)

public class NaiveSingleton
{
    private static NaiveSingleton _instance;

    // Private constructor prevents external instantiation
    private NaiveSingleton()
    {
        Console.WriteLine("NaiveSingleton instance created");
    }

    public static NaiveSingleton Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new NaiveSingleton();
            }
            return _instance;
        }
    }

    public void DoSomething()
    {
        Console.WriteLine("Doing something with the singleton instance");
    }
}

// Usage
var singleton1 = NaiveSingleton.Instance;
var singleton2 = NaiveSingleton.Instance;
Console.WriteLine(ReferenceEquals(singleton1, singleton2)); // True

Problems with this approach:

Thread-Safe Singleton Implementations

2. Simple Thread-Safe Version (Lock-Based)

public class ThreadSafeSingleton
{
    private static ThreadSafeSingleton _instance;
    private static readonly object _lock = new object();

    private ThreadSafeSingleton()
    {
        Console.WriteLine("ThreadSafeSingleton instance created");
    }

    public static ThreadSafeSingleton Instance
    {
        get
        {
            lock (_lock)
            {
                if (_instance == null)
                {
                    _instance = new ThreadSafeSingleton();
                }
                return _instance;
            }
        }
    }

    public void DoSomething()
    {
        Console.WriteLine("Thread-safe singleton doing something");
    }
}

Improvement: Thread-safe, but has performance overhead due to locking on every access.

3. Double-Checked Locking Pattern

public class DoubleCheckedLockingSingleton
{
    private static DoubleCheckedLockingSingleton _instance;
    private static readonly object _lock = new object();

    private DoubleCheckedLockingSingleton()
    {
        Console.WriteLine("DoubleCheckedLockingSingleton instance created");
    }

    public static DoubleCheckedLockingSingleton Instance
    {
        get
        {
            // First check without locking
            if (_instance == null)
            {
                lock (_lock)
                {
                    // Second check with locking
                    if (_instance == null)
                    {
                        _instance = new DoubleCheckedLockingSingleton();
                    }
                }
            }
            return _instance;
        }
    }

    public void DoSomething()
    {
        Console.WriteLine("Double-checked locking singleton doing something");
    }
}

Benefits: Reduces locking overhead while maintaining thread safety.

public class LazySingleton
{
    private static readonly Lazy<LazySingleton> _lazy = 
        new Lazy<LazySingleton>(() => new LazySingleton());

    private LazySingleton()
    {
        Console.WriteLine("LazySingleton instance created");
    }

    public static LazySingleton Instance => _lazy.Value;

    public void DoSomething()
    {
        Console.WriteLine("Lazy singleton doing something");
    }
}

Benefits:

5. Static Constructor Implementation

public class StaticConstructorSingleton
{
    private static readonly StaticConstructorSingleton _instance = new StaticConstructorSingleton();

    // Static constructor ensures thread safety
    static StaticConstructorSingleton()
    {
        Console.WriteLine("Static constructor called");
    }

    private StaticConstructorSingleton()
    {
        Console.WriteLine("StaticConstructorSingleton instance created");
    }

    public static StaticConstructorSingleton Instance => _instance;

    public void DoSomething()
    {
        Console.WriteLine("Static constructor singleton doing something");
    }
}

Benefits:

Real-World Singleton Examples

Configuration Manager

public class ConfigurationManager
{
    private static readonly Lazy<ConfigurationManager> _lazy = 
        new Lazy<ConfigurationManager>(() => new ConfigurationManager());
    
    private readonly Dictionary<string, string> _settings;

    private ConfigurationManager()
    {
        _settings = LoadConfiguration();
    }

    public static ConfigurationManager Instance => _lazy.Value;

    private Dictionary<string, string> LoadConfiguration()
    {
        // Simulate loading configuration from file/database
        return new Dictionary<string, string>
        {
            { "DatabaseConnectionString", "Server=localhost;Database=MyApp;" },
            { "ApiBaseUrl", "https://api.myapp.com" },
            { "MaxRetryAttempts", "3" }
        };
    }

    public string GetSetting(string key)
    {
        return _settings.TryGetValue(key, out var value) ? value : null;
    }

    public void SetSetting(string key, string value)
    {
        _settings[key] = value;
    }

    public T GetSetting<T>(string key, T defaultValue = default)
    {
        if (!_settings.TryGetValue(key, out var stringValue))
            return defaultValue;

        try
        {
            return (T)Convert.ChangeType(stringValue, typeof(T));
        }
        catch
        {
            return defaultValue;
        }
    }
}

// Usage
var config = ConfigurationManager.Instance;
var connectionString = config.GetSetting("DatabaseConnectionString");
var maxRetries = config.GetSetting<int>("MaxRetryAttempts", 1);

Logger Service

public class Logger
{
    private static readonly Lazy<Logger> _lazy = new Lazy<Logger>(() => new Logger());
    private readonly object _lock = new object();
    private readonly List<string> _logs = new List<string>();

    private Logger() { }

    public static Logger Instance => _lazy.Value;

    public void LogInfo(string message)
    {
        Log("INFO", message);
    }

    public void LogWarning(string message)
    {
        Log("WARNING", message);
    }

    public void LogError(string message)
    {
        Log("ERROR", message);
    }

    public void LogError(Exception exception)
    {
        Log("ERROR", $"{exception.Message}\n{exception.StackTrace}");
    }

    private void Log(string level, string message)
    {
        var logEntry = $"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] [{level}] {message}";
        
        lock (_lock)
        {
            _logs.Add(logEntry);
            Console.WriteLine(logEntry);
            
            // In real implementation, write to file or external service
            WriteToFile(logEntry);
        }
    }

    private void WriteToFile(string logEntry)
    {
        // Simulate file writing
        // File.AppendAllTextAsync("app.log", logEntry + Environment.NewLine);
    }

    public IReadOnlyList<string> GetLogs()
    {
        lock (_lock)
        {
            return new List<string>(_logs);
        }
    }

    public void ClearLogs()
    {
        lock (_lock)
        {
            _logs.Clear();
        }
    }
}

// Usage
Logger.Instance.LogInfo("Application started");
Logger.Instance.LogWarning("Low disk space detected");
Logger.Instance.LogError(new InvalidOperationException("Something went wrong"));

var allLogs = Logger.Instance.GetLogs();

Database Connection Manager

public class DatabaseConnectionManager
{
    private static readonly Lazy<DatabaseConnectionManager> _lazy = 
        new Lazy<DatabaseConnectionManager>(() => new DatabaseConnectionManager());
    
    private readonly ConcurrentQueue<IDbConnection> _connectionPool;
    private readonly string _connectionString;
    private readonly int _maxPoolSize;

    private DatabaseConnectionManager()
    {
        _connectionString = ConfigurationManager.Instance.GetSetting("DatabaseConnectionString");
        _maxPoolSize = ConfigurationManager.Instance.GetSetting<int>("MaxPoolSize", 10);
        _connectionPool = new ConcurrentQueue<IDbConnection>();
        
        InitializePool();
    }

    public static DatabaseConnectionManager Instance => _lazy.Value;

    private void InitializePool()
    {
        for (int i = 0; i < _maxPoolSize; i++)
        {
            var connection = CreateConnection();
            _connectionPool.Enqueue(connection);
        }
    }

    private IDbConnection CreateConnection()
    {
        // In real implementation, return actual database connection
        return new SqlConnection(_connectionString);
    }

    public IDbConnection GetConnection()
    {
        if (_connectionPool.TryDequeue(out var connection))
        {
            if (connection.State != ConnectionState.Open)
            {
                connection.Open();
            }
            return connection;
        }

        // If pool is empty, create new connection
        return CreateConnection();
    }

    public void ReturnConnection(IDbConnection connection)
    {
        if (connection != null && connection.State == ConnectionState.Open)
        {
            _connectionPool.Enqueue(connection);
        }
    }

    public int AvailableConnections => _connectionPool.Count;
}

// Usage
var dbManager = DatabaseConnectionManager.Instance;
var connection = dbManager.GetConnection();
try
{
    // Use connection for database operations
    // var command = connection.CreateCommand();
    // ...
}
finally
{
    dbManager.ReturnConnection(connection);
}

Generic Singleton Base Class

For scenarios where you need multiple singleton classes, you can create a generic base:

public abstract class Singleton<T> where T : class, new()
{
    private static readonly Lazy<T> _lazy = new Lazy<T>(() => new T());

    public static T Instance => _lazy.Value;
}

// Usage
public class MyService : Singleton<MyService>
{
    public void DoWork()
    {
        Console.WriteLine("MyService doing work");
    }
}

public class AnotherService : Singleton<AnotherService>
{
    public void ProcessData()
    {
        Console.WriteLine("AnotherService processing data");
    }
}

// Usage
MyService.Instance.DoWork();
AnotherService.Instance.ProcessData();

Modern Alternatives: Dependency Injection

While Singleton pattern works, modern C# applications often use dependency injection containers instead:

Using Microsoft.Extensions.DependencyInjection

// Service interface
public interface IConfigurationService
{
    string GetSetting(string key);
    T GetSetting<T>(string key, T defaultValue = default);
}

// Service implementation
public class ConfigurationService : IConfigurationService
{
    private readonly Dictionary<string, string> _settings;

    public ConfigurationService()
    {
        _settings = LoadConfiguration();
    }

    private Dictionary<string, string> LoadConfiguration()
    {
        return new Dictionary<string, string>
        {
            { "DatabaseConnectionString", "Server=localhost;Database=MyApp;" },
            { "ApiBaseUrl", "https://api.myapp.com" },
            { "MaxRetryAttempts", "3" }
        };
    }

    public string GetSetting(string key)
    {
        return _settings.TryGetValue(key, out var value) ? value : null;
    }

    public T GetSetting<T>(string key, T defaultValue = default)
    {
        if (!_settings.TryGetValue(key, out var stringValue))
            return defaultValue;

        try
        {
            return (T)Convert.ChangeType(stringValue, typeof(T));
        }
        catch
        {
            return defaultValue;
        }
    }
}

// Registration in Program.cs or Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    // Register as singleton using DI container
    services.AddSingleton<IConfigurationService, ConfigurationService>();
    
    // Register other services that depend on it
    services.AddScoped<IUserService, UserService>();
}

// Usage in controllers or services
public class UserController : ControllerBase
{
    private readonly IConfigurationService _configService;

    public UserController(IConfigurationService configService)
    {
        _configService = configService;
    }

    [HttpGet]
    public IActionResult GetSettings()
    {
        var apiUrl = _configService.GetSetting("ApiBaseUrl");
        return Ok(new { ApiUrl = apiUrl });
    }
}

Benefits of DI over Traditional Singleton

  1. Better Testability: Easy to mock dependencies
  2. Loose Coupling: Dependencies are injected, not hardcoded
  3. Lifecycle Management: Container manages object lifetime
  4. Configuration: Lifetime can be changed without code modification

Testing Singleton Classes

Testing singleton classes can be challenging. Here are some strategies:

1. Making Singleton Testable

public interface ILogger
{
    void LogInfo(string message);
    void LogError(string message);
}

public class Logger : ILogger
{
    private static readonly Lazy<Logger> _lazy = new Lazy<Logger>(() => new Logger());
    private readonly List<string> _logs = new List<string>();

    private Logger() { }

    public static Logger Instance => _lazy.Value;

    public void LogInfo(string message)
    {
        Log("INFO", message);
    }

    public void LogError(string message)
    {
        Log("ERROR", message);
    }

    private void Log(string level, string message)
    {
        var logEntry = $"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] [{level}] {message}";
        _logs.Add(logEntry);
        Console.WriteLine(logEntry);
    }

    // For testing purposes
    internal IReadOnlyList<string> GetLogs() => _logs.AsReadOnly();
    internal void ClearLogs() => _logs.Clear();
}

// Service that uses logger
public class UserService
{
    private readonly ILogger _logger;

    public UserService(ILogger logger = null)
    {
        _logger = logger ?? Logger.Instance;
    }

    public void CreateUser(string username)
    {
        _logger.LogInfo($"Creating user: {username}");
        // User creation logic
    }
}

2. Unit Tests

[Test]
public void UserService_CreateUser_LogsCorrectMessage()
{
    // Arrange
    var mockLogger = new Mock<ILogger>();
    var userService = new UserService(mockLogger.Object);

    // Act
    userService.CreateUser("testuser");

    // Assert
    mockLogger.Verify(l => l.LogInfo("Creating user: testuser"), Times.Once);
}

[Test]
public void Logger_Instance_ReturnsSameInstance()
{
    // Arrange & Act
    var logger1 = Logger.Instance;
    var logger2 = Logger.Instance;

    // Assert
    Assert.AreSame(logger1, logger2);
}

[Test]
public void Logger_LogInfo_AddsMessageToLogs()
{
    // Arrange
    var logger = Logger.Instance;
    logger.ClearLogs(); // Clear previous test logs

    // Act
    logger.LogInfo("Test message");

    // Assert
    var logs = logger.GetLogs();
    Assert.That(logs.Count, Is.EqualTo(1));
    Assert.That(logs[0], Does.Contain("Test message"));
    Assert.That(logs[0], Does.Contain("[INFO]"));
}

Performance Considerations

Benchmarking Different Implementations

[MemoryDiagnoser]
public class SingletonBenchmark
{
    private const int IterationCount = 1000000;

    [Benchmark]
    public void LazySingleton_Performance()
    {
        for (int i = 0; i < IterationCount; i++)
        {
            var instance = LazySingleton.Instance;
        }
    }

    [Benchmark]
    public void DoubleCheckedLocking_Performance()
    {
        for (int i = 0; i < IterationCount; i++)
        {
            var instance = DoubleCheckedLockingSingleton.Instance;
        }
    }

    [Benchmark]
    public void StaticConstructor_Performance()
    {
        for (int i = 0; i < IterationCount; i++)
        {
            var instance = StaticConstructorSingleton.Instance;
        }
    }
}

Results typically show:

Best Practices and Guidelines

1. When to Use Singleton

✅ Good Use Cases:

❌ Avoid Singleton When:

2. Implementation Guidelines

public class BestPracticeSingleton
{
    // Use Lazy<T> for thread-safe lazy initialization
    private static readonly Lazy<BestPracticeSingleton> _lazy = 
        new Lazy<BestPracticeSingleton>(() => new BestPracticeSingleton());

    // Private constructor to prevent external instantiation
    private BestPracticeSingleton()
    {
        // Initialize resources here
        InitializeResources();
    }

    // Public property for accessing the instance
    public static BestPracticeSingleton Instance => _lazy.Value;

    // Initialize any required resources
    private void InitializeResources()
    {
        // Initialization logic
    }

    // Public methods for singleton functionality
    public void DoWork()
    {
        // Implementation
    }

    // Implement IDisposable if needed for cleanup
    public void Dispose()
    {
        // Cleanup resources
    }
}

3. Error Handling

public class RobustSingleton
{
    private static readonly Lazy<RobustSingleton> _lazy = 
        new Lazy<RobustSingleton>(CreateInstance);

    private static RobustSingleton CreateInstance()
    {
        try
        {
            return new RobustSingleton();
        }
        catch (Exception ex)
        {
            // Log the error
            Console.WriteLine($"Failed to create singleton instance: {ex.Message}");
            
            // You might want to retry or throw a more specific exception
            throw new SingletonCreationException("Failed to initialize singleton", ex);
        }
    }

    private RobustSingleton()
    {
        // Initialization that might fail
        InitializeWithPossibleFailure();
    }

    public static RobustSingleton Instance => _lazy.Value;

    private void InitializeWithPossibleFailure()
    {
        // Simulate initialization that might throw
        if (DateTime.Now.Millisecond % 2 == 0)
        {
            throw new InvalidOperationException("Random initialization failure");
        }
    }
}

public class SingletonCreationException : Exception
{
    public SingletonCreationException(string message, Exception innerException) 
        : base(message, innerException)
    {
    }
}

Common Pitfalls and How to Avoid Them

1. The Problem with Inheritance

// ❌ Don't do this - Singleton with inheritance
public class BaseSingleton
{
    private static BaseSingleton _instance;
    
    protected BaseSingleton() { }
    
    public static BaseSingleton Instance
    {
        get
        {
            if (_instance == null)
                _instance = new BaseSingleton();
            return _instance;
        }
    }
}

public class DerivedSingleton : BaseSingleton
{
    // This breaks the singleton pattern!
    private DerivedSingleton() : base() { }
}

// ✅ Better approach - Use composition over inheritance
public interface ISingletonService
{
    void DoWork();
}

public class SingletonService : ISingletonService
{
    private static readonly Lazy<SingletonService> _lazy = 
        new Lazy<SingletonService>(() => new SingletonService());

    private SingletonService() { }

    public static SingletonService Instance => _lazy.Value;

    public void DoWork()
    {
        Console.WriteLine("Doing work in singleton service");
    }
}

2. Serialization Issues

[Serializable]
public class SerializableSingleton : ISerializable
{
    private static readonly Lazy<SerializableSingleton> _lazy = 
        new Lazy<SerializableSingleton>(() => new SerializableSingleton());

    private SerializableSingleton() { }

    public static SerializableSingleton Instance => _lazy.Value;

    // Prevent multiple instances during deserialization
    protected SerializableSingleton(SerializationInfo info, StreamingContext context)
    {
        // Return existing instance instead of creating new one
    }

    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        // Serialization logic
    }

    // Ensure singleton pattern during deserialization
    private object ReadResolve()
    {
        return Instance;
    }
}

Conclusion

The Singleton pattern is a powerful tool when used appropriately, but it should be applied judiciously. Here are the key takeaways:

  1. Use Lazy<T> for most singleton implementations - it’s thread-safe, performant, and clean
  2. Consider Dependency Injection as a modern alternative for better testability and flexibility
  3. Keep singletons stateless or immutable when possible
  4. Implement proper error handling during initialization

Best Practices:

Modern C# Approach:

// Traditional singleton
var config = ConfigurationManager.Instance;

// Modern DI approach (preferred)
public class MyService
{
    private readonly IConfigurationService _config;
    
    public MyService(IConfigurationService config)
    {
        _config = config;
    }
}

The Singleton pattern remains relevant in C#, but modern dependency injection containers often provide better alternatives for managing object lifetimes while maintaining the benefits of single-instance objects with improved testability and flexibility.

Remember: the goal is not just to implement a pattern, but to solve real problems in your application architecture effectively and maintainably.



Previous Post
Orchard Harvest in New York
Next Post
Conférence Orchard Harvest 2017