diff --git a/src/EFCore/Metadata/Builders/IConventionEntityTypeBuilder.cs b/src/EFCore/Metadata/Builders/IConventionEntityTypeBuilder.cs index 796af9c54ff..da8fa4ea64a 100644 --- a/src/EFCore/Metadata/Builders/IConventionEntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Builders/IConventionEntityTypeBuilder.cs @@ -173,6 +173,22 @@ bool CanHaveIndexerProperty( MemberInfo memberInfo, bool fromDataAnnotation = false); + /// + /// Returns an object that can be used to configure the service property with the given member info. + /// If no matching property exists, then a new property will be added. + /// + /// The type of the service. + /// The or of the property. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// An object that can be used to configure the property if it exists on the entity type, + /// otherwise. + /// + IConventionServicePropertyBuilder? ServiceProperty( + Type serviceType, + MemberInfo memberInfo, + bool fromDataAnnotation = false); + /// /// Returns a value indicating whether the given service property can be added to this entity type. /// diff --git a/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs b/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs index 58f10d4a7a3..dbe0837ff3c 100644 --- a/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs +++ b/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs @@ -385,6 +385,7 @@ protected virtual void ProcessPropertyAnnotations( private static RuntimeServiceProperty Create(IServiceProperty property, RuntimeEntityType runtimeEntityType) => runtimeEntityType.AddServiceProperty( property.Name, + property.ClrType, property.PropertyInfo, property.FieldInfo, property.GetPropertyAccessMode()); diff --git a/src/EFCore/Metadata/IConventionEntityType.cs b/src/EFCore/Metadata/IConventionEntityType.cs index 1867405f0f0..a726aa4194f 100644 --- a/src/EFCore/Metadata/IConventionEntityType.cs +++ b/src/EFCore/Metadata/IConventionEntityType.cs @@ -971,6 +971,15 @@ public interface IConventionEntityType : IReadOnlyEntityType, IConventionTypeBas /// The newly created service property. IConventionServiceProperty AddServiceProperty(MemberInfo memberInfo, bool fromDataAnnotation = false); + /// + /// Adds a service property to this entity type. + /// + /// The type of the service. + /// The or of the property to add. + /// Indicates whether the configuration was specified using a data annotation. + /// The newly created service property. + IConventionServiceProperty AddServiceProperty(Type serviceType, MemberInfo memberInfo, bool fromDataAnnotation = false); + /// /// Gets the service property with a given name. /// Returns if no property with the given name is defined. diff --git a/src/EFCore/Metadata/IMutableEntityType.cs b/src/EFCore/Metadata/IMutableEntityType.cs index 65a54085369..29f28aac8be 100644 --- a/src/EFCore/Metadata/IMutableEntityType.cs +++ b/src/EFCore/Metadata/IMutableEntityType.cs @@ -839,6 +839,14 @@ IMutableProperty AddIndexerProperty( /// The newly created service property. IMutableServiceProperty AddServiceProperty(MemberInfo memberInfo); + /// + /// Adds a service property to this entity type. + /// + /// The type of the service. + /// The or of the property to add. + /// The newly created service property. + IMutableServiceProperty AddServiceProperty(Type serviceType, MemberInfo memberInfo); + /// /// Gets the service property with a given name. /// Returns if no property with the given name is defined. diff --git a/src/EFCore/Metadata/Internal/EntityType.cs b/src/EFCore/Metadata/Internal/EntityType.cs index 213e1088828..83d55e3318a 100644 --- a/src/EFCore/Metadata/Internal/EntityType.cs +++ b/src/EFCore/Metadata/Internal/EntityType.cs @@ -2793,6 +2793,7 @@ public virtual IReadOnlyList ValueGeneratingProperties /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual ServiceProperty AddServiceProperty( + Type serviceType, MemberInfo memberInfo, // ReSharper disable once MethodOverloadWithOptionalParameter ConfigurationSource configurationSource) @@ -2816,6 +2817,7 @@ public virtual ServiceProperty AddServiceProperty( name, memberInfo as PropertyInfo, memberInfo as FieldInfo, + serviceType, this, configurationSource); @@ -5145,7 +5147,17 @@ IEnumerable IEntityType.GetValueGeneratingProperties() /// [DebuggerStepThrough] IMutableServiceProperty IMutableEntityType.AddServiceProperty(MemberInfo memberInfo) - => AddServiceProperty(memberInfo, ConfigurationSource.Explicit); + => AddServiceProperty(memberInfo.GetMemberType(), memberInfo, ConfigurationSource.Explicit); + + /// + /// 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. + /// + [DebuggerStepThrough] + IMutableServiceProperty IMutableEntityType.AddServiceProperty(Type serviceType, MemberInfo memberInfo) + => AddServiceProperty(serviceType, memberInfo, ConfigurationSource.Explicit); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -5155,7 +5167,20 @@ IMutableServiceProperty IMutableEntityType.AddServiceProperty(MemberInfo memberI /// [DebuggerStepThrough] IConventionServiceProperty IConventionEntityType.AddServiceProperty(MemberInfo memberInfo, bool fromDataAnnotation) - => AddServiceProperty(memberInfo, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + => AddServiceProperty( + memberInfo.GetMemberType(), memberInfo, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + [DebuggerStepThrough] + IConventionServiceProperty IConventionEntityType.AddServiceProperty(Type serviceType, MemberInfo memberInfo, bool fromDataAnnotation) + => AddServiceProperty( + serviceType, memberInfo, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs index c33292c3222..15414e3c93a 100644 --- a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs @@ -880,6 +880,18 @@ public virtual IMutableNavigationBase Navigation(string navigationName) public virtual InternalServicePropertyBuilder? ServiceProperty( MemberInfo memberInfo, ConfigurationSource? configurationSource) + => ServiceProperty(memberInfo.GetMemberType(), memberInfo, configurationSource); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalServicePropertyBuilder? ServiceProperty( + Type serviceType, + MemberInfo memberInfo, + ConfigurationSource? configurationSource) { var propertyName = memberInfo.GetSimpleMemberName(); List? propertiesToDetach = null; @@ -999,7 +1011,7 @@ public virtual IMutableNavigationBase Navigation(string navigationName) } } - builder = Metadata.AddServiceProperty(memberInfo, configurationSource.Value).Builder; + builder = Metadata.AddServiceProperty(serviceType, memberInfo, configurationSource.Value).Builder; if (detachedProperties != null) { @@ -5287,6 +5299,17 @@ IConventionEntityTypeBuilder IConventionEntityTypeBuilder.RemoveUnusedImplicitPr IConventionServicePropertyBuilder? IConventionEntityTypeBuilder.ServiceProperty(MemberInfo memberInfo, bool fromDataAnnotation) => ServiceProperty(memberInfo, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// + /// 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. + /// + [DebuggerStepThrough] + IConventionServicePropertyBuilder? IConventionEntityTypeBuilder.ServiceProperty( + Type serviceType, MemberInfo memberInfo, bool fromDataAnnotation) + => ServiceProperty(serviceType, memberInfo, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore/Metadata/Internal/InternalServicePropertyBuilder.cs b/src/EFCore/Metadata/Internal/InternalServicePropertyBuilder.cs index 1916fe38d4c..82c4be9d829 100644 --- a/src/EFCore/Metadata/Internal/InternalServicePropertyBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalServicePropertyBuilder.cs @@ -93,7 +93,7 @@ public virtual bool CanSetParameterBinding( public virtual InternalServicePropertyBuilder? Attach(InternalEntityTypeBuilder entityTypeBuilder) { var newPropertyBuilder = entityTypeBuilder.ServiceProperty( - Metadata.GetIdentifyingMemberInfo()!, Metadata.GetConfigurationSource()); + Metadata.ClrType, Metadata.GetIdentifyingMemberInfo()!, Metadata.GetConfigurationSource()); if (newPropertyBuilder == null) { return null; diff --git a/src/EFCore/Metadata/Internal/ServiceProperty.cs b/src/EFCore/Metadata/Internal/ServiceProperty.cs index 60619deda5c..35adf4c85f6 100644 --- a/src/EFCore/Metadata/Internal/ServiceProperty.cs +++ b/src/EFCore/Metadata/Internal/ServiceProperty.cs @@ -29,12 +29,13 @@ public ServiceProperty( string name, PropertyInfo? propertyInfo, FieldInfo? fieldInfo, + Type serviceType, EntityType declaringEntityType, ConfigurationSource configurationSource) : base(name, propertyInfo, fieldInfo, configurationSource) { DeclaringEntityType = declaringEntityType; - ClrType = (propertyInfo?.PropertyType ?? fieldInfo?.FieldType)!; + ClrType = serviceType; _builder = new InternalServicePropertyBuilder(this, declaringEntityType.Model.Builder); } diff --git a/src/EFCore/Metadata/RuntimeEntityType.cs b/src/EFCore/Metadata/RuntimeEntityType.cs index 69a18b52228..0c564d2e124 100644 --- a/src/EFCore/Metadata/RuntimeEntityType.cs +++ b/src/EFCore/Metadata/RuntimeEntityType.cs @@ -715,11 +715,29 @@ public virtual RuntimeServiceProperty AddServiceProperty( PropertyInfo? propertyInfo = null, FieldInfo? fieldInfo = null, PropertyAccessMode propertyAccessMode = Internal.Model.DefaultPropertyAccessMode) + => AddServiceProperty(name, (propertyInfo?.PropertyType ?? fieldInfo?.FieldType)!, propertyInfo, fieldInfo, propertyAccessMode); + + /// + /// Adds a service property to this entity type. + /// + /// The name of the property to add. + /// The type of the service. + /// The corresponding CLR property or for a shadow property. + /// The corresponding CLR field or for a shadow property. + /// The used for this property. + /// The newly created service property. + public virtual RuntimeServiceProperty AddServiceProperty( + string name, + Type serviceType, + PropertyInfo? propertyInfo = null, + FieldInfo? fieldInfo = null, + PropertyAccessMode propertyAccessMode = Internal.Model.DefaultPropertyAccessMode) { var serviceProperty = new RuntimeServiceProperty( name, propertyInfo, fieldInfo, + serviceType, this, propertyAccessMode); diff --git a/src/EFCore/Metadata/RuntimeServiceProperty.cs b/src/EFCore/Metadata/RuntimeServiceProperty.cs index 09f6e33f947..480d63a5334 100644 --- a/src/EFCore/Metadata/RuntimeServiceProperty.cs +++ b/src/EFCore/Metadata/RuntimeServiceProperty.cs @@ -28,6 +28,7 @@ public RuntimeServiceProperty( string name, PropertyInfo? propertyInfo, FieldInfo? fieldInfo, + Type serviceType, RuntimeEntityType declaringEntityType, PropertyAccessMode propertyAccessMode) : base(name, propertyInfo, fieldInfo, propertyAccessMode) @@ -35,7 +36,7 @@ public RuntimeServiceProperty( Check.NotNull(declaringEntityType, nameof(declaringEntityType)); DeclaringEntityType = declaringEntityType; - ClrType = (propertyInfo?.PropertyType ?? fieldInfo?.FieldType)!; + ClrType = serviceType; } /// diff --git a/test/EFCore.Specification.Tests/LazyLoadTestBase.cs b/test/EFCore.Specification.Tests/LazyLoadTestBase.cs index a4e946d7c63..2d75111315c 100644 --- a/test/EFCore.Specification.Tests/LazyLoadTestBase.cs +++ b/test/EFCore.Specification.Tests/LazyLoadTestBase.cs @@ -5641,6 +5641,966 @@ public virtual void Lazy_load_collection_already_partially_loaded_delegate_loade } } + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_collection_delegate_loader_with_state_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Set().Single(); + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior); + + var collectionEntry = context.Entry(parent).Collection(e => e.Children); + + Assert.False(collectionEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(parent.Children); // Explicitly detached + } + else + { + Assert.NotNull(parent.Children); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(collectionEntry.IsLoaded); + + Assert.All(parent.Children.Select(e => e.Parent), p => Assert.Same(parent, p)); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(2, parent.Children.Count()); + } + + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_many_to_one_reference_to_principal_delegate_loader_with_state_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var child = context.Set().Single(e => e.Id == 12); + + ClearLog(); + + SetState(context, child, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(child.Parent); // Explicitly detached + } + else + { + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + } + else + { + Assert.NotNull(child.Parent); + } + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(child, child.Parent!.Children.Single()); + } + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + Assert.Null(parent.Children); + } + else + { + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.Children.Single()); + } + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_principal_delegate_loader_with_state_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var single = context.Set().Single(); + + ClearLog(); + + SetState(context, single, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(single.Parent); // Explicitly detached + } + else + { + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + } + else + { + Assert.NotNull(single.Parent); + } + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(single, single.Parent!.Single); + } + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.Single); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + } + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_dependent_delegate_loader_with_state_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Set().Single(); + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(parent).Reference(e => e.Single); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(parent.Single); // Explicitly detached + } + else + { + Assert.NotNull(parent.Single); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(parent, parent.Single.Parent); + } + + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(single, parent.Single); + Assert.Same(parent, single.Parent); + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK_delegate_loader_with_state_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var child = context.Attach(new ChildDelegateLoaderWithStateByProperty { Id = 767, ParentId = null }).Entity; + + ClearLog(); + + SetState(context, child, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(child.Parent); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Null(child.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK_delegate_loader_with_state_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var single = context.Attach(new SingleDelegateLoaderWithStateByProperty { Id = 767, ParentId = null }).Entity; + + ClearLog(); + + SetState(context, single, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(single.Parent); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + + Assert.Null(single.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_collection_not_found_delegate_loader_with_state_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Attach(new ParentDelegateLoaderWithStateByProperty { Id = 767 }).Entity; + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior, isAttached: true); + + var collectionEntry = context.Entry(parent).Collection(e => e.Children); + + Assert.False(collectionEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Detached) + { + Assert.Null(parent.Children); // Explicitly detached + } + else + { + Assert.Empty(parent.Children); + Assert.False(changeDetector.DetectChangesCalled); + Assert.True(collectionEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Single(context.ChangeTracker.Entries()); + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_many_to_one_reference_to_principal_not_found_delegate_loader_with_state_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var child = context.Attach(new ChildDelegateLoaderWithStateByProperty { Id = 767, ParentId = 787 }).Entity; + + ClearLog(); + + SetState(context, child, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(child.Parent); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Null(child.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_principal_not_found_delegate_loader_with_state_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var single = context.Attach(new SingleDelegateLoaderWithStateByProperty { Id = 767, ParentId = 787 }).Entity; + + ClearLog(); + + SetState(context, single, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(single.Parent); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + + Assert.Null(single.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_dependent_not_found_delegate_loader_with_state_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Attach(new ParentDelegateLoaderWithStateByProperty { Id = 767 }).Entity; + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(parent).Reference(e => e.Single); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(parent.Single); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Null(parent.Single); + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_collection_already_loaded_delegate_loader_with_state_property_injection( + EntityState state, + CascadeTiming deleteOrphansTiming, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + context.ChangeTracker.DeleteOrphansTiming = deleteOrphansTiming; + + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Set().Include(e => e.Children).Single(); + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior); + + var collectionEntry = context.Entry(parent).Collection(e => e.Children); + + Assert.True(collectionEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(parent.Children); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(collectionEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(2, parent.Children.Count()); + + if (queryTrackingBehavior == QueryTrackingBehavior.TrackAll + && state == EntityState.Deleted + && deleteOrphansTiming != CascadeTiming.Never) + { + Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Null(c)); + } + else + { + Assert.All(parent.Children.Select(e => e.Parent), p => Assert.Same(parent, p)); + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_many_to_one_reference_to_principal_already_loaded_delegate_loader_with_state_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var child = context.Set().Include(e => e.Parent).Single(e => e.Id == 12); + + ClearLog(); + + SetState(context, child.Parent, state, queryTrackingBehavior); + SetState(context, child, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + if (state == EntityState.Deleted && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.False(referenceEntry.IsLoaded); + Assert.Null(child.Parent); + } + else + { + Assert.True(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(child.Parent); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + Assert.Same(child, child.Parent.Children.Single()); + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.Children.Single()); + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_principal_already_loaded_delegate_loader_with_state_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var single = context.Set().Include(e => e.Parent).Single(); + + ClearLog(); + + SetState(context, single.Parent, state, queryTrackingBehavior); + SetState(context, single, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + if (state == EntityState.Deleted && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.False(referenceEntry.IsLoaded); + Assert.Null(single.Parent); + } + else + { + Assert.True(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(single.Parent); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + Assert.Same(single, single.Parent.Single); + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_dependent_already_loaded_delegate_loader_with_state_property_injection( + EntityState state, + CascadeTiming deleteOrphansTiming, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + context.ChangeTracker.DeleteOrphansTiming = deleteOrphansTiming; + + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Set().Include(e => e.Single).Single(); + + ClearLog(); + + SetState(context, parent.Single, state, queryTrackingBehavior); + SetState(context, parent, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(parent).Reference(e => e.Single); + + Assert.True(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(parent.Single); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state == EntityState.Deleted + && deleteOrphansTiming != CascadeTiming.Never) + { + Assert.Same(parent, parent.Single.Parent); + } + + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(single, parent.Single); + Assert.Same(parent, single.Parent); + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_collection_already_partially_loaded_delegate_loader_with_state_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + context.ChangeTracker.LazyLoadingEnabled = false; + + var child = context.Set().OrderBy(e => e.Id).First(); + var parent = context.Set().Single(); + if (parent.Children == null) + { + parent.Children = new List { child }; + child.Parent = parent; + } + + context.ChangeTracker.LazyLoadingEnabled = true; + + ClearLog(); + + SetState(context, child, state, queryTrackingBehavior); + SetState(context, parent, state, queryTrackingBehavior); + + var collectionEntry = context.Entry(parent).Collection(e => e.Children); + + Assert.False(collectionEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(parent.Children); + + Assert.False(changeDetector.DetectChangesCalled); + + RecordLog(); + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.False(collectionEntry.IsLoaded); // Explicitly detached + Assert.Equal(1, parent.Children.Count()); + + Assert.All(parent.Children.Select(e => e.Parent), p => Assert.Same(parent, p)); + } + else + { + Assert.True(collectionEntry.IsLoaded); + + context.ChangeTracker.LazyLoadingEnabled = false; + + // Note that when detached there is no identity resolution, so loading results in duplicates + Assert.Equal( + state == EntityState.Detached && queryTrackingBehavior != QueryTrackingBehavior.NoTrackingWithIdentityResolution + ? 3 + : 2, parent.Children.Count()); + + Assert.All(parent.Children.Select(e => e.Parent), p => Assert.Same(parent, p)); + } + } + [ConditionalFact] public virtual void Lazy_loading_uses_field_access_when_abstract_base_class_navigation() { diff --git a/test/EFCore.Specification.Tests/LoadTestBase.cs b/test/EFCore.Specification.Tests/LoadTestBase.cs index 790bd7f3287..d7c5bec1ea6 100644 --- a/test/EFCore.Specification.Tests/LoadTestBase.cs +++ b/test/EFCore.Specification.Tests/LoadTestBase.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema; using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Internal; // ReSharper disable InconsistentNaming namespace Microsoft.EntityFrameworkCore; @@ -5777,6 +5778,92 @@ public ParentDelegateLoaderByProperty Parent } } + protected class ParentDelegateLoaderWithStateByProperty + { + private IEnumerable _children; + private SingleDelegateLoaderWithStateByProperty _single; + + private object LazyLoaderState { get; set; } + private Action LazyLoader { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + + public IEnumerable Children + { + get => LazyLoader.Load(this, ref _children); + set => _children = value; + } + + public SingleDelegateLoaderWithStateByProperty Single + { + get => _single ?? LazyLoader.Load(this, ref _single); + set => _single = value; + } + } + + protected class ChildDelegateLoaderWithStateByProperty + { + private ParentDelegateLoaderWithStateByProperty _parent; + private int? _parentId; + + private object LazyLoaderState { get; set; } + private Action LazyLoader { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + + public int? ParentId + { + get => _parentId; + set + { + if (_parentId != value) + { + _parentId = value; + _parent = null; + } + } + } + + public ParentDelegateLoaderWithStateByProperty Parent + { + get => _parent ?? LazyLoader.Load(this, ref _parent); + set => _parent = value; + } + } + + protected class SingleDelegateLoaderWithStateByProperty + { + private ParentDelegateLoaderWithStateByProperty _parent; + private int? _parentId; + + private object LazyLoaderState { get; set; } + private Action LazyLoader { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + + public int? ParentId + { + get => _parentId; + set + { + if (_parentId != value) + { + _parentId = value; + _parent = null; + } + } + } + + public ParentDelegateLoaderWithStateByProperty Parent + { + get => _parent ?? LazyLoader.Load(this, ref _parent); + set => _parent = value; + } + } + protected DbContext CreateContext(bool lazyLoadingEnabled = false, bool noTracking = false) { var context = Fixture.CreateContext(); @@ -5922,6 +6009,50 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con .HasForeignKey(e => e.ParentId); }); + modelBuilder.Entity( + b => + { + var serviceProperty = (ServiceProperty)b.Metadata.AddServiceProperty( + typeof(ILazyLoader), + typeof(ChildDelegateLoaderWithStateByProperty).GetAnyProperty("LazyLoaderState")!); + + serviceProperty.SetParameterBinding( + new DependencyInjectionParameterBinding(typeof(object), typeof(ILazyLoader), serviceProperty), + ConfigurationSource.Explicit); + }); + + modelBuilder.Entity( + b => + { + var serviceProperty = (ServiceProperty)b.Metadata.AddServiceProperty( + typeof(ILazyLoader), + typeof(SingleDelegateLoaderWithStateByProperty).GetAnyProperty("LazyLoaderState")!); + + serviceProperty.SetParameterBinding( + new DependencyInjectionParameterBinding(typeof(object), typeof(ILazyLoader), serviceProperty), + ConfigurationSource.Explicit); + }); + + modelBuilder.Entity( + b => + { + var serviceProperty = (ServiceProperty)b.Metadata.AddServiceProperty( + typeof(ILazyLoader), + typeof(ParentDelegateLoaderWithStateByProperty).GetAnyProperty("LazyLoaderState")!); + + serviceProperty.SetParameterBinding( + new DependencyInjectionParameterBinding(typeof(object), typeof(ILazyLoader), serviceProperty), + ConfigurationSource.Explicit); + + b.HasMany(nameof(ParentDelegateLoaderWithStateByProperty.Children)) + .WithOne(nameof(ChildDelegateLoaderWithStateByProperty.Parent)) + .HasForeignKey(e => e.ParentId); + + b.HasOne(nameof(ParentDelegateLoaderWithStateByProperty.Single)) + .WithOne(e => e.Parent) + .HasForeignKey(e => e.ParentId); + }); + modelBuilder.Entity(); modelBuilder.Entity(); modelBuilder.Entity(); @@ -5974,6 +6105,14 @@ protected override void Seed(PoolableDbContext context) Single = new SingleDelegateLoaderByProperty { Id = 21 } }); + context.Add( + new ParentDelegateLoaderWithStateByProperty + { + Id = 707, + Children = new List { new() { Id = 11 }, new() { Id = 12 } }, + Single = new SingleDelegateLoaderWithStateByProperty { Id = 21 } + }); + context.Add( new SimpleProduct { Deposit = new Deposit() });