Search code examples
c#.netentity-frameworkentity-framework-coreef-code-first

DbUpdateConcurrencyException in EF Core during SaveChangesAsync


I'm working with Entity Framework Core and encountering the following error:

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

This error occurs when I try to save changes to an entity with

await _personnelContext.SaveChangesAsync(cancellationToken) 

after attempting to update a Person entity with associated WorkExperience.

Here's the code:

[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);
}

Repositories:

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);
    }
}

What could be causing the DbUpdateConcurrencyException error in this case, and how can I fix it? Should I implement optimistic concurrency control, or is there something wrong with how I'm handling entity updates or state tracking?

The error DbUpdateConcurrencyException happens when I call

await _personnelContext.SaveChangesAsync(cancellationToken);

inside the UpdateAsync method of the PersonWriteRepository.

I add a Person to the database via AddAsync. Then I update the Person by adding some WorkExperience data to navigation property in the Person's ICollection<WorkExperience> WorkExperiences.

The update fails with the error mentioned above.

I checked whether the entity is in the correct state before updating, but still get the error.


Solution

  • The error Attempted to update or delete an entity that does not exist in the store is saying WorkExperience doesn't exist in the database since on update EF Core tries to find the primary key of the navigation but seems in your code you added a child (WorkExperience) to the Person with a arbitrary Id.

    So there are two options to fix:

    1- Inserting Person and with its children in one go to the database:

    // Arrange
    person.AddWorkExperience(
        workExperienceDto.Id,
        workExperienceDto.JobTitle,
        workExperienceDto.Street,
        workExperienceDto.Country,
        workExperienceDto.City,
        workExperienceDto.Description,
        workExperienceDto.HiringDate,
        workExperienceDto.OrganizationName,
        workExperienceDto.DismissalDate
    );
    
    // add Person and its children
    await writeRepository.AddAsync(person);
    
    var dbPerson = await readRepository.GetByIdAsync(person.Id);
    
    // rest of the code
    

    2- First add the WorkExperience and then use the primary key

    The point is you can't expect EF Core to insert the children using the Update method.