.Net Core: 如何初始化一个需要DBContext的单例?

21 浏览
0 Comments

.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 提供的宝贵见解。我最终能够实现以下目标:

  1. 创建一个“查找器”,可以在应用程序的整个生命周期内由任何消费者随时使用。
  2. 在程序启动时初始化一次。

    值得注意的是,将初始化推迟到某个用户触发它的时候是不可接受的 - 初始化所需的时间太长。

  3. 使用依赖服务(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 ListQueryNamesFromDB()
    {
        // 使用.CreateScope() 而不是构造函数注入是解决问题的关键
        using (var scope = _scopeFactory.CreateScope())
        {
            MyDBContext _context =
                scope.ServiceProvider.GetRequiredService();
            var allNames = _context.MyTable.Select(e => e.Name).Distinct().ToList();
            return allNames;
        }
    }
}

0
0 Comments

问题的原因是,应用了不同的原则,如防止Captive Dependencies和Closure Composition Model。这些原则要求组件只能依赖于具有相等或更长生命周期的服务,同时要求在对象图中捕获运行时数据。然而,在需要在整个应用程序的生命周期内重用的状态的情况下,这些原则似乎无法实现。

解决方法之一是将状态从MyLookup中提取出来,放入一个不具有自身依赖(或仅依赖于单例)的依赖项中,然后将其变为单例。然后,MyLookup可以被“降级”为Scoped,并将运行时数据传递给负责缓存的单例依赖项。

另一种解决方法是在单个操作中包装一个作用域。通过使用IServiceScopeFactory创建和销毁IServiceScope,可以将MyLookup注入IServiceScopeFactory,并在作用域内获取MyDbContext。然而,这种方法的缺点是MyLookup现在需要依赖于DI容器,而只有作为组合根的类才应该知道DI容器的存在。

为了将对DI容器的依赖与业务逻辑分离,可以将整个类移动到组合根中,或者将类分成两个部分,使业务逻辑保持在组合根之外。

至于触发重建的位置和方式,这取决于何时需要触发重建。如果是定时器触发,应该放在组合根中。如果触发是一个Web请求,那就不太适合放在组合根中。

希望这些解决方法对你有帮助。在这里还提供了一个类似的问题的解答,你可以参考。

0