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

人气:878 发布:2022-10-16 标签: asp.net dependency-injection c# entity-framework dbcontext

问题描述

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

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

为什么这是一个好主意?通过使用这种方法,您获得了哪些优势?在某些情况下这是一个好主意吗?在为每个存储库方法调用实例化 DbContexts 时,您是否可以使用这种技术做一些事情?

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,但是它适用于任何类型的工作单元实现,例如LINQ to SQL 的 DataContext 和 NHibernate 的 ISession.

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.

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

Let start by echoing Ian: Having a single DbContext for 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 DbContext is not thread-safe and and since the DbContext caches 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 DbContext into anyone who needs it. (for more information about why a single DbContext -or even on context per thread- is bad, read this answer).

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

Let me start by saying that registering a DbContext as 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.

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

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:

由于每个对象都有自己的实例,因此每个更改系统状态的类都需要调用 _context.SaveChanges()(否则更改会丢失).这会使您的代码复杂化,并为代码添加第二个职责(控制上下文的职责),并且违反 Single责任原则.您需要确保实体 [由 DbContext 加载和保存] 永远不会离开此类的范围,因为它们不能在另一个类的上下文实例中使用.这会使您的代码变得非常复杂,因为当您需要这些实体时,您需要通过 id 再次加载它们,这也可能导致性能问题.由于 DbContext 实现了 IDisposable,您可能仍希望 Dispose 所有创建的实例.如果你想这样做,你基本上有两个选择.您需要在调用 context.SaveChanges() 后立即在相同的方法中处理它们,但在这种情况下,业务逻辑会获得它从外部传递的对象的所有权.第二个选项是在 Http 请求的边界上处理所有创建的实例,但在这种情况下,您仍然需要某种范围来让容器知道何时需要处理这些实例. 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 DbContext implements 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.

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

Another option is to not inject a DbContext at all. Instead, you inject a DbContextFactory that 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:

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

        context.Entities.InsertOnSubmit(entities);

        context.SaveChanges();
    }
}

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

The plus side of this is that you manage the life of the DbContext explicitly 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 DbContext from 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 DbContext is 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.

方法注入并没有那么糟糕,但是当业务逻辑变得更复杂,涉及到更多的类时,你将不得不将它从方法传递到方法,从类传递到类,这会使代码变得非常复杂(我过去见过).不过对于一个简单的应用程序,这种方法就可以了.

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.

由于缺点,这种工厂方法适用于更大的系统,另一种方法可能有用,那就是让容器或基础设施代码/CompositionRoot 管理工作单元.这就是您的问题所针对的风格.

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 Root manage the unit of work. This is the style that your question is about.

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

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?

处理一个工作单元可以在网络请求结束时完成.然而,许多人错误地假设这也是提交工作单元的地方.但是,在应用程序的那个时候,您根本无法确定工作单​​元是否应该实际提交.例如如果业务层代码抛出了在调用堆栈更高层捕获的异常,那么您肯定不想要提交.

Disposing a unit of work can be done at the end of the web request. Many people however, incorrectly assume 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't want to Commit.

真正的解决方案是再次显式管理某种范围,但这次是在合成根中进行.抽象命令/处理程序模式背后的所有业务逻辑,您将能够编写一个可以包裹在每个允许执行此操作的命令处理程序周围的装饰器.示例:

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:

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

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

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.

906