升级.NET5后DbContext并发异常修复:代码及问题排查
背景
原代码运行在.NET Framework 4.7.2 MVC + Entity Framework 6环境,已将业务逻辑、领域对象及数据层迁移至.NET Standard 2.0,数据层采用Entity Framework Core 3.1。近期将部分站点升级至.NET5,数据层未改动,升级后偶发以下异常:
Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded.
at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ThrowAggregateUpdaFHoncurrencyException(Int32 commandIndex, Int32 expectedRowsAffected, Int32 rowsAffected)
at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ConsumeResultSetWithoutPropagation(Int32 commandIndex, RelationalDataReader reader)
at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.Consume(RelationalDataReader reader)
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.Execute(IRelationalConnection connection)
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(IEnumerable1 commandBatches, IRelationalConnection connection) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(IList1 entriesToSave)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(DbContext _, Boolean acceptAllChangesOnSuccess)
at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.Execute[TState,TResult](TState state, Func3 operation, Func3 verifySucceeded)
at Microsoft.EntityFrameworkCore.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess)
at FH.Data.Models.EntityFramework.DataContext.FHDbContext.SaveChanges() in D:\Code\cntr\src\FH.Data.Models\Partials\DataContext\FHDbContext.cs:line 73
at FH.Data.Access.Repositories.Time.TimesheetRepository.SaveTimePunches(List1 timePunches) in D:\Code\cntr\src\FH.Data.Access\Repositories\Time\TimesheetRepository.cs:line 2096 at FH.BusinessLogic.Core.Timesheet.TimesheetController.SaveTimePunches(List1 timePunches) in D:\Code\cntr\src\FHBllStandard\Timesheet\TimesheetController.cs:line 892
at FH.Employees.Controllers.TimesheetController.SavePunches(List`1 timepunches) in D:\Code\cntr\src\FH.Employees\Controllers\TimesheetController.cs:line 139
at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()
相关代码
异常触发于SaveChanges()行:
var workDates = timePunches.Select(x => x.WorkDate.Date) .Distinct().ToList(); var jobId = timePunches.First().JobId; var punches = _context.TimePunches .Where(y => workDates.Contains(y.WorkDate.Date) && y.JobId == jobId) .ToList(); if (punches.Any()) { _context.TimePunches.RemoveRange(punches); _context.SaveChanges(); }
问题原因
- 并发概率提升:.NET5的请求处理效率更高,同一批TimePunch数据被多请求同时操作的概率增加。原代码先查询再删除的逻辑存在时间窗口:查询出数据后到执行删除前,其他请求可能已经删除了这批数据,导致当前
SaveChanges()时实际影响行数为0,触发EF Core的并发验证。 - EF与EF Core行为差异:Entity Framework 6默认不会对“预期影响行数不符”抛出异常(除非配置了并发令牌),但EF Core 3.1默认会针对
RemoveRange这类操作检查受影响行数,当实际行数与预期不符时抛出DbUpdateConcurrencyException,这是升级后异常显现的核心原因。
修复方案
方案1:直接执行批量删除(推荐)
跳过加载实体的步骤,直接在数据库层面执行删除,彻底消除并发时间窗口:
var workDates = timePunches.Select(x => x.WorkDate.Date) .Distinct().ToList(); var jobId = timePunches.First().JobId; // EF Core 3.1需引入Microsoft.EntityFrameworkCore.Relational包 var deleteCount = _context.TimePunches .Where(y => workDates.Contains(y.WorkDate.Date) && y.JobId == jobId) .ExecuteDelete();
若版本不支持ExecuteDelete,可直接执行原生SQL:
var jobIdParam = new SqlParameter("@JobId", jobId); var workDateParams = workDates.Select((date, index) => new SqlParameter($"@WorkDate{index}", date)).ToArray(); var datePlaceholders = string.Join(", ", workDateParams.Select(p => p.ParameterName)); var sql = $"DELETE FROM TimePunches WHERE CAST(WorkDate AS DATE) IN ({datePlaceholders}) AND JobId = @JobId"; _context.Database.ExecuteSqlRaw(sql, workDateParams.Concat(new[] { jobIdParam }).ToArray());
方案2:捕获并处理并发异常
若需保留先查询再删除的逻辑,可捕获异常后重试或忽略:
var workDates = timePunches.Select(x => x.WorkDate.Date) .Distinct().ToList(); var jobId = timePunches.First().JobId; try { var punches = _context.TimePunches .Where(y => workDates.Contains(y.WorkDate.Date) && y.JobId == jobId) .ToList(); if (punches.Any()) { _context.TimePunches.RemoveRange(punches); _context.SaveChanges(); } } catch (DbUpdateConcurrencyException) { // 重试一次,确认数据是否真的已被删除 var retryPunches = _context.TimePunches .Where(y => workDates.Contains(y.WorkDate.Date) && y.JobId == jobId) .ToList(); if (retryPunches.Any()) { _context.TimePunches.RemoveRange(retryPunches); _context.SaveChanges(); } }
方案3:关闭EF Core并发检查(不推荐)
通过配置关闭受影响行数验证,但会隐藏真实并发问题,仅适合临时应急:
在FHDbContext的SaveChanges方法中修改:
public override int SaveChanges() { return base.SaveChanges(SaveChangesOptions.IgnoreAllConcurrencyConflicts); }
内容的提问来源于stack exchange,提问作者geoff swartz

