From 3633fa30c6a3c41d97b01de3c57ba55c699bbb8b Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Fri, 15 Jul 2022 14:43:02 +0100 Subject: [PATCH] Make EntityEntryGraphIterator publicly usable Fixes #26461 --- .../ChangeTracking/EntityEntryGraphNode.cs | 23 ++--- .../ChangeTracking/EntityEntryGraphNode`.cs | 36 +++++--- .../ChangeTracking/TrackGraphTestBase.cs | 92 +++++++++++++++++-- 3 files changed, 110 insertions(+), 41 deletions(-) diff --git a/src/EFCore/ChangeTracking/EntityEntryGraphNode.cs b/src/EFCore/ChangeTracking/EntityEntryGraphNode.cs index ecb0aef26a0..c4e82161709 100644 --- a/src/EFCore/ChangeTracking/EntityEntryGraphNode.cs +++ b/src/EFCore/ChangeTracking/EntityEntryGraphNode.cs @@ -89,26 +89,15 @@ InternalEntityEntry IInfrastructure.Instance => _entry; /// - /// Creates a new node for the entity that is being traversed next in the graph. + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - /// The node that the entity is being traversed from. - /// - /// The internal entry tracking information about the entity being traversed to. - /// - /// The navigation property that is being traversed to reach the new node. - /// The newly created node. + [EntityFrameworkInternal] public virtual EntityEntryGraphNode CreateNode( EntityEntryGraphNode currentNode, InternalEntityEntry internalEntityEntry, INavigationBase reachedVia) - { - Check.NotNull(currentNode, nameof(currentNode)); - Check.NotNull(internalEntityEntry, nameof(internalEntityEntry)); - Check.NotNull(reachedVia, nameof(reachedVia)); - - return new EntityEntryGraphNode( - internalEntityEntry, - currentNode.Entry.GetInfrastructure(), - reachedVia); - } + => new(internalEntityEntry, currentNode.Entry.GetInfrastructure(), reachedVia); } diff --git a/src/EFCore/ChangeTracking/EntityEntryGraphNode`.cs b/src/EFCore/ChangeTracking/EntityEntryGraphNode`.cs index 3a913f952d5..5701f6d7cb5 100644 --- a/src/EFCore/ChangeTracking/EntityEntryGraphNode`.cs +++ b/src/EFCore/ChangeTracking/EntityEntryGraphNode`.cs @@ -32,33 +32,41 @@ public EntityEntryGraphNode( NodeState = state; } + /// + /// Creates a new node in the entity graph. + /// + /// The entry for the entity represented by this node. + /// A state object that will be available when processing each node. + /// The entry from which this node was reached, or if this is the root node. + /// The navigation from the source node to this node, or if this is the root node. + public EntityEntryGraphNode( + EntityEntry entry, + TState state, + EntityEntry? sourceEntry, + INavigationBase? inboundNavigation) + : this(entry.GetInfrastructure(), state, sourceEntry?.GetInfrastructure(), inboundNavigation) + { + } + /// /// Gets or sets state that will be available to all nodes that are visited after this node. /// public virtual TState NodeState { get; set; } /// - /// Creates a new node for the entity that is being traversed next in the graph. + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - /// The node that the entity is being traversed from. - /// - /// The internal entry tracking information about the entity being traversed to. - /// - /// The navigation property that is being traversed to reach the new node. - /// The newly created node. + [EntityFrameworkInternal] public override EntityEntryGraphNode CreateNode( EntityEntryGraphNode currentNode, InternalEntityEntry internalEntityEntry, INavigationBase reachedVia) - { - Check.NotNull(currentNode, nameof(currentNode)); - Check.NotNull(internalEntityEntry, nameof(internalEntityEntry)); - Check.NotNull(reachedVia, nameof(reachedVia)); - - return new EntityEntryGraphNode( + => new EntityEntryGraphNode( internalEntityEntry, ((EntityEntryGraphNode)currentNode).NodeState, currentNode.Entry.GetInfrastructure(), reachedVia); - } } diff --git a/test/EFCore.Tests/ChangeTracking/TrackGraphTestBase.cs b/test/EFCore.Tests/ChangeTracking/TrackGraphTestBase.cs index 308a430ee64..280958b32a8 100644 --- a/test/EFCore.Tests/ChangeTracking/TrackGraphTestBase.cs +++ b/test/EFCore.Tests/ChangeTracking/TrackGraphTestBase.cs @@ -23,6 +23,88 @@ protected override IList TrackGraph(DbContext context, object root, Acti return traversal; } + + [ConditionalTheory] // Issue #26461 + [InlineData(false)] + [InlineData(true)] + public async Task Can_iterate_over_graph_using_public_surface(bool async) + { + using var context = new EarlyLearningCenter(GetType().Name); + var category = new Category + { + Id = 1, + Products = new List + { + new() + { + Id = 1, + CategoryId = 1, + Details = new ProductDetails { Id = 1 } + }, + new() + { + Id = 2, + CategoryId = 1, + Details = new ProductDetails { Id = 2 } + }, + new() + { + Id = 3, + CategoryId = 1, + Details = new ProductDetails { Id = 3 } + } + } + }; + + var rootEntry = context.Attach(category); + + var graphIterator = context.GetService(); + + var visited = new HashSet(); + var traversal = new List(); + + bool Callback(EntityEntryGraphNode> node) + { + if (node.NodeState.Contains(node.Entry.Entity)) + { + return false; + } + + node.NodeState.Add(node.Entry.Entity); + + traversal.Add(NodeString(node)); + + return true; + } + + if (async) + { + await graphIterator.TraverseGraphAsync( + new EntityEntryGraphNode>(rootEntry, visited, null, null), + (node, _) => Task.FromResult(Callback(node))); + } + else + { + graphIterator.TraverseGraph( + new EntityEntryGraphNode>(rootEntry, visited, null, null), + Callback); + } + + 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); + } } public class TrackGraphTestWithState : TrackGraphTestBase @@ -1152,16 +1234,6 @@ public void TrackGraph_overload_can_visit_an_already_attached_graph() 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)