Question
AddTransient vs AddScoped vs AddSingleton in ASP.NET Core Dependency Injection
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.
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
service1andservice2will usually have differentGuidvalues.- Each resolution creates a new object.
If registered as Scoped
- During the same request,
service1andservice2will usually have the sameGuidvalue. - During the next request, a new instance is created.
If registered as Singleton
service1andservice2will have the sameGuidvalue.- 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
- A browser sends an HTTP request to
/Demo/Index. - ASP.NET Core creates a request scope for that HTTP request.
- The framework starts building
DemoController. - It sees that the controller needs two
IRequestTrackerdependencies. - Because the service is registered as scoped, the container creates one
RequestTrackerinstance for this request scope. - That same instance is used for both
tracker1andtracker2. - The action runs and prints the same
Guidtwice. - The request ends.
- The scoped instance is disposed if needed.
- A new request creates a new scope and gets a new
RequestTrackerinstance.
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
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:
Singletonshould not depend onScopedSingletonshould not depend onTransientif 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:
AppDbContextis scopedReportCacheServiceis 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
| Lifetime | Instance creation | Reused within one request? | Reused across requests? | Typical use |
|---|---|---|---|---|
Transient | Every time requested | No | No | Stateless helpers, formatters |
Scoped | Once per scope/request | Yes | No | DbContext, request services |
Singleton | Once per application | Yes | Yes | Shared caches, config, metadata |
Transient vs Scoped
| Question | Transient | Scoped |
|---|---|---|
| New object each injection? | Yes | No, same within a request |
| Good for request state? | Usually no | Yes |
Common for EF DbContext? | No | Yes |
| Easy to use for stateless helpers? | Yes | Sometimes |
Scoped vs Singleton
| Question | Scoped | Singleton |
|---|---|---|
| Lives for one request? | Yes | No |
| Shared between users? | No | Yes |
| Safe for mutable request data? | Yes | Usually no |
| Must be thread-safe? | Less critical than singleton | Yes, 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.
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 timeScoped= same object during one requestSingleton= 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->ScopedScoped->SingletonTransient->ScopedTransient->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.
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
Guidproperty. - 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.
Keep learning
Related questions
Best Way to Repeat a Character in C#: Building Repeated Strings Efficiently
Learn the best way to repeat a character in C#, compare StringBuilder, string concatenation, and simpler built-in options.
C# Access Modifiers and static: public, private, protected, and Defaults
Learn how C# public, private, protected, and default access control visibility, and how static differs from instance members.
C# Array Initialization Syntaxes Explained
Learn all common C# array initialization syntaxes with examples, rules, comparisons, and mistakes beginners often make.