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

Introduction
When building modern ASP.NET Core applications, Dependency Injection (DI) is a key design pattern that helps you write clean, maintainable, and testable code.
But one question often confuses developers:
❓ What’s the difference between AddSingleton, AddScoped, and AddTransient in ASP.NET Core?
In this article, we’ll break down each service lifetime, explain when to use which, and provide real C# examples you can copy and paste into your projects.
What Is Dependency Injection?
Dependency Injection is a technique where the framework automatically provides (injects) instances of required services into your classes.
In ASP.NET Core, you register services in the DI container (usually in Program.cs) like this:
builder.Services.AddSingleton<IMyService, MyService>();
builder.Services.AddScoped<IMyService, MyService>();
builder.Services.AddTransient<IMyService, MyService>();
But what’s the difference?
Each of these methods defines how long the created service instance should live — in other words, its lifetime.
🧩 AddSingleton — One Instance for the Entire Application
A Singleton service is created once and shared across the entire application lifetime.
That means:
- The same instance is reused for every request and every dependency.
- The instance lives until the application stops.
✅ When to use AddSingleton
Use AddSingleton for:
- Stateless services
- Configuration providers
- Logging
- Caching layers
❌ Avoid when
- The service holds request-specific data
- The service depends on a scoped or transient dependency
💡 Example
public interface IGuidService
{
string GetGuid();
}
public class SingletonGuidService : IGuidService
{
private readonly string _guid;
public SingletonGuidService()
{
_guid = Guid.NewGuid().ToString();
}
public string GetGuid() => _guid;
}
// In Program.cs
builder.Services.AddSingleton<IGuidService, SingletonGuidService>();
Each time you request IGuidService, you get the same instance and the same GUID value.
🌐 AddScoped – One Instance per HTTP Request
AddScoped creates one instance per HTTP request.
All components handling that same request share the same instance, but a new instance is created for each new request.
This makes it ideal for request-specific data and database operations.
✅ Best For
- Database contexts (
DbContextin Entity Framework Core) - Unit of Work pattern
- Request-specific data (services, repositories)
💡 Example
public interface IRequestService
{
Guid GetRequestId();
}
public class RequestService : IRequestService
{
private readonly Guid _requestId = Guid.NewGuid();
public Guid GetRequestId() => _requestId;
}
// Register in Program.cs
builder.Services.AddScoped<IRequestService, RequestService>();
// Example controller
[ApiController]
[Route("api/[controller]")]
public class ScopedExampleController : ControllerBase
{
private readonly IRequestService _service1;
private readonly IRequestService _service2;
public ScopedExampleController(IRequestService service1, IRequestService service2)
{
_service1 = service1;
_service2 = service2;
}
[HttpGet]
public IActionResult Get()
{
return Ok(new
{
FirstInstance = _service1.GetRequestId(),
SecondInstance = _service2.GetRequestId()
});
}
}
🧠 What you’ll see:
Within the same HTTP request, both FirstInstance and SecondInstance will return the same GUID. When you make another request, a new GUID will appear — confirming a new instance was created.
⚡ AddTransient — A New Instance Every Time
An AddTransient service is created each time it’s requested from the Dependency Injection (DI) container.
Let’s imagine you’re building an email notification system in your ASP.NET Core application.
You might have a lightweight EmailFormatter service that prepares email content before it’s sent.
This service:
- Doesn’t store state
- Performs quick, one-off operations
- Should be short-lived (you don’t want to reuse old email content or data)
That means:
- Every injection or method call gets a brand new object.
- This is ideal for lightweight, stateless, and non-expensive services.
✅ When to Use AddTransient
Use AddTransient for:
- Utility or helper classes
- Small operations
- Short-lived, non-expensive services
❌ Avoid When
- The service performs expensive initialization
- The service holds shared state or data that needs to persist between calls
💡 Example
public interface IEmailFormatter
{
string FormatWelcomeEmail(string userName);
}
public class EmailFormatter : IEmailFormatter
{
public string FormatWelcomeEmail(string userName)
{
return $"Hi {userName}, welcome to our platform! 🎉";
}
}
// Program.cs
builder.Services.AddTransient<IEmailFormatter, EmailFormatter>();
🚀 Why AddTransient Works Here
Each email send operation gets a fresh EmailFormatter. No state is reused between emails. It’s lightweight, stateless, and safe to recreate on demand.
🚀 Summary Table
Here’s a quick reference table summarizing the differences between the three main lifetimes in ASP.NET Core Dependency Injection:
| Lifetime | Created... | Shared Within Request | Shared Across Requests | Common Use Cases |
|---|---|---|---|---|
| Singleton | Once (application lifetime) | ✅ Yes | ✅ Yes | Logging, configuration, caching |
| Scoped | Once per request | ✅ Yes | ❌ No | DbContext, request-based services |
| Transient | Every time it’s requested | ❌ No | ❌ No | Helpers, small utility or stateless services |
🧠 Key Takeaways
- Choose Singleton for shared, thread-safe, stateless services.
- Choose Scoped for request-level services (like DbContext).
- Choose Transient for lightweight, short-lived dependencies.
- Correct use of lifetimes ensures performance, stability, and predictable behavior in your ASP.NET Core applications.