DbSet.Local遍历触发‘Collection was modified; enumeration operation may not execute’异常的原因咨询
DbSet.Local遍历触发‘Collection was modified; enumeration operation may not execute’异常的原因咨询
嘿,我来给你捋清楚这个问题的来龙去脉,你遇到的这个异常其实是集合枚举的经典“坑”,完全不是你的代码写错了,而是DbSet.Local的特性导致的~
核心原因:DbSet.Local是动态集合,枚举时不允许修改
_ctx.Set<TObject>().Local本质是一个动态的ObservableCollection,它会实时反映当前DbContext中被跟踪的实体状态。当你直接枚举这个集合(比如你的原始InCache方法直接返回它)的时候,.NET的枚举器会检测集合的版本号——如果在枚举过程中(或者前后两次迭代之间)集合被修改(添加/删除元素),就会抛出Collection was modified异常,这是为了避免枚举结果不一致的问题。
你的代码触发异常的具体流程
我们来拆解你的循环逻辑:
- 第一次循环:
- 调用
GetByCache,先查Local集合(此时没有对应Entity),然后从数据库加载该实体,加载后EF会自动把这个实体加入Local集合。 - 接着调用
_repo.Insert(me),如果是新创建的Entity,EF也会把它加入Local集合。
- 调用
- 第二次循环:
- 再次调用
GetByCache,此时你直接枚举Local集合(原始InCache返回的就是它本身),但这个集合在第一次循环中已经被修改过(新增了元素),枚举器检测到集合版本变化,直接抛出异常。
- 再次调用
为什么ToList()能解决问题?
你修改后的InCache方法用ToList()把Local集合的元素一次性复制到一个静态的ListDbSet.Local的,后续对Local的修改不会影响它。枚举这个静态集合时,因为它的状态不会再变化,自然不会触发修改检测的异常。
关于你“不舒服”的小补充
其实这个解决方案完全合理,不用纠结~如果担心性能开销,一般来说Local集合里的实体数量不会特别多(毕竟是当前上下文跟踪的实体),ToList()的复制成本可以忽略不计。
另外,也可以给你一个小优化建议:在GetByCache里直接对Local做查询时,先转成List再查,避免每次都单独写ToList():
public T GetByCache<T>(Expression<Func<T, bool>> w) { var localEntities = _ctx.Set<T>().Local.ToList(); // 先转成静态List T e = localEntities.Where(w.Compile()).FirstOrDefault(); if (e == null) e = AllIncluding<T>().Where(w).FirstOrDefault(); return e; }
备注:内容来源于stack exchange,提问作者tschmit
相关产品推荐
相关产品推荐

