依赖注入模式
何时使用此技能
当需要:
- 在ASP.NET Core应用程序中组织服务注册
- 避免Program.cs/Startup.cs文件中有数百个注册的庞大体积
- 使服务配置在生产和测试之间可重用
- 设计与Microsoft.Extensions.DependencyInjection集成的库
问题
没有组织,Program.cs变得难以管理:
// 坏:200多行无组织的注册
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<IEmailComposer, MjmlEmailComposer>();
builder.Services.AddSingleton<IEmailLinkGenerator, EmailLinkGenerator>();
builder.Services.AddScoped<IPaymentProcessor, StripePaymentProcessor>();
builder.Services.AddScoped<IInvoiceGenerator, InvoiceGenerator>();
// ... 150多行 ...
问题:
- 难以找到相关注册
- 子系统之间没有明确的界限
- 不能在测试中重用配置
- 团队设置中的合并冲突
- 没有封装内部依赖
解决方案:扩展方法组合
将相关注册分组到扩展方法中:
// 好:干净,可组合的Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddUserServices()
.AddOrderServices()
.AddEmailServices()
.AddPaymentServices()
.AddValidators();
var app = builder.Build();
每个Add*方法封装了一组有凝聚力的注册。
扩展方法模式
基本结构
namespace MyApp.Users;
public static class UserServiceCollectionExtensions
{
public static IServiceCollection AddUserServices(this IServiceCollection services)
{
// 仓库
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IUserReadStore, UserReadStore>();
services.AddScoped<IUserWriteStore, UserWriteStore>();
// 服务
services.AddScoped<IUserService, UserService>();
services.AddScoped<IUserValidationService, UserValidationService>();
// 返回以链式
return services;
}
}
带配置
namespace MyApp.Email;
public static class EmailServiceCollectionExtensions
{
public static IServiceCollection AddEmailServices(
this IServiceCollection services,
string configSectionName = "EmailSettings")
{
// 绑定配置
services.AddOptions<EmailOptions>()
.BindConfiguration(configSectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
// 注册服务
services.AddSingleton<IMjmlTemplateRenderer, MjmlTemplateRenderer>();
services.AddSingleton<IEmailLinkGenerator, EmailLinkGenerator>();
services.AddScoped<IUserEmailComposer, UserEmailComposer>();
services.AddScoped<IOrderEmailComposer, OrderEmailComposer>();
// SMTP客户端依赖于环境
services.AddScoped<IEmailSender, SmtpEmailSender>();
return services;
}
}
依赖其他扩展
namespace MyApp.Orders;
public static class OrderServiceCollectionExtensions
{
public static IServiceCollection AddOrderServices(this IServiceCollection services)
{
// 此子系统依赖于电子邮件服务
// 调用者负责先调用AddEmailServices()
// 或者我们可以在这里调用它,如果是幂等的
services.AddScoped<IOrderRepository, OrderRepository>();
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<IOrderEmailNotifier, OrderEmailNotifier>();
return services;
}
}
文件组织
将扩展方法放置在它们注册的服务附近:
src/
MyApp.Api/
Program.cs # 组合所有Add*方法
MyApp.Users/
Services/
UserService.cs
IUserService.cs
Repositories/
UserRepository.cs
UserServiceCollectionExtensions.cs # AddUserServices()
MyApp.Orders/
Services/
OrderService.cs
OrderServiceCollectionExtensions.cs # AddOrderServices()
MyApp.Email/
Composers/
UserEmailComposer.cs
EmailServiceCollectionExtensions.cs # AddEmailServices()
约定:{Feature}ServiceCollectionExtensions.cs紧挨着特性的服务。
命名约定
| 模式 | 用途 |
|---|---|
Add{Feature}Services() |
一般特性注册 |
Add{Feature}() |
无歧义时的短形式 |
Configure{Feature}() |
主要设置选项时 |
Use{Feature}() |
中间件(在IApplicationBuilder上) |
// 特性服务
services.AddUserServices();
services.AddEmailServices();
services.AddPaymentServices();
// 第三方集成
services.AddStripePayments();
services.AddSendGridEmail();
// 配置繁重
services.ConfigureAuthentication();
services.ConfigureAuthorization();
测试好处
主要优势:在测试中重用生产配置。
WebApplicationFactory
public class ApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public ApiTests(WebApplicationFactory<Program> factory)
{
_factory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// 生产服务已经通过Add*方法注册
// 只覆盖测试中不同的部分
// 用测试双替换电子邮件发送器
services.RemoveAll<IEmailSender>();
services.AddSingleton<IEmailSender, TestEmailSender>();
// 替换外部支付处理器
services.RemoveAll<IPaymentProcessor>();
services.AddSingleton<IPaymentProcessor, FakePaymentProcessor>();
});
});
}
[Fact]
public async Task CreateOrder_SendsConfirmationEmail()
{
var client = _factory.CreateClient();
var emailSender = _factory.Services.GetRequiredService<IEmailSender>() as TestEmailSender;
await client.PostAsJsonAsync("/api/orders", new CreateOrderRequest(...));
Assert.Single(emailSender!.SentEmails);
}
}
Akka.Hosting.TestKit
public class OrderActorSpecs : Akka.Hosting.TestKit.TestKit
{
protected override void ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider)
{
// 重用生产Akka配置
builder.AddOrderActors();
}
protected override void ConfigureServices(IServiceCollection services)
{
// 重用生产服务配置
services.AddOrderServices();
// 仅覆盖外部依赖项
services.RemoveAll<IPaymentProcessor>();
services.AddSingleton<IPaymentProcessor, FakePaymentProcessor>();
}
[Fact]
public async Task OrderActor_ProcessesPayment()
{
var orderActor = ActorRegistry.Get<OrderActor>();
orderActor.Tell(new ProcessOrder(orderId));
ExpectMsg<OrderProcessed>();
}
}
独立单元测试
public class UserServiceTests
{
private readonly ServiceProvider _provider;
public UserServiceTests()
{
var services = new ServiceCollection();
// 重用生产注册
services.AddUserServices();
// 添加测试基础设施
services.AddSingleton<IUserRepository, InMemoryUserRepository>();
_provider = services.BuildServiceProvider();
}
[Fact]
public async Task CreateUser_ValidData_Succeeds()
{
var service = _provider.GetRequiredService<IUserService>();
var result = await service.CreateUserAsync(new CreateUserRequest(...));
Assert.True(result.IsSuccess);
}
}
分层扩展
对于更大的应用程序,层次化地组合扩展:
// 顶级:应用程序需要的一切
public static class AppServiceCollectionExtensions
{
public static IServiceCollection AddAppServices(this IServiceCollection services)
{
return services
.AddDomainServices()
.AddInfrastructureServices()
.AddApiServices();
}
}
// 领域层
public static class DomainServiceCollectionExtensions
{
public static IServiceCollection AddDomainServices(this IServiceCollection services)
{
return services
.AddUserServices()
.AddOrderServices()
.AddProductServices();
}
}
// 基础设施层
public static class InfrastructureServiceCollectionExtensions
{
public static IServiceCollection AddInfrastructureServices(this IServiceCollection services)
{
return services
.AddEmailServices()
.AddPaymentServices()
.AddStorageServices();
}
}
Akka.Hosting集成
相同的模式适用于Akka.NET演员配置:
public static class OrderActorExtensions
{
public static AkkaConfigurationBuilder AddOrderActors(
this AkkaConfigurationBuilder builder)
{
return builder
.WithActors((system, registry, resolver) =>
{
var orderProps = resolver.Props<OrderActor>();
var orderRef = system.ActorOf(orderProps, "orders");
registry.Register<OrderActor>(orderRef);
})
.WithShardRegion<OrderShardActor>(
typeName: "order-shard",
(system, registry, resolver) =>
entityId => resolver.Props<OrderShardActor>(entityId),
new OrderMessageExtractor(),
ShardOptions.Create());
}
}
// Program.cs中的使用
builder.Services.AddAkka("MySystem", (builder, sp) =>
{
builder
.AddOrderActors()
.AddInventoryActors()
.AddNotificationActors();
});
查看akka/hosting-actor-patterns技能以获取完整的Akka.Hosting模式。
常见模式
条件注册
public static IServiceCollection AddEmailServices(
this IServiceCollection services,
IHostEnvironment environment)
{
services.AddSingleton<IEmailComposer, MjmlEmailComposer>();
if (environment.IsDevelopment())
{
// 在开发中使用Mailpit
services.AddSingleton<IEmailSender, MailpitEmailSender>();
}
else
{
// 在生产中使用真实的SMTP
services.AddSingleton<IEmailSender, SmtpEmailSender>();
}
return services;
}
基于工厂的注册
public static IServiceCollection AddPaymentServices(
this IServiceCollection services,
string configSection = "Stripe")
{
services.AddOptions<StripeOptions>()
.BindConfiguration(configSection)
.ValidateOnStart();
// 工厂用于复杂的初始化
services.AddSingleton<IPaymentProcessor>(sp =>
{
var options = sp.GetRequiredService<IOptions<StripeOptions>>().Value;
var logger = sp.GetRequiredService<ILogger<StripePaymentProcessor>>();
return new StripePaymentProcessor(options.ApiKey, options.WebhookSecret, logger);
});
return services;
}
键控服务(.NET 8+)
public static IServiceCollection AddNotificationServices(this IServiceCollection services)
{
// 注册多个实现与键
services.AddKeyedSingleton<INotificationSender, EmailNotificationSender>("email");
services.AddKeyedSingleton<INotificationSender, SmsNotificationSender>("sms");
services.AddKeyedSingleton<INotificationSender, PushNotificationSender>("push");
// 选择正确服务的解析器
services.AddScoped<INotificationDispatcher, NotificationDispatcher>();
return services;
}
反模式
不要:在Program.cs中注册一切
// 坏:庞大的Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
// ... 200多行 ...
不要:创建过于通用的扩展
// 坏:太模糊,不能传达注册的内容
public static IServiceCollection AddServices(this IServiceCollection services)
{
// 注册50个随机事项
}
不要:隐藏重要配置
// 坏:隐藏重要设置
public static IServiceCollection AddDatabase(this IServiceCollection services)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer("hardcoded-connection-string")); // 隐藏!
}
// 好:明确接受配置
public static IServiceCollection AddDatabase(
this IServiceCollection services,
string connectionString)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString));
}
最佳实践总结
| 实践 | 好处 |
|---|---|
将相关服务分组到Add*方法 |
清洁的Program.cs,清晰的界限 |
| 将扩展放置在它们注册的服务附近 | 易于查找和维护 |
返回IServiceCollection以链式 |
流畅的API |
| 接受配置参数 | 灵活性 |
使用一致的命名(Add{Feature}Services) |
可发现性 |
| 通过重用生产扩展进行测试 | 信心,减少重复 |
生命周期管理
根据状态选择合适的生命周期:
| 生命周期 | 何时使用 | 示例 |
|---|---|---|
| Singleton | 无状态,线程安全,昂贵的创建 | 配置,HttpClient工厂,缓存 |
| Scoped | 每个请求有状态,数据库上下文 | DbContext,仓库,用户上下文 |
| Transient | 轻量级,有状态,便宜创建 | 验证器,短命助手 |
经验法则
// SINGLETON:无状态服务,安全共享
services.AddSingleton<IMjmlTemplateRenderer, MjmlTemplateRenderer>();
services.AddSingleton<IEmailLinkGenerator, EmailLinkGenerator>();
// SCOPED:数据库访问,每个请求状态
services.AddScoped<IUserRepository, UserRepository>(); // DbContext依赖
services.AddScoped<IOrderService, OrderService>(); // 使用范围仓库
// TRANSIENT:便宜,短命
services.AddTransient<CreateUserRequestValidator>();
范围要求
范围服务需要范围存在。 在ASP.NET Core中,每个HTTP请求自动创建一个范围。但在其他上下文(后台服务,演员)中,必须手动创建范围。
// ASP.NET控制器 - 自动存在范围
public class OrdersController : ControllerBase
{
private readonly IOrderService _orderService; // 范围 - 有效!
public OrdersController(IOrderService orderService)
{
_orderService = orderService;
}
}
// 后台服务 - 没有自动范围!
public class OrderProcessingService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
public OrderProcessingService(IServiceProvider serviceProvider)
{
// 注入IServiceProvider,而不是直接注入范围服务
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
// 手动为每个工作单元创建范围
using var scope = _serviceProvider.CreateScope();
var orderService = scope.ServiceProvider.GetRequiredService<IOrderService>();
await orderService.ProcessPendingOrdersAsync(ct);
await Task.Delay(TimeSpan.FromMinutes(1), ct);
}
}
}
Akka.NET演员范围管理
演员没有自动DI范围。 如果您需要在演员内部使用范围服务,注入IServiceProvider并手动创建范围。
模式:每个消息一个范围
public sealed class AccountProvisionActor : ReceiveActor
{
private readonly IServiceProvider _serviceProvider;
private readonly IActorRef _mailingActor;
public AccountProvisionActor(
IServiceProvider serviceProvider,
IRequiredActor<MailingActor> mailingActor)
{
_serviceProvider = serviceProvider;
_mailingActor = mailingActor.ActorRef;
ReceiveAsync<ProvisionAccount>(HandleProvisionAccount);
}
private async Task HandleProvisionAccount(ProvisionAccount msg)
{
// 为这个消息处理创建范围
using var scope = _serviceProvider.CreateScope();
// 解析范围服务
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<User>>();
var orderRepository = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
var emailComposer = scope.ServiceProvider.GetRequiredService<IPaymentEmailComposer>();
// 使用范围服务进行工作
var user = await userManager.FindByIdAsync(msg.UserId);
var order = await orderRepository.CreateAsync(msg.Order);
// DbContext在范围释放时提交
}
}
为什么这个模式有效
- 每个消息获得新的DbContext - 没有陈旧的实体跟踪
- 正确处置 - 连接在每个消息后释放
- 隔离 - 一个消息的错误不影响其他消息
- 可测试 - 可以注入模拟IServiceProvider
演员中的Singleton服务
对于无状态服务,直接注入(不需要范围):
public sealed class NotificationActor : ReceiveActor
{
private readonly IEmailLinkGenerator _linkGenerator; // Singleton - 好的!
private readonly IActorRef _mailingActor;
public NotificationActor(
IEmailLinkGenerator linkGenerator, // 直接注入
IRequiredActor<MailingActor> mailingActor)
{
_linkGenerator = linkGenerator;
_mailingActor = mailingActor.ActorRef;
Receive<SendWelcomeEmail>(Handle);
}
}
Akka.DependencyInjection参考
Akka.NET的DI集成文档在:
- Akka.DependencyInjection: https://getakka.net/articles/actors/dependency-injection.html
- Akka.Hosting: https://github.com/akkadotnet/Akka.Hosting
常见错误
将范围注入到Singleton中
// 坏:Singleton捕获范围服务 - 陈旧的DbContext!
public class CacheService // 注册为Singleton
{
private readonly IUserRepository _repo; // 范围!
public CacheService(IUserRepository repo) // 在启动时捕获!
{
_repo = repo; // 这个DbContext永远存在 - 坏
}
}
// 好:注入工厂或IServiceProvider
public class CacheService
{
private readonly IServiceProvider _serviceProvider;
public CacheService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task<User> GetUserAsync(string id)
{
using var scope = _serviceProvider.CreateScope();
var repo = scope.ServiceProvider.GetRequiredService<IUserRepository>();
return await repo.GetByIdAsync(id);
}
}
后台工作中没有范围
// 坏:没有范围用于范围服务
public class BadBackgroundService : BackgroundService
{
private readonly IOrderService _orderService; // 范围!
public BadBackgroundService(IOrderService orderService)
{
_orderService = orderService; // 将抛出或行为异常
}
}
// 好:为每个工作单元创建范围
public class GoodBackgroundService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
public GoodBackgroundService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken ct)
{
using var scope = _scopeFactory.CreateScope();
var orderService = scope.ServiceProvider.GetRequiredService<IOrderService>();
// ...
}
}