Singleton Pattern and AddSingleton of .net core - Part 2 (dotnet core singleton scope)

In Part 1, there is different discussion around singleton. In this article there is discussion around same topic but in the context of .net core and DI container.

Here I am making assumption that reader who read has some basic knowledge of dotnet core or dotnet 5 or dotnet 6.

There are three different ways to registered dependency in dotnet core.

  1. AddSingleton - Single instance for entire application. This article will cover more.
  2. AddScoped - For every request single instance. Here request is http request.
  3. AddTransient - Everytime service requested, it get new instance.

So we begin with where we left in last article.

public class SampleOne
{ 
    public SampleOne()
    {
    }   
}

This is how you can test. Here thread-safety taken care by DI container so we don't have to worry much about that part.

using Microsoft.Extensions.DependencyInjection;

var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(typeof(SampleOne));
var serviceProvider = serviceCollection.BuildServiceProvider();

Parallel.ForEach(Enumerable.Range(1, 100), cc =>
{
    var instance = serviceProvider.GetRequiredService<SampleOne>();
    Console.WriteLine(instance.GetHashCode());
});

In above code following line registered dependency.

serviceCollection.AddSingleton(typeof(SampleOne));

Later that dependency is resolved by service provider inside Parallel loop.

var instance = serviceProvider.GetRequiredService();

Few points to remember.

  1. Here class SampleOne itself is not the Singleton.
  2. We are getting singleinstance of with help of DI ( dotnet core or dotnet 5/6) container. So lifespan is handle by DI container.

with this you can even resolve it against the interface type and this is most of time done to make it dependency that later can be replace during test.

public interface ISampleOne
{

}

public class SampleOne : ISampleOne
{ 
    public SampleOne()
    {

    }   
}

This is how it can test.

using Microsoft.Extensions.DependencyInjection;

var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(typeof(ISampleOne),typeof(SampleOne));
var serviceProvider = serviceCollection.BuildServiceProvider();

Parallel.ForEach(Enumerable.Range(1, 100), cc =>
{
    var instance = serviceProvider.GetRequiredService<ISampleOne>();
    Console.WriteLine(instance.GetHashCode());
});

There are various possibility and option with AddSingleton where you can even provider instance. Following example illustrate that.

using Microsoft.Extensions.DependencyInjection;

var serviceCollection = new ServiceCollection();
var sampleOneInstance = new SampleOne();
serviceCollection.AddSingleton(typeof(ISampleOne),sampleOneInstance);
var serviceProvider = serviceCollection.BuildServiceProvider();

Parallel.ForEach(Enumerable.Range(1, 100), cc =>
{
    var instance = serviceProvider.GetRequiredService<ISampleOne>();
    Console.WriteLine(instance.GetHashCode());
});

Here we first create instance and then it registered against dependency. For our example our SampleOne class is simple and it does not have any other dependency so it is good. If it has some other dependency then also it needs to register and also same scope like singleton.

public interface IAnotherDependency
{
    void Print();
}

public class AnotherDependency : IAnotherDependency
{
    public void Print()
    {
        Console.WriteLine(this.GetHashCode());
    }
}
public interface ISampleOne
{
    public IAnotherDependency AnotherDependency { get; }
}

public class SampleOne : ISampleOne
{ 
    public SampleOne(IAnotherDependency anotherDependency)
    {
        AnotherDependency = anotherDependency;
    }

    public IAnotherDependency AnotherDependency { get; }
}

In above code, to create the SampleOne it required IAnotherDependency otherwise it will get failed. Also as SampleOne is being resolved by singleton scoped another dependency also needs to be singleton. This is how you can test.

using Microsoft.Extensions.DependencyInjection;

var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(typeof(IAnotherDependency),typeof(AnotherDependency));
serviceCollection.AddSingleton(typeof(ISampleOne),typeof(SampleOne));
var serviceProvider = serviceCollection.BuildServiceProvider();

Parallel.ForEach(Enumerable.Range(1, 100), cc =>
{
    var instance = serviceProvider.GetRequiredService<ISampleOne>();
    Console.WriteLine(instance.GetHashCode());
    Console.WriteLine(instance.AnotherDependency.GetHashCode());
});

In this case single instance of SampleOne and single instance of AnotherDependency created.

So far so good and with DI container it is easy to get singleton without worry about thread-safety related code much as it handle by DI container.

Though there is one scenario that needs to be take care and that is what will happen if class implement multiple interface and each one them registered as dependency and later resolved. What do you expect ? Single instance or multiple instance of class. More on this in next article.