Dependency Injection Lifetimes in .NET
Dependency Injection
2 Articles
In this article, let's learn about Lifetimes in Dependency Injection
in .NET.
Note: If you have not done so already, I recommend you read the article on Introducing Dependency Injection in .NET.
Table of Contents
Introduction
Lifetimes in dependency injection describe the way object instance is created, reused and disposed by the framework. Services can be registered with one of the following lifetimes:
- Transient
- Scoped
- Singleton
The following sections describe each of the preceding lifetimes. Choose an appropriate lifetime for each registered service.
Transient
Transient lifetime services are created each time they're requested from the service container. This lifetime works best for lightweight,
stateless services. Register transient services with AddTransient
.
Code Sample - Transient Dependency
Demo - Transient Service Dependency
TransientServiceDemo
is registered inProgram.cs
asTransient
service usingservices.AddTransient<TransientServiceDemo>();
.-
Let's resolve and
@inject TransientServiceDemo
twice within a component as follows.@inject TransientServiceDemo TransientService1;
and@inject TransientServiceDemo TransientService2;
. -
When we read
Guid Id
from both instances it should bedifferent
as two instances are not same. -
Now let's compare the object reference of two
TransientServiceDemo
instance usingObject.ReferenceEquals(TransientService1, TransientService2)
. This should returnFalse
.
Guid from Transient Service 1 : dd014035-c0d4-4c18-af31-2d2c68480f1d
Guid from Transient Service 2 : ff1ca126-e772-4c2a-aba7-d9304ca37f2b
Transient Dependency TransientService1 Equals TransientService2 : False
Scoped
For web applications, a scoped lifetime indicates that services are created once per client request (connection). Register scoped services
with AddScoped
.
In apps that process requests, scoped services are disposed at the end of the request.
When using Entity Framework Core, the AddDbContext
extension method registers DbContext types with a scoped
lifetime by default.
Code Sample - Scoped Dependency
Demo - Scoped Service Dependency
ScopedServiceDemo
is registered inProgram.cs
asScoped
service usingservices.AddScoped<ScopedServiceDemo>();
.-
Let's resolve and
@inject ScopedServiceDemo
twice within a component as follows.@inject ScopedServiceDemo ScopedService1;
and@inject ScopedServiceDemo ScopedService2;
. -
When we read
Guid Id
from both instances it should besame
as two instances are same per scope (request). -
Now let's compare the object reference of two
ScopedServiceDemo
instance usingObject.ReferenceEquals(ScopedService1, ScopedService2)
. This should returnTrue
.
Guid from Scoped Service 1 : 22833996-9fef-4b5b-a457-3c855f674223
Guid from Scoped Service 2 : 22833996-9fef-4b5b-a457-3c855f674223
Scoped Dependency ScopedService1 Equals ScopedService2 : True
Singleton
Singleton lifetime services are created either:
- The first time they're requested.
- By the developer, when providing an implementation instance directly to the container. This approach is rarely needed.
Every subsequent request of the service implementation from the dependency injection container uses the same instance. If the app requires singleton behavior, allow the service container to manage the service's lifetime. Don't implement the singleton design pattern and provide code to dispose of the singleton. Services should never be disposed by code that resolved the service from the container. If a type or factory is registered as a singleton, the container disposes the singleton automatically.
Register singleton services with AddSingleton
. Singleton services must be thread safe and are often used in
stateless services.
In apps that process requests, singleton services are disposed when the ServiceProvider
is disposed on
application shutdown. Because memory is not released until the app is shut down, consider memory use with a singleton service.
Code Sample - Singleton Dependency
Demo - Singleton Service Dependency
SingletonServiceDemo
is registered inProgram.cs
asSingleton
service usingservices.AddSingleton<SingletonServiceDemo>();
.-
Let's resolve and
@inject SingletonServiceDemo
twice within a component as follows.@inject SingletonServiceDemo SingletonService1;
and@inject SingletonServiceDemo SingletonService2;
. -
When we read
Guid Id
from both instances it should besame
as two instances are same per application life. -
Now let's compare the object reference of two
SingletonServiceDemo
instance usingObject.ReferenceEquals(SingletonService1, SingletonService2)
. This should returnTrue
.
Guid from Singleton Service 1 : 7625a180-56d6-4729-873c-04127e16917a
Guid from Singleton Service 2 : 7625a180-56d6-4729-873c-04127e16917a
Singleton Dependency SingletonService1 Equals SingletonService2 : True
Captive Dependency
A service should not depend on a service with lifetime shorter than its own. This is called Dependency Captivity. For example, a service registered with singleton lifetime should not depend on transient service. Captive Dependency will make service to live longer than expected causing runtime bugs which are hard to track down.
A service is said to have safe dependency when it has a dependency of lifetime same or above its own lifetime. Here is a table describing the safe dependency.
Safe Dependencies | Transient | Scoped | Singleton |
---|---|---|---|
Transient | ✅ | ✅ | ✅ |
Scoped | ❌ | ✅ | ✅ |
Singleton | ❌ | ❌ | ✅ |
By default, in the development environment, resolving a service from another service with a longer lifetime throws an exception. For more information, see Scope Validation.
Scope Validation
When the app runs in the Development
environment and calls CreateDefaultBuilder
to build the host, the default service provider performs checks to verify that:
- Scoped services aren't resolved from the root service provider.
- Scoped services aren't injected into singletons.
The root service provider is created when BuildServiceProvider
is called. The root service provider's
lifetime corresponds to the app's lifetime when the provider starts with the app and is disposed when the app shuts down.
Scoped services are disposed by the container that created them. If a scoped service is created in the root container, the service's lifetime is
effectively promoted to singleton because it's only disposed by the root container when the app shuts down. Validating service scopes catches
these situations when BuildServiceProvider
is called.
Scope Validation can be enabled explicitly on build using ValidateOnBuild
or in all environment using
ValidateScopes
options inside UseDefaultServiceProvider
.
Code Sample - Dependency Injection Scope Validation
Summary
In this article, we learn't about different lifetimes
in Dependency Injection in .NET and when to use
what lifetime for registering services. We also understood what is captive dependency and how to avoid it by validating the scopes.