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),触发逻辑如下:
- 通过
AddAsync添加Person并执行SaveChanges,Person成功写入数据库。 GetByIdAsync获取的dbPerson处于当前DbContext的跟踪状态(ReadRepository和WriteRepository共享同一个Context实例)。- 调用
dbPerson.AddWorkExperience添加了一个带有预定义Id的WorkExperience实体。 - 执行
UpdateAsync时,_persons.Update(dbPerson)会遍历实体的所有导航属性:对于集合中的实体,若主键(Id)有非默认值,EF会将其标记为Modified状态,而非Added。 - 执行
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
相关产品推荐
相关产品推荐

