Environment
- Z.EntityFramework.Extensions.EFCore 9.105.4
- Microsoft.EntityFrameworkCore / SqlServer 9.0.6
- .NET 8 / 9
Summary
After calling BulkSaveChangesAsync to insert an owner entity thta has "OwnsMany(...).ToTable(...)" children with shadow composite keys, the owned entities are still present in the ChangeTracker.Entries() (state = Unchanged) but IStateManager.TryGetValue(key, values) returns null for them.
Because the EF identity map is no longer consistent, any subsequent attempt to track a different instance of the same owned entity (same shadow key values) throws:
InvalidOperationException: The instance of entity type 'OrderLine' cannot be tracked because another instance with the same key value for { 'OrderId', 'Index' } is already being tracked
This does not happen with standard SaveChangesAsync()
Minimal repro
public class Order
{
public Guid Id {get;set;}
public List<OrderLine> Lines {get;set;} = new();
}
public class OrderLine
{
public string Description {get; set;} = "";
// keyed by shadow properties - OrderID FK and Index position
}
modelBuilder.Entity<Order>(b =>
{
b.HasKey(o => o.Id);
b.OwnsMany<OrderLine>(o => o.Lines, x=> {
x.ToTable("OrderLine");
x.Property<Guid>("OrderId");
x.Property<int>("Index");
x.HasKey("OrderId", "Index");
x.WithOwner().HasForeignKey("OrderId")
});
});
await using var cts = bnew MyDbContext(options);
var orderId = Guid.NewGuid();
var order = new Order { Id = orderId };
order.Lines.AddRange(new[] {
new OrderLine { Description = "Line1" },
new OrderLine { Description = "Line2" }
});
int lineIdx = 0;
ctx.ChangeTracker.TrackGraph(order, node => {
if (node.Entry.Entity is Order) {
node.Entry.State = EntityState.Added;
}
else if (node.Entry.Entity is OrderLine) {
node.Entry.Property("OrderId").CurrentValue =orderId;
node.Entry.Propertyy("Index").CurrentValue = ++lineIdx;
node.Entry.State = EntityState.Added;
}
return true;;
});
await ctx.BulkSaveChangesAsync();
var tracked = ctx.ChangeTracker.Entries<OrderLine>().ToList();
Console.WriteLine($"Tracked entries: {tracked.Count}"); // prints 2 - tick!
var stateManager = ctx.GetService<IStateManager>();
var key = ctx.Model.FindEntityType(typeof(OrderLine))!.GetKeys().First();
var found = stateManager.TryGetEntry(key, new object[]{ orderId, 1 });
Console.WriteLine($"TryGetEntry result; {found}"); // prints NULL - bug
// consequence
var line1Updated = new OrderLine{ Description = "Line 1 Updated" };
ctx.ChangeTracker.TrackGraph(line1Updated, node => {
node.Entry.Property("OrderId").CurrentValue = orderId;
node.Entry.Property("Index").CurrentValue = 1;
node.Entry.State = EntityState.Modified // <-- throws
return true;
});
Expected behaviour
After BulkSaveChangesAsync, entries that were inserted should be in state Unchanged and findable through IStateManager.TryGetEntry - consisten with SaveChangesAsync()
Actual behaviour
ChangeTracker.Entries() still returns the inserted entries (so they are not detatched) but IStateManager.TryGetEntry returns null for each. The internal identiy map (IStateManager's key -> entry dict) is no longer consistent with the tracked entry list.
NB
Possibly related to #568?
Environment
Summary
After calling BulkSaveChangesAsync to insert an owner entity thta has "OwnsMany(...).ToTable(...)" children with shadow composite keys, the owned entities are still present in the ChangeTracker.Entries() (state = Unchanged) but IStateManager.TryGetValue(key, values) returns null for them.
Because the EF identity map is no longer consistent, any subsequent attempt to track a different instance of the same owned entity (same shadow key values) throws:
InvalidOperationException: The instance of entity type 'OrderLine' cannot be tracked because another instance with the same key value for { 'OrderId', 'Index' } is already being tracked
This does not happen with standard SaveChangesAsync()
Minimal repro
Expected behaviour
After BulkSaveChangesAsync, entries that were inserted should be in state Unchanged and findable through IStateManager.TryGetEntry - consisten with SaveChangesAsync()
Actual behaviour
ChangeTracker.Entries() still returns the inserted entries (so they are not detatched) but IStateManager.TryGetEntry returns null for each. The internal identiy map (IStateManager's key -> entry dict) is no longer consistent with the tracked entry list.
NB
Possibly related to #568?