EF异常探究(An entity object cannot be referenced by multiple instances of IEntityChangeTracker.)


今天在改造以前旧项目时出现了一项BUG,是由于以前不规范的EF写法所导致。异常信息如下:

"An entity object cannot be referenced by multiple instances of IEntityChangeTracker(一个实体对象不能由多个 IEntityChangeTracker 实例引用)"

这个问题其实很容易定位,是因为在程序中

使用了不同的DbContext来追踪同一个实体

以下的Demo代码可以轻松地引发该异常:

            using (var dbContext = new ADbContext())
            {var aa = dbContext.ClassA.Where(p => p.Id == 1).FirstOrDefault();
                dbContext.Entry<ClassA>(aa).State = System.Data.Entity.EntityState.Modified;
                using (var db2 = new ADbContext())
                {
                    db2.Entry<ClassA>(aa).State = System.Data.Entity.EntityState.Modified;
                    dbContext.SaveChanges();
                }
                dbContext.SaveChanges();
            }

注意ClassA一定要具有  导航属性  ,如下:

    public class ClassA
        
    {
        public int Id { get; set; }
        public string guid { get; set; }
        public int? child_id { get; set; }

        [ForeignKey("child_id")]
        public virtual ClassB child { get; set; }
}

多数场景下,一个设计好的系统中都会使用Uow(工作单元)来保证每一次请求使用同一个DbContext(也会有一些系统会需要MSDTC,不在讨论范围内),任意新建DbContext不但容易引发异常,还有可能因为释放不及时而导致内存问题。

要解决上面的这个异常很简单——使用同一个DbContext就行了。

可问题是:

为什么这个项目在以前的运行过程中没有引发错误?

探究


还是上面那个项目,我们把导航属性的 Virtual 去掉,如下:

    public class ClassA
        
    {
        public int Id { get; set; }
        public string guid { get; set; }
        public int? child_id { get; set; }

        [ForeignKey("child_id")]
        public ClassB child { get; set; }
    }

运行项目,你会发现异常不见了。

究竟是什么原因?

对EF稍微了解的同学都知道,EF的导航属性默认是开启延迟加载的。

不了解的同学请搜索关键字补充一下关于EF延迟加载的基础知识,英语能力不错的同学请浏览官方文档( 关联与导航属性加载关联实体

去掉了virtual后,关闭了该导航属性的延迟加载功能,然后异常消失了。

是因为延迟加载导致的这个异常吗?

延迟加载又和IEntityChangeTracker有什么关系呢?

原因


顾名思义,其实 IEntityChangeTracker 就是用来追踪实体信息的,但令人不解的是,为什么关闭延迟加载之后,就算实体同时被两个DbContext追踪也不会报错。

来考虑一下EF的延迟加载是如何实现的,

EF使用了与Castle类似的动态代理技术,同时也存在着相同的缺陷(无法拦截没有被标识为virtual的成员)。

由于没看过EF的源码,官方文档也没有详细的说明,所以我只能推测,IEntityChangeTracker其实发挥类似拦截器的功能,

调用DbContext时,由EF产生实体的动态代理,在访问导航属性时,拦截请求访问数据库并填充导航属性。

而动态代理在产生之后,就无法在Attach到其他的DbContext中。

基于这个推测,我们可以使用一下的代码进行测试,关闭DbContext动态代理。

            using (var dbContext = new ADbContext())
            {
                dbContext.Configuration.ProxyCreationEnabled = false;
                var aa = dbContext.ClassA.Where(p => p.Id == 1).FirstOrDefault();
                dbContext.Entry<ClassA>(aa).State = System.Data.Entity.EntityState.Modified;
                using (var db2 = new ADbContext())
                {
                    db2.Entry<ClassA>(aa).State = System.Data.Entity.EntityState.Modified;
                    dbContext.SaveChanges();
                }
                dbContext.SaveChanges();
            }

结果通过。

为了证明该异常其实跟延迟加载没有关系,我们可以开启动态代理,然后关闭延迟加载。

            using (var dbContext = new ADbContext())
            {
                dbContext.Configuration.ProxyCreationEnabled = true;
                dbContext.Configuration.LazyLoadingEnabled = false;
                var aa = dbContext.ClassA.Where(p => p.Id == 1).FirstOrDefault();
                dbContext.Entry<ClassA>(aa).State = System.Data.Entity.EntityState.Modified;
                using (var db2 = new ADbContext())
                {
                    db2.Entry<ClassA>(aa).State = System.Data.Entity.EntityState.Modified;
                    dbContext.SaveChanges();
                }
                dbContext.SaveChanges();
            }

异常依旧发生。

证明引发该异常的并不是延迟加载功能,而在于EF动态代理的对象只能由一个DbContext追踪。

还有一点值得一提的是:

如果一个实体没有导航属性的话,EF也不会生成它的动态代理。

欢迎转载,注明出处即可。 如果你觉得这篇博文帮助到你了,请点下右下角的推荐让更多人看到它。
原文地址:https://www.cnblogs.com/RobotZero/p/6496964.html