You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Entity Framework Core更新时DbUpdateConcurrencyException异常排查

.NET Core + ASP.NET Identity + EF Core 更新Game实体时的并发异常问题

问题场景

在学习.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的用户信息,需确保提交完整的关键字段并手动跟踪状态:

  1. 在视图中添加Creator的Id隐藏字段:
<input asp-for="Creator.Id" hidden />
  1. 修改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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 22:58:11