Dependency Injection In .NET Core

Since dotnet core is being release and most recently dotnet 5 and dotnet 5, It has integrated dependency injection container and It is like first class citizen in order to work with framework.

There are some of the feature are very well known and at the same time there are many more feature and functionality that is good to know. It will help during specific scenario.

For this demo I am using .NET 6 and Visual Studio 2022 Community Edition. For editor any editor will works.

Before moving forward let's take simple example. In our scenario, we have one service that return some string message. In our case that is IService. There are multiple implementation of that service possible and in our case that is ServiceA and ServiceB.

public interface IService
{
    string GetMessage();
}

public class ServiceA : IService
{
    public string GetMessage()
    {
        return "Message from ServiceA.";
    }
}

public class ServiceB : IService
{
    public string GetMessage()
    {
        return "Message from ServiceB.";
    }
}

Following is the minimal API.

using Microsoft.Extensions.DependencyInjection.Extensions;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IService, ServiceA>();
var app = builder.Build();

app.MapGet("/", (IService service) =>
{
    return service.GetMessage();
});

app.Run("http://localhost:5000");

In above code, api will be running at http://localhost:5000. Just open the browser and put that url.

Now, let's start about dependency injection in .NET core/ .net 5/ .net 6.

Different types of Service Lifetime.

  • Singleton : Only single instance of that service type.
  • Scoped : Single instance per every request. Mainly good for web api and web app scenario.
  • Transient : New instance on every request. There are too many details about this but for this blog I will not discuss this in details.

Register Dependency

  • This is already done in above example.
    builder.Services.AddScoped<IService, ServiceA>();
    
    Now if you open browser and hit the url. Following is the response.
    Message from ServiceA.
    

Register second implementation of same service type. Now it will looks like following.

  • Add new implementation ServiceB.
    builder.Services.AddScoped<IService, ServiceA>();
    builder.Services.AddScoped<IService, ServiceB>();
    
  • Hit the browser and following is the output.
    Message from ServiceB.
    
    If you inspect builder.Services after two dependency registered you will see following. It register two implementation of same service type but higher index will return. image.png

Resolve multiple dependency. This is quite simple and you have to use IEnumrable of serviceType. In our case It will look like following.

  • For this I have added new endpoint.
    app.MapGet("/multiple", (IEnumerable<IService> services) =>
    {
      string message = string.Empty;
      foreach (var service in services)
          message += service.GetMessage();
      return message;
    });
    
  • Hit the browser with url this time with http://localhost:5000/multiple. It will return
    Message from ServiceA.Message from ServiceB.
    

Use of TryAddScoped.

  • We have seen above that if we use AddScoped then It will not check that service type already registered or not. TryAddScoped will give that functionality.
  • Replace AddScoped with TryAddScoped.
    builder.Services.TryAddScoped<IService, ServiceA>();
    builder.Services.TryAddScoped<IService, ServiceB>();
    
  • Hit the browser with url http://localhost:5000 and http://localhost:5000/multiple.
  • For both above url result is same.
    Message from ServiceA.
    
  • Even during debug there is only one serviceType to implementation is there. image.png

Remove all implementation of service type.

  • Now let's say we have scenario in which we have to remove all the implementation of registered for servicetype and registered new one. This is usually useful when you don't have control over other registration.
  • For this RemoveAll method is there. For example try this. In which we remove implementation ServiceA and add ServiceB.
    builder.Services.AddScoped<IService, ServiceA>();
    builder.Services.RemoveAll<IService>();
    builder.Services.AddScoped<IService, ServiceB>();
    
  • Hit the browser and both url. Same output.
    Message from ServiceB.
    
  • Debug also you can see the behavior. image.png

Now before moving to next set of method, It is good to see how we can utilize ServiceDescriptor and its method to register dependency. Internally ServiceDescriptor is main class that is being used to register, maintain list of service and later resolve service.

Something about ServiceDescriptor

  • Describe method to create service descriptor and register with services.
    var serviceADescriptor = ServiceDescriptor.Describe(typeof(IService), typeof(ServiceA), ServiceLifetime.Scoped);
    
  • Above scneario our service type is IService and implementation type is ServiceA and service lifetime is scoped.
  • It can register following way.
    builder.Services.Add(serviceADescriptor);
    
  • Other then Describe there is separate method for Singleton, Scoped and Transient service lifetime in Service Descriptor.
    var serviceBDescriptor = ServiceDescriptor.Scoped(typeof(IService), typeof(ServiceB));
    builder.Services.Add(serviceBDescriptor);
    
    By this way following two are same.
    Way 1 ( Mostly used)
    builder.Services.AddScoped<IService, ServiceA>();
    builder.Services.AddScoped<IService, ServiceB>();
    
    Way 2
    builder.Services.Add(ServiceDescriptor.Describe(typeof(IService), typeof(ServiceA), ServiceLifetime.Scoped));
    builder.Services.Add(ServiceDescriptor.Scoped(typeof(IService), typeof(ServiceB)));
    

Enough about ServiceDescriptor and now we will see how it will be used in replace method.

Replace the existing registration.

  • Consider that over the period of time there are two more implementation of IService. You want to replace it.
  • The way replace works is that, It replace existing registration with new one and starting from lower index if there are multiple registration already there. Also it replace only one registration at a time. image.png

  • Our current registration is like this.

    builder.Services.AddScoped<IService, ServiceA>();
    builder.Services.AddScoped<IService, ServiceB>();
    
  • Now let's replace.
    builder.Services.Replace(ServiceDescriptor.Scoped<IService, ServiceC>());
    
  • Above statement replace ServiceA with ServiceC.
  • Same way if we make call next time.
    builder.Services.Replace(ServiceDescriptor.Scoped<IService, ServiceD>());
    
  • Above statement replace ServiceB with ServiceD.
  • After this only ServiceC and ServiceD will be there.
  • This is how it looks. image.png

One thing that not supported is Decorating the existing implementation with new implementation by taking current implementation underneath that. It is useful for adding extra concern without changing the existing functionality.

Decorate the existing implementation

  • This is not supported from Built-In IoC container of .net core.
  • There is one nuget package and it support that functionality.
    dotnet add package Scrutor --version 4.1.0
    
  • Once package added It is quite simple.
  • For example we have to decorate each message with Log text. So implement that time and that implement same interface as other services like ServiceA, ServiceB, ServiceC and ServiceD.

    public class ServiceLogger : IService
    {
      private readonly IService service;
    
      public ServiceLogger(IService service)
      {
          this.service = service;
      }
    
      public string GetMessage()
      {
          string message = "Log:START";
          message += this.service.GetMessage();
          message += "Log:End";
          return message;
      }
    }
    
  • Call Decorate method.
    builder.Services.Decorate<IService,ServiceLogger>();
    
  • Now open the browser and both the url.
  • For localhost:5000, it will give following output. As ServiceD is the last one registered so it return with decorated message.
    Log:STARTMessage from ServiceD.Log:End
    
  • For localhost:5000/multiple. It gives following output.
    Log:STARTMessage from ServiceC.Log:EndLog:STARTMessage from ServiceD.Log:End
    

    Register multiple implementation at once. In fact this helps to register multiple services at once. Also It takes care of duplication so only unique one get registered.

  • For this use TryAddEnumrable
    builder.Services.TryAddEnumerable(new ServiceDescriptor[]
    {
      ServiceDescriptor.Scoped<IService, ServiceA>(),
      ServiceDescriptor.Scoped<IService, ServiceA>(),
      ServiceDescriptor.Scoped<IService, ServiceB>(),
      ServiceDescriptor.Scoped<IService, ServiceC>(),
      ServiceDescriptor.Scoped<IService, ServiceD>(),
    });
    
  • This is how it registered. image.png

I have tried to cover most of the aspect of dependency injection in .net core/.net 5/.net 6. There are many more methods out there and also the extensions method but this will gives you head start and understanding of DI in .net core.

Also I have not covered Lifetime aspect of service. One thing I have covered in my earlier post is about Singleton registration and scenario related to that. It is worth looking at that part as well. Part-3 of Singleton

Hope this helps.