Efcore: Changes on Owned Entites Properties causes a concurrency conflict on same dbContext

Created on 2 Aug 2018  路  5Comments  路  Source: dotnet/efcore

When changing an Owned Entities Property and than save twice, the second SaveChanges call throws an DbUpdateConcurrencyException because the local entities rowversion was not updated.

Steps to reproduce

public class Name
{
    public string First { get; set; }
    public string Last { get; set; }
}
public class Author
{
    public int Id { get; set; }
    public Name Name { get; set; }
    public string Description { get; set; }
    public byte[] Rowversion { get; set; }
}
using (var context = new BookDbContext())
{
    var author = context.Authors.First();
    author.Name.First = "Lukas";
    context.SaveChanges();

    author.Description = "Some very important information";
    context.SaveChanges();    // -> DbUpdateConcurrencyException
}



md5-7b70687874e1e37ba07a43180512ad6a



Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.



md5-e37abd4ca4bd726f6cfccedbc643a5ca



   at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ThrowAggregateUpdateConcurrencyException(Int32 commandIndex, Int32 expectedRowsAffected, Int32 rowsAffected)
   at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ConsumeResultSetWithPropagation(Int32 commandIndex, RelationalDataReader reader)
   at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.Consume(RelationalDataReader reader)
   at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.Execute(IRelationalConnection connection)
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(DbContext _, ValueTuple`2 parameters)
   at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(IEnumerable`1 commandBatches, IRelationalConnection connection)
   at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChanges(IReadOnlyList`1 entries)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(IReadOnlyList`1 entriesToSave)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(Boolean acceptAllChangesOnSuccess)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChanges()
   at EfCoreOwnedEntitesConcurrencyProblemTest.Program.ConcurrencyProblem() in C:\Development\EfCoreOwnedEntitesConcurrencyProblemTest\EfCoreOwnedEntitesConcurrencyProblemTest\Program.cs:line 32
   at EfCoreOwnedEntitesConcurrencyProblemTest.Program.Main(String[] args) in C:\Development\EfCoreOwnedEntitesConcurrencyProblemTest\EfCoreOwnedEntitesConcurrencyProblemTest\Program.cs:line 20

Further technical details

EF Core version: 2.1.1
Database Provider: Microsoft.EntityFrameworkCore.SqlServer
Operating system: Windows 10 1803
IDE: Visual Studio 2017 15.7.5

closed-fixed customer-reported type-bug

Most helpful comment

My work around;

C# private void ConcurrencyFix(){ foreach (var entry in ChangeTracker.Entries().Where(e => e.State == EntityState.Unchanged && e.References.Any(r => r.TargetEntry != null && r.TargetEntry.State == EntityState.Modified && r.TargetEntry.Metadata.IsOwned() && e.Metadata.Relational().TableName == r.TargetEntry.Metadata.Relational().TableName))) entry.State = EntityState.Modified; }

Then override DbContext.SaveChanges[Async] and call this first.

All 5 comments

See also #13608 for non-rowversion example.

Worse than the thrown exception, the first SaveChanges will update the database without checking the concurrency token, silently overwriting any other concurrent update.

My work around;

C# private void ConcurrencyFix(){ foreach (var entry in ChangeTracker.Entries().Where(e => e.State == EntityState.Unchanged && e.References.Any(r => r.TargetEntry != null && r.TargetEntry.State == EntityState.Modified && r.TargetEntry.Metadata.IsOwned() && e.Metadata.Relational().TableName == r.TargetEntry.Metadata.Relational().TableName))) entry.State = EntityState.Modified; }

Then override DbContext.SaveChanges[Async] and call this first.

Following lakeman advice I created a workaround and it worked. Just checked also if the EntityState was Added too.

I wrote a post about it here

The "right" way to handle concurrency / audit column handling of client generated values is to add shadow properties to each owned type. The work around I posted above has some serious performance issues if a large number of rows are loaded, but unmodified.

Was this page helpful?
0 / 5 - 0 ratings