关于 dotnet-csharp-dependency-injection
面向使用 Microsoft.Extensions.DependencyInjection 构建 .NET 应用的开发者,介绍服务生命周期、注册和解析模式,以及托管服务。还涵盖 keyed services、装饰器和作用域验证。
主要功能
- 01服务生命周期与依赖关系
- 02工厂委托和多实现注册
- 03net8.0+ keyed services
- 04装饰器注册模式
- 05托管服务与作用域验证
适用场景
- 01避免单例捕获短生命周期服务
- 02按键注册和解析服务实现
- 03为服务添加日志或缓存装饰器
- 04为后台服务创建依赖注入作用域
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-csharp-dependency-injection安装后在 Claude Code 中输入/dotnet-csharp-dependency-injection确认已加载。
详细介绍
dotnet-csharp-dependency-injection
适用于 .NET 应用程序的高级 Microsoft.Extensions.DependencyInjection 模式。涵盖服务生命周期、键控服务(net8.0+)、装饰、工厂委托、作用域验证和托管服务注册。
交叉引用:[skill:dotnet-csharp-async-patterns],了解 BackgroundService 异步模式;[skill:dotnet-csharp-configuration],了解 IOptions<T> 绑定。
服务生命周期
| 生命周期 | 注册方式 | 适用场景 |
|---|---|---|
| 瞬态 | AddTransient<T>() | 轻量、无状态的服务。每次注入时创建新实例。 |
| 作用域 | AddScoped<T>() | 每个请求的状态(EF Core DbContext、工作单元)。 |
| 单例 | AddSingleton<T>() | 线程安全、无状态或共享状态(缓存、配置)。 |
生命周期不匹配(被俘获的依赖)
切勿将生命周期较短的服务注入生命周期较长的服务:
// 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);
}
}
启用作用域验证(开发环境)
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();
注册模式
接口-实现配对
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
多个实现
// 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);
}
}
}
工厂委托
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);
});
库注册使用 TryAdd
库应使用 TryAdd,以便应用程序可以覆盖注册:
// Library code -- won't overwrite app registrations
builder.Services.TryAddScoped<IOrderRepository, DefaultOrderRepository>();
// Application code -- takes precedence if registered first
builder.Services.AddScoped<IOrderRepository, CustomOrderRepository>();
键控服务(net8.0+)
通过键注册和解析服务,取代命名服务模式。
// 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");
仅适用于 net8.0+。 对于更早的 TFM,请使用工厂模式或基于字典的方案。
装饰模式
内置容器不原生支持装饰。可使用以下方法之一:
手动装饰
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);
}
}
Scrutor 库(常用替代方案)
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.Decorate<IOrderRepository, LoggingOrderRepository>();
builder.Services.Decorate<IOrderRepository, CachingOrderRepository>();
// Outer -> CachingOrderRepository -> LoggingOrderRepository -> SqlOrderRepository
托管服务和后台工作程序
BackgroundService(首选)
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>();
IHostedService(启动/关闭钩子)
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>();
托管服务的关键规则
- 始终使用
IServiceScopeFactory创建作用域——托管服务是单例 - 切勿将作用域服务直接注入托管服务构造函数
- 在
ExecuteAsync中处理异常——未处理的异常会导致宿主停止(net8.0+) - 有关后台工作程序中的异步模式,请参阅 [skill:dotnet-csharp-async-patterns]
组织注册
将相关注册归入扩展方法,使 Program.cs 更简洁:
// 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();
使用 DI 进行测试
[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);
}
对于单元测试,建议使用模拟对象直接进行构造函数注入,而不是构建完整容器。
参考资料
评论
支持 Markdown