dotnet-csharp-dependency-injection
Advanced Microsoft.Extensions.DependencyInjection patterns for .NET applications. Covers service lifetimes, keyed
services (net8.0+), decoration, factory delegates, scope validation, and hosted service registration.
Scope
- Service lifetimes (transient, scoped, singleton) and captive dependency detection
- Keyed services (.NET 8+) and factory delegates
- Decorator pattern and scope validation
- Hosted service registration
Out of scope
- Async/await patterns in BackgroundService -- see [skill:dotnet-csharp-async-patterns]
- Options pattern binding and IOptions -- see [skill:dotnet-csharp-configuration]
- SOLID/DRY design principles -- see [skill:dotnet-solid-principles]
Cross-references: [skill:dotnet-csharp-async-patterns] for BackgroundService async patterns,
[skill:dotnet-csharp-configuration] for IOptions<T> binding.
Service Lifetimes
| Lifetime |
Registration |
When to Use |
| Transient |
AddTransient<T>() |
Lightweight, stateless services. New instance per injection. |
| Scoped |
AddScoped<T>() |
Per-request state (EF Core DbContext, unit of work). |
| Singleton |
AddSingleton<T>() |
Thread-safe, stateless, or shared state (caches, config). |
Lifetime Mismatches (Captive Dependencies)
Never inject a shorter-lived service into a longer-lived one:
// WRONG -- scoped DbContext captured in singleton = same context for all requests
builder.Services.AddSingleton<OrderService>(); // singleton
builder.Services.AddScoped<AppDbContext>(); // scoped -- CAPTIVE!
// CORRECT -- use IServiceScopeFactory in singletons
public sealed class OrderService(IServiceScopeFactory scopeFactory)
{
public async Task ProcessAsync(CancellationToken ct = default)
{
using var scope = scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await db.Orders.Where(o => o.IsPending).ToListAsync(ct);
}
}
```text
### Enable Scope Validation (Development)
```csharp
var builder = WebApplication.CreateBuilder(args);
// In Development, ValidateScopes is already true by default.
// For non-web hosts:
var host = Host.CreateDefaultBuilder(args)
.UseDefaultServiceProvider(options =>
{
options.ValidateScopes = true;
options.ValidateOnBuild = true; // Validates all registrations at startup
})
.Build();
```text
---
## Registration Patterns
### Interface-Implementation Pair
```csharp
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
```csharp
### Multiple Implementations
```csharp
// Register multiple implementations
builder.Services.AddScoped<INotifier, EmailNotifier>();
builder.Services.AddScoped<INotifier, SmsNotifier>();
builder.Services.AddScoped<INotifier, PushNotifier>();
// Inject all -- order matches registration order
public sealed class CompositeNotifier(IEnumerable<INotifier> notifiers)
{
public async Task NotifyAsync(string message, CancellationToken ct = default)
{
foreach (var notifier in notifiers)
{
await notifier.NotifyAsync(message, ct);
}
}
}
```text
### Factory Delegates
```csharp
builder.Services.AddScoped<IOrderService>(sp =>
{
var repo = sp.GetRequiredService<IOrderRepository>();
var logger = sp.GetRequiredService<ILogger<OrderService>>();
var options = sp.GetRequiredService<IOptions<OrderOptions>>();
return new OrderService(repo, logger, options.Value.MaxRetries);
});
```text
### `TryAdd` for Library Registrations
Libraries should use `TryAdd` so applications can override:
```csharp
// Library code -- won't overwrite app registrations
builder.Services.TryAddScoped<IOrderRepository, DefaultOrderRepository>();
// Application code -- takes precedence if registered first
builder.Services.AddScoped<IOrderRepository, CustomOrderRepository>();
```text
---
## Keyed Services (net8.0+)
Register and resolve services by a key, replacing the need for named service patterns.
```csharp
// Registration
builder.Services.AddKeyedScoped<ICache, RedisCache>("distributed");
builder.Services.AddKeyedScoped<ICache, MemoryCache>("local");
// Injection via attribute
public sealed class OrderService(
[FromKeyedServices("distributed")] ICache distributedCache,
[FromKeyedServices("local")] ICache localCache)
{
public async Task<Order?> GetAsync(int id, CancellationToken ct = default)
{
// Check local cache first, then distributed
return await localCache.GetAsync<Order>(id.ToString(), ct)
?? await distributedCache.GetAsync<Order>(id.ToString(), ct);
}
}
// Manual resolution
var cache = sp.GetRequiredKeyedService<ICache>("distributed");
```text
> **net8.0+ only.** On earlier TFMs, use factory patterns or a dictionary-based approach.
---
## Decoration Pattern
The built-in container does not natively support decoration. Use one of these approaches:
### Manual Decoration
```csharp
builder.Services.AddScoped<SqlOrderRepository>();
builder.Services.AddScoped<IOrderRepository>(sp =>
{
var inner = sp.GetRequiredService<SqlOrderRepository>();
var logger = sp.GetRequiredService<ILogger<LoggingOrderRepository>>();
return new LoggingOrderRepository(inner, logger);
});
public sealed class LoggingOrderRepository(
IOrderRepository inner,
ILogger<LoggingOrderRepository> logger) : IOrderRepository
{
public async Task<Order?> GetByIdAsync(int id, CancellationToken ct = default)
{
logger.LogInformation("Getting order {OrderId}", id);
return await inner.GetByIdAsync(id, ct);
}
}
```text
### Scrutor Library (Popular Alternative)
```csharp
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.Decorate<IOrderRepository, LoggingOrderRepository>();
builder.Services.Decorate<IOrderRepository, CachingOrderRepository>();
// Outer -> CachingOrderRepository -> LoggingOrderRepository -> SqlOrderRepository
```text
---
## Hosted Services and Background Workers
### `BackgroundService` (Preferred)
```csharp
public sealed class QueueProcessorWorker(
IServiceScopeFactory scopeFactory,
ILogger<QueueProcessorWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("Queue processor starting");
while (!stoppingToken.IsCancellationRequested)
{
try
{
using var scope = scopeFactory.CreateScope();
var processor = scope.ServiceProvider
.GetRequiredService<IQueueProcessor>();
await processor.ProcessNextBatchAsync(stoppingToken);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
logger.LogError(ex, "Error processing queue batch");
}
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
}
// Registration
builder.Services.AddHostedService<QueueProcessorWorker>();
```text
### `IHostedService` (Startup/Shutdown Hooks)
```csharp
public sealed class DatabaseMigrationService(
IServiceScopeFactory scopeFactory,
ILogger<DatabaseMigrationService> logger) : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
using var scope = scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await db.Database.MigrateAsync(cancellationToken);
logger.LogInformation("Database migration completed");
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
builder.Services.AddHostedService<DatabaseMigrationService>();
```text
### Key Rules for Hosted Services
- Always use `IServiceScopeFactory` to create scopes -- hosted services are singletons
- Never inject scoped services directly into hosted service constructors
- Handle exceptions inside `ExecuteAsync` -- unhandled exceptions stop the host (net8.0+)
- See [skill:dotnet-csharp-async-patterns] for async patterns in background workers
---
## Organizing Registrations
Group related registrations into extension methods for clean `Program.cs`:
```csharp
// ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddOrderServices(this IServiceCollection services)
{
services.AddScoped<IOrderRepository, SqlOrderRepository>();
services.AddScoped<IOrderService, OrderService>();
services.AddHostedService<OrderProcessorWorker>();
return services;
}
public static IServiceCollection AddNotificationServices(this IServiceCollection services)
{
services.AddScoped<INotifier, EmailNotifier>();
services.AddScoped<INotifier, SmsNotifier>();
return services;
}
}
// Program.cs
builder.Services.AddOrderServices();
builder.Services.AddNotificationServices();
```csharp
---
## Testing with DI
```csharp
[Fact]
public async Task OrderService_UsesRepository()
{
// Arrange -- build a service provider for integration tests
var services = new ServiceCollection();
services.AddScoped<IOrderRepository, InMemoryOrderRepository>();
services.AddScoped<IOrderService, OrderService>();
services.AddLogging();
using var provider = services.BuildServiceProvider();
using var scope = provider.CreateScope();
var service = scope.ServiceProvider.GetRequiredService<IOrderService>();
// Act
var order = await service.GetByIdAsync(1);
// Assert
Assert.NotNull(order);
}
```text
For unit tests, prefer direct constructor injection with mocks rather than building a full container.
---
## Code Navigation (Serena MCP)
**Primary approach:** Use Serena symbol operations for efficient code navigation:
1. **Find definitions**: `serena_find_symbol` instead of text search
2. **Understand structure**: `serena_get_symbols_overview` for file organization
3. **Track references**: `serena_find_referencing_symbols` for impact analysis
4. **Precise edits**: `serena_replace_symbol_body` for clean modifications
**When to use Serena vs traditional tools:**
- ✅ **Use Serena**: Navigation, refactoring, dependency analysis, precise edits
- ✅ **Use Read/Grep**: Reading full files, pattern matching, simple text operations
- ✅ **Fallback**: If Serena unavailable, traditional tools work fine
**Example workflow:**
```text
# Instead of:
Read: src/Services/OrderService.cs
Grep: "public void ProcessOrder"
# Use:
serena_find_symbol: "OrderService/ProcessOrder"
serena_get_symbols_overview: "src/Services/OrderService.cs"
```
## References
- [Dependency injection in .NET](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection)
- [Keyed services in .NET 8](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection#keyed-services)
- [Background tasks with hosted services](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services)
- [Service lifetimes](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection#service-lifetimes)
- [.NET Framework Design Guidelines](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/)
1---2name: dotnet-csharp-dependency-injection3description: Registers and resolves services with MS DI. Keyed services, scopes, decoration, lifetimes.4license: MIT5targets: ['*']6category: fundamentals7subcategory: di-and-services8tags:9 - csharp10 - dotnet11 - skill12 - dependency-injection13 - di14version: '1.0.0'15author: 'dotnet-agent-harness'16invocable: true17related_skills:18 - dotnet-csharp-configuration19 - dotnet-validation-patterns20 - dotnet-architecture-patterns21claudecode:22 allowed-tools: ['Read', 'Grep', 'Glob', 'Bash', 'Write', 'Edit']23codexcli:24 short-description: '.NET skill guidance for csharp tasks'25opencode:26 allowed-tools: ['Read', 'Grep', 'Glob', 'Bash', 'Write', 'Edit']27copilot: {}28geminicli: {}29antigravity: {}30---3132# dotnet-csharp-dependency-injection3334Advanced Microsoft.Extensions.DependencyInjection patterns for .NET applications. Covers service lifetimes, keyed35services (net8.0+), decoration, factory delegates, scope validation, and hosted service registration.3637## Scope3839- Service lifetimes (transient, scoped, singleton) and captive dependency detection40- Keyed services (.NET 8+) and factory delegates41- Decorator pattern and scope validation42- Hosted service registration4344## Out of scope4546- Async/await patterns in BackgroundService -- see [skill:dotnet-csharp-async-patterns]47- Options pattern binding and IOptions<T> -- see [skill:dotnet-csharp-configuration]48- SOLID/DRY design principles -- see [skill:dotnet-solid-principles]4950Cross-references: [skill:dotnet-csharp-async-patterns] for `BackgroundService` async patterns,51[skill:dotnet-csharp-configuration] for `IOptions<T>` binding.5253---5455## Service Lifetimes5657| Lifetime | Registration | When to Use |58| --------- | ------------------- | ------------------------------------------------------------ |59| Transient | `AddTransient<T>()` | Lightweight, stateless services. New instance per injection. |60| Scoped | `AddScoped<T>()` | Per-request state (EF Core `DbContext`, unit of work). |61| Singleton | `AddSingleton<T>()` | Thread-safe, stateless, or shared state (caches, config). |6263### Lifetime Mismatches (Captive Dependencies)6465Never inject a shorter-lived service into a longer-lived one:6667````csharp6869// WRONG -- scoped DbContext captured in singleton = same context for all requests70builder.Services.AddSingleton<OrderService>(); // singleton71builder.Services.AddScoped<AppDbContext>(); // scoped -- CAPTIVE!7273// CORRECT -- use IServiceScopeFactory in singletons74public sealed class OrderService(IServiceScopeFactory scopeFactory)75{76 public async Task ProcessAsync(CancellationToken ct = default)77 {78 using var scope = scopeFactory.CreateScope();79 var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();80 await db.Orders.Where(o => o.IsPending).ToListAsync(ct);81 }82}8384```text8586### Enable Scope Validation (Development)8788```csharp8990var builder = WebApplication.CreateBuilder(args);91// In Development, ValidateScopes is already true by default.92// For non-web hosts:93var host = Host.CreateDefaultBuilder(args)94 .UseDefaultServiceProvider(options =>95 {96 options.ValidateScopes = true;97 options.ValidateOnBuild = true; // Validates all registrations at startup98 })99 .Build();100101```text102103---104105## Registration Patterns106107### Interface-Implementation Pair108109```csharp110111builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();112113```csharp114115### Multiple Implementations116117```csharp118119// Register multiple implementations120builder.Services.AddScoped<INotifier, EmailNotifier>();121builder.Services.AddScoped<INotifier, SmsNotifier>();122builder.Services.AddScoped<INotifier, PushNotifier>();123124// Inject all -- order matches registration order125public sealed class CompositeNotifier(IEnumerable<INotifier> notifiers)126{127 public async Task NotifyAsync(string message, CancellationToken ct = default)128 {129 foreach (var notifier in notifiers)130 {131 await notifier.NotifyAsync(message, ct);132 }133 }134}135136```text137138### Factory Delegates139140```csharp141142builder.Services.AddScoped<IOrderService>(sp =>143{144 var repo = sp.GetRequiredService<IOrderRepository>();145 var logger = sp.GetRequiredService<ILogger<OrderService>>();146 var options = sp.GetRequiredService<IOptions<OrderOptions>>();147 return new OrderService(repo, logger, options.Value.MaxRetries);148});149150```text151152### `TryAdd` for Library Registrations153154Libraries should use `TryAdd` so applications can override:155156```csharp157158// Library code -- won't overwrite app registrations159builder.Services.TryAddScoped<IOrderRepository, DefaultOrderRepository>();160161// Application code -- takes precedence if registered first162builder.Services.AddScoped<IOrderRepository, CustomOrderRepository>();163164```text165166---167168## Keyed Services (net8.0+)169170Register and resolve services by a key, replacing the need for named service patterns.171172```csharp173174// Registration175builder.Services.AddKeyedScoped<ICache, RedisCache>("distributed");176builder.Services.AddKeyedScoped<ICache, MemoryCache>("local");177178// Injection via attribute179public sealed class OrderService(180 [FromKeyedServices("distributed")] ICache distributedCache,181 [FromKeyedServices("local")] ICache localCache)182{183 public async Task<Order?> GetAsync(int id, CancellationToken ct = default)184 {185 // Check local cache first, then distributed186 return await localCache.GetAsync<Order>(id.ToString(), ct)187 ?? await distributedCache.GetAsync<Order>(id.ToString(), ct);188 }189}190191// Manual resolution192var cache = sp.GetRequiredKeyedService<ICache>("distributed");193194```text195196> **net8.0+ only.** On earlier TFMs, use factory patterns or a dictionary-based approach.197198---199200## Decoration Pattern201202The built-in container does not natively support decoration. Use one of these approaches:203204### Manual Decoration205206```csharp207208builder.Services.AddScoped<SqlOrderRepository>();209builder.Services.AddScoped<IOrderRepository>(sp =>210{211 var inner = sp.GetRequiredService<SqlOrderRepository>();212 var logger = sp.GetRequiredService<ILogger<LoggingOrderRepository>>();213 return new LoggingOrderRepository(inner, logger);214});215216public sealed class LoggingOrderRepository(217 IOrderRepository inner,218 ILogger<LoggingOrderRepository> logger) : IOrderRepository219{220 public async Task<Order?> GetByIdAsync(int id, CancellationToken ct = default)221 {222 logger.LogInformation("Getting order {OrderId}", id);223 return await inner.GetByIdAsync(id, ct);224 }225}226227```text228229### Scrutor Library (Popular Alternative)230231```csharp232233builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();234builder.Services.Decorate<IOrderRepository, LoggingOrderRepository>();235builder.Services.Decorate<IOrderRepository, CachingOrderRepository>();236// Outer -> CachingOrderRepository -> LoggingOrderRepository -> SqlOrderRepository237238```text239240---241242## Hosted Services and Background Workers243244### `BackgroundService` (Preferred)245246```csharp247248public sealed class QueueProcessorWorker(249 IServiceScopeFactory scopeFactory,250 ILogger<QueueProcessorWorker> logger) : BackgroundService251{252 protected override async Task ExecuteAsync(CancellationToken stoppingToken)253 {254 logger.LogInformation("Queue processor starting");255256 while (!stoppingToken.IsCancellationRequested)257 {258 try259 {260 using var scope = scopeFactory.CreateScope();261 var processor = scope.ServiceProvider262 .GetRequiredService<IQueueProcessor>();263264 await processor.ProcessNextBatchAsync(stoppingToken);265 }266 catch (Exception ex) when (ex is not OperationCanceledException)267 {268 logger.LogError(ex, "Error processing queue batch");269 }270271 await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);272 }273 }274}275276// Registration277builder.Services.AddHostedService<QueueProcessorWorker>();278279```text280281### `IHostedService` (Startup/Shutdown Hooks)282283```csharp284285public sealed class DatabaseMigrationService(286 IServiceScopeFactory scopeFactory,287 ILogger<DatabaseMigrationService> logger) : IHostedService288{289 public async Task StartAsync(CancellationToken cancellationToken)290 {291 using var scope = scopeFactory.CreateScope();292 var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();293 await db.Database.MigrateAsync(cancellationToken);294 logger.LogInformation("Database migration completed");295 }296297 public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;298}299300builder.Services.AddHostedService<DatabaseMigrationService>();301302```text303304### Key Rules for Hosted Services305306- Always use `IServiceScopeFactory` to create scopes -- hosted services are singletons307- Never inject scoped services directly into hosted service constructors308- Handle exceptions inside `ExecuteAsync` -- unhandled exceptions stop the host (net8.0+)309- See [skill:dotnet-csharp-async-patterns] for async patterns in background workers310311---312313## Organizing Registrations314315Group related registrations into extension methods for clean `Program.cs`:316317```csharp318319// ServiceCollectionExtensions.cs320public static class ServiceCollectionExtensions321{322 public static IServiceCollection AddOrderServices(this IServiceCollection services)323 {324 services.AddScoped<IOrderRepository, SqlOrderRepository>();325 services.AddScoped<IOrderService, OrderService>();326 services.AddHostedService<OrderProcessorWorker>();327 return services;328 }329330 public static IServiceCollection AddNotificationServices(this IServiceCollection services)331 {332 services.AddScoped<INotifier, EmailNotifier>();333 services.AddScoped<INotifier, SmsNotifier>();334 return services;335 }336}337338// Program.cs339builder.Services.AddOrderServices();340builder.Services.AddNotificationServices();341342```csharp343344---345346## Testing with DI347348```csharp349350[Fact]351public async Task OrderService_UsesRepository()352{353 // Arrange -- build a service provider for integration tests354 var services = new ServiceCollection();355 services.AddScoped<IOrderRepository, InMemoryOrderRepository>();356 services.AddScoped<IOrderService, OrderService>();357 services.AddLogging();358359 using var provider = services.BuildServiceProvider();360 using var scope = provider.CreateScope();361 var service = scope.ServiceProvider.GetRequiredService<IOrderService>();362363 // Act364 var order = await service.GetByIdAsync(1);365366 // Assert367 Assert.NotNull(order);368}369370```text371372For unit tests, prefer direct constructor injection with mocks rather than building a full container.373374---375376377378## Code Navigation (Serena MCP)379380**Primary approach:** Use Serena symbol operations for efficient code navigation:3813821. **Find definitions**: `serena_find_symbol` instead of text search3832. **Understand structure**: `serena_get_symbols_overview` for file organization3843. **Track references**: `serena_find_referencing_symbols` for impact analysis3854. **Precise edits**: `serena_replace_symbol_body` for clean modifications386387**When to use Serena vs traditional tools:**388- ✅ **Use Serena**: Navigation, refactoring, dependency analysis, precise edits389- ✅ **Use Read/Grep**: Reading full files, pattern matching, simple text operations390- ✅ **Fallback**: If Serena unavailable, traditional tools work fine391392**Example workflow:**393```text394# Instead of:395Read: src/Services/OrderService.cs396Grep: "public void ProcessOrder"397398# Use:399serena_find_symbol: "OrderService/ProcessOrder"400serena_get_symbols_overview: "src/Services/OrderService.cs"401```402## References403404- [Dependency injection in .NET](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection)405- [Keyed services in .NET 8](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection#keyed-services)406- [Background tasks with hosted services](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services)407- [Service lifetimes](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection#service-lifetimes)408- [.NET Framework Design Guidelines](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/)409````