Dependency Injection Lifetime Management in .NET Core

Dependency Injection is one of the most important features in building a maintainable application. In .NET Framework we have to rely on an external component to help us with this. Some of the most popular are Ninject, Unity, Spring.NET and others. Now, in .NET Core the dependency injection capability is built into the framework so we don’t have to use external components any more.

The easiest way to use it is just to register the class you want to inject. In .NET Core this is usually done in the Startup.ConfigureServices by adding each type to the container using the appropriate extension according to the lifetime. There are three lifetimes that can be used: Transient, Singleton and Scoped.

Transient has the shortest lifetime of the three. A transient lifetime service is created each time it is requested from the service container.

services.AddTransient<IOperationTransient, Operation>();

Scoped lifetime services are created once per client request.

services.AddScoped<IOperationScoped, Operation>();

The singleton lifetime service is create the first time it is requested. Every following request for the service will use the same instance of the service. It has the longest lifetime.

services.AddSingleton<IOperationSingleton, Operation>();

A service itself should not depend on another service with a shorter lifetime of its own. Objects should not live beyond their intended lifetime. This could lead to bugs and problematic behaviors.

As shown in the table below we see that inside a transient service we can create services with any of the scoped services, a scoped service should only be dependent on scoped and singleton services and a singleton service should only depend on a singleton services. The left most column represents the parent service registration type.

Transient Scoped Singleton
TransientOKOK OK
ScopedXOKOK
SingletonXXOK

Aside from the Add[LifetimeType] extension methods we can also use the TryAdd[LifetimeType] extension methods. This one adds the service if the given service has not been registered yet.

services.TryAddTransient<IOperationTransient, Operation>();
services.TryAddScoped<IOperationScoped, Operation>();
services.TryAddSingleton<IOperationSingleton, Operation>();

Demo:

In the following demo we will show a simple web application with a service that generates a random number from a transient a scoped and a singleton service. We will show the result of the registered services from another service (we call it the ConsumerService) and we will show the results from the services in another HTTP call.

First we register all the service in the ConfigureServices of the Startup class.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();

    services.AddTransient<IConsumerService, ConsumerService>();

    services.AddTransient<INumberServiceTransient, NumberService>();
    services.AddScoped<INumberServiceScoped, NumberService>();
    services.AddSingleton<INumberServiceSingleton, NumberService>();
}

Then we have the simple NumberService:

public interface INumberService
{
    int Number { get; }
}

public interface INumberServiceTransient : INumberService { }

public interface INumberServiceScoped : INumberService { }

public interface INumberServiceSingleton : INumberService { }
public class NumberService : INumberServiceTransient, INumberServiceScoped, INumberServiceSingleton
{
    public int Number { get; private set; }

    public NumberService()
    {
        var random = new Random();
        Number = random.Next(1, 1000);
    }
}

Next we create the ConsumerService to demonstrate the value of the scoped service.

public interface IConsumerService
{
    int GetNumberFromTransientService();

    int GetNumberFromScopedService();

    int GetNumberFromSingletonService();
}
public class ConsumerService : IConsumerService
{
    ...

    public int GetNumberFromTransientService()
    {
        return transientNumberService.Number;
    }

    public int GetNumberFromScopedService()
    {
        return scopedNumberService.Number;
    }

    public int GetNumberFromSingletonService()
    {
        return singletonNumberService.Number;
    }
}

Then in the controller we simply use the registered services and we output the result:

public class HomeController : Controller
{
    private readonly IConsumerService consumerService;
    private readonly INumberServiceTransient transientNumberService;
    private readonly INumberServiceScoped scopedNumberService;
    private readonly INumberServiceSingleton singletonNumberService;

    public HomeController(
        IConsumerService consumerService,
        INumberServiceTransient transientNumberService,
        INumberServiceScoped scopedNumberService,
        INumberServiceSingleton singletonNumberService)
    {
        this.consumerService = consumerService;
        this.transientNumberService = transientNumberService;
        this.scopedNumberService = scopedNumberService;
        this.singletonNumberService = singletonNumberService;
    }
    public IActionResult Index()
    {
        var output = @$"First usage (in the controller): 
            Transient service result: {transientNumberService.Number}
            Scoped service result: {scopedNumberService.Number}
            Singleton service result: {singletonNumberService.Number}

        Second usage (in the consumer service):
            Transient service result: {consumerService.GetNumberFromTransientService()}
            Scoped service result: {consumerService.GetNumberFromScopedService()}
            Singleton service result: {consumerService.GetNumberFromSingletonService()}" ;

        return Content(output);
    }
}

The full source code can be found here.

In the result we can see the following for the first HTTP request:

Here we see that the transient service returned a different value in the second call while the scoped and the singleton returned the same result in the second call.

Now lets see what happens in the second HTTP call:

Now we see that the transient and the scoped services returned a different value from the first HTTP request while the singleton return the same result for all cases.

Conclusion: When working with dependency injection be aware when using the lifetime, especially when its the Singleton class as this one should not host other registration types like transient or scoped as if the singleton is called the second time it will be using the scoped or transient from the first singleton call and it should not be the intention and could lead to unwanted behavior and bugs.

Leave a Reply

Your email address will not be published.