如何让Entity Framework正确处理关联实体数组的修改?
我定义了带审计字段的抽象类AuditObject,用于在实体保存前自动设置字段:
public abstract class AuditObject { public string CreatedBy { get; set; } public string UpdatedBy { get; set; } }
在DbContext的保存前方法中设置审计字段:
private void OnBeforeSaving() { var user = _someService.GetUserName(); foreach (var entry in ChangeTracker.Entries()) { if (entry.State == EntityState.Modified && entry.Entity is AuditObject auditObject) { auditObject.UpdatedBy = user; entry.Property("CreatedBy").IsModified = false; } else if (entry.State == EntityState.Added && entry.Entity is AuditObject auditObject) { auditObject.CreatedBy = user; auditObject.UpdatedBy = user; } } }
继承该抽象类的实体及关联实体:
public abstract class Order: AuditObject { public long Id { get; set; } public List<OrderProducts> Products { get; set; } } public abstract class OrderProducts: AuditObject { public long Id { get; set; } public string Name { get; set; } }
更新接口中直接替换Products列表:
[HttpPut("{id:long}")] public async Task<Order> UpdateOrder(long id, Order order) { var currentOrder = await _dbContext.Order .Include(x => x.Products) .FirstOrDefaultAsync(x => x.Id == order.Id); currentOrder.Products = order.Products; await _dbContext.SaveChangesAsync(); return Ok(currentOrder); }
调试时发现EF的ChangeTracker将此操作识别为“修改+删除”,创建新的Products记录并抛出异常:
System.InvalidOperationException: The property 'OrderProduct.CreatedBy' cannot be assigned a value generated by the database. Store-generated values can only be assigned to properties configured to use store-generated values.
仅在更新包含已有Product ID的列表时出现该异常,创建实体时无问题。需要解决的是:如何让EF替换Products列表时,不识别为修改+删除操作?
1. 手动同步关联实体状态(推荐)
直接替换列表会让EF误判原有实体为删除、传入实体为新增。正确做法是对比新旧列表,手动处理增删改:
[HttpPut("{id:long}")] public async Task<IActionResult> UpdateOrder(long id, Order order) { var currentOrder = await _dbContext.Order .Include(x => x.Products) .FirstOrDefaultAsync(x => x.Id == order.Id); if (currentOrder == null) { return NotFound(); } // 移除当前订单中不存在于传入列表的产品 var productsToRemove = currentOrder.Products .Where(p => !order.Products.Any(op => op.Id == p.Id)) .ToList(); foreach (var product in productsToRemove) { _dbContext.OrderProducts.Remove(product); } // 处理新增或更新产品 foreach (var incomingProduct in order.Products) { var existingProduct = currentOrder.Products.FirstOrDefault(p => p.Id == incomingProduct.Id); if (existingProduct == null) { currentOrder.Products.Add(incomingProduct); } else { // 复制需要更新的属性,CreatedBy会被OnBeforeSaving标记为未修改 _dbContext.Entry(existingProduct).CurrentValues.SetValues(incomingProduct); } } await _dbContext.SaveChangesAsync(); return Ok(currentOrder); }
2. 手动设置传入实体的跟踪状态
如果坚持替换列表,需先将传入的已有实体附加到ChangeTracker并标记状态:
[HttpPut("{id:long}")] public async Task<IActionResult> UpdateOrder(long id, Order order) { var currentOrder = await _dbContext.Order .Include(x => x.Products) .FirstOrDefaultAsync(x => x.Id == order.Id); if (currentOrder == null) { return NotFound(); } // 标记传入产品的状态 foreach (var product in order.Products) { if (product.Id > 0) { // 已有实体标记为未变更,如需更新可改为Modified _dbContext.Entry(product).State = EntityState.Unchanged; } else { _dbContext.Entry(product).State = EntityState.Added; } } // 清空原有列表并添加传入列表 currentOrder.Products.Clear(); currentOrder.Products.AddRange(order.Products); await _dbContext.SaveChangesAsync(); return Ok(currentOrder); }
3. 检查CreatedBy字段配置
异常提示该字段被识别为数据库生成字段,需在实体配置中明确标记:
// 在DbContext的OnModelCreating方法中 modelBuilder.Entity<OrderProducts>() .Property(p => p.CreatedBy) .ValueGeneratedNever();
直接替换关联列表时,传入的OrderProducts实体不在当前ChangeTracker的跟踪范围内,EF会默认将其标记为Added状态,触发新增审计逻辑。同时原列表中的实体被标记为Deleted,两者叠加导致CreatedBy字段的处理冲突,最终抛出异常。
内容的提问来源于stack exchange,提问作者Matt

