diff --git a/src/EFCore/ChangeTracking/EntityEntryGraphNode`.cs b/src/EFCore/ChangeTracking/EntityEntryGraphNode`.cs index de64cce50d8..5c7ca6480e7 100644 --- a/src/EFCore/ChangeTracking/EntityEntryGraphNode`.cs +++ b/src/EFCore/ChangeTracking/EntityEntryGraphNode`.cs @@ -37,7 +37,7 @@ public EntityEntryGraphNode( /// /// Gets or sets state that will be available to all nodes that are visited after this node. /// - public virtual TState NodeState { get; } + public virtual TState NodeState { get; [param: CanBeNull] set; } /// /// Creates a new node for the entity that is being traversed next in the graph. diff --git a/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs b/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs index 958dd3d167b..9be8eb6c891 100644 --- a/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs +++ b/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -1098,7 +1097,7 @@ private static readonly IServiceProvider _poolProvider = new ServiceCollection() .AddDbContextPool( p => p.UseInMemoryDatabase(nameof(LikeAZooContextPooled)) - .UseInternalServiceProvider(InMemoryFixture.BuildServiceProvider(_loggerFactory))) + .UseInternalServiceProvider(InMemoryFixture.BuildServiceProvider(_loggerFactory))) .BuildServiceProvider(); private class LikeAZooContextPooled : LikeAZooContext @@ -1301,841 +1300,6 @@ public void Can_get_Context() Assert.Same(context, context.ChangeTracker.Context); } - private static string NodeString(EntityEntryGraphNode node) - => EntryString(node.SourceEntry) - + " ---" - + node.InboundNavigation?.Name - + "--> " - + EntryString(node.Entry); - - private static string EntryString(EntityEntry entry) - => entry == null - ? "" - : entry.Metadata.DisplayName() - + ":" - + entry.Property(entry.Metadata.FindPrimaryKey().Properties[0].Name).CurrentValue; - - [ConditionalTheory] - [InlineData(false, false)] - [InlineData(false, true)] - [InlineData(true, false)] - [InlineData(true, true)] - public void Can_attach_nullable_PK_parent_with_child_collection(bool useAttach, bool setKeys) - { - using var context = new EarlyLearningCenter(); - var category = new NullbileCategory - { - Products = new List - { - new NullbileProduct(), - new NullbileProduct(), - new NullbileProduct() - } - }; - - if (setKeys) - { - context.Entry(category).Property("Id").CurrentValue = 1; - context.Entry(category.Products[0]).Property("Id").CurrentValue = 1; - context.Entry(category.Products[1]).Property("Id").CurrentValue = 2; - context.Entry(category.Products[2]).Property("Id").CurrentValue = 3; - } - - if (useAttach) - { - context.Attach(category); - } - else - { - var traversal = new List(); - - context.ChangeTracker.TrackGraph( - category, e => - { - e.Entry.State = e.Entry.IsKeySet ? EntityState.Unchanged : EntityState.Added; - traversal.Add(NodeString(e)); - }); - - Assert.Equal( - new List - { - " -----> NullbileCategory:1", - "NullbileCategory:1 ---Products--> NullbileProduct:1", - "NullbileCategory:1 ---Products--> NullbileProduct:2", - "NullbileCategory:1 ---Products--> NullbileProduct:3" - }, - traversal); - } - - Assert.Equal(4, context.ChangeTracker.Entries().Count()); - - var categoryEntry = context.Entry(category); - var product0Entry = context.Entry(category.Products[0]); - var product1Entry = context.Entry(category.Products[1]); - var product2Entry = context.Entry(category.Products[2]); - - var expectedState = setKeys ? EntityState.Unchanged : EntityState.Added; - Assert.Equal(expectedState, categoryEntry.State); - Assert.Equal(expectedState, product0Entry.State); - Assert.Equal(expectedState, product1Entry.State); - Assert.Equal(expectedState, product2Entry.State); - - Assert.Same(category, category.Products[0].Category); - Assert.Same(category, category.Products[1].Category); - Assert.Same(category, category.Products[2].Category); - - var categoryId = categoryEntry.Property("Id").CurrentValue; - Assert.NotNull(categoryId); - - Assert.Equal(categoryId, product0Entry.Property("CategoryId").CurrentValue); - Assert.Equal(categoryId, product1Entry.Property("CategoryId").CurrentValue); - Assert.Equal(categoryId, product2Entry.Property("CategoryId").CurrentValue); - } - - [ConditionalTheory] - [InlineData(false, false)] - [InlineData(false, true)] - [InlineData(true, false)] - [InlineData(true, true)] - public void Can_attach_nullable_PK_parent_with_one_to_one_children(bool useAttach, bool setKeys) - { - using var context = new EarlyLearningCenter(); - var category = new NullbileCategory { Info = new NullbileCategoryInfo() }; - - if (setKeys) - { - context.Entry(category).Property("Id").CurrentValue = 1; - context.Entry(category.Info).Property("Id").CurrentValue = 1; - } - - if (useAttach) - { - context.Attach(category); - } - else - { - var traversal = new List(); - - context.ChangeTracker.TrackGraph( - category, e => - { - e.Entry.State = e.Entry.IsKeySet ? EntityState.Unchanged : EntityState.Added; - traversal.Add(NodeString(e)); - }); - - Assert.Equal( - new List { " -----> NullbileCategory:1", "NullbileCategory:1 ---Info--> NullbileCategoryInfo:1" }, - traversal); - } - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var expectedState = setKeys ? EntityState.Unchanged : EntityState.Added; - Assert.Equal(expectedState, context.Entry(category).State); - Assert.Equal(expectedState, context.Entry(category.Info).State); - - Assert.Same(category, category.Info.Category); - } - - [ConditionalTheory] - [InlineData(false, false, false)] - [InlineData(false, true, false)] - [InlineData(true, false, false)] - [InlineData(true, true, false)] - [InlineData(false, false, true)] - [InlineData(false, true, true)] - [InlineData(true, false, true)] - [InlineData(true, true, true)] - public void Can_attach_parent_with_owned_dependent(bool useAttach, bool setPrincipalKey, bool setDependentKey) - { - using var context = new EarlyLearningCenter(); - var sweet = new Sweet { Dreams = new Dreams { Are = new AreMade(), Made = new AreMade() } }; - - if (setPrincipalKey) - { - sweet.Id = 1; - } - - if (setDependentKey) - { - var dreamsEntry = context.Entry(sweet).Reference(e => e.Dreams).TargetEntry; - dreamsEntry.Property("SweetId").CurrentValue = 1; - dreamsEntry.Reference(e => e.Are).TargetEntry.Property("DreamsSweetId").CurrentValue = 1; - dreamsEntry.Reference(e => e.Made).TargetEntry.Property("DreamsSweetId").CurrentValue = 1; - } - - if (useAttach) - { - context.Attach(sweet); - } - else - { - var traversal = new List(); - - context.ChangeTracker.TrackGraph( - sweet, e => - { - if (e.Entry.Metadata.IsOwned()) - { - e.Entry.State = e.SourceEntry.State; - } - else - { - e.Entry.State = e.Entry.IsKeySet ? EntityState.Unchanged : EntityState.Added; - } - - traversal.Add(NodeString(e)); - }); - - Assert.Equal( - new List - { - " -----> Sweet:1", - "Sweet:1 ---Dreams--> Dreams:1", - "Dreams:1 ---Are--> Dreams.Are#AreMade:1", - "Dreams:1 ---Made--> Dreams.Made#AreMade:1" - }, - traversal); - } - - Assert.Equal(4, context.ChangeTracker.Entries().Count()); - - var dependentEntry = context.Entry(sweet.Dreams); - var dependentEntry2a = context.Entry(sweet.Dreams.Are); - var dependentEntry2b = context.Entry(sweet.Dreams.Made); - - var expectedPrincipalState = setPrincipalKey ? EntityState.Unchanged : EntityState.Added; - var expectedDependentState = setPrincipalKey || (setDependentKey && useAttach) ? EntityState.Unchanged : EntityState.Added; - - Assert.Equal(expectedPrincipalState, context.Entry(sweet).State); - Assert.Equal(expectedDependentState, dependentEntry.State); - Assert.Equal(expectedDependentState, dependentEntry2a.State); - Assert.Equal(expectedDependentState, dependentEntry2b.State); - - Assert.Equal(1, sweet.Id); - Assert.Equal(1, dependentEntry.Property(dependentEntry.Metadata.FindPrimaryKey().Properties[0].Name).CurrentValue); - Assert.Equal(1, dependentEntry2a.Property(dependentEntry2a.Metadata.FindPrimaryKey().Properties[0].Name).CurrentValue); - Assert.Equal(1, dependentEntry2b.Property(dependentEntry2b.Metadata.FindPrimaryKey().Properties[0].Name).CurrentValue); - } - - [ConditionalTheory] - [InlineData(false, false)] - [InlineData(false, true)] - [InlineData(true, false)] - [InlineData(true, true)] - public void Can_attach_owned_dependent_with_reference_to_parent(bool useAttach, bool setDependentKey) - { - using var context = new EarlyLearningCenter(); - var dreams = new Dreams - { - Sweet = new Sweet { Id = 1 }, - Are = new AreMade(), - Made = new AreMade() - }; - - if (setDependentKey) - { - var dreamsEntry = context.Entry(dreams); - dreamsEntry.Property("SweetId").CurrentValue = 1; - dreamsEntry.Reference(e => e.Are).TargetEntry.Property("DreamsSweetId").CurrentValue = 1; - dreamsEntry.Reference(e => e.Made).TargetEntry.Property("DreamsSweetId").CurrentValue = 1; - } - - if (useAttach) - { - context.Attach(dreams); - } - else - { - var traversal = new List(); - - context.ChangeTracker.TrackGraph( - dreams, e => - { - e.Entry.State = e.Entry.IsKeySet ? EntityState.Unchanged : EntityState.Added; - - traversal.Add(NodeString(e)); - }); - - Assert.Equal( - new List - { - " -----> Dreams:1", - "Dreams:1 ---Are--> Dreams.Are#AreMade:1", - "Dreams:1 ---Made--> Dreams.Made#AreMade:1", - "Dreams:1 ---Sweet--> Sweet:1" - }, - traversal); - } - - Assert.Equal(4, context.ChangeTracker.Entries().Count()); - - var dependentEntry = context.Entry(dreams); - var dependentEntry2a = context.Entry(dreams.Are); - var dependentEntry2b = context.Entry(dreams.Made); - - var expectedPrincipalState = EntityState.Unchanged; - var expectedDependentState = setDependentKey ? EntityState.Unchanged : EntityState.Added; - - Assert.Equal(expectedPrincipalState, context.Entry(dreams.Sweet).State); - Assert.Equal(expectedDependentState, dependentEntry.State); - Assert.Equal(expectedDependentState, dependentEntry2a.State); - Assert.Equal(expectedDependentState, dependentEntry2b.State); - - Assert.Equal(1, dreams.Sweet.Id); - Assert.Equal(1, dependentEntry.Property(dependentEntry.Metadata.FindPrimaryKey().Properties[0].Name).CurrentValue); - Assert.Equal(1, dependentEntry2a.Property(dependentEntry2a.Metadata.FindPrimaryKey().Properties[0].Name).CurrentValue); - Assert.Equal(1, dependentEntry2b.Property(dependentEntry2b.Metadata.FindPrimaryKey().Properties[0].Name).CurrentValue); - } - - [ConditionalFact] - public void Can_attach_parent_with_child_collection() - { - using var context = new EarlyLearningCenter(); - var category = new Category - { - Id = 1, - Products = new List - { - new Product { Id = 1 }, - new Product { Id = 2 }, - new Product { Id = 3 } - } - }; - - var traversal = new List(); - - context.ChangeTracker.TrackGraph( - category, e => - { - traversal.Add(NodeString(e)); - e.Entry.State = EntityState.Modified; - }); - - Assert.Equal( - new List - { - " -----> Category:1", - "Category:1 ---Products--> Product:1", - "Category:1 ---Products--> Product:2", - "Category:1 ---Products--> Product:3" - }, - traversal); - - Assert.Equal(4, context.ChangeTracker.Entries().Count()); - - Assert.Equal(EntityState.Modified, context.Entry(category).State); - Assert.Equal(EntityState.Modified, context.Entry(category.Products[0]).State); - Assert.Equal(EntityState.Modified, context.Entry(category.Products[1]).State); - Assert.Equal(EntityState.Modified, context.Entry(category.Products[2]).State); - - Assert.Same(category, category.Products[0].Category); - Assert.Same(category, category.Products[1].Category); - Assert.Same(category, category.Products[2].Category); - - Assert.Equal(category.Id, category.Products[0].CategoryId); - Assert.Equal(category.Id, category.Products[1].CategoryId); - Assert.Equal(category.Id, category.Products[2].CategoryId); - } - - [ConditionalFact] - public void Can_attach_child_with_reference_to_parent() - { - using var context = new EarlyLearningCenter(); - var product = new Product { Id = 1, Category = new Category { Id = 1 } }; - - var traversal = new List(); - - context.ChangeTracker.TrackGraph( - product, e => - { - traversal.Add(NodeString(e)); - e.Entry.State = EntityState.Modified; - }); - - Assert.Equal( - new List { " -----> Product:1", "Product:1 ---Category--> Category:1" }, - traversal); - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - Assert.Equal(EntityState.Modified, context.Entry(product).State); - Assert.Equal(EntityState.Modified, context.Entry(product.Category).State); - - Assert.Same(product, product.Category.Products[0]); - Assert.Equal(product.Category.Id, product.CategoryId); - } - - [ConditionalFact] - public void Can_attach_parent_with_one_to_one_children() - { - using var context = new EarlyLearningCenter(); - var product = new Product { Id = 1, Details = new ProductDetails { Id = 1, Tag = new ProductDetailsTag { Id = 1 } } }; - - var traversal = new List(); - - context.ChangeTracker.TrackGraph( - product, e => - { - traversal.Add(NodeString(e)); - e.Entry.State = EntityState.Unchanged; - }); - - Assert.Equal( - new List - { - " -----> Product:1", - "Product:1 ---Details--> ProductDetails:1", - "ProductDetails:1 ---Tag--> ProductDetailsTag:1" - }, - traversal); - - Assert.Equal(3, context.ChangeTracker.Entries().Count()); - - Assert.Equal(EntityState.Unchanged, context.Entry(product).State); - Assert.Equal(EntityState.Unchanged, context.Entry(product.Details).State); - Assert.Equal(EntityState.Unchanged, context.Entry(product.Details.Tag).State); - - Assert.Same(product, product.Details.Product); - Assert.Same(product.Details, product.Details.Tag.Details); - } - - [ConditionalFact] - public void Can_attach_child_with_one_to_one_parents() - { - using var context = new EarlyLearningCenter(); - var tag = new ProductDetailsTag { Id = 1, Details = new ProductDetails { Id = 1, Product = new Product { Id = 1 } } }; - - var traversal = new List(); - - context.ChangeTracker.TrackGraph( - tag, e => - { - traversal.Add(NodeString(e)); - e.Entry.State = EntityState.Unchanged; - }); - - Assert.Equal( - new List - { - " -----> ProductDetailsTag:1", - "ProductDetailsTag:1 ---Details--> ProductDetails:1", - "ProductDetails:1 ---Product--> Product:1" - }, - traversal); - - Assert.Equal(3, context.ChangeTracker.Entries().Count()); - - Assert.Equal(EntityState.Unchanged, context.Entry(tag).State); - Assert.Equal(EntityState.Unchanged, context.Entry(tag.Details).State); - Assert.Equal(EntityState.Unchanged, context.Entry(tag.Details.Product).State); - - Assert.Same(tag, tag.Details.Tag); - Assert.Same(tag.Details, tag.Details.Product.Details); - } - - [ConditionalFact] - public void Can_attach_entity_with_one_to_one_parent_and_child() - { - using var context = new EarlyLearningCenter(); - var details = new ProductDetails - { - Id = 1, - Product = new Product { Id = 1 }, - Tag = new ProductDetailsTag { Id = 1 } - }; - - var traversal = new List(); - - context.ChangeTracker.TrackGraph( - details, e => - { - traversal.Add(NodeString(e)); - e.Entry.State = EntityState.Unchanged; - }); - - Assert.Equal( - new List - { - " -----> ProductDetails:1", - "ProductDetails:1 ---Product--> Product:1", - "ProductDetails:1 ---Tag--> ProductDetailsTag:1" - }, - traversal); - - Assert.Equal(3, context.ChangeTracker.Entries().Count()); - - Assert.Equal(EntityState.Unchanged, context.Entry(details).State); - Assert.Equal(EntityState.Unchanged, context.Entry(details.Product).State); - Assert.Equal(EntityState.Unchanged, context.Entry(details.Tag).State); - - Assert.Same(details, details.Tag.Details); - Assert.Same(details, details.Product.Details); - } - - [ConditionalFact] - public void Entities_that_are_already_tracked_will_not_get_attached() - { - using var context = new EarlyLearningCenter(); - var existingProduct = context.Attach( - new Product { Id = 2, CategoryId = 1 }).Entity; - - var category = new Category - { - Id = 1, - Products = new List - { - new Product { Id = 1 }, - existingProduct, - new Product { Id = 3 } - } - }; - - var traversal = new List(); - - context.ChangeTracker.TrackGraph( - category, e => - { - traversal.Add(NodeString(e)); - e.Entry.State = EntityState.Modified; - }); - - Assert.Equal( - new List - { - " -----> Category:1", - "Category:1 ---Products--> Product:1", - "Category:1 ---Products--> Product:3" - }, - traversal); - - Assert.Equal(4, context.ChangeTracker.Entries().Count()); - - Assert.Equal(EntityState.Modified, context.Entry(category).State); - Assert.Equal(EntityState.Modified, context.Entry(category.Products[0]).State); - Assert.Equal(EntityState.Unchanged, context.Entry(category.Products[1]).State); - Assert.Equal(EntityState.Modified, context.Entry(category.Products[2]).State); - - Assert.Same(category, category.Products[0].Category); - Assert.Same(category, category.Products[1].Category); - Assert.Same(category, category.Products[2].Category); - - Assert.Equal(category.Id, category.Products[0].CategoryId); - Assert.Equal(category.Id, category.Products[1].CategoryId); - Assert.Equal(category.Id, category.Products[2].CategoryId); - } - - [ConditionalFact] - public void Further_graph_traversal_stops_if_an_entity_is_not_attached() - { - using var context = new EarlyLearningCenter(); - var category = new Category - { - Id = 1, - Products = new List - { - new Product - { - Id = 1, - CategoryId = 1, - Details = new ProductDetails { Id = 1 } - }, - new Product - { - Id = 2, - CategoryId = 1, - Details = new ProductDetails { Id = 2 } - }, - new Product - { - Id = 3, - CategoryId = 1, - Details = new ProductDetails { Id = 3 } - } - } - }; - - var traversal = new List(); - - context.ChangeTracker.TrackGraph( - category, e => - { - traversal.Add(NodeString(e)); - if (!(e.Entry.Entity is Product product) - || product.Id != 2) - { - e.Entry.State = EntityState.Unchanged; - } - }); - - Assert.Equal( - new List - { - " -----> Category:1", - "Category:1 ---Products--> Product:1", - "Product:1 ---Details--> ProductDetails:1", - "Category:1 ---Products--> Product:2", - "Category:1 ---Products--> Product:3", - "Product:3 ---Details--> ProductDetails:3" - }, - traversal); - - Assert.Equal(5, context.ChangeTracker.Entries().Count(e => e.State != EntityState.Detached)); - - Assert.Equal(EntityState.Unchanged, context.Entry(category).State); - Assert.Equal(EntityState.Unchanged, context.Entry(category.Products[0]).State); - Assert.Equal(EntityState.Unchanged, context.Entry(category.Products[0].Details).State); - Assert.Equal(EntityState.Detached, context.Entry(category.Products[1]).State); - Assert.Equal(EntityState.Detached, context.Entry(category.Products[1].Details).State); - Assert.Equal(EntityState.Unchanged, context.Entry(category.Products[2]).State); - Assert.Equal(EntityState.Unchanged, context.Entry(category.Products[2].Details).State); - - Assert.Same(category, category.Products[0].Category); - Assert.Null(category.Products[1].Category); - Assert.Same(category, category.Products[2].Category); - - Assert.Equal(category.Id, category.Products[0].CategoryId); - Assert.Equal(category.Id, category.Products[1].CategoryId); - Assert.Equal(category.Id, category.Products[2].CategoryId); - - Assert.Same(category.Products[0], category.Products[0].Details.Product); - Assert.Null(category.Products[1].Details.Product); - Assert.Same(category.Products[2], category.Products[2].Details.Product); - } - - [ConditionalFact] - public void Graph_iterator_does_not_go_visit_Apple() - { - using var context = new EarlyLearningCenter(); - var details = new ProductDetails { Id = 1, Product = new Product { Id = 1 } }; - details.Product.Details = details; - - var traversal = new List(); - - context.ChangeTracker.TrackGraph(details, e => traversal.Add(NodeString(e))); - - Assert.Equal( - new List { " -----> ProductDetails:1" }, - traversal); - - Assert.Equal(0, context.ChangeTracker.Entries().Count(e => e.State != EntityState.Detached)); - } - - [ConditionalFact] - public void Can_attach_parent_with_some_new_and_some_existing_entities() - { - KeyValueAttachTest( - (category, changeTracker) => - { - var traversal = new List(); - - changeTracker.TrackGraph( - category, - e => - { - traversal.Add(NodeString(e)); - e.Entry.State = e.Entry.Entity is Product product && product.Id == 0 - ? EntityState.Added - : EntityState.Unchanged; - }); - - Assert.Equal( - new List - { - " -----> Category:77", - "Category:77 ---Products--> Product:77", - "Category:77 ---Products--> Product:0", - "Category:77 ---Products--> Product:78" - }, - traversal); - }); - } - - [ConditionalFact] - public void Can_attach_graph_using_built_in_tracker() - { - var tracker = new KeyValueEntityTracker(updateExistingEntities: false); - - KeyValueAttachTest((category, changeTracker) => changeTracker.TrackGraph(category, tracker.TrackEntity)); - } - - [ConditionalFact] - public void Can_update_graph_using_built_in_tracker() - { - var tracker = new KeyValueEntityTracker(updateExistingEntities: true); - - KeyValueAttachTest((category, changeTracker) => changeTracker.TrackGraph(category, tracker.TrackEntity), expectModified: true); - } - - private static void KeyValueAttachTest(Action tracker, bool expectModified = false) - { - using var context = new EarlyLearningCenter(); - var category = new Category - { - Id = 77, - Products = new List - { - new Product { Id = 77, CategoryId = expectModified ? 0 : 77 }, - new Product { Id = 0, CategoryId = expectModified ? 0 : 77 }, - new Product { Id = 78, CategoryId = expectModified ? 0 : 77 } - } - }; - - tracker(category, context.ChangeTracker); - - Assert.Equal(4, context.ChangeTracker.Entries().Count()); - - var nonAddedState = expectModified ? EntityState.Modified : EntityState.Unchanged; - - Assert.Equal(nonAddedState, context.Entry(category).State); - Assert.Equal(nonAddedState, context.Entry(category.Products[0]).State); - Assert.Equal(EntityState.Added, context.Entry(category.Products[1]).State); - Assert.Equal(nonAddedState, context.Entry(category.Products[2]).State); - - Assert.Equal(77, category.Products[0].Id); - Assert.Equal(1, category.Products[1].Id); - Assert.Equal(78, category.Products[2].Id); - - Assert.Same(category, category.Products[0].Category); - Assert.Same(category, category.Products[1].Category); - Assert.Same(category, category.Products[2].Category); - - Assert.Equal(category.Id, category.Products[0].CategoryId); - Assert.Equal(category.Id, category.Products[1].CategoryId); - Assert.Equal(category.Id, category.Products[2].CategoryId); - } - - [ConditionalFact] - public void Can_attach_graph_using_custom_delegate() - { - var tracker = new MyTracker(updateExistingEntities: false); - - using var context = new EarlyLearningCenter(); - var category = new Category - { - Id = 77, - Products = new List - { - new Product { Id = 77, CategoryId = 77 }, - new Product { Id = 0, CategoryId = 77 }, - new Product { Id = 78, CategoryId = 77 } - } - }; - - context.ChangeTracker.TrackGraph(category, tracker.TrackEntity); - - Assert.Equal(4, context.ChangeTracker.Entries().Count()); - - Assert.Equal(EntityState.Unchanged, context.Entry(category).State); - Assert.Equal(EntityState.Unchanged, context.Entry(category.Products[0]).State); - Assert.Equal(EntityState.Added, context.Entry(category.Products[1]).State); - Assert.Equal(EntityState.Unchanged, context.Entry(category.Products[2]).State); - - Assert.Equal(77, category.Products[0].Id); - Assert.Equal(777, category.Products[1].Id); - Assert.Equal(78, category.Products[2].Id); - - Assert.Same(category, category.Products[0].Category); - Assert.Same(category, category.Products[1].Category); - Assert.Same(category, category.Products[2].Category); - - Assert.Equal(category.Id, category.Products[0].CategoryId); - Assert.Equal(category.Id, category.Products[1].CategoryId); - Assert.Equal(category.Id, category.Products[2].CategoryId); - } - - private class MyTracker : KeyValueEntityTracker - { - public MyTracker(bool updateExistingEntities) - : base(updateExistingEntities) - { - } - - public override EntityState DetermineState(EntityEntry entry) - { - if (!entry.IsKeySet) - { - entry.GetInfrastructure()[entry.Metadata.FindPrimaryKey().Properties.Single()] = 777; - return EntityState.Added; - } - - return base.DetermineState(entry); - } - } - - [ConditionalTheory] - [InlineData(false, false)] - [InlineData(false, true)] - [InlineData(true, false)] - [InlineData(true, true)] - public void Can_add_owned_dependent_with_reference_to_parent(bool useAdd, bool setDependentKey) - { - using var context = new EarlyLearningCenter(); - var dreams = new Dreams - { - Sweet = new Sweet { Id = 1 }, - Are = new AreMade(), - Made = new AreMade() - }; - - context.Entry(dreams.Sweet).State = EntityState.Unchanged; - - if (setDependentKey) - { - var dreamsEntry = context.Entry(dreams); - dreamsEntry.Property("SweetId").CurrentValue = 1; - dreamsEntry.Reference(e => e.Are).TargetEntry.Property("DreamsSweetId").CurrentValue = 1; - dreamsEntry.Reference(e => e.Made).TargetEntry.Property("DreamsSweetId").CurrentValue = 1; - } - - if (useAdd) - { - context.Add(dreams); - } - else - { - var traversal = new List(); - - context.ChangeTracker.TrackGraph( - dreams, e => - { - e.Entry.State = e.Entry.IsKeySet && !e.Entry.Metadata.IsOwned() - ? EntityState.Unchanged - : EntityState.Added; - - traversal.Add(NodeString(e)); - }); - - Assert.Equal( - new List - { - " -----> Dreams:1", - "Dreams:1 ---Are--> Dreams.Are#AreMade:1", - "Dreams:1 ---Made--> Dreams.Made#AreMade:1" - }, - traversal); - } - - Assert.Equal(4, context.ChangeTracker.Entries().Count()); - - var dependentEntry = context.Entry(dreams); - var dependentEntry2a = context.Entry(dreams.Are); - var dependentEntry2b = context.Entry(dreams.Made); - - var expectedPrincipalState = EntityState.Unchanged; - var expectedDependentState = EntityState.Added; - - Assert.Equal(expectedPrincipalState, context.Entry(dreams.Sweet).State); - Assert.Equal(expectedDependentState, dependentEntry.State); - Assert.Equal(expectedDependentState, dependentEntry2a.State); - Assert.Equal(expectedDependentState, dependentEntry2b.State); - - Assert.Equal(1, dreams.Sweet.Id); - Assert.Equal(1, dependentEntry.Property(dependentEntry.Metadata.FindPrimaryKey().Properties[0].Name).CurrentValue); - Assert.Equal(1, dependentEntry2a.Property(dependentEntry2a.Metadata.FindPrimaryKey().Properties[0].Name).CurrentValue); - Assert.Equal(1, dependentEntry2b.Property(dependentEntry2b.Metadata.FindPrimaryKey().Properties[0].Name).CurrentValue); - } - [ConditionalTheory] // Issue #17828 [InlineData(false)] [InlineData(true)] @@ -2173,161 +1337,6 @@ public void DetectChanges_reparents_even_when_immediate_cascade_enabled(bool del Assert.Equal(EntityState.Modified, context.Entry(child).State); } - [ConditionalTheory] // Issue #12590 - [InlineData(false, false)] - [InlineData(false, true)] - [InlineData(true, false)] - [InlineData(true, true)] - public void Dependents_are_detached_not_deleted_when_principal_is_detached(bool delayCascade, bool trackNewDependents) - { - using var context = new EarlyLearningCenter(); - - var category = new Category - { - Id = 1, - Products = new List - { - new Product { Id = 1 }, - new Product { Id = 2 }, - new Product { Id = 3 } - } - }; - - context.Attach(category); - - var categoryEntry = context.Entry(category); - var product0Entry = context.Entry(category.Products[0]); - var product1Entry = context.Entry(category.Products[1]); - var product2Entry = context.Entry(category.Products[2]); - - Assert.Equal(EntityState.Unchanged, categoryEntry.State); - Assert.Equal(EntityState.Unchanged, product0Entry.State); - Assert.Equal(EntityState.Unchanged, product1Entry.State); - Assert.Equal(EntityState.Unchanged, product2Entry.State); - - if (delayCascade) - { - context.ChangeTracker.CascadeDeleteTiming = CascadeTiming.OnSaveChanges; - } - - context.Entry(category).State = EntityState.Detached; - - Assert.Equal(EntityState.Detached, categoryEntry.State); - - if (delayCascade) - { - Assert.Equal(EntityState.Unchanged, product0Entry.State); - Assert.Equal(EntityState.Unchanged, product1Entry.State); - Assert.Equal(EntityState.Unchanged, product2Entry.State); - } - else - { - Assert.Equal(EntityState.Detached, product0Entry.State); - Assert.Equal(EntityState.Detached, product1Entry.State); - Assert.Equal(EntityState.Detached, product2Entry.State); - } - - var newCategory = new Category { Id = 1, }; - - if (trackNewDependents) - { - newCategory.Products = new List - { - new Product { Id = 1 }, - new Product { Id = 2 }, - new Product { Id = 3 } - }; - } - - var traversal = new List(); - - if (delayCascade && trackNewDependents) - { - Assert.Equal( - CoreStrings.IdentityConflict(nameof(Product), "{'Id'}"), - Assert.Throws(TrackGraph).Message); - } - else - { - TrackGraph(); - - Assert.Equal( - trackNewDependents - ? new List - { - " -----> Category:1", - "Category:1 ---Products--> Product:1", - "Category:1 ---Products--> Product:2", - "Category:1 ---Products--> Product:3" - } - : new List - { - " -----> Category:1" - }, - traversal); - - if (trackNewDependents || delayCascade) - { - Assert.Equal(4, context.ChangeTracker.Entries().Count()); - - categoryEntry = context.Entry(newCategory); - product0Entry = context.Entry(newCategory.Products[0]); - product1Entry = context.Entry(newCategory.Products[1]); - product2Entry = context.Entry(newCategory.Products[2]); - - Assert.Equal(EntityState.Modified, categoryEntry.State); - - if (trackNewDependents) - { - Assert.Equal(EntityState.Modified, product0Entry.State); - Assert.Equal(EntityState.Modified, product1Entry.State); - Assert.Equal(EntityState.Modified, product2Entry.State); - - Assert.NotSame(newCategory.Products[0], category.Products[0]); - Assert.NotSame(newCategory.Products[1], category.Products[1]); - Assert.NotSame(newCategory.Products[2], category.Products[2]); - } - else - { - Assert.Equal(EntityState.Unchanged, product0Entry.State); - Assert.Equal(EntityState.Unchanged, product1Entry.State); - Assert.Equal(EntityState.Unchanged, product2Entry.State); - - Assert.Same(newCategory.Products[0], category.Products[0]); - Assert.Same(newCategory.Products[1], category.Products[1]); - Assert.Same(newCategory.Products[2], category.Products[2]); - } - - Assert.Same(newCategory, newCategory.Products[0].Category); - Assert.Same(newCategory, newCategory.Products[1].Category); - Assert.Same(newCategory, newCategory.Products[2].Category); - - Assert.Equal(newCategory.Id, product0Entry.Property("CategoryId").CurrentValue); - Assert.Equal(newCategory.Id, product1Entry.Property("CategoryId").CurrentValue); - Assert.Equal(newCategory.Id, product2Entry.Property("CategoryId").CurrentValue); - } - else - { - Assert.Single(context.ChangeTracker.Entries()); - - categoryEntry = context.Entry(newCategory); - - Assert.Equal(EntityState.Modified, categoryEntry.State); - Assert.Null(newCategory.Products); - } - } - - void TrackGraph() - { - context.ChangeTracker.TrackGraph( - newCategory, n => - { - n.Entry.State = EntityState.Modified; - traversal.Add(NodeString(n)); - }); - } - } - [ConditionalTheory] // Issue #19203 [InlineData(false, false)] [InlineData(false, true)] @@ -3026,163 +2035,6 @@ public void Explicitly_calling_DetectChanges_works_even_if_auto_DetectChanges_is Assert.Equal(EntityState.Modified, entry.State); } - [ConditionalFact] - public void TrackGraph_does_not_call_DetectChanges() - { - var provider = - InMemoryTestHelpers.Instance.CreateServiceProvider( - new ServiceCollection().AddScoped()); - using var context = new EarlyLearningCenter(provider); - var changeDetector = (ChangeDetectorProxy)context.GetService(); - - changeDetector.DetectChangesCalled = false; - - context.ChangeTracker.TrackGraph(CreateSimpleGraph(2), e => e.Entry.State = EntityState.Unchanged); - - Assert.False(changeDetector.DetectChangesCalled); - - context.ChangeTracker.DetectChanges(); - - Assert.True(changeDetector.DetectChangesCalled); - } - - [ConditionalFact] - public void TrackGraph_overload_can_visit_an_already_attached_graph() - { - using var context = new EarlyLearningCenter(); - var category = new Category - { - Id = 1, - Products = new List - { - new Product - { - Id = 1, - CategoryId = 1, - Details = new ProductDetails { Id = 1 } - }, - new Product - { - Id = 2, - CategoryId = 1, - Details = new ProductDetails { Id = 2 } - }, - new Product - { - Id = 3, - CategoryId = 1, - Details = new ProductDetails { Id = 3 } - } - } - }; - - context.Attach(category); - - var visited = new HashSet(); - var traversal = new List(); - - context.ChangeTracker.TrackGraph( - category, visited, e => - { - if (e.NodeState.Contains(e.Entry.Entity)) - { - return false; - } - - e.NodeState.Add(e.Entry.Entity); - - traversal.Add(NodeString(e)); - - return true; - }); - - Assert.Equal( - new List - { - " -----> Category:1", - "Category:1 ---Products--> Product:1", - "Product:1 ---Details--> ProductDetails:1", - "Category:1 ---Products--> Product:2", - "Product:2 ---Details--> ProductDetails:2", - "Category:1 ---Products--> Product:3", - "Product:3 ---Details--> ProductDetails:3" - }, - traversal); - - Assert.Equal(7, visited.Count); - } - - [ConditionalFact] - public void TrackGraph_overload_can_visit_a_graph_without_attaching() - { - using var context = new EarlyLearningCenter(); - var category = new Category - { - Id = 1, - Products = new List - { - new Product - { - Id = 1, - CategoryId = 1, - Details = new ProductDetails { Id = 1 } - }, - new Product - { - Id = 2, - CategoryId = 1, - Details = new ProductDetails { Id = 2 } - }, - new Product - { - Id = 3, - CategoryId = 1, - Details = new ProductDetails { Id = 3 } - } - } - }; - - var visited = new HashSet(); - var traversal = new List(); - - context.ChangeTracker.TrackGraph( - category, visited, e => - { - if (e.NodeState.Contains(e.Entry.Entity)) - { - return false; - } - - e.NodeState.Add(e.Entry.Entity); - - traversal.Add(NodeString(e)); - - return true; - }); - - Assert.Equal( - new List - { - " -----> Category:1", - "Category:1 ---Products--> Product:1", - "Product:1 ---Details--> ProductDetails:1", - "Category:1 ---Products--> Product:2", - "Product:2 ---Details--> ProductDetails:2", - "Category:1 ---Products--> Product:3", - "Product:3 ---Details--> ProductDetails:3" - }, - traversal); - - Assert.Equal(7, visited.Count); - - foreach (var entity in new object[] { category } - .Concat(category.Products) - .Concat(category.Products.Select(e => e.Details))) - { - Assert.Equal(EntityState.Detached, context.Entry(entity).State); - } - } - [ConditionalFact] public void Does_not_throw_when_instance_of_unmapped_derived_type_is_used() { @@ -3282,35 +2134,6 @@ private class Dark { } - private static Product CreateSimpleGraph(int id) - => new Product { Id = id, Category = new Category { Id = id } }; - - private class ChangeDetectorProxy : ChangeDetector - { - public ChangeDetectorProxy( - IDiagnosticsLogger logger, - ILoggingOptions loggingOptions) - : base(logger, loggingOptions) - { - } - - public bool DetectChangesCalled { get; set; } - - public override void DetectChanges(InternalEntityEntry entry) - { - DetectChangesCalled = true; - - base.DetectChanges(entry); - } - - public override void DetectChanges(IStateManager stateManager) - { - DetectChangesCalled = true; - - base.DetectChanges(stateManager); - } - } - private class Category { public int Id { get; set; } @@ -3394,24 +2217,6 @@ private class OrderDetails public Product Product { get; set; } } - private class NullbileCategory - { - public List Products { get; set; } - public NullbileCategoryInfo Info { get; set; } - } - - private class NullbileCategoryInfo - { - // ReSharper disable once MemberHidesStaticFromOuterClass - public NullbileCategory Category { get; set; } - } - - private class NullbileProduct - { - // ReSharper disable once MemberHidesStaticFromOuterClass - public NullbileCategory Category { get; set; } - } - private class Sweet { public int? Id { get; set; } @@ -3455,34 +2260,6 @@ public EarlyLearningCenter(IServiceProvider serviceProvider) protected internal override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder - .Entity( - b => - { - b.Property("Id"); - b.Property("CategoryId"); - b.HasKey("Id"); - }); - - modelBuilder - .Entity( - b => - { - b.Property("Id"); - b.Property("CategoryId"); - b.HasKey("Id"); - }); - - modelBuilder - .Entity( - b => - { - b.Property("Id"); - b.HasKey("Id"); - b.HasMany(e => e.Products).WithOne(e => e.Category).HasForeignKey("CategoryId"); - b.HasOne(e => e.Info).WithOne(e => e.Category).HasForeignKey("CategoryId"); - }); - modelBuilder.Entity().OwnsOne( e => e.Dreams, b => { diff --git a/test/EFCore.Tests/ChangeTracking/TrackGraphTestBase.cs b/test/EFCore.Tests/ChangeTracking/TrackGraphTestBase.cs new file mode 100644 index 00000000000..f9bbc637a0a --- /dev/null +++ b/test/EFCore.Tests/ChangeTracking/TrackGraphTestBase.cs @@ -0,0 +1,1400 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.ChangeTracking +{ + public abstract class TrackGraphTestBase + { + public class TrackGraphTest : TrackGraphTestBase + { + protected override IList TrackGraph(DbContext context, object root, Action callback) + { + var traversal = new List(); + + context.ChangeTracker.TrackGraph(root, node => + { + callback(node); + + traversal.Add(NodeString(node)); + }); + + return traversal; + } + } + + public class TrackGraphTestWithState : TrackGraphTestBase + { + protected override IList TrackGraph(DbContext context, object root, Action callback) + { + var traversal = new List(); + + context.ChangeTracker.TrackGraph( + root, + default, + node => + { + if (node.Entry.State != EntityState.Detached) + { + return false; + } + + callback(node); + + traversal.Add(NodeString(node)); + + return node.Entry.State != EntityState.Detached; + }); + + return traversal; + } + } + + protected abstract IList TrackGraph( + DbContext context, + object root, + Action callback); + + private static string NodeString(EntityEntryGraphNode node) + => EntryString(node.SourceEntry) + + " ---" + + node.InboundNavigation?.Name + + "--> " + + EntryString(node.Entry); + + private static string EntryString(EntityEntry entry) + => entry == null + ? "" + : entry.Metadata.DisplayName() + + ":" + + entry.Property(entry.Metadata.FindPrimaryKey().Properties[0].Name).CurrentValue; + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void Can_attach_nullable_PK_parent_with_child_collection(bool useAttach, bool setKeys) + { + using var context = new EarlyLearningCenter(GetType().Name); + var category = new NullbileCategory + { + Products = new List + { + new NullbileProduct(), + new NullbileProduct(), + new NullbileProduct() + } + }; + + if (setKeys) + { + context.Entry(category).Property("Id").CurrentValue = 1; + context.Entry(category.Products[0]).Property("Id").CurrentValue = 1; + context.Entry(category.Products[1]).Property("Id").CurrentValue = 2; + context.Entry(category.Products[2]).Property("Id").CurrentValue = 3; + } + + if (useAttach) + { + context.Attach(category); + } + else + { + Assert.Equal( + new List + { + " -----> NullbileCategory:1", + "NullbileCategory:1 ---Products--> NullbileProduct:1", + "NullbileCategory:1 ---Products--> NullbileProduct:2", + "NullbileCategory:1 ---Products--> NullbileProduct:3" + }, + TrackGraph( + context, + category, node => node.Entry.State = node.Entry.IsKeySet ? EntityState.Unchanged : EntityState.Added)); + } + + Assert.Equal(4, context.ChangeTracker.Entries().Count()); + + var categoryEntry = context.Entry(category); + var product0Entry = context.Entry(category.Products[0]); + var product1Entry = context.Entry(category.Products[1]); + var product2Entry = context.Entry(category.Products[2]); + + var expectedState = setKeys ? EntityState.Unchanged : EntityState.Added; + Assert.Equal(expectedState, categoryEntry.State); + Assert.Equal(expectedState, product0Entry.State); + Assert.Equal(expectedState, product1Entry.State); + Assert.Equal(expectedState, product2Entry.State); + + Assert.Same(category, category.Products[0].Category); + Assert.Same(category, category.Products[1].Category); + Assert.Same(category, category.Products[2].Category); + + var categoryId = categoryEntry.Property("Id").CurrentValue; + Assert.NotNull(categoryId); + + Assert.Equal(categoryId, product0Entry.Property("CategoryId").CurrentValue); + Assert.Equal(categoryId, product1Entry.Property("CategoryId").CurrentValue); + Assert.Equal(categoryId, product2Entry.Property("CategoryId").CurrentValue); + } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void Can_attach_nullable_PK_parent_with_one_to_one_children(bool useAttach, bool setKeys) + { + using var context = new EarlyLearningCenter(GetType().Name); + var category = new NullbileCategory { Info = new NullbileCategoryInfo() }; + + if (setKeys) + { + context.Entry(category).Property("Id").CurrentValue = 1; + context.Entry(category.Info).Property("Id").CurrentValue = 1; + } + + if (useAttach) + { + context.Attach(category); + } + else + { + Assert.Equal( + new List { " -----> NullbileCategory:1", "NullbileCategory:1 ---Info--> NullbileCategoryInfo:1" }, + TrackGraph( + context, + category, node => node.Entry.State = node.Entry.IsKeySet ? EntityState.Unchanged : EntityState.Added)); + } + + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + + var expectedState = setKeys ? EntityState.Unchanged : EntityState.Added; + Assert.Equal(expectedState, context.Entry(category).State); + Assert.Equal(expectedState, context.Entry(category.Info).State); + + Assert.Same(category, category.Info.Category); + } + + [ConditionalTheory] + [InlineData(false, false, false)] + [InlineData(false, true, false)] + [InlineData(true, false, false)] + [InlineData(true, true, false)] + [InlineData(false, false, true)] + [InlineData(false, true, true)] + [InlineData(true, false, true)] + [InlineData(true, true, true)] + public void Can_attach_parent_with_owned_dependent(bool useAttach, bool setPrincipalKey, bool setDependentKey) + { + using var context = new EarlyLearningCenter(GetType().Name); + var sweet = new Sweet { Dreams = new Dreams { Are = new AreMade(), Made = new AreMade() } }; + + if (setPrincipalKey) + { + sweet.Id = 1; + } + + if (setDependentKey) + { + var dreamsEntry = context.Entry(sweet).Reference(e => e.Dreams).TargetEntry; + dreamsEntry.Property("SweetId").CurrentValue = 1; + dreamsEntry.Reference(e => e.Are).TargetEntry.Property("DreamsSweetId").CurrentValue = 1; + dreamsEntry.Reference(e => e.Made).TargetEntry.Property("DreamsSweetId").CurrentValue = 1; + } + + if (useAttach) + { + context.Attach(sweet); + } + else + { + Assert.Equal( + new List + { + " -----> Sweet:1", + "Sweet:1 ---Dreams--> Dreams:1", + "Dreams:1 ---Are--> Dreams.Are#AreMade:1", + "Dreams:1 ---Made--> Dreams.Made#AreMade:1" + }, + TrackGraph( + context, + sweet, + node => node.Entry.State = node.Entry.Metadata.IsOwned() + ? node.SourceEntry.State + : node.Entry.IsKeySet + ? EntityState.Unchanged + : EntityState.Added)); + } + + Assert.Equal(4, context.ChangeTracker.Entries().Count()); + + var dependentEntry = context.Entry(sweet.Dreams); + var dependentEntry2a = context.Entry(sweet.Dreams.Are); + var dependentEntry2b = context.Entry(sweet.Dreams.Made); + + var expectedPrincipalState = setPrincipalKey ? EntityState.Unchanged : EntityState.Added; + var expectedDependentState = setPrincipalKey || (setDependentKey && useAttach) ? EntityState.Unchanged : EntityState.Added; + + Assert.Equal(expectedPrincipalState, context.Entry(sweet).State); + Assert.Equal(expectedDependentState, dependentEntry.State); + Assert.Equal(expectedDependentState, dependentEntry2a.State); + Assert.Equal(expectedDependentState, dependentEntry2b.State); + + Assert.Equal(1, sweet.Id); + Assert.Equal(1, dependentEntry.Property(dependentEntry.Metadata.FindPrimaryKey().Properties[0].Name).CurrentValue); + Assert.Equal(1, dependentEntry2a.Property(dependentEntry2a.Metadata.FindPrimaryKey().Properties[0].Name).CurrentValue); + Assert.Equal(1, dependentEntry2b.Property(dependentEntry2b.Metadata.FindPrimaryKey().Properties[0].Name).CurrentValue); + } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void Can_attach_owned_dependent_with_reference_to_parent(bool useAttach, bool setDependentKey) + { + using var context = new EarlyLearningCenter(GetType().Name); + var dreams = new Dreams + { + Sweet = new Sweet { Id = 1 }, + Are = new AreMade(), + Made = new AreMade() + }; + + if (setDependentKey) + { + var dreamsEntry = context.Entry(dreams); + dreamsEntry.Property("SweetId").CurrentValue = 1; + dreamsEntry.Reference(e => e.Are).TargetEntry.Property("DreamsSweetId").CurrentValue = 1; + dreamsEntry.Reference(e => e.Made).TargetEntry.Property("DreamsSweetId").CurrentValue = 1; + } + + if (useAttach) + { + context.Attach(dreams); + } + else + { + Assert.Equal( + new List + { + " -----> Dreams:1", + "Dreams:1 ---Are--> Dreams.Are#AreMade:1", + "Dreams:1 ---Made--> Dreams.Made#AreMade:1", + "Dreams:1 ---Sweet--> Sweet:1" + }, + TrackGraph( + context, + dreams, + node => node.Entry.State = node.Entry.IsKeySet ? EntityState.Unchanged : EntityState.Added)); + } + + Assert.Equal(4, context.ChangeTracker.Entries().Count()); + + var dependentEntry = context.Entry(dreams); + var dependentEntry2a = context.Entry(dreams.Are); + var dependentEntry2b = context.Entry(dreams.Made); + + var expectedPrincipalState = EntityState.Unchanged; + var expectedDependentState = setDependentKey ? EntityState.Unchanged : EntityState.Added; + + Assert.Equal(expectedPrincipalState, context.Entry(dreams.Sweet).State); + Assert.Equal(expectedDependentState, dependentEntry.State); + Assert.Equal(expectedDependentState, dependentEntry2a.State); + Assert.Equal(expectedDependentState, dependentEntry2b.State); + + Assert.Equal(1, dreams.Sweet.Id); + Assert.Equal(1, dependentEntry.Property(dependentEntry.Metadata.FindPrimaryKey().Properties[0].Name).CurrentValue); + Assert.Equal(1, dependentEntry2a.Property(dependentEntry2a.Metadata.FindPrimaryKey().Properties[0].Name).CurrentValue); + Assert.Equal(1, dependentEntry2b.Property(dependentEntry2b.Metadata.FindPrimaryKey().Properties[0].Name).CurrentValue); + } + [ConditionalFact] + public void Can_attach_parent_with_child_collection() + { + using var context = new EarlyLearningCenter(GetType().Name); + var category = new Category + { + Id = 1, + Products = new List + { + new Product { Id = 1 }, + new Product { Id = 2 }, + new Product { Id = 3 } + } + }; + + Assert.Equal( + new List + { + " -----> Category:1", + "Category:1 ---Products--> Product:1", + "Category:1 ---Products--> Product:2", + "Category:1 ---Products--> Product:3" + }, + TrackGraph( + context, + category, + node => node.Entry.State = EntityState.Modified)); + + Assert.Equal(4, context.ChangeTracker.Entries().Count()); + + Assert.Equal(EntityState.Modified, context.Entry(category).State); + Assert.Equal(EntityState.Modified, context.Entry(category.Products[0]).State); + Assert.Equal(EntityState.Modified, context.Entry(category.Products[1]).State); + Assert.Equal(EntityState.Modified, context.Entry(category.Products[2]).State); + + Assert.Same(category, category.Products[0].Category); + Assert.Same(category, category.Products[1].Category); + Assert.Same(category, category.Products[2].Category); + + Assert.Equal(category.Id, category.Products[0].CategoryId); + Assert.Equal(category.Id, category.Products[1].CategoryId); + Assert.Equal(category.Id, category.Products[2].CategoryId); + } + + [ConditionalFact] + public void Can_attach_child_with_reference_to_parent() + { + using var context = new EarlyLearningCenter(GetType().Name); + var product = new Product { Id = 1, Category = new Category { Id = 1 } }; + + Assert.Equal( + new List { " -----> Product:1", "Product:1 ---Category--> Category:1" }, + TrackGraph( + context, + product, + node => node.Entry.State = EntityState.Modified)); + + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + + Assert.Equal(EntityState.Modified, context.Entry(product).State); + Assert.Equal(EntityState.Modified, context.Entry(product.Category).State); + + Assert.Same(product, product.Category.Products[0]); + Assert.Equal(product.Category.Id, product.CategoryId); + } + + [ConditionalFact] + public void Can_attach_parent_with_one_to_one_children() + { + using var context = new EarlyLearningCenter(GetType().Name); + var product = new Product { Id = 1, Details = new ProductDetails { Id = 1, Tag = new ProductDetailsTag { Id = 1 } } }; + + Assert.Equal( + new List + { + " -----> Product:1", + "Product:1 ---Details--> ProductDetails:1", + "ProductDetails:1 ---Tag--> ProductDetailsTag:1" + }, + TrackGraph( + context, + product, + node => node.Entry.State = EntityState.Unchanged)); + + Assert.Equal(3, context.ChangeTracker.Entries().Count()); + + Assert.Equal(EntityState.Unchanged, context.Entry(product).State); + Assert.Equal(EntityState.Unchanged, context.Entry(product.Details).State); + Assert.Equal(EntityState.Unchanged, context.Entry(product.Details.Tag).State); + + Assert.Same(product, product.Details.Product); + Assert.Same(product.Details, product.Details.Tag.Details); + } + + [ConditionalFact] + public void Can_attach_child_with_one_to_one_parents() + { + using var context = new EarlyLearningCenter(GetType().Name); + var tag = new ProductDetailsTag { Id = 1, Details = new ProductDetails { Id = 1, Product = new Product { Id = 1 } } }; + + Assert.Equal( + new List + { + " -----> ProductDetailsTag:1", + "ProductDetailsTag:1 ---Details--> ProductDetails:1", + "ProductDetails:1 ---Product--> Product:1" + }, + TrackGraph( + context, + tag, + node => node.Entry.State = EntityState.Unchanged)); + + Assert.Equal(3, context.ChangeTracker.Entries().Count()); + + Assert.Equal(EntityState.Unchanged, context.Entry(tag).State); + Assert.Equal(EntityState.Unchanged, context.Entry(tag.Details).State); + Assert.Equal(EntityState.Unchanged, context.Entry(tag.Details.Product).State); + + Assert.Same(tag, tag.Details.Tag); + Assert.Same(tag.Details, tag.Details.Product.Details); + } + + [ConditionalFact] + public void Can_attach_entity_with_one_to_one_parent_and_child() + { + using var context = new EarlyLearningCenter(GetType().Name); + var details = new ProductDetails + { + Id = 1, + Product = new Product { Id = 1 }, + Tag = new ProductDetailsTag { Id = 1 } + }; + + Assert.Equal( + new List + { + " -----> ProductDetails:1", + "ProductDetails:1 ---Product--> Product:1", + "ProductDetails:1 ---Tag--> ProductDetailsTag:1" + }, + TrackGraph( + context, + details, + node => node.Entry.State = EntityState.Unchanged)); + + Assert.Equal(3, context.ChangeTracker.Entries().Count()); + + Assert.Equal(EntityState.Unchanged, context.Entry(details).State); + Assert.Equal(EntityState.Unchanged, context.Entry(details.Product).State); + Assert.Equal(EntityState.Unchanged, context.Entry(details.Tag).State); + + Assert.Same(details, details.Tag.Details); + Assert.Same(details, details.Product.Details); + } + + [ConditionalFact] + public void Entities_that_are_already_tracked_will_not_get_attached() + { + using var context = new EarlyLearningCenter(GetType().Name); + var existingProduct = context.Attach( + new Product { Id = 2, CategoryId = 1 }).Entity; + + var category = new Category + { + Id = 1, + Products = new List + { + new Product { Id = 1 }, + existingProduct, + new Product { Id = 3 } + } + }; + + Assert.Equal( + new List + { + " -----> Category:1", + "Category:1 ---Products--> Product:1", + "Category:1 ---Products--> Product:3" + }, + TrackGraph( + context, + category, + node => node.Entry.State = EntityState.Modified)); + + Assert.Equal(4, context.ChangeTracker.Entries().Count()); + + Assert.Equal(EntityState.Modified, context.Entry(category).State); + Assert.Equal(EntityState.Modified, context.Entry(category.Products[0]).State); + Assert.Equal(EntityState.Unchanged, context.Entry(category.Products[1]).State); + Assert.Equal(EntityState.Modified, context.Entry(category.Products[2]).State); + + Assert.Same(category, category.Products[0].Category); + Assert.Same(category, category.Products[1].Category); + Assert.Same(category, category.Products[2].Category); + + Assert.Equal(category.Id, category.Products[0].CategoryId); + Assert.Equal(category.Id, category.Products[1].CategoryId); + Assert.Equal(category.Id, category.Products[2].CategoryId); + } + + [ConditionalFact] + public void Further_graph_traversal_stops_if_an_entity_is_not_attached() + { + using var context = new EarlyLearningCenter(GetType().Name); + var category = new Category + { + Id = 1, + Products = new List + { + new Product + { + Id = 1, + CategoryId = 1, + Details = new ProductDetails { Id = 1 } + }, + new Product + { + Id = 2, + CategoryId = 1, + Details = new ProductDetails { Id = 2 } + }, + new Product + { + Id = 3, + CategoryId = 1, + Details = new ProductDetails { Id = 3 } + } + } + }; + + Assert.Equal( + new List + { + " -----> Category:1", + "Category:1 ---Products--> Product:1", + "Product:1 ---Details--> ProductDetails:1", + "Category:1 ---Products--> Product:2", + "Category:1 ---Products--> Product:3", + "Product:3 ---Details--> ProductDetails:3" + }, + TrackGraph( + context, + category, + node => + { + if (!(node.Entry.Entity is Product product) + || product.Id != 2) + { + node.Entry.State = EntityState.Unchanged; + } + })); + + Assert.Equal(5, context.ChangeTracker.Entries().Count(e => e.State != EntityState.Detached)); + + Assert.Equal(EntityState.Unchanged, context.Entry(category).State); + Assert.Equal(EntityState.Unchanged, context.Entry(category.Products[0]).State); + Assert.Equal(EntityState.Unchanged, context.Entry(category.Products[0].Details).State); + Assert.Equal(EntityState.Detached, context.Entry(category.Products[1]).State); + Assert.Equal(EntityState.Detached, context.Entry(category.Products[1].Details).State); + Assert.Equal(EntityState.Unchanged, context.Entry(category.Products[2]).State); + Assert.Equal(EntityState.Unchanged, context.Entry(category.Products[2].Details).State); + + Assert.Same(category, category.Products[0].Category); + Assert.Null(category.Products[1].Category); + Assert.Same(category, category.Products[2].Category); + + Assert.Equal(category.Id, category.Products[0].CategoryId); + Assert.Equal(category.Id, category.Products[1].CategoryId); + Assert.Equal(category.Id, category.Products[2].CategoryId); + + Assert.Same(category.Products[0], category.Products[0].Details.Product); + Assert.Null(category.Products[1].Details.Product); + Assert.Same(category.Products[2], category.Products[2].Details.Product); + } + + [ConditionalFact] + public void Graph_iterator_does_not_go_visit_Apple() + { + using var context = new EarlyLearningCenter(GetType().Name); + var details = new ProductDetails { Id = 1, Product = new Product { Id = 1 } }; + details.Product.Details = details; + + Assert.Equal( + new List { " -----> ProductDetails:1" }, + TrackGraph( + context, + details, + e => { })); + + Assert.Equal(0, context.ChangeTracker.Entries().Count(e => e.State != EntityState.Detached)); + } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void Can_add_owned_dependent_with_reference_to_parent(bool useAdd, bool setDependentKey) + { + using var context = new EarlyLearningCenter(GetType().Name); + var dreams = new Dreams + { + Sweet = new Sweet { Id = 1 }, + Are = new AreMade(), + Made = new AreMade() + }; + + context.Entry(dreams.Sweet).State = EntityState.Unchanged; + + if (setDependentKey) + { + var dreamsEntry = context.Entry(dreams); + dreamsEntry.Property("SweetId").CurrentValue = 1; + dreamsEntry.Reference(e => e.Are).TargetEntry.Property("DreamsSweetId").CurrentValue = 1; + dreamsEntry.Reference(e => e.Made).TargetEntry.Property("DreamsSweetId").CurrentValue = 1; + } + + if (useAdd) + { + context.Add(dreams); + } + else + { + Assert.Equal( + new List + { + " -----> Dreams:1", + "Dreams:1 ---Are--> Dreams.Are#AreMade:1", + "Dreams:1 ---Made--> Dreams.Made#AreMade:1" + }, + TrackGraph( + context, + dreams, + node => node.Entry.State = node.Entry.IsKeySet && !node.Entry.Metadata.IsOwned() + ? EntityState.Unchanged + : EntityState.Added)); + } + + Assert.Equal(4, context.ChangeTracker.Entries().Count()); + + var dependentEntry = context.Entry(dreams); + var dependentEntry2a = context.Entry(dreams.Are); + var dependentEntry2b = context.Entry(dreams.Made); + + var expectedPrincipalState = EntityState.Unchanged; + var expectedDependentState = EntityState.Added; + + Assert.Equal(expectedPrincipalState, context.Entry(dreams.Sweet).State); + Assert.Equal(expectedDependentState, dependentEntry.State); + Assert.Equal(expectedDependentState, dependentEntry2a.State); + Assert.Equal(expectedDependentState, dependentEntry2b.State); + + Assert.Equal(1, dreams.Sweet.Id); + Assert.Equal(1, dependentEntry.Property(dependentEntry.Metadata.FindPrimaryKey().Properties[0].Name).CurrentValue); + Assert.Equal(1, dependentEntry2a.Property(dependentEntry2a.Metadata.FindPrimaryKey().Properties[0].Name).CurrentValue); + Assert.Equal(1, dependentEntry2b.Property(dependentEntry2b.Metadata.FindPrimaryKey().Properties[0].Name).CurrentValue); + } + + [ConditionalTheory] // Issue #12590 + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void Dependents_are_detached_not_deleted_when_principal_is_detached(bool delayCascade, bool trackNewDependents) + { + using var context = new EarlyLearningCenter(GetType().Name); + + var category = new Category + { + Id = 1, + Products = new List + { + new Product { Id = 1 }, + new Product { Id = 2 }, + new Product { Id = 3 } + } + }; + + context.Attach(category); + + var categoryEntry = context.Entry(category); + var product0Entry = context.Entry(category.Products[0]); + var product1Entry = context.Entry(category.Products[1]); + var product2Entry = context.Entry(category.Products[2]); + + Assert.Equal(EntityState.Unchanged, categoryEntry.State); + Assert.Equal(EntityState.Unchanged, product0Entry.State); + Assert.Equal(EntityState.Unchanged, product1Entry.State); + Assert.Equal(EntityState.Unchanged, product2Entry.State); + + if (delayCascade) + { + context.ChangeTracker.CascadeDeleteTiming = CascadeTiming.OnSaveChanges; + } + + context.Entry(category).State = EntityState.Detached; + + Assert.Equal(EntityState.Detached, categoryEntry.State); + + if (delayCascade) + { + Assert.Equal(EntityState.Unchanged, product0Entry.State); + Assert.Equal(EntityState.Unchanged, product1Entry.State); + Assert.Equal(EntityState.Unchanged, product2Entry.State); + } + else + { + Assert.Equal(EntityState.Detached, product0Entry.State); + Assert.Equal(EntityState.Detached, product1Entry.State); + Assert.Equal(EntityState.Detached, product2Entry.State); + } + + var newCategory = new Category { Id = 1, }; + + if (trackNewDependents) + { + newCategory.Products = new List + { + new Product { Id = 1 }, + new Product { Id = 2 }, + new Product { Id = 3 } + }; + } + + if (delayCascade && trackNewDependents) + { + Assert.Equal( + CoreStrings.IdentityConflict(nameof(Product), "{'Id'}"), + Assert.Throws(TrackGraph).Message); + } + else + { + Assert.Equal( + trackNewDependents + ? new List + { + " -----> Category:1", + "Category:1 ---Products--> Product:1", + "Category:1 ---Products--> Product:2", + "Category:1 ---Products--> Product:3" + } + : new List + { + " -----> Category:1" + }, + TrackGraph()); + + if (trackNewDependents || delayCascade) + { + Assert.Equal(4, context.ChangeTracker.Entries().Count()); + + categoryEntry = context.Entry(newCategory); + product0Entry = context.Entry(newCategory.Products[0]); + product1Entry = context.Entry(newCategory.Products[1]); + product2Entry = context.Entry(newCategory.Products[2]); + + Assert.Equal(EntityState.Modified, categoryEntry.State); + + if (trackNewDependents) + { + Assert.Equal(EntityState.Modified, product0Entry.State); + Assert.Equal(EntityState.Modified, product1Entry.State); + Assert.Equal(EntityState.Modified, product2Entry.State); + + Assert.NotSame(newCategory.Products[0], category.Products[0]); + Assert.NotSame(newCategory.Products[1], category.Products[1]); + Assert.NotSame(newCategory.Products[2], category.Products[2]); + } + else + { + Assert.Equal(EntityState.Unchanged, product0Entry.State); + Assert.Equal(EntityState.Unchanged, product1Entry.State); + Assert.Equal(EntityState.Unchanged, product2Entry.State); + + Assert.Same(newCategory.Products[0], category.Products[0]); + Assert.Same(newCategory.Products[1], category.Products[1]); + Assert.Same(newCategory.Products[2], category.Products[2]); + } + + Assert.Same(newCategory, newCategory.Products[0].Category); + Assert.Same(newCategory, newCategory.Products[1].Category); + Assert.Same(newCategory, newCategory.Products[2].Category); + + Assert.Equal(newCategory.Id, product0Entry.Property("CategoryId").CurrentValue); + Assert.Equal(newCategory.Id, product1Entry.Property("CategoryId").CurrentValue); + Assert.Equal(newCategory.Id, product2Entry.Property("CategoryId").CurrentValue); + } + else + { + Assert.Single(context.ChangeTracker.Entries()); + + categoryEntry = context.Entry(newCategory); + + Assert.Equal(EntityState.Modified, categoryEntry.State); + Assert.Null(newCategory.Products); + } + } + + IList TrackGraph() + { + return this.TrackGraph( + context, + newCategory, + node => node.Entry.State = EntityState.Modified); + } + } + + [ConditionalFact] + public void TrackGraph_overload_can_visit_a_graph_without_attaching() + { + using var context = new EarlyLearningCenter(GetType().Name); + var category = new Category + { + Id = 1, + Products = new List + { + new Product + { + Id = 1, + CategoryId = 1, + Details = new ProductDetails { Id = 1 } + }, + new Product + { + Id = 2, + CategoryId = 1, + Details = new ProductDetails { Id = 2 } + }, + new Product + { + Id = 3, + CategoryId = 1, + Details = new ProductDetails { Id = 3 } + } + } + }; + + var visited = new HashSet(); + var traversal = new List(); + + context.ChangeTracker.TrackGraph( + category, + visited, + node => + { + if (node.NodeState.Contains(node.Entry.Entity)) + { + return false; + } + + node.NodeState.Add(node.Entry.Entity); + + traversal.Add(NodeString(node)); + + return true; + }); + + Assert.Equal( + new List + { + " -----> Category:1", + "Category:1 ---Products--> Product:1", + "Product:1 ---Details--> ProductDetails:1", + "Category:1 ---Products--> Product:2", + "Product:2 ---Details--> ProductDetails:2", + "Category:1 ---Products--> Product:3", + "Product:3 ---Details--> ProductDetails:3" + }, + traversal); + + Assert.Equal(7, visited.Count); + + foreach (var entity in new object[] { category } + .Concat(category.Products) + .Concat(category.Products.Select(e => e.Details))) + { + Assert.Equal(EntityState.Detached, context.Entry(entity).State); + } + } + + [ConditionalFact] + public void Can_attach_parent_with_some_new_and_some_existing_entities() + { + KeyValueAttachTest( + GetType().Name, + (category, changeTracker) => + { + Assert.Equal( + new List + { + " -----> Category:77", + "Category:77 ---Products--> Product:77", + "Category:77 ---Products--> Product:1", + "Category:77 ---Products--> Product:78" + }, + TrackGraph( + changeTracker.Context, + category, + node => node.Entry.State = node.Entry.Entity is Product product && product.Id == 0 + ? EntityState.Added + : EntityState.Unchanged)); + }); + } + + [ConditionalFact] + public void Can_attach_graph_using_built_in_tracker() + { + var tracker = new KeyValueEntityTracker(updateExistingEntities: false); + + KeyValueAttachTest( + GetType().Name, + (category, changeTracker) => changeTracker.TrackGraph(category, tracker.TrackEntity)); + } + + [ConditionalFact] + public void Can_update_graph_using_built_in_tracker() + { + var tracker = new KeyValueEntityTracker(updateExistingEntities: true); + + KeyValueAttachTest( + GetType().Name, + (category, changeTracker) => changeTracker.TrackGraph(category, tracker.TrackEntity), + expectModified: true); + } + + private static void KeyValueAttachTest(string databaseName, Action tracker, bool expectModified = false) + { + using var context = new EarlyLearningCenter(databaseName); + var category = new Category + { + Id = 77, + Products = new List + { + new Product { Id = 77, CategoryId = expectModified ? 0 : 77 }, + new Product { Id = 0, CategoryId = expectModified ? 0 : 77 }, + new Product { Id = 78, CategoryId = expectModified ? 0 : 77 } + } + }; + + tracker(category, context.ChangeTracker); + + Assert.Equal(4, context.ChangeTracker.Entries().Count()); + + var nonAddedState = expectModified ? EntityState.Modified : EntityState.Unchanged; + + Assert.Equal(nonAddedState, context.Entry(category).State); + Assert.Equal(nonAddedState, context.Entry(category.Products[0]).State); + Assert.Equal(EntityState.Added, context.Entry(category.Products[1]).State); + Assert.Equal(nonAddedState, context.Entry(category.Products[2]).State); + + Assert.Equal(77, category.Products[0].Id); + Assert.Equal(1, category.Products[1].Id); + Assert.Equal(78, category.Products[2].Id); + + Assert.Same(category, category.Products[0].Category); + Assert.Same(category, category.Products[1].Category); + Assert.Same(category, category.Products[2].Category); + + Assert.Equal(category.Id, category.Products[0].CategoryId); + Assert.Equal(category.Id, category.Products[1].CategoryId); + Assert.Equal(category.Id, category.Products[2].CategoryId); + } + + [ConditionalFact] + public void Can_attach_graph_using_custom_delegate() + { + var tracker = new MyTracker(updateExistingEntities: false); + + using var context = new EarlyLearningCenter(GetType().Name); + var category = new Category + { + Id = 77, + Products = new List + { + new Product { Id = 77, CategoryId = 77 }, + new Product { Id = 0, CategoryId = 77 }, + new Product { Id = 78, CategoryId = 77 } + } + }; + + context.ChangeTracker.TrackGraph(category, tracker.TrackEntity); + + Assert.Equal(4, context.ChangeTracker.Entries().Count()); + + Assert.Equal(EntityState.Unchanged, context.Entry(category).State); + Assert.Equal(EntityState.Unchanged, context.Entry(category.Products[0]).State); + Assert.Equal(EntityState.Added, context.Entry(category.Products[1]).State); + Assert.Equal(EntityState.Unchanged, context.Entry(category.Products[2]).State); + + Assert.Equal(77, category.Products[0].Id); + Assert.Equal(777, category.Products[1].Id); + Assert.Equal(78, category.Products[2].Id); + + Assert.Same(category, category.Products[0].Category); + Assert.Same(category, category.Products[1].Category); + Assert.Same(category, category.Products[2].Category); + + Assert.Equal(category.Id, category.Products[0].CategoryId); + Assert.Equal(category.Id, category.Products[1].CategoryId); + Assert.Equal(category.Id, category.Products[2].CategoryId); + } + + private class MyTracker : KeyValueEntityTracker + { + public MyTracker(bool updateExistingEntities) + : base(updateExistingEntities) + { + } + + public override EntityState DetermineState(EntityEntry entry) + { + if (!entry.IsKeySet) + { + entry.GetInfrastructure()[entry.Metadata.FindPrimaryKey().Properties.Single()] = 777; + return EntityState.Added; + } + + return base.DetermineState(entry); + } + } + + [ConditionalFact] + public void TrackGraph_does_not_call_DetectChanges() + { + var provider = + InMemoryTestHelpers.Instance.CreateServiceProvider( + new ServiceCollection().AddScoped()); + using var context = new EarlyLearningCenter(GetType().Name, provider); + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + changeDetector.DetectChangesCalled = false; + + context.ChangeTracker.TrackGraph(CreateSimpleGraph(2), e => e.Entry.State = EntityState.Unchanged); + + Assert.False(changeDetector.DetectChangesCalled); + + context.ChangeTracker.DetectChanges(); + + Assert.True(changeDetector.DetectChangesCalled); + } + + [ConditionalFact] + public void TrackGraph_overload_can_visit_an_already_attached_graph() + { + using var context = new EarlyLearningCenter(GetType().Name); + var category = new Category + { + Id = 1, + Products = new List + { + new Product + { + Id = 1, + CategoryId = 1, + Details = new ProductDetails { Id = 1 } + }, + new Product + { + Id = 2, + CategoryId = 1, + Details = new ProductDetails { Id = 2 } + }, + new Product + { + Id = 3, + CategoryId = 1, + Details = new ProductDetails { Id = 3 } + } + } + }; + + context.Attach(category); + + var visited = new HashSet(); + var traversal = new List(); + + context.ChangeTracker.TrackGraph( + category, visited, e => + { + if (e.NodeState.Contains(e.Entry.Entity)) + { + return false; + } + + e.NodeState.Add(e.Entry.Entity); + + traversal.Add(NodeString(e)); + + return true; + }); + + Assert.Equal( + new List + { + " -----> Category:1", + "Category:1 ---Products--> Product:1", + "Product:1 ---Details--> ProductDetails:1", + "Category:1 ---Products--> Product:2", + "Product:2 ---Details--> ProductDetails:2", + "Category:1 ---Products--> Product:3", + "Product:3 ---Details--> ProductDetails:3" + }, + traversal); + + Assert.Equal(7, visited.Count); + } + + private static void AssertValuesSaved(int id, int someInt, string someString) + { + using var context = new TheShadows(); + var entry = context.Entry(context.Set().Single(e => EF.Property(e, "Id") == id)); + + Assert.Equal(id, entry.Property("Id").CurrentValue); + Assert.Equal(someInt, entry.Property("SomeInt").CurrentValue); + Assert.Equal(someString, entry.Property("SomeString").CurrentValue); + } + + private class TheShadows : DbContext + { + protected internal override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity( + b => + { + b.Property("Id").ValueGeneratedOnAdd(); + b.Property("SomeInt"); + b.Property("SomeString"); + }); + + protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseInternalServiceProvider(InMemoryFixture.DefaultServiceProvider) + .UseInMemoryDatabase(nameof(TheShadows)); + } + + private class Dark + { + } + + private static Product CreateSimpleGraph(int id) + => new Product { Id = id, Category = new Category { Id = id } }; + + private class ChangeDetectorProxy : ChangeDetector + { + public ChangeDetectorProxy( + IDiagnosticsLogger logger, + ILoggingOptions loggingOptions) + : base(logger, loggingOptions) + { + } + + public bool DetectChangesCalled { get; set; } + + public override void DetectChanges(InternalEntityEntry entry) + { + DetectChangesCalled = true; + + base.DetectChanges(entry); + } + + public override void DetectChanges(IStateManager stateManager) + { + DetectChangesCalled = true; + + base.DetectChanges(stateManager); + } + } + + private class Category + { + public int Id { get; set; } + + public List Products { get; set; } + } + + private class Product + { + public int Id { get; set; } + + public int CategoryId { get; set; } + public Category Category { get; set; } + + public ProductDetails Details { get; set; } + + // ReSharper disable once CollectionNeverUpdated.Local + // ReSharper disable once MemberHidesStaticFromOuterClass + public List OrderDetails { get; set; } + } + + private class ProductDetails + { + public int Id { get; set; } + + public Product Product { get; set; } + + public ProductDetailsTag Tag { get; set; } + } + + private class ProductDetailsTag + { + public int Id { get; set; } + + public ProductDetails Details { get; set; } + + public ProductDetailsTagDetails TagDetails { get; set; } + } + + private class ProductDetailsTagDetails + { + public int Id { get; set; } + + public ProductDetailsTag Tag { get; set; } + } + + private class Order + { + public int Id { get; set; } + + // ReSharper disable once CollectionNeverUpdated.Local + // ReSharper disable once MemberHidesStaticFromOuterClass + public List OrderDetails { get; set; } + } + + private class OrderDetails + { + public int OrderId { get; set; } + public int ProductId { get; set; } + + public Order Order { get; set; } + public Product Product { get; set; } + } + + private class NullbileCategory + { + public List Products { get; set; } + public NullbileCategoryInfo Info { get; set; } + } + + private class NullbileCategoryInfo + { + // ReSharper disable once MemberHidesStaticFromOuterClass + public NullbileCategory Category { get; set; } + } + + private class NullbileProduct + { + // ReSharper disable once MemberHidesStaticFromOuterClass + public NullbileCategory Category { get; set; } + } + + private class Sweet + { + public int? Id { get; set; } + public Dreams Dreams { get; set; } + } + + private class Dreams + { + public Sweet Sweet { get; set; } + public AreMade Are { get; set; } + public AreMade Made { get; set; } + public OfThis OfThis { get; set; } + } + + private class AreMade + { + } + + private class OfThis : AreMade + { + } + + private class EarlyLearningCenter : DbContext + { + private readonly string _databaseName; + private readonly IServiceProvider _serviceProvider; + + public EarlyLearningCenter(string databaseName) + { + _databaseName = databaseName; + _serviceProvider = InMemoryTestHelpers.Instance.CreateServiceProvider(); + } + + public EarlyLearningCenter(string databaseName, IServiceProvider serviceProvider) + { + _databaseName = databaseName; + _serviceProvider = serviceProvider; + } + + protected internal override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder + .Entity( + b => + { + b.Property("Id"); + b.Property("CategoryId"); + b.HasKey("Id"); + }); + + modelBuilder + .Entity( + b => + { + b.Property("Id"); + b.Property("CategoryId"); + b.HasKey("Id"); + }); + + modelBuilder + .Entity( + b => + { + b.Property("Id"); + b.HasKey("Id"); + b.HasMany(e => e.Products).WithOne(e => e.Category).HasForeignKey("CategoryId"); + b.HasOne(e => e.Info).WithOne(e => e.Category).HasForeignKey("CategoryId"); + }); + + modelBuilder.Entity().OwnsOne( + e => e.Dreams, b => + { + b.WithOwner(e => e.Sweet); + b.OwnsOne(e => e.Are); + b.OwnsOne(e => e.Made); + b.OwnsOne(e => e.OfThis); + }); + + modelBuilder + .Entity().HasMany(e => e.Products).WithOne(e => e.Category); + + modelBuilder + .Entity().HasOne(e => e.TagDetails).WithOne(e => e.Tag) + .HasForeignKey(e => e.Id); + + modelBuilder + .Entity().HasOne(e => e.Tag).WithOne(e => e.Details) + .HasForeignKey(e => e.Id); + + modelBuilder + .Entity().HasOne(e => e.Details).WithOne(e => e.Product) + .HasForeignKey(e => e.Id); + + modelBuilder.Entity( + b => + { + b.HasKey( + e => new { e.OrderId, e.ProductId }); + b.HasOne(e => e.Order).WithMany(e => e.OrderDetails).HasForeignKey(e => e.OrderId); + b.HasOne(e => e.Product).WithMany(e => e.OrderDetails).HasForeignKey(e => e.ProductId); + }); + } + + protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseInternalServiceProvider(_serviceProvider) + .UseInMemoryDatabase(_databaseName); + } + + public class KeyValueEntityTracker + { + private readonly bool _updateExistingEntities; + + public KeyValueEntityTracker(bool updateExistingEntities) + { + _updateExistingEntities = updateExistingEntities; + } + + public virtual void TrackEntity(EntityEntryGraphNode node) + => node.Entry.GetInfrastructure().SetEntityState(DetermineState(node.Entry), acceptChanges: true); + + public virtual EntityState DetermineState(EntityEntry entry) + => entry.IsKeySet + ? (_updateExistingEntities ? EntityState.Modified : EntityState.Unchanged) + : EntityState.Added; + } + + } +}