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

EF Core调用SaveChangesAsync时出现DbUpdateConcurrencyException的排查与解决

EF Core DbUpdateConcurrencyException 问题分析与解决

错误详情

Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException
Attempted to update or delete an entity that does not exist in the store.
   at Microsoft.EntityFrameworkCore.InMemory.Storage.Internal.InMemoryTable`1.Update(IUpdateEntry entry, IDiagnosticsLogger`1 updateLogger)
   at Microsoft.EntityFrameworkCore.InMemory.Storage.Internal.InMemoryStore.ExecuteTransaction(IList`1 entries, IDiagnosticsLogger`1 updateLogger)
   at Microsoft.EntityFrameworkCore.InMemory.Storage.Internal.InMemoryDatabase.SaveChangesAsync(IList`1 entries, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
   at Personnel.Infrastructure.Repositories.Persons.PersonWriteRepository.UpdateAsync(Person person, CancellationToken cancellationToken) in C:\Users\user\RiderProjects\staffpro.learn.redkozubov\src\Services\Personnel\Infrastructure\Personnel.Infrastructure.Repositories\Persons\PersonWriteRepository.cs:line 54
   at Personnel.Unit.RepositoryTests.Persons.Write.PersonWriteRepositoryPositiveTests.UpdateAsync_WithWorkExperience_AddsPersonAndContext(Person person, WorkExperienceDto workExperienceDto, PersonWriteRepository writeRepository, PersonReadRepository readRepository) in C:\Users\user\RiderProjects\staffpro.learn.redkozubov\src\Services\Personnel\Tests\Personnel.Unit\RepositoryTests\Persons\Write\PersonWriteRepositoryPositiveTests.cs:line 71
   at Xunit.Sdk.TestInvoker`1.<>c__DisplayClass48_0.<<InvokeTestMethodAsync>b__1>d.MoveNext() in /_/src/xunit.execution/Sdk/Frameworks/Runners/TestInvoker.cs:line 276
--- End of stack trace from previous location ---
   at Xunit.Sdk.ExecutionTimer.AggregateAsync(Func`1 asyncAction) in /_/src/xunit.execution/Sdk/Frameworks/ExecutionTimer.cs:line 48
   at Xunit.Sdk.ExceptionAggregator.RunAsync(Func`1 code) in /_/src/xunit.core/Sdk/ExceptionAggregator.cs:line 90

相关代码

测试代码

[Theory]
[MemberData(nameof(ValidTestDataGenerator.GetPersonWithWorkExperienceDataAndRepositories), MemberType = typeof(ValidTestDataGenerator))]
internal async Task UpdateAsync_WithWorkExperience_AddsPersonAndContext(Person person, WorkExperienceDto workExperienceDto, PersonWriteRepository writeRepository, PersonReadRepository readRepository)
{
    // Arrange
    await writeRepository.AddAsync(person);

    var dbPerson = await readRepository.GetByIdAsync(person.Id);

    dbPerson.AddWorkExperience(
        workExperienceDto.Id,
        workExperienceDto.JobTitle,
        workExperienceDto.Street,
        workExperienceDto.Country,
        workExperienceDto.City,
        workExperienceDto.Description,
        workExperienceDto.HiringDate,
        workExperienceDto.OrganizationName,
        workExperienceDto.DismissalDate
    );

    // Act
    await writeRepository.UpdateAsync(dbPerson);

    // Assert
    dbPerson.Should().BeEquivalentTo(person);
}

仓储代码

public class PersonReadRepository
{
    private readonly DbSet<Person> _persons;

    public PersonReadRepository(PersonnelContext personnelContext)
    {
        _persons = personnelContext.Set<Person>();
    }

    public async Task<Person?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
    {
        return await _persons.FindAsync(id, cancellationToken);
    }
}

public class PersonWriteRepository
{
    private readonly DbSet<Person> _persons;
    private readonly PersonnelContext _personnelContext;

    public PersonWriteRepository(PersonnelContext personnelContext, IPersonReadRepository personReadRepository)
    {
        _personnelContext = personnelContext;
        PersonReadRepository = personReadRepository;
        _persons = personnelContext.Set<Person>();
    }

    public IPersonReadRepository PersonReadRepository { get; }

    public async Task AddAsync(Person person, CancellationToken cancellationToken = default)
    {
        await _persons.AddAsync(person, cancellationToken);
        await _personnelContext.SaveChangesAsync(cancellationToken);
    }

    public async Task UpdateAsync(Person person, CancellationToken cancellationToken = default)
    {
        _persons.Update(person);
        await _personnelContext.SaveChangesAsync(cancellationToken);
    }
}

问题原因

这个错误的核心是EF Core 错误地尝试更新一个不存在于数据库中的关联实体(WorkExperience),触发逻辑如下:

  1. 通过AddAsync添加Person并执行SaveChanges,Person成功写入数据库。
  2. GetByIdAsync获取的dbPerson处于当前DbContext的跟踪状态(ReadRepository和WriteRepository共享同一个Context实例)。
  3. 调用dbPerson.AddWorkExperience添加了一个带有预定义Id的WorkExperience实体。
  4. 执行UpdateAsync时,_persons.Update(dbPerson)会遍历实体的所有导航属性:对于集合中的实体,若主键(Id)有非默认值,EF会将其标记为Modified状态,而非Added。
  5. 执行SaveChanges时,EF尝试更新这个尚未存在于数据库中的WorkExperience,因此抛出异常。

另外,测试中的断言dbPerson.Should().BeEquivalentTo(person)本身不合理:dbPerson已添加WorkExperience,而原始person没有,断言必然失败。

修复方案

方案1:移除不必要的Update调用(推荐)

因为dbPerson处于当前DbContext的跟踪状态,EF会自动检测到WorkExperience的添加操作,无需手动调用Update。修改PersonWriteRepository的UpdateAsync方法:

public async Task UpdateAsync(Person person, CancellationToken cancellationToken = default)
{
    // 移除_persons.Update(person);
    await _personnelContext.SaveChangesAsync(cancellationToken);
}

方案2:手动处理关联实体状态(适用于实体 detached 的场景)

如果后续场景中dbPerson可能来自其他DbContext(处于 detached 状态),需要手动标记关联实体的状态:

public async Task UpdateAsync(Person person, CancellationToken cancellationToken = default)
{
    _persons.Attach(person);
    // 标记Person本身为Modified
    _personnelContext.Entry(person).State = EntityState.Modified;
    // 遍历WorkExperiences,标记新增的实体为Added
    foreach (var workExperience in person.WorkExperiences)
    {
        if (_personnelContext.Entry(workExperience).State == EntityState.Detached)
        {
            _personnelContext.Entry(workExperience).State = EntityState.Added;
        }
    }
    await _personnelContext.SaveChangesAsync(cancellationToken);
}

方案3:调整测试断言

修正测试中的断言,验证WorkExperience是否成功添加:

// Assert
var updatedPerson = await readRepository.GetByIdAsync(person.Id);
updatedPerson.WorkExperiences.Should().ContainSingle();
updatedPerson.WorkExperiences.First().JobTitle.Should().Be(workExperienceDto.JobTitle);
// 其他字段的验证...

关于乐观并发控制

此场景不需要实现乐观并发控制,错误并非由并发冲突导致,而是实体状态跟踪的逻辑错误。乐观并发控制用于处理多个用户同时修改同一实体的情况,与当前问题无关。

内容的提问来源于stack exchange,提问作者LemonPotion

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 10:43:13