Service lifetime defines how long an object created by the Dependency Injection container will exist.
| Lifetime | Object creation | Common use |
|---|---|---|
Transient |
Every time it is requested | Lightweight, stateless services |
Scoped |
Once per HTTP request | Business services, DbContext |
Singleton |
Once for the entire application | Shared, thread-safe services |
1. Transient
A new object is created every time the service is requested.
builder.Services.AddTransient<IEmailService, EmailService>();
Request 1:
Controller → Instance A
Service → Instance B
Request 2:
Controller → Instance C
Use it for:
- Lightweight services
- Stateless operations
- Formatting or calculation services
2. Scoped
One object is created per HTTP request. The same instance is reused throughout that request.
builder.Services.AddScoped<IProductService, ProductService>();
Request 1 → Instance A → reused within Request 1
Request 2 → Instance B → reused within Request 2
Use it for:
- Business services
- Repositories
- EF Core
DbContext
AddDbContext() registers DbContext as Scoped by default.
3. Singleton
Only one object is created and shared by every request until the application stops.
builder.Services.AddSingleton<IConfigService, ConfigService>();
Request 1 ─┐
Request 2 ─┼→ Same instance
Request 3 ─┘
Use it for:
- Application-wide configuration
- Thread-safe shared services
- Expensive objects that are safe to reuse
Do not store user-specific data inside a Singleton.
Important rule
Do not inject a Scoped service into a Singleton:
builder.Services.AddSingleton<ReportService>();
builder.Services.AddScoped<AppDbContext>();
A Singleton lives for the application lifetime, but AppDbContext should live for only one request. This creates a captive dependency.
Use:
builder.Services.AddScoped<ReportService>();
builder.Services.AddScoped<AppDbContext>();
Simple memory technique
- Transient: Every use
- Scoped: Every request
- Singleton: Entire application
What problems occur when a Singleton depends on a Scoped service?
When a Singleton depends on a Scoped service, the Scoped service can incorrectly remain alive for the Singleton’s entire lifetime. This is called a captive dependency.
Singleton: Entire application lifetime
└── Scoped service: Should live for one request
Incorrect example
builder.Services.AddSingleton<ReportService>();
builder.Services.AddScoped<AppDbContext>();
public class ReportService
{
private readonly AppDbContext _dbContext;
public ReportService(AppDbContext dbContext)
{
_dbContext = dbContext;
}
}
Here, ReportService is created once, but AppDbContext should be created separately for every request.
ASP.NET Core commonly detects this and throws an exception:
Cannot consume scoped service 'AppDbContext'
from singleton 'ReportService'.
Problems
1. Incorrect lifetime
The Scoped service may effectively behave like a Singleton instead of being created per request.
2. Concurrent access
Multiple requests may use the same Scoped instance simultaneously.
DbContext is not thread-safe, so this can cause concurrency exceptions and incorrect behaviour.
3. Stale data
A long-lived DbContext may continue tracking old entities and return outdated data.
4. Memory growth
A long-lived DbContext may keep tracking more entities, increasing memory usage.
5. Disposed-object errors
Depending on how the service is resolved, the Singleton may attempt to use a Scoped object after its scope has been disposed:
ObjectDisposedException
6. User-data leakage
If the Scoped service stores request-specific information, one user’s data could be incorrectly shared with another request.
Recommended solution
Make the consuming service Scoped:
builder.Services.AddScoped<ReportService>();
builder.Services.AddScoped<AppDbContext>();
Now both objects are created once per HTTP request.
If the service must be Singleton
Create a new scope whenever the Scoped service is needed:
public class ReportService
{
private readonly IServiceScopeFactory _scopeFactory;
public ReportService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public async Task GenerateAsync()
{
await using var scope =
_scopeFactory.CreateAsyncScope();
var dbContext =
scope.ServiceProvider
.GetRequiredService<AppDbContext>();
// Use DbContext only inside this scope
await dbContext.SaveChangesAsync();
}
}
For EF Core background work, IDbContextFactory<TContext> is often simpler:
builder.Services.AddDbContextFactory<AppDbContext>();
public class ReportService
{
private readonly IDbContextFactory<AppDbContext> _factory;
public ReportService(
IDbContextFactory<AppDbContext> factory)
{
_factory = factory;
}
public async Task GenerateAsync()
{
await using var dbContext =
await _factory.CreateDbContextAsync();
// Use DbContext
}
}
Key point
A Singleton must not directly capture a Scoped dependency. Make the Singleton Scoped, or create and dispose a separate scope whenever the Scoped service is required.
How do you inject services into controllers, views, middleware, and filters?
ASP.NET Core’s built-in DI supports injection into controllers, Razor views, middleware, and filters. No external library is required.
First, register the service in Program.cs:
builder.Services.AddScoped<IProductService, ProductService>();
1. Inject into controllers
Use constructor injection:
public class ProductController : Controller
{
private readonly IProductService _productService;
public ProductController(IProductService productService)
{
_productService = productService;
}
public IActionResult Index()
{
var products = _productService.GetProducts();
return View(products);
}
}
This is the recommended approach.
2. Inject into Razor views
Use the @inject directive:
@inject IProductService ProductService
@{
var products = ProductService.GetProducts();
}
You can place commonly used injections in _ViewImports.cshtml:
@inject IConfiguration Configuration
Use view injection mainly for presentation-related services, not complex business logic. Normally, controllers should load data and pass a ViewModel to the view.
3. Inject into middleware
Conventional middleware
Singleton services can be injected into the constructor:
public class LoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<LoggingMiddleware> _logger;
public LoggingMiddleware(
RequestDelegate next,
ILogger<LoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(
HttpContext context,
IProductService productService)
{
// Scoped service is injected into InvokeAsync
await _next(context);
}
}
Register the middleware:
app.UseMiddleware<LoggingMiddleware>();
Important: conventional middleware is created once. Therefore, inject Scoped services into InvokeAsync(), not its constructor.
IMiddleware approach
IMiddleware allows constructor injection of Scoped services:
public class LoggingMiddleware : IMiddleware
{
private readonly IProductService _productService;
public LoggingMiddleware(IProductService productService)
{
_productService = productService;
}
public async Task InvokeAsync(
HttpContext context,
RequestDelegate next)
{
await next(context);
}
}
Register it:
builder.Services.AddScoped<LoggingMiddleware>();
app.UseMiddleware<LoggingMiddleware>();
4. Inject into filters
Create a filter with constructor injection:
public class AuditFilter : IActionFilter
{
private readonly ILogger<AuditFilter> _logger;
public AuditFilter(ILogger<AuditFilter> logger)
{
_logger = logger;
}
public void OnActionExecuting(
ActionExecutingContext context)
{
_logger.LogInformation("Action started");
}
public void OnActionExecuted(
ActionExecutedContext context)
{
_logger.LogInformation("Action completed");
}
}
Using ServiceFilter
Register the filter:
builder.Services.AddScoped<AuditFilter>();
Apply it:
[ServiceFilter(typeof(AuditFilter))]
public IActionResult Index()
{
return View();
}
Using TypeFilter
Explicit registration of the filter itself is not normally required:
[TypeFilter(typeof(AuditFilter))]
public IActionResult Index()
{
return View();
}
Its dependencies, however, must be registered.
Register globally
builder.Services.AddScoped<AuditFilter>();
builder.Services.AddControllersWithViews(options =>
{
options.Filters.AddService<AuditFilter>();
});
Quick summary
| Location | Injection method |
|---|---|
| Controller | Constructor injection |
| Razor view | @inject |
| Conventional middleware | Constructor for Singleton-safe services; InvokeAsync() for Scoped services |
IMiddleware |
Constructor injection |
| Filter |
|