· Sergiy Oliynyk · .NET · 7 min read
.NET Dependency Injection lifetimes
Deep dive into .NET Dependency Injection lifetimes (Singleton vs Scoped vs Transient). Find out when use Singleton vs Scoped vs Transient.

Introduction
Nowadays Dependency Injection (DI) is an intrinsic part of every .NET web application. At first glance it’s quite simple: just register classes at startup using one of the extension methods: AddSingleton()
, AddScoped()
or AddTransient()
. Each of these methods registers a class for DI with corresponding lifetime. But which lifetime should we prefer? Let’s figure it out!
In the context of Dependency Injection a “service” means a class registered in DI container and instantiated by it.
Although I sometimes will mention different types of applications, the main concern of this article is web applications.
Lifetimes
The term “lifetime” in DI context is slightly deceptive: it doesn’t define the lifetime of an object but which instance will be passed to a dependent object: a new or already existing.
Transient
Instances of classes, registered with transient lifetime (using AddTransient()
method), are created each time they are requested. In other words, each object that depends on a transient service, will receive a unique instance of the service.
The transient lifetime is most suitable for lightweight, stateless services, that don’t create expensive resources and don’t allocate much memory. Examples: mappers, formatters, validators, utilities.
Scoped
Scoped lifetime services (AddScoped()
method) are created once per scope. What is a scope? For web applications, a scope is created for each web request. E.g., one instance will be created of a scoped lifetime service during each web request. Also, you can manually create a scope using CreateScope()
method of the IServiceScopeFactory
. If the application doesn’t create a scope automatically (for example, a console or background service application) you must manually create a scope using IServiceScopeFactory.CreateScope()
. Otherwise, a scoped service becomes a singleton.
The scoped lifetime is best for units of work tied to a web request (or scope in general). Scoped services can have state that they share with other services during a web request. Examples: EF Core DbContext, authentication/authorization handlers.
Singleton
Singleton lifetime services (AddSingleton()
method) are created only once during the lifetime of the application. All objects that depend on it will be provided with the same instance. Singleton services must be thread-safe. A singleton lifetime is used for stateless thread-safe objects (logging services) or stateful services that share data between requests (cache). Examples: ILogger<>, HybridCache.
Dependencies between services with different lifetimes
DI container doesn’t keep references to transient services, so they have the shortest life time - as soon as the service that asked for the transient service releases it (for example, by assigning null
to the variable/field that contains reference to the service or by becoming garbage itself) they become garbage that can be freed by Garbage Collector (GC). In case of scoped and singleton services DI container holds references to created objects. Scoped services released when the scope ends (end of a request for web applications). Singleton lives to the end of the application.
Because of this, a transient service can depend on transient, scoped and singleton services, and a scoped service can depend on scoped and singleton. In these cases the dependent services’ lifetime is always less or equal to the dependency service’s lifetime.
What if a singleton service depends on a transient or scoped services, or a scoped service depends on transient service? In all these cases the lifetime of dependencies will be extended to the lifetime of their owners (providing that dependent services hold references to their dependencies). It’s usually acceptable for transient dependencies because according to best practices a transient service should be a lightweight, stateless class. But there is always a risk that later somebody adds a state to a transient service, so, if it is possible, try not to pass transient dependencies to scoped and especially to singleton services.
Singleton dependency on scoped service is almost always a bad combination because scoped services usually share some state and don’t support concurrency. In Development
environment the default service provider throws an exception if scoped services are injected into singletons (since .NET Core 3.0).
For example, this code
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<MySingleton>();builder.Services.AddScoped<MyScoped>();
var app = builder.Build(); // InvalidOperationException
app.Run();
public class MySingleton{ public MySingleton(MyScoped scoped) {}}
public class MyScoped{}
will fail with an exception:
Service \ Depend on | Transient | Scoped | Singleton |
---|---|---|---|
Transient | ✔️ | ✔️ | ✔️ |
Scoped | ⚠️ | ✔️ | ✔️ |
Singleton | ⚠️ | ❌ | ✔️ |
✔️ - Safe to use
⚠️ - Use with caution (potential lifetime extension or performance issues)
❌ - Invalid (will throw an exception in Development)
If you need to use a scoped service from a singleton, you can create a scoped block in singleton’s method:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<MySingleton>();builder.Services.AddScoped<MyScoped>();
var app = builder.Build();app.Run();
public class MySingleton{ private readonly IServiceScopeFactory _serviceScopeFactory;
public MySingleton(IServiceScopeFactory serviceScopeFactory) { _serviceScopeFactory = serviceScopeFactory; } public void DoSomething() { using IServiceScope scope = _serviceScopeFactory.CreateScope(); // Create a scope var myScoped = scope.ServiceProvider.GetRequiredService<MyScoped>(); myScoped.DoSomething(); // End of the scope, myScoped will be available for GC }}
public class MyScoped{ public void DoSomething() { }}
In this code snippet a new instance of the MyScoped
class will be created each time when the MySingleton.DoSomething()
method is called. If you want to get the instance of MyScoped
tied to a web request scope you can use this approach instead:
public class MySingleton{ private readonly IHttpContextAccessor _httpContextAccessor;
public MySingleton(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public void DoSomething() { // Get the same instance of MyScoped as other objects during the same web request var myScoped = _httpContextAccessor.HttpContext?.RequestServices.GetRequiredService<MyScoped>(); myScoped?.DoSomething(); }}
Choosing the right lifetime
Singleton
Since singleton services exist in a single instance during the whole lifetime of an application, they are a good fit for sharing some global state between requests. Their implementation must support multi-threading access. Be careful not to use too much memory without need for such state since it can affect the available memory.
Scoped and Transient
In web applications difference between scoped and transient lifetime often is quite subtle. For example, a common request looks like:
sequenceDiagram CustomerController->>CustomerService: GetCustomers() CustomerService->>Database: SELECT * FROM Customers Database->>CustomerService: Customers CustomerService->>CustomerController: CustomerDto[]
The CustomerController
is instantiated by the framework, DI provides to controller an instance of the CustomerService
. In this case, there is no difference whether it is registered as transient or scoped because its lifetime tied to the controller’s lifetime. Also, the CustomerSevice
is used only from the CustomerController
. So, you can choose any of these two lifetimes (but keep consistent - use the same lifetime for all similar classes). The only rule I can propose in this case - since CustomerService
’s real lifetime is web request (i.e. scoped), it makes sense to register it as scoped for clarity.
However, if a class has some state that should be shared between several classes during a request it probably should be registered as a scoped service. Conversely, transient services are lightweight, stateless, and cheap to create.
Cloud environment
If you run your application on several instances behavior of transient and scoped services is the same. But singleton services are created for each instance.
In case of AWS Lambda (or Azure Function) transient services are the same. A scope in Lambda is equivalent to lifetime of Lambda. If you use lambdas to process web requests, each request will be processed by a separate lambda, so a scoped lifetime has the same meaning. Does it mean that a singleton has the same lifetime as scoped when an application is an AWS Lambda (since a lifetime of lambda is a web request)? Not exactly. Each Lambda is running in a container and this container can be stopped after lambda finished. But if there are other requests to the same Lambda, containers will be reused (to eliminate cold start delay). So, singleton services persist across invocations if the container is reused, but this reuse is not guaranteed. It means that it is better not to use singleton services in Lambdas at all (since their lifetime is unpredictable).
Links
Dependency injection - .NET | Microsoft Learn