Modern Singleton Pattern
The classic Singleton pattern has not aged well. It’s considered an anti-pattern because it is difficult to test and incompatible with the Dependency Injection paradigm.
A modern approach to the Singleton pattern is to use a Dependency Injection container to manage the lifecycle of the singleton instance (e.g., IServiceCollection.AddSingleton
).
The main problem with the modern Singleton is that it is challenging to ensure the constructor is used only in the appropriate context, typically from the Startup
code and unit tests. Metalama can help you enforce architectural constraints and report violations directly in the code editor as warnings.
Example
The following PerformanceCounterManager
class is a modern Singleton example. It has a public constructor but is registered as a Singleton in the DI container.
[Singleton]
public class PerformanceCounterManager : IPerformanceCounterManager
{
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 );
}
The [Singleton]
aspect reports warnings when any class other than Startup
or a unit test attempts to call the constructor.
class MetricsCollection( IPerformanceCounterUploader uploader )
{
// WARNING! The class is a [Singleton].
PerformanceCounterManager _performanceManager = new( uploader );
}
Show me how it works!
Here is the code of the [Singleton]
aspect:
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]." );
}
}
Metalama benefits
- Reduce human errors. You can be confident that no improper code is calling the constructor directly instead of getting an instance from the DI container.