| name | dependency-injection |
| description | Use when registering services, choosing lifetimes, or implementing DI patterns like decorator or keyed services.
|
| metadata | {"category":"core","agent":"dotnet-architect","alwaysApply":true} |
Dependency Injection
Core Principles
- Use constructor injection exclusively — avoid service locator pattern
- Choose the correct lifetime: Singleton, Scoped, or Transient
- Organize registrations in
IServiceCollection extension methods
- Use keyed services (.NET 8+) for multiple implementations of the same interface
- Validate registrations at startup where possible
- Never resolve scoped services from a singleton
Patterns
Service Registration Extension Methods
public static class DependencyInjection
{
public static IServiceCollection AddApplicationServices(
this IServiceCollection services)
{
services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));
services.AddValidatorsFromAssembly(
typeof(DependencyInjection).Assembly);
return services;
}
public static IServiceCollection AddInfrastructureServices(
this IServiceCollection services, IConfiguration configuration)
{
services.AddScoped<IOrderRepository, OrderRepository>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));
services.AddSingleton<IDateTimeProvider, DateTimeProvider>();
services.AddTransient<IEmailSender, SmtpEmailSender>();
return services;
}
}
builder.Services
.AddApplicationServices()
.AddInfrastructureServices(builder.Configuration);
Lifetime Decision Guide
Singleton — stateless services, caches, configuration wrappers
Example: IDateTimeProvider, HybridCache, IOptions<T>
Warning: must be thread-safe
Scoped — per-request state, DbContext, UnitOfWork, repositories
Example: AppDbContext, ICurrentUserService
Default choice for most services
Transient — lightweight stateless services, factory output
Example: IEmailSender, validators
Warning: do not hold expensive resources
Keyed Services (.NET 8+)
services.AddKeyedSingleton<IPaymentGateway, StripeGateway>("stripe");
services.AddKeyedSingleton<IPaymentGateway, PayPalGateway>("paypal");
services.AddKeyedSingleton<IPaymentGateway, SquareGateway>("square");
public sealed class CheckoutService(
[FromKeyedServices("stripe")] IPaymentGateway gateway)
{
public Task ChargeAsync(decimal amount) => gateway.ChargeAsync(amount);
}
public sealed class PaymentRouter(IServiceProvider provider)
{
public IPaymentGateway GetGateway(string name)
=> provider.GetRequiredKeyedService<IPaymentGateway>(name);
}
Decorator Pattern
services.AddScoped<IOrderService, OrderService>();
services.Decorate<IOrderService, CachedOrderService>();
services.Decorate<IOrderService, LoggingOrderService>();
public sealed class CachedOrderService(
IOrderService inner,
HybridCache cache) : IOrderService
{
public async Task<Order?> GetOrderAsync(Guid id, CancellationToken ct)
{
return await cache.GetOrCreateAsync(
$"orders:{id}",
async token => await inner.GetOrderAsync(id, token),
cancellationToken: ct);
}
}
Factory Delegates
services.AddScoped<IReportGenerator>(sp =>
{
var config = sp.GetRequiredService<IOptions<ReportOptions>>().Value;
return config.Format switch
{
"pdf" => sp.GetRequiredService<PdfReportGenerator>(),
"csv" => sp.GetRequiredService<CsvReportGenerator>(),
_ => throw new InvalidOperationException(
$"Unknown report format: {config.Format}")
};
});
Background Service Scoping
public sealed class OrderProcessor(
IServiceScopeFactory scopeFactory,
ILogger<OrderProcessor> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
using var scope = scopeFactory.CreateScope();
var repository = scope.ServiceProvider
.GetRequiredService<IOrderRepository>();
var unitOfWork = scope.ServiceProvider
.GetRequiredService<IUnitOfWork>();
await ProcessPendingOrdersAsync(repository, unitOfWork, ct);
await Task.Delay(TimeSpan.FromSeconds(30), ct);
}
}
}
Options Pattern Registration
services.AddOptions<DatabaseOptions>()
.BindConfiguration(DatabaseOptions.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddOptions<JwtOptions>()
.BindConfiguration("Jwt")
.ValidateDataAnnotations()
.ValidateOnStart();
Anti-Patterns
public class OrderService(IServiceProvider provider)
{
public void Process()
{
var repo = provider.GetRequiredService<IOrderRepository>();
}
}
services.AddSingleton<MySingleton>();
public class OrderService(IServiceProvider provider)
{
private readonly IOrderRepository _repo =
provider.GetRequiredService<IOrderRepository>();
}
public class OrderController
{
private readonly OrderService _service = new OrderService(
new OrderRepository(new AppDbContext()));
}
Detect Existing Patterns
- Search for
builder.Services.Add calls in Program.cs
- Look for
IServiceCollection extension methods in the codebase
- Check for
[FromKeyedServices] usage (indicates .NET 8+ keyed DI)
- Search for
AddScoped, AddTransient, AddSingleton registration patterns
- Look for
IServiceScopeFactory usage in background services
- Check for
Scrutor package (provides Decorate<> extension)
Adding to Existing Project
- Group registrations — move scattered
builder.Services.Add* calls into extension methods per layer
- Audit lifetimes — verify no captive dependencies (scoped inside singleton)
- Add ValidateOnStart — add
ValidateOnStart() to all Options registrations
- Migrate to keyed services — replace manual factory delegates with keyed services if on .NET 8+
- Use
Scrutor for decorator support if not already available:
<PackageReference Include="Scrutor" />
Decision Guide
| Scenario | Lifetime | Notes |
|---|
| DbContext | Scoped | One per request |
| Repository | Scoped | Matches DbContext lifetime |
| HttpClient handler | Transient | Via IHttpClientFactory |
| Cache wrapper | Singleton | Thread-safe required |
| Options | Singleton | Via IOptions |
| Current user | Scoped | Per-request context |
| Background service | Singleton | Use IServiceScopeFactory inside |
References