Entity Framework Core 2.0变更追踪引发异常的替代更新方案咨询
你遇到的这个问题其实是EF Core相较于旧版EF,实体跟踪机制更严格导致的典型冲突。先理清楚问题场景:
在Entity Framework 1.0/1.1中,这段代码能正常运行:
var pupilFound = await context.Pupils.SingleOrDefaultAsync(p => p.Id == pupil.Id); if (pupilFound == null) { throw new BadDataException($"{nameof(pupil.Id)} is not valid"); } if (pupilFound.UserId != userId) { throw new NotAuthorizedException(); } pupil.UserId = userId; context.Entry(pupil).State = EntityState.Modified; await context.SaveChangesAsync();升级到EF Core 2.0后,抛出异常:
Cannot be tracked because another instance of this type with the same key is already being tracked。临时用AsNoTracking规避了问题,但官方明确说这个方法只适用于只读场景,想找到正确更新客户端传来的Pupil所有属性的方式。
问题根源
当你用SingleOrDefaultAsync查询出pupilFound后,EF Core的上下文已经开始跟踪这个实体了。之后你又把客户端传来的pupil实体标记为Modified,而它的主键和pupilFound完全一致——EF Core不允许同一个上下文同时跟踪两个同类型、同主键的实体,所以就抛出了冲突异常。
正确的解决方案
这里给你两种靠谱的实现方式,根据你的业务场景选择:
方案1:利用已跟踪的实体更新属性(推荐)
既然已经查询到了pupilFound(上下文正在跟踪它),直接把客户端传来的实体属性复制到这个已跟踪的实例上就行,不需要手动设置实体状态:
var pupilFound = await context.Pupils.SingleOrDefaultAsync(p => p.Id == pupil.Id); if (pupilFound == null) { throw new BadDataException($"{nameof(pupil.Id)} is not valid"); } if (pupilFound.UserId != userId) { throw new NotAuthorizedException(); } // 一键复制客户端传来的所有属性值到已跟踪的实体 context.Entry(pupilFound).CurrentValues.SetValues(pupil); // 单独确保UserId是当前用户的(覆盖客户端传来的值,保证权限安全) pupilFound.UserId = userId; await context.SaveChangesAsync();
这种方式的优势很明显:上下文只跟踪一个实体,完全避免了冲突;而且SetValues会自动识别属性变化,最终只会更新有改动的字段(默认开启变更追踪的情况下),性能也更优。
方案2:直接附加实体更新(适合已知实体存在的场景)
如果你能提前确认客户端传来的pupil肯定存在于数据库中,也可以跳过查询步骤,直接附加实体并处理跟踪冲突:
// 先检查上下文本地缓存中是否已经在跟踪该实体 var existingPupil = context.Pupils.Local.FirstOrDefault(p => p.Id == pupil.Id); if (existingPupil != null) { // 已经在跟踪,就用客户端数据更新属性 context.Entry(existingPupil).CurrentValues.SetValues(pupil); } else { // 不在跟踪列表里,附加并标记为修改状态 context.Entry(pupil).State = EntityState.Modified; } // 强制设置UserId为当前用户,确保权限合规 pupil.UserId = userId; await context.SaveChangesAsync();
注意:这种方式需要你确保实体一定存在,否则EF Core可能会把它当成新实体插入数据库(如果主键是自增类型的话)。如果需要验证实体是否存在,方案1会更稳妥。
为什么不推荐用AsNoTracking?
你用AsNoTracking虽然能临时解决冲突,但它的设计初衷是只读场景:
- 加上这个方法后,查询出来的
pupilFound不会被上下文跟踪,后续如果对它做修改,这些改动不会被保存到数据库; - 绕过跟踪机制可能会导致并发冲突、数据不一致等潜在问题,不符合EF Core的最佳实践。
内容的提问来源于stack exchange,提问作者HelloWorld

