C# 每个 Web 请求一个 DbContext ......为什么?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/10585478/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-09 14:23:33  来源:igfitidea点击:

One DbContext per web request... why?

c#asp.netentity-frameworkdependency-injectiondbcontext

提问by Andrew

I have been reading a lot of articles explaining how to set up Entity Framework's DbContextso that only one is created and used per HTTP web request using various DI frameworks.

我一直在阅读很多文章,解释如何设置实体框架,DbContext以便使用各种 DI 框架每个 HTTP Web 请求只创建和使用一个。

Why is this a good idea in the first place? What advantages do you gain by using this approach? Are there certain situations where this would be a good idea? Are there things that you can do using this technique that you can't do when instantiating DbContexts per repository method call?

为什么首先这是一个好主意?使用这种方法你有什么好处?在某些情况下这是一个好主意吗?在DbContext每个存储库方法调用实例化s时,您是否可以使用此技术执行某些操作?

采纳答案by Steven

NOTE: This answer talks about the Entity Framework's DbContext, but it is applicable to any sort of Unit of Work implementation, such as LINQ to SQL's DataContext, and NHibernate's ISession.

注意:此答案讨论了实体框架的DbContext,但它适用于任何类型的工作单元实现,例如 LINQ to SQLDataContext和 NHibernate 的ISession.

Let start by echoing Ian: Having a single DbContextfor the whole application is a Bad Idea. The only situation where this makes sense is when you have a single-threaded application and a database that is solely used by that single application instance. The DbContextis not thread-safe and and since the DbContextcaches data, it gets stale pretty soon. This will get you in all sorts of trouble when multiple users/applications work on that database simultaneously (which is very common of course). But I expect you already know that and just want to know why not to just inject a new instance (i.e. with a transient lifestyle) of the DbContextinto anyone who needs it. (for more information about why a single DbContext-or even on context per thread- is bad, read this answer).

让我们从回应 Ian 开始:DbContext为整个应用程序使用单个是一个坏主意。唯一有意义的情况是当您有一个单线程应用程序和一个仅由该单个应用程序实例使用的数据库时。该DbContext不是线程安全的,而且由于DbContext高速缓存数据,它陈旧很快得到。当多个用户/应用程序同时在该数据库上工作时,这会给您带来各种麻烦(这当然很常见)。但是我希望您已经知道这一点,并且只想知道为什么不将 的新实例(即具有短暂的生活方式)DbContext注入需要它的任何人。(有关为什么单个DbContext- 甚至每个线程的上下文 - 不好的更多信息,请阅读此答案)。

Let me start by saying that registering a DbContextas transient could work, but typically you want to have a single instance of such a unit of work within a certain scope. In a web application, it can be practical to define such a scope on the boundaries of a web request; thus a Per Web Request lifestyle. This allows you to let a whole set of objects operate within the same context. In other words, they operate within the same business transaction.

首先让我说将 a 注册DbContext为瞬态可以工作,但通常您希望在某个范围内拥有这样一个工作单元的单个实例。在 Web 应用程序中,在 Web 请求的边界上定义这样的范围是可行的;因此,每个 Web 请求的生活方式。这允许您让一整套对象在同一上下文中运行。换句话说,它们在同一个业务交易中运作。

If you have no goal of having a set of operations operate inside the same context, in that case the transient lifestyle is fine, but there are a few things to watch:

如果您没有让一组操作在同一上下文中运行的目标,在这种情况下,短暂的生活方式很好,但有一些事情需要注意:

  • Since every object gets its own instance, every class that changes the state of the system, needs to call _context.SaveChanges()(otherwise changes would get lost). This can complicate your code, and adds a second responsibility to the code (the responsibility of controlling the context), and is a violation of the Single Responsibility Principle.
  • You need to make sure that entities [loaded and saved by a DbContext] never leave the scope of such a class, because they can't be used in the context instance of another class. This can complicate your code enormously, because when you need those entities, you need to load them again by id, which could also cause performance problems.
  • Since DbContextimplements IDisposable, you probably still want to Dispose all created instances. If you want to do this, you basically have two options. You need to dispose them in the same method right after calling context.SaveChanges(), but in that case the business logic takes ownership of an object it gets passed on from the outside. The second option is to Dispose all created instances on the boundary of the Http Request, but in that case you still need some sort of scoping to let the container know when those instances need to be Disposed.
  • 由于每个对象都有自己的实例,因此每个更改系统状态的类都需要调用_context.SaveChanges()(否则更改会丢失)。这会使您的代码复杂化,并为代码添加第二个职责(控制上下文的职责),并且违反了单一职责原则
  • 您需要确保实体 [由 a 加载和保存DbContext] 永远不会离开此类类的范围,因为它们不能在另一个类的上下文实例中使用。这会使您的代码变得非常复杂,因为当您需要这些实体时,您需要通过 id 再次加载它们,这也可能导致性能问题。
  • 由于DbContextimplements IDisposable,您可能仍然希望处理所有创建的实例。如果你想这样做,你基本上有两个选择。您需要在调用 之后立即以相同的方法处理它们context.SaveChanges(),但在这种情况下,业务逻辑会获得从外部传递的对象的所有权。第二个选项是在 Http 请求的边界上处理所有创建的实例,但在这种情况下,您仍然需要某种范围来让容器知道何时需要处理这些实例。

Another option is to notinject a DbContextat all. Instead, you inject a DbContextFactorythat is able to create a new instance (I used to use this approach in the past). This way the business logic controls the context explicitly. If might look like this:

另一种选择是根本不注入 a DbContext。相反,您注入了DbContextFactory能够创建新实例的 a(我过去曾使用这种方法)。通过这种方式,业务逻辑显式地控制上下文。如果可能看起来像这样:

public void SomeOperation()
{
    using (var context = this.contextFactory.CreateNew())
    {
        var entities = this.otherDependency.Operate(
            context, "some value");

        context.Entities.InsertOnSubmit(entities);

        context.SaveChanges();
    }
}

The plus side of this is that you manage the life of the DbContextexplicitly and it is easy to set this up. It also allows you to use a single context in a certain scope, which has clear advantages, such as running code in a single business transaction, and being able to pass around entities, since they originate from the same DbContext.

这样做的好处是您可以DbContext明确地管理生命周期,并且很容易进行设置。它还允许您在某个范围内使用单个上下文,这具有明显的优势,例如在单个业务事务中运行代码,并且能够传递实体,因为它们源自相同的DbContext.

The downside is that you will have to pass around the DbContextfrom method to method (which is termed Method Injection). Note that in a sense this solution is the same as the 'scoped' approach, but now the scope is controlled in the application code itself (and is possibly repeated many times). It is the application that is responsible for creating and disposing the unit of work. Since the DbContextis created after the dependency graph is constructed, Constructor Injection is out of the picture and you need to defer to Method Injection when you need to pass on the context from one class to the other.

缺点是您必须将DbContextfrom 方法传递到方法(称为方法注入)。请注意,从某种意义上说,此解决方案与“范围化”方法相同,但现在范围在应用程序代码本身中受到控制(并且可能会重复多次)。应用程序负责创建和配置工作单元。由于DbContext是在构建依赖图之后创建的,因此构造函数注入不在画面中,当您需要将上下文从一个类传递到另一个类时,您需要遵循方法注入。

Method Injection isn't that bad, but when the business logic gets more complex, and more classes get involved, you will have to pass it from method to method and class to class, which can complicate the code a lot (I've seen this in the past). For a simple application, this approach will do just fine though.

方法注入并没有那么糟糕,但是当业务逻辑变得更复杂,并且涉及更多类时,您将不得不将其从方法传递到方法,从类传递到类,这会使代码复杂化很多(我见过这在过去)。对于一个简单的应用程序,这种方法虽然很好。

Because of the downsides, this factory approach has for bigger systems, another approach can be useful and that is the one where you let the container or the infrastructure code / Composition Rootmanage the unit of work. This is the style that your question is about.

由于存在缺点,这种工厂方法适用于更大的系统,另一种方法可能很有用,那就是让容器或基础设施代码/组合根管理工作单元。这就是你的问题所涉及的风格。

By letting the container and/or the infrastructure handle this, your application code is not polluted by having to create, (optionally) commit and Dispose a UoW instance, which keeps the business logic simple and clean (just a Single Responsibility). There are some difficulties with this approach. For instance, were do you Commit and Dispose the instance?

通过让容器和/或基础设施处理此问题,您的应用程序代码不会因必须创建、(可选)提交和处置 UoW 实例而受到污染,这使业务逻辑保持简单和干净(只是单一职责)。这种方法存在一些困难。例如,您是否提交和处置实例?

Disposing a unit of work can be done at the end of the web request. Many people however, incorrectlyassume that this is also the place to Commit the unit of work. However, at that point in the application, you simply can't determine for sure that the unit of work should actually be committed. e.g. If the business layer code threw an exception that was caught higher up the callstack, you definitely don'twant to Commit.

处理工作单元可以在 Web 请求结束时完成。然而,许多人错误地认为这也是提交工作单元的地方。但是,在应用程序的那个点上,您根本无法确定确实应该提交工作单元。例如,如果业务层的代码抛出异常是被提到的调用堆栈越高,你肯定希望提交。

The real solution is again to explicitly manage some sort of scope, but this time do it inside the Composition Root. Abstracting all business logic behind the command / handler pattern, you will be able to write a decorator that can be wrapped around each command handler that allows to do this. Example:

真正的解决方案是再次明确管理某种范围,但这次是在 Composition Root 内部进行。抽象命令/处理程序模式背后的所有业务逻辑,您将能够编写一个装饰器,该装饰器可以包装在允许执行此操作的每个命令处理程序周围。例子:

class TransactionalCommandHandlerDecorator<TCommand>
    : ICommandHandler<TCommand>
{
    readonly DbContext context;
    readonly ICommandHandler<TCommand> decorated;

    public TransactionCommandHandlerDecorator(
        DbContext context,
        ICommandHandler<TCommand> decorated)
    {
        this.context = context;
        this.decorated = decorated;
    }

    public void Handle(TCommand command)
    {
        this.decorated.Handle(command);

        context.SaveChanges();
    } 
}

This ensures that you only need to write this infrastructure code once. Any solid DI container allows you to configure such a decorator to be wrapped around all ICommandHandler<T>implementations in a consistent manner.

这确保您只需编写一次此基础架构代码。任何实体 DI 容器都允许您配置这样的装饰器ICommandHandler<T>,以一致的方式包裹所有实现。

回答by RB.

What I like about it is that it aligns the unit-of-work (as the user sees it - i.e. a page submit) with the unit-of-work in the ORM sense.

我喜欢它的地方在于它使工作单元(如用户所见——即页面提交)与 ORM 意义上的工作单元保持一致。

Therefore, you can make the entire page submission transactional, which you could not do if you were exposing CRUD methods with each creating a new context.

因此,您可以使整个页面提交成为事务性的,如果您将 CRUD 方法公开并创建一个新的上下文,则无法做到这一点。

回答by Ian

I'm pretty certain it is because the DbContext is not at all thread safe. So sharing the thing is never a good idea.

我很确定这是因为 DbContext 根本不是线程安全的。所以分享东西从来都不是一个好主意。

回答by Miroslav Holec

I agree with previous opinions. It is good to say, that if you are going to share DbContext in single thread app, you'll need more memory. For example my web application on Azure (one extra small instance) needs another 150 MB of memory and I have about 30 users per hour. Application sharing DBContext in HTTP Request

我同意以前的意见。可以说,如果要在单线程应用程序中共享 DbContext,则需要更多内存。例如,我在 Azure 上的 Web 应用程序(一个额外的小实例)需要另外 150 MB 的内存,并且我每小时有大约 30 个用户。 应用程序在 HTTP 请求中共享 DBContext

Here is real example image: application have been deployed in 12PM

这是真实的示例图像:应用程序已在下午 12 点部署

回答by Dmitry S.

Another understated reason for not using a singleton DbContext, even in a single threaded single user application, is because of the identity map pattern it uses. It means that every time you retrieve data using query or by id, it will keep the retrieved entity instances in cache. The next time you retrieve the same entity, it will give you the cached instance of the entity, if available, with any modifications you have done in the same session. This is necessary so the SaveChanges method does not end up with multiple different entity instances of the same database record(s); otherwise, the context would have to somehow merge the data from all those entity instances.

另一个不使用单例 DbContext 的低调原因,即使在单线程单用户应用程序中,也是因为它使用的身份映射模式。这意味着每次使用查询或通过 id 检索数据时,它都会将检索到的实体实例保存在缓存中。下次您检索同一实体时,它将为您提供该实体的缓存实例(如果可用),以及您在同一会话中所做的任何修改。这是必要的,因此 SaveChanges 方法不会以同一数据库记录的多个不同实体实例结束;否则,上下文必须以某种方式合并来自所有这些实体实例的数据。

The reason that is a problem is a singleton DbContext can become a time bomb that could eventually cache the whole database + the overhead of .NET objects in memory.

这是一个问题的原因是单例 DbContext 可能成为一个定时炸弹,最终可能会缓存整个数据库 + 内存中 .NET 对象的开销。

There are ways around this behavior by only using Linq queries with the .NoTracking()extension method. Also these days PCs have a lot of RAM. But usually that is not the desired behavior.

通过仅将 Linq 查询与.NoTracking()扩展方法一起使用,可以通过多种方法来解决此行为。此外,如今的 PC 拥有大量 RAM。但通常这不是理想的行为。

回答by user4893106

Not a single answer here actually answers the question. The OP did not ask about a singleton/per-application DbContext design, he asked about a per-(web)request design and what potential benefits could exist.

这里没有一个答案实际上回答了这个问题。OP 没有询问单例/每个应用程序 DbContext 设计,他询问了每个(Web)请求设计以及可能存在的潜在好处。

I'll reference http://mehdi.me/ambient-dbcontext-in-ef6/as Mehdi is a fantastic resource:

我将参考http://mehdi.me/ambient-dbcontext-in-ef6/因为 Mehdi 是一个很棒的资源:

Possible performance gains.

Each DbContext instance maintains a first-level cache of all the entities its loads from the database. Whenever you query an entity by its primary key, the DbContext will first attempt to retrieve it from its first-level cache before defaulting to querying it from the database. Depending on your data query pattern, re-using the same DbContext across multiple sequential business transactions may result in a fewer database queries being made thanks to the DbContext first-level cache.

It enables lazy-loading.

If your services return persistent entities (as opposed to returning view models or other sorts of DTOs) and you'd like to take advantage of lazy-loading on those entities, the lifetime of the DbContext instance from which those entities were retrieved must extend beyond the scope of the business transaction. If the service method disposed the DbContext instance it used before returning, any attempt to lazy-load properties on the returned entities would fail (whether or not using lazy-loading is a good idea is a different debate altogether which we won't get into here). In our web application example, lazy-loading would typically be used in controller action methods on entities returned by a separate service layer. In that case, the DbContext instance that was used by the service method to load these entities would need to remain alive for the duration of the web request (or at the very least until the action method has completed).

可能的性能提升。

每个 DbContext 实例都维护其从数据库加载的所有实体的一级缓存。每当您通过主键查询实体时,DbContext 将首先尝试从其一级缓存中检索它,然后默认从数据库中查询它。根据您的数据查询模式,由于 DbContext 一级缓存,跨多个顺序业务事务重用相同的 DbContext 可能会导致更少的数据库查询。

它启用延迟加载。

如果您的服务返回持久实体(而不是返回视图模型或其他类型的 DTO)并且您希望利用这些实体的延迟加载,则从中检索这些实体的 DbContext 实例的生命周期必须超出商业交易的范围。如果服务方法在返回之前处理了它使用的 DbContext 实例,则任何对返回实体的延迟加载属性的尝试都将失败(使用延迟加载是否是一个好主意是一个完全不同的辩论,我们不会进入这里)。在我们的 Web 应用程序示例中,延迟加载通常用于由单独服务层返回的实体的控制器操作方法中。在这种情况下,

Keep in mind there are cons as well. That link contains many other resources to read on the subject.

请记住,也有缺点。该链接包含许多其他有关该主题的阅读资源。

Just posting this in case someone else stumbles upon this question and doesn't get absorbed in answers that don't actually address the question.

只是发布这个以防其他人偶然发现这个问题并且没有专注于实际上没有解决问题的答案。

回答by Rick Strahl

One thing that's not really addressed in the question or the discussion is the fact that DbContext can't cancel changes. You can submit changes, but you can't clear out the change tree, so if you use a per request context you're out of luck if you need to throw changes away for whatever reason.

问题或讨论中没有真正解决的一件事是 DbContext 无法取消更改。您可以提交更改,但您无法清除更改树,因此如果您使用每个请求上下文,如果您出于任何原因需要丢弃更改,那么您就不走运了。

Personally I create instances of DbContext when needed - usually attached to business components that have the ability to recreate the context if required. That way I have control over the process, rather than having a single instance forced onto me. I also don't have to create the DbContext at each controller startup regardless of whether it actually gets used. Then if I still want to have per request instances I can create them in the CTOR (via DI or manually) or create them as needed in each controller method. Personally I usually take the latter approach as to avoid creating DbContext instances when they are not actually needed.

我个人在需要时创建 DbContext 的实例 - 通常附加到能够在需要时重新创建上下文的业务组件。这样我就可以控制整个过程,而不是将单个实例强加给我。我也不必在每个控制器启动时创建 DbContext,而不管它是否真的被使用。然后,如果我仍然想要每个请求实例,我可以在 CTOR 中创建它们(通过 DI 或手动)或根据需要在每个控制器方法中创建它们。我个人通常采用后一种方法,以避免在实际不需要时创建 DbContext 实例。

It depends from which angle you look at it too. To me the per request instance has never made sense. Does the DbContext really belong into the Http Request? In terms of behavior that's the wrong place. Your business components should be creating your context, not the Http request. Then you can create or throw away your business components as needed and never worry about the lifetime of the context.

这也取决于你从哪个角度看它。对我来说,每个请求实例从来没有意义。DbContext 真的属于 Http Request 吗?就行为而言,这是错误的地方。您的业​​务组件应该创建您的上下文,而不是 Http 请求。然后您可以根据需要创建或丢弃您的业务组件,而不必担心上下文的生命周期。

回答by Ted Elliott

Another issue to watch out for with Entity Framework specifically is when using a combination of creating new entities, lazy loading, and then using those new entities (from the same context). If you don't use IDbSet.Create (vs just new), Lazy loading on that entity doesn't work when its retrieved out of the context it was created in. Example:

使用 Entity Framework 需要特别注意的另一个问题是使用创建新实体、延迟加载和使用这些新实体(来自同一上下文)的组合时。如果您不使用 IDbSet.Create(而不是新的),则当从创建它的上下文中检索该实体时,该实体上的延迟加载将不起作用。示例:

 public class Foo {
     public string Id {get; set; }
     public string BarId {get; set; }
     // lazy loaded relationship to bar
     public virtual Bar Bar { get; set;}
 }
 var foo = new Foo {
     Id = "foo id"
     BarId = "some existing bar id"
 };
 dbContext.Set<Foo>().Add(foo);
 dbContext.SaveChanges();

 // some other code, using the same context
 var foo = dbContext.Set<Foo>().Find("foo id");
 var barProp = foo.Bar.SomeBarProp; // fails with null reference even though we have BarId set.

回答by Anestis Kivranoglou

There are two contradictingrecommendations by microsoft and many people use DbContexts in a completely divergent manner.

微软有两个相互矛盾的建议,许多人以完全不同的方式使用 DbContexts。

  1. One recommendation is to "Dispose DbContexts as soon as posible"because having a DbContext Alive occupies valuable resources like db connections etc....
  2. The other states that One DbContext per request is highly reccomended
  1. 一个建议是“尽快处理 DbContexts”,因为 DbContext Alive 会占用宝贵的资源,例如 db 连接等......
  2. 另一个声明强烈推荐每个请求一个 DbContext

Those contradict to each other because if your Request is doing a lot of unrelated to the Db stuff , then your DbContext is kept for no reason. Thus it is waste to keep your DbContext alive while your request is just waiting for random stuff to get done...

这些相互矛盾,因为如果您的 Request 做了很多与 Db 无关的事情,那么您的 DbContext 就会无缘无故地保留下来。因此,当您的请求只是在等待随机的事情完成时,让您的 DbContext 保持活动状态是浪费...

So many people who follow rule 1have their DbContexts inside their "Repository pattern"and create a new Instance per Database Queryso X*DbContextper Request

许多遵循规则 1 的人将他们的 DbContexts 放在他们的“存储库模式”中,为每个数据库查询创建一个新实例,因此每个请求X*DbContext

They just get their data and dispose the context ASAP. This is considered by MANYpeople an acceptable practice. While this has the benefits of occupying your db resources for the minimum time it clearly sacrifices all the UnitOfWorkand Cachingcandy EF has to offer.

他们只是获取他们的数据并尽快处理上下文。许多人认为这是一种可接受的做法。虽然这样做的好处是可以在最短的时间内占用您的数据库资源,但它显然牺牲了EF 必须提供的所有UnitOfWork缓存糖果。

Keeping alive a single multipurposeinstance of DbContext maximizes the benefits of Cachingbut since DbContext is not thread safeand each Web request runs on it's own thread, a DbContext per Request is the longestyou can keep it.

保持DbContext的单个多用途实例的活动最大化缓存的好处,但由于 DbContext不是线程安全的,并且每个 Web 请求都在其自己的线程上运行,因此每个请求的 DbContext 是您可以保留的最长时间

So EF's team recommendation about using 1 Db Context per request it's clearly based on the fact that in a Web Application a UnitOfWork most likely is going to be within one request and that request has one thread. So one DbContext per request is like the ideal benefit of UnitOfWork and Caching.

因此,EF 的团队建议每个请求使用 1 Db 上下文,这显然是基于这样一个事实,即在 Web 应用程序中,UnitOfWork 很可能位于一个请求中,并且该请求具有一个线程。因此,每个请求一个 DbContext 就像是 UnitOfWork 和缓存的理想优势。

Butin many cases this is not true. I consider Logginga separate UnitOfWork thus having a new DbContext for Post-Request Logging in async threadsis completely acceptable

在许多情况下,事实并非如此。我考虑记录一个单独的 UnitOfWork,因此在异步线程中为 Post-Request Logging 设置一个新的 DbContext是完全可以接受的

So Finally it turns down that a DbContext's lifetime is restricted to these two parameters. UnitOfWorkand Thread

所以最后它拒绝了 DbContext 的生命周期仅限于这两个参数。工作单元线程