From b5fbba404c54eb663328f0f9de0d1d97eaaf8c2c Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Tue, 31 Jan 2023 19:58:53 +0000 Subject: [PATCH] Avoid throwing relationship severed exceptions until SaveChanges is finished Fixes #30122 Because the entities for which the relationship is being severed may end up being deleted later on in SaveChanges. --- .../ChangeTracking/Internal/ChangeDetector.cs | 50 ++-- .../ChangeTracking/Internal/IStateManager.cs | 16 ++ .../Internal/InternalEntityEntry.cs | 29 ++- .../ChangeTracking/Internal/StateManager.cs | 29 ++- .../GraphUpdates/GraphUpdatesTestBase.cs | 244 ++++++++++++++---- .../GraphUpdatesTestBaseMiscellaneous.cs | 29 +++ .../GraphUpdatesTestBaseOneToMany.cs | 136 ++++------ .../GraphUpdatesSqlServerOwnedTest.cs | 4 + .../TestUtilities/FakeStateManager.cs | 5 + 9 files changed, 377 insertions(+), 165 deletions(-) diff --git a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs index 349db5a3e7d..20c34cf5e9a 100644 --- a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs +++ b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs @@ -117,28 +117,42 @@ public virtual void DetectChanges(IStateManager stateManager) _logger.DetectChangesStarting(stateManager.Context); - foreach (var entry in stateManager.ToList()) // Might be too big, but usually _all_ entities are using Snapshot tracking + try { - switch (entry.EntityState) - { - case EntityState.Detached: - break; - case EntityState.Deleted: - if (entry.SharedIdentityEntry != null) - { - continue; - } - - goto default; - default: - if (LocalDetectChanges(entry)) - { - changesFound = true; - } + stateManager.PostponeConceptualNullExceptions = true; - break; + foreach (var entry in stateManager.ToList()) // Might be too big, but usually _all_ entities are using Snapshot tracking + { + switch (entry.EntityState) + { + case EntityState.Detached: + break; + case EntityState.Deleted: + if (entry.SharedIdentityEntry != null) + { + continue; + } + + goto default; + default: + if (LocalDetectChanges(entry)) + { + changesFound = true; + } + + break; + } } } + finally + { + stateManager.PostponeConceptualNullExceptions = false; + } + + if (stateManager.DeleteOrphansTiming == CascadeTiming.Immediate) + { + stateManager.HandleConceptualNulls(false); + } _logger.DetectChangesCompleted(stateManager.Context); diff --git a/src/EFCore/ChangeTracking/Internal/IStateManager.cs b/src/EFCore/ChangeTracking/Internal/IStateManager.cs index 04c4b39b9de..1876665c85e 100644 --- a/src/EFCore/ChangeTracking/Internal/IStateManager.cs +++ b/src/EFCore/ChangeTracking/Internal/IStateManager.cs @@ -562,6 +562,22 @@ void SetEvents( /// void CascadeChanges(bool force); + /// + /// 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. + /// + void HandleConceptualNulls(bool force); + + /// + /// 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. + /// + bool PostponeConceptualNullExceptions { get; set; } + /// /// 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 diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs index 5b6bce5375d..164be5c2c34 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs @@ -1708,26 +1708,27 @@ public void HandleConceptualNulls(bool sensitiveLoggingEnabled, bool force, bool SetEntityState(cascadeState); } - else if (fks.Count > 0) + else if (!StateManager.PostponeConceptualNullExceptions) { - var foreignKey = fks.First(); - - if (sensitiveLoggingEnabled) + if (fks.Count > 0) { + var foreignKey = fks.First(); + + if (sensitiveLoggingEnabled) + { + throw new InvalidOperationException( + CoreStrings.RelationshipConceptualNullSensitive( + foreignKey.PrincipalEntityType.DisplayName(), + EntityType.DisplayName(), + this.BuildOriginalValuesString(foreignKey.Properties))); + } + throw new InvalidOperationException( - CoreStrings.RelationshipConceptualNullSensitive( + CoreStrings.RelationshipConceptualNull( foreignKey.PrincipalEntityType.DisplayName(), - EntityType.DisplayName(), - this.BuildOriginalValuesString(foreignKey.Properties))); + EntityType.DisplayName())); } - throw new InvalidOperationException( - CoreStrings.RelationshipConceptualNull( - foreignKey.PrincipalEntityType.DisplayName(), - EntityType.DisplayName())); - } - else - { var property = EntityType.GetProperties().FirstOrDefault( p => (EntityState != EntityState.Modified || IsModified(p)) diff --git a/src/EFCore/ChangeTracking/Internal/StateManager.cs b/src/EFCore/ChangeTracking/Internal/StateManager.cs index 834fb6f4fc3..9140185ae83 100644 --- a/src/EFCore/ChangeTracking/Internal/StateManager.cs +++ b/src/EFCore/ChangeTracking/Internal/StateManager.cs @@ -1162,6 +1162,22 @@ public virtual void CascadeChanges(bool force) { // Perf sensitive + HandleConceptualNulls(force); + + foreach (var entry in this.ToListForState(deleted: true)) + { + CascadeDelete(entry, force); + } + } + + /// + /// 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 virtual void HandleConceptualNulls(bool force) + { var toHandle = new List(); foreach (var entry in GetEntriesForState(modified: true, added: true)) @@ -1176,13 +1192,16 @@ public virtual void CascadeChanges(bool force) { entry.HandleConceptualNulls(SensitiveLoggingEnabled, force, isCascadeDelete: false); } - - foreach (var entry in this.ToListForState(deleted: true)) - { - CascadeDelete(entry, force); - } } + /// + /// 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 virtual bool PostponeConceptualNullExceptions { get; set; } + /// /// 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 diff --git a/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBase.cs b/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBase.cs index 27ab87f1a66..669ae73ba0f 100644 --- a/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBase.cs +++ b/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBase.cs @@ -1,10 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.ObjectModel; using System.ComponentModel; using System.ComponentModel.DataAnnotations.Schema; using System.Runtime.CompilerServices; -using Microsoft.EntityFrameworkCore.Internal; // ReSharper disable ParameterOnlyUsedForPreconditionCheck.Local // ReSharper disable ArrangeAccessorOwnerBody @@ -481,62 +481,65 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con modelBuilder.Entity(); modelBuilder.Entity().HasData( - new SomethingCategory - { - Id = 1, - Name = "A" - }, - new SomethingCategory - { - Id = 2, - Name = "B" - }, - new SomethingCategory - { - Id = 3, - Name = "C" - }); + new SomethingCategory { Id = 1, Name = "A" }, + new SomethingCategory { Id = 2, Name = "B" }, + new SomethingCategory { Id = 3, Name = "C" }); modelBuilder.Entity().HasOne(s => s.SomethingCategory) .WithMany() .HasForeignKey(s => s.CategoryId) .OnDelete(DeleteBehavior.ClientSetNull); - modelBuilder.Entity(builder => - { - builder.Property("CategoryId").IsRequired(); + modelBuilder.Entity( + builder => + { + builder.Property("CategoryId").IsRequired(); - builder.HasKey(nameof(SomethingOfCategoryA.SomethingId), "CategoryId"); + builder.HasKey(nameof(SomethingOfCategoryA.SomethingId), "CategoryId"); - builder.HasOne(d => d.Something) - .WithOne(p => p.SomethingOfCategoryA) - .HasPrincipalKey(p => new {p.Id, p.CategoryId}) - .HasForeignKey(nameof(SomethingOfCategoryA.SomethingId), "CategoryId") - .OnDelete(DeleteBehavior.ClientSetNull); + builder.HasOne(d => d.Something) + .WithOne(p => p.SomethingOfCategoryA) + .HasPrincipalKey(p => new { p.Id, p.CategoryId }) + .HasForeignKey(nameof(SomethingOfCategoryA.SomethingId), "CategoryId") + .OnDelete(DeleteBehavior.ClientSetNull); - builder.HasOne() - .WithMany() - .HasForeignKey("CategoryId") - .OnDelete(DeleteBehavior.ClientSetNull); - }); + builder.HasOne() + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.ClientSetNull); + }); - modelBuilder.Entity(builder => - { - builder.Property(e => e.CategoryId).IsRequired(); + modelBuilder.Entity( + builder => + { + builder.Property(e => e.CategoryId).IsRequired(); - builder.HasKey(e => new {e.SomethingId, e.CategoryId}); + builder.HasKey(e => new { e.SomethingId, e.CategoryId }); - builder.HasOne(d => d.Something) - .WithOne(p => p.SomethingOfCategoryB) - .HasPrincipalKey(p => new {p.Id, p.CategoryId}) - .HasForeignKey(socb => new {socb.SomethingId, socb.CategoryId}) - .OnDelete(DeleteBehavior.ClientSetNull); + builder.HasOne(d => d.Something) + .WithOne(p => p.SomethingOfCategoryB) + .HasPrincipalKey(p => new { p.Id, p.CategoryId }) + .HasForeignKey(socb => new { socb.SomethingId, socb.CategoryId }) + .OnDelete(DeleteBehavior.ClientSetNull); - builder.HasOne(e => e.SomethingCategory) - .WithMany() - .HasForeignKey(e => e.CategoryId) - .OnDelete(DeleteBehavior.ClientSetNull); - }); + builder.HasOne(e => e.SomethingCategory) + .WithMany() + .HasForeignKey(e => e.CategoryId) + .OnDelete(DeleteBehavior.ClientSetNull); + }); + + modelBuilder.Entity().HasMany(e => e.TurnipSwedes).WithOne(e => e.Swede).OnDelete(DeleteBehavior.Restrict); + modelBuilder.Entity().HasData(new Parsnip { Id = 1 }); + modelBuilder.Entity().HasData(new Carrot { Id = 1, ParsnipId = 1 }); + modelBuilder.Entity().HasData(new Turnip { Id = 1, CarrotsId = 1 }); + modelBuilder.Entity().HasData(new Swede { Id = 1, ParsnipId = 1 }); + modelBuilder.Entity().HasData( + new TurnipSwede + { + Id = 1, + SwedesId = 1, + TurnipId = 1 + }); } protected virtual object CreateFullGraph() @@ -3686,6 +3689,159 @@ public virtual Something Something } } + protected class Parsnip : NotifyingEntity + { + private int _id; + private Carrot _carrot; + private Swede _swede; + + public int Id + { + get => _id; + set => SetWithNotify(value, ref _id); + } + + public Carrot Carrot + { + get => _carrot; + set => SetWithNotify(value, ref _carrot); + } + + public Swede Swede + { + get => _swede; + set => SetWithNotify(value, ref _swede); + } + } + + protected class Carrot : NotifyingEntity + { + private int _id; + private int _parsnipId; + private Parsnip _parsnip; + private ICollection _turnips = new ObservableHashSet(); + + public int Id + { + get => _id; + set => SetWithNotify(value, ref _id); + } + + public int ParsnipId + { + get => _parsnipId; + set => SetWithNotify(value, ref _parsnipId); + } + + public Parsnip Parsnip + { + get => _parsnip; + set => SetWithNotify(value, ref _parsnip); + } + + public ICollection Turnips + { + get => _turnips; + set => SetWithNotify(value, ref _turnips); + } + } + + protected class Turnip : NotifyingEntity + { + private int _id; + private int _carrotsId; + private Carrot _carrot; + + public int Id + { + get => _id; + set => SetWithNotify(value, ref _id); + } + + public int CarrotsId + { + get => _carrotsId; + set => SetWithNotify(value, ref _carrotsId); + } + + public Carrot Carrot + { + get => _carrot; + set => SetWithNotify(value, ref _carrot); + } + } + + protected class Swede : NotifyingEntity + { + private int _id; + private int _parsnipId; + private Parsnip _parsnip; + private ICollection _turnipSwede = new ObservableHashSet(); + + public int Id + { + get => _id; + set => SetWithNotify(value, ref _id); + } + + public int ParsnipId + { + get => _parsnipId; + set => SetWithNotify(value, ref _parsnipId); + } + + public Parsnip Parsnip + { + get => _parsnip; + set => SetWithNotify(value, ref _parsnip); + } + + public ICollection TurnipSwedes + { + get => _turnipSwede; + set => SetWithNotify(value, ref _turnipSwede); + } + } + + protected class TurnipSwede : NotifyingEntity + { + private int _id; + private int _swedesId; + private Swede _swede; + private int _turnipId; + private Turnip _turnip; + + public int Id + { + get => _id; + set => SetWithNotify(value, ref _id); + } + + public int SwedesId + { + get => _swedesId; + set => SetWithNotify(value, ref _swedesId); + } + + public Swede Swede + { + get => _swede; + set => SetWithNotify(value, ref _swede); + } + + public int TurnipId + { + get => _turnipId; + set => SetWithNotify(value, ref _turnipId); + } + + public Turnip Turnip + { + get => _turnip; + set => SetWithNotify(value, ref _turnip); + } + } + protected class NotifyingEntity : INotifyPropertyChanging, INotifyPropertyChanged { protected void SetWithNotify(T value, ref T field, [CallerMemberName] string propertyName = "") diff --git a/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseMiscellaneous.cs b/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseMiscellaneous.cs index 621b7bde6d0..83033796961 100644 --- a/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseMiscellaneous.cs +++ b/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseMiscellaneous.cs @@ -1739,4 +1739,33 @@ public void Can_attach_full_required_composite_graph_of_duplicates() Assert.Equal(0, context.SaveChanges()); }); + + [ConditionalTheory] // Issue #30122 + [InlineData(false)] + [InlineData(true)] + public virtual Task Sever_relationship_that_will_later_be_deleted(bool async) + => ExecuteWithStrategyInTransactionAsync( + async context => + { + var swedes = context.Set() + .Include(x => x.Carrot) + .ThenInclude(x => x.Turnips) + .Include(x => x.Swede) + .ThenInclude(x => x.TurnipSwedes) + .Single(x => x.Id == 1); + + swedes.Carrot.Turnips.Clear(); + swedes.Swede.TurnipSwedes.Clear(); + + _ = async + ? await context.SaveChangesAsync() + : context.SaveChanges(); + + var entries = context.ChangeTracker.Entries(); + Assert.Equal(3, entries.Count()); + Assert.All(entries, e => Assert.Equal(EntityState.Unchanged, e.State)); + Assert.Contains(entries, e => e.Entity.GetType() == typeof(Carrot)); + Assert.Contains(entries, e => e.Entity.GetType() == typeof(Parsnip)); + Assert.Contains(entries, e => e.Entity.GetType() == typeof(Swede)); + }); } diff --git a/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseOneToMany.cs b/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseOneToMany.cs index 5e03a7f96ec..95c882725e0 100644 --- a/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseOneToMany.cs +++ b/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseOneToMany.cs @@ -865,58 +865,42 @@ public virtual void Reparent_dependent_one_to_many( child.ParentId = newParent.Id; } - if (!Fixture.ForceClientNoAction - || deleteOrphansTiming != CascadeTiming.Immediate - || (changeMechanism & ChangeMechanism.Fk) != 0 - || changeMechanism == ChangeMechanism.Dependent) - { - Assert.True(context.ChangeTracker.HasChanges()); + Assert.True(context.ChangeTracker.HasChanges()); - Assert.DoesNotContain(child, oldParent.Children); - Assert.Contains(child, newParent.Children); - Assert.Equal(newParent.Id, child.ParentId); - Assert.Equal(EntityState.Modified, context.Entry(child).State); - Assert.Equal(EntityState.Unchanged, context.Entry(oldParent).State); - Assert.Equal(EntityState.Unchanged, context.Entry(newParent).State); + Assert.DoesNotContain(child, oldParent.Children); + Assert.Contains(child, newParent.Children); + Assert.Equal(newParent.Id, child.ParentId); + Assert.Equal(EntityState.Modified, context.Entry(child).State); + Assert.Equal(EntityState.Unchanged, context.Entry(oldParent).State); + Assert.Equal(EntityState.Unchanged, context.Entry(newParent).State); - context.SaveChanges(); + context.SaveChanges(); - Assert.False(context.ChangeTracker.HasChanges()); + Assert.False(context.ChangeTracker.HasChanges()); - Assert.DoesNotContain(child, oldParent.Children); - Assert.Contains(child, newParent.Children); - Assert.Equal(newParent.Id, child.ParentId); - Assert.Equal(EntityState.Unchanged, context.Entry(child).State); - Assert.Equal(EntityState.Unchanged, context.Entry(oldParent).State); - Assert.Equal(EntityState.Unchanged, context.Entry(newParent).State); - } - else - { - Assert.Throws(() => context.ChangeTracker.DetectChanges()); - } + Assert.DoesNotContain(child, oldParent.Children); + Assert.Contains(child, newParent.Children); + Assert.Equal(newParent.Id, child.ParentId); + Assert.Equal(EntityState.Unchanged, context.Entry(child).State); + Assert.Equal(EntityState.Unchanged, context.Entry(oldParent).State); + Assert.Equal(EntityState.Unchanged, context.Entry(newParent).State); }, context => { - if (!Fixture.ForceClientNoAction - || deleteOrphansTiming != CascadeTiming.Immediate - || (changeMechanism & ChangeMechanism.Fk) != 0 - || changeMechanism == ChangeMechanism.Dependent) - { - var root = LoadRequiredGraph(context); + var root = LoadRequiredGraph(context); - Assert.False(context.ChangeTracker.HasChanges()); + Assert.False(context.ChangeTracker.HasChanges()); - oldParent = root.RequiredChildren.First(e => e.Id == oldParent.Id); - newParent = root.RequiredChildren.First(e => e.Id == newParent.Id); - child = newParent.Children.First(e => e.Id == child.Id); + oldParent = root.RequiredChildren.First(e => e.Id == oldParent.Id); + newParent = root.RequiredChildren.First(e => e.Id == newParent.Id); + child = newParent.Children.First(e => e.Id == child.Id); - Assert.DoesNotContain(child, oldParent.Children); - Assert.Contains(child, newParent.Children); - Assert.Equal(newParent.Id, child.ParentId); - Assert.Equal(EntityState.Unchanged, context.Entry(child).State); - Assert.Equal(EntityState.Unchanged, context.Entry(oldParent).State); - Assert.Equal(EntityState.Unchanged, context.Entry(newParent).State); - } + Assert.DoesNotContain(child, oldParent.Children); + Assert.Contains(child, newParent.Children); + Assert.Equal(newParent.Id, child.ParentId); + Assert.Equal(EntityState.Unchanged, context.Entry(child).State); + Assert.Equal(EntityState.Unchanged, context.Entry(oldParent).State); + Assert.Equal(EntityState.Unchanged, context.Entry(newParent).State); }); } @@ -984,58 +968,42 @@ public virtual void Reparent_dependent_one_to_many_ak( child.ParentId = newParent.AlternateId; } - if (!Fixture.ForceClientNoAction - || deleteOrphansTiming != CascadeTiming.Immediate - || (changeMechanism & ChangeMechanism.Fk) != 0 - || changeMechanism == ChangeMechanism.Dependent) - { - Assert.True(context.ChangeTracker.HasChanges()); + Assert.True(context.ChangeTracker.HasChanges()); - Assert.DoesNotContain(child, oldParent.Children); - Assert.Contains(child, newParent.Children); - Assert.Equal(newParent.AlternateId, child.ParentId); - Assert.Equal(EntityState.Modified, context.Entry(child).State); - Assert.Equal(EntityState.Unchanged, context.Entry(oldParent).State); - Assert.Equal(EntityState.Unchanged, context.Entry(newParent).State); + Assert.DoesNotContain(child, oldParent.Children); + Assert.Contains(child, newParent.Children); + Assert.Equal(newParent.AlternateId, child.ParentId); + Assert.Equal(EntityState.Modified, context.Entry(child).State); + Assert.Equal(EntityState.Unchanged, context.Entry(oldParent).State); + Assert.Equal(EntityState.Unchanged, context.Entry(newParent).State); - context.SaveChanges(); + context.SaveChanges(); - Assert.False(context.ChangeTracker.HasChanges()); + Assert.False(context.ChangeTracker.HasChanges()); - Assert.DoesNotContain(child, oldParent.Children); - Assert.Contains(child, newParent.Children); - Assert.Equal(newParent.AlternateId, child.ParentId); - Assert.Equal(EntityState.Unchanged, context.Entry(child).State); - Assert.Equal(EntityState.Unchanged, context.Entry(oldParent).State); - Assert.Equal(EntityState.Unchanged, context.Entry(newParent).State); - } - else - { - Assert.Throws(() => context.ChangeTracker.DetectChanges()); - } + Assert.DoesNotContain(child, oldParent.Children); + Assert.Contains(child, newParent.Children); + Assert.Equal(newParent.AlternateId, child.ParentId); + Assert.Equal(EntityState.Unchanged, context.Entry(child).State); + Assert.Equal(EntityState.Unchanged, context.Entry(oldParent).State); + Assert.Equal(EntityState.Unchanged, context.Entry(newParent).State); }, context => { - if (!Fixture.ForceClientNoAction - || deleteOrphansTiming != CascadeTiming.Immediate - || (changeMechanism & ChangeMechanism.Fk) != 0 - || changeMechanism == ChangeMechanism.Dependent) - { - var root = LoadRequiredAkGraph(context); + var root = LoadRequiredAkGraph(context); - Assert.False(context.ChangeTracker.HasChanges()); + Assert.False(context.ChangeTracker.HasChanges()); - oldParent = root.RequiredChildrenAk.First(e => e.Id == oldParent.Id); - newParent = root.RequiredChildrenAk.First(e => e.Id == newParent.Id); - child = newParent.Children.First(e => e.Id == child.Id); + oldParent = root.RequiredChildrenAk.First(e => e.Id == oldParent.Id); + newParent = root.RequiredChildrenAk.First(e => e.Id == newParent.Id); + child = newParent.Children.First(e => e.Id == child.Id); - Assert.DoesNotContain(child, oldParent.Children); - Assert.Contains(child, newParent.Children); - Assert.Equal(newParent.AlternateId, child.ParentId); - Assert.Equal(EntityState.Unchanged, context.Entry(child).State); - Assert.Equal(EntityState.Unchanged, context.Entry(oldParent).State); - Assert.Equal(EntityState.Unchanged, context.Entry(newParent).State); - } + Assert.DoesNotContain(child, oldParent.Children); + Assert.Contains(child, newParent.Children); + Assert.Equal(newParent.AlternateId, child.ParentId); + Assert.Equal(EntityState.Unchanged, context.Entry(child).State); + Assert.Equal(EntityState.Unchanged, context.Entry(oldParent).State); + Assert.Equal(EntityState.Unchanged, context.Entry(newParent).State); }); } diff --git a/test/EFCore.SqlServer.FunctionalTests/GraphUpdates/GraphUpdatesSqlServerOwnedTest.cs b/test/EFCore.SqlServer.FunctionalTests/GraphUpdates/GraphUpdatesSqlServerOwnedTest.cs index e6df4ea04e3..7f782bbbb23 100644 --- a/test/EFCore.SqlServer.FunctionalTests/GraphUpdates/GraphUpdatesSqlServerOwnedTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/GraphUpdates/GraphUpdatesSqlServerOwnedTest.cs @@ -10,6 +10,10 @@ public GraphUpdatesSqlServerOwnedTest(SqlServerFixture fixture) { } + // No owned types + public override Task Sever_relationship_that_will_later_be_deleted(bool async) + => Task.CompletedTask; + // Owned dependents are always loaded public override void Required_one_to_one_are_cascade_deleted_in_store( CascadeTiming? cascadeDeleteTiming, diff --git a/test/EFCore.Tests/TestUtilities/FakeStateManager.cs b/test/EFCore.Tests/TestUtilities/FakeStateManager.cs index a3319ee9c8a..05f3ec78977 100644 --- a/test/EFCore.Tests/TestUtilities/FakeStateManager.cs +++ b/test/EFCore.Tests/TestUtilities/FakeStateManager.cs @@ -247,6 +247,11 @@ public void OnStateChanged(InternalEntityEntry internalEntityEntry, EntityState public void CascadeChanges(bool force) => throw new NotImplementedException(); + public void HandleConceptualNulls(bool force) + => throw new NotImplementedException(); + + public bool PostponeConceptualNullExceptions { get; set; } + public void CascadeDelete(InternalEntityEntry entry, bool force, IEnumerable foreignKeys = null) => throw new NotImplementedException();