Dependency lifetime in ASP.NET Core

What is a dependency lifetime?

Dependency lifetime (or service lifetime; I’ll be using the words “dependency” and “service” interchangeably in this article) defines when a dependency is instantiated and how long does it live. The dependency injection (DI) container is responsible for controlling this. Traditionally, in previous versions of ASP.NET, developers had to use third-party libraries, such as Autofac, to do this. In ASP.NET Core a simple DI container is built in and it doesn’t require a lot of setup (and can be replaced in there’s a need to do so). I wrote about dependency injection in a past post, so I won’t go into much detail here.

The dependency injection system build in ASP.NET Core allows us to define the rules of reusability of instances of services. There are three available options:

  • Singleton – a single instance of the service class is created, stored in memory and reused for all injections. Can be used for services that are expensive to instantiate. Note that the instance is kept in memory during the whole application lifetime, so watch out for the memory usage. On the plus side, the memory will be allocated just once, so the garbage collector will have less to do.
  • Scoped – simplifying a little, this is best described as per-request singleton (technically, the scope doesn’t have to be equal to the request; I’ll explain it deeper later). All middlewares, MVC controllers, etc. that participate in handling of a single request will get the same instance. A good candidate for a scoped service is an Entity Framework context. Scoped services should not be resolved directly from the Application container (more on that below).
  • Transient – every time the service is resolved from a DI container, a new instance is created. This may cause frequent allocations and deallocations of memory and thus can have a negative impact on performance if used very often.

To visualize the differences between these lifetimes, consider the following application:

The ConfigureServices method registers three services, one as a singleton, one scoped and one transient. Then, there are two middlewares. The first one gets instances of all services by accessing HttpContext.RequestServices and increases their counters by one. The second one does the same and then writes the counter values to the response.

After submitting the first request to this application we get the following result:

The second request will yield a different response:

This looks as expected: singleton values are not discarded between requests, so there is a single counter for the whole application. The scoped service is reused for a single request. The second request got a fresh instance of the ScopedService class, so counter was always 2 at the end of the request. Whenever a transient service was resolved, a new instance was created, so its value we incremented in the first middleware was effectively discarded.

RequestServices vs ApplicationServices

So far, we’ve discussed resolving services in middleware. However, quite often we need to get an instance of a dependency during configuration phase, that is, outside of any middleware. During configuration there is no HttpContext object available to retreive services from. In this case we can use the ApplicationServices container found in a IApplicationBuilder instance.

Why there are two different containers, though? The ApplicationServices is the root container. It is not associated with any lifetime scope (or, more correctly, its lifetime scope is equal to the application lifetime scope). Thus, resolving a scoped service from the ApplicationServicesis basically equivalent to resolving a singleton. This may lead to subtle errors if done inadvertently, so the authors of the framework added a check that prevents users from resolving scoped dependencies from ApplicationServices. This check works only if the app is running in Development environment. It is configured in a WebHost.CreateDefaultBuilder, so if you opted out from using it, you’ll have to add it yourself manually.

RequestServices, on the other hand, is a scoped container created from the root on each request. The rule of thumb is that if you are in a context of a request, use RequestServices. Otherwise, ApplicationServices is the only option.

The following code will throw an InvalidOperationException on Development environment (assuming the same service registration as in the previous example):

Resolving scoped service on application level

If you need to resolve a scoped service outside of any middlewares, you need to explicitly create a scope for it:

In the above case, the DI container will resolve a ScopedService without any errors and will dispose it as soon as the application exits the using block.

Dependency injection

So far, we’ve been getting hold of the dependencies using ApplicationServices or RequestServices. This is the only option in the anonymous method middlewares, but when defining a middleware a a class, we can use true dependency injection.

There are two places that support injection in a middleware class: constructor and Invoke method. The difference between them is similar to ApplicationServices and RequestServices. The middleware’s constructor is executed during application initialization phase, while the Invoke method runs per request. This means we shouldn’t resolved scoped services inside the constructor. Also, if you inject a transient dependency and store it in a field, it will effective become a singleton.

This is correct:

This it not, as both _scopedService and _transientService will become singletons:

The framework will throw an exception when you try to inject a scoped service into the middleware constructor, but it will happily accept the transient service. It is developer’s responsibility not to store a transient dependency in a field.

Dependency injection in MVC

In case of MVC, we can safely inject all kinds of services in controllers’ constructors. The controllers, unlike middleware classes, are instantiated per request so the proper request scope is set.

Summary

The main takeaway from this article is that developers should be careful how they register and resolve dependencies. The dependency lifetime is not a complicated topic but when used incorrectly, it can cause bugs that are hard to find.

Happy coding!

3 comments

  1. Thank you. I hadn’t thought that through before, and… pardon me while I go check my code.

    Appreciate the sharing.

    Scott on 1 March 2018, 7:37 UTC Reply
  2. Thank you. Now I do understand better. I helped myself, doing the DI in the Invoke method of the middleware. This should be right solution, as far as I understand.

    Matt on 12 March 2018, 1:36 UTC Reply
    1. Yep, if you have a scoped service, the Invoke method is the right place to inject it.

      Michał Dudak on 12 March 2018, 18:11 UTC Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

Michał Dudak