Skip to content

AddTransient vs AddScoped vs AddSingleton in ASP.NET Core

When you register a service with the ASP.NET Core dependency injection container, you also choose its lifetime. The lifetime decides when the container creates a new instance and how long that instance is reused before it is disposed.

MethodLifetimeA new instance is created...
AddTransientTransientEvery time the service is requested from the container
AddScopedScopedOnce per scope, and reused within that scope. In an ASP.NET Core web app, a scope is one HTTP request.
AddSingletonSingletonOnce for the whole application; every request and every class shares it

Scoped does not mean "shared across all requests". A scoped service is created once per scope and reused only inside that scope. ASP.NET Core creates a new scope for every HTTP request, so each request gets its own instance, which is shared by all the classes that take part in handling that request, and is disposed when the request ends. The service shared across all requests is the singleton.

The rest of this article proves each of these rules with a small application. All outputs come from running it on .NET 8.

A simple analogy: a hotel

  • Transient is a paper cup at the water cooler: every time you want a drink, you take a new one.
  • Scoped is your room key: you get one at check-in and use it for everything during your stay (the scope). The next guest gets a different key.
  • Singleton is the hotel lobby: there is exactly one, and every guest uses the same one for as long as the hotel is open.

The demo: an object that reports its ID

To see when instances are created, we use a class that gets a new Guid when it is constructed, and prints a message when it is disposed. We register the same class three times, once for each lifetime, behind three interfaces:

public interface IOperation { Guid Id { get; } }
public interface ITransientOperation : IOperation { }
public interface IScopedOperation : IOperation { }
public interface ISingletonOperation : IOperation { }

public class Operation : ITransientOperation, IScopedOperation, ISingletonOperation, IDisposable
{
    public Guid Id { get; } = Guid.NewGuid();      // a new ID for every new instance

    public void Dispose() => Console.WriteLine($"   Disposed {Id.ToString()[..8]}");
}

A second service, OrderService, also depends on all three. This lets us check whether different classes in the same request receive the same instance:

public class OrderService
{
    public OrderService(ITransientOperation transient, IScopedOperation scoped, ISingletonOperation singleton)
    {
        Transient = transient;
        Scoped = scoped;
        Singleton = singleton;
    }

    public ITransientOperation Transient { get; }
    public IScopedOperation Scoped { get; }
    public ISingletonOperation Singleton { get; }
}

Registration in Program.cs

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();

builder.Services.AddTransient<ITransientOperation, Operation>();
builder.Services.AddScoped<IScopedOperation, Operation>();
builder.Services.AddSingleton<ISingletonOperation, Operation>();
builder.Services.AddScoped<OrderService>();

var app = builder.Build();
app.MapDefaultControllerRoute();
app.Run();

The controller

The controller asks for each lifetime twice, plus OrderService, and prints the first 8 characters of every ID:

public class LifetimeController : Controller
{
    private readonly ITransientOperation _transient1, _transient2;
    private readonly IScopedOperation _scoped1, _scoped2;
    private readonly ISingletonOperation _singleton1, _singleton2;
    private readonly OrderService _orderService;

    public LifetimeController(ITransientOperation transient1, ITransientOperation transient2,
                              IScopedOperation scoped1, IScopedOperation scoped2,
                              ISingletonOperation singleton1, ISingletonOperation singleton2,
                              OrderService orderService)
    {
        _transient1 = transient1; _transient2 = transient2;
        _scoped1 = scoped1; _scoped2 = scoped2;
        _singleton1 = singleton1; _singleton2 = singleton2;
        _orderService = orderService;
    }

    public IActionResult Index()
    {
        static string S(IOperation o) => o.Id.ToString()[..8];
        return Content(
            $"Transient: controller #1 {S(_transient1)} | controller #2 {S(_transient2)} | OrderService {S(_orderService.Transient)}\n" +
            $"Scoped:    controller #1 {S(_scoped1)} | controller #2 {S(_scoped2)} | OrderService {S(_orderService.Scoped)}\n" +
            $"Singleton: controller #1 {S(_singleton1)} | controller #2 {S(_singleton2)} | OrderService {S(_orderService.Singleton)}\n");
    }
}

The result: two separate requests

GET /Lifetime (request 1)
Transient: controller #1 f4796c38 | controller #2 d40f9203 | OrderService b6bd4a33
Scoped:    controller #1 72dfcf6a | controller #2 72dfcf6a | OrderService 72dfcf6a
Singleton: controller #1 a44c6051 | controller #2 a44c6051 | OrderService a44c6051
GET /Lifetime (request 2)
Transient: controller #1 06795a9b | controller #2 707bf40d | OrderService 92992c9b
Scoped:    controller #1 2109facb | controller #2 2109facb | OrderService 2109facb
Singleton: controller #1 a44c6051 | controller #2 a44c6051 | OrderService a44c6051

Reading the result row by row:

LifetimeWithin one requestBetween requestsInstances created in these 2 requests
TransientDifferent for every injection, even in the same controllerDifferent6
ScopedSame for the controller and OrderServiceDifferent (request 1: 72dfcf6a, request 2: 2109facb)2 (one per request)
SingletonSameSame (a44c6051 in both)1 (for the whole app)
Request 1 Request 2 Transient new each time Scoped one per request Singleton one per app Controller #1 Controller #2 OrderService Controller #1 Controller #2 OrderService instance 1 instance 2 instance 3 instance 4 instance 5 instance 6 Controller #1 Controller #2 OrderService Controller #1 Controller #2 OrderService 1 instance for request 1 1 instance for request 2 Controller #1 Controller #2 OrderService Controller #1 Controller #2 OrderService 1 instance for the whole app
Who shares an instance: transient never shares, scoped shares within one request, singleton shares across all requests.

This is exactly why the scoped lifetime is so useful: all classes involved in one request work with the same instance (for example the same DbContext, so they share one database transaction), but no data leaks from one user's request to another's.

What exactly is a scope?

A scope is a "container within the container" that holds scoped instances and is disposed as a unit. ASP.NET Core creates one automatically for every HTTP request. Outside a web request (in a console app, a background service or a unit test) you create scopes yourself:

Console.WriteLine("--- scope 1");
using (var scope = app.Services.CreateScope())
{
    var a = scope.ServiceProvider.GetRequiredService<IScopedOperation>();
    var b = scope.ServiceProvider.GetRequiredService<IScopedOperation>();
    var t = scope.ServiceProvider.GetRequiredService<ITransientOperation>();
    Console.WriteLine($"   scoped a={a.Id.ToString()[..8]} b={b.Id.ToString()[..8]} same={ReferenceEquals(a, b)}; transient={t.Id.ToString()[..8]}");
    Console.WriteLine("   end of scope 1:");
}

Console.WriteLine("--- scope 2");
using (var scope = app.Services.CreateScope())
{
    var c = scope.ServiceProvider.GetRequiredService<IScopedOperation>();
    Console.WriteLine($"   scoped c={c.Id.ToString()[..8]}");
    Console.WriteLine("   end of scope 2:");
}
Output
--- scope 1
   scoped a=2a71fcb1 b=2a71fcb1 same=True; transient=58469caa
   end of scope 1:
   Disposed 58469caa
   Disposed 2a71fcb1
--- scope 2
   scoped c=e412e126
   end of scope 2:
   Disposed e412e126
  • Inside one scope, the scoped service is the same object (same=True).
  • A new scope gets a new scoped instance.
  • When the scope ends, the container disposes the scoped and transient instances it created in that scope (the Disposed lines). In a web app this happens automatically at the end of each request.
  • Singletons are disposed only when the application shuts down.

Which lifetime should I use?

LifetimeUse it forExamples
TransientLightweight, stateless services; anything that must never be sharedValidators, mappers, small calculators
ScopedServices that hold per-request state, or must be shared by all classes within one requestDbContext (registered as scoped by AddDbContext), repositories, unit of work, the current user's context
SingletonServices that are expensive to create, or hold application-wide state, and are thread-safeIn-memory caches, configuration objects, IHttpClientFactory, logging

Singletons must be thread-safe. One singleton instance serves every request at the same time. Never store per-user or per-request data in a singleton's fields, and protect any data it changes (for example with ConcurrentDictionary or locks).

The classic mistake: a captive dependency

A service must not depend on a service with a shorter lifetime. The most common case is a singleton that receives a scoped service:

public class ReportCache                     // registered as a singleton
{
    public ReportCache(IScopedOperation scoped) { }   // depends on a scoped service
}

builder.Services.AddSingleton<ReportCache>();

The singleton is created once and lives forever, so it would keep the scoped instance from the first request forever too, holding it "captive". With a DbContext, this means one database context shared by all users and threads, which leads to corrupted data and errors. In the Development environment, ASP.NET Core checks for this at start-up and refuses to run:

Error at start-up
System.AggregateException: Some services are not able to be constructed (Error while validating the service
descriptor 'ServiceType: ReportCache Lifetime: Singleton ImplementationType: ReportCache': Cannot consume
scoped service 'IScopedOperation' from singleton 'ReportCache'.)

This validation is off by default in Production, where the bug would silently happen. That's why it's important to run and test your app in Development.

Safe dependency directions:

A service with lifetime...can safely depend on
TransientTransient, Scoped, Singleton
ScopedTransient, Scoped, Singleton
SingletonSingleton (and transient services that have no scoped dependencies)

If a singleton or a BackgroundService really needs a scoped service, inject IServiceScopeFactory and create a short scope each time:

public class NightlyReportJob : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    public NightlyReportJob(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();   // a fresh DbContext for this run
        // ... use db ...
    }   // the scope (and the DbContext) is disposed here
}

Comparison table

PointTransientScopedSingleton
RegistrationAddTransientAddScopedAddSingleton
New instanceEvery time it's requestedOnce per scope (per HTTP request)Once per application
Shared within one requestNoYesYes
Shared between requestsNoNoYes
DisposedAt the end of the scope that created itAt the end of the scopeWhen the app shuts down
Thread safety neededRarelyRarelyYes
Typical useStateless helpersDbContext, repositoriesCaches, configuration

Interview questions

1. What is the difference between AddTransient, AddScoped and AddSingleton?

Transient creates a new instance every time the service is requested. Scoped creates one instance per scope, which in ASP.NET Core is one per HTTP request, shared by all classes within that request. Singleton creates one instance for the lifetime of the application, shared by all requests.

2. Is a scoped service shared between two users?

No. Each HTTP request has its own scope, so two requests (from the same or different users) get different instances. Only singletons are shared between requests.

3. Why is DbContext registered as scoped?

A DbContext tracks entities and is not thread-safe. Scoped gives each request its own context, shared by all repositories in that request so they work in the same unit of work, and disposes it when the request ends.

4. What is a captive dependency?

A longer-lived service holding a shorter-lived one, typically a singleton that depends on a scoped service. The scoped instance then lives as long as the singleton. ASP.NET Core detects this in Development with the error "Cannot consume scoped service ... from singleton ...".

5. How do you use a scoped service inside a singleton or background service?

Inject IServiceScopeFactory, create a scope with CreateScope(), resolve the scoped service from scope.ServiceProvider, and dispose the scope when finished.

6. Who disposes services created by the container?

The container. Scoped and transient disposable services are disposed when their scope ends; singletons when the application stops. Don't dispose injected services yourself.

Summary

  • Transient: a new instance every time it is requested.
  • Scoped: one instance per scope; in a web app, one per HTTP request, shared within that request only.
  • Singleton: one instance for the whole application, shared by every request; it must be thread-safe.
  • Never let a singleton depend on a scoped service; use IServiceScopeFactory when you need one.
  • The container disposes the instances it creates when their scope or the application ends.