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