Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delete optional orphans when foreign key value is set to null #25957

Merged
merged 1 commit into from
Sep 10, 2021
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
57 changes: 35 additions & 22 deletions src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1257,28 +1257,7 @@ private void SetProperty(
{
if (value == null)
{
if (EntityState != EntityState.Deleted
&& EntityState != EntityState.Detached)
{
_stateData.FlagProperty(propertyIndex, PropertyFlag.Null, isFlagged: true);

if (setModified)
{
SetPropertyModified(
asProperty, changeState: true, isModified: true,
isConceptualNull: true);
}

if (!isCascadeDelete
&& StateManager.DeleteOrphansTiming == CascadeTiming.Immediate)
{
HandleConceptualNulls(
StateManager.SensitiveLoggingEnabled,
force: false,
isCascadeDelete: false);
}
}

HandleNullForeignKey(asProperty, setModified, isCascadeDelete);
writeValue = false;
}
else
Expand Down Expand Up @@ -1377,6 +1356,40 @@ private void SetProperty(
}
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public void HandleNullForeignKey(
IProperty property,
bool setModified = false,
bool isCascadeDelete = false)
{
if (EntityState != EntityState.Deleted
&& EntityState != EntityState.Detached)
{
_stateData.FlagProperty(property.GetIndex(), PropertyFlag.Null, isFlagged: true);

if (setModified)
{
SetPropertyModified(
property, changeState: true, isModified: true,
isConceptualNull: true);
}

if (!isCascadeDelete
&& StateManager.DeleteOrphansTiming == CascadeTiming.Immediate)
{
HandleConceptualNulls(
StateManager.SensitiveLoggingEnabled,
force: false,
isCascadeDelete: false);
}
}
}

private static Func<object?, object?, bool> ValuesEqualFunc(IProperty property)
{
var comparer = property.GetValueComparer();
Expand Down
7 changes: 7 additions & 0 deletions src/EFCore/ChangeTracking/Internal/NavigationFixer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,13 @@ var targetDependentEntry
}
}

if (newValue == null
&& (foreignKey.DeleteBehavior == DeleteBehavior.Cascade
|| foreignKey.DeleteBehavior == DeleteBehavior.ClientCascade))
{
entry.HandleNullForeignKey(property);
}

stateManager.UpdateDependentMap(entry, foreignKey);
}

Expand Down
96 changes: 67 additions & 29 deletions test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1923,15 +1923,34 @@ public void Dependent_FKs_are_not_nulled_when_principal_is_detached(bool delayCa
}
}

[ConditionalTheory] // Issue #16546
[InlineData(false)]
[InlineData(true)]
public void Optional_relationship_with_cascade_still_cascades(bool delayCascade)
[ConditionalTheory] // Issues #16546 #25360
[InlineData(false, false, false, true, false)]
[InlineData(true, false, false, true, false)]
[InlineData(false, true, false, true, false)]
[InlineData(true, true, false, true, false)]
[InlineData(false, false, true, true, false)]
[InlineData(true, false, true, true, false)]
[InlineData(false, true, false, false, true)]
[InlineData(true, true, false, false, true)]
[InlineData(false, false, true, false, true)]
[InlineData(true, false, true, false, true)]
[InlineData(false, true, false, true, true)]
[InlineData(true, true, false, true, true)]
[InlineData(false, false, true, true, true)]
[InlineData(true, false, true, true, true)]
public void Optional_relationship_with_cascade_still_cascades(
bool delayCascade,
bool setProperty,
bool setCurrentValue,
bool useForeignKey,
bool useNavigation)
{
Kontainer detachedContainer;
var databaseName = "K" + delayCascade;
using (var context = new KontainerContext(databaseName))
using (var context = new KontainerContext())
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();

context.Add(
new Kontainer
{
Expand All @@ -1949,51 +1968,77 @@ public void Optional_relationship_with_cascade_still_cascades(bool delayCascade)
.Single();
}

using (var context = new KontainerContext(databaseName))
using (var context = new KontainerContext())
{
var attachedContainer = context.Set<Kontainer>()
.Include(container => container.Rooms)
.ThenInclude(room => room.Troduct)
.Single();

var attachedRoom = attachedContainer.Rooms.Single();
var attachedTroduct = attachedRoom.Troduct;

Assert.Equal(3, context.ChangeTracker.Entries().Count());
Assert.Equal(EntityState.Unchanged, context.Entry(attachedContainer).State);
Assert.Equal(EntityState.Unchanged, context.Entry(attachedContainer.Rooms.Single()).State);
Assert.Equal(EntityState.Unchanged, context.Entry(attachedContainer.Rooms.Single().Troduct).State);

var detachedRoom = detachedContainer.Rooms.Single();
detachedRoom.Troduct = null;
detachedRoom.TroductId = null;

var attachedRoom = attachedContainer.Rooms.Single();
Assert.Equal(EntityState.Unchanged, context.Entry(attachedRoom).State);
Assert.Equal(EntityState.Unchanged, context.Entry(attachedTroduct).State);

if (delayCascade)
{
context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.OnSaveChanges;
}

context.Entry(attachedRoom).CurrentValues.SetValues(detachedRoom);
if (setProperty)
{
if (useForeignKey)
{
attachedRoom.TroductId = null;
}

if (useNavigation)
{
attachedRoom.Troduct = null;
}
}
else if (setCurrentValue)
{
if (useForeignKey)
{
context.Entry(attachedRoom).Property(e => e.TroductId).CurrentValue = null;
}

if (useNavigation)
{
context.Entry(attachedRoom).Reference(e => e.Troduct).CurrentValue = null;
}
}
else
{
var detachedRoom = detachedContainer.Rooms.Single();
detachedRoom.TroductId = null;
context.Entry(attachedRoom).CurrentValues.SetValues(detachedRoom);
}

Assert.Equal(3, context.ChangeTracker.Entries().Count());
Assert.Equal(EntityState.Unchanged, context.Entry(attachedContainer).State);
Assert.Equal(EntityState.Unchanged, context.Entry(attachedContainer.Rooms.Single().Troduct).State);
Assert.Equal(EntityState.Unchanged, context.Entry(attachedTroduct).State);

if (delayCascade)
{
Assert.Equal(EntityState.Modified, context.Entry(attachedContainer.Rooms.Single()).State);
Assert.Equal(EntityState.Modified, context.Entry(attachedRoom).State);
}
else
{
// Deleted because FK with cascade has been set to null
Assert.Equal(EntityState.Deleted, context.Entry(attachedContainer.Rooms.Single()).State);
Assert.Equal(EntityState.Deleted, context.Entry(attachedRoom).State);
}

context.ChangeTracker.CascadeChanges();

Assert.Equal(3, context.ChangeTracker.Entries().Count());
Assert.Equal(EntityState.Unchanged, context.Entry(attachedContainer).State);
Assert.Equal(EntityState.Unchanged, context.Entry(attachedContainer.Rooms.Single().Troduct).State);
Assert.Equal(EntityState.Deleted, context.Entry(attachedContainer.Rooms.Single()).State);
Assert.Equal(EntityState.Unchanged, context.Entry(attachedTroduct).State);
Assert.Equal(EntityState.Deleted, context.Entry(attachedRoom).State);

context.SaveChanges();
}
Expand Down Expand Up @@ -2025,13 +2070,6 @@ private class Troduct

private class KontainerContext : DbContext
{
private readonly string _databaseName;

public KontainerContext(string databaseName)
{
_databaseName = databaseName;
}

protected internal override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<KontainerRoom>()
Expand All @@ -2045,7 +2083,7 @@ protected internal override void OnModelCreating(ModelBuilder modelBuilder)
protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseInternalServiceProvider(InMemoryFixture.DefaultServiceProvider)
.UseInMemoryDatabase(_databaseName);
.UseInMemoryDatabase(nameof(KontainerContext));
}

[ConditionalTheory]
Expand Down