已将DbContext跟踪行为设为NoTracking,但测试仍失败,是否遗漏其他需设置NoTracking的位置?
你的测试失败的核心原因并不是遗漏了NoTracking的设置点,而是你误解了QueryTrackingBehavior.NoTracking的作用范围——它只控制查询操作返回的实体是否被跟踪,而通过Add、Attach等方法添加到DbContext的实体,在SaveChanges后仍然会处于被跟踪状态(状态为Unchanged)。
具体为什么会失败?
结合你的测试流程和代码来看:
- 调用
uow.SimpleRepo.Add(testEntity1)时,Repository会把testEntity1(TEntityOut)映射为TEntityIn实例并添加到DbSet,此时这个TEntityIn实例会被DbContext跟踪。 - 执行
SaveChangesAsync()后,该TEntityIn实例的状态变为Unchanged,但仍然处于DbContext的跟踪范围内。 - 你修改
testEntity1.Name后调用Update,Repository又会把修改后的testEntity1映射为一个全新的TEntityIn实例,再调用DbSet.Update()。 - 此时DbContext已经在跟踪一个同Id的
TEntityIn实例,EF会直接抛出InvalidOperationException:"The instance of entity type 'XXX' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked."
怎么解决?
你不需要额外设置NoTracking,而是要调整Update方法的实现,处理已被跟踪的实体:
方案1:更新已跟踪实体的属性(推荐)
修改BaseRepository的Update方法,先检查是否有已跟踪的同Id实体,如果有就直接更新它的属性,而不是创建新实例调用Update:
public virtual TEntityOut Update(TEntityOut? entity) { if (entity == null) throw new ArgumentNullException(nameof(entity)); var entityIn = Mapper.Map(entity)!; var trackedEntry = RepoDbContext.ChangeTracker.Entries<TEntityIn>() .FirstOrDefault(e => e.Entity.Id.Equals(entityIn.Id)); if (trackedEntry != null) { // 更新已跟踪实体的属性值 trackedEntry.CurrentValues.SetValues(entityIn); return Mapper.Map(trackedEntry.Entity)!; } else { // 没有跟踪的实体,直接使用Update return Mapper.Map(RepoDbSet.Update(entityIn).Entity)!; } }
方案2:直接设置实体状态为Modified
另一种方式是跳过DbSet.Update(),直接将实体状态设为Modified,这样即使有已跟踪的实体,EF也会直接更新它的状态:
public virtual TEntityOut Update(TEntityOut? entity) { if (entity == null) throw new ArgumentNullException(nameof(entity)); var entityIn = Mapper.Map(entity)!; RepoDbContext.Entry(entityIn).State = EntityState.Modified; return Mapper.Map(entityIn)!; }
方案3:SaveChanges后手动Detach实体
如果你希望Add后的实体也不被跟踪,可以在SaveChanges后手动将其从跟踪中移除(需要拿到对应的TEntityIn实例):
uow.SimpleRepo.Add(testEntity1); await uow.SaveChangesAsync(); // 从DbContext的本地跟踪集合中找到对应实体并解除跟踪 var trackedEntity = dbContext.Set<DalSimpleEntity>().Local.First(e => e.Id == id1); dbContext.Entry(trackedEntity).State = EntityState.Detached;
不过这个方案需要调整Repository的Add方法返回对应的TEntityIn实例,或者从DbContext的Local集合中查找,实用性不如前两个方案。
补充说明
全局的NoTracking设置本身没有问题,它确实会让所有查询返回的实体不被跟踪,但Add/Attach操作的实体不受这个设置影响——因为EF需要跟踪这些实体才能正确执行插入、更新操作。如果你希望完全禁用跟踪,唯一的方式是在每个操作后手动Detach,或者使用AsNoTracking()查询后修改再更新,但这会增加代码复杂度。
内容的提问来源于stack exchange,提问作者Hannes Härm

