Table of Contents
- Course Overview
- Introduction to the Singleton pattern
- Structure and characteristics of the Singleton
- Naive implementation — v1 (Naive Singleton)
1. Course Overview
This course covers the Singleton pattern, one of the most well-known but also most misused design patterns. Steve Smith (@ardalis) is an experienced .NET developer, architect and trainer. Its corporate workshops and Pluralsight courses have helped thousands of developers and teams write better code, faster.
Design patterns are like individual tools that one can add to their toolbox as a software developer. They don’t take long to introduce, but can require a lot of practice to master.
Main topics covered
- What problem is the Singleton pattern supposed to solve?
- What are the different ways to implement the pattern (good and bad)?
- Why is the Singleton pattern often called an anti-pattern?
- What are other ways to achieve the same behavior without using the Singleton pattern itself?
By the end of this course, you will be able to recognize situations where the Singleton pattern makes sense, and apply it—or an alternative—with confidence.
2. Introduction to the Singleton pattern
A Singleton is a class designed to allow the creation of only a single instance of itself. The class itself is responsible for enforcing this design constraint.
Typical use cases
Single instances of classes are often needed to model some sort of shared resource, such as:
- The file system — a single file manager for the entire application
- A shared network resource — a scanner, a print spooler
- Cost-to-create resources — when the creation cost is so high that it is best to incur it only once
Sometimes, even though multiple instances would be logically acceptable, the cost of creating the instance is so high that it is better to incur it only once, for performance reasons.
Lazy Instantiation
For performance reasons, it is often desirable to only incur the creation cost when the instance is requested for the first time. This is called lazy instantiation or lazy creation. Most implementations of the pattern assume this behavior by default.
It is also possible to simply create the instance necessary to start the application, then use this instance for the entire life of the application.
3. Structure and characteristics of the Singleton
In terms of structure, the Singleton is relatively simple. This is a single class with:
- A private instance (
private static) - A public static method or property that is the only way to access this instance
- A private constructor (without parameters)
+---------------------------+
| Singleton |
+---------------------------+
| - _instance: Singleton | ← champ privé statique
+---------------------------+
| - Singleton() | ← constructeur privé
| + Instance: Singleton | ← propriété statique publique
+---------------------------+
Structural rules
| Rule | Reason |
|---|---|
Class marked sealed | Prevents inheritance and optimizes JIT compiler |
private constructor without parameters | Prevent instantiation from outside |
private static field for the instance | Only place where reference exists |
public static property or method | Single point of access to the instance |
Important Notes
- No parameters: Singleton classes are created without parameters. If you need similar variations based on different parameters, look at Factory patterns instead.
sealed: To enforce intent and help optimize the JIT compiler, Singleton classes should be markedsealed. This also prevents the creation of subclasses.- 0 or 1 instance: At any time in the life of an application, a Singleton class will have either 0 (before first use) or 1 instance.
4. Naive implementation — v1 (Naive Singleton)
4.1 Explanation
The typical implementation of the Singleton pattern looks like this. We start with a sealed class. We add a private constructor without parameters, making it impossible to instantiate the class outside of itself. Next, we create a private static field to contain the single instance. Finally, we add a static property to control access to this private field.
To ensure lazy instantiation, we only create the instance on the first request, at which point it will be null.
With nullable reference types (C# 8+): We can enable nullable at the project or class level, mark the private static instance as nullable (?), then replace the if check with the null-coalescent assignment operator (??=). This operator only evaluates the right side if the left side is null, which is basically the same as if checking and assignment, but more concisely.
Thread safety problem: This implementation is not thread-safe. If two threads call Instance simultaneously while _instance is still null, they could both create a new instance. This is why a parallel test in the demos can demonstrate that the constructor is called more than once.
4.2 C# Code — v1
namespace DesignPatternsInCSharp.Singleton.v1
{
// Bad code (not thread-safe)
#nullable enable
public sealed class Singleton
{
private static Singleton? _instance;
public static Singleton Instance
{
get
{
Logger.Log("Instance called.");
return _instance ??= new Singleton();
}
}
private Singleton()
{
// cannot be created except within this class
Logger.Log("Constructor invoked.");
}
}
}
Note: The
??=(null-coalescing assignment) operator is available since C# 8. It assigns the right value only if the left variable isnull. This approach is equivalent to:if (_instance == null) _instance = new Singleton(); return _instance;
4.3 Unit testing — v1
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;
[assembly: CollectionBehavior(CollectionBehavior.CollectionPerClass, DisableTestParallelization = true)]
namespace DesignPatternsInCSharp.Singleton.v1
{
public class SingletonInstance
{
private readonly ITestOutputHelper _output;
public SingletonInstance(ITestOutputHelper output)
{
_output = output;
SingletonTestHelpers.Reset(typeof(Singleton));
Logger.Clear();
}
[Fact]
public void ReturnsNonNullSingletonInstance()
{
Assert.Null(SingletonTestHelpers.GetPrivateStaticInstance<Singleton>());
var result = Singleton.Instance;
Assert.NotNull(result);
Assert.IsType<Singleton>(result);
Logger.Output().ToList().ForEach(h => _output.WriteLine(h));
}
[Fact]
public void OnlyCallsConstructorOnceGivenThreeInstanceCalls()
{
Assert.Null(SingletonTestHelpers.GetPrivateStaticInstance<Singleton>());
var one = Singleton.Instance;
var two = Singleton.Instance;
var three = Singleton.Instance;
var log = Logger.Output();
Assert.Equal(1, log.Count(log => log.Contains("Constructor")));
Assert.Equal(3, log.Count(log => log.Contains("Instance")));
Logger.Output().ToList().ForEach(h => _output.WriteLine(h));
}
[Fact]
public void CallsConstructorMultipleTimesGivenThreeParallelInstanceCalls()
{
Assert.Null(SingletonTestHelpers.GetPrivateStaticInstance<Singleton>());
// configure logger to slow down the creation long enough to cause problems
Logger.DelayMilliseconds = 50;
var strings = new List<string>() { "one", "two", "three" };
var instances = new List<Singleton>();
var options = new ParallelOptions() { MaxDegreeOfParallelism = 3 };
Parallel.ForEach(strings, options, instance =>
{
instances.Add(Singleton.Instance);
});
var log = Logger.Output();
try
{
// Demonstrates the bug: constructor may be called more than once!
Assert.True(log.Count(log => log.Contains("Constructor")) > 1);
Assert.Equal(3, log.Count(log => log.Contains("Instance")));
}
finally
{
Logger.Output().ToList().ForEach(h => _output.WriteLine(h));
}
}
}
}
What these tests demonstrate:
ReturnsNonNullSingletonInstance: Checks thatInstancereturns a non-null instance.OnlyCallsConstructorOnceGivenThreeInstanceCalls: Verifies that the constructor is only called once even ifInstanceis called three times sequentially.CallsConstructorMultipleTimesGivenThreeParallelInstanceCalls: Demonstrates the bug: in parallel conditions with an artificial delay of 50ms, the constructor can be called multiple times, proving that the naive implementation is not thread safe.
5. Implementation with lock — v2 (Thread Safe Singleton with lock)
5.1 Explanation
To get around the lack of thread safety, the simplest and most obvious approach is to add a lock. We create a new object instance to use as a padlock variable. We lock on this padlock instance before performing the check to see if our Singleton instance is null.
This ensures that multiple threads will have to synchronize and enter this code block one by one, ensuring that two threads never create the instance at the same time.
Disadvantage: This approach works, but it has negative performance impacts because each use of the Instance property will incur the overhead of this lock. The lock is only needed during instance creation, which should only happen once in the entire life of the application — yet we pay this cost for the entire life of the application.
Important rule about locks: Always use a dedicated private instance variable as a lock object, never a shared public or static value that could be used by another lock elsewhere in the application. Lock instances must be matched one-to-one with their
lockinstructions.
5.2 C# Code — v2
namespace DesignPatternsInCSharp.Singleton.v2
{
// Bad code (lock on every access)
// Source: https://csharpindepth.com/articles/singleton
public sealed class Singleton
{
private static Singleton _instance = null;
private static readonly object padlock = new object();
public static Singleton Instance
{
get
{
Logger.Log("Instance called.");
lock (padlock) // this lock is used on *every* reference to Singleton
{
if (_instance == null)
{
_instance = new Singleton();
}
return _instance;
}
}
}
private Singleton()
{
// cannot be created except within this class
Logger.Log("Constructor invoked.");
}
}
}
5.3 Unit testing — v2
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;
namespace DesignPatternsInCSharp.Singleton.v2
{
public class SingletonInstance
{
private readonly ITestOutputHelper _output;
public SingletonInstance(ITestOutputHelper output)
{
_output = output;
SingletonTestHelpers.Reset(typeof(Singleton));
Logger.Clear();
}
[Fact]
public void OnlyCallsConstructorOnceGivenThreeInstanceCalls()
{
Assert.Null(SingletonTestHelpers.GetPrivateStaticInstance<Singleton>());
Logger.DelayMilliseconds = 10;
var result1 = Singleton.Instance;
Thread.Sleep(1);
var result2 = Singleton.Instance;
Thread.Sleep(1);
var result3 = Singleton.Instance;
var log = Logger.Output();
Assert.Equal(1, log.Count(log => log.Contains("Constructor")));
Assert.Equal(3, log.Count(log => log.Contains("Instance")));
Logger.Output().ToList().ForEach(h => _output.WriteLine(h));
}
[Fact]
public void CallsConstructorMultipleTimesGivenThreeParallelInstanceCalls()
{
Assert.Null(SingletonTestHelpers.GetPrivateStaticInstance<Singleton>());
Logger.DelayMilliseconds = 50;
var strings = new List<string>() { "one", "two", "three" };
var instances = new List<Singleton>();
var options = new ParallelOptions() { MaxDegreeOfParallelism = 3 };
Parallel.ForEach(strings, options, instance =>
{
instances.Add(Singleton.Instance);
});
var log = Logger.Output();
try
{
// Now with the lock, the constructor is only called once even in parallel
Assert.Equal(1, log.Count(log => log.Contains("Constructor")));
Assert.Equal(3, log.Count(log => log.Contains("Instance")));
}
finally
{
Logger.Output().ToList().ForEach(h => _output.WriteLine(h));
}
}
}
}
Test result: Although Instance is called three times in parallel, the constructor is only invoked once thanks to the lock.
6. Double Checked Locking — v3 (Best Lock)
6.1 Explanation
Double-Checked Locking is an improvement on the single-checked locking approach. Instead of locking every time the Instance property is accessed, we first check if the instance is null before acquiring the lock. We only lock if necessary, then check again after acquiring the lock.
Operation:
- First check (
if _instance == null) — without lock, to avoid unnecessary overhead - If
null, acquisition of the lock (lock) - Second check (
if _instance == null) — with lock, to guarantee thread safety - Creating the instance only if it is still
null
Advantage: The lock is only acquired when the instance is null, which only happens once in the life of the application.
Disadvantages:
- More complex code, easier to get wrong
- Does not necessarily work with the ECMA Common Language Interface (CLI) specification. Jon Skeet describes this concern in detail on his website (csharpindepth.com/articles/singleton).
6.2 C# Code — v3
namespace DesignPatternsInCSharp.Singleton.v3
{
// Bad code (double-checked locking has issues)
// Source: https://csharpindepth.com/articles/singleton
public sealed class Singleton
{
private static Singleton _instance = null;
private static readonly object padlock = new object();
public static Singleton Instance
{
get
{
Logger.Log("Instance called.");
if (_instance == null) // only get a lock if the instance is null
{
lock (padlock)
{
if (_instance == null)
{
_instance = new Singleton();
}
}
}
return _instance;
}
}
private Singleton()
{
// cannot be created except within this class
Logger.Log("Constructor invoked.");
}
}
}
6.3 Unit testing — v3
The v3 tests are structurally identical to those of v2. This time, the parallel test also verifies that the constructor is only called once, even under parallel load with delay.
[Fact]
public void CallsConstructorMultipleTimesGivenThreeParallelInstanceCalls()
{
Assert.Null(SingletonTestHelpers.GetPrivateStaticInstance<Singleton>());
Logger.DelayMilliseconds = 50;
var strings = new List<string>() { "one", "two", "three" };
var instances = new List<Singleton>();
var options = new ParallelOptions() { MaxDegreeOfParallelism = 3 };
Parallel.ForEach(strings, options, instance =>
{
instances.Add(Singleton.Instance);
});
var log = Logger.Output();
try
{
Assert.Equal(1, log.Count(log => log.Contains("Constructor")));
Assert.Equal(3, log.Count(log => log.Contains("Instance")));
}
finally
{
Logger.Output().ToList().ForEach(h => _output.WriteLine(h));
}
}
7. Analysis of lock approaches
| Release | Approach | Thread-Safe | Performance | Complexity |
|---|---|---|---|---|
| v1 | Naïve (no lock) | ❌ No | ✅ Excellent | ✅ Simple |
| v2 | Systematic lock | ✅ Yes | ❌ Lock on each access | ✅ Simple |
| v3 | Double-Checked Locking | ⚠️ Partially | ✅ Good | ❌ Complex |
Conclusions:
- The v1 (naive) approach is simple but not thread-safe.
- v2 works, but the lock applies to every access to the
Instanceproperty, which is useless once the instance is created. We pay this cost for the entire life of the application even though it is only necessary once. - v3 is better because it uses Double-Checked Locking. However, it is complex, easy to misimplement, and does not necessarily work correctly with the ECMA CLI specification.
- Ultimately, none of these lock approaches work as well as the following ones we’re going to see.
8. Static and Singleton Constructors
8.1 The beforefieldinit mechanism
Another approach that does not involve locks is to exploit the C# functionality of static type constructors. C# static constructors run only once per application domain (AppDomain), making them a good tool for implementing Singleton behavior.
They are called the first time a static member of the type is referenced, which provides some degree of lazy instantiation — although not perfect, because any reference to a static member, not necessarily our Singleton reference, will trigger the constructor to execute.
beforefieldinit is a flag that the compiler uses to indicate that static initializers can be called earlier. This is the default behavior if the type does not have an explicit static constructor.
Key rule: Make sure to use an explicit static constructor (even empty) to avoid problems with the C# compiler and
beforefieldinit. Adding an explicit static constructor preventsbeforefieldinitfrom being applied, making our Singleton behavior lazier.
9. Singleton with static initialization — v4 (Less Lazy)
9.1 Explanation
In this version, we use a static field initializer to instantiate the Singleton directly where the static field is declared (private static readonly Singleton _instance = new Singleton()). We then add an empty static constructor to prevent the class from being marked beforefieldinit.
Problem with this approach: If there are other fields or static members on the class (like public static readonly string GREETING = "Hi!"), any reference to these members in the application will also cause the Singleton to be instantiated. Instantiation is therefore not as lazy as desirable.
9.2 C# Code — v4
namespace DesignPatternsInCSharp.Singleton.v4
{
// Source: https://csharpindepth.com/articles/singleton
public sealed class Singleton
{
private static readonly Singleton _instance = new Singleton();
// reading this will ALSO initialize the _instance (unwanted side effect!)
public static readonly string GREETING = "Hi!";
// Tell C# compiler not to mark type as beforefieldinit
// (https://csharpindepth.com/articles/BeforeFieldInit)
static Singleton()
{
}
public static Singleton Instance
{
get
{
Logger.Log("Instance called.");
return _instance;
}
}
private Singleton()
{
// cannot be created except within this class
Logger.Log("Constructor invoked.");
}
}
}
Observed behavior: If any other part of the code accesses Singleton.GREETING, the Singleton will be instantiated as a side effect, which is undesirable in a truly lazy approach.
10. Singleton with nested class — v5 (Nested Lazy)
10.1 Explanation
This solution fixes the v4 issue with a bit of clever code. It comes from an article published by Jon Skeet on his site. The idea is to create a nested private class (Nested) which holds the _instance field. The outer Singleton class accesses the instance through this nested class.
Why it works:
- The
Nestedclass is only initialized when first accessed — that is, only whenSingleton.Instanceis requested. - The
Nestedclass has no other static members, so it will not be initialized accidentally if other static members of the externalSingletonclass are accessed (likeGREETING). - Empty static constructor of
Nestedpreventsbeforefieldinitfrom being applied on this nested class.
Limitation: Unfortunately, we cannot make the _instance field of the Nested class accessible only from the enclosing class Singleton — we must mark it internal rather than private.
10.2 C# Code — v5
namespace DesignPatternsInCSharp.Singleton.v5
{
// Source: https://csharpindepth.com/articles/singleton
public sealed class Singleton
{
// reading this will initialize the Singleton.GREETING static field only,
// NOT the nested class — true lazy initialization for Instance
public static readonly string GREETING = "Hi!";
public static Singleton Instance
{
get
{
Logger.Log("Instance called.");
return Nested._instance;
}
}
private class Nested
{
// Tell C# compiler not to mark type as beforefieldinit
// (https://csharpindepth.com/articles/BeforeFieldInit)
static Nested()
{
}
internal static readonly Singleton _instance = new Singleton();
}
private Singleton()
{
// cannot be created except within this class
Logger.Log("Constructor invoked.");
}
}
}
Behavior: Accessing Singleton.GREETING no longer instantiates the Singleton. The instance is only created when Singleton.Instance is first accessed.
11. Analysis of approaches by static constructor
| Release | Approach | Thread-Safe | Performance | Lazy | Complexity |
|---|---|---|---|---|---|
| v4 | Static initializer | ✅ Yes | ✅ Excellent | ⚠️ Partial | ✅ Simple |
| v5 | Nested class | ✅ Yes | ✅ Excellent | ✅ True | ❌ Complex |
Conclusions:
- Both approaches are thread-safe without the need for locks — excellent performance characteristics.
- v4 is simple but not entirely lazy.
- The best solution proposed so far, the nested class approach (v5), is quite complex and non-intuitive. This is typically not something one would naturally turn to as a first option.
12. Singleton with Lazy<T> — v6 (Recommended approach)
12.1 Explanation
Lazy<T> is available since .NET 4 (2010). This is framework support for lazy initialization. When we create a Lazy<T> type, we specify the type and a function that returns an instance of this type.
Advantages:
- Inherently thread-safe
- Handling
nullchecking automatically supported - Clear and intuitive syntax
- No need for manual locks
- Available in all modern versions of .NET
Operation:
- Static readonly private field is of type
Lazy<Singleton>rather than justSingleton - This field is initialized at construction time to create a new
Lazy<T>instance, and a lambda function is passed into theLazy<T>constructor with the logic needed to create the Singleton instance - In the static property
Instance, instead of directly returning the_instancefield, we return the.Valueproperty of the_lazyfield - The
.Valueproperty ensures that it will never benulland there will only be one instance, because it comes from a static field
12.2 C# Code — v6
using System;
namespace DesignPatternsInCSharp.Singleton.v6
{
// Source: https://csharpindepth.com/articles/singleton
public sealed class Singleton
{
// reading this will initialize the instance
public static readonly Lazy<Singleton> _lazy = new Lazy<Singleton>(() => new Singleton());
public static Singleton Instance
{
get
{
Logger.Log("Instance called.");
return _lazy.Value;
}
}
private Singleton()
{
// cannot be created except within this class
Logger.Log("Constructor invoked.");
}
}
}
Why this approach is recommended: Compared to other options explored, it provides a very easy to understand representation of the pattern, and if you want to implement the Singleton pattern in your class itself, this approach is widely supported and has all the desirable performance and thread safety features.
12.3 Unit testing — v6
using System.Linq;
using System.Threading;
using Xunit;
using Xunit.Abstractions;
namespace DesignPatternsInCSharp.Singleton.v6
{
public class SingletonInstance
{
private readonly ITestOutputHelper _output;
public SingletonInstance(ITestOutputHelper output)
{
_output = output;
// This doesn't work with a static readonly field
// SingletonTestHelpers.Reset(typeof(Singleton));
Logger.Clear();
}
[Fact]
public void ReturnsNonNullSingletonInstance()
{
// This no longer works with Lazy<T>
// Assert.Null(SingletonTestHelpers.GetPrivateStaticInstance<Singleton>());
var result = Singleton.Instance;
Assert.NotNull(result);
Assert.IsType<Singleton>(result);
Logger.Output().ToList().ForEach(h => _output.WriteLine(h));
}
[Fact]
public void OnlyCallsConstructorOnceGivenThreeInstanceCalls()
{
Logger.DelayMilliseconds = 10;
var result1 = Singleton.Instance;
Thread.Sleep(1);
var result2 = Singleton.Instance;
Thread.Sleep(1);
var result3 = Singleton.Instance;
var log = Logger.Output();
// we can't check this since it depends on if this test is run alone or after others
// Assert.Equal(1, log.Count(log => log.Contains("Constructor")));
Assert.Equal(3, log.Count(log => log.Contains("Instance")));
Logger.Output().ToList().ForEach(h => _output.WriteLine(h));
}
}
}
Important note: With the
Lazy<T>approach, it is no longer possible to reset the instance viaSingletonTestHelpers.Reset()because the field isstatic readonly. Tests must be adapted accordingly.
13. Is the Singleton an anti pattern?
Although the Singleton appears in the original Gang of Four book (Design Patterns, 1994), many consider the Singleton pattern to be an anti-pattern. Here’s why:
Identified issues
| Problem | Explanation |
|---|---|
| Strong coupling | Using direct and static in code (instead of passing an interface) creates strong coupling and difficult-to-test code |
| SRP Violation | Classes that implement the pattern violate the Single Responsibility Principle because they are responsible for managing their lifetime in addition to their actual work |
| DRY Violation | If we implement multiple Singletons, we must duplicate all the logic necessary to enforce the Singleton behavior, violating the Don’t Repeat Yourself |
| Difficult to test | Direct static access creates the so-called “Static Cling” code smell, making unit testing very difficult |
| OCP Violation | Difficult to extend without modifying existing code |
Useful references
- Steve Smith’s course on SOLID Principles of C# (Pluralsight)
- Steve Smith’s course on Refactoring for C# (Pluralsight) — covers the “Static Cling” code smell
14. Singleton vs. Static classes
In C#, a class can be declared static. The compiler then ensures that the class definition contains only static members. Static classes and Singletons are often confused, but there are important differences:
| Characteristic | Singleton | Static class |
|---|---|---|
| Type | Actual instance of a class | Collection of static methods |
| Interfaces | ✅ Can implement interfaces | ❌ Cannot implement interfaces |
| Passing as an argument | ✅ Can be passed as an argument to methods | ❌ Cannot be passed as an argument |
| Dependency injection | ✅ Supports DI and Strategy pattern | ❌ Does not support DI |
| Assignment to variables | ✅ Can be assigned to a variable | ❌ Cannot be assigned |
| Polymorphism | ✅ Supports polymorphism | ❌ Purely procedural |
| State | ✅ Can have state | ⚠️ Access to global status only |
| Serialization | ✅ Can be serialized | ❌ No serialization support |
Conclusion: Singletons are real instances of classes, which gives them much greater flexibility than static classes. The first three differences (interfaces, argument passing, DI) mean that Singletons can use polymorphism, which static classes absolutely cannot do.
15. Using IoC/DI Containers
15.1 Lifetimes in .NET
IoC/DI (Inversion of Control / Dependency Injection) containers are advanced factories that many modern frameworks use to enable widespread use of techniques like dependency injection. Note: they have nothing to do with Docker containers.
.NET Core has built-in support for these IoC/DI containers. You can configure your own or use the built-in IServiceCollection.
In applications that use containers and dependency injection, classes typically request their dependencies in their constructor. Ideally, all of these classes follow the explicit dependencies principle: a class should have no hidden dependencies. Hidden dependencies are only revealed at runtime, while explicit dependencies are exposed by the class constructor.
When we configure a container, we establish a correspondence between abstractions and their implementations for the application. For example :
// Mapping abstraction -> implémentation
services.AddSingleton<IEmailService, LocalSMTPEmailService>();
// Plus tard, on peut facilement changer pour un fournisseur cloud
// services.AddSingleton<IEmailService, CloudEmailService>();
This change can only be made in the container mapping logic without affecting any other application code.
.NET has three main lifetimes for types registered in the container:
| Lifespan | Registration method | Behavior |
|---|---|---|
| Transient | services.AddTransient<T>() | New instance on each request |
| Scoped | services.AddScoped<T>() | One instance per scope (e.g. per HTTP request) |
| Singleton | services.AddSingleton<T>() | A single instance for the entire life of the application |
Using AddSingleton allows you to obtain Singleton behavior without the class itself needing to implement the Singleton pattern! Responsibility for shelf life management is delegated to the container.
15.2 Example with GreeterService
To illustrate how to configure a DI container in a .NET application that is not a web application, the course uses the CleanArchitecture.WorkerService template available on GitHub. This template is for building .NET Worker Service applications that use clean architecture, and it already includes dependency injection and configured tests.
.NET provides a generic HostBuilder that includes features like configuration and log support. It also includes support for configuring the built-in DI container that ships with .NET Core.
// Program.cs - GreeterService
using Microsoft.Extensions.Hosting;
namespace GreeterService
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
// Startup.cs - Configuration du conteneur DI
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace GreeterService
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc();
// Exemple d'enregistrement en Singleton via le conteneur :
// services.AddSingleton<IMyService, MyService>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<GreeterService>();
});
}
}
}
16. Testing Considerations
16.1 Test Utilities
Singleton tests use two specialized helpers that access via reflection the static private instance field used in most Singleton implementations. This is necessary because the xUnit testing framework does not provide a way to completely reset the application domain.
Note: AppDomains aren’t really a thing in .NET Core — the equivalent is the environment in which the tests run. The alternative would be to isolate each test into its own test project, but that would be too cumbersome.
using System;
using System.Reflection;
namespace DesignPatternsInCSharp.Singleton
{
public static class SingletonTestHelpers
{
/// <summary>
/// Réinitialise l'instance statique privée d'un Singleton à null via réflexion.
/// Utile pour remettre le Singleton dans son état initial entre les tests.
/// </summary>
public static void Reset(Type type)
{
FieldInfo info = type.GetField("_instance", BindingFlags.NonPublic | BindingFlags.Static);
info.SetValue(null, null);
}
/// <summary>
/// Récupère la valeur actuelle du champ d'instance statique privée d'un Singleton via réflexion.
/// Utile pour vérifier que l'instance est null avant chaque test.
/// </summary>
public static T GetPrivateStaticInstance<T>() where T : class
{
Type type = typeof(T);
FieldInfo info = type.GetField("_instance", BindingFlags.NonPublic | BindingFlags.Static);
return info.GetValue(null) as T;
}
}
}
Usage in tests:
// Avant chaque test : réinitialiser le Singleton et nettoyer les logs
SingletonTestHelpers.Reset(typeof(Singleton));
Logger.Clear();
// Au début du test : vérifier que l'instance est bien null
Assert.Null(SingletonTestHelpers.GetPrivateStaticInstance<Singleton>());
Limitation: These helpers do not work with implementations using
static readonly(v4, v5, v6) because areadonlyfield cannot be reassigned by reflection after initialization.
16.2 Thread-safe logger for testing
The Logger used in the demos is designed to be thread-safe through the use of a ConcurrentQueue<string>. This decision eliminates the race condition issues that were present with a simple List<string>.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
namespace DesignPatternsInCSharp.Singleton
{
public static class Logger
{
private static ConcurrentQueue<string> _log = new ConcurrentQueue<string>();
/// <summary>
/// Délai optionnel pour forcer des conditions de course dans les tests
/// </summary>
public static int DelayMilliseconds { get; set; } = 0;
public static void Log(string message)
{
System.Threading.Thread.Sleep(DelayMilliseconds);
_log.Enqueue(message);
}
public static void Clear()
{
_log.Clear();
}
public static string StringDump()
{
return string.Join(Environment.NewLine, _log);
}
public static List<string> Output()
{
return _log.ToList();
}
}
}
Logger key features:
ConcurrentQueue<string>: guarantees thread safety for threading/unthreading operationsDelayMilliseconds: allows you to introduce an artificial delay to force race conditions in the tests (demonstrate thread safety bugs)Clear(): reset the log between tests
16.3 Testability Best Practices
The code SingletonTesting.cs illustrates the different approaches to using a Singleton in testable code:
namespace DesignPatternsInCSharp.Singleton
{
public class SingletonTesting
{
private readonly v6.Singleton _singleton;
private readonly IServiceThatSingletonImplements _singleton2;
// ✅ Bon : Injection via constructeur (Dependency Injection)
public SingletonTesting(v6.Singleton singleton)
{
_singleton = singleton;
}
// ✅ Encore mieux : Injection via interface
public SingletonTesting(IServiceThatSingletonImplements singleton)
{
_singleton2 = singleton;
}
// ✅ Acceptable : Passage en argument de méthode
public void DoSomething1(v6.Singleton singleton)
{
// do something with the singleton instance
}
// ✅ Encore mieux avec une interface
public void DoSomething1(IServiceThatSingletonImplements singleton)
{
// do something with the singleton instance
}
// ❌ Mauvais : Accès statique direct (Static Cling)
public void DoSomething2()
{
// Do some logic
// THIS IS THE PROBLEM - Static Cling!
// This makes it very difficult to unit test this method
// v6.Singleton.Instance.SaveToDatabase(data);
// Do some other logic
}
}
public interface IServiceThatSingletonImplements
{ }
}
Golden rule for testability:
| Approach | Testability | Recommendation |
|---|---|---|
Direct static access (Singleton.Instance.X) | ❌ Very difficult | Avoid — code smell “Static Cling” |
| Injection via constructor (concrete type) | ✅ Good | Acceptable |
| Injection via constructor (interface) | ✅✅ Excellent | Recommended approach |
| Passing method argument (concrete type) | ✅ Good | Acceptable |
| Passing method argument (interface) | ✅✅ Excellent | Recommended approach |
Best approach: use dependency injection. So the class that uses the Singleton has no idea that it is using a Singleton and simply uses its local reference of the interface. This makes unit testing easier because one can substitute a mock or fake implementation for testing purposes.
17. Key Takeaways
-
A Singleton is designed to have only one instance created. The pattern revolves around the class itself being responsible for enforcing this behavior.
-
It is easy to misimplement this pattern. Naive approaches (v1) have thread safety issues, and lock approaches (v2, v3) have performance or complexity issues.
-
Lazy<T>is the recommended approach if you want to implement the pattern with the responsibility on the class itself. It is thread-safe, lazy, efficient and easy to understand. -
There are many differences between Singletons and static classes. The most important: Singletons are actual instances of classes, which allows them to implement interfaces, to be injected, to use polymorphism — none of this is possible with static classes.
-
If your framework supports dependency injection and IoC containers, these are generally the best place to manage the lifetime of instances of your classes, rather than putting this responsibility in the classes themselves. Use
services.AddSingleton<T>(). -
Direct static access (
Singleton.Instance) creates strong coupling and makes the code difficult to test (“Static Cling” code smell). Prefer injection via interfaces.
Summary of the 6 implementations
| Release | Name | Thread-Safe | Lazy | Performance | Recommended |
|---|---|---|---|---|---|
| v1 | Naive (??=) | ❌ No | ✅ Yes | ✅ Excellent | ❌ No (parallel bugs) |
| v2 | Simple lock | ✅ Yes | ✅ Yes | ❌ Lock on each access | ❌ No |
| v3 | Double-Checked Locking | ⚠️ Partial | ✅ Yes | ✅ Good | ❌ No (complex) |
| v4 | Static initializer | ✅ Yes | ⚠️ Partial | ✅ Excellent | ⚠️ Partial |
| v5 | Nested class | ✅ Yes | ✅ True | ✅ Excellent | ⚠️ Complex |
| v6 | Lazy<T> | ✅ Yes | ✅ True | ✅ Excellent | ✅ Recommended |
18. Resources and references
- GitHub demo repository: DesignPatternsInCSharp — Demos for this course as well as several other design patterns
- Template CleanArchitecture.WorkerService: available on GitHub by @ardalis
- Article by Jon Skeet on Singletons: https://csharpindepth.com/articles/singleton
- Article on
beforefieldinit: https://csharpindepth.com/articles/BeforeFieldInit - Additional courses from Steve Smith on Pluralsight:
- SOLID Principles of C#
- Refactoring for C#
- Other courses on Design Patterns
- Steve Smith online:
- Website: ardalis.com
- Podcast: weeklydevtips.com
- Social networks: @ardalis
- Demos License: MIT — free to use in your own projects
Search Terms
c-sharp · design · patterns · singleton · testing · architecture · c# · .net · development · explanation · lazy · static · unit · lock · analysis · approaches · pattern · references