The dependency injection container built into .NET covers the vast majority of cases. Knowing its lifetimes and newest features well avoids bugs that are hard to track down.
The three lifetimes
- Singleton: a single instance for the whole application. It must be thread-safe.
- Scoped: one instance per web request (or per manually created scope).
- Transient: a new instance every time it is requested.
The classic mistake is injecting a scoped service into a singleton: the singleton holds on to it forever, sharing for example a database context across different requests. In the development environment .NET reports this at startup.
Keyed services
When you need several implementations of the same interface, register them with a key:
builder.Services.AddKeyedSingleton<INotifier, EmailNotifier>("email");
builder.Services.AddKeyedSingleton<INotifier, SmsNotifier>("sms");
public class AlertService(
[FromKeyedServices("email")] INotifier email,
[FromKeyedServices("sms")] INotifier sms)
{
public Task UrgentAsync(string text) =>
Task.WhenAll(email.SendAsync(text), sms.SendAsync(text));
}
In Minimal APIs the same attribute is used on handler parameters.
Background services and scopes
A BackgroundService is a singleton. To use scoped services, such as a repository, you create an explicit scope:
public class CleanupService(IServiceScopeFactory scopes) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
while (await timer.WaitForNextTickAsync(ct))
{
using var scope = scopes.CreateScope();
var repo = scope.ServiceProvider.GetRequiredService<ILogRepository>();
await repo.DeleteOldAsync(ct);
}
}
}
Rule of thumb: register as singleton only what is truly shareable and free of per-user state, and keep everything that touches request data scoped.
Comments (0)
No comments yet.