Part 2.1 – Singleton Design Pattern
Series: Design Patterns in C# and ASP.NET Core
Part: 2.1 – Creational Design Patterns – Singleton Pattern
Target Audience: Beginners to Advanced .NET Developers
Table of Contents
Introduction
What is the Singleton Pattern?
Why Do We Need the Singleton Pattern?
Problem Statement
Real-World Analogy
Key Characteristics
UML Class Diagram
Basic Singleton Implementation
Thread-Safe Singleton
Lazy Singleton
Singleton in ASP.NET Core
Real-World Use Cases
Advantages
Disadvantages
Best Practices
Common Mistakes
Singleton vs Static Class
Interview Questions
Summary
Introduction
As applications become more complex, there are situations where creating multiple instances of a class is unnecessary or even harmful. Consider services such as logging, application configuration, caching, or database connection management. If every component creates its own instance, the application may waste memory, produce inconsistent state, or become difficult to manage.
The Singleton Design Pattern solves this problem by ensuring that only one instance of a class exists throughout the application's lifetime while providing a single, globally accessible point to that instance.
The Singleton Pattern is one of the Creational Design Patterns introduced by the Gang of Four (GoF) and is widely used in enterprise applications, including those built with C# and ASP.NET Core.
What is the Singleton Pattern?
Definition
The Singleton Pattern ensures that:
Only one instance of a class can exist.
A global access point is provided to that instance.
The instance is created in a controlled manner.
In other words, regardless of how many times your application requests the object, it always receives the same instance.
Why Do We Need the Singleton Pattern?
Imagine a logging service used throughout an application.
Without Singleton:
Every class creates its own logger.
Multiple log files or streams may be opened.
Memory usage increases.
Configuration may become inconsistent.
With Singleton:
One shared logger handles all logging requests.
Consistent behavior is maintained.
Resource usage is minimized.
Problem Statement
Suppose you have a configuration manager.
public class ConfigurationManager
{
}
Now imagine different parts of the application create their own instances:
var config1 = new ConfigurationManager();
var config2 = new ConfigurationManager();
var config3 = new ConfigurationManager();
This creates three independent objects, which may each load the same configuration and consume additional resources unnecessarily.
The Singleton Pattern ensures there is only one shared instance.
Real-World Analogy
Imagine an office with one printer.
Hundreds of employees can send print jobs, but they all use the same physical printer.
The printer represents the Singleton instance:
One printer
Many users
Shared access
Another example is the President of a company. There is only one CEO at a time, and everyone interacts with that same person rather than creating new CEOs.
Key Characteristics
Only one object exists.
Constructor is private.
Object creation is controlled internally.
Global access point is provided.
Prevents accidental instantiation.
UML Class Diagram
+----------------------+
| Singleton |
+----------------------+
| - instance |
+----------------------+
| - Singleton() |
| + Instance |
| + DoWork() |
+----------------------+
Explanation:
The constructor is private.
The static
Instanceproperty returns the single object.Clients access the object through
Instance.
Basic Singleton Implementation
public sealed class Logger
{
private static Logger _instance;
private Logger()
{
}
public static Logger Instance
{
get
{
if (_instance == null)
{
_instance = new Logger();
}
return _instance;
}
}
public void Log(string message)
{
Console.WriteLine($"[LOG] {message}");
}
}
Usage
class Program
{
static void Main()
{
Logger logger1 = Logger.Instance;
Logger logger2 = Logger.Instance;
logger1.Log("Application Started");
Console.WriteLine(object.ReferenceEquals(logger1, logger2));
}
}
Output
[LOG] Application Started
True
True confirms that both variables reference the same object.
Why Use sealed?
The sealed keyword prevents other classes from inheriting the Singleton class.
public sealed class Logger
{
}
This avoids scenarios where inheritance could accidentally allow multiple instances or alter the Singleton's intended behavior.
Thread-Safe Singleton
The basic implementation is not thread-safe. If two threads access Instance simultaneously, they could each create a new object.
A common solution is to use a lock.
public sealed class Logger
{
private static Logger _instance;
private static readonly object _lock = new object();
private Logger()
{
}
public static Logger Instance
{
get
{
lock (_lock)
{
if (_instance == null)
{
_instance = new Logger();
}
return _instance;
}
}
}
}
This guarantees that only one thread creates the instance.
Lazy Singleton
The .NET Framework provides the Lazy<T> class, which simplifies thread-safe lazy initialization.
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($"[LOG] {message}");
}
}
Why prefer Lazy<T>?
Thread-safe by default
Cleaner implementation
Object created only when first accessed
Recommended for most new applications
Singleton in ASP.NET Core
ASP.NET Core has built-in support for singleton services through Dependency Injection.
Service
public interface ILoggerService
{
void Log(string message);
}
public class LoggerService : ILoggerService
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
Register in Program.cs
builder.Services.AddSingleton<ILoggerService, LoggerService>();
Inject into a Controller
public class HomeController : Controller
{
private readonly ILoggerService _logger;
public HomeController(ILoggerService logger)
{
_logger = logger;
}
public IActionResult Index()
{
_logger.Log("Home page requested.");
return View();
}
}
The Dependency Injection container creates one instance of LoggerService and shares it across the application's lifetime.
Note: In ASP.NET Core, prefer using the built-in Dependency Injection container (
AddSingleton) instead of implementing your own Singleton class unless you have a specific reason.
Real-World Use Cases
The Singleton Pattern is suitable for services that represent shared application-wide resources.
| Use Case | Why Singleton Fits |
|---|---|
| Logging Service | A single logging pipeline simplifies management. |
| Application Configuration | One shared configuration source avoids duplication. |
| In-Memory Cache | Centralized cache improves consistency. |
| License Manager | One authority validates application licensing. |
| Printer Spooler | Coordinates access to a shared printer. |
| Feature Flag Service | Maintains a consistent set of feature toggles. |
Advantages
Ensures a single shared instance.
Reduces unnecessary object creation.
Provides a centralized access point.
Conserves memory and resources.
Simplifies management of shared services.
Works well with Dependency Injection.
Disadvantages
Introduces global state, which can make reasoning about the application harder.
Can complicate unit testing if overused.
May hide dependencies if accessed directly instead of through Dependency Injection.
Incorrect implementations may not be thread-safe.
Overuse can lead to tightly coupled code.
Best Practices
Prefer
Lazy<T>for manual Singleton implementations.Use
sealedto prevent inheritance.Keep Singleton classes focused on one responsibility.
Avoid storing mutable global state unless necessary.
In ASP.NET Core, use
AddSingletonthrough the DI container whenever possible.Inject Singleton services into consumers rather than calling static
Instanceproperties throughout the codebase.
Common Mistakes
Public Constructor
public Logger()
{
}
This allows anyone to create additional instances.
Correct approach: Make the constructor private.
Ignoring Thread Safety
In multi-threaded applications, a non-thread-safe Singleton can produce multiple instances.
Correct approach: Use Lazy<T> or proper synchronization.
Using Singleton Everywhere
Not every service should be a Singleton.
For example, classes that maintain request-specific or user-specific state are usually better suited to scoped or transient lifetimes in ASP.NET Core.
Singleton vs Static Class
| Feature | Singleton | Static Class |
|---|---|---|
| Instance | One object | No object |
| Implements Interfaces | ✔ Yes | ✖ No |
| Dependency Injection | ✔ Yes | ✖ No |
| Inheritance | ✔ Can implement interfaces (though the class itself is often sealed) | ✖ Not supported |
| Can Hold State | ✔ Yes | ✔ Yes (static state) |
| Lazy Initialization | ✔ Yes | Limited (type initialization semantics) |
| Supports Polymorphism | ✔ Yes | ✖ No |
When to choose which?
Use a Singleton when you need an object with identity that participates in Dependency Injection and can implement interfaces.
Use a static class for stateless utility methods, such as mathematical helpers or string manipulation functions.
Frequently Asked Interview Questions
1. What is the Singleton Design Pattern?
It is a creational design pattern that ensures only one instance of a class exists and provides a global access point to that instance.
2. Why is the constructor private?
A private constructor prevents external code from creating additional instances using the new keyword.
3. Why should a Singleton class often be sealed?
To prevent inheritance from changing or bypassing the Singleton behavior.
4. Is the basic Singleton implementation thread-safe?
No. Concurrent access can result in multiple instances being created.
5. What is the recommended Singleton implementation in modern C#?
Using Lazy<T> for manual implementations, or leveraging the ASP.NET Core Dependency Injection container with AddSingleton.
6. Can Singleton work with Dependency Injection?
Yes. In fact, ASP.NET Core encourages registering shared services with AddSingleton instead of implementing the pattern manually in many cases.
7. What are some real-world examples of Singleton?
Logging services
Configuration providers
In-memory caches
Printer spoolers
Feature flag managers
Summary
The Singleton Design Pattern is one of the simplest yet most widely used creational patterns. It ensures that only one instance of a class exists and provides controlled global access to that instance. While manual implementations are useful for understanding the concept, modern ASP.NET Core applications typically rely on the built-in Dependency Injection container to manage singleton lifetimes.
Used appropriately, Singleton can improve resource management and maintain consistent application-wide behavior. However, it should be applied thoughtfully, avoiding unnecessary global state and ensuring thread safety where required.
What's Next?
In Part 2.2, we'll explore the Factory Method Design Pattern, where you'll learn:
Why direct object creation with
newcan become a maintenance problemHow the Factory Method Pattern decouples object creation from object usage
UML Class Diagram
Complete C# examples
ASP.NET Core implementation
Real-world scenarios
Advantages and disadvantages
Factory Method vs. Simple Factory
Common interview questions
This will build on the Singleton concepts and further strengthen your understanding of creational design patterns in modern .NET development.
No comments:
Post a Comment