ASP.NET Core+EF仅保存修改实体时报错:实体已被跟踪
我只想保存修改过的Period实体,移除以下判断语句时程序运行正常:
if (!period.IsSame(_context.Periods.First(p => p.ID == period.ID)))
但保留该语句后,执行_context.Attach(period)或Update方法时会触发错误:
InvalidOperationException: The instance of entity type 'Period' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
我不清楚该如何正确检测实体是否修改,相关代码如下:
public async Task<IActionResult> OnPostAsync(List<Period> periods) { if (!ModelState.IsValid) { return Page(); } int i = 0; foreach (var period in periods) { TimeOnly startTime = TimeOnly.Parse(Request.Form["StartTime" + i].ToString()); TimeOnly endTime = TimeOnly.Parse(Request.Form["EndTime" + i].ToString()); period.StartHour = startTime.Hour; period.StartMinute = startTime.Minute; period.EndHour = endTime.Hour; period.EndMinute = endTime.Minute; period.StatusDate = DateTime.Now; // 已存在的实体 if (period.ID > 0) { // 判断是否修改 if (!period.IsSame(_context.Periods.First(p => p.ID == period.ID))) { _context.Attach(period); if (period.Delete) { period.Status = (int)Status.deleted; } else { period.Status = (int)Status.modified; } } } // 新增实体 else { period.Status = (int)Status.created; _context.Attach(period); } i++; } await _context.SaveChangesAsync(); return RedirectToPage("./Periods"); }
注:网页端传来的实体已脱离上下文跟踪。
报错的核心原因是:_context.Periods.First(p => p.ID == period.ID)会将数据库中对应ID的Period实体加载到当前上下文并开启跟踪,之后你尝试Attach网页传来的同ID实体时,EF Core不允许同一个上下文同时跟踪两个拥有相同主键的实体实例,因此抛出跟踪冲突异常。
下面提供几种可行的解决思路,你可以根据实际需求选择:
思路一:加载数据库实体时禁用跟踪
获取数据库中的实体时,使用AsNoTracking()方法让上下文不跟踪该实例,这样后续Attach网页传来的实体就不会产生冲突:
if (period.ID > 0) { // 禁用跟踪加载数据库实体 var dbPeriod = _context.Periods.AsNoTracking().FirstOrDefault(p => p.ID == period.ID); if (dbPeriod != null && !period.IsSame(dbPeriod)) { _context.Attach(period); if (period.Delete) { period.Status = (int)Status.deleted; } else { period.Status = (int)Status.modified; } } }
这里把First改成FirstOrDefault,避免找不到实体时直接抛出异常,代码更健壮。
思路二:直接修改已跟踪的数据库实体
既然已经加载了数据库中的实体到上下文,那就直接修改这个已跟踪的实体,不需要再Attach网页传来的实例,EF Core会自动检测变化并保存:
if (period.ID > 0) { var dbPeriod = _context.Periods.FirstOrDefault(p => p.ID == period.ID); if (dbPeriod != null && !period.IsSame(dbPeriod)) { // 将网页实体的属性同步到已跟踪的数据库实体 dbPeriod.StartHour = period.StartHour; dbPeriod.StartMinute = period.StartMinute; dbPeriod.EndHour = period.EndHour; dbPeriod.EndMinute = period.EndMinute; dbPeriod.StatusDate = period.StatusDate; if (period.Delete) { dbPeriod.Status = (int)Status.deleted; } else { dbPeriod.Status = (int)Status.modified; } // 无需Attach,上下文已跟踪dbPeriod,SaveChanges会自动保存修改 } }
思路三:先解除原有实体的跟踪再Attach新实例
如果一定要使用网页传来的实体进行操作,可以先将上下文中原有的跟踪实体解除跟踪,再Attach新实例:
if (period.ID > 0) { var dbPeriod = _context.Periods.FirstOrDefault(p => p.ID == period.ID); if (dbPeriod != null && !period.IsSame(dbPeriod)) { // 解除原有实体的跟踪 _context.Entry(dbPeriod).State = EntityState.Detached; _context.Attach(period); if (period.Delete) { period.Status = (int)Status.deleted; } else { period.Status = (int)Status.modified; } } }
- 异步方法中建议使用
FirstOrDefaultAsync代替同步的FirstOrDefault,提升性能:await _context.Periods.AsNoTracking().FirstOrDefaultAsync(p => p.ID == period.ID); - 可以考虑将实体属性同步的逻辑封装到Period类的方法中(比如
UpdateFrom(Period source)),避免重复代码。
内容的提问来源于stack exchange,提问作者Rose-marie Paradis

