Skip to content
Elisenda Gascon By Elisenda Gascon Apprentice Engineer II
Service Lifetimes in ASP.NET Core

In my last post, I explained why using dependency injection to register services in ASP.NET Core apps was needed to design an application that respects the principles of inversion of control and loose coupling, and I gave an example of how to do so using the Microsoft's built in dependency injection container.

There was one crucial aspect of the registration of services in the container that I didn't explain – service lifetimes. These control how long the container will hold onto a resolved object after creating it.

The Dependency Injection container

The dependency injection container is a tool that manages the instantiation and configuration of objects in an application. Even though it is technically possible to build an application without using the principle of inversion of control, the use of a container is always recommended to simplify the management of dependencies within your code.

At startup, services are registered in the container. Whenever these services are required, instances of those services are resolved from the container at runtime. The container is responsible for the creation and disposal of instances of the required services, by keeping track of them and maintaining them for the duration of their lifetime.

Service Lifetimes

There are three lifetimes available with the Microsoft Dependency Injection container: transient, singleton, and scoped. The lifetime of the service is specified when registering the service in the container. As we will see shortly, because a service can be used in different places in the application, the service lifetime will affect whether the same instance of the service is consumed across the application, thus affecting the output.

To understand and illustrate how lifetimes work, we will use an ASP.NET Core application that displays the date and time in the index page. A class will be responsible for providing the date and time, and it will be invoked by methods in two different classes – a middleware component, and the page model of the index page.

The service class, Time, is an implementation of IDateTime. It is responsible for capturing the date at the time when it is instantiated by the dependency injection container and setting a private field to the value. The creation of the service (and thus the request and capture of the date) is being delegates to the container. This way, the lifetime used to register the service in the container will affect the output of the service. Finally, the class contains a method that, when called, returns that value in a string format.

The Time class is the following:

public class Time : IDateTime
{
    private readonly string _time;

    public Time()
    {
        _time = DateTime.Now.ToString();
    }

    public string GetDate()
    {
        return _time;
    }
}

And the IDateTime interface is:

public interface IDateTime
{
    string GetDate();
}

This service is invoked in two different classes.

First, it will be called from a custom middleware component that we have added to the request pipeline.

public class DateCustomMiddleware
{
    public const string ContextItemsKey = nameof(ContextItemsKey);

    private readonly RequestDelegate _next;
    private readonly ILogger<DateCustomMiddleware> _logger;

    public CustomMiddleware(RequestDelegate next, ILogger<DateCustomMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context, IDateTime dateTime)
    {
        await Task.Delay(TimeSpan.FromSeconds(10)).ConfigureAwait(false);

        var date = dateTime.GetDate();

        context.Items.Add(ContextItemsKey, date);

        var logMessage = $"Middleware: The Date from Time is {date}";

        _logger.LogInformation(logMessage);

        await _next(context);
    }
}

The InvokeAsync method calls GetDate() on the IDateTime service to get the current date and time.

The line:

await Task.Delay(TimeSpan.FromSeconds(10)).ConfigureAwait(false);

will stop the code from running for 10 seconds. The idea is that we want the injection of the IDateTime service in the middleware and the injection in the IndexModel to be 10 seconds apart. That way, we will be able to tell if the same instance of the method is being used by both classes (the two times displayed match) or if they are each using a unique instance (the times displayed differ by 10 seconds).

Note that we are not using constructor injection to inject an instance of the IDateTime service in the middleware component. For reasons that will become apparent by the end of this post, injecting services in a middleware component through constructor injection can result in an InvalidOperationException. Injecting the services through the InvokeAsync method is safer. I'll explain why once we have been through the different lifetimes.

The other place where we request the Time service is the IndexModel class.

public class IndexModel : PageModel
{
    private readonly IDateTime _date;

    public IndexModel(IDateTime date)
    {
        _date = date;
    }

    public string DateFromDependency { get; set; }

    public string DateFromMiddleware { get; set; }

    public void OnGet()
    {
        if (HttpContext.Items.TryGetValue(DateCustomMiddleware.ContextItemsKey,
            out var mwDate) && mwDate is string contextItemsKey)
        {
            DateFromMiddleware = contextItemsKey;
        }

        var date = _date.GetDate();
        DateFromDependency = date;
    }
}

We populate the DateFromMiddleware property with the value that the ContextItemsKey put in the HttpContext.Items dictionary.

We call GetDate to populate the DateFromDependency variable. The service DateTime is injected in the constructor of the class, which need to be created in the container, once we register it.

We now have dates coming from places with different injections of the DateTime service.

Now that the application is set up, we can register the service in the container with the different lifetimes and see how they each behave.

Transient Lifetime

The transient lifetime is the most straightforward to understand, and usually the safest to use. Instances of services registered with a transient lifetime are created every time that their injection into a class is required.

To register a service with the transient lifetime, use the AddTransient extension method on the IServiceCollection:

builder.Services.AddTransient<IDateTime, Time>();

In our example, the instance of the IDateTime service injected into the middleware and the IndexModel will be different. Note how the two dates are 10 seconds apart, because a new instance of the service was created every time that it was required.

Showing the index page once we run the application. One value for "Date from middleware" and one for "Date" are shown. Date from middleware shows "16/06/2022 18:29:14" whereas Date shows "16/06/2022 18:29:24"

If we refresh the page, the two times displayed are updated, and are still 10 seconds apart.

Singleton Lifetime

Services registered with a singleton lifetime are only instantiated once during the lifetime of the application. This means that any injection of the service during the lifetime of the application will be done with the same instance of the service.

To register a service with the singleton lifetime, use the AddSingleton extension method on the IServiceCollection:

builder.Services.AddSingleton<IDateTime, Time>();

In our example both classes will receive the same instance of the Time service. Because the middleware component is the first one to require an instance of the service, the instance will be created before the middleware's InvokeAsync is called. After that, it will be reused wherever needed during the lifetime of the application.

When running the application, both times displayed are the same.

Showing the index page once we run the application. Both values show "16/06/2022 18:33:03"

When refreshing the page, the same times will be displayed. Sending a new request doesn't restart the application, so the same instance of the service will be reused with new requests.

Scoped Lifetime

When an HTTP request is sent to the server, an HttpContext is created, which holds information on the request. This is when a scope for the current request is created within the application. The HttpContext is then sent through the request pipeline, which includes several middleware components as well as endpoints, which generate the final response.

This definition of a scope is important to understand scoped lifetimes. In ASP.NET Core, the lifetime of a scope is equivalent to the lifetime of an HTTP request. Services registered with a scoped lifetime will be maintained and used during the lifetime of the scope (or HTTP request) they have been created in. So for one same request, the instance of an object injected in different classes will be the same.

To register a service with the scoped lifetime, use the AddScoped extension method on the IServiceCollection:

builder.Services.AddScoped<IDateTime, Time>();

When we run the application, the two times displayed are the same – the instance injected into the middleware component and into the IndexModel was the same.

Showing the index page once we run the application. Both values show "16/06/2022 18:38:40"

However, when refreshing the page, the time displayed is updated (but the two times still match). Refreshing the page sends a new HTTP request, and a new scope is created within the application. Within this new scope, a new instance of the Time service is created and injected into both classes using the service.

Showing the index page once we refresh the page. Both values show "16/06/2022 18:40:59"

Correctly injecting services in middleware components

Now that we have seen how each of the lifetimes behave, let’s quickly go back to the reason we didn't inject our Time service through constructor injection in our custom middleware component.

The middleware request pipeline is created once for the duration of the application's lifetime. This means that any service injected through constructor injection in a middleware component can only be injected once during the application's lifetime. When this injected service is registered with a singleton lifetime, this poses no problem as the lifetime of the request pipeline and of the service are the same.

However, if the service injected in the middleware component has a transient lifetime, an instance of the service would be injected only once, as the first instantiation would occur at the same time that the pipeline is created and the component is constructed. Middleware components are only constructed once during the application's lifetime, which means they are singletons within the application. Dependencies injected via the constructor are captured for the application's lifetime. Subsequent injection of newly resolved services could not happen because the lifetime of the middleware component is longer than that of the service. This would condition the application's behaviour, as the middleware component would receive only one instance of the service.

Capturing a short-lived service inside of a longer-lived service will result in an InvalidOperationException being thrown because instances of the services can't be correctly injected.

The way to solve this is to inject services as parameters in the Invoke or InvokeAsync method of the middleware component. As this method will be invoked once per request, its parameters will be resolved from the container within the scope of the current request.

Conclusion

In this post, I have explained one of the most important aspects to consider when registering dependencies in the dependency injection container – service lifetimes. We have seen that services with transient lifetimes are instantiated every time they need to be injected, services with scoped lifetimes are created once per request, and singleton services have the same lifetime as the application. We have also seen an example of why it is important to be mindful of the lifetimes of our registered dependencies, as capturing short-lived services inside long-lived services can introduce bugs into our code.

Elisenda Gascon

Apprentice Engineer II

Elisenda Gascon

Elisenda is a mathematics graduate from UCL. During her years at university, Elisenda took a couple of introductory modules in Python and Machine Learning, which led her to take a few online courses on those subjects.

After finishing her mathematics degree, Elisenda's motivation to join endjin was a desire to put her problem solving skills to the test and further develop her understanding of technology. She is currently expanding her knowledge of cloud computing and its various applications, and discovering the fascinating world of Microsoft Azure.