在ASP.NET MVC中每个请求使用一个DbContext(不使用IOC容器)

51

如果这个问题已经有答案了,那么对不起,但是如果您不使用 IOC 容器,如何保证每个请求只有一个 Entity Framework DbContext?(目前我找到的答案都是关于 IOC 容器的解决方案。)

看起来大部分解决方案都会钩入 HttpContext.Current.Items 字典,但是当请求完成时,如何确保 DbContext 被清除?(或者使用 EF DbContext 时是否不必要清除?)

编辑

我目前在我的控制器中实例化和清除了 DbContext,但是我还有一些在 ActionFilters 和 MembershipProvider 中独立实例化的 DbContext(我刚刚还发现,还有一些验证器)。因此,我认为将我的 DbContext 的实例化和存储集中化可能是个好主意,以减少开销。


这与工作单元模式有关吗,还是不想将上下文传递给将使用它的多个对象可能会出现问题? - Adam Tuliper
6个回答

73

虽然这不是一个新问题,但我仍然要发布我的答案,因为我相信可能有人会发现它有用。

像许多其他人一样,我按照接受的答案中提到的步骤进行操作。耶,它起作用了。然而,这里有一个问题:

BeginRequest()EndRequest()方法每次发出请求时都会触发,但不仅限于aspx页面,还包括所有静态内容!也就是说,如果您使用上面提到的代码,并且在您的页面上有30个图像,您将重新实例化您的数据库上下文30次!

解决办法是使用一个包装类来检索上下文,类似于以下内容:

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;
          }
      }
 }

然后对其进行销毁处理

protected void Application_EndRequest(object sender, EventArgs e)
{
   var entityContext = HttpContext.Current.Items["myContext"] as DB1Entities;
   if (entityContext != null) 
      entityContext.Dispose();
}
这种修改方式确保您仅在需要时每个请求仅实例化和处理一次上下文。而选定的答案会每次都实例化上下文。
注意:DB1Entities是从DbContext派生出来的(由VS生成)。您可能想要使用您的上下文名称进行更改 ;)
注意2:在此示例中,我只使用了一个DbContext。如果您需要处理多个,请根据需要修改此代码。请不要把它看作解决世界问题的终极方案,因为它当然不是最终产品。它只是为了提示如何以非常简单的方式实现它。
注意3:相同的方法也可以在其他情况下使用,例如当您想共享SqlConnection或任何其他实例时......这个解决方案不仅限于DbContext对象,也不仅适用于Entity Framework。

这太棒了,将静态的Current方法放在Context类中,对我来说非常完美 :) - BjarkeCK
2
我想使用那个解决方案,但是我有另一个处理数据库访问的项目,所以我不能使用HttpContext。有没有办法我能够实现你的建议呢? - MattheW
@MattheW,如果你能够将你的数据库管理类暴露给UI,以便在请求结束时可以处理它,那么是的。整个问题都是关于创建一个上下文并在请求结束之前共享它,这样您就不必在单个用户请求期间反复创建它。您只需要在需要时实例化它,并在完成后处理它并将结果发送给用户。 - walther
嗨,沃尔特 - 我很困惑。我一直在阅读关于将上下文存储在静态属性中可能会产生一些严重错误的文章 - 这是真的吗?参考:https://dev59.com/n1PTa4cB1Zd3GeqPnN-K#4847930 - 99823
@Loren,不会的,因为在我的解决方案中,上下文存储在HttpContext.Current.Items集合中,并在“请求结束”事件期间处理。静态属性仅从集合中公开此上下文,并在不存在时实例化它。它永远不会存储在静态变量中,并且是当前上下文特定的,因此您不会遇到像您提到的那样的问题。您也可以将该属性转换为方法,一切都将完全相同。我只是更喜欢属性的语法,所以我使用了它而不是方法。 - walther
我可能会使用 Lazy<T>,它非常适合这种情况,只有在真正需要创建实例时才会创建。此外,实例化 DbContext 是非常便宜的(仅第一次昂贵)。 - Jone Polvora

61

我会使用BeginRequest/EndRequest方法,这有助于确保在请求结束时正确处理上下文。

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类中...

public class EntityContext
{
    public static EntityContext Current
    {
        get { return HttpContext.Current.Items["_EntityContext"] as EntityContext; }
    }
}

当我在控制器内声明一个私有实例(private EntityContext _db = EntityContext.Current;)并在所有操作中使用它时,我观察到有内存泄漏。有任何想法为什么会出现这种情况? - Santosh
我已经尝试过这个,但是如果Razor视图需要额外的调用,那么我们会得到一个异常,即ObjectContext被公开并且我们正在尝试访问数据库。看起来EndRequest在Razor视图完成渲染之前被调用了?我可能做错了什么? - MoXplod

10
一种方法是订阅Application_BeginRequest事件,将DbContext注入到当前的HttpContext中,在Application_EndRequest中从HttpContext获取并处理。在这之间(几乎包括所有步骤),都可以从当前HttpContext中获取DbContext并使用它。同时需要注意,确实需要将其Dispose。另外,是否有某些原因导致您不使用已经为您做好这些工作的DI框架呢?

1
+1 谢谢你的回答。这很有道理。因为应用程序相对简单,而且我还没有遇到任何复杂的依赖注入场景,所以我目前没有使用IOC容器。 我几乎只是将我的DbContext注入到各种服务中。但我并不反对使用IOC容器。 - devuxer
3
不管你的应用程序有多简单,都要进行未来的规划。现在就设置它,它还很简单的时候,这样当你的应用程序变得更大时,需要时它就会更容易。当然,除非你的应用程序将保持小型。 - RPM1984
1
@RPM1984,我理解你的观点...这个项目可能不会再有更多的深度层次,但会在广度上扩展。换句话说,我预计会添加更多页面,但基本上我已经有了模型、服务、控制器、视图和验证器,而且我不希望发生改变。现在,连接所有东西非常容易(实际上,很多都是由MVC自动化完成的)。我很难看出IOC容器如何让我的生活更轻松,但我很想听听你的策略。 - devuxer
对我来说很简单。你应该始终使用基于接口的编程,至少用于单元测试各个组件。然后一旦您使用了接口,请创建一个基本的 DI 注册表来设置依赖项。 不到 1 小时的工作,可以让您的代码变得清晰且易于测试。如果您正在将具体服务注入控制器中,那么我很想看看您如何对控制器进行单元测试。 - RPM1984
1
@DanM - 好吧,那是你的偏好。但就我个人而言,我会(并且已经)编写测试:确保在 POST 请求中有效模型返回重定向结果(例如测试 PRG),确保在 POST 请求中无效模型时,视图结果被返回并且模型状态包含错误等。对于这些测试,_service 应该在测试项目中声明为 IService 并实现为 ITestService,以便轻松进行测试。总之,说了这么多,仍然由你决定。 :) - RPM1984
显示剩余5条评论

7

对于Chad Moran所提供的答案,我有一个小补充。它受到了Walther Notes的启发。为了避免为静态内容进行上下文初始化,我们应该检查当前路由处理程序(以下示例仅适用于MVC):

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,因为在处理管道的此时点上它将被设置。 - Jeff Camera

1

如果您在控制器中实现了IDisposable,并在disposing方法中dispose上下文,并在控制器构造函数中实例化新上下文,则每个请求实例化控制器时应该是安全的。

但我不明白,为什么要这样做呢?...

您应该使用依赖注入(DI),或制作一个带有一个静态上下文实例的上下文工厂。如果您不使用一个实例(为每个请求创建一个实例),则在某些时候会遇到问题。未处理的上下文的问题在于EF会缓存上下文中的数据,如果另一个上下文实例更改已经在另一个上下文中缓存的DB中的内容,则存在不一致状态。在DI变得如此流行之前,我曾经在应用程序中的某个地方有一个静态上下文实例,这比让每个请求都创建自己的上下文快得多且更安全,但是需要实现状态检查代码以确保上下文与数据库的连接正常...解决此问题有更好的解决方案,其中最好的是使用一些DI框架。我建议使用Ninject与MVCTurbine结合使用,它易于设置,可以通过NuGet添加。


我没有理解你的观点。使用依赖注入框架如何保护免受不一致状态的影响?如果两个用户同时尝试编辑同一记录,我不认为共享单个上下文或使用两个独立上下文会有任何区别。其中一个用户(提交得更早的那个)将遇到意想不到的结果。 - devuxer
假设我决定编辑你的答案。我点击“编辑”按钮,现在我正在查看一个编辑框,其中包含你的答案。我改变了几个词。但是,在我提交之前,你决定编辑你的答案。现在你正在查看一个编辑框,但你看到的答案没有我所做的任何单词更改。现在我提交了我的答案。你做了一些改变,并提交了你的答案。结果是?不一致的状态。即使我们共享数据上下文,也没有关系,因为数据上下文在用户提交之前不知道是否已进行更改。 - devuxer
幸运的是,StackOverflow保存了整个编辑历史记录,因此即使我的更改被你的覆盖,我仍然可以返回并查看我所写的内容。 - devuxer
是的,这不容易解决,但如果您需要让系统处理这种情况,您必须在数据库级别处理它 - 在数据库中为记录添加版本控制。当您尝试创建新版本时,您会遇到其他用户创建的版本,然后您可以要求用户决定是否忽略/覆盖等操作。 - Goran Obradovic
obrad,听起来你指的是在下拉菜单中频繁使用表格的情况,并且目标是避免重复从数据库请求数据,除非它发生了变化。在这种情况下,我肯定会想要缓存数据,但这对我来说并不是一个常见的场景,而且我的应用程序有受限制的用户人口,因此最小化对数据库的访问并不是很关键。然而,我确实关心数据完整性。我通过限制谁可以删除记录以及在什么条件下来处理这个问题。 - devuxer
显示剩余3条评论

0

这里的悬崖是状态不一致。如果您的应用程序将有多个用户,并且他们有可能同时更改数据,那么如果您保留单个上下文,则可能会遇到数据完整性问题。


1
mymex,不一致的状态绝对是一个问题,但我不明白用户是共享一个数据上下文还是使用单独的上下文会有什么区别。正如我对obrad所说,如果两个用户同时编辑同一条记录,提交时间较早的用户的更改将被后者覆盖。 - devuxer
mymex,请也看一下我对obrad回答的评论。 - devuxer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接