From 837cde8ba1e199e518f3d44e190ee713d3c71f8a Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Tue, 31 Jan 2023 19:58:53 +0000 Subject: [PATCH] Prevent re-entrance into DetectChanges Fixes #30122 Fixes #30135 EF7 contained some new calls to local `DetectChanges`. This resulted in re-entrance into `DetectChanges` for some types of graphs. This change prevents that. --- .../ChangeTracking/Internal/ChangeDetector.cs | 80 ++-- ...phUpdatesIdentityResolutionInMemoryTest.cs | 153 +++++++ .../GraphUpdates/GraphUpdatesInMemoryTest.cs | 177 +------- .../GraphUpdatesInMemoryTestBase.cs | 188 +++++++++ .../GraphUpdates/GraphUpdatesTestBase.cs | 389 +++++++++++++++--- .../GraphUpdatesTestBaseMiscellaneous.cs | 363 ++++++++++++---- .../GraphUpdatesSqlServerOwnedTest.cs | 28 ++ ...hUpdatesSqliteSnapshotNotificationsTest.cs | 2 +- .../GraphUpdatesSqliteTestBase.cs | 2 +- .../StoreValueGenerationLegacySqliteTest.cs | 3 + 10 files changed, 1053 insertions(+), 332 deletions(-) create mode 100644 test/EFCore.InMemory.FunctionalTests/GraphUpdates/GraphUpdatesIdentityResolutionInMemoryTest.cs create mode 100644 test/EFCore.InMemory.FunctionalTests/GraphUpdates/GraphUpdatesInMemoryTestBase.cs diff --git a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs index 349db5a3e7d..fd80af87197 100644 --- a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs +++ b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs @@ -17,6 +17,7 @@ public class ChangeDetector : IChangeDetector { private readonly IDiagnosticsLogger _logger; private readonly ILoggingOptions _loggingOptions; + private bool _inCascadeDelete; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -112,37 +113,51 @@ public virtual void PropertyChanging(InternalEntityEntry entry, IPropertyBase pr /// public virtual void DetectChanges(IStateManager stateManager) { - OnDetectingAllChanges(stateManager); - var changesFound = false; - - _logger.DetectChangesStarting(stateManager.Context); + if (_inCascadeDelete) + { + return; + } - 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; - } + _inCascadeDelete = true; - goto default; - default: - if (LocalDetectChanges(entry)) - { - changesFound = true; - } + OnDetectingAllChanges(stateManager); + var changesFound = false; + + _logger.DetectChangesStarting(stateManager.Context); - 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; + } } - } - _logger.DetectChangesCompleted(stateManager.Context); + _logger.DetectChangesCompleted(stateManager.Context); - OnDetectedAllChanges(stateManager, changesFound); + OnDetectedAllChanges(stateManager, changesFound); + } + finally + { + _inCascadeDelete = false; + } } /// @@ -152,7 +167,22 @@ public virtual void DetectChanges(IStateManager stateManager) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual void DetectChanges(InternalEntityEntry entry) - => DetectChanges(entry, new HashSet { entry }); + { + if (_inCascadeDelete) + { + return; + } + + try + { + _inCascadeDelete = true; + DetectChanges(entry, new HashSet { entry }); + } + finally + { + _inCascadeDelete = false; + } + } private bool DetectChanges(InternalEntityEntry entry, HashSet visited) { diff --git a/test/EFCore.InMemory.FunctionalTests/GraphUpdates/GraphUpdatesIdentityResolutionInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/GraphUpdates/GraphUpdatesIdentityResolutionInMemoryTest.cs new file mode 100644 index 00000000000..3c096d372f6 --- /dev/null +++ b/test/EFCore.InMemory.FunctionalTests/GraphUpdates/GraphUpdatesIdentityResolutionInMemoryTest.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore; + +public class GraphUpdatesIdentityResolutionInMemoryTest + : GraphUpdatesInMemoryTestBase +{ + public GraphUpdatesIdentityResolutionInMemoryTest(InMemoryIdentityResolutionFixture fixture) + : base(fixture) + { + } + + [ConditionalFact] + public void Can_attach_full_required_graph_of_duplicates() + => ExecuteWithStrategyInTransaction( + context => + { + var trackedRoot = LoadRequiredGraph(context); + var entries = context.ChangeTracker.Entries().ToList(); + + context.Attach(QueryRequiredGraph(context).AsNoTracking().Single(IsTheRoot)); + + AssertEntries(entries, context.ChangeTracker.Entries().ToList()); + AssertNavigations(trackedRoot); + + Assert.Equal(0, context.SaveChanges()); + }); + + [ConditionalFact] + public void Can_attach_full_optional_graph_of_duplicates() + => ExecuteWithStrategyInTransaction( + context => + { + var trackedRoot = LoadOptionalGraph(context); + var entries = context.ChangeTracker.Entries().ToList(); + + context.Attach(QueryOptionalGraph(context).AsNoTracking().Single(IsTheRoot)); + + AssertEntries(entries, context.ChangeTracker.Entries().ToList()); + AssertNavigations(trackedRoot); + + Assert.Equal(0, context.SaveChanges()); + }); + + [ConditionalFact] + public void Can_attach_full_required_non_PK_graph_of_duplicates() + => ExecuteWithStrategyInTransaction( + context => + { + var trackedRoot = LoadRequiredNonPkGraph(context); + var entries = context.ChangeTracker.Entries().ToList(); + + context.Attach(QueryRequiredNonPkGraph(context).AsNoTracking().Single(IsTheRoot)); + + AssertEntries(entries, context.ChangeTracker.Entries().ToList()); + AssertNavigations(trackedRoot); + + Assert.Equal(0, context.SaveChanges()); + }); + + [ConditionalFact] + public void Can_attach_full_required_AK_graph_of_duplicates() + => ExecuteWithStrategyInTransaction( + context => + { + var trackedRoot = LoadRequiredAkGraph(context); + var entries = context.ChangeTracker.Entries().ToList(); + + context.Attach(QueryRequiredAkGraph(context).AsNoTracking().Single(IsTheRoot)); + + AssertEntries(entries, context.ChangeTracker.Entries().ToList()); + AssertNavigations(trackedRoot); + + Assert.Equal(0, context.SaveChanges()); + }); + + [ConditionalFact] + public void Can_attach_full_optional_AK_graph_of_duplicates() + => ExecuteWithStrategyInTransaction( + context => + { + var trackedRoot = LoadOptionalAkGraph(context); + var entries = context.ChangeTracker.Entries().ToList(); + + context.Attach(QueryOptionalAkGraph(context).AsNoTracking().Single(IsTheRoot)); + + AssertEntries(entries, context.ChangeTracker.Entries().ToList()); + AssertNavigations(trackedRoot); + + Assert.Equal(0, context.SaveChanges()); + }); + + [ConditionalFact] + public void Can_attach_full_required_non_PK_AK_graph_of_duplicates() + => ExecuteWithStrategyInTransaction( + context => + { + var trackedRoot = LoadRequiredNonPkAkGraph(context); + var entries = context.ChangeTracker.Entries().ToList(); + + context.Attach(QueryRequiredNonPkAkGraph(context).AsNoTracking().Single(IsTheRoot)); + + AssertEntries(entries, context.ChangeTracker.Entries().ToList()); + AssertNavigations(trackedRoot); + + Assert.Equal(0, context.SaveChanges()); + }); + + [ConditionalFact] + public void Can_attach_full_required_one_to_many_graph_of_duplicates() + => ExecuteWithStrategyInTransaction( + context => + { + var trackedRoot = LoadOptionalOneToManyGraph(context); + var entries = context.ChangeTracker.Entries().ToList(); + + context.Attach(QueryOptionalOneToManyGraph(context).AsNoTracking().Single(IsTheRoot)); + + AssertEntries(entries, context.ChangeTracker.Entries().ToList()); + AssertNavigations(trackedRoot); + + Assert.Equal(0, context.SaveChanges()); + }); + + [ConditionalFact] + public void Can_attach_full_required_composite_graph_of_duplicates() + => ExecuteWithStrategyInTransaction( + context => + { + var trackedRoot = LoadRequiredCompositeGraph(context); + var entries = context.ChangeTracker.Entries().ToList(); + + context.Attach(QueryRequiredCompositeGraph(context).AsNoTracking().Single(IsTheRoot)); + + AssertEntries(entries, context.ChangeTracker.Entries().ToList()); + AssertNavigations(trackedRoot); + + Assert.Equal(0, context.SaveChanges()); + }); + + public class InMemoryIdentityResolutionFixture : GraphUpdatesInMemoryFixtureBase + { + protected override string StoreName + => "GraphUpdatesIdentityResolutionTest"; + + public override bool HasIdentityResolution + => true; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder).AddInterceptors(new UpdatingIdentityResolutionInterceptor()); + } +} diff --git a/test/EFCore.InMemory.FunctionalTests/GraphUpdates/GraphUpdatesInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/GraphUpdates/GraphUpdatesInMemoryTest.cs index b23e13f5865..57ad5484c02 100644 --- a/test/EFCore.InMemory.FunctionalTests/GraphUpdates/GraphUpdatesInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/GraphUpdates/GraphUpdatesInMemoryTest.cs @@ -3,188 +3,17 @@ namespace Microsoft.EntityFrameworkCore; -public class GraphUpdatesInMemoryTest : GraphUpdatesTestBase +public class GraphUpdatesInMemoryTest + : GraphUpdatesInMemoryTestBase { public GraphUpdatesInMemoryTest(InMemoryFixture fixture) : base(fixture) { } - // In-memory database does not have database default values - public override Task Can_insert_when_composite_FK_has_default_value_for_one_part(bool async) - => Task.CompletedTask; - - // In-memory database does not have database default values - public override Task Can_insert_when_FK_has_default_value(bool async) - => Task.CompletedTask; - - public override void Required_many_to_one_dependents_are_cascade_deleted_in_store( - CascadeTiming? cascadeDeleteTiming, - CascadeTiming? deleteOrphansTiming) - { - // FK uniqueness not enforced in in-memory database - } - - public override void Optional_many_to_one_dependents_are_orphaned_in_store( - CascadeTiming? cascadeDeleteTiming, - CascadeTiming? deleteOrphansTiming) - { - // FK uniqueness not enforced in in-memory database - } - - public override void Required_many_to_one_dependents_with_alternate_key_are_cascade_deleted_in_store( - CascadeTiming? cascadeDeleteTiming, - CascadeTiming? deleteOrphansTiming) - { - // FK uniqueness not enforced in in-memory database - } - - public override void Optional_many_to_one_dependents_with_alternate_key_are_orphaned_in_store( - CascadeTiming? cascadeDeleteTiming, - CascadeTiming? deleteOrphansTiming) - { - // FK uniqueness not enforced in in-memory database - } - - public override void Optional_one_to_one_relationships_are_one_to_one( - CascadeTiming? deleteOrphansTiming) - { - // FK uniqueness not enforced in in-memory database - } - - public override void Required_one_to_one_relationships_are_one_to_one( - CascadeTiming? deleteOrphansTiming) - { - // FK uniqueness not enforced in in-memory database - } - - public override void Save_required_one_to_one_changed_by_reference( - ChangeMechanism changeMechanism, - CascadeTiming? deleteOrphansTiming) - { - // FK uniqueness not enforced in in-memory database - } - - public override void Sever_required_one_to_one( - ChangeMechanism changeMechanism, - CascadeTiming? deleteOrphansTiming) - { - // FK uniqueness not enforced in in-memory database - } - - public override void Required_one_to_one_are_cascade_deleted_in_store( - CascadeTiming? cascadeDeleteTiming, - CascadeTiming? deleteOrphansTiming) - { - // FK uniqueness not enforced in in-memory database - } - - public override void Required_non_PK_one_to_one_are_cascade_deleted_in_store( - CascadeTiming? cascadeDeleteTiming, - CascadeTiming? deleteOrphansTiming) - { - // FK uniqueness not enforced in in-memory database - } - - public override void Optional_one_to_one_are_orphaned_in_store( - CascadeTiming? cascadeDeleteTiming, - CascadeTiming? deleteOrphansTiming) - { - // FK uniqueness not enforced in in-memory database - } - - public override void Required_one_to_one_are_cascade_detached_when_Added( - CascadeTiming? cascadeDeleteTiming, - CascadeTiming? deleteOrphansTiming) - { - // FK uniqueness not enforced in in-memory database - } - - public override void Required_non_PK_one_to_one_are_cascade_detached_when_Added( - CascadeTiming? cascadeDeleteTiming, - CascadeTiming? deleteOrphansTiming) - { - // FK uniqueness not enforced in in-memory database - } - - public override void Optional_one_to_one_with_AK_relationships_are_one_to_one( - CascadeTiming? deleteOrphansTiming) - { - // FK uniqueness not enforced in in-memory database - } - - public override void Required_one_to_one_with_AK_relationships_are_one_to_one( - CascadeTiming? deleteOrphansTiming) - { - // FK uniqueness not enforced in in-memory database - } - - public override void Required_one_to_one_with_alternate_key_are_cascade_deleted_in_store( - CascadeTiming? cascadeDeleteTiming, - CascadeTiming? deleteOrphansTiming) - { - // FK uniqueness not enforced in in-memory database - } - - public override void Required_non_PK_one_to_one_with_alternate_key_are_cascade_deleted_in_store( - CascadeTiming? cascadeDeleteTiming, - CascadeTiming? deleteOrphansTiming) - { - // FK uniqueness not enforced in in-memory database - } - - public override void Optional_one_to_one_with_alternate_key_are_orphaned_in_store( - CascadeTiming? cascadeDeleteTiming, - CascadeTiming? deleteOrphansTiming) - { - // FK uniqueness not enforced in in-memory database - } - - public override void Required_non_PK_one_to_one_with_alternate_key_are_cascade_detached_when_Added( - CascadeTiming? cascadeDeleteTiming, - CascadeTiming? deleteOrphansTiming) - { - // FK uniqueness not enforced in in-memory database - } - - public override void Required_one_to_one_with_alternate_key_are_cascade_detached_when_Added( - CascadeTiming? cascadeDeleteTiming, - CascadeTiming? deleteOrphansTiming) - { - // FK uniqueness not enforced in in-memory database - } - - protected override void ExecuteWithStrategyInTransaction( - Action testOperation, - Action nestedTestOperation1 = null, - Action nestedTestOperation2 = null, - Action nestedTestOperation3 = null) - { - base.ExecuteWithStrategyInTransaction(testOperation, nestedTestOperation1, nestedTestOperation2, nestedTestOperation3); - Fixture.Reseed(); - } - - protected override async Task ExecuteWithStrategyInTransactionAsync( - Func testOperation, - Func nestedTestOperation1 = null, - Func nestedTestOperation2 = null, - Func nestedTestOperation3 = null) - { - await base.ExecuteWithStrategyInTransactionAsync( - testOperation, nestedTestOperation1, nestedTestOperation2, nestedTestOperation3); - - Fixture.Reseed(); - } - - public class InMemoryFixture : GraphUpdatesFixtureBase + public class InMemoryFixture : GraphUpdatesInMemoryFixtureBase { protected override string StoreName => "GraphUpdatesTest"; - - protected override ITestStoreFactory TestStoreFactory - => InMemoryTestStoreFactory.Instance; - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - => base.AddOptions(builder).ConfigureWarnings(w => w.Log(InMemoryEventId.TransactionIgnoredWarning)); } } diff --git a/test/EFCore.InMemory.FunctionalTests/GraphUpdates/GraphUpdatesInMemoryTestBase.cs b/test/EFCore.InMemory.FunctionalTests/GraphUpdates/GraphUpdatesInMemoryTestBase.cs new file mode 100644 index 00000000000..20d3d07362c --- /dev/null +++ b/test/EFCore.InMemory.FunctionalTests/GraphUpdates/GraphUpdatesInMemoryTestBase.cs @@ -0,0 +1,188 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore; + +public abstract class GraphUpdatesInMemoryTestBase : GraphUpdatesTestBase + where TFixture : GraphUpdatesInMemoryTestBase.GraphUpdatesInMemoryFixtureBase, new() +{ + protected GraphUpdatesInMemoryTestBase(TFixture fixture) + : base(fixture) + { + } + + // In-memory database does not have database default values + public override Task Can_insert_when_composite_FK_has_default_value_for_one_part(bool async) + => Task.CompletedTask; + + // In-memory database does not have database default values + public override Task Can_insert_when_FK_has_default_value(bool async) + => Task.CompletedTask; + + public override void Required_many_to_one_dependents_are_cascade_deleted_in_store( + CascadeTiming? cascadeDeleteTiming, + CascadeTiming? deleteOrphansTiming) + { + // FK uniqueness not enforced in in-memory database + } + + public override void Optional_many_to_one_dependents_are_orphaned_in_store( + CascadeTiming? cascadeDeleteTiming, + CascadeTiming? deleteOrphansTiming) + { + // FK uniqueness not enforced in in-memory database + } + + public override void Required_many_to_one_dependents_with_alternate_key_are_cascade_deleted_in_store( + CascadeTiming? cascadeDeleteTiming, + CascadeTiming? deleteOrphansTiming) + { + // FK uniqueness not enforced in in-memory database + } + + public override void Optional_many_to_one_dependents_with_alternate_key_are_orphaned_in_store( + CascadeTiming? cascadeDeleteTiming, + CascadeTiming? deleteOrphansTiming) + { + // FK uniqueness not enforced in in-memory database + } + + public override void Optional_one_to_one_relationships_are_one_to_one( + CascadeTiming? deleteOrphansTiming) + { + // FK uniqueness not enforced in in-memory database + } + + public override void Required_one_to_one_relationships_are_one_to_one( + CascadeTiming? deleteOrphansTiming) + { + // FK uniqueness not enforced in in-memory database + } + + public override void Save_required_one_to_one_changed_by_reference( + ChangeMechanism changeMechanism, + CascadeTiming? deleteOrphansTiming) + { + // FK uniqueness not enforced in in-memory database + } + + public override void Sever_required_one_to_one( + ChangeMechanism changeMechanism, + CascadeTiming? deleteOrphansTiming) + { + // FK uniqueness not enforced in in-memory database + } + + public override void Required_one_to_one_are_cascade_deleted_in_store( + CascadeTiming? cascadeDeleteTiming, + CascadeTiming? deleteOrphansTiming) + { + // FK uniqueness not enforced in in-memory database + } + + public override void Required_non_PK_one_to_one_are_cascade_deleted_in_store( + CascadeTiming? cascadeDeleteTiming, + CascadeTiming? deleteOrphansTiming) + { + // FK uniqueness not enforced in in-memory database + } + + public override void Optional_one_to_one_are_orphaned_in_store( + CascadeTiming? cascadeDeleteTiming, + CascadeTiming? deleteOrphansTiming) + { + // FK uniqueness not enforced in in-memory database + } + + public override void Required_one_to_one_are_cascade_detached_when_Added( + CascadeTiming? cascadeDeleteTiming, + CascadeTiming? deleteOrphansTiming) + { + // FK uniqueness not enforced in in-memory database + } + + public override void Required_non_PK_one_to_one_are_cascade_detached_when_Added( + CascadeTiming? cascadeDeleteTiming, + CascadeTiming? deleteOrphansTiming) + { + // FK uniqueness not enforced in in-memory database + } + + public override void Optional_one_to_one_with_AK_relationships_are_one_to_one( + CascadeTiming? deleteOrphansTiming) + { + // FK uniqueness not enforced in in-memory database + } + + public override void Required_one_to_one_with_AK_relationships_are_one_to_one( + CascadeTiming? deleteOrphansTiming) + { + // FK uniqueness not enforced in in-memory database + } + + public override void Required_one_to_one_with_alternate_key_are_cascade_deleted_in_store( + CascadeTiming? cascadeDeleteTiming, + CascadeTiming? deleteOrphansTiming) + { + // FK uniqueness not enforced in in-memory database + } + + public override void Required_non_PK_one_to_one_with_alternate_key_are_cascade_deleted_in_store( + CascadeTiming? cascadeDeleteTiming, + CascadeTiming? deleteOrphansTiming) + { + // FK uniqueness not enforced in in-memory database + } + + public override void Optional_one_to_one_with_alternate_key_are_orphaned_in_store( + CascadeTiming? cascadeDeleteTiming, + CascadeTiming? deleteOrphansTiming) + { + // FK uniqueness not enforced in in-memory database + } + + public override void Required_non_PK_one_to_one_with_alternate_key_are_cascade_detached_when_Added( + CascadeTiming? cascadeDeleteTiming, + CascadeTiming? deleteOrphansTiming) + { + // FK uniqueness not enforced in in-memory database + } + + public override void Required_one_to_one_with_alternate_key_are_cascade_detached_when_Added( + CascadeTiming? cascadeDeleteTiming, + CascadeTiming? deleteOrphansTiming) + { + // FK uniqueness not enforced in in-memory database + } + + protected override void ExecuteWithStrategyInTransaction( + Action testOperation, + Action nestedTestOperation1 = null, + Action nestedTestOperation2 = null, + Action nestedTestOperation3 = null) + { + base.ExecuteWithStrategyInTransaction(testOperation, nestedTestOperation1, nestedTestOperation2, nestedTestOperation3); + Fixture.Reseed(); + } + + protected override async Task ExecuteWithStrategyInTransactionAsync( + Func testOperation, + Func nestedTestOperation1 = null, + Func nestedTestOperation2 = null, + Func nestedTestOperation3 = null) + { + await base.ExecuteWithStrategyInTransactionAsync( + testOperation, nestedTestOperation1, nestedTestOperation2, nestedTestOperation3); + + Fixture.Reseed(); + } + + public abstract class GraphUpdatesInMemoryFixtureBase : GraphUpdatesFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => InMemoryTestStoreFactory.Instance; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder).ConfigureWarnings(w => w.Log(InMemoryEventId.TransactionIgnoredWarning)); + } +} diff --git a/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBase.cs b/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBase.cs index 27ab87f1a66..9fce941f4dd 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 @@ -31,8 +31,11 @@ public virtual bool ForceClientNoAction public virtual bool NoStoreCascades => false; - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - => base.AddOptions(builder).AddInterceptors(new UpdatingIdentityResolutionInterceptor()); + public virtual bool HasIdentityResolution + => false; + + public virtual bool AutoDetectChanges + => true; protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) { @@ -481,62 +484,70 @@ 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(e => e.SomethingCategory) + .WithMany() + .HasForeignKey(e => e.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); + 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 + }); - builder.HasOne(e => e.SomethingCategory) - .WithMany() - .HasForeignKey(e => e.CategoryId) - .OnDelete(DeleteBehavior.ClientSetNull); - }); + modelBuilder.Entity(); + modelBuilder.Entity(); + modelBuilder.Entity(); + modelBuilder.Entity(); } protected virtual object CreateFullGraph() @@ -3686,6 +3697,292 @@ 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 Bayaz : NotifyingEntity + { + private int _bayazId; + private string _bayazName; + private ICollection _firstLaw = new ObservableHashSet(); + + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int BayazId + { + get => _bayazId; + set => SetWithNotify(value, ref _bayazId); + } + + public string BayazName + { + get => _bayazName; + set => SetWithNotify(value, ref _bayazName); + } + + public virtual ICollection FirstLaw + { + get => _firstLaw; + set => SetWithNotify(value, ref _firstLaw); + } + } + + protected class FirstLaw : NotifyingEntity + { + private int _firstLawId; + private string _firstLawName; + private int _bayazId; + private Bayaz _bayaz = null!; + private readonly ICollection _secondLaw = new ObservableHashSet(); + + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int FirstLawId + { + get => _firstLawId; + set => SetWithNotify(value, ref _firstLawId); + } + + public string FirstLawName + { + get => _firstLawName; + set => SetWithNotify(value, ref _firstLawName); + } + + public int BayazId + { + get => _bayazId; + set => SetWithNotify(value, ref _bayazId); + } + + public virtual Bayaz Bayaz + { + get => _bayaz; + set => SetWithNotify(value, ref _bayaz); + } + + public virtual ICollection SecondLaw + => _secondLaw; + } + + protected class SecondLaw : NotifyingEntity + { + private int _secondLawId; + private string _secondLawName; + private int _firstLawId; + private FirstLaw _firstLaw = null!; + private readonly ICollection _thirdLaw = new ObservableHashSet(); + + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int SecondLawId + { + get => _secondLawId; + set => SetWithNotify(value, ref _secondLawId); + } + + public string SecondLawName + { + get => _secondLawName; + set => SetWithNotify(value, ref _secondLawName); + } + + public int FirstLawId + { + get => _firstLawId; + set => SetWithNotify(value, ref _firstLawId); + } + + public virtual FirstLaw FirstLaw + { + get => _firstLaw; + set => SetWithNotify(value, ref _firstLaw); + } + + public virtual ICollection ThirdLaw + => _thirdLaw; + } + + protected class ThirdLaw : NotifyingEntity + { + private int _thirdLawId; + private string _thirdLawName; + private int _secondLawId; + private SecondLaw _secondLaw = null!; + + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int ThirdLawId + { + get => _thirdLawId; + set => SetWithNotify(value, ref _thirdLawId); + } + + public string ThirdLawName + { + get => _thirdLawName; + set => SetWithNotify(value, ref _thirdLawName); + } + + public int SecondLawId + { + get => _secondLawId; + set => SetWithNotify(value, ref _secondLawId); + } + + public virtual SecondLaw SecondLaw + { + get => _secondLaw; + set => SetWithNotify(value, ref _secondLaw); + } + } + 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..2959e19cbcb 100644 --- a/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseMiscellaneous.cs +++ b/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseMiscellaneous.cs @@ -115,7 +115,7 @@ public virtual async Task Saving_multiple_modified_entities_with_the_same_key_do } else { - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(Fixture.HasIdentityResolution ? 2 : 3, context.ChangeTracker.Entries().Count()); Assert.Equal(EntityState.Deleted, context.Entry(college).State); Assert.Equal(EntityState.Unchanged, context.Entry(city).State); } @@ -1612,131 +1612,324 @@ public virtual void Can_add_multiple_dependents_when_multiple_possible_principal } }); - [ConditionalFact] - public void Can_attach_full_required_graph_of_duplicates() - => ExecuteWithStrategyInTransaction( - context => + [ConditionalTheory] // Issue #30122 + [InlineData(false)] + [InlineData(true)] + public virtual Task Sever_relationship_that_will_later_be_deleted(bool async) + => ExecuteWithStrategyInTransactionAsync( + async context => { - var trackedRoot = LoadRequiredGraph(context); - var entries = context.ChangeTracker.Entries().ToList(); + 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)); + }); - context.Attach(QueryRequiredGraph(context).AsNoTracking().Single(IsTheRoot)); + [ConditionalTheory] // Issue #30135 + [InlineData(false)] + [InlineData(true)] + public virtual Task Update_root_by_collection_replacement_of_inserted_first_level(bool async) + => ExecuteWithStrategyInTransactionAsync( + async context => + { + PopulateGraph(context); + var newRoot = BuildNewRoot(firstLevel1: true, secondLevel1: true, thirdLevel1: true, firstLevel2: true); - AssertEntries(entries, context.ChangeTracker.Entries().ToList()); - AssertNavigations(trackedRoot); + Assert.Equal(1, context.Set().Count(x => x.BayazId == 1)); - Assert.Equal(0, context.SaveChanges()); + if (await UpdateRoot(context, newRoot, async)) + { + Assert.Equal( + Fixture.HasIdentityResolution || !Fixture.AutoDetectChanges ? 1 : 2, + context.Set().Count(x => x.BayazId == 1)); + } }); - [ConditionalFact] - public void Can_attach_full_optional_graph_of_duplicates() - => ExecuteWithStrategyInTransaction( - context => + [ConditionalTheory] // Issue #30135 + [InlineData(false)] + [InlineData(true)] + public virtual Task Update_root_by_collection_replacement_of_deleted_first_level(bool async) + => ExecuteWithStrategyInTransactionAsync( + async context => { - var trackedRoot = LoadOptionalGraph(context); - var entries = context.ChangeTracker.Entries().ToList(); + PopulateGraph(context); + var newRoot = BuildNewRoot(); - context.Attach(QueryOptionalGraph(context).AsNoTracking().Single(IsTheRoot)); + Assert.Equal(1, context.Set().Count(x => x.BayazId == 1)); - AssertEntries(entries, context.ChangeTracker.Entries().ToList()); - AssertNavigations(trackedRoot); - - Assert.Equal(0, context.SaveChanges()); + if (await UpdateRoot(context, newRoot, async)) + { + Assert.Equal(Fixture.AutoDetectChanges ? 0 : 1, context.Set().Count(x => x.BayazId == 1)); + } }); - [ConditionalFact] - public void Can_attach_full_required_non_PK_graph_of_duplicates() - => ExecuteWithStrategyInTransaction( - context => + [ConditionalTheory] // Issue #30135 + [InlineData(false)] + [InlineData(true)] + public virtual Task Update_root_by_collection_replacement_of_inserted_second_level(bool async) + => ExecuteWithStrategyInTransactionAsync( + async context => { - var trackedRoot = LoadRequiredNonPkGraph(context); - var entries = context.ChangeTracker.Entries().ToList(); - - context.Attach(QueryRequiredNonPkGraph(context).AsNoTracking().Single(IsTheRoot)); + PopulateGraph(context); + var newRoot = BuildNewRoot(firstLevel1: true, secondLevel1: true, thirdLevel1: true, firstLevel2: true, secondLevel2: true); - AssertEntries(entries, context.ChangeTracker.Entries().ToList()); - AssertNavigations(trackedRoot); + Assert.Equal(1, context.Set().Count(x => x.BayazId == 1)); + Assert.Equal(1, context.Set().Count(x => x.FirstLawId == 11)); - Assert.Equal(0, context.SaveChanges()); + if (await UpdateRoot(context, newRoot, async)) + { + if (Fixture.AutoDetectChanges) + { + Assert.Equal(Fixture.HasIdentityResolution ? 1 : 2, context.Set().Count(x => x.BayazId == 1)); + Assert.Equal(Fixture.HasIdentityResolution ? 0 : 2, context.Set().Count(x => x.FirstLawId == 11)); + } + else + { + Assert.Equal(1, context.Set().Count(x => x.BayazId == 1)); + Assert.Equal(1, context.Set().Count(x => x.FirstLawId == 11)); + } + } }); - [ConditionalFact] - public void Can_attach_full_required_AK_graph_of_duplicates() - => ExecuteWithStrategyInTransaction( - context => + [ConditionalTheory] // Issue #30135 + [InlineData(false)] + [InlineData(true)] + public virtual Task Update_root_by_collection_replacement_of_deleted_second_level( + bool async) + => ExecuteWithStrategyInTransactionAsync( + async context => { - var trackedRoot = LoadRequiredAkGraph(context); - var entries = context.ChangeTracker.Entries().ToList(); + PopulateGraph(context); + var newRoot = BuildNewRoot(firstLevel1: true); - context.Attach(QueryRequiredAkGraph(context).AsNoTracking().Single(IsTheRoot)); + Assert.Equal(1, context.Set().Count(x => x.BayazId == 1)); + Assert.Equal(1, context.Set().Count(x => x.FirstLawId == 11)); - AssertEntries(entries, context.ChangeTracker.Entries().ToList()); - AssertNavigations(trackedRoot); - - Assert.Equal(0, context.SaveChanges()); + if (await UpdateRoot(context, newRoot, async)) + { + Assert.Equal(Fixture.HasIdentityResolution ? 0 : 1, context.Set().Count(x => x.BayazId == 1)); + Assert.Equal(Fixture.AutoDetectChanges ? 0 : 1, context.Set().Count(x => x.FirstLawId == 11)); + } }); - [ConditionalFact] - public void Can_attach_full_optional_AK_graph_of_duplicates() - => ExecuteWithStrategyInTransaction( - context => + [ConditionalTheory] // Issue #30135 + [InlineData(false)] + [InlineData(true)] + public virtual Task Update_root_by_collection_replacement_of_inserted_first_level_level(bool async) + => ExecuteWithStrategyInTransactionAsync( + async context => { - var trackedRoot = LoadOptionalAkGraph(context); - var entries = context.ChangeTracker.Entries().ToList(); + PopulateGraph(context); + var newRoot = BuildNewRoot( + firstLevel1: true, secondLevel1: true, thirdLevel1: true, firstLevel2: true, secondLevel2: true, thirdLevel2: true); - context.Attach(QueryOptionalAkGraph(context).AsNoTracking().Single(IsTheRoot)); + Assert.Equal(1, context.Set().Count(x => x.BayazId == 1)); + Assert.Equal(1, context.Set().Count(x => x.FirstLawId == 11)); + Assert.Equal(1, context.Set().Count(x => x.SecondLawId == 111)); - AssertEntries(entries, context.ChangeTracker.Entries().ToList()); - AssertNavigations(trackedRoot); - - Assert.Equal(0, context.SaveChanges()); + if (await UpdateRoot(context, newRoot, async)) + { + if (Fixture.AutoDetectChanges) + { + Assert.Equal(Fixture.HasIdentityResolution ? 1 : 2, context.Set().Count(x => x.BayazId == 1)); + Assert.Equal(Fixture.HasIdentityResolution ? 0 : 2, context.Set().Count(x => x.FirstLawId == 11)); + Assert.Equal(Fixture.HasIdentityResolution ? 0 : 2, context.Set().Count(x => x.SecondLawId == 111)); + } + else + { + Assert.Equal(1, context.Set().Count(x => x.BayazId == 1)); + Assert.Equal(1, context.Set().Count(x => x.FirstLawId == 11)); + Assert.Equal(1, context.Set().Count(x => x.SecondLawId == 111)); + } + } }); - [ConditionalFact] - public void Can_attach_full_required_non_PK_AK_graph_of_duplicates() - => ExecuteWithStrategyInTransaction( - context => + [ConditionalTheory] // Issue #30135 + [InlineData(false)] + [InlineData(true)] + public virtual Task Update_root_by_collection_replacement_of_deleted_third_level(bool async) + => ExecuteWithStrategyInTransactionAsync( + async context => { - var trackedRoot = LoadRequiredNonPkAkGraph(context); - var entries = context.ChangeTracker.Entries().ToList(); + PopulateGraph(context); + var newRoot = BuildNewRoot(firstLevel1: true, secondLevel1: true); + + Assert.Equal(1, context.Set().Count(x => x.BayazId == 1)); + Assert.Equal(1, context.Set().Count(x => x.FirstLawId == 11)); + Assert.Equal(1, context.Set().Count(x => x.SecondLawId == 111)); + + if (await UpdateRoot(context, newRoot, async)) + { + Assert.Equal(Fixture.HasIdentityResolution ? 0 : 1, context.Set().Count(x => x.BayazId == 1)); + Assert.Equal(Fixture.HasIdentityResolution ? 0 : 1, context.Set().Count(x => x.FirstLawId == 11)); + Assert.Equal(Fixture.AutoDetectChanges ? 0 : 1, context.Set().Count(x => x.SecondLawId == 111)); + } + }); + + protected async Task UpdateRoot(DbContext context, Bayaz newRoot, bool async) + { + var existingRoot = context.Set() + .Include(x => x.FirstLaw) + .ThenInclude(x => x.SecondLaw) + .ThenInclude(x => x.ThirdLaw) + .Single(x => x.BayazId == newRoot.BayazId); + + existingRoot.BayazName = newRoot.BayazName; + existingRoot.FirstLaw = newRoot.FirstLaw; + + if (Fixture.ForceClientNoAction) + { + Assert.Equal( + CoreStrings.RelationshipConceptualNullSensitive(nameof(Bayaz), nameof(FirstLaw), "{BayazId: 1}"), + (await Assert.ThrowsAsync( + async () => + { + _ = async + ? await context.SaveChangesAsync() + : context.SaveChanges(); + })).Message); + + return false; + } + + _ = async + ? await context.SaveChangesAsync() + : context.SaveChanges(); + + return true; + } - context.Attach(QueryRequiredNonPkAkGraph(context).AsNoTracking().Single(IsTheRoot)); + protected void PopulateGraph(DbContext context) + { + context.Add(new Bayaz { BayazId = 1, BayazName = "bayaz" }); - AssertEntries(entries, context.ChangeTracker.Entries().ToList()); - AssertNavigations(trackedRoot); + context.Add( + new FirstLaw + { + FirstLawId = 11, + FirstLawName = "firstLaw1", + BayazId = 1 + }); - Assert.Equal(0, context.SaveChanges()); + context.Add( + new SecondLaw + { + SecondLawId = 111, + SecondLawName = "secondLaw1", + FirstLawId = 11 }); - [ConditionalFact] - public void Can_attach_full_required_one_to_many_graph_of_duplicates() - => ExecuteWithStrategyInTransaction( - context => + context.Add( + new ThirdLaw { - var trackedRoot = LoadOptionalOneToManyGraph(context); - var entries = context.ChangeTracker.Entries().ToList(); + ThirdLawId = 1111, + ThirdLawName = "thirdLaw1", + SecondLawId = 111 + }); + + context.SaveChanges(); + } - context.Attach(QueryOptionalOneToManyGraph(context).AsNoTracking().Single(IsTheRoot)); + protected Bayaz BuildNewRoot( + bool firstLevel1 = false, + bool firstLevel2 = false, + bool secondLevel1 = false, + bool secondLevel2 = false, + bool thirdLevel1 = false, + bool thirdLevel2 = false) + { + var root = new Bayaz { BayazId = 1, BayazName = "bayaz" }; - AssertEntries(entries, context.ChangeTracker.Entries().ToList()); - AssertNavigations(trackedRoot); + if (firstLevel1) + { + root.FirstLaw.Add(AddFirstLevel(secondLevel1, secondLevel2, thirdLevel1, thirdLevel2)); + } - Assert.Equal(0, context.SaveChanges()); + if (firstLevel2) + { + root.FirstLaw.Add(new FirstLaw + { + FirstLawId = 12, + FirstLawName = "firstLaw2", + BayazId = 1 }); + } - [ConditionalFact] - public void Can_attach_full_required_composite_graph_of_duplicates() - => ExecuteWithStrategyInTransaction( - context => + return root; + } + + private FirstLaw AddFirstLevel(bool secondLevel1, bool secondLevel2, bool thirdLevel1, bool thirdLevel2) + { + var firstLevel = new FirstLaw + { + FirstLawId = 11, + FirstLawName = "firstLaw1", + BayazId = 1 + }; + + if (secondLevel1) + { + firstLevel.SecondLaw.Add(AddSecondLevel(thirdLevel1, thirdLevel2)); + } + + if (secondLevel2) + { + firstLevel.SecondLaw.Add(new SecondLaw { - var trackedRoot = LoadRequiredCompositeGraph(context); - var entries = context.ChangeTracker.Entries().ToList(); + SecondLawId = 112, + SecondLawName = "secondLaw2", + FirstLawId = 11 + }); + } - context.Attach(QueryRequiredCompositeGraph(context).AsNoTracking().Single(IsTheRoot)); + return firstLevel; + } - AssertEntries(entries, context.ChangeTracker.Entries().ToList()); - AssertNavigations(trackedRoot); + private static SecondLaw AddSecondLevel(bool thirdLevel1, bool thirdLevel2) + { + var secondLevel = new SecondLaw + { + SecondLawId = 111, + SecondLawName = "secondLaw1", + FirstLawId = 11 + }; + + if (thirdLevel1) + { + secondLevel.ThirdLaw.Add(new ThirdLaw + { + ThirdLawId = 1111, + ThirdLawName = "thirdLaw1", + SecondLawId = 111 + }); + } - Assert.Equal(0, context.SaveChanges()); + if (thirdLevel2) + { + secondLevel.ThirdLaw.Add(new ThirdLaw + { + ThirdLawId = 1112, + ThirdLawName = "thirdLaw2", + SecondLawId = 111 }); + } + + return secondLevel; + } } diff --git a/test/EFCore.SqlServer.FunctionalTests/GraphUpdates/GraphUpdatesSqlServerOwnedTest.cs b/test/EFCore.SqlServer.FunctionalTests/GraphUpdates/GraphUpdatesSqlServerOwnedTest.cs index e6df4ea04e3..f357c6c4902 100644 --- a/test/EFCore.SqlServer.FunctionalTests/GraphUpdates/GraphUpdatesSqlServerOwnedTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/GraphUpdates/GraphUpdatesSqlServerOwnedTest.cs @@ -10,6 +10,34 @@ public GraphUpdatesSqlServerOwnedTest(SqlServerFixture fixture) { } + // No owned types + public override Task Update_root_by_collection_replacement_of_inserted_first_level(bool async) + => Task.CompletedTask; + + // No owned types + public override Task Update_root_by_collection_replacement_of_deleted_first_level(bool async) + => Task.CompletedTask; + + // No owned types + public override Task Update_root_by_collection_replacement_of_inserted_second_level(bool async) + => Task.CompletedTask; + + // No owned types + public override Task Update_root_by_collection_replacement_of_deleted_second_level(bool async) + => Task.CompletedTask; + + // No owned types + public override Task Update_root_by_collection_replacement_of_inserted_first_level_level(bool async) + => Task.CompletedTask; + + // No owned types + public override Task Update_root_by_collection_replacement_of_deleted_third_level(bool async) + => Task.CompletedTask; + + // 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.Sqlite.FunctionalTests/GraphUpdates/GraphUpdatesSqliteSnapshotNotificationsTest.cs b/test/EFCore.Sqlite.FunctionalTests/GraphUpdates/GraphUpdatesSqliteSnapshotNotificationsTest.cs index cbb5cf5a182..3b55c4a1055 100644 --- a/test/EFCore.Sqlite.FunctionalTests/GraphUpdates/GraphUpdatesSqliteSnapshotNotificationsTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/GraphUpdates/GraphUpdatesSqliteSnapshotNotificationsTest.cs @@ -19,7 +19,7 @@ public class SqliteFixture : GraphUpdatesSqliteFixtureBase protected override string StoreName => "GraphUpdatesSnapshotTest"; - protected override bool AutoDetectChanges + public override bool AutoDetectChanges => true; protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) diff --git a/test/EFCore.Sqlite.FunctionalTests/GraphUpdates/GraphUpdatesSqliteTestBase.cs b/test/EFCore.Sqlite.FunctionalTests/GraphUpdates/GraphUpdatesSqliteTestBase.cs index 0ff1fa938e4..033e824f294 100644 --- a/test/EFCore.Sqlite.FunctionalTests/GraphUpdates/GraphUpdatesSqliteTestBase.cs +++ b/test/EFCore.Sqlite.FunctionalTests/GraphUpdates/GraphUpdatesSqliteTestBase.cs @@ -52,7 +52,7 @@ protected override ITestStoreFactory TestStoreFactory public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) => base.AddOptions(builder.ConfigureWarnings(b => b.Ignore(SqliteEventId.CompositeKeyWithValueGeneration))); - protected virtual bool AutoDetectChanges + public override bool AutoDetectChanges => false; public override PoolableDbContext CreateContext() diff --git a/test/EFCore.Sqlite.FunctionalTests/Update/StoreValueGenerationLegacySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Update/StoreValueGenerationLegacySqliteTest.cs index 77c577444c3..288948b047f 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Update/StoreValueGenerationLegacySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Update/StoreValueGenerationLegacySqliteTest.cs @@ -440,6 +440,9 @@ DELETE FROM "WithSomeDatabaseGenerated2" public class StoreValueGenerationWithoutReturningSqliteFixture : StoreValueGenerationSqliteFixture { + protected override string StoreName + => "StoreValueGenerationWithoutReturningTest"; + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) { base.OnModelCreating(modelBuilder, context);