← Back to Blog

The Singleton Pattern in C# — Practical Guide

The Singleton pattern is one of the most commonly used design patterns in software development, especially in C#. It ensures that a class has only one instance and provides a global point of access to it. This might sound simple, but implementing it correctly, especially in multithreaded environments, requires a clear understanding of the nuances of object initialization, memory management, and thread safety. In this article, we will explore every aspect of the Singleton pattern in C#, from the basic implementation to advanced scenarios including multithreading, dependency injection, serialization, and real-world applications.

Understanding the Singleton Pattern

At its core, the Singleton pattern serves two main purposes:

  • Ensures that only one instance of a class exists throughout the application lifetime.
  • Provides a single point of access to that instance, often through a static property or method.

Singleton is widely used for shared resources such as configuration settings, logging services, cache management, and connection pools. Without a Singleton, developers might inadvertently create multiple instances of such services, which could lead to inconsistent behavior, increased memory usage, or race conditions.

Singleton is ideal for global state management, but it must be used cautiously. Overuse can lead to tight coupling and testing difficulties.

Basic Singleton Implementation

Let’s start with the simplest implementation of a Singleton in C#. This is a non-thread-safe version suitable for single-threaded applications.

public class Singleton
{
    private static Singleton _instance;

    private Singleton() 
    {
        // Private constructor prevents instantiation
    }

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

Explanation:

  • _instance holds the single instance of the class.
  • The constructor is private to prevent external instantiation.
  • The Instance property checks if the instance exists and creates it if it doesn’t.

While this is simple, it is not thread-safe. If two threads access the Instance property simultaneously, two instances might be created.

Thread-Safe Singleton Implementations

In real-world applications, thread safety is crucial. C# offers multiple ways to implement a thread-safe Singleton.

Using Lock for Thread Safety

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

    private Singleton() { }

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

Here, we use a lock object to prevent multiple threads from creating multiple instances. While effective, this approach has a minor performance overhead because every access to Instance requires locking.

Double-Check Locking

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

    private Singleton() { }

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

Double-check locking reduces unnecessary locking once the instance has been created, improving performance while remaining thread-safe.

Using Lazy<T> for Lazy Initialization

public class Singleton
{
    private static readonly Lazy<Singleton> _instance = new Lazy<Singleton>(() => new Singleton());

    private Singleton() { }

    public static Singleton Instance => _instance.Value;
}

Explanation:

  • Lazy<T> ensures the instance is created only when accessed for the first time.
  • Thread safety is built-in by default.
  • Clean, concise, and recommended for most use cases.

Eager Initialization

Eager initialization creates the instance at the time of class loading. This method is simple and thread-safe without locks.

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

    private Singleton() { }

    public static Singleton Instance => _instance;
}

Pros:

  • Thread-safe without locks
  • Simple implementation
Cons:
  • Instance is created even if it’s never used, which could waste resources

Singleton with Inheritance Considerations

Allowing inheritance in Singleton can break the pattern because subclasses can instantiate multiple instances. To prevent this, use the sealed keyword.

public sealed class Singleton
{
    private static readonly Singleton _instance = new Singleton();
    private Singleton() { }
    public static Singleton Instance => _instance;
}

By sealing the class, we prevent subclassing and ensure only one instance exists.

Singleton in Multithreaded Async Scenarios

Sometimes initialization involves asynchronous operations. Standard lazy initialization doesn't support async directly, but we can use AsyncLazy patterns.

public class Singleton
{
    private static readonly Lazy<Task<Singleton>> _instance =
        new Lazy<Task<Singleton>>(async () => await InitializeAsync());

    private Singleton() { }

    public static Task<Singleton> Instance => _instance.Value;

    private static async Task<Singleton> InitializeAsync()
    {
        await Task.Delay(100); // simulate async work
        return new Singleton();
    }
}

This approach ensures the singleton instance is fully initialized even when async operations are involved.

Dependency Injection and Singleton

Modern C# applications often use DI frameworks. DI provides native support for singletons while maintaining testability.

// Register singleton service
services.AddSingleton<ILoggingService, LoggingService>();

// Usage in constructor injection
public class OrderService
{
    private readonly ILoggingService _logger;
    public OrderService(ILoggingService logger)
    {
        _logger = logger;
    }
}

This method removes the need for manual singleton implementation while allowing unit testing by replacing the service with mocks.

Serialization Considerations

Serializing a singleton can break its uniqueness because deserialization creates a new instance. Implement ISerializable to maintain the singleton pattern during serialization.

[Serializable]
public sealed class Singleton : ISerializable
{
    private static readonly Singleton _instance = new Singleton();
    private Singleton() { }

    public static Singleton Instance => _instance;

    // Ensure singleton during deserialization
    private Singleton(SerializationInfo info, StreamingContext context) { }

    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        // Optional: add custom serialization logic
    }

    [OnDeserialized]
    private void OnDeserialized(StreamingContext context)
    {
        // Replace the deserialized object with the singleton instance
        System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof(Singleton));
    }
}

Common Pitfalls and Best Practices

Developers often misuse the Singleton pattern. Here are common mistakes and best practices:

  • Global state abuse: Use singletons only for true global services.
  • Thread safety: Always consider multithreading implications.
  • Testing challenges: Favor DI frameworks to improve testability.
  • Serialization pitfalls: Ensure only one instance exists after deserialization.
Remember: A Singleton is about controlled access, not just a global variable. Avoid overusing it.

Real-World Singleton Examples

Singleton is commonly used in logging, configuration, caching, and connection pools. Let’s take a logging example.

public sealed class Logger
{
    private static readonly Lazy<Logger> _instance = new Lazy<Logger>(() => new Logger());
    private Logger() { }
    public static Logger Instance => _instance.Value;

    public void Log(string message)
    {
        Console.WriteLine($"{DateTime.Now}: {message}");
    }
}

// Usage
Logger.Instance.Log("Application started.");

Here, we have a globally accessible logger instance, ensuring all parts of the application use the same logging mechanism.

Summary

The Singleton pattern is a powerful design pattern in C# that ensures a class has only one instance and provides controlled access to it. We explored multiple implementations:

  • Basic non-thread-safe Singleton
  • Thread-safe Singleton with locking and double-check
  • Lazy<T> initialization
  • Eager initialization
  • Async-safe Singleton
  • DI-managed Singleton for testable applications

We also examined advanced scenarios including serialization, inheritance concerns, and real-world use cases. The key takeaway is that Singleton should be used judiciously: ensure thread safety, avoid global state abuse, and leverage DI where possible. Mastering Singleton allows you to create consistent, reliable, and maintainable services in your C# applications.