DI工厂的使用是否恰当?ASP.NET依赖注入实践相关疑问
ASP.NET DI配置方案对比与最佳实践疑问
我接手了一段ASP.NET应用代码,原服务容器注册逻辑如下:
public void Configure(IServiceCollection serviceCollection) { serviceCollection.AddTransient<IConfigService, ConfigService>(); serviceCollection.AddTransient(p => p.GetService<IConfigService>().GetConfigFromDb()); }
其中GetConfigFromDb()方法会多次查询跨数据中心的独立物理数据库,获取上下文相关配置值,后续这些值通过构造函数注入到部分服务中,示例服务类:
public class SomeService { private Configs configs; public SomeService(Configs configs) { this.configs = configs; } public void SomeMethod() { // 使用 configs.SomeValue 处理逻辑 } }
我直觉上倾向于避免服务定位器模式,把GetConfigFromDb()的调用移出服务容器,修改后的注册代码:
public void Configure(IServiceCollection serviceCollection) { serviceCollection.AddTransient<IConfigService, ConfigService>(); }
对应服务类调整为:
public class SomeService { private Configs configs; public SomeService(IConfigService configService) { this.configs = configService.GetConfigFromDb(); } public void SomeMethod() { // 使用 configs.SomeValue 处理逻辑 } }
这似乎符合微软DI指南中「保持DI工厂快速且同步」的建议,但该建议更侧重异步/同步及死锁问题,我也不确定「快速」的具体定义。现提出以下问题:
- 两种方案各有哪些明显的优缺点?是否存在实质性差异?
- 除上述微软指南外,是否有权威的DI工厂最佳实践资料?
- 是否有公开代码示例展示DI工厂的常见使用场景?
问题1:两种方案的优缺点与实质性差异
原方案(DI容器中调用GetConfigFromDb())
优点:
- 配置获取逻辑集中在DI注册阶段,服务类无需感知
IConfigService,只依赖最终的Configs对象,职责更单一 - 所有依赖
Configs的服务都能复用同一份DI注册逻辑,无需在每个服务中重复调用GetConfigFromDb() - 若后续需要调整配置获取方式,只需修改DI注册代码,无需改动多个服务类
缺点:
- 违反「DI工厂应快速同步」原则:
GetConfigFromDb()是跨数据中心的数据库查询,属于耗时操作,会拖慢服务启动或首次服务实例化速度;Web应用场景下可能导致首次请求响应延迟 - 隐藏依赖关系:从服务类构造函数无法看出
Configs是通过数据库查询得到的,排查问题时需要追溯DI注册逻辑 - 服务定位器隐患:DI工厂中使用
p.GetService<IConfigService>()属于服务定位器模式,降低代码可测试性——测试时需模拟DI容器的服务获取逻辑,而非直接注入依赖
修改后方案(服务类中调用GetConfigFromDb())
优点:
- 符合DI工厂轻量化原则:DI容器仅负责注册服务,不执行耗时操作,保证服务启动/实例化速度
- 依赖关系透明:服务类构造函数明确依赖
IConfigService,从代码就能看出配置的获取来源,可读性和可维护性更好 - 可测试性更强:测试时可直接模拟
IConfigService返回预设的Configs,无需涉及DI容器的复杂逻辑
缺点:
- 配置获取逻辑分散:每个依赖
Configs的服务都需在构造函数中调用GetConfigFromDb(),后续调整获取逻辑时要修改多个服务类 - 服务类职责略有膨胀:原本只负责业务逻辑的服务,现在需要处理配置获取逻辑,有违反单一职责原则的倾向
实质性差异:
核心差异在于职责边界和性能时机:
- 原方案将配置获取职责交给DI容器,代价是牺牲DI工厂的轻量化,且依赖关系不透明
- 修改后方案将配置获取职责交还给服务类,保证DI工厂高效,但可能导致代码重复和职责模糊
另外,若GetConfigFromDb()是上下文相关的(比如依赖当前请求的用户信息),原方案在DI容器的瞬态注册中可能无法正确获取上下文,而修改后方案在服务实例化时(通常处于请求上下文内)调用,能正确获取上下文数据。
问题2:权威的DI工厂最佳实践资料
除微软官方指南外,以下是一些权威的DI最佳实践资料:
- 《Dependency Injection Principles, Practices, and Patterns》:由Mark Seemann和Steven van Deursen编写,是DI领域经典书籍,详细讲解DI的原则、模式和最佳实践,包含DI工厂的设计规范
- Autofac官方文档:作为.NET生态中流行的DI容器,其文档包含大量DI工厂的最佳实践,比如避免在工厂中执行耗时操作、规避服务定位器模式等
- Martin Fowler的DI相关文章:作为面向对象设计权威,他关于依赖注入和控制反转的文章中,提到了DI容器的使用原则,包括工厂逻辑的轻量化要求
问题3:DI工厂常见使用场景的公开代码示例
DI工厂(即DI容器中的工厂注册逻辑)常见使用场景包括:
场景1:创建依赖外部参数的实例
根据配置值创建不同的服务实现:
serviceCollection.AddTransient<IStorageService>(p => { var config = p.GetRequiredService<IConfiguration>(); var storageType = config["Storage:Type"]; return storageType switch { "Local" => new LocalStorageService(), "Cloud" => new CloudStorageService(p.GetRequiredService<ICloudClient>()), _ => throw new InvalidOperationException("无效的存储类型") }; });
场景2:包装第三方组件
当第三方组件未适配DI时,用工厂封装实例创建逻辑:
serviceCollection.AddTransient<IMessageQueue>(p => { var connectionString = p.GetRequiredService<IConfiguration>()["MessageQueue:ConnectionString"]; var queue = new ThirdPartyMessageQueue(connectionString); queue.Initialize(); // 执行必要的轻量初始化操作 return queue; });
场景3:创建瞬态的上下文相关实例
创建依赖当前用户ID的上下文对象:
serviceCollection.AddScoped<IUserContext>(p => { var httpContextAccessor = p.GetRequiredService<IHttpContextAccessor>(); var userId = httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; return new UserContext(userId); });
注意:这些场景中的工厂逻辑均为轻量、同步操作,不会包含跨网络、数据库查询等耗时任务,符合DI工厂的最佳实践。
内容的提问来源于stack exchange,提问作者Sebastian Lay
相关产品推荐
相关产品推荐

