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.
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
EF Core version: 2.1.1
Database Provider: Microsoft.EntityFrameworkCore.SqlServer
Operating system: Windows 10 1803
IDE: Visual Studio 2017 15.7.5
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.
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.