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

One DbContext per web request... why?

提问人:Andrew 提问时间:5/14/2012 最后编辑:ArunPratapAndrew 更新时间:1/19/2023 访问量:117445

问:

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

为什么这首先是一个好主意?使用这种方法可以获得哪些优势?在某些情况下,这是否是一个好主意?使用这种技术可以做一些事情,而在实例化每个存储库方法调用时,你做不到吗?DbContext

C# asp.net 实体框架 依赖关系注入 dbContext

评论

11赞 Christoph 9/17/2016
Gueddari 在 mehdi.me/ambient-dbcontext-in-ef6 中调用每个存储库方法调用 DbContext 实例,调用反模式。引用:“通过这样做,您将失去 Entity Framework 通过 DbContext 提供的几乎所有功能,包括其第一级缓存、标识映射、工作单元以及更改跟踪和延迟加载功能。优秀的文章,为处理 DBContext 的生命周期提供了很好的建议。绝对值得一读。
0赞 Jone Polvora 3/30/2022
因为工作单元模式,在每个请求上实例化和释放 DbContext 实例,所以它以这种方式工作,它非常具有执行力

答:

3赞 RB. 5/14/2012 #1

我喜欢它的一点是,它将工作单元(如用户所看到的 - 即页面提交)与 ORM 意义上的工作单元保持一致。

因此,可以使整个页面提交成为事务性提交,如果在每个创建新上下文时公开 CRUD 方法,则无法做到这一点。

21赞 Ian 5/14/2012 #2

我很确定这是因为 DbContext 根本不是线程安全的。因此,分享这件事从来都不是一个好主意。

评论

0赞 Andrew 5/16/2012
你的意思是在HTTP请求之间共享它从来都不是一个好主意吗?
2赞 Elisabeth 2/10/2013
是的,安德鲁,这就是他的意思。共享上下文仅适用于单线程桌面应用。
10赞 Lyubomir Velchev 4/2/2015
共享一个请求的上下文怎么样?因此,对于一个请求,我们可以访问不同的存储库,并通过共享一个相同的上下文在它们之间进行交易吗?
623赞 Steven 5/15/2012 #3

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

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

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

如果您没有让一组操作在同一上下文中运行的目标,在这种情况下,短暂的生活方式是可以的,但有几件事需要注意:

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

另一种选择是根本不注射。取而代之的是,你注入了一个能够创建一个新实例的实例(我过去曾经使用过这种方法)。这样,业务逻辑就可以显式控制上下文。如果可能看起来像这样:DbContextDbContextFactory

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

        context.Entities.InsertOnSubmit(entities);

        context.SaveChanges();
    }
}

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

缺点是您必须从一个方法传递到另一个方法(这称为方法注入)。请注意,从某种意义上说,此解决方案与“作用域”方法相同,但现在作用域在应用程序代码本身中受到控制(并且可能重复多次)。它是负责创建和释放工作单元的应用程序。由于 是在构造依赖关系图之后创建的,因此构造函数注入是不存在的,当您需要将上下文从一个类传递到另一个类时,您需要遵循方法注入。DbContextDbContext

方法注入还不错,但是当业务逻辑变得更加复杂,并且涉及更多的类时,您将不得不将其从一个方法传递到另一个方法,从一个类传递到另一个类,这可能会使代码变得非常复杂(我过去见过这种情况)。对于简单的应用程序,这种方法可以很好地工作。

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

通过让容器和/或基础设施处理这个问题,您的应用程序代码不会因为必须创建、(可选)提交和释放 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();
    } 
}

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

评论

2赞 Andrew 5/16/2012
哇 - 感谢您的详尽回答。如果我能投两次赞成票,我会的。上面,你说“......无意让一整套操作在同一环境中运行,在这种情况下,短暂的生活方式很好......”。具体来说,“瞬态”是什么意思?
16赞 Steven 5/16/2012
@Andrew:“瞬态”是一个依赖注入概念,这意味着如果将服务配置为瞬态,则每次将服务注入使用者时都会创建一个新的服务实例。
1赞 Steven 9/5/2012
@user981375:对于 CRUD 操作,您可以创建泛型和泛型(对 Update 和 Delete 执行相同的操作,并且有一个查询)。不过,您应该问问自己,这个模型是否是 CRUD 操作的有用抽象,或者它是否只是增加了复杂性。不过,使用此模型轻松添加横切关注点(通过装饰器)的可能性可能会使您受益。您必须权衡利弊。CreateCommand<TEnity>CreateCommandHandler<TEntity> : ICommandHandler<CreateCommand<TEntity>>GetByIdQuery<TEntity>
3赞 Ruben Bartelink 3/26/2013
+1 Would you believe I wrote all of this answer before actually reading this ? BTW IMO I think its important for you to discuss the Disposal of the DbContext at the end (though its great that you're staying container agnostic)
1赞 Masoud 6/14/2014
But you don't pass the context to decorated class, how the decorated class could work with the same context that passed to the ? for example if the decorated class be class, how could it register insert operation to the context(DbContext in EF)?TransactionCommandHandlerDecoratorInsertCommandHandler
12赞 Miroslav Holec 4/8/2013 #4

I agree with previous opinions. It is good to say, that if you are going to share DbContext in single thread app, you'll need more memory. For example my web application on Azure (one extra small instance) needs another 150 MB of memory and I have about 30 users per hour. Application sharing DBContext in HTTP Request

Here is real example image: application have been deployed in 12PM

评论

0赞 Lyubomir Velchev 4/2/2015
Possibly the idea is to share the context for one request. If we access different repositories and - DBSet classes and want the operations with them to be transactional that should be a good solution. Have a look at the open source project mvcforum.com I think that is done in their implementation of Unit Of Work design pattern.
3赞 Dmitry S. 8/7/2015 #5

Another understated reason for not using a singleton DbContext, even in a single threaded single user application, is because of the identity map pattern it uses. It means that every time you retrieve data using query or by id, it will keep the retrieved entity instances in cache. The next time you retrieve the same entity, it will give you the cached instance of the entity, if available, with any modifications you have done in the same session. This is necessary so the SaveChanges method does not end up with multiple different entity instances of the same database record(s); otherwise, the context would have to somehow merge the data from all those entity instances.

The reason that is a problem is a singleton DbContext can become a time bomb that could eventually cache the whole database + the overhead of .NET objects in memory.

There are ways around this behavior by only using Linq queries with the extension method. Also these days PCs have a lot of RAM. But usually that is not the desired behavior..NoTracking()

评论

0赞 Franc 7/7/2016
This is correct, but you have to assume the Garbage Collector will work, making this problem more virtual than actual.
3赞 Dmitry S. 7/8/2016
Garbage collector is not going to collect any object instances held by an active static/singleton object. They will end up in the gen 2 of the heap.
34赞 user4893106 12/16/2015 #6

Not a single answer here actually answers the question. The OP did not ask about a singleton/per-application DbContext design, he asked about a per-(web)request design and what potential benefits could exist.

I'll reference http://mehdi.me/ambient-dbcontext-in-ef6/ as Mehdi is a fantastic resource:

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).

Keep in mind there are cons as well. That link contains many other resources to read on the subject.

Just posting this in case someone else stumbles upon this question and doesn't get absorbed in answers that don't actually address the question.

评论

0赞 aggsol 4/18/2019
Good link! Explicit managing the DBContext looks like the safest approach.
17赞 Rick Strahl 2/25/2016 #7

One thing that's not really addressed in the question or the discussion is the fact that DbContext can't cancel changes. You can submit changes, but you can't clear out the change tree, so if you use a per request context you're out of luck if you need to throw changes away for whatever reason.

Personally I create instances of DbContext when needed - usually attached to business components that have the ability to recreate the context if required. That way I have control over the process, rather than having a single instance forced onto me. I also don't have to create the DbContext at each controller startup regardless of whether it actually gets used. Then if I still want to have per request instances I can create them in the CTOR (via DI or manually) or create them as needed in each controller method. Personally I usually take the latter approach as to avoid creating DbContext instances when they are not actually needed.

It depends from which angle you look at it too. To me the per request instance has never made sense. Does the DbContext really belong into the Http Request? In terms of behavior that's the wrong place. Your business components should be creating your context, not the Http request. Then you can create or throw away your business components as needed and never worry about the lifetime of the context.

评论

1赞 Steven 3/7/2016
This is an interesting answer and I partially agree with you. To me, a DbContext doesn't have to be tied to a web request, but it IS always typed to one single 'request' as in: 'business transaction'. And when you tie the context to a business transaction, change cancellation becomes really weird to do. But not having it on web request boundary doesn't mean that the business components (BCs) should be creating the context; I think that is not their responsibility. Instead, you can apply scoping using decorators around your BCs. This way you can even change scoping without any code change.
2赞 Rick Strahl 3/17/2016
Well in that case the injection into the business object should deal with the lifetime management. In my view the business object owns the context and as such should control the lifetime.
0赞 tntwyckoff 9/20/2016
In brief, what do you mean when you say "the ability to recreate the context if required"? are you rolling your own rollback ability? can you elaborate a tad?
2赞 Daniel Lorenz 7/15/2017
Personally, I think it is a bit troublesome to force a DbContext at the start there. There is no guarantee that you even need to hit the database. Maybe you are calling a 3rd party service that changes state on that side. Or maybe you actually have 2 or 3 databases you are working with at the same time. You wouldn't create a bunch of DbContexts at the start just in case you end up using them. The business knows the data it is working with, so it belongs with that. Just put a TransactionScope at the start if it is needed. I don't think all calls need one. It does take resources.
0赞 davidcarr 5/10/2019
Thats the question of whether you allow the container to control lifetime of the dbcontext which then controls lifetime of parent controls, sometimes unduly. Say if I want a simple service singleton injected into my controllers then I will not be able to use constuctor inject due to per request semantic.
1赞 Ted Elliott 3/24/2016 #8

Another issue to watch out for with Entity Framework specifically is when using a combination of creating new entities, lazy loading, and then using those new entities (from the same context). If you don't use IDbSet.Create (vs just new), Lazy loading on that entity doesn't work when its retrieved out of the context it was created in. Example:

 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.
44赞 Anestis Kivranoglou 12/13/2016 #9

There are two contradicting recommendations by microsoft and many people use DbContexts in a completely divergent manner.

  1. One recommendation is to "Dispose DbContexts as soon as posible" because having a DbContext Alive occupies valuable resources like db connections etc....
  2. The other states that One DbContext per request is highly reccomended

Those contradict to each other because if your Request is doing a lot of unrelated to the Db stuff , then your DbContext is kept for no reason. Thus it is waste to keep your DbContext alive while your request is just waiting for random stuff to get done...

So many people who follow rule 1 have their DbContexts inside their "Repository pattern" and create a new Instance per Database Query so X*DbContext per Request

They just get their data and dispose the context ASAP. This is considered by MANY people an acceptable practice. While this has the benefits of occupying your db resources for the minimum time it clearly sacrifices all the UnitOfWork and Caching candy EF has to offer.

Keeping alive a single multipurpose instance of DbContext maximizes the benefits of Caching but since DbContext is not thread safe and each Web request runs on it's own thread, a DbContext per Request is the longest you can keep it.

So EF's team recommendation about using 1 Db Context per request it's clearly based on the fact that in a Web Application a UnitOfWork most likely is going to be within one request and that request has one thread. So one DbContext per request is like the ideal benefit of UnitOfWork and Caching.

But in many cases this is not true. I consider Logging a separate UnitOfWork thus having a new DbContext for Post-Request Logging in async threads is completely acceptable

So Finally it turns down that a DbContext's lifetime is restricted to these two parameters. UnitOfWork and Thread

评论

3赞 crush 8/10/2018
In all fairness, your HTTP requests should be finishing rather quickly (few ms). If they are going longer than that, then you might want to think about doing some background processing with something like an external job scheduler so that the request can return immediately. That said, your architecture shouldn't really rely on HTTP either. Overall, a good answer though.