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

我一直在阅读大量文章,解释如何设置entity framework的DbContext以便只使用各种DI框架为每个HTTP Web请求创build和使用一个。

为什么这是一个好主意呢? 使用这种方法你有什么好处? 是否有某些情况下这是一个好主意? 有没有事情,你可以使用这种技术,你不能做每个存储库方法调用实例化DbContext

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

首先回应Ian:为整个应用程序提供一个DbContext是一个糟糕的想法。 唯一有意义的情况是当您有一个单线程应用程序和一个单独的应用程序实例使用的数据库时。 DbContext不是线程安全的,而且由于DbContextcaching数据,它很快就会变陈旧。 当多个用户/应用程序同时在该数据库上工作时(这当然是非常普遍的),这会给你带来种种麻烦。 但是我希望你已经知道了,只是想知道为什么不把DbContext一个新实例(即暂时的生活方式)注入任何需要它的人。 (有关为什么单个DbContext或者每个线程上下文)的更多信息,请阅读此答案 )。

让我开始说,注册一个DbContext作为瞬态可以工作,但通常你想在一定范围内有一个这样的工作单元的单个实例。 在Web应用程序中,在Web请求的边界上定义这样一个范围是可行的; 从而成为每个Web请求生活方式。 这允许您让一组对象在相同的上下文中操作。 换句话说,他们在相同的业务交易中运作。

如果你没有在同一个环境下进行一系列操作的目标,在这种情况下,暂时性的生活方式是好的,但有几件事值得注意:

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

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

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

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

缺点是你将不得不传递DbContext从方法到方法(这被称为方法注入)。 请注意,从某种意义上说,这个解决scheme与'范围'方法相同,但现在范围在应用程序代码本身中进行控制(可能重复多次)。 这是负责创build和处理工作单元的应用程序。 由于DbContext是在构造依赖关系图之后创build的,所以构造函数注入不在图中,当需要将上下文从一个类传递到另一个类时,您需要DbContext方法注入。

方法注入并不是那么糟糕,但是当业务逻辑变得越来越复杂,涉及更多的类时,就必须把它从方法传递到方法和类到类,这会使代码复杂化很多(我见过这在过去)。 对于一个简单的应用程序来说,这个方法可以做得很好。

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

通过让容器和/或基础设施处理这个,你的应用程序代码不会被创build(可选)提交和处置一个UoW实例,从而保持业务逻辑简单和干净(只有一个责任)而受到污染。 这种方法有一些困难。 比如,你提交和处理实例吗?

处理工作单元可以在networking请求结束时完成。 然而,许多人错误地认为这也是承担工作单位的地方。 但是,在应用程序的这一点上,你根本无法确定工作单​​元是否应该实际承担。 例如,如果业务层代码抛出一个被调用的callstack更高的exception,那么肯定不会提交。

真正的解决scheme是再次明确地pipe理某种范围,但这次在组合根内部。 抽象出命令/处理程序模式背后的所有业务逻辑,你将能够编写一个装饰器,可以包装每个命令处理程序,允许这样做。 例:

 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容器都允许你configuration这样一个装饰器,以一致的方式包裹所有的ICommandHandler<T>实现。

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

这里没有一个答案实际上回答了这个问题。 OP没有询问关于单一/每个应用程序的DbContextdevise,他询问了一个per-(web)请求devise以及可能存在哪些好处。

我将引用http://mehdi.me/ambient-dbcontext-in-ef6/,因为Mehdi是一个很好的资源:;

可能的性能提升。

每个DbContext实例维护从数据库加载的所有实体的一级caching。 无论何时通过主键查询实体,DbContext都会首先尝试从其第一级高速caching中检索它,然后默认从数据库中查询该实体。 根据您的数据查询模式,在多个顺序业务事务中重复使用相同的DbContext可能会导致由于DbContext第一级caching而导致的数据库查询数量减less。

它启用延迟加载。

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

请记住也有缺点。 该链接包含许多其他资源来阅读这个问题。

只要发布这个,以防其他人绊倒这个问题,并没有专注于解决问题的答案。

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

这里是真正的示例图像:应用程序已在12PM部署

在问题或讨论中没有真正解决的一件事是DbContext无法取消更改。 您可以提交更改,但是无法清除更改树,所以如果您使用每个请求上下文,则不pipe出于何种原因都需要抛出更改。

就我个人而言,我会在需要时创buildDbContext的实例 – 通常附加到有能力根据需要重新创build上下文的业务组件。 这样我就可以控制这个过程,而不是强迫我一个实例。 然后,如果我仍然想要每个请求实例,我可以在CTOR中(通过DI或手动)创build它们,或者根据需要在每个控制器方法中创build它们。 就个人而言,我通常采取后一种方法,以避免创buildDbContext实例时,实际上不需要它们。

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

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

  1. 一个build议是“Disposable DbContexts尽快posible”,因为有一个DbContext Alive占用有价值的资源,如数据库连接等….
  2. 其他状态, 每个请求一个DbContext是非常reccomended

这些矛盾对方,因为如果你的请求是做了很多不相关的Db的东西,那么你的DbContext保持无故。 因此,当您的请求只是等待随机的东西完成时,保持您的DbContext活着是很浪费的…

所以很多遵循规则1的人在他们的“Repository模式”中都有他们的DbContext,并且每个请求创build一个新的每个数据库查询实例,所以X * DbContext

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

保持DbContext的单一多用途实例最大化caching的好处,但由于DbContext 不是线程安全的,并且每个Web请求都在它自己的线程上运行,所以每个请求的DbContext是最长的

因此,EF团队build议每个请求使用1个Db上下文,这显然是基于这样一个事实:在一个Web应用程序中,一个UnitOfWork很可能在一个请求之内,并且该请求有一个线程。 所以每个请求一个DbContext就像是UnitOfWork和Caching的理想好处。

在很多情况下这是不正确的。 我认为logging一个单独的UnitOfWork,因此有一个新的DbContext用于Post-Requestloggingasynchronous线程是完全可以接受的

所以最后,DbContext的生存期限于这两个参数。 UnitOfWork线程

我喜欢的是它将工作单元(就像用户看到的那样 – 即页面提交)与ORM意义上的工作单元alignment。

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

即使在单线程单用户应用程序中,不使用单例DbContext的另一个低估原因是由于它使用的标识映射模式。 这意味着每次使用查询或id检索数据时,都会将检索到的实体实例保存在caching中。 下一次检索同一个实体时,它会给你实体的caching实例(如果有的话),以及在同一个会话中做的任何修改。 这是必要的,因此SaveChanges方法不会以相同数据库logging的多个不同实体实例结束; 否则,上下文将不得不以某种方式合并来自所有这些实体实例的数据。

原因是一个单身DbContext可以成为一个时间炸弹,最终可能caching整个数据库+ .NET对象在内存中的开销。

只有使用.NoTracking()扩展方法使用Linq查询才能解决此问题。 而且这些天PC也有很多RAM。 但通常情况下,这不是理想的行为。

另一个需要注意Entity Framework的问题是使用创build新实体,延迟加载,然后使用这些新实体(来自同一个上下文)的组合。 如果你不使用IDbSet.Create(而不是新的),那么当它从其创build的上下文中被取出时,对该实体的延迟加载不起作用。例如:

  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.