Entity Framework Core更新时DbUpdateConcurrencyException异常排查
问题场景
在学习.NET Core结合ASP.NET Identity与EF Core开发时,更新Game实体信息时遇到DbUpdateConcurrencyException异常,移除Game实体中的Creator和Winner导航属性后更新功能正常,但希望保留与AspNetUsers表的关联。
代码结构
自定义实体类
// Game.cs - 表示游戏主实体 public class Game { [Key] public int Id { get; set; } public string Name { get; set; } = null!; public string CreatorId { get; set; } = null!; public ApplicationUser Creator { get; set; } = null!; public DateTime CreatedDate { get; set; } public string? WinnerId { get; set; } public ApplicationUser? Winner { get; set; } = null!; public int RemainingPlayers { get; set; } public int EliminatedPlayers { get; set; } } public class FixtureSet { [Key] public int Id { get; set; } public int GameId { get; set; } public Game Game { get; set; } = null!; } public class Fixture { [Key] public int Id { get; set; } public int FixtureSetId { get; set; } public FixtureSet FixtureSet { get; set; } = null!; public string? HomeTeam { get; set; } public string? AwayTeam { get; set; } public FixtureOutcome FixtureOutcome { get; set; } }
DbContext代码
public class ApplicationDbContext : IdentityDbContext<ApplicationUser> { public DbSet<ApplicationUser> ApplicationUsers { get; set; } public DbSet<Game> Games { get; set; } public DbSet<FixtureSet> FixtureSets { get; set; } public DbSet<Fixture> Fixtures { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<Game>() .HasOne(g => g.Creator) .WithMany() .HasForeignKey(g => g.CreatorId) .OnDelete(DeleteBehavior.Restrict); modelBuilder.Entity<Game>() .HasOne(g => g.Winner) .WithMany() .HasForeignKey(g => g.WinnerId) .OnDelete(DeleteBehavior.Restrict); //seed data... } }
更新控制器方法
public IActionResult Update(int gameId) { Game? game = _dbContext.Games .Include(x => x.Winner) .Include(x => x.Creator) .FirstOrDefault(u => u.Id == gameId); if (game == null) { return RedirectToAction("Error", "Home"); } return View(game); } [HttpPost] public IActionResult Update(Game game) { if (ModelState.IsValid) { _dbContext.Games.Update(game); _dbContext.SaveChanges(); return RedirectToAction("Index"); } return View(game); }
Update.cshtml视图
@model LastPersonStanding.Domain.Entities.Game <div class="w-100 card border-0 p-4"> <div class="card-header bg-success bg-gradient ml-0 py-3"> <div class="row"> <div class="col-12 text-center"> <h2 class="text-white py-2">Update Game</h2> </div> </div> </div> <div class="card-body border p-4"> <form method="post" class="row" enctype="multipart/form-data"> <div class="p-3"> <div class="form-floating py-1 col-12"> <label asp-for="Id" class="ms-2">Game Id</label> <p class="form-control border shadow">@Model.Id</p> </div> @*<div class="form-floating py-1 col-12"> <label asp-for="@Model.Creator.Id" class="ms-2">Creator Id</label> <p class="form-control border shadow">@Model.CreatorId</p> </div>*@ <div class="form-floating py-1 col-12"> <input asp-for="Creator.FirstName" class="form-control border shadow" /> <label asp-for="Creator.FirstName" class="ms-2">First Name</label> <span asp-validation-for="Creator.FirstName" class="text-danger"></span> </div> <div class="form-floating py-1 col-12"> <input asp-for="Creator.LastName" class="form-control border shadow" /> <label asp-for="Creator.LastName" class="ms-2">Last Name</label> <span asp-validation-for="Creator.LastName" class="text-danger"></span> </div> <div class="form-floating py-1 col-12"> <input asp-for="Name" class="form-control border shadow" /> <label asp-for="Name" class="ms-2"></label> <span asp-validation-for="Name" class="text-danger"></span> </div> <div class="row pt-2"> <div class="col-6 col-md-3"> <button type="submit" class="btn btn-success w-100"> <i class="bi bi-check-circle"></i> Update </button> </div> <div class="col-6 col-md-3"> <a asp-controller="Game" asp-action="Index" class="btn btn-secondary w-100"> <i class="bi bi-x-circle"></i> Cancel </a> </div> </div> </div> <input asp-for="Id" hidden /> <input asp-for="CreatorId" hidden /> </form> </div> </div> @section Scripts{ @{ <partial name="_ValidationScriptsPartial" /> } }
问题现象
调用[HttpPost]更新方法时,抛出异常:
DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded.
额外上下文
- 种子数据正常工作,所有DbSet数据已存在于SQL数据库
- Creator和Winner关系在
OnModelCreating中配置为DeleteBehavior.Restrict - Game表关联AspNetUsers表,记录创建者和获胜者,包含两个指向ApplicationUser的导航属性
- 已扩展IdentityUser为ApplicationUser
- 异常在
_dbContext.SaveChanges()时抛出 - 参考的Udemy课程使用不同实体关系,同类型代码无异常
问题原因
使用_dbContext.Games.Update(game)时,EF Core会将整个Game实体及其关联的Creator/Winner标记为修改状态。但从视图提交的Game对象中,Creator仅包含FirstName和LastName,缺少ApplicationUser的关键字段(如Id、SecurityStamp等),EF Core尝试更新ApplicationUser时无法匹配数据库中的行,从而触发并发异常。
解决方案
方案1:仅更新Game实体本身(不修改关联User)
如果不需要修改Creator信息,仅更新Game的属性,修改Post方法:
[HttpPost] public IActionResult Update(Game game) { if (ModelState.IsValid) { var existingGame = _dbContext.Games.Find(game.Id); if (existingGame == null) { return RedirectToAction("Error", "Home"); } // 仅更新需要修改的Game属性 existingGame.Name = game.Name; // 如需更新其他属性,例如RemainingPlayers,在此添加 _dbContext.SaveChanges(); return RedirectToAction("Index"); } return View(game); }
同时修改视图,将Creator的输入框改为静态展示:
<div class="form-floating py-1 col-12"> <label class="ms-2">Creator Name</label> <p class="form-control border shadow">@Model.Creator.FirstName @Model.Creator.LastName</p> </div>
方案2:需要修改Creator信息时的处理
如果要同时更新Creator的用户信息,需确保提交完整的关键字段并手动跟踪状态:
- 在视图中添加Creator的Id隐藏字段:
<input asp-for="Creator.Id" hidden />
- 修改Post方法:
[HttpPost] public IActionResult Update(Game game) { if (ModelState.IsValid) { // 附加Game实体并标记需要修改的属性 _dbContext.Games.Attach(game); _dbContext.Entry(game).Property(g => g.Name).IsModified = true; // 处理Creator实体 if (game.Creator != null) { _dbContext.ApplicationUsers.Attach(game.Creator); _dbContext.Entry(game.Creator).Property(u => u.FirstName).IsModified = true; _dbContext.Entry(game.Creator).Property(u => u.LastName).IsModified = true; } _dbContext.SaveChanges(); return RedirectToAction("Index"); } return View(game); }
方案3:使用ViewModel规范数据传递(推荐)
创建专门的ViewModel,仅包含需要更新的字段,避免直接传递实体:
public class UpdateGameViewModel { public int Id { get; set; } public string Name { get; set; } = null!; public string CreatorId { get; set; } = null!; public string CreatorFirstName { get; set; } = null!; public string CreatorLastName { get; set; } = null!; // 添加其他需要更新的Game属性 }
Get方法中映射实体到ViewModel:
public IActionResult Update(int gameId) { var game = _dbContext.Games .Include(x => x.Creator) .FirstOrDefault(u => u.Id == gameId); if (game == null) { return RedirectToAction("Error", "Home"); } var viewModel = new UpdateGameViewModel { Id = game.Id, Name = game.Name, CreatorId = game.CreatorId, CreatorFirstName = game.Creator.FirstName, CreatorLastName = game.Creator.LastName }; return View(viewModel); }
Post方法中映射ViewModel到实体:
[HttpPost] public IActionResult Update(UpdateGameViewModel viewModel) { if (ModelState.IsValid) { var game = _dbContext.Games.Find(viewModel.Id); if (game == null) { return RedirectToAction("Error", "Home"); } game.Name = viewModel.Name; var creator = _dbContext.ApplicationUsers.Find(viewModel.CreatorId); if (creator != null) { creator.FirstName = viewModel.CreatorFirstName; creator.LastName = viewModel.CreatorLastName; } _dbContext.SaveChanges(); return RedirectToAction("Index"); } return View(viewModel); }
视图修改为绑定ViewModel:
@model UpdateGameViewModel <div class="form-floating py-1 col-12"> <input asp-for="CreatorFirstName" class="form-control border shadow" /> <label asp-for="CreatorFirstName" class="ms-2">First Name</label> <span asp-validation-for="CreatorFirstName" class="text-danger"></span> </div> <div class="form-floating py-1 col-12"> <input asp-for="CreatorLastName" class="form-control border shadow" /> <label asp-for="CreatorLastName" class="ms-2">Last Name</label> <span asp-validation-for="CreatorLastName" class="text-danger"></span> </div>
内容的提问来源于stack exchange,提问作者Paul

