EF DbContext.Configuration.ProxyCreationEnabled 什么鬼?
今天在开发项目的时候,使用 EF,突然遇到了这样一个错误:
An entity object cannot be referenceed by multiple instances of IEntityChangeTracker
这个异常我想大家应该很熟悉,大致的意思是 EF 实体操作不在同一个 DbContext,我贴下出现错误的代码:
public class AdTextUnitService : IAdTextUnitService { private IAdTextUnitRepository _adTextUnitRepository; private IUnitOfWork _unitOfWork; public AdTextUnitService(IAdTextRepository adTextRepository, IUnitOfWork unitOfWork) { _adTextUnitRepository = adTextRepository; _unitOfWork = unitOfWork; } public async Task<bool> Update(int id, string title) { var adTextUnit = await _adTextUnitRepository.Get(id); adTextUnit.Title = title; _unitOfWork.RegisterDirty(adTextUnit);// 这里出错 return await _unitOfWork.CommitAsync()); } }Update 的过程就是先使用 adTextUnitRepository 获取 adTextUnit 对象,然后进行修改,最后通过 _unitOfWork 提交更改,Repository 和 UnitOfWork 共享一个 IDbContext,具体实现请点击这里,既然共享同一个 IDbContext,那为什么还会出现上面的错误呢?并且同样的代码,我对其他实体操作却是可以,但对 AdTextUnit 操作就是不行。
对比了AdTextUnit 和其他实体的区别,发现 AdTextUnit 存在 virtual 属性,那为什么有 virtual 属性就不行呢?首先,我想到了 EF 中的 LazyLoadingEnabled(懒加载配置),然后我就在 DbContext 中进行了下面配置:
public CNBlogsAdDbContext() { Configuration.LazyLoadingEnabled = false; }但运行测试代码,发现还是出现错误,那就肯定不是 LazyLoadingEnabled 的问题,Google 搜索相关问题,发现解决方式就是“共享”同一个 IDbContext,比如下面示例代码:
//1. 操作同一 using 块中 using (var context = new CNBlogsAdDbContext()) { var adTextUnit = context.AdTextUnit.FirstOrDefault(); adTextUnit.Title = title; context.SaveChanges(); } //2. DbContext 放在 HttpContext 请求中 internal static class ContextPerRequest { internal static CNBlogsAdDbContext Current { get { if (!HttpContext.Current.Items.Contains("context")) { HttpContext.Current.Items.Add("context", new CNBlogsAdDbContext()); } return HttpContext.Current.Items["context"] as CNBlogsAdDbContext; } } } protected void Application_EndRequest(object sender, EventArgs e) { var entityContext = HttpContext.Current.Items["context"] as CNBlogsAdDbContext; if (entityContext != null) entityContext.Dispose(); }插一段:以上内容是我下午写的,现在是晚上时间,本来很简单的问题,让我搞复杂了,汗汗汗!!!当时除了尝试 LazyLoadingEnabled 解决,还试了 IoC.RegisterPerRequestType 注入(上面的第二种解决方案),但当时写代码比较急,RegisterPerRequestType 写在了 IUnitOfWork 接口注入上(眼瞎啊),然后我以为这种方式无效,就找其他方案,虽然最后用 ProxyCreationEnabled 解决了,这个过程真是坑爹,但也加深了对 DbContext 的一些理解。
还是要记录一下,一开始的问题,我们只需要这样就可以解决(和使用 using 是一样的效果):
IoC.RegisterPerRequestType<IDbContext, CNBlogsAdDbContext>();RegisterPerRequestType 的具体实现,就是上面第二种解决方案,虽然解决了问题,但还是有些疑惑,下面我们做个测试:
[Fact] public void DbContextTest() { var context = new CNBlogsAdDbContext(); var context2 = new CNBlogsAdDbContext(); AdTextUnit adTextUnit; using (context) { adTextUnit = context.Set<AdTextUnit>().FirstOrDefault(); } using (context2) { adTextUnit.DateUpdated = DateTime.Now; context2.Entry<AdTextUnit>(adTextUnit).State = EntityState.Modified; context2.SaveChanges(); } }上面的测试代码,我们是想模拟不同 DbContext 操作实体的情况,由第一个 DbContext 查询,然后第二个 DbContext 进行修改提交,执行测试代码,测试是通过并且正确,是不是有点奇怪?这种情况和我们的项目代码差不多(不使用 RegisterPerRequestType 的情况下),为什么测试代码却是可以的呢?想不通没关系,下面我们对上面测试代码进行修改,去掉 using:
[Fact] public void DbContextTest() { var context = new CNBlogsAdDbContext(); var context2 = new CNBlogsAdDbContext(); AdTextUnit adTextUnit; adTextUnit = context.Set<AdTextUnit>().FirstOrDefault(); adTextUnit.DateUpdated = DateTime.Now; context2.Entry<AdTextUnit>(adTextUnit).State = EntityState.Modified; context2.SaveChanges(); }执行测试代码:
这个错误是那么的熟悉,通过测试代码,我们大致可以推断出这个错误到底是如何发生的?
下面我说一下自己的理解,EF 要修改实体对象,需要记录实体对象值的变更,怎么记录的呢?就是通过 EntityChangeTracker(注意上面错误中的 IEntityChangeTracker),比如这段代码 adTextUnit.DateUpdated = DateTime.Now;,如果这个修改操作是在一个 using 中,那 EF DbContext 会通过 EntityChangeTracker 进行记录修改值的变化,具体是怎么记录的?大家可以看下 EF 的源码,我之前有篇博文(《追根溯源:EntityFramework 实体的状态变化》也说了一点点,之前有人问我,EF 修改实体的某一个属性值,最后执行 SQL 的时候,到底是更改一个属性值,还是全部更新呢?我们看一个段简单代码:
[Fact] public void DbContextTest() { using (var context = new CNBlogsAdDbContext()) { var adTextUnit = context.Set<AdTextUnit>().FirstOrDefault(); adTextUnit.DateUpdated = DateTime.Now; context.SaveChanges(); } }