ASP.NET Core 依赖注入深入理解
2017.06.22
C#
3 min
1.3k 字
// 目录 · contents
ASP.NET Core 把依赖注入提升为框架的一等公民,内置 DI
容器开箱即用。用了半年之后,我发现大多数问题都源于对生命周期的误解。这篇文章把我踩过的坑和总结的最佳实践都写下来。
1. 三种生命周期
1 2 3
| services.AddTransient<IEmailService, SmtpEmailService>(); services.AddScoped<IOrderService, OrderService>(); services.AddSingleton<ICacheService, RedisCacheService>();
|
| Transient |
每次注入 |
使用完立即 |
无状态、轻量级服务 |
| Scoped |
每个 HTTP 请求 |
请求结束 |
DbContext、工作单元 |
| Singleton |
应用启动 |
应用关闭 |
配置、缓存、无状态工具 |
Scoped
是最常用的——一次请求内共享同一个实例,既避免了每次创建的开销,又保证了请求之间的隔离。DbContext
就应该是 Scoped。
2. 生命周期陷阱:Captive
Dependency
这是最常见的错误:在长生命周期的服务里注入短生命周期的服务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public class OrderService { private readonly AppDbContext _db;
public OrderService(AppDbContext db) { _db = db; } }
services.AddScoped<IOrderService, OrderService>();
public class OrderService { private readonly IServiceScopeFactory _scopeFactory;
public OrderService(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; }
public async Task ProcessOrderAsync(int orderId) { using var scope = _scopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService<AppDbContext>(); } }
|
ASP.NET Core 在开发模式下会自动检测并抛出
InvalidOperationException 来提醒这个问题。
3. 注册模式
接口注册(最常用):
1
| services.AddScoped<IUserService, UserService>();
|
泛型服务注册:
1 2 3 4 5 6 7 8 9
| services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
public class OrderService { public OrderService(IRepository<Order> orderRepo, IRepository<User> userRepo) { } }
|
工厂方法注册:
1 2 3 4 5 6 7 8 9 10 11 12
| services.AddScoped<IPaymentService>(provider => { var config = provider.GetRequiredService<IConfiguration>(); var paymentType = config["Payment:Type"];
return paymentType switch { "alipay" => new AlipayService(config["Payment:AlipayKey"]), "wechat" => new WechatPayService(config["Payment:WechatKey"]), _ => throw new NotSupportedException($"不支持的支付方式: {paymentType}") }; });
|
同一接口多实现(策略模式):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| services.AddScoped<INotificationService, EmailNotificationService>(); services.AddScoped<INotificationService, SmsNotificationService>(); services.AddScoped<INotificationService, PushNotificationService>();
public class NotificationManager { private readonly IEnumerable<INotificationService> _services;
public NotificationManager(IEnumerable<INotificationService> services) { _services = services; }
public async Task SendAllAsync(string message) { await Task.WhenAll(_services.Select(s => s.SendAsync(message))); } }
|
4. Options 模式:强类型配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| { "Email": { "SmtpHost": "smtp.example.com", "SmtpPort": 587, "FromAddress": "[email protected]" } }
public class EmailOptions { public string SmtpHost { get; set; } public int SmtpPort { get; set; } public string FromAddress { get; set; } }
services.Configure<EmailOptions>(Configuration.GetSection("Email"));
public class EmailService { public EmailService(IOptions<EmailOptions> options) { var opts = options.Value; }
public EmailService(IOptionsSnapshot<EmailOptions> options) { var opts = options.Value; }
public EmailService(IOptionsMonitor<EmailOptions> monitor) { monitor.OnChange(opts => Console.WriteLine("配置已更新")); } }
|
5. 集成第三方容器
内置 DI
功能基本够用,但有些高级场景(属性注入、拦截器、自动扫描)需要 Autofac
等:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddMvc();
var builder = new ContainerBuilder(); builder.Populate(services);
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()) .Where(t => t.Name.EndsWith("Service")) .AsImplementedInterfaces() .InstancePerLifetimeScope();
builder.RegisterType<LoggingInterceptor>(); builder.RegisterType<OrderService>() .As<IOrderService>() .EnableInterfaceInterceptors() .InterceptedBy(typeof(LoggingInterceptor));
var container = builder.Build(); return new AutofacServiceProvider(container); }
|
踩坑记录
我们项目里有一次诡异的 bug:DbContext
偶发性地在一个请求里被两个操作同时使用,抛出”A second operation started
on this context before a previous asynchronous operation
completed”异常。
排查了很久才发现:有一个 BackgroundService 是
Singleton,里面注入了 IOrderService(Scoped),而这个
IOrderService 又依赖了 DbContext。因为
BackgroundService 持有的是第一个请求的
DbContext,多个后台任务并发时共享了同一个实例。
解决方案就是用 IServiceScopeFactory
在每个后台任务执行时创建独立的 scope,不直接持有 Scoped 服务。
总结
- Transient/Scoped/Singleton
三种生命周期,
DbContext 用 Scoped,缓存/配置用
Singleton
- 不要在 Singleton 里注入 Scoped
服务,这是最常见的陷阱
- Options 模式是读取配置的最佳实践,优于直接注入
IConfiguration
- 同一接口多实现通过
IEnumerable<T> 注入,配合策略模式非常优雅
- 后台服务里需要 Scoped 服务时,用
IServiceScopeFactory 手动创建作用域