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

40
我正在尝试使用EF Core文档中的DbContext配置部分讨论的新DbContextFactory模式。我已经成功地在我的Blazor应用中启动了DbContextFactory,但我想保留直接注入DbContext实例的选项,以保持现有代码的工作。但是,当我尝试这样做时,我会收到类似以下内容的错误提示:“System.AggregateException:无法构造某些服务(验证服务描述符时出错'ServiceType:Microsoft.EntityFrameworkCore.IDbContextFactory1[MyContext] Lifetime:Singleton ImplementationType:Microsoft.EntityFrameworkCore.Internal.DbContextFactory 1 [MyContext] ':无法从单例'Microsoft.EntityFrameworkCore.IDbContextFactory 1 [MyContext]' 消耗范围服务'Microsoft.EntityFrameworkCore.DbContextOptions 1 [MyContext]'。)--->System.InvalidOperationException:验证服务描述符时出错'ServiceType:Microsoft.EntityFrameworkCore.IDbContextFactory1[MyContext] Lifetime:Singleton ImplementationType:Microsoft.EntityFrameworkCore.Internal.DbContextFactory1[MyContext]':无法从单例'Microsoft.EntityFrameworkCore.IDbContextFactory1[MyContext]'消耗范围服务'Microsoft.EntityFrameworkCore.DbContextOptions1[MyContext]'。--->System.InvalidOperationException:无法从单例'Microsoft.EntityFrameworkCore.IDbContextFactory1[MyContext]'消耗范围服务'Microsoft.EntityFrameworkCore.DbContextOptions1[MyContext]'。 我还曾在试验期间遇到过这个错误:“无法从根提供程序解析范围服务'Microsoft.EntityFrameworkCore.DbContextOptions1[MyContext]'。”理论上是否可能同时使用AddDbContextAddDbContextFactory
4个回答

59

重点在于理解各种元素的生存期,并正确设置这些生存期。

默认情况下,AddDbContextFactory()扩展方法创建的 DbContextFactory 具有 Singleton 生存期。如果使用 AddDbContext() 扩展方法并使用其默认设置,则会创建具有 Scoped 生命周期的 DbContextOptions请参见此处的源代码)。由于 Singleton 不能使用具有更短 Scoped 生命周期的对象,因此会引发错误。

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

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

在 EF Core 的 GitHub 仓库中,这里开始有一次很好的讨论。此外,在这里查看 DbContextFactory 的源代码也非常值得一试。

或者,您还可以通过在构造函数中设置 ServiceLifetime 参数来更改 DbContextFactory 的生命周期

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

选项应该被配置得完全一样,如同对于普通的DbContext所做的那样,因为这些选项将会被设置在工厂创建的DbContext上。


4
这个答案对于那些想在同一个项目中使用Blazor和MVC Core的人来说非常有用,尽管我不知道ApplyOurOptions是什么。如果这不是EF中我所不知道的一部分,也许考虑转换为使用普通选项? - Brian MacKay
1
嗨@BrianMacKay,那正是我们的情况。抱歉,ApplyOurOptions是我们代码中的一个辅助函数,我已经更改了代码并添加了一些说明。 - tomRedox
1
这是因为对于 AddDbContextFactory,你无法指定 optionsLifetimecontextLifetime 不同(至少如果有的话请告诉我!)。连接池也存在类似的情况。 - Simon_Weaver
1
即使到了2022年,@BrianMacKay的这个回答仍然有用,因为Blazor仍然使用标准的Razor页面来处理整个ASP.NET Core身份验证!我在我的Blazor应用程序中进行了脚手架构建,并以这种方式发现它 - 真是可怕! - Zimano
2
你的 AddDbContext 示例代码中有 contextLifetime: ServiceLifetime.Transient。这并没有什么问题,但它与默认值不同,即作用域为 scoped。由于重点是选项生命周期,你可能希望在示例中将服务生命周期保留为默认值以避免混淆(即删除具有 contextLifetime 的行)。 - Edward Brey
显示剩余4条评论

13

重要点:

AddDbContextFactoryAddDbContext都会在内部使用TryAdd来将DbContextOptions<T>注册到共享的私有方法AddCoreServices中。(来源

这实际上意味着你代码中先调用哪个函数,就会使用哪一个。

因此,你实际上可以采用以下方式进行更简洁的设置:

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);    

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


编辑:最终我制作了自己的上下文工厂,以便为创建的每个新上下文创建新选项。唯一的原因是允许非线程安全的拦截器,但如果您需要类似的东西,则应该可以使用此方法。

受以下项目启发: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
那真的很有趣,谢谢。我会看一下的。我想我会做完全相同的事情,只是加上一个提醒来帮助我记住正在发生的事情! - tomRedox
2
基本上整天都在探索异步相关的问题,以及一些周边问题。成功地从一个需要1.5秒的操作中节省了1秒钟。它每天只运行一次,所以我每年可以节省大约6分钟 :-) - Simon_Weaver
1
哇,谢谢你。我很惊讶看到这种行为。感觉.NET应该跟踪不同“请求者”使用哪些选项;我很高兴在我的情况下,无论是使用ContextPool还是ContextFactory,选项都是相同的。 - Jarvis
@Jarvis,它只是通过通用类型名称 DbContextOptions<MyContextName> 由依赖注入跟踪 - 因此,无论是工厂还是非工厂请求,您将获得相同的选项(并且作用域确定它是否为共享副本)。 - Simon_Weaver
@Ciantic 我通常会使用 (using var db = new ...),这是多年习惯造成的 - 所以这很可能是真的,我已经在绕过它了!我想知道如何确定。 - Simon_Weaver
显示剩余2条评论

3

从 EF Core 6 (.NET 6) 开始,在大多数情况下,您不需要同时使用两者,因为 AddDbContextFactory 还会将上下文类型本身注册为作用域服务。

因此,在具有 Singleton 生命周期的服务中注入 IDbContextFactory<MyDbContext>,在具有 Scoped 生命周期的服务(例如 MVC 或 API 控制器)中直接注入 MyDbContext

如果您想在每种情况下使用不同的 DbContextOptions,或者如果 DbContextOptionsSingleton 生命周期不符合您的需求,则必须使用其他答案提供的解决方案。

另请参见:
相关 Github 增强问题


2

我曾经遇到过类似的错误,但是我用了不同的方法解决了它,因为对于AddDbContextFactory我需要使用Sigleton。 我按照不同的顺序添加了服务,先添加AddDbContextFactory,然后再添加AddDbContext。以下是我的代码:

builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
    options.UseNpgsql(connectionString, c => c.UseNodaTime()));

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseNpgsql(connectionString, c => c.UseNodaTime()));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接