在同一个项目中同时使用 AddDbContextFactory() 和 AddDbContext() 扩展方法

Posted

技术标签:

【中文标题】在同一个项目中同时使用 AddDbContextFactory() 和 AddDbContext() 扩展方法【英文标题】:Use both AddDbContextFactory() and AddDbContext() extension methods in the same project 【发布时间】:2021-03-09 08:58:46 【问题描述】:

我正在尝试使用the DbContext configuration section of the EF Core docs 中讨论的新DbContextFactory 模式。

我已在我的 Blazor 应用程序中成功启动并运行 DbContextFactory,但我想保留直接注入 DbContext 实例的选项,以保持现有代码正常工作。

但是,当我尝试这样做时,我遇到了如下错误:

System.AggregateException:某些服务无法 构造(验证服务描述符时出错 '服务类型: Microsoft.EntityFrameworkCore.IDbContextFactory1[MyContext] Lifetime: Singleton ImplementationType: Microsoft.EntityFrameworkCore.Internal.DbContextFactory1[MyContext]': 无法使用范围服务 'Microsoft.EntityFrameworkCore.DbContextOptions1[MyContext]' from singleton 'Microsoft.EntityFrameworkCore.IDbContextFactory1[MyContext]'。)---> System.InvalidOperationException:验证服务时出错 描述符'服务类型: Microsoft.EntityFrameworkCore.IDbContextFactory1[MyContext] Lifetime: Singleton ImplementationType: Microsoft.EntityFrameworkCore.Internal.DbContextFactory1[MyContext]': 无法使用范围服务 'Microsoft.EntityFrameworkCore.DbContextOptions1[MyContext]' from singleton 'Microsoft.EntityFrameworkCore.IDbContextFactory1[MyContext]'。 ---> System.InvalidOperationException:无法使用范围服务 'Microsoft.EntityFrameworkCore.DbContextOptions1[MyContext]' from singleton 'Microsoft.EntityFrameworkCore.IDbContextFactory1[MyContext]'。

我在实验时也曾遇到过这个错误:

无法解析范围服务 'Microsoft.EntityFrameworkCore.DbContextOptions`1[MyContext]' 来自 根提供者。

理论上可以同时使用AddDbContextAddDbContextFactory吗?

【问题讨论】:

【参考方案1】:

是的,这完全是为了理解各种元素的生命周期并正确设置它们。

默认情况下,由AddDbContextFactory() 扩展方法创建的DbContextFactory 具有单例生命周期。如果您使用具有默认设置的AddDbContext() 扩展方法,它将创建具有Scoped 寿命(see the source-code here)的DbContextOptions,并且Singleton 不能使用具有较短Scoped 寿命的东西,抛出错误。

为了解决这个问题,我们需要将DbContextOptions 的生命周期更改为“Singleton”。这可以通过显式设置AddDbContext()DbContextOptions 参数的范围来完成

services.AddDbContext<FusionContext>(options =>
    options.UseSqlServer(YourSqlConnection),
    contextLifetime: ServiceLifetime.Transient, 
    optionsLifetime: ServiceLifetime.Singleton);

关于on the EF core GitHub repository starting here 的讨论非常好。还值得一看 DbContextFactory here 的源代码。

或者,您还可以通过setting the ServiceLifetime parameter in the constructor 更改DbContextFactory 的生命周期:

services.AddDbContextFactory<FusionContext>(options => 
    options.UseSqlServer(YourSqlConnection), 
    ServiceLifetime.Scoped);

这些选项应该完全配置为as you would for a normal DbContext,因为这些选项将在工厂创建的 DbContext 上设置。

【讨论】:

这个答案对于升级到 .NET 5 并希望在同一个项目中使用 Blazor 和 MVC Core 的人非常有用,尽管我不知道 ApplyOurOptions 是什么。如果这不是我不知道的 EF 的一部分,也许考虑将其转换为使用普通选项? 嗨@BrianMacKay,这正是我们的情况。抱歉,ApplyOurOptions 是我们代码中的辅助函数,我已经更改了代码并添加了一些说明。 @tomRedux 我敢打赌,在 .NET 5 之前,您必须和我做同样的事情并弄清楚如何实现自己的 AddDbContextFactory 扩展。美好时光。 :) 一个警告:如果您单独使用options.AddInterceptors(...)AddDbContext,您将为每个新的Scoped 上下文获得每个拦截器的新实例(因为拦截器是与选项一起创建的)。所以通常这意味着对于每个 Web 请求,您都会获得干净的拦截器。但是,如果您切换到使用AddDbContextFactory,您现在将能够创建多个独立的上下文,但它们都将共享拦截器(通过共享选项)。如果您的拦截器是完全线程安全的,这很好,但如果不是,请小心! ...这是因为对于AddDbContextFactory,您无法指定optionsLifetimecontextLifetime 不同(至少如果有让我知道!)。池化也有类似的情况。【参考方案2】:

重点:

AddDbContextFactoryAddDbContext 在内部使用 TryAdd 在共享私有方法 AddCoreServices 中注册 DbContextOptions&lt;T&gt;。 (source)

这实际上意味着代码中的任何一个第一个都是被使用的。

因此,您实际上可以这样做以获得更简洁的设置:

services.AddDbContext<RRStoreContext>(options => 

   // apply options

);

services.AddDbContextFactory<RRStoreContext>(lifetime: ServiceLifetime.Scoped);

我正在使用以下内容向自己证明它确实具有这样的功能:

services.AddDbContextFactory<RRStoreContext>(options =>

   throw new Exception("Oops!");  // this should never be reached

, ServiceLifetime.Scoped);    

不幸的是,我有一些不是线程安全的查询拦截器(这就是我想用工厂创建多个实例的全部原因),所以我认为我需要创建自己的上下文工厂,因为我有单独的初始化用于 Context 与 ContextFactory。


编辑:我结束了你制作自己的上下文工厂,以便能够为创建的每个新上下文创建新选项。唯一的原因是允许非线程安全的拦截器,但如果你需要它或类似的东西,那么这应该可以工作。

受以下影响:DbContextFactory

public class SmartRRStoreContextFactory : IDbContextFactory<RRStoreContext>

    private readonly IServiceProvider _serviceProvider;

    public SmartRRStoreContextFactory(IServiceProvider serviceProvider)
    
        _serviceProvider = serviceProvider;
    

    public virtual RRStoreContext CreateDbContext()
    
        // need a new options object for each 'factory generated' context
        // because of thread safety isuess with Interceptors
        var options = (DbContextOptions<RRStoreContext>) _serviceProvider.GetService(typeof(DbContextOptions<RRStoreContext>));
        return new RRStoreContext(options);
    

注意:我只有一个上下文需要这个,所以我在 CreateDbContext 方法中对新上下文进行硬编码。另一种方法是使用反射——类似DbContextFactorySource。

然后在我的 Startup.cs 中有:

services.AddDbContext<RRStoreContext>(options => 

    var connection = CONNECTION_STRING;

    options.UseSqlServer(connection, sqlOptions =>
    
        sqlOptions.EnableRetryOnFailure();
    );

    // this is not thread safe
    options.AddInterceptors(new RRSaveChangesInterceptor());

, optionsLifetime: ServiceLifetime.Transient);

// add context factory, this uses the same options builder that was just defined
// but with a custom factory to force new options every time
services.AddDbContextFactory<RRStoreContext, SmartRRStoreContextFactory>();  

我将以警告结束。如果您在“正常”注入的 DbContext 之外使用工厂 (CreateDbContext),请特别确保不要混合实体。例如,如果您在错误的上下文中调用 SaveChanges,那么您的实体将不会被保存。

【讨论】:

这真的很有趣,谢谢。我会看看那个。我想我会做同样的事情,除了帮助提醒我发生了什么事! 基本上花了一整天的时间来探索这个和异步的一些周边问题。设法从需要 1.5 秒的操作中节省 1 秒。它每天运行一次,所以我每年节省大约 6 分钟 :-) 哇,谢谢。我很惊讶地看到这种行为。感觉 .NET 应该跟踪哪些选项用于不同的“请求者”;我很高兴,就我而言,无论是使用 ContextPool 还是 ContextFactory,选项都是相同的。 @Jarvis 它只是通过使用泛型类型名称 DbContextOptions&lt;MyContextName&gt; 的依赖注入进行跟踪 - 因此无论工厂还是非工厂请求它,您都会获得相同的选项(并且范围决定是否不是共享副本) 嗯,当使用具有瞬态生命周期的 AddDbContext 时,它可能会泄漏类型。通常,作用域生命周期用于 DbContext,因为它在作用域结束后调用 dispose。但是当一个人使用瞬态时,用户必须调用我认为的处置,它可能会泄漏?

以上是关于在同一个项目中同时使用 AddDbContextFactory() 和 AddDbContext() 扩展方法的主要内容,如果未能解决你的问题,请参考以下文章

在同一个项目中同时使用 Realm Swift 和 Realm Objective-C

我们可以在一个项目中同时使用 Java Spring mvc 和 Spring Boot 吗?

在 django 中同时使用项目级和应用级静态文件

在项目中同时使用 bootstrap 和 material-UI

在单个 Google App Engine 项目中同时使用 Java 和 Python

我可以在单反应原生项目中同时使用 redux 和 Flux 吗?