关于C#:每个Web请求一个dbContext…为什么?

One DbContext per web request… why?

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

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


NOTE: This answer talks about the Entity Framework's DbContext, but
it is applicable to any sort of Unit of Work implementation, such as
LINQ to SQL's DataContext, and NHibernate's ISession.

Ok.

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

首先,我要说,将DbContext注册为瞬态可以工作,但通常情况下,您希望在特定范围内拥有这样一个工作单元的单个实例。在Web应用程序中,在Web请求的边界上定义这样一个范围是可行的;因此每个Web请求的生活方式也是可行的。这允许您让一组对象在同一上下文中操作。换句话说,它们在同一个业务事务中运行。好的。

如果你没有在相同的环境下进行一系列操作的目标,那么在这种情况下,短暂的生活方式是可以的,但是有一些事情需要注意:好的。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
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。好的。

缺点是您必须将DbContext从一个方法传递到另一个方法(称为方法注入)。请注意,在某种意义上,此解决方案与"范围"方法相同,但现在范围由应用程序代码本身控制(并且可能重复多次)。它是负责创建和处理工作单元的应用程序。由于DbContext是在构造依赖关系图之后创建的,因此构造函数注入不在考虑范围内,当需要将上下文从一个类传递到另一个类时,需要推迟方法注入。好的。

方法注入并没有那么糟糕,但是当业务逻辑变得更复杂,并且涉及到更多的类时,您必须将它从一个方法传递到另一个方法,并将它从一个类传递到另一个类,这会使代码变得非常复杂(我以前见过这种情况)。对于一个简单的应用程序,这种方法可以做得很好。好的。

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

通过让容器和/或基础结构处理这一点,您的应用程序代码不会因为必须创建(可选)提交和处理UOW实例而受到污染,这样可以保持业务逻辑的简单和干净(只是一个职责)。这种方法有一些困难。例如,您是否提交并释放了该实例?好的。

处理工作单元可以在Web请求结束时完成。然而,许多人错误地认为这也是提交工作单元的地方。然而,在应用程序中的这一点上,您只是不能确定工作单元是否应该被提交。例如,如果业务层代码抛出了一个异常,而该异常在调用堆栈中被捕获得更高,那么您肯定不想提交。好的。

真正的解决方案是再次显式地管理某种范围,但这次在复合根目录中进行。抽象命令/处理程序模式背后的所有业务逻辑后,您将能够编写一个修饰器,它可以包装在每个允许这样做的命令处理程序周围。例子:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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实现中。好的。好啊。


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

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

Possible performance gains.

Each DbContext instance maintains a first-level cache of all the entities its loads from the database. Whenever you query an entity by its primary key, the DbContext will first attempt to retrieve it from its first-level cache before defaulting to querying it from the database. Depending on your data query pattern, re-using the same DbContext across multiple sequential business transactions may result in a fewer database queries being made thanks to the DbContext first-level cache.

It enables lazy-loading.

If your services return persistent entities (as opposed to returning view models or other sorts of DTOs) and you'd like to take advantage of lazy-loading on those entities, the lifetime of the DbContext instance from which those entities were retrieved must extend beyond the scope of the business transaction. If the service method disposed the DbContext instance it used before returning, any attempt to lazy-load properties on the returned entities would fail (whether or not using lazy-loading is a good idea is a different debate altogether which we won't get into here). In our web application example, lazy-loading would typically be used in controller action methods on entities returned by a separate service layer. In that case, the DbContext instance that was used by the service method to load these entities would need to remain alive for the duration of the web request (or at the very least until the action method has completed).

记住也有缺点。这个链接包含许多其他关于这个主题的阅读资源。

如果有人偶然发现了这个问题,而没有全神贯注于那些没有真正解决这个问题的答案,就把它贴出来。


微软提出了两个相互矛盾的建议,许多人使用dbContexts的方式完全不同。

  • 一个建议是"在可能的情况下尽快处理dbContexts"因为活的dbContext占用了像db这样的宝贵资源连接等…
  • 另一个声明每个请求一个dbContext高度重述
  • 这些内容相互矛盾,因为如果您的请求做了很多与数据库无关的事情,那么您的dbContext将毫无理由地保留。因此,当您的请求只是等待随机的事情完成时,让dbContext保持活动状态是浪费的…

    很多遵循规则1的人都将dbContexts放在"存储库模式"中,并为每个数据库查询创建一个新实例,因此每个请求都有x*dbContext

    他们只需获取数据并尽快处理上下文。许多人认为这是一种可以接受的做法。虽然这样做的好处是占用您的数据库资源的时间最短,但它显然牺牲了所有UnitOfWork和缓存Candy EF必须提供的功能。

    保持dbContext的单个多用途实例的活动状态可以最大限度地提高缓存的好处,但由于dbContext不是线程安全的,并且每个Web请求都在其自己的线程上运行,因此每个请求的dbContext是可以保持它的最长时间。

    因此,EF的团队建议每个请求使用1 db上下文,这显然是基于这样一个事实:在一个Web应用程序中,一个工作单元很可能在一个请求中,而该请求只有一个线程。因此,每个请求一个dbContext类似于UnitOfWork和缓存的理想好处。

    但在许多情况下,这是不正确的。我考虑将一个单独的工作单元记录下来,因此在异步线程中为请求后记录提供一个新的dbContext是完全可以接受的。

    最后,我们发现dbContext的生存期仅限于这两个参数。工作单元和螺纹


    我很确定这是因为dbContext根本不安全。所以分享这件事从来都不是一个好主意。


    问题或讨论中没有真正解决的一件事是dbContext无法取消更改。您可以提交更改,但不能清除更改树,因此,如果您使用每个请求上下文,那么无论出于什么原因需要放弃更改,您都会走运。

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

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


    我同意以前的意见。很好地说,如果要在单线程应用程序中共享dbContext,则需要更多内存。例如,我在Azure上的Web应用程序(一个非常小的实例)需要另一个150MB的内存,我每小时有大约30个用户。Application sharing DBContext in HTTP Request

    下面是真实的示例图片:应用程序已在下午12点部署


    我喜欢的是它将工作单元(如用户所见,即页面提交)与ORM意义上的工作单元对齐。

    因此,可以使整个页面提交成为事务性的,如果在每次创建新上下文时都公开CRUD方法,则不能这样做。


    另一个不使用singleton dbcontext(即使在单线程单用户应用程序中)的原因是它使用的标识映射模式。这意味着每次使用查询或按ID检索数据时,它都会将检索到的实体实例保存在缓存中。下次检索同一个实体时,它将为您提供该实体的缓存实例(如果可用),以及您在同一会话中所做的任何修改。这是必需的,因此savechanges方法不会以同一数据库记录的多个不同实体实例结束;否则,上下文将不得不以某种方式合并来自所有这些实体实例的数据。

    出现问题的原因是,单个dbContext可能成为一个定时炸弹,最终可能会缓存整个数据库+内存中.NET对象的开销。

    有一些方法可以避免这种行为,只使用带有.NoTracking()扩展方法的LINQ查询。而且现在PC有很多RAM。但这通常不是所期望的行为。


    另一个需要特别注意的关于实体框架的问题是,使用创建新实体、延迟加载以及使用这些新实体(来自同一上下文)的组合时。如果不使用idbset.create(vs只是new),则当从创建实体的上下文中检索到该实体时,该实体上的延迟加载将无法工作。例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
     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.