I have an aggregate defined like this:
public class Product {
public int LocalId { get; private set; }
public Something Something { get; private set; }
public ICollection<Price> Prices { get; private set; }
}
public class Something {
public string Name { get; set; }
}
public class Price {
public int Type { get; set; }
public decimal Value { get; set; }
}
And a schema defined like this:
private void DefineProduct(ModelBuilder builder) =>
builder
.Entity<Product>(builder =>
{
builder.HasKey(p => p.LocalId);
builder
.OwnsOne(p => p.Something, smth =>
{
smth.ToTable("somethings");
})
.OwnsMany(p => p.Prices, pp =>
{
pp.ToTable("prices");
});
});
When a price change is requested, I do this (inside the product method not included here for brevity):
Prices.First(p => p.Type == type).Value = newValue;
And then I try to save the product like this:
public async Task UpdateProperties(Product product, IEnumerable<object> props)
{
_context.Attach(product);
_context.Update(product);
foreach (var prop in props)
{
_context.Update(prop);
}
try
{
await _context.SaveChangesAsync();
}
catch (Exception ex)
{
Console.WriteLine("Who the hell allowed such a bug to go into a production release?");
}
}
Now I should mention that the product comes in from an initial query whose results were not tracked (via AsNoTracking()
call), that's why I'm calling the Attach
method in the first line of the method body. The problem is that I'm hitting that catch
statement with an exception message saying:
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."}
The thing is that I'm not updating the same product anywhere else and that's the only place touching it. Also I use AsNoTracking
as the default. If I comment out the line with _context.Update(prop);
, then there's no exception raised, but the price is not being updated. Also, if I don't update that prices collection but the Something
property, everything goes well. What. The. Hell.
EF Core documentation for Collections of owned types explicitly states that you have to define the owned entity PK (as opposed to OwnsOne
where the shadow FK is normally used as PK).
Hence you need to either define its own PK (like Id
you did), or composite PK - for instance, if Price.Type
is unique inside the owner, then you can use something like
pp.HasKey("LocalId", "Type");
and avoid the additional Id
column.