efcore icon indicating copy to clipboard operation
efcore copied to clipboard

Allow detection of state for entities with generated keys to be switched off

Open flostony opened this issue 1 year ago • 3 comments

I have a two models (parent, child) with GUID as PrimaryKey. In a one-to-many relation. By default, the option "ValueGeneratedOnAdd" is activated for PrimaryKey as described here: https://learn.microsoft.com/en-us/ef/core/modeling/generated-properties?tabs=data-annotations#primary-keys

Now I would like to add child-models which have already a primary key set by the client application. I add the child-models to an list of childs in the parent-model which already exists in database and is loaded before.

In that case the context tracks this child-entites as modified also described here (information card): https://learn.microsoft.com/en-us/ef/core/modeling/keys?tabs=data-annotations#key-types-and-values

Doesn't Work:

public async Task<IActionResult> GetDemo()
{
    var parent = await context.Parents.FindAsync(Guid.Parse("0b2e2dc0-fd67-11ee-8fec-84a93832722b"));
    // child has a primary key set
    var child = new Child("child1", Guid.NewGuid());
    
    parent.Children.Add(child);

    // EntityEntry (child) state is: modified
    await context.SaveChangesAsync();

    return Ok();
}

Works:

public async Task<IActionResult> GetDemo()
{
    var parent = await context.Parents.FindAsync(Guid.Parse("0b2e2dc0-fd67-11ee-8fec-84a93832722b"));
    // child has none primary key set   
    var child = new Child("child1");
    
    parent.Children.Add(child);

    // EntityEntry (child) state is: added
    await context.SaveChangesAsync();

    return Ok();
}

On SaveChanges I get the following 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.

 at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ThrowAggregateUpdateConcurrencyExceptionAsync(RelationalDataReader reader, Int32 commandIndex, Int32 expectedRowsAffected, Int32 rowsAffected, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ConsumeResultSetWithRowsAffectedOnlyAsync(Int32 commandIndex, 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.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.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
   at Pomelo.EntityFrameworkCore.MySql.Storage.Internal.MySqlExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)

If I follow the approche, described here: https://learn.microsoft.com/en-us/ef/core/modeling/generated-properties?tabs=data-annotations#overriding-value-generation

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Parent>().Property(x => x.PrimaryKey)
        .ValueGeneratedOnAddOrUpdate()
        .Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Save);
}

This cannot be configured for properties which are configured as primary key.

How can I configure to use provided primary or generate if null?

EF Core version: 8.0.2 Database provider: Pomelo.EntityFrameworkCore.MySql Target framework: NET 8.0

flostony avatar Apr 18 '24 09:04 flostony

When a property is marked as value-generated, as your key property is by convention, then EF will generate a key value unless a non-default key value has already been set. However, if you do set a key value explicitly, then you will need to also tell EF explicitly that the entity is new--otherwise EF treats it as existing because it has a key value. This can be done by tracking the entity by calling Add on the context or DbSet directly. For example:

var child = new Child("child1", Guid.NewGuid());
context.Add(child);
parent.Children.Add(child);

ajcvickers avatar Apr 21 '24 23:04 ajcvickers

We use our DbContext behind a repository pattern where the parent-entity is the aggregateroot and therefore we don't have public access to DbSet<Child>.

We would like to achieve an approach where we can use client-side and server-side generated keys. In this case it would be helpful if we could disable this behavior regarding "with key" is modified and "without key" an entity is added.

flostony avatar Apr 23 '24 13:04 flostony

@flostony There currently isn't any way to disable this behavior. We will consider it.

ajcvickers avatar Apr 24 '24 18:04 ajcvickers