The Singleton Pattern in C# Today Is Not Your Dad's One!

by Metalama Team on 01 Aug 2024

The Singleton pattern is one of the oldest, but its role and implementation in modern C# have evolved. This article presents two versions of Singleton in C#: the classic “GoF” version and the modern, DI-friendly one. We briefly discuss techniques that ensure that the essence of Singleton classes is respected through the code base, i.e., that instances are not illegally created.

What is a Singleton in C#?

A Singleton is a class whose a single instance can exist in the application.

It’s one of the original design patterns described in the 1994 Design Patterns book by the “Gang of Four”.

Initially, the interpretation of the Singleton process was that a single instance could exist per process.

In modern C#, where dependency injection and unit testing are ubiquitous, the definition of a Singleton has evolved: it now means a class whose a single instance can exist in each application context, and each unit test typically has its own application context, but all share the same process.

When should you use the Singleton pattern?

The Singleton pattern feels so natural that you probably already use it without knowing it’s a pattern.

Ensuring that only a single object instance exists is essential in several cases:

  • When access to an external resource, such as a file, must be synchronized.
  • When shared data must be kept consistent.
  • To optimize access to a resource-intensive functionality, where a Singleton can significantly reduce memory and processing overhead.

As we will see in this article, the Singleton pattern has evolved. Nowadays, most application services managed by the dependency injection container are de facto singletons.

How to implement the Singleton pattern in C#?

The classic implementation of the Singleton pattern involves having a private constructor in the class, preventing it from being instantiated from outside and exposing a single instance through a static member.

A performance counter manager serves as an excellent example for the Singleton pattern, as its metrics must be consistently gathered across the entire application:

public partial class PerformanceCounterManager
{
    private readonly Stopwatch _stopwatch = Stopwatch.StartNew();

    private readonly ConcurrentDictionary<string, int> _counters = new();

    public void IncrementCounter( string name )
        => this._counters.AddOrUpdate( name, 1, ( _, value ) => value + 1 );

    public void PrintAndReset()
    {
        Dictionary<string, int> oldCounters;
        TimeSpan elapsed;

        lock ( this._stopwatch )
        {
            oldCounters = this._counters.RemoveAll();

            elapsed = this._stopwatch.Elapsed;
            this._stopwatch.Restart();
        }

        foreach ( var counter in oldCounters )
        {
            Console.WriteLine(
                $"{counter.Key}: {counter.Value / elapsed.TotalSeconds:f2} calls/s" );
        }
    }
}

Here is the implementation of the Singleton pattern:

public partial class PerformanceCounterManager
{
    private PerformanceCounterManager() { }

    public static PerformanceCounterManager Instance { get; } = new();
}

{}

The full source code of examples in this article is available on .

As you can see, this class has a private constructor that prevents it from being instantiated from outside and a static Instance property that holds the only class instance. Like any static initializer in .NET, it is guaranteed to be executed a single time before any other member of the type is accessed, ensuring a single instance of the type.

Since the single instance is shared across the whole application, you need to make the type thread-safe, as its public members can be accessed from any thread. In the example above, this is done using ConcurrentDictionary and a lock.

{}

The full source code of examples in this article is available on .

Reducing the Singleton boilerplate

Implementing the Singleton pattern requires some boilerplate code, which can be avoided by using either Roslyn source generators or Metalama, a more straightforward code generator for C#.

Regardless of the technique you choose, it’s good practice to mark singletons with a [Singleton] custom attribute. This practice makes it immediately apparent to anyone reading the code,  without parsing the implementation details, that the class follows the Singleton pattern:

[Singleton]
public partial class PerformanceCounterManager

To check the implementation of the [Singleton] attribute, refer to this example in the Metalama documentation. It generates the same code as above and conveniently reports warnings if the type contains accessible constructors.

Is Singleton an anti-pattern?

The main problem with the classic Singleton pattern is that it tightly couples singleton consumers with singleton implementations. Tight coupling means you can’t easily replace the singleton implementation with a different one or have multiple instances of the class.

This can be especially problematic when writing unit tests for two reasons:

  1. You will certainly want to run your tests in parallel, so you must isolate them from each other. In this case, you will often need one instance of some singleton classes for each test. For instance, if I want to test the PerformanceCounterManager class, I will want each test method to have its own instance.
  2. You might want your tests to use a specific implementation of the Singleton. For instance, we might want our PerformanceCounterManager to use AWS in production but an in-memory mock for unit tests.

The incompatibility of the classic Singleton pattern with the whole Dependency Injection paradigm is why Singleton is sometimes considered an anti-pattern, i.e., something to avoid.

These issues can be alleviated by having the Singleton class implement an interface and accessing it through the interface. Now, you are on your way toward the modern version of the Singleton pattern, which uses dependency injection.

The modern Singleton pattern

The modern Singleton pattern is an evolution of the classic pattern, allowing one instance of the Singleton class per context. In production, there is typically a single context for the whole application, which means there’s only one instance of the Singleton class for the entire application. Each unit test, however, would have its own context, which means each test would have its own instance of the Singleton class. In modern .NET, the Singleton concept is built into the concept of a service container, also called a dependency injection (DI) container.

If you’re using the most common Microsoft.Extensions.DependencyInjection DI framework, you typically add a singleton to an IServiceCollection using the AddSingleton extension method. Today, it’s hard to think of a case where you might want a Singleton but not use IServiceCollection.AddSingleton.

Therefore, today’s Singletons are almost indistinguishable from other service classes (those managed by the DI framework) because most of these services are indeed singletons. They are regular classes, have a public constructor, can have dependencies, and usually implement an interface.

Here is a modern variation of the PerformanceCounterManager. The significant changes here are that the class is now abstracted by an IPerformanceCounterManager interface, has a public constructor that accepts a dependency to an abstracted IPerformanceCounterUploader, and does not have the landmark static Instance property.

public class PerformanceCounterManager : IPerformanceCounterManager
{
    private readonly Stopwatch _stopwatch = Stopwatch.StartNew();

    private readonly ConcurrentDictionary<string, int> _counters = new();
    private readonly IPerformanceCounterUploader _uploader;

    public PerformanceCounterManager( IPerformanceCounterUploader uploader )
    {
        this._uploader = uploader;
    }

    public void IncrementCounter( string name )
        => this._counters.AddOrUpdate( name, 1, ( _, value ) => value + 1 );

    public void UploadAndReset()
    {
        Dictionary<string, int> oldCounters;
        TimeSpan elapsed;

        lock ( this._stopwatch )
        {
            oldCounters = this._counters.RemoveAll();

            elapsed = this._stopwatch.Elapsed;
            this._stopwatch.Restart();
        }

        foreach ( var counter in oldCounters )
        {
            this._uploader.UploadCounter( counter.Key, counter.Value / elapsed.TotalSeconds );
        }
    }
}

The only thing that makes this class a Singleton is that we are using AddSingleton to register it into our DI container:

public static class Startup
{
    public static void ConfigureServices( IServiceCollection serviceCollection )
    {
        serviceCollection
            .AddSingleton<IPerformanceCounterUploader, AwsPerformanceCounterUploader>()
            .AddSingleton<IPerformanceCounterManager, PerformanceCounterManager>();
    }
}

Since we are using AddSingleton, the DI container will create at most one class instance. And since a production application usually has only one DI container, the class will effectively be a Singleton.

This is different in tests, where each test commonly has its own DI container. This is precisely why we want to isolate the tests from each other.

And speaking of tests, you can now write all kinds of tests related to PerformanceCounterManager:

  1. Unit tests for PerformanceCounterManager itself.
  2. Unit tests for classes that generally use PerformanceCounterManager but use a mock implementation for testing.
  3. Integration tests, where the PerformanceCounterManager is used as an actual implementation.

None of these are easy or even possible with the classic Singleton pattern.

Preventing illegal uses of a Singleton class

The modern DI-based approach seems to solve all the problems associated with the classic Singleton pattern.

However, it brings its own problems: because of the presence of a public constructor, nothing is stopping you (and your colleagues!) from creating multiple instances of the class. While that is very useful for testing, it can be downright wrong in production code.

How do we prevent the constructor from being used outside the Startup class or unit tests? No C# feature would allow you to do that. We need another way to express these constraints in the code and a tool to enforce them. A few solutions are available:

  • Roslyn analyzers tend to be excessively complex for this kind of verification unless you’re building a framework for thousands of developers.
  • Architectural unit tests verify your architecture from (almost) regular unit tests.
  • Metalama can validate your code against your architecture in real time.

Let’s dig into the last two options.

Architectural unit tests

Architectural unit testing aims to write unit tests that validate the architecture, running queries and assertions over the code model and failing if the constraints are unmet.

The leading project for architectural unit testing in .NET is ArchUnitNET.

To make this general, we first need to detect all modern Singletons in the code. As mentioned above, a practical idea is to mark them with a [Singleton] custom attribute. This attribute can have an empty implementation since it only works as a marker. The test would then look like this:

using static ArchUnitNET.Fluent.ArchRuleDefinition;

public class SingletonTest
{
    private static readonly Architecture _architecture = new ArchLoader()
        .LoadAssemblies( typeof(PerformanceCounterManager).Assembly )
        .Build();

    private static readonly IObjectProvider<Class> _singletons =
        Classes().That().HaveAnyAttributes( typeof(SingletonAttribute) ).As( "singletons" );

    [Fact]
    public void SingletonContructorCannnotBeAccessed()
    {
        Types()
            .Should()
            .NotCallAny(
                MethodMembers().That().AreConstructors().And().AreDeclaredIn( _singletons ) )
            .Check( _architecture );
    }
}

As is typical with ArchUnitNET, we first create an Architecture object, which includes all the assemblies to check. We then select all classes with the [Singleton] attribute and finally create a test that checks that none of the constructors of these classes are called.

If the constraint is violated, running the test will fail with a message like this:

ArchUnitNET.xUnit.FailedArchRuleException : "Types should not call Method members that are constructors and are declared in singletons" failed:
Program does call System.Void PerformanceCounterManager::.ctor(IPerformanceCounterUploader)

Note that this test ensures that constructors of singletons are never called in your code, leaving the reflection-based DI code as the only place where these constructors are called. If you have a reason to call the constructor of a Singleton manually in your DI code, you can add an exception to the test:

[Fact]
public void SingletonContructorCannnotBeAccessed()
{
    Types()
        .That()
        .AreNot( typeof(Startup) )
        .Should()
        .NotCallAny(
            MethodMembers().That().AreConstructors().And().AreDeclaredIn( _singletons ) )
        .Check( _architecture );
}

The problem with most architectural unit testing frameworks is that they behave like unit tests rather than code linters. In case of a violation, you end up with a broken test, while developers usually prefer having a squiggle directly in their code. To achieve this development experience, you must either write your own Roslyn analyzer or use Metalama, which is much simpler.

Metalama Architecture Validation

The Metalama.Extensions.Architecture open-source package allows you to enforce architectural constraints both on-the-fly, in the IDE, and at build time. The library is quite flexible and offers several approaches. Here, we’ll add the validation logic directly into the [Singleton] attribute. If you’ve used Metalama aspects before, you may be familiar with the concept of an aspect, which often encapsulates code transformations. Another lesser-known feature of aspects is that they can perform code validation and report errors. This is what we are going to do here.

public class SingletonAttribute : TypeAspect
{
    public override void BuildAspect( IAspectBuilder<INamedType> builder )
    {
        builder.Outbound
            .SelectMany( t => t.Constructors )
            .CanOnlyBeUsedFrom(
                scope => scope.Type( typeof(Startup) ).Or().Namespace( "**.Tests.**" ),
                description: "The class is a [Singleton]." );
    }
}

The [Singleton] aspect selects all constructors and calls the CanOnlyBeUsedFrom method, which produces a warning if the constructor is called from an unwanted part of the code.

When the Singleton is misused, you will get a familiar squiggle in your code:

If you build your code (whether in your IDE or from your CI), you will get this kind of warning.

warning LAMA0905: The 'PerformanceCounterManager.PerformanceCounterManager(IPerformanceCounterUploader)' constructor cannot be referenced by the 'Program' type. The class is a [Singleton].

And of course, double-clicking on the warning will get you straight to the offending line of code.

Conclusion

The Singleton pattern is one of the most classic and ubiquitous ones. With the adoption of unit testing and dependency injection, its implementation rules have widely evolved. Today, what defines a service as a singleton is the use of AddSingleton to add it to the DI container, ensuring that only one instance is created. However, this approach does not prevent multiple instances from being created since other parts of the code, unaware of the singleton nature of the service, may illegally use the public constructor. To address this issue, you can use architectural testing libraries or Metalama to enforce constraints on the Singleton class. Automatic code validation ensures that your Singleton class is used correctly and avoids potential issues in your codebase.

 

This article was first published on a https://blog.postsharp.net under the title The Singleton Pattern in C# Today Is Not Your Dad's One!.

Discover Metalama, the leading code generation and validation toolkit for C#

  • Write and maintain less code by eliminating boilerplate, generating it dynamically during compilation, typically reducing code lines and bugs by 15%.
  • Validate your codebase against your own rules in real-time to enforce adherence to your architecture, patterns, and conventions. No need to wait for code reviews.
  • Excel with large, complex, or old codebases. Metalama does not require you to change your architecture. Beyond getting started, it's at scale that it really shines.

Discover Metalama Free Edition