You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

单例服务中获取依赖多实例:注入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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 16:20:10