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
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 ()
{
_service1 = service1;
_service2 = service2;
}
{
Content();
}
}
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
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>();
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
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
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
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->Singleton
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.
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
C# Type Checking Explained: typeof vs GetType() vs is
Learn when to use typeof, GetType(), and is in C#. Understand exact type checks, inheritance, and safe type testing clearly.
C# Version Numbers Explained: C# vs .NET Framework and Why “C# 3.5” Is Incorrect
Learn the correct C# version numbers, how they map to .NET releases, and why terms like C# 3.5 are inaccurate and confusing.
C# foreach Closure Behavior Explained: Why Loop Variables Were Reused
Learn why C# foreach loop variables once caused closure bugs, how the compiler handled scope, and what changed in newer C# versions.