小编典典

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

all

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

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


阅读 92

收藏
2022-03-21

共1个答案

小编典典

注意:这个答案是关于实体框架的DbContext,但它适用于任何类型的工作单元实现,例如 LINQ to SQLDataContext
NHibernate 的ISession

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

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

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

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

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

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.

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

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

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

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

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

真正的解决方案是再次显式管理某种范围,但这次是在 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();
    } 
}

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

2022-03-21