一尘不染

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

c#

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

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


阅读 330

收藏
2020-05-19

共1个答案

一尘不染

注意:这个答案是关于Entity Framework的DbContext,但是它适用于任何类型的工作单元实现,例如LINQ to
SQL的DataContext和NHibernate的ISession

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

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

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

  • 由于每个对象都有其自己的实例,因此更改系统状态的每个类都需要调用_context.SaveChanges()(否则更改将丢失)。这可能会使您的代码复杂化,并给代码增加第二个责任(控制上下文的责任),并且违反了“ 单一责任原则”
  • 您需要确保[由DbContext] 加载和保存的实体永远不会离开此类的范围,因为它们不能在另一个类的上下文实例中使用。这会使您的代码变得非常复杂,因为当您需要这些实体时,需要通过id重新加载它们,这也可能导致性能问题。
  • 自从DbContext实现以来IDisposable,您可能仍想处置所有创建的实例。如果要执行此操作,则基本上有两个选择。您需要在调用后立即将它们以相同的方法进行处理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

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

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

由于不利因素,这种工厂方法适用于较大的系统,另一种方法可能有用,那就是让容器或基础结构代码/
组合根管理工作单元的方法。这是您的问题涉及的样式。

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

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

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

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>以一致的方式包装在所有实现中。

2020-05-19