单例服务中获取依赖多实例:注入ServiceProvider是否合规?
问题描述
我有一个结构如下的单例后台服务,它会轮询外部配置服务,收到新配置时创建对应的MyWorker实例:
class MyBackgroundService : BackgroundService { // ... Dictionary<int, MyWorker> _workers public MyBackgroundService(/*一堆注入的依赖*/) { // 将注入的依赖存储到私有字段中 } protected override Task ExecuteAsync(CancellationToken token) { while (true) { if (_myWatcher.GetNewConfigurations(out var newConfigurations)) { foreach (var newConfiguration in newConfigurations) { _workers[newConfiguration.Id] = new MyWorker(/*传入所有注入的私有字段*/); } } // 移除字典中不再需要的worker的代码 // 适时退出循环的代码 } } // ... }
目前的问题是:MyBackgroundService本身并不使用这些注入的依赖,只是用来实例化MyWorker,导致构造函数臃肿。
我考虑过两种改进方案,但都有疑虑:
- 直接注入
IServiceProvider,在创建MyWorker时调用GetService<MyWorker>(),代码更简洁,但感觉直接传递服务容器不太妥当。class MyBackgroundService : BackgroundService { // ... Dictionary<int, MyWorker> _workers private readonly IServiceProvider _serviceProvider; public MyBackgroundService(IServiceProvider sp) { _serviceProvider = sp; } protected override Task ExecuteAsync(CancellationToken token) { while (true) { if (_myWatcher.GetNewConfigurations(out var newConfigurations)) { foreach (var newConfiguration in newConfigurations) { _workers[newConfiguration.Id] = _serviceProvider.GetService<MyWorker>(); } } // 移除字典中不再需要的worker的代码 // 适时退出循环的代码 } } // ... } - 使用工厂模式,注入
MyWorkerFactory,但这只是把不需要的依赖转移到工厂中,本质问题没解决。
另外我曾误以为把MyWorker注册为Transient就能解决,但意识到因为MyBackgroundService是单例,如果直接注入MyWorker的话只会拿到同一个实例,无法创建多个。
请问这种场景下的正确实现方式是什么?
正确实现方式
1. 使用带参数的强类型工厂(推荐)
不要手动实现工厂,利用DI框架的特性创建强类型工厂,既避免直接依赖IServiceProvider,又能优雅传递配置参数给MyWorker。
步骤:
- 定义工厂接口,明确创建
MyWorker所需的参数:public interface IMyWorkerFactory { MyWorker Create(Configuration config); } - 让DI框架自动实现该工厂(以.NET Core/.NET 5+为例):
// 注册工厂,直接通过容器解析MyWorker的依赖并传入配置 services.AddTransient<IMyWorkerFactory>(sp => config => { var dep1 = sp.GetRequiredService<IWorkerDependency1>(); var dep2 = sp.GetRequiredService<IWorkerDependency2>(); return new MyWorker(dep1, dep2, config); }); - 在
MyBackgroundService中注入工厂并使用:class MyBackgroundService : BackgroundService { private readonly IMyWorkerFactory _workerFactory; private readonly Dictionary<int, MyWorker> _workers = new(); public MyBackgroundService(IMyWorkerFactory workerFactory) { _workerFactory = workerFactory; } protected override Task ExecuteAsync(CancellationToken token) { while (!token.IsCancellationRequested) { if (_myWatcher.GetNewConfigurations(out var newConfigurations)) { foreach (var config in newConfigurations) { _workers[config.Id] = _workerFactory.Create(config); } } // 移除过期worker的逻辑 await Task.Delay(TimeSpan.FromSeconds(5), token); } return Task.CompletedTask; } }
2. 使用IServiceScope处理Scoped依赖
如果MyWorker依赖Scoped服务(比如数据库上下文),直接从根容器获取会导致依赖生命周期和单例绑定,此时需要创建独立的服务范围:
class MyBackgroundService : BackgroundService { private readonly IServiceProvider _serviceProvider; // 存储worker和对应的服务范围,方便后续释放 private readonly Dictionary<int, (MyWorker Worker, IServiceScope Scope)> _workers = new(); public MyBackgroundService(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } protected override Task ExecuteAsync(CancellationToken token) { while (!token.IsCancellationRequested) { if (_myWatcher.GetNewConfigurations(out var newConfigurations)) { foreach (var config in newConfigurations) { // 创建新的服务范围,隔离Scoped依赖 var scope = _serviceProvider.CreateScope(); var worker = scope.ServiceProvider.GetRequiredService<MyWorker>(); // 传入配置(可通过方法注入或构造函数结合工厂) worker.SetConfiguration(config); _workers[config.Id] = (worker, scope); } } // 移除过期worker时,必须释放对应的服务范围,避免内存泄漏 var expiredIds = _workers.Keys.Where(id => /*判断worker是否过期*/).ToList(); foreach (var id in expiredIds) { _workers[id].Scope.Dispose(); _workers.Remove(id); } await Task.Delay(TimeSpan.FromSeconds(5), token); } // 服务停止时,释放所有剩余的服务范围 foreach (var entry in _workers.Values) { entry.Scope.Dispose(); } return Task.CompletedTask; } }
3. 需要避免的错误做法
- 不要在单例服务中直接注入
Transient或Scoped依赖:这会导致依赖被强制提升为单例生命周期,引发线程安全或资源泄漏问题。 - 不要滥用
IServiceProvider作为服务定位器:虽然可行,但会降低代码的可测试性和可读性,强类型工厂是更清晰的替代方案。
内容的提问来源于stack exchange,提问作者ewok
相关产品推荐
相关产品推荐

