Dependency Injection Lifetimes in .NET

Dependency Injection Lifetimes in .NET

dependency injection

2 Articles

Improve

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

  1. Introduction
  2. Transient
  3. Scoped
  4. Singleton
  5. Captive Dependency
  6. Scope Validation
  7. Summary

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

  1. TransientServiceDemo is registered in Program.cs as Transient service using services.AddTransient<TransientServiceDemo>();.
  2. Let's resolve and @inject TransientServiceDemo twice within a component as follows. @inject TransientServiceDemo TransientService1; and @inject TransientServiceDemo TransientService2;.
  3. When we read Guid Id from both instances it should be different as two instances are not same.
  4. Now let's compare the object reference of two TransientServiceDemo instance using Object.ReferenceEquals(TransientService1, TransientService2). This should return False.


Guid from Transient Service 1 : 95da4827-7c04-4ffd-acd3-328a6fdd9b0f

Guid from Transient Service 2 : 47feba77-355a-4a2a-b606-7a4f2484d2ff

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

  1. ScopedServiceDemo is registered in Program.cs as Scoped service using services.AddScoped<ScopedServiceDemo>();.
  2. Let's resolve and @inject ScopedServiceDemo twice within a component as follows. @inject ScopedServiceDemo ScopedService1; and @inject ScopedServiceDemo ScopedService2;.
  3. When we read Guid Id from both instances it should be same as two instances are same per scope (request).
  4. Now let's compare the object reference of two ScopedServiceDemo instance using Object.ReferenceEquals(ScopedService1, ScopedService2). This should return True.


Guid from Scoped Service 1 : e97d4c72-16bf-4bd2-b7a0-47bea9802d9a

Guid from Scoped Service 2 : e97d4c72-16bf-4bd2-b7a0-47bea9802d9a

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

  1. SingletonServiceDemo is registered in Program.cs as Singleton service using services.AddSingleton<SingletonServiceDemo>();.
  2. Let's resolve and @inject SingletonServiceDemo twice within a component as follows. @inject SingletonServiceDemo SingletonService1; and @inject SingletonServiceDemo SingletonService2;.
  3. When we read Guid Id from both instances it should be same as two instances are same per application life.
  4. Now let's compare the object reference of two SingletonServiceDemo instance using Object.ReferenceEquals(SingletonService1, SingletonService2). This should return True.


Guid from Singleton Service 1 : f6524fc3-1a47-45ae-973a-7b596e9a3f89

Guid from Singleton Service 2 : f6524fc3-1a47-45ae-973a-7b596e9a3f89

Singleton Dependency SingletonService1 Equals SingletonService2 : True

Lifetimes in Dependency Injection in DotNet

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.

Dependency Captivity in Dependency Injection in DotNet

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.

  • Dependency Injection
  • Lifetimes
  • Transient
  • Singleton
  • Scoped
  • Dependency Captivity
  • Captive Dependencies