Skip to content

RowVersion concurrency issue when replacing entities with inheritance and owned types #37588

@Schaeri

Description

@Schaeri

Bug description

The issue can best be demonstrated using a simple sample application. Explaining it purely in text is relatively difficult.

ConsoleApp.zip

The structure of the sample application is as follows:

  • There is a base class EntityBase which contains a RowVersion property used as a concurrency token.
  • EntityA and EntityB inherit from EntityBase and add their own additional properties.
  • EntityB additionally contains a complex type OwnedEntity, which is mapped as an owned type.
  • The DbContext configuration is relatively simple.

When the following steps are performed:

  • Load an existing EntityA
  • Delete that entity
  • Create and save a new EntityB using the same primary key

the following exception is thrown:
'Unhandled exception. Microsoft.EntityFrameworkCore.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. See https://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ThrowAggregateUpdateConcurrencyExceptionAsync(RelationalDataReader reader, Int32 commandIndex, Int32 expectedRowsAffected, Int32 rowsAffected, CancellationToken cancellationToken)'

The issue only occurs with this specific combination of:

  • inheritance
  • RowVersion configured as a concurrency token, and
  • the presence of an owned entity.

The problem can be worked around by either:

  • removing the OwnedEntity from EntityB (including its mapping), or
  • commenting out the b.Property(x => x.RowVersion) configuration in the DbContext.

Your code

public abstract class EntityBase(string id)
{
    public string Id { get; } = id;

    public long RowVersion { get; }
}


public record OwnedEntity(DateTime CreationDate);

public class EntityA(string id, bool someValue,  OwnedEntity? owned) : EntityBase(id)
{
    protected EntityA()
        : this(null!, false, null)
    {
    }

    public bool SomeValue { get; } = someValue;

    public OwnedEntity? Owned { get; private set; } = owned;
}

public class EntityB(
    string id,
    string name) : EntityBase(id)
{
    protected EntityB() : this(null!, null!)
    {
    }

    public string Name { get; } = name;
}

public class MyDbContext(DbContextOptions<MyDbContext> options) : DbContext(options)
{
    protected override void OnModelCreating(
        ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<EntityBase>(
            b =>
            {
                b.HasDiscriminator<string>("Type")
                    .HasValue<EntityA>(nameof(EntityA))
                    .HasValue<EntityB>(nameof(EntityB));

                b.HasKey(x => x.Id);
                b.Property(x => x.Id).HasMaxLength(10);

                b.Property(x => x.RowVersion)
                    .IsRequired()
                    .IsRowVersion()
                    .IsConcurrencyToken()
                    .HasConversion<byte[]>();
            });

        modelBuilder.Entity<EntityA>(
            b =>
            {
                b.Property(x => x.SomeValue);
                
                b.OwnsOne(
                    x => x.Owned,
                    x =>
                    {
                        x.Property(x => x.CreationDate);
                    });
            });

         modelBuilder.Entity<EntityB>(
            b =>
            {
                b.Property(x => x.Name).HasMaxLength(100);
            });
    }
}

const string id = "SOMEID";

await using var dbContext = MyContextFactory.Create();
var entityA = await dbContext.Set<EntityA>().Where(x => x.Id == id).SingleAsync();
   
dbContext.Remove(entityA);

var entityB = new EntityB(id, "Any");

await dbContext.AddAsync(entityB);

await dbContext.SaveChangesAsync();

Stack traces

Unhandled exception. Microsoft.EntityFrameworkCore.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. See https://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
   at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ThrowAggregateUpdateConcurrencyExceptionAsync(RelationalDataReader reader, Int32 commandIndex, Int32 expectedRowsAffected, Int32 rowsAffected, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ConsumeResultSetAsync(Int32 startCommandIndex, RelationalDataReader reader, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ConsumeAsync(RelationalDataReader reader, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.SqlServer.Update.Internal.SqlServerModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.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.SqlServer.Storage.Internal.SqlServerExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
   at Program.<Main>$(String[] args) in C:\Users\rs\Desktop\ConsoleApp\ConsoleApp\Program.cs:line 27
   at Program.<Main>$(String[] args) in C:\Users\rs\Desktop\ConsoleApp\ConsoleApp\Program.cs:line 27
   at Program.<Main>(String[] args)

Process finished with exit code -532,462,766.

Verbose output


EF Core version

10.0.2

Database provider

Microsoft.EntityFrameworkCore.SqlServer

Target framework

.Net 10

Operating system

Windows 11

IDE

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions