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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
public class SingletonService { public int Counter; } public class ScopedService { public int Counter; } public class TransientService { public int Counter; } public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddSingleton<SingletonService>(); services.AddScoped<ScopedService>(); services.AddTransient<TransientService>(); } public void Configure(IApplicationBuilder app) { app.Use((ctx, next) => { // Get all the services and increase their counters... var singleton = ctx.RequestServices.GetRequiredService<SingletonService>(); var scoped = ctx.RequestServices.GetRequiredService<ScopedService>(); var transient = ctx.RequestServices.GetRequiredService<TransientService>(); singleton.Counter++; scoped.Counter++; transient.Counter++; return next(); }); app.Run(async ctx => { // ...then do it again... var singleton = ctx.RequestServices.GetRequiredService<SingletonService>(); var scoped = ctx.RequestServices.GetRequiredService<ScopedService>(); var transient = ctx.RequestServices.GetRequiredService<TransientService>(); singleton.Counter++; scoped.Counter++; transient.Counter++; // ...and display the counter values. await ctx.Response.WriteAsync($"Singleton: {singleton.Counter}\n"); await ctx.Response.WriteAsync($"Scoped: {scoped.Counter}\n"); await ctx.Response.WriteAsync($"Transient: {transient.Counter}\n"); }); } } |
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:
1 2 3 |
Singleton: 2 Scoped: 2 Transient: 1 |
The second request will yield a different response:
1 2 3 |
Singleton: 4 Scoped: 2 Transient: 1 |
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 ApplicationServices
is 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):
1 2 3 4 5 6 7 8 9 |
public void Configure(IApplicationBuilder app) { var thisWontWorkWell = app.ApplicationServices.GetRequiredService<ScopedService>(); app.Run(async ctx => { await ctx.Response.WriteAsync("Hello"); }); } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public void Configure(IApplicationBuilder app) { using (var scope = app.ApplicationServices.CreateScope()) { var s = scope.ServiceProvider.GetRequiredService<ScopedService>(); // you have access to ScopedService here but don't let it leak to any middleware } app.Run(async ctx => { await ctx.Response.WriteAsync("Hello"); }); } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class CounterIncreasingMiddleware { private readonly RequestDelegate _next; private readonly SingletonService singletonService; public CounterIncreasingMiddleware(RequestDelegate next, SingletonService singletonService) { _next = next; this.singletonService = singletonService; } public Task Invoke(HttpContext context, ScopedService scopedService, TransientService transientService) { return _next(context); } } |
This it not, as both _scopedService
and _transientService
will become singletons:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public class CounterIncreasingMiddleware { private readonly RequestDelegate _next; private readonly SingletonService _singletonService; private readonly ScopedService _scopedService; private readonly TransientService _transientService; public CounterIncreasingMiddleware(RequestDelegate next, SingletonService singletonService, ScopedService scopedService, TransientService transientService) { _next = next; _singletonService = singletonService; // DON'T DO IT! _scopedService = scopedService; _transientService = transientService; } public Task Invoke(HttpContext context) { return _next(context); } } |
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!
Thank you. I hadn’t thought that through before, and… pardon me while I go check my code.
Appreciate the sharing.
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.
Yep, if you have a scoped service, the Invoke method is the right place to inject it.
Yeah man! Good and clear explanation.
Thanks!
Thank you really much for this article! Now i have good understanding of dependency injection in ASP.NET Core thanks to you.
Thank you, I had a problem with scoped dependency injected into middlewares constructor. I have just solved it using your guidance.
I am really happy with your blog……..
your article is very unique and powerful for new reader.
thanks for sharing……keep going on…….
When I run your example,
the second request yields a response as Singleton 6, Scoped 2 and Transient 1.
Can you explain this? Thanks
SO do we not use transientService inside a of middle ware? How do we use it if we can’t store it a class to access it from other methods?
I like your article, Thanks for sharing