启用AutoInclude后EF Core删除实体出现跟踪冲突的解决方法
实体类与EF配置
我有GdsReceptionBlackProduct、Employee和Customer三个实体类,代码定义如下:
public partial class GdsReceptionBlackProduct { public int Id { get; set; } public string? CreatedBy { get; set; } public virtual Employee? CreatedByNavigation { get; set; } } public void Configure(EntityTypeBuilder<GdsReceptionBlackProduct> entity) { entity.HasOne(d => d.CreatedByNavigation).WithMany(p => p.CreatedBy) .HasPrincipalKey(p => p.IdEmployee) .HasForeignKey(d => d.CreatedBy) .HasConstraintName("GDSReceptionBlackProduct_Employee_CreatedBy"); entity.Navigation(e => e.CreatedByNavigation).AutoInclude(); } public partial class Employee { public string IdEmployee { get; set; } = null!; public int? FkCountry { get; set; } public virtual Country Country { get; set; } public virtual ICollection<GdsReceptionBlackProduct> CreatedBy { get; } = new List<GdsReceptionBlackProduct>(); } public void Configure(EntityTypeBuilder<Employee> entity) { entity.HasOne(d => d.Country) .WithMany(p => p.Employees) .HasForeignKey(d => d.FkCountry) .HasConstraintName("FK_Employee_Country"); entity.Navigation(e => e.Country).AutoInclude(); } public partial class Customer { public int? Fkcountry { get; set; } public virtual Country? Country { get; set; } public virtual ICollection<GdsReceptionBlackProduct> GdsReceptionBlackProduct { get; } = new List<GdsReceptionBlackProduct>(); } public void Configure(EntityTypeBuilder<Customer> entity) { entity.Navigation(e => e.Country).AutoInclude(); }
服务注入与删除逻辑
我采用工作单元+仓储模式结合依赖注入,在FrmGDSReceptionBlackProduct窗体中注入服务的代码如下:
public FrmGDSReceptionBlackProduct(GdsReceptionBlackProductService gdsReceptionBlackProductService, CustomerService customerService) { InitializeComponent(); this.gdsReceptionBlackProductService = gdsReceptionBlackProductService; this.customerService = customerService; }
执行删除GdsReceptionBlackProduct实体的代码:
await gdsReceptionBlackProductService.RemoveGdsReceptionBlackProductAsync(Convert.ToInt32(txtID.Text)); // 泛型仓储中的RemoveAsync实现 public virtual async Task<bool> RemoveAsync(object id) { T? exist = await dbSet.FindAsync(id); if (exist == null) throw new ArgumentNullException($"{nameof(RemoveAsync)} entity must not be null"); dbSet.Remove(exist); return true; }
错误信息
执行删除时出现如下错误:
id could not be removed: The instance of entity type 'Country' cannot be tracked because another instance with the key value '{IdCountry: 1}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
注释掉entity.Navigation(e => e.CreatedByNavigation).AutoInclude();后删除可成功,但我需要保留AutoInclude来在GridView中显示关联属性,且所有CRUD操作都在同一窗体中,该如何解决?
更新信息
泛型仓储构造函数代码:
public class GenericRepository<T> : IGenericRepository<T>, IDisposable where T : class { protected SIMContext _context; internal DbSet<T> dbSet; public GenericRepository(SIMContext context) { _context = context; _context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; dbSet = context.Set<T>(); } }
原因分析
问题核心是实体跟踪冲突:
- 泛型仓储全局设置了
QueryTrackingBehavior.NoTracking,但FindAsync方法会强制跟踪找到的实体。 AutoInclude自动加载CreatedByNavigation(Employee)及其关联的Country,该Country会被上下文跟踪。- 窗体中注入的
CustomerService可能已加载过同ID的Country实体(Customer也配置了AutoInclude Country),导致同一上下文存在两个相同Key的Country跟踪实例,删除时触发冲突。
解决方法
方法1:删除前解除关联实体的跟踪
修改RemoveAsync方法,手动停止关联实体的跟踪状态:
public virtual async Task<bool> RemoveAsync(object id) { T? exist = await dbSet.FindAsync(id); if (exist == null) throw new ArgumentNullException($"{nameof(RemoveAsync)} entity must not be null"); // 针对GdsReceptionBlackProduct类型处理关联实体 if (exist is GdsReceptionBlackProduct blackProduct) { if (blackProduct.CreatedByNavigation != null) { // 停止跟踪Employee _context.Entry(blackProduct.CreatedByNavigation).State = EntityState.Detached; // 停止跟踪关联的Country if (blackProduct.CreatedByNavigation.Country != null) { _context.Entry(blackProduct.CreatedByNavigation.Country).State = EntityState.Detached; } } } dbSet.Remove(exist); return true; }
方法2:查询删除实体时禁用自动包含
删除操作不需要关联实体,查询时直接忽略AutoInclude:
public virtual async Task<bool> RemoveAsync(object id) { // 忽略自动包含,仅加载主实体 T? exist = await dbSet.IgnoreAutoIncludes() .FirstOrDefaultAsync(e => EF.Property<object>(e, "Id") == id); if (exist == null) throw new ArgumentNullException($"{nameof(RemoveAsync)} entity must not be null"); // 附加实体并标记为删除 dbSet.Attach(exist); dbSet.Remove(exist); return true; }
方法3:调整上下文生命周期
如果当前使用的是单例上下文,改为作用域上下文(每个操作实例化一个上下文),不同操作的上下文相互隔离,避免跨操作的跟踪冲突。
方法4:拆分查询逻辑
为GridView显示和删除操作分别提供不同的查询方法:
// 用于GridView显示,保留自动包含 public async Task<IEnumerable<T>> GetAllWithIncludesAsync() { return await dbSet.AsTracking().ToListAsync(); } // 用于删除/更新,禁用自动包含 public async Task<T?> GetByIdWithoutIncludesAsync(object id) { return await dbSet.IgnoreAutoIncludes().FindAsync(id); }
删除时调用GetByIdWithoutIncludesAsync获取实体再执行删除。
内容的提问来源于stack exchange,提问作者M.Bouabdallah

