MetalamaCommented examplesSingletonVersion 1: Classic singleton
Open sandboxFocusImprove this doc

Example: The Classic Singleton Pattern

The "classic" version of the Singleton pattern in C# requires two components: a private constructor, and a static field, property or method exposing the unique instance of the class. The private constructor, or more importantly, the absence of a non-private constructor, ensures that the class cannot be externally instantiated.

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

1using System.Collections.Concurrent;
2using System.Diagnostics;
3
4public partial class PerformanceCounterManager
5{
6    private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
7    private ConcurrentDictionary<string, int> _counters = new();
8
9    private PerformanceCounterManager() { }
10
11    public static PerformanceCounterManager Instance { get; } = new();
12
13    public void IncrementCounter( string name )
14        => this._counters.AddOrUpdate( name, 1, ( _, value ) => value + 1 );
15}

If you frequently use the Singleton pattern, you might start noticing several issues with this code:

  1. Clarity. It's not immediately clear that the type is a Singleton. You need to parse more code to understand the pattern the class follows.
  2. Consistency. Different team members may implement the Singleton pattern in slightly different ways, making the codebase harder to learn and understand. For instance, the Instance property could have a different name, or it could be a method instead.
  3. Boilerplate. Each Singleton class repeats the same code, which is tedious and could potentially lead to bugs due to inattention.
  4. Safety. There's nothing preventing someone from making the constructor public and then creating multiple instances of the class. You would typically rely on code reviews to detect violations of the pattern.

Step 1: Generating the Instance property on the fly

To ensure consistency and avoid boilerplate code, we'll add code to SingletonAttribute. This will implement the repetitive part of the Singleton pattern for us by introducing the Instance property (see Introducing members), with an initializer that invokes the constructor.

First, we add a template property to the aspect class, which outlines the shape of the Instance property (we can't reference the type of the Singleton here, so we use object as the property type instead and replace it later):

10    [Template] 
11    public static object Instance { get; }

Then, we add code to the BuildAspect method to actually introduce the Instance property:

25            buildProperty: propertyBuilder => 
26            {
27                propertyBuilder.Type = builder.Target;
28
29                var initializer = new ExpressionBuilder();
30                initializer.AppendVerbatim( "new " );
31                initializer.AppendTypeName( builder.Target );
32                initializer.AppendVerbatim( "()" );
33
34                propertyBuilder.InitializerExpression = initializer.ToExpression();
35            } ); 

Here, we call IntroduceProperty, specifying the type into which the property should be introduced, the name of the template, and a lambda that is used to customize the property further. Inside the lambda, we replace the object type with the actual type of the Singleton class and set the initializer to invoke the constructor. We use ExpressionBuilder to build the expression that calls the constructor, including the AppendTypeName(IType) method, which ensures that the type name is correctly formatted.

The resulting Singleton class is a bit simpler, and doing this automatically ensures that all Singletons in the codebase are implemented in the same way:

Source Code
1[Singleton]
2public class MySingleton
3{
4    private MySingleton() { }

5}
Transformed Code
1[Singleton]
2public class MySingleton
3{
4    private MySingleton() { }
5public static MySingleton Instance { get; } = new global::MySingleton();
6}

Step 2: Verifying that constructors are private

To ensure safety, we can verify that all constructors are private and produce a warning (see Reporting and suppressing diagnostics) if they are not. To do this, we first add a definition of the warning as a static field to the aspect class:

14        _constructorHasToBePrivate 
15            = new(
16                "SING01",
17                Severity.Warning,
18                "The '{0}' constructor must be private because the class is [Singleton]."); 

The type of the field is DiagnosticDefinition<T>, where the type argument specifies the types of parameters used in the diagnostic message as a tuple. The message uses the same format string syntax as the <xref:System.String.Format*?displayProperty=nameWithType> method.

We then add code to the BuildAspect method to check if the constructor is private and produce a warning if it isn't:

37        foreach ( var constructor in builder.Target.Constructors ) 
38        {
39            if ( constructor.Accessibility != Accessibility.Private )
40            {
41                builder.Diagnostics.Report( 
42                    _constructorHasToBePrivate.WithArguments( (constructor, builder.Target) ),
43                    location: constructor );
44            }
45        } 

To do this, we iterate over all constructors of the type, check the Accessibility for each of them, and then report the warning specified above if the accessibility is not Private. We specify the formatting arguments for the diagnostic message as a tuple using the WithArguments method. We also set the location of the diagnostic to the constructor; otherwise, the warning would be reported at the type level, because we're reporting it through the IAspectBuilder<TAspectTarget> for the Singleton type.

Notice the warning on the public constructor in the following code.

1[Singleton]
2internal class PublicConstructorSingleton
3{
    Warning SING01: The 'PublicConstructorSingleton.PublicConstructorSingleton()' constructor must be private because the class is [Singleton].

4    public PublicConstructorSingleton() { }

5}

Aspect implementation

The complete aspect implementation is provided below:

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Code.SyntaxBuilders;
4using Metalama.Framework.Diagnostics;
5
6#pragma warning disable CS8618
7
8public class SingletonAttribute : TypeAspect
9{
10    [Template] 
11    public static object Instance { get; }
12
13    private static readonly DiagnosticDefinition<(IConstructor, INamedType)>
14        _constructorHasToBePrivate 
15            = new(
16                "SING01",
17                Severity.Warning,
18                "The '{0}' constructor must be private because the class is [Singleton]."); 
19
20    public override void BuildAspect( IAspectBuilder<INamedType> builder )
21    {
22        builder.Advice.IntroduceProperty(
23            builder.Target,
24            nameof(Instance),
25            buildProperty: propertyBuilder => 
26            {
27                propertyBuilder.Type = builder.Target;
28
29                var initializer = new ExpressionBuilder();
30                initializer.AppendVerbatim( "new " );
31                initializer.AppendTypeName( builder.Target );
32                initializer.AppendVerbatim( "()" );
33
34                propertyBuilder.InitializerExpression = initializer.ToExpression();
35            } ); 
36
37        foreach ( var constructor in builder.Target.Constructors ) 
38        {
39            if ( constructor.Accessibility != Accessibility.Private )
40            {
41                builder.Diagnostics.Report( 
42                    _constructorHasToBePrivate.WithArguments( (constructor, builder.Target) ),
43                    location: constructor );
44            }
45        } 
46    }
47}