One DbContext per request in ASP.NET MVC (without IOC container)
如果已经回答了这个问题,那么很抱歉,但是如果您不使用IOC容器,如何保证每个请求都有一个实体框架dbContext?(到目前为止,我所得到的答案是关于IOC集装箱解决方案的。)
似乎大多数解决方案都与HttpContext.Current.Items字典挂钩,但如何保证在请求完成时处理dbContext?(或者,是否有必要对ef DbContext进行处置?)
编辑
我目前正在我的控制器中实例化和处理我的dbContext,但是我在actionfilters和我的membershipProvider中也有几个单独的dbContext实例化(我刚刚注意到,还有几个验证器)。因此,我认为将dbContext的实例化和存储集中起来以减少开销可能是个好主意。
- 这与工作模式的单位有关,还是……这是否是一个潜在的问题,不想将上下文传递给将使用它的多个对象?
- @亚当,请看我的编辑。
我知道这不是最近的问题,但我还是会把我的答案贴出来,因为我相信有人会发现它很有用。
像其他许多人一样,我遵循了公认答案中提到的步骤。是的,它起作用了。但是,有一个要点:
每次发出请求时,方法BeginRequest()和EndRequest()都会激发,但不仅针对ASPX页,而且针对所有静态内容!也就是说,如果您使用上面提到的代码,并且您的页面上有30个图像,那么您将对dbContext进行30次实例化!
解决方法是使用包装类来检索上下文,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| internal static class ContextPerRequest
{
internal static DB1Entities Current
{
get
{
if (!HttpContext.Current.Items.Contains("myContext"))
{
HttpContext.Current.Items.Add("myContext", new DB1Entities());
}
return HttpContext.Current.Items["myContext"] as DB1Entities;
}
}
} |
然后处理
1 2 3 4 5 6
| protected void Application_EndRequest(object sender, EventArgs e)
{
var entityContext = HttpContext.Current.Items["myContext"] as DB1Entities;
if (entityContext != null)
entityContext.Dispose();
} |
这种修改确保您在每个请求中仅实例化和处理一次上下文,并且仅在需要时才这样做。选定的答案每次都实例化上下文。
注意:db1实体是从dbContext(由vs生成)派生的。您可能希望用上下文名称来更改它;)
注意2:在这个示例中,我只使用一个dbContext。如果需要使用multiple,则需要根据需要修改此代码。不要把它当作解决世界问题的终极方案,因为它肯定不是最终产品。它的意思只是给出一个提示,它是如何以一种非常简单的方式实现的。
注3:同样的方法也可以在不同的情况下使用,例如当您希望共享一个sqlconnection实例或任何其他实例时…此解决方案不排斥dbContext对象,也不排斥实体框架。
- 这很好,将静态电流方法放在上下文类中,这对我来说是完美的:)
- 我想使用那个解决方案,但我在解决方案中有另一个项目处理数据库访问,所以我不能使用httpcontext。我有什么办法可以执行你的建议吗?
- @Matthew,如果您可以以某种方式向UI公开您的DB管理类,以便在请求结束时处理它,那么是的。整个问题都是关于创建一个单独的上下文,并在请求结束之前共享它,因此在单个用户请求期间,您不会反复创建它。您只是不关心它——您可以根据需要实例化它,并在完成后处理它,然后将结果发送给用户。
- 嗨,沃尔特-我很困惑,我一直在读,将您的上下文存储在静态属性中会产生一些严重的错误-是这样吗?参考:stackoverflow.com/questions/4847892/…
- @Loren,不,因为在我的解决方案中,上下文存储在httpContext.current.items集合中,并在request end事件期间释放。静态属性只公开集合中的此上下文,如果该上下文尚不存在,则将其实例化。它从不存储在静态变量中,并且是当前特定于上下文的,因此您不会遇到像前面提到的那样的问题。您还可以将该属性转换为方法,所有内容的功能都将完全相同。我更喜欢属性的语法,所以我用它来代替方法。
- 我可能会使用lazy,它非常适合这种情况,只有在真正需要的时候才会创建实例。除此之外,实例化dbContext非常便宜(第一次很昂贵)。
我将使用BeginRequest/EndRequest方法,这有助于确保在请求结束时正确地处理您的上下文。
1 2 3 4 5 6 7 8 9 10 11
| protected virtual void Application_BeginRequest()
{
HttpContext.Current.Items["_EntityContext"] = new EntityContext();
}
protected virtual void Application_EndRequest()
{
var entityContext = HttpContext.Current.Items["_EntityContext"] as EntityContext;
if (entityContext != null)
entityContext.Dispose();
} |
在你的EntityContext类中…
1 2 3 4 5 6 7
| public class EntityContext
{
public static EntityContext Current
{
get { return HttpContext.Current.Items["_EntityContext"] as EntityContext; }
}
} |
- 当我在控制器(private entityContext _db=entityContext.current;)中声明私有实例并在所有操作中使用它时,我会观察到内存泄漏。你知道为什么吗?
- 我已经尝试过了,但是如果从Razor视图中需要额外的调用,那么我们会得到一个异常,即ObjectContext是公开的,并且我们正在尝试访问数据库。似乎在Razor视图完成呈现之前调用了EndRequest?我可能做错什么了?
一种方法是订阅Application_BeginRequest事件,将dbContext注入当前的httpContext,并在Application_EndRequest中从httpContext获取并释放。介于两者之间的任何内容(几乎都是:-)都可以从当前的httpContext中提取dbContext并使用它。是的,你应该把它处理掉。顺便问一下,您是否有任何理由不使用已经为您做了这件事的DI框架?
- +1谢谢你的回答。这是有道理的。我(目前)没有使用IOC容器,因为这个应用程序相对简单,而且我还没有遇到任何远程复杂的依赖注入场景。我几乎只是将dbContext注入到各种服务中。但我并不反对使用IOC容器。
- 你的应用程序是否简单无关紧要。这是未来的发展。现在就设置它,虽然它很简单,但当你需要它的时候,它会变得更容易,因为你的应用程序越来越大。当然,除非你的应用程序保持小规模。
- @RPM1984,我明白你的观点了……这个项目可能不会再开发出更多的深度层,而是会在广度上扩展。换句话说,我希望添加更多的页面,但我基本上拥有模型、服务、控制器、视图和验证器,而且我不希望这会改变。现在,很容易把所有东西连接起来(事实上,大部分都是由MVC自动化的)。我很难看到一个IOC容器如何让我的生活更轻松,但我有兴趣听听你的策略是什么。
- 对我来说很简单。您应该始终使用接口驱动的编程,至少对于单个组件的单元测试是如此。然后,在处理接口之后,创建一个基本的DI注册表来设置依赖项。<1小时的工作,使您的代码干净且可测试。你在向你的控制器注入具体的服务吗?如果是这样的话,我想看看你是如何对控制器进行单元测试的。
- @RPM1984,我想你已经碰到了这个问题……我从来没有想过要对我的控制器进行单元测试。它们只是返回视图。我要测试什么?
- @丹-真的吗?不跟模型说话,不设置视图模型,不逻辑?我觉得很难相信。如果是这样,那么就不需要单元测试。
- @RPM1984,是的,我有点夸张了。但在大多数情况下,逻辑是var model = _service.GetEditorModel(id); return View(model)或if (!ModelState.IsValid) return View(model); _service.Edit(model); return RedirectToAction("Index");。我使用一个基类,这样具体的类通常非常简单。我确实有构造函数重载,所以我的控制器可以进行单元测试,但实际上我没有编写任何测试。
- @丹-好吧,那是你的偏好。但就我个人而言,我会(也会)进行如下测试:确保返回后重定向结果中的有效模型(例如,测试prg),确保对于后重定向中的无效模型,返回视图结果,并且模型状态包含错误等。对于这些测试,_service应声明为IService并实现为ITestService在您的测试项目中,使测试变得容易。不管怎样,大声嚷嚷吧——这取决于你。:)
- @RPM1984,我意识到我并没有完全遵循这里的最佳实践。一般来说,单元测试是我还没有完全了解的事情,因为我几乎总是在小的、一个人的、非生或死的项目上工作。我一直在用测试来构建项目(例如,我几乎总是注入依赖性而不是"新事物",并且我使用诸如用于测试的MVC&MVVM之类的模式),但是我从来没有写过很多测试。我觉得我应该这样做,但我遇到的几乎每一个bug似乎都是只有集成测试才能捕捉到的。
- @RPM1984,尽管如此,请随时向我推销它:)我已经读过很多次TDD和单元测试的好处,但是当我没有失败而不去做的时候,很难想去做它。
查德·莫兰答案的小补充。它的灵感来自华尔特的音符。为了避免静态内容的上下文初始化,我们应该检查当前的路由处理程序(此示例仅适用于MVC):
1 2 3 4 5 6 7 8
| protected virtual void Application_BeginRequest()
{
var routeData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(this.Context));
if (routeData != null && routeData.RouteHandler is MvcRouteHandler)
{
HttpContext.Current.Items["_EntityContext"] = new EntityContext();
}
} |
- 谢谢您。比起全局静态方法,我更喜欢这个解决方案。另外,如果您改为钩住Application_PreRequestHandlerExecute,您只需检查this.Context.Handler is MvcHandler,因为它将设置在处理管道的这个点上。
如果您在控制器中实现IDisposable,并在Disposing方法中释放上下文,并在控制器构造函数中实例化新的上下文,那么您应该是安全的,因为控制器是为每个请求实例化的。不过,我不明白,你为什么要这样做?…您应该使用DI,或者使用上下文的一个静态实例创建一个上下文工厂。如果您不使用一个实例(每个请求一个实例),那么在某个时刻就会出现问题。未解析上下文的问题是,ef在上下文中缓存数据,如果上下文的其他实例更改了数据库中已经在另一个上下文中缓存的内容,则说明您处于不存在状态。在DI变得如此流行之前,我曾经在应用程序的某个地方有一个上下文的静态实例,这比让每个请求生成自己的上下文要快得多,而且更安全,但是您需要实现状态检查代码,以确保到DB的上下文连接是正常的…对于这个问题有很多更好的解决方案,最好是使用一些DI框架。我建议Ninject与MVC涡轮结合使用,它很容易设置,您可以通过Nuget添加它。
- 我不明白你的意思。使用DI框架如何防止状态不一致?如果两个用户试图同时编辑同一个记录,我看不出共享一个上下文或使用两个单独的上下文会有什么不同。其中一个用户(首先提交的用户)将遇到意想不到的结果。
- 例如:假设我决定编辑您的答案。我点击"编辑"按钮,现在我看到一个编辑框,里面有你的答案。我改了几个字。在我点击提交之前,你决定编辑你的答案。现在您看到的是一个编辑框,但是您看到的答案没有我所做的任何更改。现在我提交我的答案。你自己做一些修改,然后提交你的答案。结果如何?状态不一致。即使我们共享一个数据上下文,它也不会有任何影响,因为在用户提交之前,数据上下文并不知道已经进行了更改。
- 示例(续):谢天谢地,stackoverflow保存了整个编辑历史,所以即使我的更改是由您写的,我仍然可以返回查看我写的内容。
- 是的,这是不容易解决的,但是如果您需要让您的系统处理这种情况,您必须在数据库级别处理它-向数据库中的记录添加版本控制。当您尝试创建新版本时,您将遇到其他用户创建的版本,然后您可以要求用户决定忽略/覆盖等。
- 我的观点是,当用户编辑复杂对象时,您向他显示一些下拉列表,以便他/她可以在表单上选择值。如果管理员在用户的dbContext已读取该表后更改了代码簿(在下拉列表中添加/删除值),它将不会反映在现有上下文中-这就是为什么如果您希望拥有当前数据,则需要确保为每个请求构建新上下文的原因。缺点是性能——每次都从数据库中读取值。你需要决定做哪一个权衡-如果你不想考虑交易和一致性…
- …当您将对象添加到上下文中时,可能会发生这种情况,但计划稍后对其进行编辑和保存,然后静态上下文可能会导致其他用户执行操作来保存对数据库的更改—意外行为。但是,如果您需要性能,一个上下文是可行的。更好的是,您可以为所有代码书等创建一个静态实例——您无论如何都需要键/值对,以及事务操作的每个实例上下文。有上百种可能性。DI只是一种工具,可以帮助你更快更容易地完成你的工作,你是解决(或创造)问题的人。
- obrad,听起来您是在引用一个在下拉列表中非常频繁使用表的情况,目的是避免在数据库中一次又一次地请求它,除非它发生了变化。在这种情况下,我可以肯定地看到想要缓存数据,但现在这对我来说并不是一个常见的场景,而且我的应用程序的用户数量有限,所以我尽量减少对数据库的访问并不是很重要。不过,我担心的是数据完整性。我通过限制谁可以删除记录以及在什么条件下来处理这个问题。
- 如果您主要关心的是完整性,我建议您考虑数据库记录版本控制:不更新;对于每次更改,都会创建版本号增加的新记录。实现是一件痛苦的事情(您需要另一个"id"字段,或者主键必须是id和version的组合,并且您需要为所有这些实现逻辑),但是使用这种方法,您将永远不会有用户尝试并成功更新记录的场景,这些记录已经由某个人在中间更新了。您将始终能够检测到比正在编辑的版本更新的版本,并向用户发出警告。
这里的滑坡状态不一致。如果你的应用上有多个用户,并且他们有可能同时更改数据,那么如果你保持一个单一的上下文,你可能会遇到数据完整性的问题。
- mymex,不一致状态绝对是一个问题,但是我不知道用户共享单个数据上下文还是使用单独的上下文有什么区别。正如我对obrad所说,如果两个用户同时编辑同一个记录,那么首先提交的用户将由第二个提交的用户来编写其更改。
- MyMex,请看我对obrad答案的评论。