Dependency Injection: AddSingleton, AddScoped, AddTransient Differences
Table of contents
Dependency Injection is a design pattern used to achieve loose coupling and to increase the maintainability in our code. Using dependency injection in C# and .NET is pretty easy. We are provided with the following methods to register our services: AddSingleton
, AddScoped
, AddTransient
. However, it is important to know the differences between these three methods to be able to manage and configure the lifetimes of the services within our applications.
Brief Rundown
AddSingleton: A single instance of a service will be created and reused for every request, throughout the application's lifetime.
- 💡Best used for services that require to maintain a shared state throughout the entire application.
- ❗Need to ensure mutability of the shared state. Need to also be careful with thread safety of the services.
AddScoped: A new instance of the service will be created once per request within the scope or context. In the case of ASP.NET, a new instance would be created per each HTTP request (the scope)
- 💡Best used for services that require to maintain state throughout a request (e.g. user information)
AddTransient: A new instance of the service will be created for each request. Each time a service is resolved, a new instance will be provided.
- 💡Best used for stateless services where a new instance is required for each request.
- ❗Entails heavy initialization, and might cause unnecessary overhead.
Usage
We will dive deeper on the life cycles of the dependency injections. In this article, we will:
Create an ASP.NET Core Web API app that uses dependency injection
Write several interfaces and corresponding implementations
Use service lifetime and scoping for DI
We are going to create an ASP.NET Core Web API minimal app to better illustrate the differences between the lifetime and scope of the DI methods. This is loosely based on the Microsoft docs.
Code
To start, let's create and define our interfaces.
public interface IExampleService
{
Guid Value { get; }
}
The IExampleService
interface contains:
- A
Guid Id
property that represents a unique identifier of the service.
Next, create the subinterfaces for IExampleService
that will represent the different lifetimes, and will be invoked by their corresponding methods. Create a class ExampleService
that implement the subinterfaces, and will provide a unique GUID.
public interface ISingletonExampleService : IExampleService { }
public interface IScopedExampleService : IExampleService { }
public interface ITransientExampleService : IExampleService { }
public class ExampleService : ISingletonExampleService, IScopedExampleService, ITransientExampleService
{
public Guid Id { get; } = Guid.NewGuid();
}
In an ASP.NET Core minimal API, add each type to the container according to its named lifetime.
In our /example
endpoint, define the dependencies to inject. To better understand the differences, we inject two instances of each service. Create a log to show the ID of the services.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<ISingletonExampleService>(new ExampleService());
builder.Services.AddScoped<IScopedExampleService, ExampleService>();
builder.Services.AddTransient<ITransientExampleService, ExampleService>();
var app = builder.Build();
app.MapGet("/example", (ISingletonExampleService exampleSingle1, ISingletonExampleService exampleSingle2,
IScopedExampleService exampleScoped1, IScopedExampleService exampleScoped2,
ITransientExampleService exampleTransient1, ITransientExampleService exampleTransient2) =>
{
return $"Singleton instance: {exampleSingle1.Id}\n" +
$"Singleton instance: {exampleSingle2.Id}\n\n" +
$"Scoped instance 1: {exampleScoped1.Id}\n" +
$"Scoped instance 2: {exampleScoped2.Id}\n\n" +
$"Transient instance 1: {exampleTransient1.Id}\n" +
$"Transient instance 2: {exampleTransient2.Id}";
});
app.Run();
Result
What we can observe whenever we create a new HTTP request (by pressing the Refresh button):
Singleton: Never changes
Scoped: Changes only after a new HTTP request (new scope)
Transient: Always changes
The code is available on Github.
Remarks
Understanding the different types of service lifetimes is important to successfully manage the dependencies and ensure correct behavior within our C# applications. We must always consider the requirements of the project and the attributes of the services in order to choose the proper service lifetime for each registration.