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.
9 回答
即使在单线程单用户应用程序中,不使用单例DbContext的另一个低调原因是它使用的身份映射模式 . 这意味着每次使用query或id检索数据时,它都会将检索到的实体实例保留在缓存中 . 下次检索同一实体时,它将为您提供实体的缓存实例(如果可用),以及您在同一会话中所做的任何修改 . 这是必要的,因此SaveChanges方法不会以相同数据库记录的多个不同实体实例结束;否则,上下文必须以某种方式合并来自所有那些实体实例的数据 .
问题的原因是单例DbContext可以成为一个定时炸弹,最终可以在整个数据库中缓存.NET对象在内存中的开销 .
通过仅使用带有
.NoTracking()
扩展方法的Linq查询,可以解决此问题 . 这些天PC也有很多内存 . 但通常这不是理想的行为 .让我们回想一下Ian:整个应用程序只有一个
DbContext
是一个坏主意 . 唯一有意义的情况是,您拥有单线程应用程序和仅由该单个应用程序实例使用的数据库 .DbContext
不是线程安全的,并且由于DbContext
缓存数据,它很快就会变得陈旧 . 当多个用户/应用程序同时处理该数据库时,这会让您遇到各种各样的麻烦(当然这很常见) . 但我希望你已经知道这一点,并且只是想知道为什么不向需要它的人注入DbContext
的新实例(即具有短暂的生活方式) . (有关为什么单个DbContext
甚至每个线程的上下文都不好的更多信息,请阅读this answer) .首先让我说将
DbContext
注册为瞬态可以起作用,但通常你想在一定范围内拥有这样一个工作单元的单个实例 . 在Web应用程序中,在Web请求的边界上定义这样的范围是可行的;因此,每Web请求的生活方式 . 这允许您让整组对象在相同的上下文中运行 . 换句话说,它们在同一商业交易中运作 .如果你没有让一组操作在同一个环境中运行的目标,那么瞬态生活方式很好,但有几点需要注意:
由于每个对象都有自己的实例,每个更改系统状态的类都需要调用
_context.SaveChanges()
(否则更改会丢失) . 这可能会使您的代码复杂化,并为代码添加第二个责任(控制上下文的责任),并且违反了Single Responsibility Principle .您需要确保[由
DbContext
加载和保存]的实体永远不会离开此类的范围,因为它们不能在另一个类的上下文实例中使用 . 这会极大地使您的代码复杂化,因为当您需要这些实体时,您需要通过id再次加载它们,这也可能导致性能问题 .由于
DbContext
实现IDisposable
,您可能仍希望Dispose所有创建的实例 . 如果你想这样做,你基本上有两个选择 . 您需要在调用context.SaveChanges()
后立即将它们置于同一方法中,但在这种情况下,业务逻辑将获取从外部传递的对象的所有权 . 第二个选项是在Http请求的边界上处理所有创建的实例,但在这种情况下,您仍需要某种范围,以便容器知道何时需要处置这些实例 .另一种选择是根本不注入
DbContext
. 相反,您注入了一个能够创建新实例的DbContextFactory
(过去我曾经使用过这种方法) . 这样,业务逻辑可以显式地控制上下文 . 如果看起来像这样:这方面的优点是你明确地管理了
DbContext
的生命,并且很容易设置它 . 它还允许您在特定范围内使用单个上下文,这具有明显的优势,例如在单个业务事务中运行代码,以及能够传递实体,因为它们源自相同的DbContext
.缺点是您必须将
DbContext
从方法传递给方法(称为方法注入) . 请注意,在某种意义上,此解决方案与'scoped'方法相同,但现在范围在应用程序代码本身中受到控制(并且可能会重复多次) . 应用程序负责创建和处理工作单元 . 由于DbContext
是在构造依赖图之后创建的,因此构造函数注入不在图片中,当您需要将上下文从一个类传递到另一个类时,需要遵循方法注入 .方法注入并不是那么糟糕,但是当业务逻辑变得更复杂,并且涉及更多类时,您将不得不将它从方法传递到方法,从类传递到类,这会使代码复杂化很多(我见过)这在过去) . 对于一个简单的应用程序,这种方法可以做得很好 .
由于缺点,这种工厂方法适用于更大的系统,另一种方法可能很有用,也就是让容器或基础设施代码/ Composition Root管理工作单元的方法 . 这是您的问题所涉及的风格 .
通过让容器和/或基础设施处理这个问题,您的应用程序代码不会因为必须创建(可选)提交和配置UoW实例而受到污染,这使得业务逻辑保持简单和干净(只是单一职责) . 这种方法存在一些困难 . 例如,您是否提交并处置实例?
可以在Web请求结束时处理一个工作单元 . 然而,许多人错误地认为这也是承诺工作单位的地方 . 但是,在应用程序的这一点上,您无法确定是否应该实际提交工作单元 . 例如如果业务层代码抛出了一个在callstack中被捕获的异常,那么你绝对想要提交 .
真正的解决方案是再次明确地管理某种范围,但这次是在组合根中进行的 . 通过抽象command / handler pattern背后的所有业务逻辑,您将能够编写一个装饰器,可以围绕每个允许执行此操作的命令处理程序 . 例:
这可确保您只需编写一次此基础结构代码 . 任何可靠的DI容器都允许您以一致的方式配置这样的装饰器以包裹所有
ICommandHandler<T>
实现 .我很确定这是因为DbContext根本不是线程安全的 . 所以分享这件事永远不是一个好主意 .
微软推荐了两个 contradicting 建议,很多人以完全不同的方式使用DbContexts .
一个建议是 "Dispose DbContexts as soon as posible" 因为有一个DbContext Alive占用宝贵的资源,如数据库连接等....
其他国家 One DbContext per request is highly reccomended
那些相互矛盾,因为如果你的请求与Db的东西有很多不相关的东西,那么你的DbContext就会无缘无故地保留下来 . 因此,当您的请求只是等待随机的东西完成时,保持您的DbContext活着是浪费...
很多关注 rule 1 的人在他们的 "Repository pattern" 中都有他们的DbContexts并创建 a new Instance per Database Query 所以 X*DbContext 每个请求
他们只是尽快获取数据并处理上下文 . 这被 MANY 人认为是可接受的做法 . 虽然这有利于在最短的时间内占用您的数据库资源,但它显然牺牲了EF提供的所有糖果 .
保持单个 multipurpose DbContext实例最大化 Caching 的好处,但由于DbContext是 not thread safe 并且每个Web请求都在它自己的线程上运行,每个请求的DbContext是 longest 你可以保留它 .
所以EF的团队建议每个请求使用1 Db Context它显然是基于这样一个事实:在Web应用程序中,UnitOfWork最有可能在一个请求中并且该请求有一个线程 . 因此,每个请求一个DbContext就像UnitOfWork和Caching的理想优势 .
But 在许多情况下这不是真的 . 我认为 Logging 是一个单独的UnitOfWork,因此在 async threads 中有一个新的DbContext用于Post-Request Logging是完全可以接受的
所以最后它拒绝了DbContext的生命周期仅限于这两个参数 . UnitOfWork 和 Thread
特别注意实体框架的另一个问题是使用创建新实体,延迟加载,然后使用这些新实体(来自相同的上下文)的组合 . 如果你不使用IDbSet.Create(vs只是new),那么当它从创建的上下文中检索到时,该实体上的延迟加载不起作用 . 示例:
我喜欢它的是它将工作单元(用户看到它 - 即页面提交)与ORM意义上的工作单元对齐 .
因此,您可以制作整个页面提交事务,如果您在创建新上下文时公开CRUD方法,则无法执行此操作 .
这里没有一个答案实际上回答了这个问题 . OP没有询问单个/每个应用程序的DbContext设计,他询问了每个(web)请求设计以及可能存在的潜在好处 .
我将参考http://mehdi.me/ambient-dbcontext-in-ef6/,因为Mehdi是一个很棒的资源:
请记住,也有缺点 . 该链接包含许多其他资源以阅读该主题 .
只是发布这个以防万一其他人偶然发现这个问题,并没有专注于实际上没有解决问题的答案 .
在问题或讨论中没有真正解决的一件事是DbContext无法取消更改 . 您可以提交更改,但无法清除更改树,因此如果您使用每个请求上下文,如果您因任何原因需要更改,则运气不佳 .
我个人在需要时创建DbContext的实例 - 通常附加到能够在需要时重新创建上下文的业务组件 . 这样我就可以控制这个过程,而不是让一个实例强加给我 . 我也不必在每个控制器启动时创建DbContext,无论它是否真正被使用 . 然后,如果我仍然想要每个请求实例,我可以在CTOR中创建它们(通过DI或手动),或者在每个控制器方法中根据需要创建它们 . 就个人而言,我通常采用后一种方法,以避免在实际不需要时创建DbContext实例 .
这取决于你从哪个角度看 . 对我来说,每个请求实例从来没有意义 . DbContext真的属于Http请求吗?在行为方面,这是错误的地方 . 您的业务组件应该创建您的上下文,而不是Http请求 . 然后,您可以根据需要创建或丢弃业务组件,而不必担心上下文的生命周期 .
我同意以前的意见 . 很高兴地说,如果您要在单线程应用程序中共享DbContext,您将需要更多内存 . 例如,我在Azure上的Web应用程序(一个额外的小实例)需要另外150 MB的内存,而我每小时大约有30个用户 .
这是真实的示例图片:应用程序已在12PM部署