EF Core拦截器未调用基类方法仍触发实体跟踪冲突错误排查
问题
在EF Core 7中实现了EnumEntitySaveInterceptor拦截器,意图移除ChangeTracker中具有相同Id的重复IEnumEntity类型条目。但调用SaveChanges时,执行代码var trackedEntities = context.ChangeTracker.Entries<IEnumEntity>()时,抛出错误:
The instance of entity type cannot be tracked because another instance of this type with the same key is already being tracked
尚未调用base.SavingChangesAsync(eventData, result, cancellationToken),却出现该错误。
拦截器代码
public class EnumEntitySaveInterceptor : SaveChangesInterceptor { public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default) { var context = eventData.Context; // Get a list of all added and modified entities grouped by their type and Id var trackedEntities = context.ChangeTracker.Entries<IEnumEntity>() .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified) .GroupBy(e => new { Type = e.Entity.GetType(), Id = GetEntityId(e.Entity) }); // Loop through each group of entities with the same Type and Id foreach (var group in trackedEntities) { // If there are multiple entities with the same Type and Id, // detach all but the first one var entitiesToDetach = group.Skip(1).ToList(); foreach (var entry in entitiesToDetach) { entry.State = EntityState.Detached; } } return base.SavingChangesAsync(eventData, result, cancellationToken); } public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result) { .... same logic as SavingChangesAsync } private int GetEntityId(object entity) { var type = entity.GetType(); var idProperty = type.GetProperty("Id"); if (idProperty != null && idProperty.PropertyType == typeof(int)) { return (int)idProperty.GetValue(entity); } throw new InvalidOperationException($"Entity type {type} does not have an Id property of type int."); } }
异常堆栈跟踪
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.ThrowIdentityConflict(InternalEntityEntry entry) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKey key, InternalEntityEntry entry, Boolean updateDuplicate) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKey key, InternalEntityEntry entry) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(InternalEntityEntry entry) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.StartTracking(InternalEntityEntry entry) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState oldState, EntityState newState, Boolean acceptChanges, Boolean modifyProperties) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState entityState, Boolean acceptChanges, Boolean modifyProperties, Nullable`1 forceStateWhenUnknownKey, Nullable`1 fallbackState) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.PaintAction(EntityEntryGraphNode`1 node) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode`1 node, Func`2 handleNode) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode`1 node, Func`2 handleNode) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.AttachGraph(InternalEntityEntry rootEntry, EntityState targetState, EntityState storeGeneratedWithKeySetTargetState, Boolean forceStateWhenUnknownKey) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.NavigationCollectionChanged(InternalEntityEntry entry, INavigationBase navigationBase, IEnumerable`1 added, IEnumerable`1 removed) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntryNotifier.NavigationCollectionChanged(InternalEntityEntry entry, INavigationBase navigationBase, IEnumerable`1 added, IEnumerable`1 removed) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectNavigationChange(InternalEntityEntry entry, INavigationBase navigationBase) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.LocalDetectChanges(InternalEntityEntry entry) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectChanges(IStateManager stateManager) at Microsoft.EntityFrameworkCore.ChangeTracking.ChangeTracker.DetectChanges() at Microsoft.EntityFrameworkCore.ChangeTracking.ChangeTracker.TryDetectChanges() at Microsoft.EntityFrameworkCore.ChangeTracking.ChangeTracker.Entries[TEntity]() at MyProject.EntityFramework.Inteceptors.EnumEntitySaveInterceptor.SavingChangesAsync(DbContextEventData eventData, InterceptionResult`1 result, CancellationToken cancellationToken) in C:\Github\........ at Microsoft.EntityFrameworkCore.Diagnostics.CoreLoggerExtensions.SaveChangesStartingAsync(IDiagnosticsLogger`1 diagnostics, DbContext context, CancellationToken cancellationToken) at Microsoft.EntityFrameworkCore.DbContext.<SaveChangesAsync>d__64.MoveNext()
原因分析
错误发生在调用ChangeTracker.Entries<IEnumEntity>()时,而非后续的base.SavingChangesAsync调用。核心原因是:
- EF Core的
ChangeTracker.Entries<T>()方法会自动触发DetectChanges(),用于同步实体状态与上下文的追踪状态。 - 在
DetectChanges()执行过程中,EF会遍历实体的导航属性,尝试将关联实体纳入ChangeTracker的追踪范围。 - 如果此时存在两个同类型且主键相同的
IEnumEntity实体(可能通过导航属性引入),EF会立即抛出重复追踪错误,这一过程早于自定义拦截逻辑的执行。
解决方案
绕过ChangeTracker.Entries<T>()自动触发的DetectChanges(),直接从EF Core内部的IStateManager获取实体条目,避免提前触发导航属性的自动追踪。
修改后的拦截器代码如下:
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Infrastructure; public class EnumEntitySaveInterceptor : SaveChangesInterceptor { public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default) { var context = eventData.Context; var stateManager = context.GetService<IStateManager>(); // 直接从StateManager获取实体,避免触发DetectChanges var trackedEntities = stateManager.GetEntries() .Where(e => e.Entity is IEnumEntity && (e.State == EntityState.Added || e.State == EntityState.Modified)) .Select(e => new { Entry = e, Entity = (IEnumEntity)e.Entity }) .GroupBy(x => new { Type = x.Entity.GetType(), Id = GetEntityId(x.Entity) }); foreach (var group in trackedEntities) { var entitiesToDetach = group.Skip(1).ToList(); foreach (var item in entitiesToDetach) { item.Entry.State = EntityState.Detached; } } return base.SavingChangesAsync(eventData, result, cancellationToken); } public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result) { var context = eventData.Context; var stateManager = context.GetService<IStateManager>(); var trackedEntities = stateManager.GetEntries() .Where(e => e.Entity is IEnumEntity && (e.State == EntityState.Added || e.State == EntityState.Modified)) .Select(e => new { Entry = e, Entity = (IEnumEntity)e.Entity }) .GroupBy(x => new { Type = x.Entity.GetType(), Id = GetEntityId(x.Entity) }); foreach (var group in trackedEntities) { var entitiesToDetach = group.Skip(1).ToList(); foreach (var item in entitiesToDetach) { item.Entry.State = EntityState.Detached; } } return base.SavingChanges(eventData, result); } private int GetEntityId(object entity) { var type = entity.GetType(); var idProperty = type.GetProperty("Id"); if (idProperty != null && idProperty.PropertyType == typeof(int)) { return (int)idProperty.GetValue(entity); } throw new InvalidOperationException($"Entity type {type} does not have an Id property of type int."); } }
关键改动说明
- 使用
context.GetService<IStateManager>()获取EF Core内部状态管理器,直接访问已追踪的实体条目。 stateManager.GetEntries()不会自动触发DetectChanges(),因此可以在处理重复实体前避免导航属性的自动追踪,从而规避提前抛出的重复键错误。- 处理完重复实体后,再调用
base.SavingChangesAsync,EF会正常执行后续的保存逻辑。
内容的提问来源于stack exchange,提问作者pantonis
相关产品推荐
相关产品推荐

