使用BuildServiceProvider引发单例多实例的替代实现方案咨询
我尝试创建作为Background Service运行的依赖注入管道服务,希望能创建多个此类服务,通过传入简单的IOption配置设置每个步骤的可变值(按服务键加载),并将它们注册为键控服务避免相互覆盖。
同时我已将IQueueManager注册为单例,它可基于传入的IOptions配置获取生产者/消费者。
为每个管道版本传入不同服务键,核心简化代码如下:
public IServiceCollection AddPipelineService(this IServiceCollection services, IConfigurationManager configuration, string serviceKey) { // 根据服务键获取步骤配置 var stepOptions = Options.Create( configuration .GetSection($"{serviceKey}:{StepOptions.StepOptionsName}") .Get<MessageFactoryOptions>()); // 构建步骤列表并注册服务 services.AddKeyedSingleton<IStep, Step1>(serviceKey, (p,o) => new Step1(p.GetRequiredService<ILogger<ConfirmResponseMessageFactory>>(), stepOptions)); services.AddKeyedSingleton<IStep, Step2>(serviceKey, (p, o) => new Step2(p.GetRequiredService<ILogger<ConfirmResponseMessageFactory>>(), stepOptions)); services.AddKeyedSingleton<IStep, Step3>(serviceKey, (p, o) => new Step3(p.GetRequiredService<ILogger<ConfirmResponseMessageFactory>>(), stepOptions)); // 创建服务提供程序 var provider = services.BuildServiceProvider(validateScopes: true); // 将步骤添加到集合中 IEnumerable<IStep> steps = provider.GetKeyedServices<IMessageProcessItem>(serviceKey); services.AddKeyedSingleton<IStepCollection, StepCollection>(serviceKey, (p, o) => new StepCollection(steps)); // 根据服务键创建队列配置 var queueOptions = Options.Create( configuration .GetSection($"{serviceKey}:{QueueOptions.QueueOptionsName}") .Get<MessageFactoryOptions>()); // 说明 // IQueueManager已提前注册为单例(我只想打开一个连接并复用它来创建消费者和生产者) // PipelineBackgroundService是BackgroundService services.AddSingleton<IHostedService, PipelineBackgroundService>( p => new PipelineBackgroundService( p.GetRequiredService<ILogger<PipelineBackgroundService>>(), p.GetRequiredService<IQueueManager>(), p.GetRequiredKeyedService<IStepCollection>(serviceKey), queueOptions)); return services; }
目前代码可运行,但因调用BuildServiceProvider创建新容器,导致IQueueManager出现多个实例。已知应避免使用BuildServiceProvider,求无此副作用的替代实现方式?
核心思路是延迟获取步骤集合,不在注册阶段构建服务提供者,而是让StepCollection在首次被解析时,从最终的根服务提供者中获取对应服务键的所有IStep实例。
1. 重构StepCollection的构造方式
修改StepCollection,让它依赖IServiceProvider和服务键,在内部延迟加载步骤:
public class StepCollection : IStepCollection { private readonly IServiceProvider _serviceProvider; private readonly string _serviceKey; private IEnumerable<IStep>? _steps; public StepCollection(IServiceProvider serviceProvider, string serviceKey) { _serviceProvider = serviceProvider; _serviceKey = serviceKey; } public IEnumerable<IStep> GetSteps() { return _steps ??= _serviceProvider.GetKeyedServices<IStep>(_serviceKey).ToList(); } }
(如果原StepCollection是直接存储IEnumerable<IStep>,可以调整为通过方法获取,或者在构造时立即解析,但要确保是在最终服务提供者中完成解析)
2. 修改服务注册逻辑
移除BuildServiceProvider的调用,改为注册StepCollection时注入IServiceProvider和当前服务键:
public IServiceCollection AddPipelineService(this IServiceCollection services, IConfigurationManager configuration, string serviceKey) { // 根据服务键获取步骤配置 var stepOptions = Options.Create( configuration .GetSection($"{serviceKey}:{StepOptions.StepOptionsName}") .Get<MessageFactoryOptions>()); // 构建步骤列表并注册服务 services.AddKeyedSingleton<IStep, Step1>(serviceKey, (p,o) => new Step1(p.GetRequiredService<ILogger<ConfirmResponseMessageFactory>>(), stepOptions)); services.AddKeyedSingleton<IStep, Step2>(serviceKey, (p, o) => new Step2(p.GetRequiredService<ILogger<ConfirmResponseMessageFactory>>(), stepOptions)); services.AddKeyedSingleton<IStep, Step3>(serviceKey, (p, o) => new Step3(p.GetRequiredService<ILogger<ConfirmResponseMessageFactory>>(), stepOptions)); // 注册StepCollection,传入服务提供者和服务键,延迟加载步骤 services.AddKeyedSingleton<IStepCollection, StepCollection>(serviceKey, (p, o) => new StepCollection(p, serviceKey)); // 根据服务键创建队列配置 var queueOptions = Options.Create( configuration .GetSection($"{serviceKey}:{QueueOptions.QueueOptionsName}") .Get<MessageFactoryOptions>()); // 注册BackgroundService services.AddSingleton<IHostedService, PipelineBackgroundService>( p => new PipelineBackgroundService( p.GetRequiredService<ILogger<PipelineBackgroundService>>(), p.GetRequiredService<IQueueManager>(), p.GetRequiredKeyedService<IStepCollection>(serviceKey), queueOptions)); return services; }
3. 可选:使用工厂注册替代直接实例化
如果不想让StepCollection依赖IServiceProvider,可以使用工厂委托,在注册时明确解析步骤:
services.AddKeyedSingleton<IStepCollection>(serviceKey, (sp, key) => { var steps = sp.GetKeyedServices<IStep>(serviceKey); return new StepCollection(steps); });
这种方式同样不需要提前构建服务提供者,因为工厂委托会在最终服务提供者初始化时执行。
方案优势
- 不再调用
BuildServiceProvider,避免创建额外容器实例,确保IQueueManager始终为同一个单例。 - 步骤集合的解析延迟到应用启动后、首次使用时,此时使用的是根服务提供者,所有单例服务都只有一个实例。
内容的提问来源于stack exchange,提问作者Nick Tucker

