.Net Core: 如何初始化一个需要DBContext的单例?
.Net Core: 如何初始化一个需要DBContext的单例?
我有一个 .Net Core 服务("MyLookup"),它执行数据库查询、一些活动目录查找,并将结果存储到内存缓存中。
在第一次尝试中,我在 Startup.cs 中使用 .AddService<>()
,将该服务注入到使用该服务的每个控制器和视图的构造函数中...一切都正常工作。
这是因为我的服务 - 以及它依赖的服务(IMemoryCache 和 DBContext)都是scoped的。但现在我想将这个服务改为singleton。我想在应用程序初始化时初始化它(执行数据库查询、AD查找,并将结果保存到内存缓存)。
问:我该如何做?
Startup.cs
public void ConfigureServices(IServiceCollection services) { services.AddDbContext(options => options.UseSqlServer(Configuration.GetConnectionString("MyDBContext"))); services.AddMemoryCache(); services.AddSingleton (); ... } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { ... // 问:这是初始化我的单例(并执行昂贵的数据库/AD查找)的好地方吗? app.ApplicationServices.GetService (); }
OneOfMyClients.cs
public IndexModel(MyDBContext context, IMyLookup myLookup) { _context = context; _myLookup = myLookup; ... }
MyLookup.cs
public class MyLookup : IMyLookup { ... public MyLookup (IMemoryCache memoryCache) { // 执行一些昂贵的查找,并将结果保存到这个缓存中 _cache = memoryCache; } ... private async void Rebuild() // 这应该只在应用程序启动时调用一次 { ClearCache(); var allNames = QueryNamesFromDB(); ... } private ListQueryNamesFromDB() { // 问:如何获取“_context”(它是一个scoped依赖项)? var allNames = _context.MyDBContext.Select(e => e.Name).Distinct().ToList (); return allSInames; } }
我尝试了不同的方法,遇到了一些异常:
InvalidOperationException: 无法从singleton 'MyLookup'中使用scoped服务 'MyDBContext'。
... 和 ...
InvalidOperationException: 无法从根提供程序 'MyLookup' 解析scoped服务 'MyDBContext'。
... 或者 ...
System.InvalidOperationException: 无法从根提供程序解析scoped服务 'IMyLookup'。
感谢 Steve 提供的宝贵见解。我最终能够实现以下目标:
- 创建一个“查找器”,可以在应用程序的整个生命周期内由任何消费者随时使用。
- 在程序启动时初始化一次。
值得注意的是,将初始化推迟到某个用户触发它的时候是不可接受的 - 初始化所需的时间太长。
- 使用依赖服务(IMemoryCache 和我的DBContext),无论这些服务的生命周期如何。
我的最终代码:
Startup.cs
public void ConfigureServices(IServiceCollection services) { services.AddDbContext(options => options.UseSqlServer(Configuration.GetConnectionString("MyDBContext"))); services.AddMemoryCache(); services.AddSingleton (); ... } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // 这最终成功地工作了... app.ApplicationServices.GetService ().Rebuild(); }
OneOfMyClients.cs
public IndexModel(MyDBContext context, IMyLookup myLookup) { // 对于所有的消费者,这部分代码保持不变 _context = context; _myLookup = myLookup; ... }
MyLookup.cs
public interface IMyLookup { Task> GetNames(string name); Task Rebuild(); } public class MyLookup : IMyLookup { private readonly IMemoryCache _cache; private readonly IServiceScopeFactory _scopeFactory; ... public MyLookup (IMemoryCache memoryCache, IServiceScopeFactory scopeFactory) { _cache = memoryCache; _scopeFactory = scopeFactory; } private async void Rebuild() { ClearCache(); var allNames = QueryNamesFromDB(); ... } private List
QueryNamesFromDB() { // 使用.CreateScope() 而不是构造函数注入是解决问题的关键 using (var scope = _scopeFactory.CreateScope()) { MyDBContext _context = scope.ServiceProvider.GetRequiredService (); var allNames = _context.MyTable.Select(e => e.Name).Distinct().ToList (); return allNames; } } }
问题的原因是,应用了不同的原则,如防止Captive Dependencies和Closure Composition Model。这些原则要求组件只能依赖于具有相等或更长生命周期的服务,同时要求在对象图中捕获运行时数据。然而,在需要在整个应用程序的生命周期内重用的状态的情况下,这些原则似乎无法实现。
解决方法之一是将状态从MyLookup中提取出来,放入一个不具有自身依赖(或仅依赖于单例)的依赖项中,然后将其变为单例。然后,MyLookup可以被“降级”为Scoped,并将运行时数据传递给负责缓存的单例依赖项。
另一种解决方法是在单个操作中包装一个作用域。通过使用IServiceScopeFactory创建和销毁IServiceScope,可以将MyLookup注入IServiceScopeFactory,并在作用域内获取MyDbContext。然而,这种方法的缺点是MyLookup现在需要依赖于DI容器,而只有作为组合根的类才应该知道DI容器的存在。
为了将对DI容器的依赖与业务逻辑分离,可以将整个类移动到组合根中,或者将类分成两个部分,使业务逻辑保持在组合根之外。
至于触发重建的位置和方式,这取决于何时需要触发重建。如果是定时器触发,应该放在组合根中。如果触发是一个Web请求,那就不太适合放在组合根中。
希望这些解决方法对你有帮助。在这里还提供了一个类似的问题的解答,你可以参考。