When an entity have owned collection, it can't to use Context.Entry and Attach.
when Attach + update the collection, error come out
Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: '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
when Entry + update the collection, ef only generate insert query, doesn't generate delete query,
for owned type is ok, only owned collection have the problem.
Model.cs
```C#
public class StreetAddress
{
public string Street { get; set; } = "";
public string City { get; set; } = "";
}
public class Distributor
{
public int Id { get; set; }
public string name { get; set; } = "";
public ICollection
public StreetAddress ShippingCenter { get; set; } = null!;
}
public class DistributorConfiguration : IEntityTypeConfiguration
{
public void Configure(EntityTypeBuilder
{
builder.OwnsOne(p => p.ShippingCenter);
builder.OwnsMany(p => p.ShippingCenters, a =>
{
a.WithOwner().HasForeignKey("OwnerId");
a.Property
a.HasKey("Id");
});
}
}
Index.cshtml.cs
```C#
public void OnGet(
[FromServices] ApplicationDbContext db
)
{
// var distributor = db.Distributors.AsTracking().First(); // work perfect.
var distributor = db.Distributors.AsNoTracking().First();
//db.Entry(distributor).State = EntityState.Unchanged; // SaveChanges doesn't generate sql delete query
db.Attach(distributor); // error when SaveChanges
distributor.ShippingCenters = new List<StreetAddress> {
new StreetAddress {
City = "22",
Street = "22"
}
};
distributor.ShippingCenter = new StreetAddress
{
City = "22",
Street = "22"
};
db.SaveChanges();
}
EF Core version: 3.1.1
Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: .NET Core 3.1.1
Operating system: Windows 10
IDE: Visual Studio 2019 16.4.4
I also ran across this the other week. (Same technical details, except EF Core v3.0.1)
To add some hopefully helpful detail, I believe the exact behavior shown is caused by the fact that when Update() attaches and tracks the new entity, it stores the provided values as the Original Values in the snapshot store and then marks all properties as Modified. The IsModified prop for the Owned Type Collection ignores this, because it actually compares the content of the new list against the content of the "original" list and finds no changes. This prevents it from being detected by the change tracking.
It works successfully when the owning entity is loaded AsTracking() because the Original Values are loaded and placed in the snapshot store, and are then available for comparison.
I'm not sure what the best general behavior would be for this situation. I can think of only two ways the framework can fulfill the expectation of automatically placing the entity and its owned collection in the supplied state:
Either one seems like it could lead to excessive work at unexpected times.
My current workload works well with loading the current values for comparison, so I have a version of Update() to do that for all collections of owned types, but it would be nice to have an official way to do it.
@keatkeat87 I am able to reproduce this and the analysis from @AngleOSaxon seems correct.
@AndriySvyryd Thoughts on this? The generated updates are shown below. We are incorrectly using a temporary value here. It's possible this is a duplicate--I haven't run on master yet.
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (1ms) [Parameters=[@p2='1', @p0='22' (Size = 4000), @p1='22' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Distributors] SET [ShippingCenter_City] = @p0, [ShippingCenter_Street] = @p1
WHERE [Id] = @p2;
SELECT @@ROWCOUNT;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (1ms) [Parameters=[@p0='-2147482646'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DELETE FROM [Distributors_ShippingCenters]
WHERE [Id] = @p0;
SELECT @@ROWCOUNT;
Every entity need to have the primary key values set to be able to be tracked. For owned reference we propagate the value from the owner, but for owned collection there's no way to get the correct value, so we end up generating and using a temporary value. Perhaps in this case we should throw early if the value generator generates temporary values.
@keatkeat87 A workaround is to set the StreetAddress.Id to the value in the store
distributor.ShippingCenters = new List<StreetAddress> {
new StreetAddress {
City = "22",
Street = "22"
}
};
distributor.ShippingCenter = new StreetAddress
{
City = "22",
Street = "22"
};
```C#
var principalEntry = context.Entry(distributor);
var i = 0;
foreach (var center in distributor.ShippingCenters)
{
var childEntry = principalEntry.Collection("ShippingCenters").FindEntry(center);
childEntry.Property("Id").CurrentValue = storeIds[i++];
childEntry.State = EntityState.Unchanged;
}
```
Notes: this should already throw a better exception in the 5.0 code. However, we may be able to make it better still. It should not mention or use temporary values, since these are a side effect rather than a root cause.
Most helpful comment
Every entity need to have the primary key values set to be able to be tracked. For owned reference we propagate the value from the owner, but for owned collection there's no way to get the correct value, so we end up generating and using a temporary value. Perhaps in this case we should throw early if the value generator generates temporary values.
@keatkeat87 A workaround is to set the
StreetAddress.Idto the value in the store```C#
var principalEntry = context.Entry(distributor);
var i = 0;
foreach (var center in distributor.ShippingCenters)
{
var childEntry = principalEntry.Collection("ShippingCenters").FindEntry(center);
childEntry.Property("Id").CurrentValue = storeIds[i++];
childEntry.State = EntityState.Unchanged;
}
```