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

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


当前回答

这里没有一个答案能真正回答这个问题。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).

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

我把这篇文章贴出来,以防别人偶然发现这个问题,并没有被那些并没有真正解决这个问题的答案所吸引。

其他回答

我喜欢它的地方在于,它将工作单元(正如用户看到的那样——即页面提交)与ORM意义上的工作单元对齐。

因此,您可以使整个页面提交都是事务性的,如果您公开CRUD方法,并且每个方法都创建一个新的上下文,则无法做到这一点。

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.

这是一个问题,因为单例DbContext会成为一个定时炸弹,最终会缓存整个数据库和。net对象在内存中的开销。

只有通过使用. notracking()扩展方法来使用Linq查询,才能避免这种行为。而且现在的电脑有很多内存。但这通常不是理想的行为。

实体框架需要特别注意的另一个问题是,当组合使用创建新实体、延迟加载,然后使用这些新实体时(从相同的上下文中)。如果你不使用IDbSet。创建(vs只是新建),当它从创建它的上下文检索时,对该实体的惰性加载将不起作用。例子:

 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.

我很确定这是因为DbContext根本不是线程安全的。所以分享从来不是个好主意。

注意:这个答案谈论的是实体框架的DbContext,但是 它适用于任何类型的工作单元实现,例如 LINQ到SQL的DataContext,以及NHibernate的ISession。

Let start by echoing Ian: Having a single DbContext for the whole application is a Bad Idea. The only situation where this makes sense is when you have a single-threaded application and a database that is solely used by that single application instance. The DbContext is not thread-safe and since the DbContext caches data, it gets stale pretty soon. This will get you in all sorts of trouble when multiple users/applications work on that database simultaneously (which is very common of course). But I expect you already know that and just want to know why not to just inject a new instance (i.e. with a transient lifestyle) of the DbContext into anyone who needs it. (for more information about why a single DbContext -or even on context per thread- is bad, read this answer).

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

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

Since every object gets its own instance, every class that changes the state of the system, needs to call _context.SaveChanges() (otherwise changes would get lost). This can complicate your code, and adds a second responsibility to the code (the responsibility of controlling the context), and is a violation of the Single Responsibility Principle. You need to make sure that entities [loaded and saved by a DbContext] never leave the scope of such a class, because they can't be used in the context instance of another class. This can complicate your code enormously, because when you need those entities, you need to load them again by id, which could also cause performance problems. Since DbContext implements IDisposable, you probably still want to Dispose all created instances. If you want to do this, you basically have two options. You need to dispose them in the same method right after calling context.SaveChanges(), but in that case the business logic takes ownership of an object it gets passed on from the outside. The second option is to Dispose all created instances on the boundary of the Http Request, but in that case you still need some sort of scoping to let the container know when those instances need to be Disposed.

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

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。

The downside is that you will have to pass around the DbContext from method to method (which is termed Method Injection). Note that in a sense this solution is the same as the 'scoped' approach, but now the scope is controlled in the application code itself (and is possibly repeated many times). It is the application that is responsible for creating and disposing the unit of work. Since the DbContext is created after the dependency graph is constructed, Constructor Injection is out of the picture and you need to defer to Method Injection when you need to pass on the context from one class to the other.

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

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

通过让容器和/或基础设施处理这个问题,您的应用程序代码不会因为必须创建、(可选地)提交和处置UoW实例而受到污染,这使业务逻辑简单而干净(只有一个Single Responsibility)。这种方法有一些困难。例如,在哪里提交和处置实例?

处理一个工作单元可以在web请求的末尾完成。然而,许多人错误地认为这也是提交工作单元的地方。然而,在应用程序的这一点上,您根本不能确定工作单元实际上应该被提交。例:如果业务层代码抛出了一个异常,并在调用堆栈的上层被捕获,你肯定不想提交。

真正的解决方案仍然是显式地管理某种范围,但这次在Composition Root中进行。将命令/处理程序模式背后的所有业务逻辑抽象出来,您将能够编写一个装饰器,该装饰器可以封装在允许执行此操作的每个命令处理程序周围。例子:

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>实现。