我该如何在MVC Core中管理DbContext生命周期?

29

根据文档,应该使用Scoped生命周期将Entity Framework上下文添加到服务容器中。如果按照上面显示的帮助方法,则会自动处理此操作。将使用Entity Framework的存储库应使用相同的生命周期。

我一直以为,我应该为每个要处理的工作单元创建一个新的Context。这让我想到,如果我有一个ServiceAServiceB,它们正在对DbContext应用不同的操作,那么它们应该获得DbContext的不同实例。

文档如下所述:

  • Transient对象始终是不同的;每个控制器和每个服务都提供一个新实例。

  • Scoped对象在请求内相同,在不同请求之间不同

回到ServiceAServiceB,听起来对我来说Transient更合适。

我已经研究过,Context应该仅在每个HttpRequest中保存一次,但我真的不明白这是如何工作的。

特别是如果我们看一个服务:

using (var transaction = dbContext.Database.BeginTransaction())
{
    //Create some entity
    var someEntity = new SomeEntity();
    dbContext.SomeEntity.Add(someEntity);

    //Save in order to get the the id of the entity
    dbContext.SaveChanges();

    //Create related entity
    var relatedEntity = new RelatedEntity
    {
        SomeEntityId = someEntity.Id
    };
    dbContext.RelatedEntity.Add(relatedEntity)
    dbContext.SaveChanges();
    transaction.Commit();
}

这里我们需要保存上下文以便获取与我们刚刚创建的实体相关联的实体的ID。

同时,另一个服务可能会更新相同的上下文。根据我所了解的,DbContext 不是线程安全的。

在这种情况下,我应该使用 Transient 吗?为什么文档建议我应该使用 Scoped

我是否遗漏了框架的某些重要部分?


4
永远不要使用瞬态的DbContext注册。瞬态生命周期将在每次被另一个服务请求时创建服务的新实例,这将导致每个请求多个DbContext。在同一请求内重复使用相同的DbContext实例没有问题:每个请求绑定到单个线程,因此完全不存在线程安全问题。 - Federico Dipuma
1
但是如果出于任何原因(例如Task.Start),服务并行处理会发生什么? - Christian Gollhardt
是的,例如如果我明确启动一个新线程,并想要注入DbContext。 我假设它是相同的DbContext实例,如果配置为“Scoped”?@FedericoDipuma - Christian Gollhardt
我只想让它可以同时处理多个服务,但当然在此之后同步它们。例如:Task.WhenAll(serviceA.DoSomethingWithContext, ServiceB.DoSomethingElseWithContext) @FedericoDipuma - Christian Gollhardt
3
我不明白并行执行I/O绑定操作(如数据库访问)的好处在哪里,但如果您真的想继续这条路,我认为您唯一的选择是注入某种工厂方法到您的DbContext中(类似于services.AddSingleton<Func<DbContext>>(s => new Func<DbContext>(() => new MyDbContext(/*..*/)));),并使用using语句自己管理它们的生命周期。 - Federico Dipuma
显示剩余4条评论
2个回答

35

正如其他人已经解释的那样,您应该使用作用域依赖项来确保数据库上下文将被正确重用。对于并发性,记住您也可以异步查询数据库,因此您可能不需要实际的线程。

如果您确实需要线程,即后台工作者,则很可能这些线程的生命周期与请求不同。因此,这些线程不应使用从请求范围检索的依赖项。当请求结束并关闭其依赖项范围时,可将一次性依赖项正确处置。对于其他线程,这意味着它们的依赖项可能会被处理,尽管它们仍然需要它们:不好的主意。

相反,您应该为每个创建的线程显式打开一个新的依赖范围。您可以通过注入IServiceScopeFactory并使用CreateScope来创建范围来实现这一点。然后,生成的对象将包含一个服务提供程序,您可以从中检索依赖项。由于这是一个单独的范围,因此像数据库上下文这样的作用域依赖项将在此范围的生命周期内重新创建。

为了避免进入服务定位器模式,您应该考虑有一个中央服务,您的线程执行该服务并汇集所有必要的依赖项。然后,线程可以执行以下操作:

using (var scope = _scopeFactory.CreateScope())
{
    var service = scope.ServiceProvider.GetService<BackgroundThreadService>();
    service.Run();
}

BackgroundThreadService及其所有依赖项可以遵循常规的依赖注入方式接收依赖项。


非常好 :) 感谢您详细的解释。 - Christian Gollhardt
即使在这种情况下,您也可以异步查询数据库,因此您可能不需要实际的线程。但是,在每次并发查询中都需要一个不同的 DbContext 实例。否则,您将收到关于不支持并发操作的错误。请参见 https://learn.microsoft.com/ef/core/querying/async 上的警告。 - binki
这个解决方案同样适用于创建一个fire and forget endpoint——ty _ = Task.Run(async () => { using (var scope = _serviceScopeFactory.CreateScope()) { - Buckrogerz

2
我相信在大多数情况下,使用作用域生命周期时不会遇到并发问题。即使在您发布的示例中也没有并发问题,因为当前请求中的服务将被连续调用。我甚至无法想象在一个HTTP请求(scope)的上下文中同时运行2个或更多服务的情况(这是可能的,但不常见)。
生命周期只是存储数据的一种方式(为了简单起见)。只需查看一些流行DI框架中的生命周期管理器,它们都工作得相当相似 - 这只是实现可处理模式的类似字典的对象。使用Transient,我相信您的获取对象方法将始终返回null,因此DI每次请求时都会创建新实例。SingleInstance将在类似静态并发字典的东西中存储对象,因此容器仅创建一次实例,然后接收现有实例。
Scoped通常意味着使用范围对象来存储已创建的对象。在asp.net管道中,这通常意味着与请求相同(因为范围可以通过管道传递)
简而言之-不要担心,只需使用scoped是安全的,并且您可以按请求调用它。
我试图非常简单地解释,您可以随时查看源代码以找到所需的详细信息。请参考https://github.com/aspnet/DependencyInjection

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