Question

AddTransient vs AddScoped vs AddSingleton in ASP.NET Core Dependency Injection

csharpasp.net-core.net-core

Question

In ASP.NET Core dependency injection, I can register a service in ConfigureServices using either AddTransient or AddScoped, and both registrations seem to work.

What is the difference between services.AddTransient(...) and services.AddScoped(...) in ASP.NET Core?

public void ConfigureServices(IServiceCollection services)
{
    // Add application services.
    services.AddTransient<IEmailSender, AuthMessageSender>();
    services.AddScoped<IEmailSender, AuthMessageSender>();
}

I would also like to understand how these compare with AddSingleton, and when each lifetime should be used.

Short Answer

By the end of this page, you will understand service lifetimes in ASP.NET Core dependency injection: Transient, Scoped, and Singleton. You will learn how often each service instance is created, when to use each one, what problems can happen if you choose the wrong lifetime, and how these lifetimes are used in real ASP.NET Core applications.

Concept

ASP.NET Core has a built-in dependency injection container. When you register a service, you are not only saying what implementation to use, but also how long that object should live.

The three main service lifetimes are:

  • AddTransient: create a new instance every time the service is requested.
  • AddScoped: create one instance per scope. In web apps, that usually means one instance per HTTP request.
  • AddSingleton: create one instance for the entire application lifetime.

That means these registrations are not equivalent:

services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddScoped<IEmailSender, AuthMessageSender>();
services.AddSingleton<IEmailSender, AuthMessageSender>();

They all resolve IEmailSender to AuthMessageSender, but they differ in instance reuse.

Why this matters

Choosing the right lifetime affects:

  • Correctness: some services must keep request-specific data, while others must not.
  • Performance: creating too many objects can be wasteful.
  • Safety: sharing the same object across requests can cause bugs if it holds mutable state.
  • Resource management: database contexts and request data usually need a scoped lifetime.

Typical meaning of each lifetime

  • Transient: good for lightweight, stateless services.
  • Scoped: good for request-based work, such as database access or per-request business logic.
  • Singleton: good for shared, reusable, thread-safe services such as configuration readers, caches, or pure utility services.

Important note

In ASP.NET Core, a scope usually means an HTTP request. So if three objects during the same request ask for the same scoped service, they all get the same instance. On the next request, a new scoped instance is created.

Mental Model

Think of service lifetimes like getting tools from a workshop:

  • Transient: every time you ask for a screwdriver, the workshop gives you a brand new screwdriver.
  • Scoped: during one job, everyone on your team shares the same screwdriver for that job only. For the next job, you get a different one.
  • Singleton: the entire workshop shares one screwdriver forever.

Now imagine that screwdriver keeps notes written on it:

  • If it is Transient, each user gets a fresh clean tool.
  • If it is Scoped, everyone working on the same task sees the same notes.
  • If it is Singleton, all tasks across the whole workshop share the same notes, which can become dangerous unless that shared state is intentional and safe.

This is why service lifetime is really about how much sharing you want.

Take Quiz

Syntax and Examples

Core syntax

services.AddTransient<IMyService, MyService>();
services.AddScoped<IMyService, MyService>();
services.AddSingleton<IMyService, MyService>();

You can also register concrete types directly:

services.AddTransient<MyService>();
services.AddScoped<MyService>();
services.AddSingleton<MyService>();

Example: seeing the difference

public interface IOperationService
{
    Guid OperationId { get; }
}

public class OperationService : IOperationService
{
    public Guid OperationId { get; } = Guid.NewGuid();
}

Register it:

services.AddTransient<IOperationService, OperationService>();

Use it in a controller:

public class HomeController : Controller
{
    private readonly IOperationService _service1;
    private readonly IOperationService _service2;

    public HomeController(IOperationService service1, IOperationService service2)
    {
        _service1 = service1;
        _service2 = service2;
    }

    public IActionResult Index()
    {
        return Content($"Service1: {_service1.OperationId}\nService2: {_service2.OperationId}");
    }
}

If registered as Transient

  • service1 and service2 will usually have different Guid values.
  • Each resolution creates a new object.

If registered as Scoped

  • During the same request, service1 and service2 will usually have the same Guid value.
  • During the next request, a new instance is created.

If registered as Singleton

  • service1 and service2 will have the same Guid value.
  • The same instance is reused for all requests for the whole app lifetime.

Example use cases

services.AddTransient<IEmailFormatter, EmailFormatter>();
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<AppDbContext>();
services.AddSingleton<ICacheService, MemoryCacheService>();

Explanation:

  • IEmailFormatter: stateless formatting logic, so transient is often fine.
  • IOrderService: request-based business logic, often scoped.
  • AppDbContext: should usually be scoped.
  • ICacheService: shared cache, often singleton if implemented safely.

Step by Step Execution

Consider this code:

public interface IRequestTracker
{
    Guid Id { get; }
}

public class RequestTracker : IRequestTracker
{
    public Guid Id { get; } = Guid.NewGuid();
}

Registration:

services.AddScoped<IRequestTracker, RequestTracker>();

Controller:

public class DemoController : Controller
{
    private readonly IRequestTracker _tracker1;
    private readonly IRequestTracker _tracker2;

    public DemoController(IRequestTracker tracker1, IRequestTracker tracker2)
    {
        _tracker1 = tracker1;
        _tracker2 = tracker2;
    }

    public IActionResult Index()
    {
        return Content($"Tracker1: {_tracker1.Id}\nTracker2: {_tracker2.Id}");
    }
}

What happens step by step

  1. A browser sends an HTTP request to /Demo/Index.
  2. ASP.NET Core creates a request scope for that HTTP request.
  3. The framework starts building DemoController.
  4. It sees that the controller needs two IRequestTracker dependencies.
  5. Because the service is registered as scoped, the container creates one RequestTracker instance for this request scope.
  6. That same instance is used for both tracker1 and tracker2.
  7. The action runs and prints the same Guid twice.
  8. The request ends.
  9. The scoped instance is disposed if needed.
  10. A new request creates a new scope and gets a new RequestTracker instance.

If it were Transient

Step 5 would be different: the container would create a new object for tracker1 and another new object for tracker2.

If it were Singleton

The instance would likely be created once, then reused for every request until the application stops.

Real World Use Cases

When to use Transient

Use transient for lightweight services that:

  • do not keep state between calls
  • are cheap to create
  • behave like helpers or formatters

Examples:

  • email body formatter
  • string manipulation service
  • simple mapper
  • calculation helper

When to use Scoped

Use scoped for services that should stay consistent during one request.

Examples:

  • Entity Framework DbContext
  • unit-of-work style services
  • current user context
  • request-specific business services
  • validation pipelines that use request data

In a web request, you usually want all code in that request to share the same database context and request-scoped services.

When to use Singleton

Use singleton for services that:

  • are shared globally
  • are expensive to create
  • are stateless or carefully thread-safe

Examples:

  • configuration providers
  • in-memory caching services
  • reusable metadata providers
  • background coordination services

Example from a web app

services.AddScoped<AppDbContext>();
services.AddScoped<IProductService, ProductService>();
services.AddTransient<IEmailTemplateRenderer, EmailTemplateRenderer>();
services.AddSingleton<IClock, SystemClock>();

This setup means:

  • one database context per request
  • one product service per request
  • a fresh email renderer when needed
  • one clock service for the whole app
Take Quiz

Real Codebase Usage

In real projects, service lifetimes are usually chosen based on state and dependency rules.

Common patterns

Guard clause thinking

A useful rule is:

  • if the service holds request data, it is probably scoped
  • if the service is stateless and cheap, it is probably transient
  • if the service is shared application-wide, it may be singleton

Database access

A common pattern is:

services.AddDbContext<AppDbContext>();
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IUserService, UserService>();

This works well because repositories and services often depend on AppDbContext, which is scoped.

Validation and business logic

Many business services are scoped because they:

  • read the current user
  • use a scoped DbContext
  • participate in one request's transaction or workflow

Stateless helpers

Formatting or transformation classes are often transient:

services.AddTransient<ISlugGenerator, SlugGenerator>();
services.AddTransient<IEmailFormatter, EmailFormatter>();

Shared app-wide services

Singleton is common for:

  • caching abstractions
  • lookup tables loaded once
  • expensive reusable factories

Example:

services.AddSingleton<ICurrencyMetadataProvider, CurrencyMetadataProvider>();

Important lifetime rule

A service should generally not depend on a shorter-lived service.

For example:

  • Singleton should not depend on Scoped
  • Singleton should not depend on Transient if it stores that instance long-term

The most common error is injecting a scoped service like DbContext into a singleton. In ASP.NET Core, this usually causes a runtime error because the singleton outlives the request scope.

Background services note

Hosted services are often singleton-like. If they need scoped services, they usually create a scope manually using IServiceScopeFactory.

Common Mistakes

1. Using Singleton for a service with request-specific state

Broken example:

public class CurrentUserService
{
    public string? UserId { get; set; }
}

services.AddSingleton<CurrentUserService>();

Why this is a problem:

  • all users and requests share the same instance
  • one request can overwrite another request's data

Use Scoped instead if the state belongs to one request.

2. Injecting a scoped service into a singleton

Broken example:

public class ReportCacheService
{
    private readonly AppDbContext _db;

    public ReportCacheService(AppDbContext db)
    {
        _db = db;
    }
}

services.AddDbContext<AppDbContext>();
services.AddSingleton<ReportCacheService>();

Why this is a problem:

  • AppDbContext is scoped
  • ReportCacheService is singleton
  • the singleton lives longer than the scoped dependency

Fixes:

  • make the service scoped if appropriate, or
  • create scopes manually in special cases

3. Assuming Scoped means one instance per controller

It does not. In web apps, scoped usually means one instance per HTTP request, not per controller or per method.

4. Registering the same service multiple times without meaning to

Example:

services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddScoped<IEmailSender, AuthMessageSender>();

For a single IEmailSender injection, the last registration is typically the one resolved.

That means this is usually not how you compare lifetimes in practice. You normally choose one lifetime for a given service registration.

5. Choosing Transient for expensive objects unnecessarily

If a service is expensive to create and safely shareable, making it transient can hurt performance.

6. Choosing Singleton for mutable non-thread-safe services

A singleton may be used by many requests at the same time. If it changes internal state, it must be thread-safe.

Comparisons

LifetimeInstance creationReused within one request?Reused across requests?Typical use
TransientEvery time requestedNoNoStateless helpers, formatters
ScopedOnce per scope/requestYesNoDbContext, request services
SingletonOnce per applicationYesYesShared caches, config, metadata

Transient vs Scoped

QuestionTransientScoped
New object each injection?YesNo, same within a request
Good for request state?Usually noYes
Common for EF DbContext?NoYes
Easy to use for stateless helpers?YesSometimes

Scoped vs Singleton

QuestionScopedSingleton
Lives for one request?YesNo
Shared between users?NoYes
Safe for mutable request data?YesUsually no
Must be thread-safe?Less critical than singletonYes, if mutable/shared

Short rule of thumb

  • Use Transient for simple stateless services.
  • Use Scoped for request-based services and database work.
  • Use Singleton only for shared, thread-safe, app-wide services.
Take Quiz

Cheat Sheet

// New instance every time the service is requested
services.AddTransient<IMyService, MyService>();

// One instance per HTTP request (in most ASP.NET Core apps)
services.AddScoped<IMyService, MyService>();

// One instance for the whole application lifetime
services.AddSingleton<IMyService, MyService>();

Quick rules

  • Transient = fresh object each time
  • Scoped = same object during one request
  • Singleton = same object for all requests

Good defaults

  • Stateless helper: Transient
  • Database context: Scoped
  • Request business service: Scoped
  • Shared cache/config provider: Singleton

Important dependency rule

Prefer depending on the same or longer lifetime, not a shorter one.

Safe direction examples:

  • Scoped -> Scoped
  • Scoped -> Singleton
  • Transient -> Scoped
  • Transient -> Singleton

Unsafe/common problem:

  • Singleton -> Scoped

Edge cases

  • In non-web apps, a scope is not automatically an HTTP request.
  • Multiple registrations of the same service type can change what gets resolved.
  • Singleton services must be safe for concurrent access.

FAQ

What is the difference between AddTransient and AddScoped in ASP.NET Core?

AddTransient creates a new instance every time the service is requested. AddScoped creates one instance per scope, which is usually one HTTP request in ASP.NET Core web apps.

When should I use AddScoped?

Use AddScoped for services that should stay consistent during a single request, especially services that use DbContext or request-specific data.

When should I use AddSingleton?

Use AddSingleton for shared application-wide services that are stateless or thread-safe, such as caches, configuration helpers, or metadata providers.

Is Entity Framework DbContext transient, scoped, or singleton?

It is typically scoped. That allows one context per request and avoids sharing it across unrelated requests.

Can I inject a scoped service into a singleton?

Usually no. This causes lifetime mismatch problems because the singleton lives longer than the scoped service.

Why do both registrations seem to work in my example?

Because both are valid registrations. But they do not behave the same way. Also, if you register the same service twice, the final resolved registration is typically the last one for normal single-service injection.

Is Transient always safer than Singleton?

Not always. Transient avoids shared state, but it can create unnecessary objects. Singleton is efficient for shared thread-safe services, but dangerous for mutable request-specific state.

Take Quiz

Mini Project

Description

Build a small ASP.NET Core example that shows how Transient, Scoped, and Singleton behave during a web request. This project is useful because it turns an abstract DI concept into something visible: you will see different Guid values depending on the service lifetime.

Goal

Create a page or endpoint that prints instance IDs for transient, scoped, and singleton services so you can compare how often each one is created.

Requirements

  • Create one interface and implementation that exposes a Guid property.
  • Register the same kind of service three times using transient, scoped, and singleton lifetimes.
  • Inject the services into a controller or minimal API endpoint.
  • Output the IDs so the lifetime behavior is easy to compare.
  • Refresh the endpoint multiple times and observe which IDs change.
Take Quiz

Keep learning

Related questions

View more