每个网络请求一个DbContext ...为什么?

And*_*rew 382 c# asp.net entity-framework dependency-injection dbcontext

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

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

Ste*_*ven 546

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

让我们回首伊恩:DbContext对整个应用程序来说,单一是一个坏主意.唯一有意义的情况是,您拥有单线程应用程序和单个应用程序实例单独使用的数据库.这DbContext不是线程安全的,并且由于DbContext缓存数据,它很快就会变得陈旧.当多个用户/应用程序同时处理该数据库时,这会让您遇到各种麻烦(当然这很常见).但是我希望你已经知道这一点,并且只是想知道为什么不向DbContext需要它的人注入新的实例(即具有短暂的生活方式).(有关为什么单个DbContext- 甚至每个线程的上下文 - 是坏的更多信息,请阅读此答案).

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

如果你没有让一组操作在同一个环境中运行的目标,那么瞬态生活方式很好,但有几点需要注意:

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

另一种选择是根本不注射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();
    }
}
Run Code Online (Sandbox Code Playgroud)

这方面的优点是你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();
    } 
}
Run Code Online (Sandbox Code Playgroud)

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

  • @Andrew:'Transient'是一个依赖注入概念,这意味着如果服务被配置为瞬态,则每次将服务注入消费者时都会创建一个新的服务实例. (14认同)
  • +1在你真正阅读之前,你是否相信我写了[这个答案](http://stackoverflow.com/a/15604758/11635)?BTW IMO我认为最重要的是讨论DbContext的处理对你来说很重要(尽管你很容易保持容器不可知) (3认同)
  • 哇 - 谢谢你的答复.如果我可以两次投票,我会的.在上面,你说"......无意让一整套操作在同一个环境中运行,在这种情况下,短暂的生活方式很好......".具体来说,"瞬态"是什么意思? (2认同)

use*_*106 32

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

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

可能的性能提升.

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

它可以实现延迟加载.

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

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

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


Ane*_*lou 31

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

  1. 一个建议是"尽快处理DbContexts", 因为DbContext Alive占用了像db连接等宝贵的资源....
  2. 另一个说明每个请求的一个DbContext被高度推荐

那些相互矛盾,因为如果你的请求与Db的东西做了很多无关,那么你的DbContext就会无缘无故地保留下来.因此,当您的请求只是等待随机的东西完成时,保持您的DbContext活着是浪费...

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

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

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

所以EF的团队建议每个请求使用1 Db Context它显然是基于这样一个事实:在Web应用程序中,UnitOfWork最有可能在一个请求中并且该请求有一个线程.因此,每个请求一个DbContext就像UnitOfWork和Caching的理想优势.

在许多情况下,情况并非如此.我认为Logging一个单独的UnitOfWork,因此在异步线程中有一个新的DbContext用于Post-Request Logging 是完全可以接受的

所以最后它拒绝了DbContext的生命周期仅限于这两个参数.UnitOfWorkThread

  • 平心而论,您的HTTP请求应该很快完成(几毫秒).如果它们比这更长,那么您可能想要考虑使用外部作业调度程序等进行一些后台处理,以便请求可以立即返回.也就是说,您的架构也不应该依赖HTTP.总的来说,一个很好的答案. (2认同)

Ian*_*Ian 22

我很确定这是因为DbContext根本不是线程安全的.所以分享这件事永远不是一个好主意.

  • 如何为一个请求共享上下文.因此,对于一个请求,我们可以访问不同的存储库,并通过共享同一个上下文在它们之间进行事务处理? (10认同)
  • 是安德鲁这就是他的意思.共享上下文仅适用于单线程桌面应用程序. (2认同)

Ric*_*ahl 14

在问题或讨论中没有真正解决的一件事是DbContext无法取消更改.您可以提交更改,但无法清除更改树,因此如果您使用每个请求上下文,如果您因任何原因需要更改,则运气不佳.

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

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

  • 在这种情况下,业务对象的注入应该处理生命周期管理。在我看来,业务对象拥有上下文,因此应该控制生命周期。 (2认同)
  • 就个人而言,我认为在那里强制使用DbContext有点麻烦.无法保证您甚至需要访问数据库.也许你正在呼叫改变第三方服务的第三方服务.或者,您实际上可能同时拥有2个或3个数据库.你不会在开始时创建一堆DbContexts以防万一你最终使用它们.企业知道它正在使用的数据,因此它属于那个.如果需要,只需将TransactionScope放在开头即可.我认为所有电话都不需要.它需要资源. (2认同)

Mir*_*lec 10

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

这是真实的示例图片:应用程序已在12PM部署