什么是最好的方式来注入 DbContext,是瞬态还是作用域?

huangapple go评论73阅读模式
英文:

What is the best to inject DbContext transient or scoped

问题

我真的很困惑该选择哪种,作用域 vs 瞬态。

如果我将 DbContext 注入为瞬态,我可以在一个请求中打开许多与 SQL 的连接,这是一个好处。

例如

var clientCountTask = _clientRepo.CountAsync();
var orderCountTask =  _orderRepo.CountAsnyc();
....
await Task.WhenAll(clientCountTask,orderCountTask,...);

此外,我可以通过一个 "工作单元" 类来处理事务,但首先我应该告诉你,工作单元是作用域注入的。

例如

await _unitOfWork.OrderRepo.AddAsync(order);
await _unitOfWork.Cart.ClearAsync();
await _unitOfWork.SaveChangesAsnyc();

对于作用域,我发现的优点是:

  1. 使用更少的内存
  2. 处理事务而无需工作单元类

作用域的缺点是:

  1. 我不能打开超过一个对数据库的请求

例如

var clientCountTask = _clientRepo.CountAsync();
var orderCountTask =  _orderRepo.CountAsnyc();
....
await Task.WhenAll(clientCountTask,orderCountTask,...);

这会抛出一个错误:

> 在上一个操作完成之前,在此上下文上启动了第二个操作。这通常是由于不同线程同时使用同一实例的 DbContext 引起的。有关如何避免与 DbContext 的线程问题的更多信息,请参见 https://go.microsoft.com/fwlink/?linkid=2097913

我的问题是:

  1. 使用瞬态注入我的 DbContext 有什么缺点?

  2. 何时应该使用作用域而不是瞬态?

  3. 何时应该使用瞬态而不是作用域?

英文:

I am really confused about which to choose scoped vs transient.

If I inject the DbContext as transient, I have the benefit that lets open many connections with SQL in one request.

For example

var clientCountTask = _clientRepo.CountAsync();
var orderCountTask =  _orderRepo.CountAsnyc();
....
await Task.WhenAll(clientCountTask,orderCountTask,...);

Also, I can handle the transaction through a "Unit-of-work" class, but first I should inform you that the unit of work is injected as scoped.

For example

await _unitOfWork.OrderRepo.AddAsync(order);
await _unitOfWork.Cart.ClearAsync();
await _unitOfWork.SaveChangesAsnyc();

For scoped the benefits that I found over transient are:

  1. uses less memory
  2. handles the transaction without the need to the unit of work class

The disadvantages for scoped are:

  1. I can't open more the one request to the database

For example

var clientCountTask = _clientRepo.CountAsync();
var orderCountTask =  _orderRepo.CountAsnyc();
....
await Task.WhenAll(clientCountTask,orderCountTask,...);

This throws an error:

> A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.

My questions are

  1. What are the disadvantages of using transient to inject my DbContext?

  2. When I should use scoped over transient?

  3. When I should use transient over scoped?

答案1

得分: 1

如果在同一范围内需要多个不同实例的上下文时,请考虑使用DbContext工厂方法:

services.AddDbContextFactory<ApplicationDbContext>(opts => ...);

它允许解析上下文,以及在需要实例时解析工厂,因此您可以在需要时构造实例:

private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;

public MyService(IDbContextFactory<ApplicationDbContext> contextFactory)
{
    _contextFactory = contextFactory;
}

public void DoSomething()
{
    using (var context = _contextFactory.CreateDbContext())
    {
        // ...
    }
}

注意:

  • 以这种方式创建的DbContext实例不由应用程序的服务提供程序管理,因此必须由应用程序进行处理。
  • 对于Web应用程序,是否实际有益于并行查询数据库存在一些争议(至少作为默认方法,需要在您的具体情况中测试是否有实际好处)。
英文:

If there are cases when you need several different instances of the context in the same scope - consider using DbContext factory approach:

services.AddDbContextFactory&lt;ApplicationDbContext&gt;(opts =&gt; ...);

It allows resolving both the context and when you need an instance - the factory, so you can construct an instance when needed:

private readonly IDbContextFactory&lt;ApplicationDbContext&gt; _contextFactory;

public MyService(IDbContextFactory&lt;ApplicationDbContext&gt; contextFactory)
{
    _contextFactory = contextFactory;
}

public void DoSomething()
{
    using (var context = _contextFactory.CreateDbContext())
    {
        // ...
    }
}

Notes:

  • The DbContext instances created in this way are not managed by the application's service provider and therefore must be disposed by the application.

  • For web applications there is some debate if it actually useful to query the database in parallel (at least as default approach, test if there are actual benefits in your concrete case).

答案2

得分: 0

在Web应用程序中,特别是在大多数情况下,最好使用范围限定的DbContext实例。根据您应用程序的结构,当您在不同的类之间分解操作,这些操作可能涉及返回实体之类的操作时,使用瞬态上下文可能会更加繁琐/有问题,因为从一个解析自己的瞬态DbContext返回的实体将不会由调用者中的DbContext跟踪。因此,调用另一个返回实例的类不能简单地与您正在填充的另一个实体相关联。您不能简单地使用Attach,因为这个DbContext实例可能已经在跟踪具有相同ID的不同实体实例,如果您忘记关联它,可能会导致约束违规或使用新ID重复数据,因为EF默认将实体视为新实例。 (EF核心可能不会出现重复数据问题,而是抱怨插入一个没有IDENTITY_INSERT=OFF的值)

当然,有些情况下,使用更短暂的生命周期范围是有道理的,例如,您希望在后台执行某些操作,或者只是执行一个操作,而不用担心DbContext被未持久化的更改污染,或者持久化您尚不想提交的更改。在这些情况下,您可以选择注入一个DbContextFactory,而不是或者与Scoped DbContext一起使用。此工厂可以使代码具有范围限定的瞬态实例的选项,并使用using块来管理其生命周期。因此,这不需要是二选一的情况。另一个考虑因素是,您只想能够隔离区域以避免污染,这时可以使用有界DbContexts。这本质上只是将实体拆分为专门构建的DbContext定义,而不是跨越整个域的单个DbContext。这对于性能和隔离更改等方面具有优势。

另一个选项是使用单元OfWork(工作单元),它有效地包装了DbContext并管理其生命周期范围。个人而言,我使用Mehdi el Gueddari最初为EF6编写的DbContextScope UoW模式,该模式已针对EF Core进行了维护:

https://github.com/zejji/DbContextScopeEFCore

...并且作为NuGet软件包提供。这旨在促进测试的存储库模式等操作。我无法强烈推荐这个UoW,因为它允许您根据需要限定一个或多个DbContext的生命周期范围,其中存储库(或DbContext使用者)可以定位范围以解析一个或多个DbContext实例,而无需传递引用。您还可以使用ForceCreateNew选项嵌套UoW范围。(否则,如果您在已经在一个范围内的调用中开始意外地限定新的UoW,它将抱怨,因为这通常是不被期望的)

英文:

For the most part in a web application in particular, a scoped DbContext instance is preferrable. Depending on how you structure your application, when you are breaking up operations across classes that might do things like return entities, using transient contexts can be more work / problematic since entities returned from one service that resolves it's own transient DbContext will not be tracked by the DbContext in the caller. So calling another class that returns instances cannot simply be associated to another entity you are populating. You cannot simply use Attach since this DbContext instance may or may not be already tracking a different entity instance with the same ID, and if you forget to associate it, you can end up with either constraint violations or duplicating data with new IDs as EF defaults to treating the entity as a new instance. (EF core may not do that duplicate data issue and instead complain about inserting a value without IDENTITY_INSERT=OFF)

There are certainly cases where it makes sense to use a more transient lifetime scope, where you want to execute something in the background, or simply perform an action without worrying about the DbContext becoming poisoned with un-persist-able changes, or persisting changes you don't want to commit yet. In these cases you can have the option to inject a DbContextFactory instead of, or alongside a Scoped DbContext. This factory can give code the option of scoping a transient instance, managing the lifetime with a using block. So it doesn't need to be an either-or. Another consideration where you just want to be able to isolate areas to avoid poisoning would be employing bounded DbContexts. This is essentially just splitting off entities into purpose-build DbContext definitions instead of a single DbContext that spans the entire domain. This has advantages for things like performance & isolating changes.

Another option is to use a unit of work that effectively wraps the DbContext entirely and manages its lifetime scope. Personally I use the DbContextScope UoW pattern by Mehdi el Gueddari originally written for EF6 which is maintained for EF Core:

https://github.com/zejji/DbContextScopeEFCore

... and available as a NuGet package. This is geared around facilitating things like a Repository pattern for testing. I cannot recommend this UoW enough as it allows you to scope the lifetime of one or more DbContext as you see fit where the Repositories (or DbContext consumers) can locate the scope to resolve a one or more DbContext instances rather than passing references around. You can also nest UoW scopes using the ForceCreateNew option. (Otherwise it will complain if you start accidentally scoping new UoW in calls already in a Scope as this is typically not expected)

huangapple
  • 本文由 发表于 2023年7月11日 01:27:57
  • 转载请务必保留本文链接:https://go.coder-hub.com/76656025.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定