Dependency Injection: AddSingleton, AddScoped, AddTransient Differences

Dependency Injection: AddSingleton, AddScoped, AddTransient Differences

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.

💡
This is for example purposes only. In reality, requests like these are done in different parts of the codebase.
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

Singleton never changes, scoped changes changes only after a new scope (HTTP request), transient always changes

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.