dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.77k stars 3.18k forks source link

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

Open flostony opened 6 months ago

flostony commented 6 months ago

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

ajcvickers commented 6 months ago

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);
flostony commented 6 months ago

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.

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.

ajcvickers commented 6 months ago

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