【问题标题】:.Net Core: How do I initialize a singleton that needs a DBContext?.Net Core:如何初始化需要 DBContext 的单例?
【发布时间】:2020-06-26 09:56:09
【问题描述】:

我有一个 .Net Core 服务(“MyLookup”),它执行数据库查询、一些 Active Directory 查找并将结果存储到内存缓存中。

对于我的第一个剪辑,我在 Startup.cs 中执行了.AddService<>(),将服务注入到每个使用该服务的控制器和视图的构造函数中......并且一切正常。

因为我的服务和它的依赖服务(IMemoryCache 和 DBContext)都是scoped,所以它起作用了。但现在我想将此服务设为singleton。我想在应用初始化时对其进行初始化(执行 DB 查询、AD 查找并将结果保存到内存缓存中)。

问:我该怎么做?

Startup.cs

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<MyDBContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("MyDBContext")));
        services.AddMemoryCache();
        services.AddSingleton<IMyLookup, MyLookup>(); 
        ...
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        ...
        // Q: Is this a good place to initialize my singleton (and perform the expensive DB/AD lookups?
        app.ApplicationServices.GetService<IDILookup>();   

OneOfMyClients.cs

    public IndexModel(MyDBContext context, IMyLookup myLookup)
    {
        _context = context;
        _myLookup = myLookup;
        ...

MyLookup.cs

public class MyLookup : IMyLookup
    ...
    public MyLookup (IMemoryCache memoryCache)
    {
        // Perform some expensive lookups, and save the results to this cache
        _cache = memoryCache;  
    }
    ...
    private async void Rebuild()  // This should only get called once, when app starts
    {
        ClearCache();
        var allNames =  QueryNamesFromDB();
        ...

    private List<string>QueryNamesFromDB()
    {
        // Q: ????How do I get "_context" (which is a scoped dependency)????
        var allNames = _context.MyDBContext.Select(e => e.Name).Distinct().ToList<string>();
        return allSInames;

我尝试过的一些例外情况:

InvalidOperationException:无法使用单例“MyLookup”中的作用域服务“MyDBContext”。

...和...

InvalidOperationException:无法从根提供程序“MyLookup”解析范围服务“MyDBContext”

...或...

System.InvalidOperationException:无法从根提供商解析范围服务“IMyLookup”。


感谢 Steve 提供宝贵的见解。我终于能够:

  1. 创建一个“查找”,在应用的生命周期内,任何消费者在任何时间、任何会话中都可以使用它。

  2. 在程序启动时初始化一次。 仅供参考,将初始化推迟到一些糟糕的用户触发它是可以接受的 - 初始化时间太长了。

  3. 使用依赖服务(IMemoryCache 和我的 DBContext),无论这些服务的生命周期如何。

我的最终代码:

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<MyDBContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("MyDBContext")));
    services.AddMemoryCache();
    // I got 80% of the way with .AddScoped()...
    // ... but I couldn't invoke it from Startup.Configure().
    services.AddSingleton<IMyLookup, MyLookup>(); 
    ...

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // This finally worked successfully...
    app.ApplicationServices.GetService<IMyLookup>().Rebuild();

OneOfMyClients.cs

public IndexModel(MyDBContext context, IMyLookup myLookup)
{
    // This remained unchanged (for all consumers)
    _context = context;
    _myLookup = myLookup;
    ...

MyLookup.cs

public interface IMyLookup
{
    Task<List<string>> 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<string>QueryNamesFromDB()
    {
        // .CreateScope() -instead of constructor DI - was the key to resolving the problem
        using (var scope = _scopeFactory.CreateScope())
        {
            MyDBContext _context =
                scope.ServiceProvider.GetRequiredService<MyDBContext>();
            var allNames = _context.MyTable.Select(e => e.Name).Distinct().ToList<string>();
            return allNames;
        }
    }

【问题讨论】:

  • 你绝对应该阅读Captive Dependencies
  • 你应该阅读DI Composition Models
  • 我读了很多书,但我都没有看到这些链接。谢谢 :) 重要提示:大多数示例都有“视图”或“控制器”。我需要在调用任何视图或控制器之前初始化我的缓存。我需要使用.Net Core DI;我不能使用“Ninject”(如果重要的话)。也许我什至可以在没有任何 DI 的情况下获得我的 DBContext :)
  • @Steven - 您的一个链接指向this link,这表明Instead of injecting the dependency, inject a factory for the creation of that dependency and call that factory every time an instance is required. 问:听起来很有希望?问:我可以在 .Net Core 中使用我的 DBContext 执行此操作的任何链接?
  • 为什么你首先需要你的服务成为单身人士?

标签: c# .net-core dependency-injection


【解决方案1】:

没有一个单一的解决方案可以解决您的问题。在起作用的是不同的原则,例如防止Captive Dependencies 的想法,它指出组件应该只依赖于具有相同或更长生命周期的服务。这个想法推动了MyLookup 类的作用,该类具有范围或短暂的生活方式。

这个想法归结为实践Closure Composition Model,这意味着您可以编写对象图,在图组件的变量中捕获运行时数据。相反的组合模型是Ambient Composition Model,它将状态保持在对象图之外并允许按需检索状态(例如您的DbContext)。

但这都是理论。起初,可能很难将其转化为实践。 (再次)理论上,应用闭包组合模型很简单,因为它只是意味着给MyLookup 一个更短的生活方式,例如Scoped。但是当MyLookup 本身捕获需要在应用程序期间重复使用的状态时,这似乎是不可能的。

但通常情况并非如此。一种解决方案是将状态从MyLookup 中提取出来,放入一个不包含其自身依赖项(或仅依赖于单例)的依赖项中,然后变成一个单例。 MyLookup 可以被“降级”为 Scoped 并将运行时数据传递给其执行缓存的单例依赖项。我很乐意向您展示一个示例,但您的问题需要更多详细信息才能做到这一点。

但如果你想让MyLookup 成为单身人士,肯定有办法做到这一点。例如,您可以将单个操作包装在范围内。示例:

public class MyLookup : IMyLookup
    ...
    public MyLookup (IMemoryCache memoryCache, IServiceScopeFactory scopeFactory)
    {
        _cache = memoryCache;
        _scopeFactory = scopeFactory;
    }

    private List<string> QueryNamesFromDB()
    {
        using (var scope = _scopeFactory.CreateScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<MyDbContext>();
            var allNames = context.Persons.Select(e => e.Name).Distinct().ToList<string>();
            return allSInames;
        }
    }
}

在此示例中,MyLookup 被注入 IServiceScopeFactory。这允许在单个调用中创建(和销毁)IServiceScope。这种方法的缺点是MyLookup 现在需要依赖 DI 容器。只有属于 Composition Root 的类才应该知道 DI 容器的存在。

因此,一种常见的方法是注入 Func&lt;MyDbContext&gt; 依赖项。但这实际上对于 MS.DI 来说是相当困难的,因为当你尝试这个时,工厂的作用域是根容器,而你的 DbContext 总是需要作用域。有一些方法可以解决这个问题,但由于我这边的时间限制,我不会深入讨论,因为这只会使我的答案复杂化。

要将 DI 容器的依赖与业务逻辑分离,您必须:

  • 将这个完整的类移动到您的合成根中
  • 或将类一分为二,让业务逻辑保持在Composition Root之外;例如,您可以使用子类或组合来实现此目的。

【讨论】:

  • 美丽。特别感谢您将“理论”转化为具体示例 :) 您的链接 - 以及此回复 - 给了我很多有用的信息。我会让你知道情况如何。跟随问题: 问:假设我再次将“MyLookup”设为范围:哪里是一个好地方/什么是调用“MyLookup.Rebuild()”的好方法?在 Startup.Configure() 中,如我上面的示例所示?
  • 赞成答案并在这里找到了类似的答案stackoverflow.com/questions/36332239/…
  • @FoggyDay:“哪里是个好地方/什么是调用“MyLookup.Rebuild()”的好方法?”这完全取决于应该触发重建的内容。如果它是一个计时器,它应该是 Composition Root。如果触发器是 Web 请求;没那么多。
  • 我要做的就是创建一个“查找服务”:在“启动”期间构建一些数据结构并将它们保存在内存缓存中,在任何请求进入之前。这是一件相当简单的事情在其他环境中进行。这似乎有问题,因为 .Net Core 似乎希望我的缓存、我的 DBContext 和查找“服务”本身都使用 DI :( Help.
猜你喜欢
  • 2021-11-08
  • 1970-01-01
  • 1970-01-01
  • 2017-07-06
  • 1970-01-01
  • 1970-01-01
  • 2018-11-20
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多