.NET 6如何初始化带依赖的单例服务实现启动时预加载数据
问题根因
你的代码存在三个核心问题,直接导致运行异常和设计缺陷:
- 循环依赖:
IpLocationService依赖IMemoryCacheProvider,MemoryCacheProvider构造函数又反向注入IIpLocationService,DI容器无法处理这种环形引用,无法确定实例创建顺序。 - 非法构建服务提供者:在服务注册阶段手动调用
BuildServiceProvider()会生成一套完全独立的DI容器副本,和后续应用正式运行使用的容器互不连通。你在这个独立容器里解析IIpLocationService时,容器内根本没有注册IMemoryCacheProvider(你是解析完才往builder的容器里加的注册),直接触发服务解析失败异常。同时这种写法会导致单例服务生成多份实例,引发内存泄漏、状态不一致问题。 - 生命周期不匹配:
IIpLocationService注册为Scoped生命周期,却被单例的MemoryCacheProvider长期持有引用,Scoped服务本应随请求Scope释放,被单例持有后会出现数据库连接泄漏、数据上下文脏读等问题。
正确实现方案
核心思路是打破循环依赖,把初始化触发逻辑从服务构造函数挪到应用启动阶段的正式管道中执行,完全满足启动即加载全量数据的需求:
- 首先重构
MemoryCacheProvider,移除对IIpLocationService的依赖,仅保留缓存存储能力:
public interface IMemoryCacheProvider { List<IpLocation> IpLocationList { get; } bool IsInitialized { get; } void LoadInitData(List<IpLocation> data); } public class MemoryCacheProvider : IMemoryCacheProvider { private readonly List<IpLocation> _ipLocationList = new(); public List<IpLocation> IpLocationList => _ipLocationList; public bool IsInitialized { get; private set; } public void LoadInitData(List<IpLocation> data) { if (IsInitialized) return; _ipLocationList.AddRange(data); IsInitialized = true; } }
- 修正服务注册逻辑,移除手动构建ServiceProvider、手动new服务实例的代码,所有服务统一交给DI容器管理:
// 注册数据库上下文、工作单元等基础服务 builder.Services.AddScoped<IUnitOfWork, UnitOfWork>(); // IpLocationService保持Scoped生命周期即可 builder.Services.AddScoped<IIpLocationService, IpLocationService>(); // 单例注册缓存提供者 builder.Services.AddSingleton<IMemoryCacheProvider, MemoryCacheProvider>();
- 在应用构建完成、启动运行前,创建临时Scope解析服务执行初始化,这部分逻辑会在应用监听请求前执行,完全符合启动预加载的要求:
var app = builder.Build(); // 执行启动预加载逻辑 using (var initScope = app.Services.CreateScope()) { var ipService = initScope.ServiceProvider.GetRequiredService<IIpLocationService>(); var cacheProvider = initScope.ServiceProvider.GetRequiredService<IMemoryCacheProvider>(); // 调用Init方法加载全量IP位置数据 var allIpData = ipService.Init(); // 写入单例缓存 cacheProvider.LoadInitData(allIpData); } // 后续正常配置中间件管道 // app.UseHttpsRedirection(); // app.UseAuthorization(); // app.MapControllers(); app.Run();
补充说明
- 如果预加载数据量极大,担心阻塞启动触发托管平台超时,可以把初始化逻辑封装为后台托管服务
IHostedService执行,但需要在缓存访问处加线程锁,避免应用启动后早期请求访问到未加载完成的空缓存。 - 永远不要在单例服务中直接注入Scoped生命周期服务,如果确实需要在单例中使用Scoped服务,要通过
IServiceScopeFactory创建临时Scope解析,使用完立即释放Scope。 - 所有服务解析操作必须基于
builder.Build()生成的正式应用服务提供者执行,不要在服务注册阶段手动调用BuildServiceProvider()创建独立容器。
内容的提问来源于stack exchange,提问作者srzsanti
相关产品推荐
相关产品推荐

