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();
}
}
这段代码再简单不过,先通过 context 获取 adTextUnit 实体对象,然后进行修改一个属性值,最后进行保存,需要注意的是,我们并没有像之前那样手动 EntityState.Modified 配置,在修改属性值的时候,EF 会通过 EntityChangeTracker 追踪这个值的修改,然后在持久化的时候,进行检测并生成最终的 SQL 代码:
exec sp_executesql N'UPDATE [dbo].[AdTextUnits]
SET [DateUpdated] = @0
WHERE ([Id] = @1)
',N'@0 datetime2(7),@1 int',@0='2015-10-27 21:23:20.4506808',@1=4
从 SQL Server Profiler 捕获的 SQL,就可以看出 EF 是很智能的,如果我们把上面的测试代码,改成下面这样:
[Fact]
public void DbContextTest()
{
using (var context = new CNBlogsAdDbContext())
{
var adTextUnit = context.Set<AdTextUnit>().FirstOrDefault();
adTextUnit.DateUpdated = DateTime.Now;
context.Entry<AdTextUnit>(adTextUnit).State = EntityState.Modified;
context.SaveChanges();
}
}
手动加了一个实体 State 修改,最后生成的 SQL 如下:
exec sp_executesql N'UPDATE [dbo].[AdTextUnits]
SET [AdTextId] = @0, [AdTextModuleId] = @1, [StartDate] = @2, [EndDate] = @3, [Sort] = @4, [IsActive] = @5, [DateAdded] = @6, [DateUpdated] = @7, [OperatorName] = @8
WHERE ([Id] = @9)
',N'@0 int,@1 int,@2 datetime2(7),@3 datetime2(7),@4 int,@5 bit,@6 datetime2(7),@7 datetime2(7),@8 nvarchar(max) ,@9 int',@0=1,@1=1,@2='2015-10-19 00:00:00',@3='2016-10-19 00:00:00',@4=4,@5=1,@6='2015-10-19 00:00:00',@7='2015-10-27 21:43:16.8353133',@8=N'田园里的蟋蟀',@9=4
结果是 AdTextUnit 的全部属性进行了更新,为什么会这样呢?就是因为我们强制对 EF 说,AdTextUnit 被更改了,你记录的 EntityChangeTracker 无效了,但我们没具体指出哪个值被更改了,所以最后 EF 把所有属性值都更改了一遍,由这个示例,我们可以看出,尽量不要“多此一举”的对 Entry State 状态进行更改,什么情况下会进行更改呢?就是我们在丢失 EntityChangeTracker 的情况下,也就是说实体对象并不是由 DbContext 进行获取的,比如下面这段代码:
[Fact]
public void DbContextTest()
{
using (var context = new CNBlogsAdDbContext())
{
var adTextUnit = new AdTextUnit { Id = 1 };
adTextUnit.DateUpdated = DateTime.Now;
context.Entry<AdTextUnit>(adTextUnit).State = EntityState.Modified;
context.SaveChanges();
}
}
注意 Modified,而不是 Added,这段代码是可以执行成功的,因为 adTextUnit 是我们手动进行创建的,如果不对 Entry State 状态进行进行设置,执行是会报错的,为什么?因为 EF 最后执行 SaveChanges 检测不到 EntityChangeTracker。
感觉越说越乱了,关于 EntityChangeTracker 就说到这,了解了这么多,感觉可以对一开始的那段测试做出一些解释,但好像还差什么?是什么呢?就是 Proxy(代理)。
Proxy(代理):为 POCO 实体类型创建实例时,实体框架常常为充当实体代理的动态生成的派生类型创建实例。此代理重写实体的某些虚拟属性,这样可在访问属性时插入挂钩,从而自动执行操作。例如,此机制用于支持关系的延迟加载。本主题中所示方法同样适用于使用 Code First 和 EF 设计器创建的模型。
需要注意关键词:虚拟属性(virtual),这也就是为什么 AdTextUnit 有问题,而其他属性却没问题,那 Proxy 具体是什么鬼呢?其实就是这个东西:

后面一串的东西到底有什么用呢?其实它的作用就是记录虚拟属性,并修改保存和加载值,用一句话概括就是:Proxy 是实体虚拟属性的 EntityChangeTracker。
我们也可以手动进行配置,比如通过设置 ProxyCreationEnabled 取消代理:
public CNBlogsAdDbContext()
{
Configuration.ProxyCreationEnabled = false;
}
再次执行:

会发现 AdTextUnit 的类型少了很长的“一坨东西”,另外,需要注意的是 ProxyCreationEnabled 和 LazyLoadingEnabled 有所不同,LazyLoadingEnabled 表示是否启用懒加载,也就是我们在 EF 查询实体的时候,会不会加载虚拟属性(导航属性)?如果设置为 false,就默认不启用懒加载,但我们可以使用 Inculde 进行手动加载,但 ProxyCreationEnabled 表示的是追踪,它和 EntityChangeTracker 很类似,只不过局限于实体的虚拟属性。
最后,我们再来说下这段测试代码,为什么会报错?
[Fact]
public void DbContextTest()
{
var context1 = new CNBlogsAdDbContext();
var context2 = new CNBlogsAdDbContext();
AdTextUnit adTextUnit;
adTextUnit = context1.Set<AdTextUnit>().FirstOrDefault();
adTextUnit.DateUpdated = DateTime.Now;
context2.Entry<AdTextUnit>(adTextUnit).State = EntityState.Modified;
context2.SaveChanges();
}
总结几个关键点:
- AdTextUnit 的查询和修改不在一个 DbContext 中。
- 两个 DbContext 没有使用 using 块。
- AdTextUnit 存在虚拟属性。
- ProxyCreationEnabled 默认为 true。
上面这几个关键点,最后就导致了报错发生,了解了我们上面的分析,其实很简单,首先,通过 context1 获取 adTextUnit 实体对象,因为我们开启了代理,所以 adTextUnit 会进行追踪其内部的虚拟属性,并且这个追踪和 context1 密切相关,接下来通过 context2 进行修改和保存 adTextUnit 实体,需要注意的是,因为我们没有 using context1,所以这时候 adTextUnit 对应的代理追踪还是存在的,又因为我们使用 context2 强制表示 adTextUnit 进行了修改,context1 下的代理追踪,怎么能在 context2 进行标识呢?所以这时候肯定会抛出异常,这个道理用通俗的话来讲,就是老李家的儿子,跑到老张家喊爸爸,老家肯定会打断他儿子的腿。
这篇博文比较乱,而且内容和标题感觉差别也比较大,但一开始确实是由 EF DbContext.Configuration.ProxyCreationEnabled 引发的,并且一步一步测试分析,把这个过程分享出来,现在感觉还是蛮有价值的,一个点可以牵出其他很多点,对 EF 的理解和使用又进了一步,就到这!
参考资料:
- Working with Proxies
- MVC3 - an entity object cannot be referenced by multiple instances of ientitychangetracker(Proxy 讲的很详细)
- What are the downsides to turning off ProxyCreationEnabled for CTP5 of EF code first(LazyLoadingEnabled 和 ProxyCreationEnabled 的区别)
EF DbContext.Configuration.ProxyCreationEnabled 什么鬼?的更多相关文章
- EF架构~为EF DbContext生成的实体添加注释(T5模板应用)
回到目录 相关文章系列 第八回 EF架构~将数据库注释添加导入到模型实体类中 第二十一回 EF架构~为EF DbContext生成的实体添加注释(T4模板应用) 第二十二回EF架构~为EF DbCo ...
- 如何重写EF DBContext 获取链接字符串的方法
public partial class byvarDBFirst: DbContext { //使用自定义连接串 private static string GetEFConnctionString ...
- EF DbContext 并发执行时可能出现的问题
现在许多Web项目都使用了IOC的DI注入组件.其中对象的生命周期管理是非常重要的. 有时我们为了提高请求的响应,经常在请求线程中执行多个子线程,然而忽略了EF的DbContext的生命周期管理. D ...
- EF架构~为EF DbContext生成的实体添加注释(T5模板应用)(转载)
转载地址:http://www.newlifex.com/showtopic-1072.aspx 最近新项目要用Entity Framework 6.x,但是我发现从数据库生成模型时没有生成字段的注释 ...
- EF dbcontext上下文的处理
,那么我们整个项目里面上下文的实例会有很多个,我们又遇到了多次,当我们在编程的时候遇到多的时候,一般我们就要想想能不能解决多这个问题. (2)这里我要说的是EF上下文怎么管理呢?很简单啦,就是要保证线 ...
- EF DBContext中DbSet中Hashset添加对象后,DataGrid UI没有刷新的问题
使用EF4/5添加数据库视图生成DBContext,如果数据表/对象之间有M-N对应关系,EF对自动添加引用类集合,是Hashset类型.由于不是ObservableCellection类型,在引用类 ...
- 【转】EF架构~为EF DbContext生成的实体添加注释(T5模板应用)
嗨,没法说,EF4的TT模版加上注释后,升级到EF5的TT模版后,注释就不通用了,所以,还得再研究一下,然后把操作方法再分享出来,没辙的微软! T4模版可能有些凌乱,这在T5模版里有了不错的改进,但我 ...
- 为EF DbContext生成的实体添加注释(T5模板应用)[转]
1 先加上类注释 找到这行代码WriteHeader(codeStringGenerator, fileManager): 在它下面加上我们的代码: string summary=string.Emp ...
- EF异常探究(An entity object cannot be referenced by multiple instances of IEntityChangeTracker.)
今天在改造以前旧项目时出现了一项BUG,是由于以前不规范的EF写法所导致.异常信息如下: "An entity object cannot be referenced by multiple ...
随机推荐
- iOS地图
地图 1.主要用到了地图展示和定位功能 CoreLocation框架的使用: 导入头文件 #import <CoreLocation/CoreLocation.h>CoreL ...
- java记录
1. 包装类与自动装箱问题:在justjavac的博客上看到翻译的一篇文章 离开java,寻找更佳语言的十大理由 中关于自动装箱的一个描述: 这个特性是为了解决因原生类型的存在所导致的问题,在Java ...
- JsonUtil
package com.test.base.util.json; import java.beans.IntrospectionException; import java.beans.Introsp ...
- (翻译)开始iOS 7中自动布局教程(二)
这篇教程的前半部分被翻译出来很久了,我也是通过这个教程学会的IOS自动布局.但是后半部分(即本篇)一直未有翻译,正好最近跳坑翻译,就寻来这篇教程,进行翻译.前半部分已经转载至本博客,后半部分即本篇.学 ...
- SageCRM 页面加载完后,用下拉框联动修改放大镜字段的取值范围
原理很简单就是修改放大镜属性中的sql. 函数如下:第一个参数是字段的名称.第二个参数是需要控制这个放大镜的sql.可以跟进下拉框的值来组织这个sql. /*--------------- For C ...
- VS2015安装&简单的C#单元测试
<软件工程>开课已经三周了,三周的上课感觉就是老师教授的概念性东西少了不少,基本就是贯穿“做中学”的教学理念,三周的时间让我学到了挺多东西,很多东西都是课本没有的. 这周的任务就是安装VS ...
- PHP的FastCGI
CGI全称是“通用网关接口”(Common Gateway Interface), 它可以让一个客户端,从网页浏览器向执行在Web服务器上的程序请求数据. CGI描述了客户端和这个程序之间传输数据的一 ...
- .Net组件程序设计之远程调用(二)
.Net组件程序设计之远程调用(二) 激活模式 引用封送对象激活类型两种, 一种是客户端激活类型,一种是服务器端激活. 客户端激活对象 客户端激活方式:当客户端创建一个远程对象时,客户端得到的是一个新 ...
- JavaScript面试时候的坑洼沟洄——数据类型
前些日子写了篇关于最近找工作的一些感受的博客 找工作的一些感悟--前端小菜的成长,没想到得到了很多园友的共鸣,得到了很多鼓励,也有园友希望我分享一些笔试.面试的经验.我觉得分享一些笔试题没太多价值,对 ...
- 2013 duilib入门简明教程 -- 自绘控件 (15)
在[2013 duilib入门简明教程 -- 复杂控件介绍 (13)]中虽然介绍了界面设计器上的所有控件,但是还有一些控件并没有被放到界面设计器上,还有一些常用控件duilib并没有提供(比如 ...