diff --git a/src/EFCore/ChangeTracking/Internal/EntityGraphAttacher.cs b/src/EFCore/ChangeTracking/Internal/EntityGraphAttacher.cs index 4541d414ec7..af07c9781da 100644 --- a/src/EFCore/ChangeTracking/Internal/EntityGraphAttacher.cs +++ b/src/EFCore/ChangeTracking/Internal/EntityGraphAttacher.cs @@ -79,6 +79,8 @@ public virtual Task AttachGraphAsync( private static bool PaintAction( EntityEntryGraphNode<(EntityState TargetState, EntityState StoreGenTargetState, bool Force)> node) { + SetReferenceLoaded(node); + var internalEntityEntry = node.GetInfrastructure(); if (internalEntityEntry.EntityState != EntityState.Detached) { @@ -103,6 +105,8 @@ private static async Task PaintActionAsync( EntityEntryGraphNode<(EntityState TargetState, EntityState StoreGenTargetState, bool Force)> node, CancellationToken cancellationToken) { + SetReferenceLoaded(node); + var internalEntityEntry = node.GetInfrastructure(); if (internalEntityEntry.EntityState != EntityState.Detached) { @@ -123,5 +127,16 @@ await internalEntityEntry.SetEntityStateAsync( return true; } + + private static void SetReferenceLoaded( + EntityEntryGraphNode<(EntityState TargetState, EntityState StoreGenTargetState, bool Force)> node) + { + var inboundNavigation = node.InboundNavigation; + if (inboundNavigation != null + && !inboundNavigation.IsCollection()) + { + node.SourceEntry.GetInfrastructure().SetIsLoaded(inboundNavigation); + } + } } } diff --git a/test/EFCore.Specification.Tests/LazyLoadProxyTestBase.cs b/test/EFCore.Specification.Tests/LazyLoadProxyTestBase.cs index 6986097701a..e4a84ecb2bb 100644 --- a/test/EFCore.Specification.Tests/LazyLoadProxyTestBase.cs +++ b/test/EFCore.Specification.Tests/LazyLoadProxyTestBase.cs @@ -26,6 +26,188 @@ public abstract class LazyLoadProxyTestBase : IClassFixture protected TFixture Fixture { get; } + [ConditionalTheory] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Added, false)] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Added, true)] + public virtual void Attached_references_to_principal_are_marked_as_loaded(EntityState state, bool lazy) + { + using (var context = CreateContext(lazy)) + { + var parent = context.CreateProxy(); + parent.Id = 707; + parent.AlternateId = "Root"; + + var singlePkToPk = context.CreateProxy(); + singlePkToPk.Id = 707; + parent.SinglePkToPk = singlePkToPk; + + var single = context.CreateProxy(); + single.Id = 21; + parent.Single = single; + + var singleAk = context.CreateProxy(); + singleAk.Id = 42; + parent.SingleAk = singleAk; + + var singleShadowFk = context.CreateProxy(); + singleShadowFk.Id = 62; + parent.SingleShadowFk = singleShadowFk; + + var singleCompositeKey = context.CreateProxy(); + singleCompositeKey.Id = 62; + parent.SingleCompositeKey = singleCompositeKey; + + context.Attach(parent); + + if (state != EntityState.Unchanged) + { + context.ChangeTracker.LazyLoadingEnabled = false; + + context.Entry(parent.SinglePkToPk).State = state; + context.Entry(parent.Single).State = state; + context.Entry(parent.SingleAk).State = state; + context.Entry(parent.SingleShadowFk).State = state; + context.Entry(parent.SingleCompositeKey).State = state; + context.Entry(parent).State = state; + + context.ChangeTracker.LazyLoadingEnabled = true; + } + + Assert.True(context.Entry(parent).Reference(e => e.SinglePkToPk).IsLoaded); + Assert.True(context.Entry(parent).Reference(e => e.Single).IsLoaded); + Assert.True(context.Entry(parent).Reference(e => e.SingleAk).IsLoaded); + Assert.True(context.Entry(parent).Reference(e => e.SingleShadowFk).IsLoaded); + Assert.True(context.Entry(parent).Reference(e => e.SingleCompositeKey).IsLoaded); + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Added, false)] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Added, true)] + public virtual void Attached_references_to_dependents_are_marked_as_loaded(EntityState state, bool lazy) + { + using (var context = CreateContext(lazy)) + { + var parent = context.CreateProxy(); + parent.Id = 707; + parent.AlternateId = "Root"; + + var singlePkToPk = context.CreateProxy(); + singlePkToPk.Id = 707; + parent.SinglePkToPk = singlePkToPk; + + var single = context.CreateProxy(); + single.Id = 21; + parent.Single = single; + + var singleAk = context.CreateProxy(); + singleAk.Id = 42; + parent.SingleAk = singleAk; + + var singleShadowFk = context.CreateProxy(); + singleShadowFk.Id = 62; + parent.SingleShadowFk = singleShadowFk; + + var singleCompositeKey = context.CreateProxy(); + singleCompositeKey.Id = 62; + parent.SingleCompositeKey = singleCompositeKey; + + context.Attach(parent); + + if (state != EntityState.Unchanged) + { + context.ChangeTracker.LazyLoadingEnabled = false; + + context.Entry(parent.SinglePkToPk).State = state; + context.Entry(parent.Single).State = state; + context.Entry(parent.SingleAk).State = state; + context.Entry(parent.SingleShadowFk).State = state; + context.Entry(parent.SingleCompositeKey).State = state; + context.Entry(parent).State = state; + + context.ChangeTracker.LazyLoadingEnabled = true; + } + + Assert.True(context.Entry(parent.SinglePkToPk).Reference(e => e.Parent).IsLoaded); + Assert.True(context.Entry(parent.Single).Reference(e => e.Parent).IsLoaded); + Assert.True(context.Entry(parent.SingleAk).Reference(e => e.Parent).IsLoaded); + Assert.True(context.Entry(parent.SingleShadowFk).Reference(e => e.Parent).IsLoaded); + Assert.True(context.Entry(parent.SingleCompositeKey).Reference(e => e.Parent).IsLoaded); + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Added, false)] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Added, true)] + public virtual void Attached_collections_are_not_marked_as_loaded(EntityState state, bool lazy) + { + using (var context = CreateContext(lazy)) + { + var parent = context.CreateProxy(); + parent.Id = 707; + parent.AlternateId = "Root"; + + var child1 = context.CreateProxy(); + child1.Id = 11; + var child2 = context.CreateProxy(); + child2.Id = 12; + parent.Children = new List { child1, child2 }; + + var childAk1 = context.CreateProxy(); + childAk1.Id = 31; + var childAk2 = context.CreateProxy(); + childAk2.Id = 32; + parent.ChildrenAk = new List { childAk1, childAk2 }; + + var childShadowFk1 = context.CreateProxy(); + childShadowFk1.Id = 51; + var childShadowFk2 = context.CreateProxy(); + childShadowFk2.Id = 52; + parent.ChildrenShadowFk = new List { childShadowFk1, childShadowFk2 }; + + var childCompositeKey1 = context.CreateProxy(); + childCompositeKey1.Id = 51; + var childCompositeKey2 = context.CreateProxy(); + childCompositeKey2.Id = 52; + parent.ChildrenCompositeKey = new List { childCompositeKey1, childCompositeKey2 }; + + context.Attach(parent); + + if (state != EntityState.Unchanged) + { + context.ChangeTracker.LazyLoadingEnabled = false; + + foreach (var child in parent.Children.Cast() + .Concat(parent.ChildrenAk) + .Concat(parent.ChildrenShadowFk) + .Concat(parent.ChildrenCompositeKey)) + { + context.Entry(child).State = state; + } + context.Entry(parent).State = state; + + context.ChangeTracker.LazyLoadingEnabled = true; + } + + Assert.False(context.Entry(parent).Collection(e => e.Children).IsLoaded); + Assert.False(context.Entry(parent).Collection(e => e.ChildrenAk).IsLoaded); + Assert.False(context.Entry(parent).Collection(e => e.ChildrenShadowFk).IsLoaded); + Assert.False(context.Entry(parent).Collection(e => e.ChildrenCompositeKey).IsLoaded); + } + } + [ConditionalTheory] [InlineData(EntityState.Unchanged, false, false)] [InlineData(EntityState.Modified, false, false)] diff --git a/test/EFCore.Specification.Tests/LoadTestBase.cs b/test/EFCore.Specification.Tests/LoadTestBase.cs index 9c0eace1ad1..0c2c87c42e1 100644 --- a/test/EFCore.Specification.Tests/LoadTestBase.cs +++ b/test/EFCore.Specification.Tests/LoadTestBase.cs @@ -26,6 +26,147 @@ public abstract class LoadTestBase : IClassFixture protected TFixture Fixture { get; } + [ConditionalTheory] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Added, false)] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Added, true)] + public virtual void Attached_references_to_principal_are_marked_as_loaded(EntityState state, bool lazy) + { + using (var context = CreateContext(lazy)) + { + var parent = new Parent + { + Id = 707, + AlternateId = "Root", + SinglePkToPk = new SinglePkToPk { Id = 707 }, + Single = new Single { Id = 21 }, + SingleAk = new SingleAk { Id = 42 }, + SingleShadowFk = new SingleShadowFk { Id = 62 }, + SingleCompositeKey = new SingleCompositeKey { Id = 62 } + }; + + context.Attach(parent); + + if (state != EntityState.Unchanged) + { + context.ChangeTracker.LazyLoadingEnabled = false; + + context.Entry(parent.SinglePkToPk).State = state; + context.Entry(parent.Single).State = state; + context.Entry(parent.SingleAk).State = state; + context.Entry(parent.SingleShadowFk).State = state; + context.Entry(parent.SingleCompositeKey).State = state; + context.Entry(parent).State = state; + + context.ChangeTracker.LazyLoadingEnabled = true; + } + + Assert.True(context.Entry(parent).Reference(e => e.SinglePkToPk).IsLoaded); + Assert.True(context.Entry(parent).Reference(e => e.Single).IsLoaded); + Assert.True(context.Entry(parent).Reference(e => e.SingleAk).IsLoaded); + Assert.True(context.Entry(parent).Reference(e => e.SingleShadowFk).IsLoaded); + Assert.True(context.Entry(parent).Reference(e => e.SingleCompositeKey).IsLoaded); + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Added, false)] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Added, true)] + public virtual void Attached_references_to_dependents_are_marked_as_loaded(EntityState state, bool lazy) + { + using (var context = CreateContext(lazy)) + { + var parent = new Parent + { + Id = 707, + AlternateId = "Root", + SinglePkToPk = new SinglePkToPk { Id = 707 }, + Single = new Single { Id = 21 }, + SingleAk = new SingleAk { Id = 42 }, + SingleShadowFk = new SingleShadowFk { Id = 62 }, + SingleCompositeKey = new SingleCompositeKey { Id = 62 } + }; + + context.Attach(parent); + + if (state != EntityState.Unchanged) + { + context.ChangeTracker.LazyLoadingEnabled = false; + + context.Entry(parent.SinglePkToPk).State = state; + context.Entry(parent.Single).State = state; + context.Entry(parent.SingleAk).State = state; + context.Entry(parent.SingleShadowFk).State = state; + context.Entry(parent.SingleCompositeKey).State = state; + context.Entry(parent).State = state; + + context.ChangeTracker.LazyLoadingEnabled = true; + } + + Assert.True(context.Entry(parent.SinglePkToPk).Reference(e => e.Parent).IsLoaded); + Assert.True(context.Entry(parent.Single).Reference(e => e.Parent).IsLoaded); + Assert.True(context.Entry(parent.SingleAk).Reference(e => e.Parent).IsLoaded); + Assert.True(context.Entry(parent.SingleShadowFk).Reference(e => e.Parent).IsLoaded); + Assert.True(context.Entry(parent.SingleCompositeKey).Reference(e => e.Parent).IsLoaded); + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Added, false)] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Added, true)] + public virtual void Attached_collections_are_not_marked_as_loaded(EntityState state, bool lazy) + { + using (var context = CreateContext(lazy)) + { + var parent = new Parent + { + Id = 707, + AlternateId = "Root", + Children = new List { new Child { Id = 11 }, new Child { Id = 12 } }, + ChildrenAk = new List { new ChildAk { Id = 31 }, new ChildAk { Id = 32 } }, + ChildrenShadowFk = new List { new ChildShadowFk { Id = 51 }, new ChildShadowFk { Id = 52 } }, + ChildrenCompositeKey = new List + { + new ChildCompositeKey { Id = 51 }, new ChildCompositeKey { Id = 52 } + } + }; + + context.Attach(parent); + + if (state != EntityState.Unchanged) + { + context.ChangeTracker.LazyLoadingEnabled = false; + + foreach (var child in parent.Children.Cast() + .Concat(parent.ChildrenAk) + .Concat(parent.ChildrenShadowFk) + .Concat(parent.ChildrenCompositeKey)) + { + context.Entry(child).State = state; + } + context.Entry(parent).State = state; + + context.ChangeTracker.LazyLoadingEnabled = true; + } + + Assert.False(context.Entry(parent).Collection(e => e.Children).IsLoaded); + Assert.False(context.Entry(parent).Collection(e => e.ChildrenAk).IsLoaded); + Assert.False(context.Entry(parent).Collection(e => e.ChildrenShadowFk).IsLoaded); + Assert.False(context.Entry(parent).Collection(e => e.ChildrenCompositeKey).IsLoaded); + } + } + [ConditionalTheory] [InlineData(EntityState.Unchanged)] [InlineData(EntityState.Modified)]