.NET 6异步任务循环中如何为每个任务注入独立服务实例
现有实现ISomethingManager接口的SomethingManager类,定义如下:
public class SomethingManager : ISomethingManager
该类位于.NET 6 Worker Service解决方案下的独立类库项目中,最初在Worker Service项目中按如下方式注册为单例:
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .UseSerilog() .ConfigureServices((hostContext, services) => { //AddSingleton or Transient here? services.AddSingleton<ISomethingManager, SomethingManager>(); // ...其他服务注册 });
项目中存在并发循环处理逻辑:
foreach (xml in xmls) { tasks.Add(StartProcessAsync(xml)); } await Task.WhenAll(tasks);
StartProcessAsync方法内部通过构造函数注入使用SomethingManager实例,但SomethingManager包含多个需要在每个任务中独立持有的私有成员:每个任务执行前需要先调用.Connect()获取独立的sessionId,不同任务的sessionId不能共享,当前单例注册的方式会导致多任务并发时出现状态冲突错误。
核心疑问是:如何通过DI注册机制保证每个任务持有的SomethingManager实例私有成员相互独立?如果无法通过DI实现,是否需要每次任务执行时手动创建新实例?示例代码如下:
public Task StartProcessAsync(xmlFileInfo xml) { return Task.Run(async () => { // 直接使用构造函数注入的实例在并发任务中会出问题,因为所有任务共享同一个sessionId //_somethingManager.DoSomething(); // 手动new实例是否可行? var somethingManager= SomethingManager(_someSettings); somethingManager.DoSomething(); var mem = somethingManager.ThePrivateMember; // 同类型的其他带私有成员的对象也存在相同问题 }); }
首先明确:当前用AddSingleton注册必然会出现状态冲突,因为单例服务在整个应用生命周期内仅创建一次,所有并发任务拿到的都是同一个实例,私有成员、sessionId会被共享。
直接把注册改成AddTransient也无法解决问题:Worker服务本身是单例生命周期(AddHostedService默认注册为单例),构造函数注入的服务只会在Worker实例化时创建一次,会被永久存到Worker的私有字段中,哪怕注册为Transient,实际使用的还是同一个实例,属于典型的** captive依赖问题**(短生命周期服务被长生命周期服务持有,实际生命周期被拉长到和宿主一致)。
下面给出两种符合DI规范的可行方案,不推荐直接手动new实例。
方案1:基于IServiceScopeFactory创建任务级作用域(推荐)
这是.NET DI原生支持的标准实现,自动管理服务生命周期和依赖注入,无需手动维护实例依赖。
- 调整服务注册,将
SomethingManager注册为Scoped生命周期即可,其依赖的配置、其他服务按原有方式注册:.ConfigureServices((hostContext, services) => { services.AddSerilog(); services.AddScoped<ISomethingManager, SomethingManager>(); // 注册配置绑定,例如SomethingManager依赖的_someSettings services.Configure<SomeSettings>(hostContext.Configuration.GetSection("SomeSettings")); services.AddHostedService<Worker>(); }); - 不要在Worker构造函数中直接注入
ISomethingManager,改为注入IServiceScopeFactory,在每个任务执行时创建独立的DI作用域,从作用域中获取独立的服务实例:public class Worker : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; // 注入作用域工厂 public Worker(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } public async Task StartProcessAsync(XmlFileInfo xml) { // 为当前任务创建独立的DI作用域,作用域内获取的Scoped/Transient服务都是独立实例 using var scope = _scopeFactory.CreateScope(); var somethingManager = scope.ServiceProvider.GetRequiredService<ISomethingManager>(); // 实例完全独立,当前Connect生成的sessionId不会和其他任务冲突 await somethingManager.Connect(); somethingManager.DoSomething(); var mem = somethingManager.ThePrivateMember; // 作用域using结束后,所有实现了IDisposable的服务会被容器自动释放,无需手动处理 } // 其他原有逻辑... }
方案2:注册强类型工厂委托
如果不想每次创建作用域,可以注册工厂委托,每次调用工厂直接获取新实例。
- 调整服务注册:
.ConfigureServices((hostContext, services) => { services.AddSerilog(); services.AddTransient<ISomethingManager, SomethingManager>(); // 注册工厂委托,单例存储工厂 services.AddSingleton<Func<ISomethingManager>>(sp => () => sp.GetRequiredService<ISomethingManager>() ); services.Configure<SomeSettings>(hostContext.Configuration.GetSection("SomeSettings")); services.AddHostedService<Worker>(); }); - 构造函数注入工厂委托,每个任务执行时调用工厂获取新实例:
public class Worker : BackgroundService { private readonly Func<ISomethingManager> _somethingManagerFactory; public Worker(Func<ISomethingManager> somethingManagerFactory) { _somethingManagerFactory = somethingManagerFactory; } public async Task StartProcessAsync(XmlFileInfo xml) { // 每次调用工厂都会返回全新的SomethingManager实例,实例间完全隔离 using var somethingManager = _somethingManagerFactory(); await somethingManager.Connect(); somethingManager.DoSomething(); var mem = somethingManager.ThePrivateMember; } }
注意:该方案下Transient实例如果被外部Dispose,容器不会重复释放;如果不手动加using包裹,实现了
IDisposable的服务会被容器跟踪直到应用退出,存在内存泄漏风险,优先使用方案1。
不推荐手动new实例的原因
示例中直接new SomethingManager(_someSettings)的方式虽然能临时跑通,但存在明显缺陷:
- 后续
SomethingManager新增依赖(比如ILogger、其他业务服务)时,所有手动new的地方都要同步修改传参,维护成本极高 - 无法享受DI容器的自动生命周期管理、依赖自动注入能力
- 单元测试时无法方便地注入Mock替代实现,测试难度提升
仅当SomethingManager是无任何外部依赖的纯静态工具类时,才考虑直接实例化。
内容的提问来源于stack exchange,提问作者Giannis Grivas

