diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs index 8b67ffc8015..29896d0e998 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs @@ -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 @@ -1377,6 +1356,40 @@ private void SetProperty( } } + /// + /// 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. + /// + 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 ValuesEqualFunc(IProperty property) { var comparer = property.GetValueComparer(); diff --git a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs index e2ea9c3f678..0efa9a2f671 100644 --- a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs +++ b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs @@ -495,6 +495,13 @@ var targetDependentEntry } } + if (newValue == null + && (foreignKey.DeleteBehavior == DeleteBehavior.Cascade + || foreignKey.DeleteBehavior == DeleteBehavior.ClientCascade)) + { + entry.HandleNullForeignKey(property); + } + stateManager.UpdateDependentMap(entry, foreignKey); } diff --git a/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs b/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs index 6542f9e97ef..0d4b7b87658 100644 --- a/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs +++ b/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs @@ -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 { @@ -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() .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(); } @@ -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() @@ -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]