Singleton Pattern and AddSingleton of .net core - Part 3 (Multiple Dependency)

In this article, I will specially cover what will happen when multiple different type of interface implement by single concrete class and later it resolve against multiple different interface type. All this in context of AddSingleton of DI container of .net core or .net 5/6.

Here are the link of previous two article. Part 1 Part 2

Let begin with scenario.

public interface ISampleOne
{

}

public interface ISampleTwo
{

}

public class Sample : ISampleOne , ISampleTwo
{ 
    public Sample()
    {

    }

}

This is how you can test.

using Microsoft.Extensions.DependencyInjection;

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

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

Now before check result, what is your thought ? You may expect that it must be single value for GetHashCode but it is not.

So for DI container It will resolve dependency for each registered type and if it is not previously requested then it will create new instance as it is not checking that another dependency for same object already resolved. Di container control the lifespan of dependency.

Now lets solve this and we want single instance of Sample one class needs to be share for two dependency.

Solution 1

This is already discussed in previous article but here it make more sense. Create instance of Sample class manually and then use that instance in registration.


using Microsoft.Extensions.DependencyInjection;

var serviceCollection = new ServiceCollection();
var sampleInstance = new Sample();
serviceCollection.AddSingleton(typeof(ISampleOne),sampleInstance);
serviceCollection.AddSingleton(typeof(ISampleTwo), sampleInstance);
var serviceProvider = serviceCollection.BuildServiceProvider();

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

So above code sampleInstance is used in AddSingleton and so now that instance return when specific dependency resolved. You get single value for both.

Solution 2

This is some what extension to Solution 1. Let's say that Sample has many other dependency like following. So in that case it is not good idea to have those dependency resolve manually.

public interface IAnotherDependency
{
    void Print();
}

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

public interface ISampleTwo
{
    public IAnotherDependency AnotherDependency { get; }
}

public class Sample : ISampleOne , ISampleTwo
{ 
    public Sample(IAnotherDependency anotherDependency)
    {
        AnotherDependency = anotherDependency;
    }

    public IAnotherDependency AnotherDependency { get; }
}

So now what we have done manually in Solution 1 to create instance of Sample will not work as its constructor expect one another dependency to available. ( It is not good idea to resolve manually as there are many dependency otherwise one could argue that we can create instance of another dependency and pass that in Sample constructor.)

Following solution also known as Forwarded type and it is available in another container like Autofac. For default DI container we can do like this.

var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IAnotherDependency,AnotherDependency>();
serviceCollection.AddSingleton<Sample>();
serviceCollection.AddSingleton(typeof(ISampleOne), sc => sc.GetRequiredService<Sample>());
serviceCollection.AddSingleton(typeof(ISampleTwo), sc => sc.GetRequiredService<Sample>());
var serviceProvider = serviceCollection.BuildServiceProvider();

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

Now let's break down what is happening over here.

  1. serviceCollection.AddSingleton<IAnotherDependency,AnotherDependency>(); This line registered the dependency required by Sample class.
  2. serviceCollection.AddSingleton<Sample>(); Here registration of Sample concreate implementation as Singleton.
  3. serviceCollection.AddSingleton(typeof(ISampleOne), sc => sc.GetRequiredService<Sample>()); and serviceCollection.AddSingleton(typeof(ISampleTwo), sc => sc.GetRequiredService<Sample>()); both use another implementation of AddSingleton. Here it requesting service using sc.GetRequiredService<Sample>(). As we have registered Sample as singleton it resolve to single instance and it shared between dependency.

Hope this helps and if there is any suggestion or so please drop comments.