-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Cascade deletions ignore current state of entities resulting in unexpected data loss #17828
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Comments
Also reproduces on the nightly build: 3.0.0-rc2.19462.2 |
Triage: The behavior observed is currently by design. You can opt out of immediate cascade behavior. We think it would be good to re-think if immediate cascading is the best default, but we can't change it now, so marking it as consider for next release. |
I'm not sure I follow you. This is not a problem with immediate cascade behavior in general, or that it is the default. The problem is with immediate cascade deleting non-related entities. |
@bachratyg The immediate cascade delete only does a local DetectChanges, that is it doesn't take into account any changes on the dependents. As another workaround you can call local DetectChanges on every dependent. context.Entry(child).DetectChanges(); |
I don't see any difference if I declare the principal to dependent navigation and manipulate that: protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Parent>().HasMany(p => p.Children).WithOne().IsRequired();
}
class Parent
{
public long Id { get; set; }
public ICollection<Child> Children { get; set; } = new HashSet<Child>();
}
class Child
{
public long Id { get; set; }
}
parent1.Children.Remove(child);
parent2.Children.Add(child);
context.Remove(parent1);
// actual output is
// parent:1:deleted
// parent:2:unchanged
// child:3:deleted In fact in this case the child is actually marked for deletion without even removing the old parent: parent1.Children.Remove(child);
parent2.Children.Add(child);
// actual output is
// parent:1:unchanged
// parent:2:unchanged
// child:3:deleted Same happens if I also add the dependent to principal navigation to the model and set that too (i.e. This works, but doesn't feel like the pit of success: parent2.Children.Add(child);
context.ChangeTracker.DetectChanges(); // must be called *beore* remove and *after* add
parent1.Children.Remove(child);
// actual output is
// parent:1:unchanged
// parent:2:unchanged
// child:3:modified |
Verified that behavior is correct in 5.0 if child.Parent = parent2;
context.Remove(parent1); This is a case where DetectChanges is needed since |
@divega wrote in #17828 (comment)
Were there any thoughts given to the default behavior? I'm really curious about the reasoning. The docs and #10114 refers to two main reasons this breaking change was made:
@ajcvickers wrote in #17828 (comment)
Are you suggesting I should litter my code with DetectChanges (all in nontrivial places) because the default behavior was changed so that the code need not be littered with DetectChanges in a different use case? Even if the current default is sound I would still think doing a recursive local detect on entities just about to be cascade deleted (see workaround 3 in op) would be the correct thing to do. DbSet.Local also does a full DetectChanges when the getter is called (i.e. data binding kicks in). Why not go all the way then? The altered object graph should reflect the desired state yet immediate cascades change it even if cascade happens only during SaveChanges. Was any thought given to my last 2 examples in #17828 (comment)? Also when
Snippet 1:
Snippet 2
|
@bachratyg In general full The general problem here is that DetectChanges is slow, so we only call it automatically when doing so is likely to always be necessary. Add rarely really needs a full DetectChanges, so EF doesn't call it automatically. (See https://blog.oneunicorn.com/2012/03/10/secrets-of-detectchanges-part-1-what-does-detectchanges-do/ for further discussion of the general problem here.) That being said it is not easy as an application developer to understand when a call to DetectChanges can be safely skipped. We will discuss this as a team. |
Notes from team discussion:
|
See also the slightly different scenario in #19652 |
Regarding this breaking change: https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/breaking-changes#cascade-deletions-now-happen-immediately-by-default
When the parent entity is removed cascade deletes happen immediately by default but ignores changes already made on child entities. This causes unexpected data loss as entities which are not orphaned are also removed.
Steps to reproduce
Using the following model
and the following helper
Construct initial state
Retarget the child to a different parent, remove the old parent
The child gets marked for deletion even though it is no longer associated with the old parent.
Possible workarounds
This has the drawback of completely reverting the new behavior and all its benefits.
This is quite tedious and error prone to use at multiple call sites, somewhat better but still a nuisance for multiple contexts. Calling DetectChanges may also have some perf impact.
This is somewhat more complex but uses internal APIs therefore fragile.
Further technical details
EF Core version: 3.0.0-preview9.19423.6
Database provider: Microsoft.EntityFrameworkCore.SqlServer (not relevant)
Target framework: .NET Core 3.0
Operating system: Windows 10 Pro 1903 (18362.356)
IDE: Visual Studio 2019 16.3.0 Preview 3.0
Complete runnable code listing to reproduce the issue
ConsoleApp6.csproj
Program.cs
The text was updated successfully, but these errors were encountered: