Skip to content

Use local DetectChanges when cascade-deleting #22079

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

Merged
1 commit merged into from
Aug 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/EFCore/ChangeTracking/Internal/StateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,10 @@ public virtual void CascadeDelete(InternalEntityEntry entry, bool force, IEnumer
{
var doCascadeDelete = force || CascadeDeleteTiming != CascadeTiming.Never;
var principalIsDetached = entry.EntityState == EntityState.Detached;
var changeDetector = Context.ChangeTracker.AutoDetectChangesEnabled
&& (string)Context.Model[CoreAnnotationNames.SkipDetectChangesAnnotation] != "true"
? Context.GetDependencies().ChangeDetector
: null;

foreignKeys ??= entry.EntityType.GetReferencingForeignKeys();
foreach (var fk in foreignKeys)
Expand All @@ -992,6 +996,8 @@ public virtual void CascadeDelete(InternalEntityEntry entry, bool force, IEnumer
foreach (InternalEntityEntry dependent in (GetDependentsFromNavigation(entry, fk)
?? GetDependents(entry, fk)).ToList())
{
changeDetector?.DetectChanges(dependent);

if (dependent.EntityState != EntityState.Deleted
&& dependent.EntityState != EntityState.Detached
&& (dependent.EntityState == EntityState.Added
Expand Down
119 changes: 119 additions & 0 deletions test/EFCore.Tests/DbContextTrackingTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1696,5 +1696,124 @@ public void Can_set_set_to_Unchanged_with_inconsistent_FK_dependent_first_refere
Assert.Equal(EntityState.Unchanged, context.Entry(category).State);
Assert.Equal(EntityState.Unchanged, context.Entry(product).State);
}

[ConditionalTheory] // Issue #17828
[InlineData(CascadeTiming.Immediate)]
[InlineData(CascadeTiming.Never)]
[InlineData(CascadeTiming.OnSaveChanges)]
public void Can_re_parent_optional_without_DetectChanges(CascadeTiming cascadeTiming)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public void Can_re_parent_optional_without_DetectChanges(CascadeTiming cascadeTiming)
public void Can_reparent_optional_without_DetectChanges(CascadeTiming cascadeTiming)

{
using var context = new Parent77Context();

context.ChangeTracker.CascadeDeleteTiming = cascadeTiming;

var parent1 = new Parent77();
var parent2 = new Parent77();
var child = new Optional77();

child.Parent77 = parent1;
context.AddRange(parent1, parent2, child);
context.SaveChanges();

Assert.Equal(3, context.ChangeTracker.Entries().Count());
Assert.Equal(EntityState.Unchanged, context.Entry(parent1).State);
Assert.Equal(EntityState.Unchanged, context.Entry(parent2).State);
Assert.Equal(EntityState.Unchanged, context.Entry(child).State);

child.Parent77 = parent2;
context.Remove(parent1);

Assert.Equal(3, context.ChangeTracker.Entries().Count());
Assert.Equal(EntityState.Deleted, context.Entry(parent1).State);
Assert.Equal(EntityState.Unchanged, context.Entry(parent2).State);
Assert.Equal(EntityState.Modified, context.Entry(child).State);
Assert.Same(parent2, child.Parent77);

context.SaveChanges();

Assert.Equal(2, context.ChangeTracker.Entries().Count());
Assert.Equal(EntityState.Detached, context.Entry(parent1).State);
Assert.Equal(EntityState.Unchanged, context.Entry(parent2).State);
Assert.Equal(EntityState.Unchanged, context.Entry(child).State);
Assert.Same(parent2, child.Parent77);
}

[ConditionalTheory] // Issue #17828
[InlineData(CascadeTiming.Immediate)]
[InlineData(CascadeTiming.Never)]
[InlineData(CascadeTiming.OnSaveChanges)]
public void Can_re_parent_required_without_DetectChanges(CascadeTiming cascadeTiming)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public void Can_re_parent_required_without_DetectChanges(CascadeTiming cascadeTiming)
public void Can_reparent_required_without_DetectChanges(CascadeTiming cascadeTiming)

{
using var context = new Parent77Context();

context.ChangeTracker.CascadeDeleteTiming = cascadeTiming;

var parent1 = new Parent77();
var parent2 = new Parent77();
var child = new Required77();

child.Parent77 = parent1;
context.AddRange(parent1, parent2, child);
context.SaveChanges();

Assert.Equal(3, context.ChangeTracker.Entries().Count());
Assert.Equal(EntityState.Unchanged, context.Entry(parent1).State);
Assert.Equal(EntityState.Unchanged, context.Entry(parent2).State);
Assert.Equal(EntityState.Unchanged, context.Entry(child).State);

child.Parent77 = parent2;
context.Remove(parent1);

Assert.Equal(3, context.ChangeTracker.Entries().Count());
Assert.Equal(EntityState.Deleted, context.Entry(parent1).State);
Assert.Equal(EntityState.Unchanged, context.Entry(parent2).State);
Assert.Equal(EntityState.Modified, context.Entry(child).State);
Assert.Same(parent2, child.Parent77);

context.SaveChanges();

Assert.Equal(2, context.ChangeTracker.Entries().Count());
Assert.Equal(EntityState.Detached, context.Entry(parent1).State);
Assert.Equal(EntityState.Unchanged, context.Entry(parent2).State);
Assert.Equal(EntityState.Unchanged, context.Entry(child).State);
Assert.Same(parent2, child.Parent77);
}

private class Parent77Context : DbContext
{
protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseInMemoryDatabase(nameof(Parent77Context));

protected internal override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Parent77>(
b =>
{
b.HasMany<Optional77>().WithOne(e => e.Parent77);
b.HasMany<Required77>().WithOne(e => e.Parent77);
});
}
}

private class Parent77
{
public int Id { get; set; }
}

private class Optional77
{
public int Id { get; set; }

public int? Parent77Id { get; set; }
public Parent77 Parent77 { get; set; }
}

private class Required77
{
public int Id { get; set; }

public int Parent77Id { get; set; }
public Parent77 Parent77 { get; set; }
}
}
}