diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs index 4d8426e8534..0620b57332f 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs @@ -1257,6 +1257,12 @@ private void Create( .Append("eagerLoaded: ").Append(_code.Literal(true)); } + if (!navigation.LazyLoadingEnabled) + { + mainBuilder.AppendLine(",") + .Append("lazyLoadingEnabled: ").Append(_code.Literal(false)); + } + mainBuilder .AppendLine(");") .AppendLine() @@ -1346,6 +1352,12 @@ private void CreateSkipNavigation( .Append("eagerLoaded: ").Append(_code.Literal(true)); } + if (!navigation.LazyLoadingEnabled) + { + mainBuilder.AppendLine(",") + .Append("lazyLoadingEnabled: ").Append(_code.Literal(false)); + } + mainBuilder .AppendLine(");") .DecrementIndent(); diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs index f471a0f0efa..5b6bce5375d 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs @@ -472,16 +472,33 @@ private void SetServiceProperties(EntityState oldState, EntityState newState) { if (EntityType.HasServiceProperties()) { - if (oldState == EntityState.Detached) + List? dependentServices = null; + foreach (var serviceProperty in EntityType.GetServiceProperties()) { - foreach (var serviceProperty in EntityType.GetServiceProperties()) + var service = this[serviceProperty] ?? serviceProperty.ParameterBinding.ServiceDelegate( + new MaterializationContext(ValueBuffer.Empty, Context), EntityType, Entity); + + if (service == null) + { + (dependentServices ??= new List()).Add(serviceProperty); + } + else + { + if (service is IInjectableService injectableService) + { + injectableService.Attaching(Context, EntityType, Entity); + } + + this[serviceProperty] = service; + } + } + + if (dependentServices != null) + { + foreach (var serviceProperty in dependentServices) { - this[serviceProperty] = (this[serviceProperty] is IInjectableService injectableService - ? injectableService.Attaching(Context, Entity, injectableService) - : null) - ?? serviceProperty - .ParameterBinding - .ServiceDelegate(new MaterializationContext(ValueBuffer.Empty, Context), EntityType, Entity); + this[serviceProperty] = serviceProperty.ParameterBinding.ServiceDelegate( + new MaterializationContext(ValueBuffer.Empty, Context), EntityType, Entity); } } else if (newState == EntityState.Detached) diff --git a/src/EFCore/Infrastructure/AccessorExtensions.cs b/src/EFCore/Infrastructure/AccessorExtensions.cs index 739b0669673..9a26c0a111b 100644 --- a/src/EFCore/Infrastructure/AccessorExtensions.cs +++ b/src/EFCore/Infrastructure/AccessorExtensions.cs @@ -41,6 +41,26 @@ public static TService GetService(this IInfrastructure InfrastructureExtensions.GetService(accessor); + /// + /// Resolves a service from the exposed from a type that implements + /// . + /// + /// + /// + /// is used to hide properties that are not intended to be used in + /// application code but can be used in extension methods written by database providers etc. + /// + /// + /// See Accessing DbContext services for more information and examples. + /// + /// + /// The object exposing the service provider. + /// The type of service to be resolved. + /// The requested service. + [DebuggerStepThrough] + public static object GetService(this IInfrastructure accessor, Type serviceType) + => InfrastructureExtensions.GetService(serviceType, accessor); + /// /// /// Gets the value from a property that is being hidden using . diff --git a/src/EFCore/Infrastructure/Internal/InfrastructureExtensions.cs b/src/EFCore/Infrastructure/Internal/InfrastructureExtensions.cs index d8beb2e3a41..54de4072cd1 100644 --- a/src/EFCore/Infrastructure/Internal/InfrastructureExtensions.cs +++ b/src/EFCore/Infrastructure/Internal/InfrastructureExtensions.cs @@ -21,21 +21,30 @@ public static class InfrastructureExtensions /// public static TService GetService(IInfrastructure accessor) where TService : class + => (TService)GetService(typeof(TService), accessor); + + /// + /// 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 static object GetService(Type serviceType, IInfrastructure accessor) { var internalServiceProvider = accessor.Instance; - var service = internalServiceProvider.GetService(typeof(TService)) + var service = internalServiceProvider.GetService(serviceType) ?? internalServiceProvider.GetService() ?.Extensions.OfType().FirstOrDefault() ?.ApplicationServiceProvider - ?.GetService(typeof(TService)); + ?.GetService(serviceType); if (service == null) { throw new InvalidOperationException( - CoreStrings.NoProviderConfiguredFailedToResolveService(typeof(TService).DisplayName())); + CoreStrings.NoProviderConfiguredFailedToResolveService(serviceType.DisplayName())); } - return (TService)service; + return service; } } diff --git a/src/EFCore/Infrastructure/Internal/LazyLoader.cs b/src/EFCore/Infrastructure/Internal/LazyLoader.cs index 94e8fb2cf80..7535ac01ca4 100644 --- a/src/EFCore/Infrastructure/Internal/LazyLoader.cs +++ b/src/EFCore/Infrastructure/Internal/LazyLoader.cs @@ -20,6 +20,7 @@ public class LazyLoader : ILazyLoader, IInjectableService private bool _detached; private IDictionary? _loadedStates; private List<(object Entity, string NavigationName)>? _isLoading; + private IEntityType? _entityType; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -41,8 +42,11 @@ public LazyLoader( /// 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 void ServiceObtained(DbContext context, ParameterBindingInfo bindingInfo) - => _queryTrackingBehavior = bindingInfo.QueryTrackingBehavior; + public virtual void Injected(DbContext context, object entity, ParameterBindingInfo bindingInfo) + { + _queryTrackingBehavior = bindingInfo.QueryTrackingBehavior; + _entityType = bindingInfo.EntityType; + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -217,18 +221,24 @@ private bool ShouldLoad(object entity, string navigationName, [NotNullWhen(true) { if (!_detached && !IsLoaded(entity, navigationName)) { - if (_disposed) - { - Logger.LazyLoadOnDisposedContextWarning(Context, entity, navigationName); - } - else if (Context!.ChangeTracker.LazyLoadingEnabled) // Check again because the nav may be loaded without the loader knowing + var navigation = _entityType?.FindNavigation(navigationName) + ?? (INavigationBase?)_entityType?.FindSkipNavigation(navigationName); + + if (navigation?.LazyLoadingEnabled != false) { - navigationEntry = Context.Entry(entity).Navigation(navigationName); // Will use local-DetectChanges, if enabled. - if (!navigationEntry.IsLoaded) + if (_disposed) { - Logger.NavigationLazyLoading(Context, entity, navigationName); + Logger.LazyLoadOnDisposedContextWarning(Context, entity, navigationName); + } + else if (Context!.ChangeTracker.LazyLoadingEnabled) + { + navigationEntry = Context.Entry(entity).Navigation(navigationName); // Will use local-DetectChanges, if enabled. + if (!navigationEntry.IsLoaded) // Check again because the nav may be loaded without the loader knowing + { + Logger.NavigationLazyLoading(Context, entity, navigationName); - return true; + return true; + } } } } @@ -268,11 +278,11 @@ public virtual bool Detaching(DbContext context, object entity) /// 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 IInjectableService? Attaching(DbContext context, object entity, IInjectableService? existingService) + public virtual void Attaching(DbContext context, IEntityType entityType, object entity) { _disposed = false; _detached = false; + _entityType = entityType; Context = context; - return this; } } diff --git a/src/EFCore/Internal/IInjectableService.cs b/src/EFCore/Internal/IInjectableService.cs index 70946bc013b..237ca4588bc 100644 --- a/src/EFCore/Internal/IInjectableService.cs +++ b/src/EFCore/Internal/IInjectableService.cs @@ -22,7 +22,7 @@ public interface IInjectableService /// 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. /// - void ServiceObtained(DbContext context, ParameterBindingInfo bindingInfo); + void Injected(DbContext context, object entity, ParameterBindingInfo bindingInfo); /// /// @@ -52,11 +52,7 @@ public interface IInjectableService /// /// /// The instance. + /// The of the instance being attached. /// The entity instance that is being attached. - /// - /// The existing instance of this service being held by the entity instance, or if there - /// is no existing instance. - /// - /// The service instance to use, or if a new instance should be created. - IInjectableService? Attaching(DbContext context, object entity, IInjectableService? existingService); + void Attaching(DbContext context, IEntityType entityType, object entity); } diff --git a/src/EFCore/Metadata/Builders/IConventionNavigationBuilder.cs b/src/EFCore/Metadata/Builders/IConventionNavigationBuilder.cs index 5d494b6ed2e..cdcb4fe0fad 100644 --- a/src/EFCore/Metadata/Builders/IConventionNavigationBuilder.cs +++ b/src/EFCore/Metadata/Builders/IConventionNavigationBuilder.cs @@ -75,6 +75,26 @@ public interface IConventionNavigationBuilder : IConventionPropertyBaseBuilder /// IConventionNavigationBuilder? AutoInclude(bool? autoInclude, bool fromDataAnnotation = false); + /// + /// Returns a value indicating whether this navigation can be configured to enable lazy-loading + /// from the current configuration source. + /// + /// A value indicating whether the navigation should be enabled for lazy-loading. + /// Indicates whether the configuration was specified using a data annotation. + /// if automatically included can be set for this navigation. + bool CanSetLazyLoadingEnabled(bool? lazyLoadingEnabled, bool fromDataAnnotation = false); + + /// + /// Configures this navigation to be enabled for lazy-loading. + /// + /// A value indicating whether the navigation should be enabled for lazy-loading. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + IConventionNavigationBuilder? EnableLazyLoading(bool? lazyLoadingEnabled, bool fromDataAnnotation = false); + /// /// Returns a value indicating whether this navigation requiredness can be configured /// from the current configuration source. diff --git a/src/EFCore/Metadata/Builders/IConventionSkipNavigationBuilder.cs b/src/EFCore/Metadata/Builders/IConventionSkipNavigationBuilder.cs index cb7fd2a93db..b1a1f8d818b 100644 --- a/src/EFCore/Metadata/Builders/IConventionSkipNavigationBuilder.cs +++ b/src/EFCore/Metadata/Builders/IConventionSkipNavigationBuilder.cs @@ -124,4 +124,24 @@ public interface IConventionSkipNavigationBuilder : IConventionPropertyBaseBuild /// otherwise. /// IConventionSkipNavigationBuilder? AutoInclude(bool? autoInclude, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether this navigation can be configured to enable lazy-loading + /// from the current configuration source. + /// + /// A value indicating whether the navigation should be enabled for lazy-loading. + /// Indicates whether the configuration was specified using a data annotation. + /// if automatically included can be set for this navigation. + bool CanSetLazyLoadingEnabled(bool? lazyLoadingEnabled, bool fromDataAnnotation = false); + + /// + /// Configures this navigation to be enabled for lazy-loading. + /// + /// A value indicating whether the navigation should be enabled for lazy-loading. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + IConventionSkipNavigationBuilder? EnableLazyLoading(bool? lazyLoadingEnabled, bool fromDataAnnotation = false); } diff --git a/src/EFCore/Metadata/Builders/NavigationBuilder.cs b/src/EFCore/Metadata/Builders/NavigationBuilder.cs index 07dbffb14b6..cf4c2495f71 100644 --- a/src/EFCore/Metadata/Builders/NavigationBuilder.cs +++ b/src/EFCore/Metadata/Builders/NavigationBuilder.cs @@ -143,6 +143,31 @@ public virtual NavigationBuilder AutoInclude(bool autoInclude = true) return this; } + /// + /// Configures whether this navigation should be enabled for lazy-loading. Note that a property can only be lazy-loaded + /// if a lazy-loading mechanism such as lazy-loading proxies or injection has been configured. + /// + /// + /// + /// See Lazy loading for more information and examples. + /// + /// + /// A value indicating if the navigation should be enabled for lazy-loading. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual NavigationBuilder EnableLazyLoading(bool lazyLoadingEnabled = true) + { + if (InternalNavigationBuilder != null) + { + InternalNavigationBuilder.EnableLazyLoading(lazyLoadingEnabled, ConfigurationSource.Explicit); + } + else + { + InternalSkipNavigationBuilder!.EnableLazyLoading(lazyLoadingEnabled, ConfigurationSource.Explicit); + } + + return this; + } + /// /// Configures whether this navigation is required. /// diff --git a/src/EFCore/Metadata/Builders/NavigationBuilder`.cs b/src/EFCore/Metadata/Builders/NavigationBuilder`.cs index 41783a6d067..da9b6f4182c 100644 --- a/src/EFCore/Metadata/Builders/NavigationBuilder`.cs +++ b/src/EFCore/Metadata/Builders/NavigationBuilder`.cs @@ -81,6 +81,20 @@ public NavigationBuilder(IMutableNavigationBase navigationOrSkipNavigation) public new virtual NavigationBuilder AutoInclude(bool autoInclude = true) => (NavigationBuilder)base.AutoInclude(autoInclude); + /// + /// Configures whether this navigation should be enabled for lazy-loading. Note that a property can only be lazy-loaded + /// if a lazy-loading mechanism such as lazy-loading proxies or injection has been configured. + /// + /// + /// + /// See Lazy loading for more information and examples. + /// + /// + /// A value indicating if the navigation should be enabled for lazy-loading. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual NavigationBuilder EnableLazyLoading(bool lazyLoadingEnabled = true) + => (NavigationBuilder)base.EnableLazyLoading(lazyLoadingEnabled); + /// /// Configures whether this navigation is required. /// diff --git a/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs b/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs index dbe0837ff3c..430e3d7df1c 100644 --- a/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs +++ b/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs @@ -551,7 +551,8 @@ private static RuntimeNavigation Create(INavigation navigation, RuntimeForeignKe navigation.PropertyInfo, navigation.FieldInfo, navigation.GetPropertyAccessMode(), - navigation.IsEagerLoaded); + navigation.IsEagerLoaded, + navigation.LazyLoadingEnabled); /// /// Updates the navigation annotations that will be set on the read-only object. @@ -590,7 +591,8 @@ private RuntimeSkipNavigation Create(ISkipNavigation navigation, RuntimeEntityTy navigation.PropertyInfo, navigation.FieldInfo, navigation.GetPropertyAccessMode(), - navigation.IsEagerLoaded); + navigation.IsEagerLoaded, + navigation.LazyLoadingEnabled); /// /// Gets the corresponding foreign key in the read-optimized model. diff --git a/src/EFCore/Metadata/DependencyInjectionMethodParameterBinding.cs b/src/EFCore/Metadata/DependencyInjectionMethodParameterBinding.cs index afa1c2c7b54..b5b5f6f7d58 100644 --- a/src/EFCore/Metadata/DependencyInjectionMethodParameterBinding.cs +++ b/src/EFCore/Metadata/DependencyInjectionMethodParameterBinding.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.Internal; + namespace Microsoft.EntityFrameworkCore.Metadata; /// @@ -13,6 +16,18 @@ namespace Microsoft.EntityFrameworkCore.Metadata; /// public class DependencyInjectionMethodParameterBinding : DependencyInjectionParameterBinding { + private static readonly MethodInfo GetServiceMethod + = typeof(InfrastructureExtensions).GetRuntimeMethod( + nameof(InfrastructureExtensions.GetService), new[] { typeof(IInfrastructure) })!; + + private static readonly MethodInfo GetServiceFromPropertyMethod + = typeof(DependencyInjectionMethodParameterBinding).GetTypeInfo().GetDeclaredMethod(nameof(GetServiceFromProperty))!; + + private static readonly MethodInfo CreateServiceMethod + = typeof(DependencyInjectionMethodParameterBinding).GetTypeInfo().GetDeclaredMethod(nameof(CreateService))!; + + private Func? _serviceDelegate; + /// /// Creates a new instance for the given method /// of the given service type. @@ -42,43 +57,102 @@ public DependencyInjectionMethodParameterBinding( /// Creates an expression tree representing the binding of the value of a property from a /// materialization expression to a parameter of the constructor, factory method, etc. /// - /// The expression representing the materialization context. - /// The expression representing the constant. + /// The binding information. /// The expression tree. - public override Expression BindToParameter( - Expression materializationExpression, - Expression bindingInfoExpression) + public override Expression BindToParameter(ParameterBindingInfo bindingInfo) { - Check.NotNull(materializationExpression, nameof(materializationExpression)); - Check.NotNull(bindingInfoExpression, nameof(bindingInfoExpression)); + var serviceInstance = bindingInfo.ServiceInstances.FirstOrDefault(e => e.Type == ServiceType); + if (serviceInstance != null) + { + var parameters = Method.GetParameters().Select( + (p, i) => Expression.Parameter(p.ParameterType, "param" + i)).ToArray(); - var parameters = Method.GetParameters().Select( - (p, i) => Expression.Parameter(p.ParameterType, "param" + i)).ToArray(); + return Expression.Condition( + Expression.ReferenceEqual(serviceInstance, Expression.Constant(null)), + Expression.Constant(null, ParameterType), + Expression.Lambda( + Expression.Call( + serviceInstance, + Method, + parameters), + parameters)); + } + + return base.BindToParameter(bindingInfo); + } + + private static object? GetServiceFromProperty(MaterializationContext materializationContext, IPropertyBase property, object entity) + => materializationContext.Context.GetDependencies().StateManager.GetOrCreateEntry(entity)[property]; + + private static object CreateService( + MaterializationContext materializationContext, Type serviceType, IEntityType entityType, object entity) + { + var service = materializationContext.Context.GetService(serviceType); - var serviceVariable = Expression.Variable(ServiceType, "service"); - var delegateVariable = Expression.Variable(ParameterType, "delegate"); + if (service is IInjectableService injectableService) + { + injectableService.Attaching(materializationContext.Context, entityType, entity); + } - return Expression.Block( - new[] { serviceVariable, delegateVariable }, - new List + return service; + } + + /// + /// A delegate to set a CLR service property on an entity instance. + /// + public override Func ServiceDelegate + => NonCapturingLazyInitializer.EnsureInitialized( + ref _serviceDelegate, this, static b => { - Expression.Assign( - serviceVariable, - base.BindToParameter(materializationExpression, bindingInfoExpression)), - Expression.Assign( - delegateVariable, - Expression.Condition( - Expression.ReferenceEqual(serviceVariable, Expression.Constant(null)), - Expression.Constant(null, ParameterType), - Expression.Lambda( - Expression.Call( - serviceVariable, - Method, - parameters), - parameters))), - delegateVariable + var materializationContextParam = Expression.Parameter(typeof(MaterializationContext)); + var entityTypeParam = Expression.Parameter(typeof(IEntityType)); + var entityParam = Expression.Parameter(typeof(object)); + + var parameters = b.Method.GetParameters().Select( + (p, i) => Expression.Parameter(p.ParameterType, "param" + i)).ToArray(); + + var entityType = (IEntityType)b.ConsumedProperties.First().DeclaringType; + var serviceStateProperty = entityType.GetServiceProperties().FirstOrDefault( + p => p.ParameterBinding != b && p.ParameterBinding.ServiceType == b.ServiceType); + + var serviceVariable = Expression.Variable(b.ServiceType, "service"); + var serviceExpression = Expression.Block( + new[] { serviceVariable }, + new List + { + Expression.Assign( + serviceVariable, + Expression.Convert( + serviceStateProperty == null + ? Expression.Call( + CreateServiceMethod, + materializationContextParam, + Expression.Constant(b.ServiceType), + entityTypeParam, + entityParam) + : Expression.Call( + GetServiceFromPropertyMethod, + materializationContextParam, + Expression.Constant(serviceStateProperty, typeof(IPropertyBase)), + entityParam), + typeof(ILazyLoader))), + Expression.Condition( + Expression.ReferenceEqual(serviceVariable, Expression.Constant(null)), + Expression.Constant(null, b.ParameterType), + Expression.Lambda( + Expression.Call( + serviceVariable, + b.Method, + parameters), + parameters)) + }); + + return Expression.Lambda>( + serviceExpression, + materializationContextParam, + entityTypeParam, + entityParam).Compile(); }); - } /// /// Creates a copy that contains the given consumed properties. diff --git a/src/EFCore/Metadata/DependencyInjectionParameterBinding.cs b/src/EFCore/Metadata/DependencyInjectionParameterBinding.cs index fa4e3fce812..1424a74a4b0 100644 --- a/src/EFCore/Metadata/DependencyInjectionParameterBinding.cs +++ b/src/EFCore/Metadata/DependencyInjectionParameterBinding.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.EntityFrameworkCore.Infrastructure.Internal; -using Microsoft.EntityFrameworkCore.Internal; namespace Microsoft.EntityFrameworkCore.Metadata; @@ -17,10 +16,8 @@ namespace Microsoft.EntityFrameworkCore.Metadata; public class DependencyInjectionParameterBinding : ServiceParameterBinding { private static readonly MethodInfo GetServiceMethod - = typeof(InfrastructureExtensions).GetMethod(nameof(InfrastructureExtensions.GetService))!; - - private static readonly MethodInfo InjectableServiceServiceObtainedMethod - = typeof(IInjectableService).GetMethod(nameof(IInjectableService.ServiceObtained))!; + = typeof(InfrastructureExtensions).GetRuntimeMethod( + nameof(InfrastructureExtensions.GetService), new[] { typeof(IInfrastructure) })!; /// /// Creates a new instance for the given service type. @@ -50,22 +47,13 @@ public override Expression BindToParameter( Check.NotNull(materializationExpression, nameof(materializationExpression)); Check.NotNull(bindingInfoExpression, nameof(bindingInfoExpression)); - var serviceVariable = Expression.Variable(ServiceType); - var getContext = Expression.Property(materializationExpression, MaterializationContext.ContextProperty); - return Expression.Block( - variables: new[] { serviceVariable }, - Expression.Assign( - serviceVariable, Expression.Call( - GetServiceMethod.MakeGenericMethod(ServiceType), - Expression.Convert(getContext, typeof(IInfrastructure)))), - Expression.IfThen( - Expression.TypeIs(serviceVariable, typeof(IInjectableService)), - Expression.Call( - Expression.Convert(serviceVariable, typeof(IInjectableService)), - InjectableServiceServiceObtainedMethod, - getContext, - bindingInfoExpression)), - serviceVariable); + return Expression.Call( + GetServiceMethod.MakeGenericMethod(ServiceType), + Expression.Convert( + Expression.Property( + materializationExpression, + MaterializationContext.ContextProperty), + typeof(IInfrastructure))); } /// @@ -75,4 +63,10 @@ public override Expression BindToParameter( /// A copy with replaced consumed properties. public override ParameterBinding With(IPropertyBase[] consumedProperties) => new DependencyInjectionParameterBinding(ParameterType, ServiceType, consumedProperties); + + /// + /// A delegate to set a CLR service property on an entity instance. + /// + public override Func ServiceDelegate + => (materializationContext, _, _) => materializationContext.Context.GetService(ServiceType); } diff --git a/src/EFCore/Metadata/IConventionNavigationBase.cs b/src/EFCore/Metadata/IConventionNavigationBase.cs index 17cb491991c..e7b92c910b7 100644 --- a/src/EFCore/Metadata/IConventionNavigationBase.cs +++ b/src/EFCore/Metadata/IConventionNavigationBase.cs @@ -34,4 +34,20 @@ public interface IConventionNavigationBase : IReadOnlyNavigationBase, IConventio /// The configuration source for . ConfigurationSource? GetIsEagerLoadedConfigurationSource() => FindAnnotation(CoreAnnotationNames.EagerLoaded)?.GetConfigurationSource(); + + /// + /// Sets a value indicating whether this navigation should be lazy-loaded, if lazy-loading is enabled and in place. + /// + /// A value indicating whether this navigation should lazy-loaded. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + bool? SetLazyLoadingEnabled(bool? lazyLoadingEnabled, bool fromDataAnnotation = false) + => (bool?)SetOrRemoveAnnotation(CoreAnnotationNames.LazyLoadingEnabled, lazyLoadingEnabled, fromDataAnnotation)?.Value; + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetLazyLoadingEnabledConfigurationSource() + => FindAnnotation(CoreAnnotationNames.LazyLoadingEnabled)?.GetConfigurationSource(); } diff --git a/src/EFCore/Metadata/IMutableNavigationBase.cs b/src/EFCore/Metadata/IMutableNavigationBase.cs index ee4f3602698..0502e94cb19 100644 --- a/src/EFCore/Metadata/IMutableNavigationBase.cs +++ b/src/EFCore/Metadata/IMutableNavigationBase.cs @@ -26,4 +26,11 @@ public interface IMutableNavigationBase : IReadOnlyNavigationBase, IMutablePrope /// A value indicating whether this navigation should be eager loaded by default. void SetIsEagerLoaded(bool? eagerLoaded) => SetOrRemoveAnnotation(CoreAnnotationNames.EagerLoaded, eagerLoaded); + + /// + /// Sets a value indicating whether this navigation should be enabled for lazy-loading. + /// + /// A value indicating whether this navigation should enabled for lazy-loading. + void SetLazyLoadingEnabled(bool? lazyLoadingEnabled) + => SetOrRemoveAnnotation(CoreAnnotationNames.LazyLoadingEnabled, lazyLoadingEnabled); } diff --git a/src/EFCore/Metadata/IReadOnlyNavigationBase.cs b/src/EFCore/Metadata/IReadOnlyNavigationBase.cs index e25633532c7..b8cbc006fcd 100644 --- a/src/EFCore/Metadata/IReadOnlyNavigationBase.cs +++ b/src/EFCore/Metadata/IReadOnlyNavigationBase.cs @@ -39,6 +39,18 @@ public interface IReadOnlyNavigationBase : IReadOnlyPropertyBase bool IsEagerLoaded => (bool?)this[CoreAnnotationNames.EagerLoaded] ?? false; + /// + /// Determines whether or not this navigation should lazy-load if lazy-loading is enabled and a mechanism for lazy-loading + /// has been configured in the model. + /// + /// + /// + /// See Lazy loading for more information and examples. + /// + /// + bool LazyLoadingEnabled + => (bool?)this[CoreAnnotationNames.LazyLoadingEnabled] ?? true; + /// // TODO: Remove when #3864 is implemented bool IReadOnlyPropertyBase.IsShadowProperty() diff --git a/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs b/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs index 1bf90857a09..8b399f698cb 100644 --- a/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs +++ b/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs @@ -196,6 +196,14 @@ public static class CoreAnnotationNames /// public const string EagerLoaded = "EagerLoaded"; + /// + /// 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 const string LazyLoadingEnabled = "LazyLoadingEnabled"; + /// /// 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 @@ -325,6 +333,7 @@ public static class CoreAnnotationNames DefiningQuery, #pragma warning restore CS0612 // Type or member is obsolete EagerLoaded, + LazyLoadingEnabled, ProviderClrType, ModelDependencies, ReadOnlyModel, diff --git a/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs b/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs index 2267274cf5a..9071a287064 100644 --- a/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs @@ -2782,6 +2782,14 @@ private static InternalForeignKeyBuilder MergeFacetsFrom(Navigation newNavigatio ((IReadOnlyNavigation)oldNavigation).IsEagerLoaded, oldIsEagerLoadedConfigurationSource.Value)!; } + var oldLazyLoadingEnabledConfigurationSource = ((IConventionNavigation)oldNavigation).GetLazyLoadingEnabledConfigurationSource(); + if (oldLazyLoadingEnabledConfigurationSource.HasValue + && builder.CanSetLazyLoadingEnabled(((IReadOnlyNavigation)oldNavigation).LazyLoadingEnabled, oldLazyLoadingEnabledConfigurationSource.Value)) + { + builder = builder.EnableLazyLoading( + ((IReadOnlyNavigation)oldNavigation).LazyLoadingEnabled, oldLazyLoadingEnabledConfigurationSource.Value)!; + } + return builder.Metadata.ForeignKey.Builder; } diff --git a/src/EFCore/Metadata/Internal/InternalNavigationBuilder.cs b/src/EFCore/Metadata/Internal/InternalNavigationBuilder.cs index 3edd0485cfb..766dbfe52c5 100644 --- a/src/EFCore/Metadata/Internal/InternalNavigationBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalNavigationBuilder.cs @@ -91,6 +91,46 @@ public virtual bool CanSetAutoInclude(bool? autoInclude, ConfigurationSource con return null; } + /// + /// 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 bool CanSetLazyLoadingEnabled(bool? lazyLoadingEnabled, ConfigurationSource configurationSource) + { + IConventionNavigation conventionNavigation = Metadata; + + return configurationSource.Overrides(conventionNavigation.GetLazyLoadingEnabledConfigurationSource()) + || conventionNavigation.LazyLoadingEnabled == lazyLoadingEnabled; + } + + /// + /// 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 InternalNavigationBuilder? EnableLazyLoading(bool? lazyLoadingEnabled, ConfigurationSource configurationSource) + { + if (CanSetLazyLoadingEnabled(lazyLoadingEnabled, configurationSource)) + { + if (configurationSource == ConfigurationSource.Explicit) + { + ((IMutableNavigation)Metadata).SetLazyLoadingEnabled(lazyLoadingEnabled); + } + else + { + ((IConventionNavigation)Metadata).SetLazyLoadingEnabled( + lazyLoadingEnabled, configurationSource == ConfigurationSource.DataAnnotation); + } + + return this; + } + + return null; + } + /// /// 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 @@ -239,6 +279,16 @@ bool IConventionNavigationBuilder.CanSetAutoInclude(bool? autoInclude, bool from IConventionNavigationBuilder? IConventionNavigationBuilder.AutoInclude(bool? autoInclude, bool fromDataAnnotation) => AutoInclude(autoInclude, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// + [DebuggerStepThrough] + bool IConventionNavigationBuilder.CanSetLazyLoadingEnabled(bool? lazyLoadingEnabled, bool fromDataAnnotation) + => CanSetLazyLoadingEnabled(lazyLoadingEnabled, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionNavigationBuilder? IConventionNavigationBuilder.EnableLazyLoading(bool? lazyLoadingEnabled, bool fromDataAnnotation) + => EnableLazyLoading(lazyLoadingEnabled, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// [DebuggerStepThrough] bool IConventionNavigationBuilder.CanSetIsRequired(bool? required, bool fromDataAnnotation) diff --git a/src/EFCore/Metadata/Internal/InternalSkipNavigationBuilder.cs b/src/EFCore/Metadata/Internal/InternalSkipNavigationBuilder.cs index c25ecac350d..d0b13f59883 100644 --- a/src/EFCore/Metadata/Internal/InternalSkipNavigationBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalSkipNavigationBuilder.cs @@ -336,6 +336,46 @@ public virtual bool CanSetAutoInclude(bool? autoInclude, ConfigurationSource con return null; } + /// + /// 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 bool CanSetLazyLoadingEnabled(bool? lazyLoadingEnabled, ConfigurationSource configurationSource) + { + IConventionSkipNavigation conventionNavigation = Metadata; + + return configurationSource.Overrides(conventionNavigation.GetLazyLoadingEnabledConfigurationSource()) + || conventionNavigation.LazyLoadingEnabled == lazyLoadingEnabled; + } + + /// + /// 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 InternalSkipNavigationBuilder? EnableLazyLoading(bool? lazyLoadingEnabled, ConfigurationSource configurationSource) + { + if (CanSetLazyLoadingEnabled(lazyLoadingEnabled, configurationSource)) + { + if (configurationSource == ConfigurationSource.Explicit) + { + ((IMutableSkipNavigation)Metadata).SetLazyLoadingEnabled(lazyLoadingEnabled); + } + else + { + ((IConventionSkipNavigation)Metadata).SetLazyLoadingEnabled( + lazyLoadingEnabled, configurationSource == ConfigurationSource.DataAnnotation); + } + + return this; + } + + return null; + } + IConventionPropertyBase IConventionPropertyBaseBuilder.Metadata { [DebuggerStepThrough] @@ -462,4 +502,14 @@ bool IConventionSkipNavigationBuilder.CanSetAutoInclude(bool? autoInclude, bool [DebuggerStepThrough] IConventionSkipNavigationBuilder? IConventionSkipNavigationBuilder.AutoInclude(bool? autoInclude, bool fromDataAnnotation) => AutoInclude(autoInclude, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionSkipNavigationBuilder.CanSetLazyLoadingEnabled(bool? lazyLoadingEnabled, bool fromDataAnnotation) + => CanSetLazyLoadingEnabled(lazyLoadingEnabled, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionSkipNavigationBuilder? IConventionSkipNavigationBuilder.EnableLazyLoading(bool? lazyLoadingEnabled, bool fromDataAnnotation) + => EnableLazyLoading(lazyLoadingEnabled, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); } diff --git a/src/EFCore/Metadata/ParameterBindingInfo.cs b/src/EFCore/Metadata/ParameterBindingInfo.cs index 7b10f3ef771..e946d9871d7 100644 --- a/src/EFCore/Metadata/ParameterBindingInfo.cs +++ b/src/EFCore/Metadata/ParameterBindingInfo.cs @@ -63,6 +63,11 @@ public ParameterBindingInfo( /// public Expression MaterializationContextExpression { get; } + /// + /// Expressions holding initialized instances for service properties. + /// + public List ServiceInstances { get; } = new(); + /// /// Gets the index into the where the property value can be found. /// diff --git a/src/EFCore/Metadata/RuntimeEntityType.cs b/src/EFCore/Metadata/RuntimeEntityType.cs index c81d04a2e03..fe19389f203 100644 --- a/src/EFCore/Metadata/RuntimeEntityType.cs +++ b/src/EFCore/Metadata/RuntimeEntityType.cs @@ -365,6 +365,7 @@ private IEnumerable GetDeclaredReferencingForeignKeys() /// The corresponding CLR field or for a shadow navigation. /// The used for this navigation. /// A value indicating whether this navigation should be eager loaded by default. + /// A value indicating whether this navigation should be enabled for lazy-loading. /// The newly created navigation property. public virtual RuntimeNavigation AddNavigation( string name, @@ -374,9 +375,10 @@ public virtual RuntimeNavigation AddNavigation( PropertyInfo? propertyInfo = null, FieldInfo? fieldInfo = null, PropertyAccessMode propertyAccessMode = Internal.Model.DefaultPropertyAccessMode, - bool eagerLoaded = false) + bool eagerLoaded = false, + bool lazyLoadingEnabled = true) { - var navigation = new RuntimeNavigation(name, clrType, propertyInfo, fieldInfo, foreignKey, propertyAccessMode, eagerLoaded); + var navigation = new RuntimeNavigation(name, clrType, propertyInfo, fieldInfo, foreignKey, propertyAccessMode, eagerLoaded, lazyLoadingEnabled); _navigations.Add(name, navigation); @@ -421,6 +423,7 @@ private IEnumerable GetNavigations() /// The corresponding CLR field or for a shadow navigation. /// The used for this navigation. /// A value indicating whether this navigation should be eager loaded by default. + /// A value indicating whether this navigation should be enabled for lazy-loading. /// The newly created skip navigation property. public virtual RuntimeSkipNavigation AddSkipNavigation( string name, @@ -432,7 +435,8 @@ public virtual RuntimeSkipNavigation AddSkipNavigation( PropertyInfo? propertyInfo = null, FieldInfo? fieldInfo = null, PropertyAccessMode propertyAccessMode = Internal.Model.DefaultPropertyAccessMode, - bool eagerLoaded = false) + bool eagerLoaded = false, + bool lazyLoadingEnabled = true) { var skipNavigation = new RuntimeSkipNavigation( name, @@ -445,7 +449,8 @@ public virtual RuntimeSkipNavigation AddSkipNavigation( collection, onDependent, propertyAccessMode, - eagerLoaded); + eagerLoaded, + lazyLoadingEnabled); _skipNavigations.Add(name, skipNavigation); diff --git a/src/EFCore/Metadata/RuntimeNavigation.cs b/src/EFCore/Metadata/RuntimeNavigation.cs index 427292af81a..3e6fb2f38b4 100644 --- a/src/EFCore/Metadata/RuntimeNavigation.cs +++ b/src/EFCore/Metadata/RuntimeNavigation.cs @@ -33,7 +33,8 @@ public RuntimeNavigation( FieldInfo? fieldInfo, RuntimeForeignKey foreignKey, PropertyAccessMode propertyAccessMode, - bool eagerLoaded) + bool eagerLoaded, + bool lazyLoadingEnabled) : base(name, propertyInfo, fieldInfo, propertyAccessMode) { ClrType = clrType; @@ -42,6 +43,10 @@ public RuntimeNavigation( { SetAnnotation(CoreAnnotationNames.EagerLoaded, true); } + if (!lazyLoadingEnabled) + { + SetAnnotation(CoreAnnotationNames.LazyLoadingEnabled, false); + } } /// diff --git a/src/EFCore/Metadata/RuntimeSkipNavigation.cs b/src/EFCore/Metadata/RuntimeSkipNavigation.cs index f5093298837..515d7819fb1 100644 --- a/src/EFCore/Metadata/RuntimeSkipNavigation.cs +++ b/src/EFCore/Metadata/RuntimeSkipNavigation.cs @@ -43,7 +43,8 @@ public RuntimeSkipNavigation( bool collection, bool onDependent, PropertyAccessMode propertyAccessMode, - bool eagerLoaded) + bool eagerLoaded, + bool lazyLoadingEnabled) : base(name, propertyInfo, fieldInfo, propertyAccessMode) { ClrType = clrType; @@ -65,6 +66,10 @@ public RuntimeSkipNavigation( { SetAnnotation(CoreAnnotationNames.EagerLoaded, true); } + if (!lazyLoadingEnabled) + { + SetAnnotation(CoreAnnotationNames.LazyLoadingEnabled, false); + } } /// diff --git a/src/EFCore/Metadata/ServiceParameterBinding.cs b/src/EFCore/Metadata/ServiceParameterBinding.cs index 00c519f4c4b..afc9578e53f 100644 --- a/src/EFCore/Metadata/ServiceParameterBinding.cs +++ b/src/EFCore/Metadata/ServiceParameterBinding.cs @@ -47,9 +47,17 @@ protected ServiceParameterBinding( /// The binding information. /// The expression tree. public override Expression BindToParameter(ParameterBindingInfo bindingInfo) - => BindToParameter( + { + var serviceInstance = bindingInfo.ServiceInstances.FirstOrDefault(e => e.Type == ServiceType); + if (serviceInstance != null) + { + return serviceInstance; + } + + return BindToParameter( bindingInfo.MaterializationContextExpression, Expression.Constant(bindingInfo)); + } /// /// Creates an expression tree representing the binding of the value of a property from a @@ -65,7 +73,7 @@ public abstract Expression BindToParameter( /// /// A delegate to set a CLR service property on an entity instance. /// - public virtual Func ServiceDelegate + public virtual Func ServiceDelegate => NonCapturingLazyInitializer.EnsureInitialized( ref _serviceDelegate, this, static b => { diff --git a/src/EFCore/Query/Internal/EntityMaterializerSource.cs b/src/EFCore/Query/Internal/EntityMaterializerSource.cs index 58fb8c61702..388b249c9e3 100644 --- a/src/EFCore/Query/Internal/EntityMaterializerSource.cs +++ b/src/EFCore/Query/Internal/EntityMaterializerSource.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using Microsoft.EntityFrameworkCore.Diagnostics.Internal; +using Microsoft.EntityFrameworkCore.Internal; namespace Microsoft.EntityFrameworkCore.Query.Internal; @@ -14,6 +15,9 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal; /// public class EntityMaterializerSource : IEntityMaterializerSource { + private static readonly MethodInfo InjectableServiceInjectedMethod + = typeof(IInjectableService).GetMethod(nameof(IInjectableService.Injected))!; + private ConcurrentDictionary>? _materializers; private ConcurrentDictionary>? _emptyMaterializers; private readonly List _bindingInterceptors; @@ -64,8 +68,7 @@ Expression IEntityMaterializerSource.CreateMaterializeExpression( EntityMaterializerSourceParameters parameters, Expression materializationContextExpression) { - var (entityType, entityInstanceName, queryTrackingBehavior) - = (parameters.EntityType, parameters.EntityInstanceName, parameters.QueryTrackingBehavior); + var (entityType, entityInstanceName) = (parameters.EntityType, parameters.EntityInstanceName); if (entityType.IsAbstract()) { @@ -73,11 +76,17 @@ Expression IEntityMaterializerSource.CreateMaterializeExpression( } var constructorBinding = ModifyBindings(entityType, entityType.ConstructorBinding!); - var bindingInfo = new ParameterBindingInfo(parameters, materializationContextExpression); + var serviceProperties = entityType.GetServiceProperties().ToList(); + var blockExpressions = new List(); + + var instanceVariable = Expression.Variable(constructorBinding.RuntimeType, entityInstanceName); + bindingInfo.ServiceInstances.Add(instanceVariable); + + CreateServiceInstances(constructorBinding, bindingInfo, blockExpressions, serviceProperties); var properties = new HashSet( - entityType.GetServiceProperties().Cast() + serviceProperties.Cast() .Concat( entityType .GetProperties() @@ -94,47 +103,33 @@ Expression IEntityMaterializerSource.CreateMaterializeExpression( if (_materializationInterceptor == null) { - if (properties.Count == 0) + if (properties.Count == 0 && blockExpressions.Count == 0) { return constructorExpression; } - var instanceVariable = Expression.Variable(constructorBinding.RuntimeType, entityInstanceName); - - var blockExpressions - = new List - { - Expression.Assign( - instanceVariable, - constructorExpression) - }; - - AddInitializeExpressions(properties, bindingInfo, materializationContextExpression, instanceVariable, blockExpressions); - - blockExpressions.Add(instanceVariable); - - return Expression.Block(new[] { instanceVariable }, blockExpressions); + return CreateMaterializeExpression(blockExpressions, instanceVariable, constructorExpression, properties, bindingInfo); } return CreateInterceptionMaterializeExpression( entityType, - entityInstanceName, properties, _materializationInterceptor, - constructorBinding, bindingInfo, constructorExpression, - materializationContextExpression); + instanceVariable, + blockExpressions); } private static void AddInitializeExpressions( HashSet properties, ParameterBindingInfo bindingInfo, - Expression materializationContextExpression, Expression instanceVariable, List blockExpressions) { - var valueBufferExpression = Expression.Call(materializationContextExpression, MaterializationContext.GetValueBufferMethod); + var valueBufferExpression = Expression.Call( + bindingInfo.MaterializationContextExpression, + MaterializationContext.GetValueBufferMethod); foreach (var property in properties) { @@ -159,6 +154,27 @@ static Expression CreateMemberAssignment(Expression parameter, MemberInfo member : Expression.MakeMemberAccess(parameter, memberInfo).Assign(value); } + private static void AddAttachServiceExpressions( + ParameterBindingInfo bindingInfo, + Expression instanceVariable, + List blockExpressions) + { + var getContext = Expression.Property(bindingInfo.MaterializationContextExpression, MaterializationContext.ContextProperty); + + foreach (var serviceInstance in bindingInfo.ServiceInstances) + { + blockExpressions.Add( + Expression.IfThen( + Expression.TypeIs(serviceInstance, typeof(IInjectableService)), + Expression.Call( + Expression.Convert(serviceInstance, typeof(IInjectableService)), + InjectableServiceInjectedMethod, + getContext, + instanceVariable, + Expression.Constant(bindingInfo, typeof(ParameterBindingInfo))))); + } + } + private static readonly ConstructorInfo MaterializationInterceptionDataConstructor = typeof(MaterializationInterceptionData).GetDeclaredConstructor( new[] @@ -199,15 +215,31 @@ private static readonly ConstructorInfo DictionaryConstructor = typeof(ValueTuple>).GetConstructor( new[] { typeof(object), typeof(Func) })!; + private static Expression CreateMaterializeExpression( + List blockExpressions, + ParameterExpression instanceVariable, + Expression constructorExpression, + HashSet properties, + ParameterBindingInfo bindingInfo) + { + blockExpressions.Add(Expression.Assign(instanceVariable, constructorExpression)); + + AddInitializeExpressions(properties, bindingInfo, instanceVariable, blockExpressions); + AddAttachServiceExpressions(bindingInfo, instanceVariable, blockExpressions); + + blockExpressions.Add(instanceVariable); + + return Expression.Block(bindingInfo.ServiceInstances, blockExpressions); + } + private static Expression CreateInterceptionMaterializeExpression( IEntityType entityType, - string entityInstanceName, HashSet properties, IMaterializationInterceptor materializationInterceptor, - InstantiationBinding constructorBinding, ParameterBindingInfo bindingInfo, Expression constructorExpression, - Expression materializationContextExpression) + ParameterExpression instanceVariable, + List blockExpressions) { // Something like: // Dictionary)> accessorFactory = CreateAccessors() @@ -225,33 +257,34 @@ private static Expression CreateInterceptionMaterializeExpression( // // return instance; - var instanceVariable = Expression.Variable(constructorBinding.RuntimeType, entityInstanceName); var materializationDataVariable = Expression.Variable(typeof(MaterializationInterceptionData), "materializationData"); var creatingResultVariable = Expression.Variable(typeof(InterceptionResult), "creatingResult"); var interceptorExpression = Expression.Constant(materializationInterceptor, typeof(IMaterializationInterceptor)); var accessorDictionaryVariable = Expression.Variable( typeof(Dictionary)>), "accessorDictionary"); - var blockExpressions = new List - { + blockExpressions.Add( Expression.Assign( accessorDictionaryVariable, - CreateAccessorDictionaryExpression()), + CreateAccessorDictionaryExpression())); + blockExpressions.Add( Expression.Assign( materializationDataVariable, Expression.New( MaterializationInterceptionDataConstructor, - materializationContextExpression, + bindingInfo.MaterializationContextExpression, Expression.Constant(entityType), Expression.Constant(bindingInfo.QueryTrackingBehavior, typeof(QueryTrackingBehavior?)), - accessorDictionaryVariable)), + accessorDictionaryVariable))); + blockExpressions.Add( Expression.Assign( creatingResultVariable, Expression.Call( interceptorExpression, CreatingInstanceMethod, materializationDataVariable, - Expression.Default(typeof(InterceptionResult)))), + Expression.Default(typeof(InterceptionResult))))); + blockExpressions.Add( Expression.Assign( instanceVariable, Expression.Convert( @@ -269,7 +302,8 @@ private static Expression CreateInterceptionMaterializeExpression( ResultProperty), instanceVariable.Type), constructorExpression)), - instanceVariable.Type)), + instanceVariable.Type))); + blockExpressions.Add( properties.Count == 0 ? Expression.Call( interceptorExpression, @@ -287,7 +321,8 @@ private static Expression CreateInterceptionMaterializeExpression( instanceVariable, Expression.Default(typeof(InterceptionResult))), IsSuppressedProperty)), - CreateInitializeExpression()), + CreateInitializeExpression())); + blockExpressions.Add( Expression.Assign( instanceVariable, Expression.Convert( @@ -296,20 +331,18 @@ private static Expression CreateInterceptionMaterializeExpression( InitializedInstanceMethod, materializationDataVariable, instanceVariable), - instanceVariable.Type)) - }; - - blockExpressions.Add(instanceVariable); + instanceVariable.Type))); return Expression.Block( - new[] { accessorDictionaryVariable, instanceVariable, materializationDataVariable, creatingResultVariable }, + bindingInfo.ServiceInstances.Concat(new[] { accessorDictionaryVariable, materializationDataVariable, creatingResultVariable }), blockExpressions); BlockExpression CreateAccessorDictionaryExpression() { var dictionaryVariable = Expression.Variable( typeof(Dictionary)>), "dictionary"); - var valueBufferExpression = Expression.Call(materializationContextExpression, MaterializationContext.GetValueBufferMethod); + var valueBufferExpression = Expression.Call( + bindingInfo.MaterializationContextExpression, MaterializationContext.GetValueBufferMethod); var snapshotBlockExpressions = new List { Expression.Assign( @@ -331,10 +364,10 @@ BlockExpression CreateAccessorDictionaryExpression() Expression.Lambda( typeof(Func<,>).MakeGenericType(typeof(MaterializationContext), property.ClrType), CreateAccessorReadExpression(), - (ParameterExpression)materializationContextExpression), + (ParameterExpression)bindingInfo.MaterializationContextExpression), Expression.Lambda>( Expression.Convert(CreateAccessorReadExpression(), typeof(object)), - (ParameterExpression)materializationContextExpression)))); + (ParameterExpression)bindingInfo.MaterializationContextExpression)))); Expression CreateAccessorReadExpression() => property is IServiceProperty serviceProperty @@ -355,12 +388,8 @@ BlockExpression CreateInitializeExpression() { var initializeBlockExpressions = new List(); - AddInitializeExpressions( - properties, - bindingInfo, - materializationContextExpression, - instanceVariable, - initializeBlockExpressions); + AddInitializeExpressions(properties, bindingInfo, instanceVariable, initializeBlockExpressions); + AddAttachServiceExpressions(bindingInfo, instanceVariable, blockExpressions); return Expression.Block(initializeBlockExpressions); } @@ -428,20 +457,35 @@ public virtual Func GetEmptyMaterializer( var bindingInfo = new ParameterBindingInfo( new EntityMaterializerSourceParameters(e, "instance", null), materializationContextExpression); + var blockExpressions = new List(); + var instanceVariable = Expression.Variable(binding.RuntimeType, "instance"); + var serviceProperties = e.GetServiceProperties().ToList(); + bindingInfo.ServiceInstances.Add(instanceVariable); + + CreateServiceInstances(binding, bindingInfo, blockExpressions, serviceProperties); + var constructorExpression = binding.CreateConstructorExpression(bindingInfo); + var properties = new HashSet(serviceProperties); + foreach (var consumedProperty in binding.ParameterBindings.SelectMany(p => p.ConsumedProperties)) + { + properties.Remove(consumedProperty); + } + return Expression.Lambda>( self._materializationInterceptor == null - ? constructorExpression + ? properties.Count == 0 && blockExpressions.Count == 0 + ? constructorExpression + : CreateMaterializeExpression( + blockExpressions, instanceVariable, constructorExpression, properties, bindingInfo) : CreateInterceptionMaterializeExpression( e, - "instance", new HashSet(), self._materializationInterceptor, - binding, bindingInfo, constructorExpression, - materializationContextExpression), + instanceVariable, + blockExpressions), materializationContextExpression) .Compile(); }, @@ -457,4 +501,32 @@ private InstantiationBinding ModifyBindings(IEntityType entityType, Instantiatio return binding; } + + private static void CreateServiceInstances( + InstantiationBinding constructorBinding, + ParameterBindingInfo bindingInfo, + List blockExpressions, + List serviceProperties) + { + foreach (var parameterBinding in constructorBinding.ParameterBindings.OfType()) + { + if (bindingInfo.ServiceInstances.All(s => s.Type != parameterBinding.ServiceType)) + { + var variable = Expression.Variable(parameterBinding.ServiceType); + blockExpressions.Add(Expression.Assign(variable, parameterBinding.BindToParameter(bindingInfo))); + bindingInfo.ServiceInstances.Add(variable); + } + } + + foreach (var serviceProperty in serviceProperties) + { + var serviceType = serviceProperty.ParameterBinding.ServiceType; + if (bindingInfo.ServiceInstances.All(e => e.Type != serviceType)) + { + var variable = Expression.Variable(serviceType); + blockExpressions.Add(Expression.Assign(variable, serviceProperty.ParameterBinding.BindToParameter(bindingInfo))); + bindingInfo.ServiceInstances.Add(variable); + } + } + } } diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs index 93d29756da3..cc263455264 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs @@ -41,6 +41,7 @@ public void Test_new_annotations_handled_for_entity_types() CoreAnnotationNames.AfterSaveBehavior, CoreAnnotationNames.ProviderClrType, CoreAnnotationNames.EagerLoaded, + CoreAnnotationNames.LazyLoadingEnabled, CoreAnnotationNames.DuplicateServiceProperties, RelationalAnnotationNames.ColumnName, RelationalAnnotationNames.ColumnOrder, @@ -200,6 +201,7 @@ public void Test_new_annotations_handled_for_properties() CoreAnnotationNames.ProductVersion, CoreAnnotationNames.NavigationAccessMode, CoreAnnotationNames.EagerLoaded, + CoreAnnotationNames.LazyLoadingEnabled, CoreAnnotationNames.QueryFilter, #pragma warning disable CS0612 // Type or member is obsolete CoreAnnotationNames.DefiningQuery, diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs index c4f9a01879a..9adfe8c5cba 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs @@ -1109,7 +1109,8 @@ public static RuntimeForeignKey CreateForeignKey2(RuntimeEntityType declaringEnt typeof(CSharpRuntimeModelCodeGeneratorTest.DependentBase), propertyInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.PrincipalDerived>).GetProperty(""Dependent"", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), fieldInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.PrincipalDerived>).GetField(""k__BackingField"", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), - eagerLoaded: true); + eagerLoaded: true, + lazyLoadingEnabled: false); return runtimeForeignKey; } @@ -1733,7 +1734,8 @@ public static RuntimeSkipNavigation CreateSkipNavigation1(RuntimeEntityType decl typeof(ICollection), propertyInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.PrincipalDerived>).GetProperty(""Principals"", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), fieldInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.PrincipalDerived>).GetField(""k__BackingField"", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), - eagerLoaded: true); + eagerLoaded: true, + lazyLoadingEnabled: false); var inverse = targetEntityType.FindSkipNavigation(""Deriveds""); if (inverse != null) @@ -1999,6 +2001,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) Assert.Equal("k__BackingField", dependentNavigation.FieldInfo.Name); Assert.False(dependentNavigation.IsCollection); Assert.True(dependentNavigation.IsEagerLoaded); + Assert.False(dependentNavigation.LazyLoadingEnabled); Assert.False(dependentNavigation.IsOnDependent); Assert.Equal(principalDerived, dependentNavigation.DeclaringEntityType); Assert.Equal("Principal", dependentNavigation.Inverse.Name); @@ -2047,6 +2050,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) Assert.Equal(typeof(ICollection), derivedSkipNavigation.ClrType); Assert.True(derivedSkipNavigation.IsCollection); Assert.True(derivedSkipNavigation.IsEagerLoaded); + Assert.False(derivedSkipNavigation.LazyLoadingEnabled); Assert.False(derivedSkipNavigation.IsOnDependent); Assert.Equal(principalDerived, derivedSkipNavigation.DeclaringEntityType); Assert.Equal("Deriveds", derivedSkipNavigation.Inverse.Name); @@ -2282,7 +2286,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey>() .OnDelete(DeleteBehavior.ClientNoAction); - eb.Navigation(e => e.Dependent).AutoInclude(); + eb.Navigation(e => e.Dependent).AutoInclude().EnableLazyLoading(false); eb.OwnsMany( typeof(OwnedType).FullName, "ManyOwned", ob => @@ -2301,7 +2305,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasColumnOrder(1); }); - eb.Navigation(e => e.Principals).AutoInclude(); + eb.Navigation(e => e.Principals).AutoInclude().EnableLazyLoading(false); eb.ToTable("PrincipalDerived"); }); @@ -2940,6 +2944,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) Assert.Equal("k__BackingField", dependentNavigation.FieldInfo.Name); Assert.False(dependentNavigation.IsCollection); Assert.False(dependentNavigation.IsEagerLoaded); + Assert.True(dependentNavigation.LazyLoadingEnabled); Assert.False(dependentNavigation.IsOnDependent); Assert.Equal(principalDerived, dependentNavigation.DeclaringEntityType); Assert.Equal("Principal", dependentNavigation.Inverse.Name); diff --git a/test/EFCore.InMemory.FunctionalTests/NonLoadingNavigationsInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/NonLoadingNavigationsInMemoryTest.cs new file mode 100644 index 00000000000..a9e20567c18 --- /dev/null +++ b/test/EFCore.InMemory.FunctionalTests/NonLoadingNavigationsInMemoryTest.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore; + +public class NonLoadingNavigationsInMemoryTest : LoadTestBase +{ + public NonLoadingNavigationsInMemoryTest(NonLoadingNavigationsInMemoryFixture fixture) + : base(fixture) + { + } + + protected override bool LazyLoadingEnabled + => false; + + public class NonLoadingNavigationsInMemoryFixture : LoadFixtureBase + { + protected override string StoreName + => "NonLoadingNavigations"; + + protected override ITestStoreFactory TestStoreFactory + => InMemoryTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Entity( + b => + { + b.Navigation(e => e.Children).EnableLazyLoading(false); + b.Navigation(e => e.ChildrenAk).EnableLazyLoading(false); + b.Navigation(e => e.ChildrenCompositeKey).EnableLazyLoading(false); + b.Navigation(e => e.ChildrenShadowFk).EnableLazyLoading(false); + b.Navigation(e => e.Single).EnableLazyLoading(false); + b.Navigation(e => e.SingleAk).EnableLazyLoading(false); + b.Navigation(e => e.SingleCompositeKey).EnableLazyLoading(false); + b.Navigation(e => e.SingleShadowFk).EnableLazyLoading(false); + b.Navigation(e => e.SinglePkToPk).EnableLazyLoading(false); + b.Navigation(e => e.RequiredSingle).EnableLazyLoading(false); + }); + + modelBuilder.Entity().Navigation(e => e.Parent).EnableLazyLoading(false); + modelBuilder.Entity().Navigation(e => e.Parent).EnableLazyLoading(false); + modelBuilder.Entity().Navigation(e => e.Parent).EnableLazyLoading(false); + modelBuilder.Entity().Navigation(e => e.Parent).EnableLazyLoading(false); + modelBuilder.Entity().Navigation(e => e.Parent).EnableLazyLoading(false); + modelBuilder.Entity().Navigation(e => e.Parent).EnableLazyLoading(false); + modelBuilder.Entity().Navigation(e => e.Parent).EnableLazyLoading(false); + modelBuilder.Entity().Navigation(e => e.Parent).EnableLazyLoading(false); + modelBuilder.Entity().Navigation(e => e.Parent).EnableLazyLoading(false); + modelBuilder.Entity().Navigation(e => e.Parent).EnableLazyLoading(false); + + modelBuilder.Entity( + b => + { + b.Navigation(e => e.Children).EnableLazyLoading(false); + b.Navigation(e => e.Single).EnableLazyLoading(false); + }); + + modelBuilder.Entity().Navigation(e => e.Parent).EnableLazyLoading(false); + modelBuilder.Entity().Navigation(e => e.Parent).EnableLazyLoading(false); + + modelBuilder.Entity( + b => + { + b.Navigation(e => e.Children).EnableLazyLoading(false); + b.Navigation(e => e.Single).EnableLazyLoading(false); + }); + + modelBuilder.Entity().Navigation(e => e.Parent).EnableLazyLoading(false); + modelBuilder.Entity().Navigation(e => e.Parent).EnableLazyLoading(false); + + modelBuilder.Entity( + b => + { + b.Navigation(e => e.Children).EnableLazyLoading(false); + b.Navigation(e => e.Single).EnableLazyLoading(false); + }); + + modelBuilder.Entity().Navigation(e => e.Parent).EnableLazyLoading(false); + modelBuilder.Entity().Navigation(e => e.Parent).EnableLazyLoading(false); + + modelBuilder.Entity( + b => + { + b.Navigation(e => e.Children).EnableLazyLoading(false); + b.Navigation(e => e.Single).EnableLazyLoading(false); + }); + + modelBuilder.Entity().Navigation(e => e.Parent).EnableLazyLoading(false); + modelBuilder.Entity().Navigation(e => e.Parent).EnableLazyLoading(false); + + modelBuilder.Entity().Navigation(e => e.Deposit).EnableLazyLoading(false); + modelBuilder.Entity().HasNoKey().Navigation(e => e.Root).EnableLazyLoading(false); + modelBuilder.Entity().HasNoKey().Navigation(e => e.Root).EnableLazyLoading(false); + } + } +} diff --git a/test/EFCore.Specification.Tests/ApiConsistencyTestBase.cs b/test/EFCore.Specification.Tests/ApiConsistencyTestBase.cs index b01a4733054..c901a2ccb97 100644 --- a/test/EFCore.Specification.Tests/ApiConsistencyTestBase.cs +++ b/test/EFCore.Specification.Tests/ApiConsistencyTestBase.cs @@ -530,34 +530,36 @@ private string ValidateConventionBuilderMethods(IReadOnlyList method var methodLookup = new Dictionary(); foreach (var method in methods) { - methodLookup[method.Name] = method; + methodLookup[Fixture.MetadataMethodNameTransformers.TryGetValue(method, out var name) ? name : method.Name] = method; } - foreach (var method in methodLookup.Values) + foreach (var keyValuePair in methodLookup) { + var method = keyValuePair.Value; + var methodName = keyValuePair.Key; if (Fixture.UnmatchedMetadataMethods.Contains(method) || method.ReturnType != builderType) { continue; } - var expectedName = method.Name.StartsWith("HasNo", StringComparison.Ordinal) - ? "CanRemove" + method.Name[5..] + var expectedName = methodName.StartsWith("HasNo", StringComparison.Ordinal) + ? "CanRemove" + methodName[5..] : "CanSet" - + (method.Name.StartsWith("Has", StringComparison.Ordinal) - || method.Name.StartsWith("Use", StringComparison.Ordinal) - ? method.Name[3..] - : method.Name.StartsWith("To", StringComparison.Ordinal) - ? method.Name[2..] - : method.Name.StartsWith("With", StringComparison.Ordinal) - ? method.Name[4..] - : method.Name); + + (methodName.StartsWith("Has", StringComparison.Ordinal) + || methodName.StartsWith("Use", StringComparison.Ordinal) + ? methodName[3..] + : methodName.StartsWith("To", StringComparison.Ordinal) + ? methodName[2..] + : methodName.StartsWith("With", StringComparison.Ordinal) + ? methodName[4..] + : methodName); if (!methodLookup.TryGetValue(expectedName, out var canSetMethod)) { - if (method.Name.StartsWith("Has", StringComparison.Ordinal)) + if (methodName.StartsWith("Has", StringComparison.Ordinal)) { - var otherExpectedName = "CanHave" + method.Name[3..]; + var otherExpectedName = "CanHave" + methodName[3..]; if (!methodLookup.TryGetValue(otherExpectedName, out canSetMethod)) { return $"{declaringType.Name} expected to have a {expectedName} or {otherExpectedName} method"; @@ -1084,6 +1086,7 @@ protected ApiConsistencyFixtureBase() public virtual HashSet NotAnnotatedMethods { get; } = new(); public virtual HashSet AsyncMethodExceptions { get; } = new(); public virtual HashSet UnmatchedMetadataMethods { get; } = new(); + public virtual Dictionary MetadataMethodNameTransformers { get; } = new(); public virtual HashSet MetadataMethodExceptions { get; } = new(); public virtual HashSet ComputedDependencyProperties { get; } diff --git a/test/EFCore.Specification.Tests/LazyLoadTestBase.cs b/test/EFCore.Specification.Tests/LazyLoadTestBase.cs index 2d75111315c..fc6e6e3af0b 100644 --- a/test/EFCore.Specification.Tests/LazyLoadTestBase.cs +++ b/test/EFCore.Specification.Tests/LazyLoadTestBase.cs @@ -56,7 +56,7 @@ public virtual async Task Lazy_load_collection(EntityState state, QueryTrackingB changeDetector.DetectChangesCalled = false; - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + if (!LazyLoadingEnabled || (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll)) { Assert.Null(await parent.LazyLoadChildren(async)); // Explicitly detached } @@ -74,9 +74,9 @@ public virtual async Task Lazy_load_collection(EntityState state, QueryTrackingB context.ChangeTracker.LazyLoadingEnabled = false; Assert.Equal(2, parent.Children.Count()); - } - Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] @@ -131,51 +131,59 @@ public virtual async Task Lazy_load_many_to_one_reference_to_principal( changeDetector.DetectChangesCalled = false; - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) - { - Assert.Null(await child.LazyLoadParent(async)); // Explicitly detached - } - else + if (LazyLoadingEnabled) { - if (state == EntityState.Deleted) + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) { - Assert.Null(await child.LazyLoadParent(async)); + Assert.Null(await child.LazyLoadParent(async)); // Explicitly detached } else { - Assert.NotNull(await child.LazyLoadParent(async)); - } - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); + if (state == EntityState.Deleted) + { + Assert.Null(await child.LazyLoadParent(async)); + } + else + { + Assert.NotNull(await child.LazyLoadParent(async)); + } - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.True(referenceEntry.IsLoaded); - if (state != EntityState.Deleted) - { - Assert.Same(child, child.Parent!.Children.Single()); - } + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - if (state != EntityState.Detached) - { - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(child.Parent); - Assert.Null(parent.Children); + Assert.Same(child, child.Parent!.Children.Single()); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.Children.Single()); + 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()); + } } } } + else + { + Assert.Null(await child.LazyLoadParent(async)); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -230,51 +238,59 @@ public virtual async Task Lazy_load_one_to_one_reference_to_principal( changeDetector.DetectChangesCalled = false; - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) - { - Assert.Null(await single.LazyLoadParent(async)); // Explicitly detached - } - else + if (LazyLoadingEnabled) { - if (state == EntityState.Deleted) + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) { - Assert.Null(await single.LazyLoadParent(async)); + Assert.Null(await single.LazyLoadParent(async)); // Explicitly detached } else { - Assert.NotNull(await single.LazyLoadParent(async)); - } - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); + if (state == EntityState.Deleted) + { + Assert.Null(await single.LazyLoadParent(async)); + } + else + { + Assert.NotNull(await single.LazyLoadParent(async)); + } - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.True(referenceEntry.IsLoaded); - if (state != EntityState.Deleted) - { - Assert.Same(single, single.Parent!.Single); - } + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - if (state != EntityState.Detached) - { - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(single.Parent); - Assert.Null(parent.Single); + Assert.Same(single, single.Parent!.Single); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.Single); + 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); + } } } } + else + { + Assert.Null(await single.LazyLoadParent(async)); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -329,36 +345,44 @@ public virtual async Task Lazy_load_one_to_one_reference_to_dependent( changeDetector.DetectChangesCalled = false; - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + if (LazyLoadingEnabled) { - Assert.Null(await parent.LazyLoadSingle(async)); // Explicitly detached - } - else - { - Assert.NotNull(await parent.LazyLoadSingle(async)); + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(await parent.LazyLoadSingle(async)); // Explicitly detached + } + else + { + Assert.NotNull(await parent.LazyLoadSingle(async)); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.True(referenceEntry.IsLoaded); + Assert.True(referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state != EntityState.Deleted) - { - Assert.Same(parent, parent.Single.Parent); - } + if (state != EntityState.Deleted) + { + Assert.Same(parent, parent.Single.Parent); + } - if (state != EntityState.Detached) - { - var single = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(single, parent.Single); - Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + Assert.Same(parent, single.Parent); + } } } + else + { + Assert.Null(await parent.LazyLoadSingle(async)); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -395,51 +419,59 @@ public virtual void Lazy_load_one_to_one_PK_to_PK_reference_to_principal(EntityS changeDetector.DetectChangesCalled = false; - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) - { - Assert.Null(single.Parent); // Explicitly detached - } - else + if (LazyLoadingEnabled) { - if (state == EntityState.Deleted) + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) { - Assert.Null(single.Parent); + Assert.Null(single.Parent); // Explicitly detached } else { - Assert.NotNull(single.Parent); - } - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + } + else + { + Assert.NotNull(single.Parent); + } - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.True(referenceEntry.IsLoaded); - if (state != EntityState.Deleted) - { - Assert.Same(single, single.Parent!.SinglePkToPk); - } + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - if (state != EntityState.Detached) - { - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(single.Parent); - Assert.Null(parent.SinglePkToPk); + Assert.Same(single, single.Parent!.SinglePkToPk); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SinglePkToPk); + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.SinglePkToPk); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SinglePkToPk); + } } } } + else + { + Assert.Null(single.Parent); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -476,33 +508,41 @@ public virtual void Lazy_load_one_to_one_PK_to_PK_reference_to_dependent(EntityS changeDetector.DetectChangesCalled = false; - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + if (LazyLoadingEnabled) { - Assert.Null(parent.SinglePkToPk); // Explicitly detached - } - else - { - Assert.NotNull(parent.SinglePkToPk); + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(parent.SinglePkToPk); // Explicitly detached + } + else + { + Assert.NotNull(parent.SinglePkToPk); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.True(referenceEntry.IsLoaded); + Assert.True(referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - Assert.Same(parent, parent.SinglePkToPk.Parent); + Assert.Same(parent, parent.SinglePkToPk.Parent); - if (state != EntityState.Detached) - { - var single = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(single, parent.SinglePkToPk); - Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SinglePkToPk); + Assert.Same(parent, single.Parent); + } } } + else + { + Assert.Null(parent.SinglePkToPk); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -557,17 +597,25 @@ public virtual async Task Lazy_load_many_to_one_reference_to_principal_null_FK( changeDetector.DetectChangesCalled = false; - Assert.Null(await child.LazyLoadParent(async)); + if (LazyLoadingEnabled) + { + Assert.Null(await child.LazyLoadParent(async)); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); - Assert.Null(child.Parent); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Null(child.Parent); + } + else + { + Assert.Null(await child.LazyLoadParent(async)); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -622,18 +670,26 @@ public virtual async Task Lazy_load_one_to_one_reference_to_principal_null_FK( changeDetector.DetectChangesCalled = false; - Assert.Null(await single.LazyLoadParent(async)); + if (LazyLoadingEnabled) + { + Assert.Null(await single.LazyLoadParent(async)); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); - Assert.Null(single.Parent); + Assert.Null(single.Parent); + } + else + { + Assert.Null(await single.LazyLoadParent(async)); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -685,20 +741,28 @@ public virtual async Task Lazy_load_collection_not_found(EntityState state, Quer changeDetector.DetectChangesCalled = false; - if (state == EntityState.Detached) + if (LazyLoadingEnabled) { - Assert.Null(await parent.LazyLoadChildren(async)); // Explicitly detached + if (state == EntityState.Detached) + { + Assert.Null(await parent.LazyLoadChildren(async)); // Explicitly detached + } + else + { + Assert.Empty(await parent.LazyLoadChildren(async)); + Assert.False(changeDetector.DetectChangesCalled); + Assert.True(collectionEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Single(context.ChangeTracker.Entries()); + } } else { - Assert.Empty(await parent.LazyLoadChildren(async)); - Assert.False(changeDetector.DetectChangesCalled); - Assert.True(collectionEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Single(context.ChangeTracker.Entries()); + Assert.Null(await parent.LazyLoadChildren(async)); + Assert.False(collectionEntry.IsLoaded); } } @@ -754,17 +818,25 @@ public virtual async Task Lazy_load_many_to_one_reference_to_principal_not_found changeDetector.DetectChangesCalled = false; - Assert.Null(await child.LazyLoadParent(async)); + if (LazyLoadingEnabled) + { + Assert.Null(await child.LazyLoadParent(async)); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); - Assert.Null(child.Parent); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Null(child.Parent); + } + else + { + Assert.Null(await child.LazyLoadParent(async)); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -819,18 +891,26 @@ public virtual async Task Lazy_load_one_to_one_reference_to_principal_not_found( changeDetector.DetectChangesCalled = false; - Assert.Null(await single.LazyLoadParent(async)); + if (LazyLoadingEnabled) + { + Assert.Null(await single.LazyLoadParent(async)); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); - Assert.Null(single.Parent); + Assert.Null(single.Parent); + } + else + { + Assert.Null(await single.LazyLoadParent(async)); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -885,18 +965,26 @@ public virtual async Task Lazy_load_one_to_one_reference_to_dependent_not_found( changeDetector.DetectChangesCalled = false; - Assert.Null(await parent.LazyLoadSingle(async)); + if (LazyLoadingEnabled) + { + Assert.Null(await parent.LazyLoadSingle(async)); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Null(parent.Single); + Assert.Null(parent.Single); - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + } + else + { + Assert.Null(await parent.LazyLoadSingle(async)); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -1071,7 +1159,7 @@ public virtual async Task Lazy_load_collection_already_partially_loaded( RecordLog(); - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + if (!LazyLoadingEnabled || (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll)) { Assert.False(collectionEntry.IsLoaded); // Explicitly detached Assert.Equal(1, parent.Children.Count()); @@ -1522,49 +1610,57 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_alternate_key( Assert.False(referenceEntry.IsLoaded); - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + if (LazyLoadingEnabled) { - Assert.Null(child.Parent); // Explicitly detached - } - else - { - if (state == EntityState.Deleted) + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) { - Assert.Null(child.Parent); + Assert.Null(child.Parent); // Explicitly detached } else { - Assert.NotNull(child.Parent); - } + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + } + else + { + Assert.NotNull(child.Parent); + } - Assert.True(referenceEntry.IsLoaded); + Assert.True(referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state != EntityState.Deleted) - { - Assert.Same(child, child.Parent!.ChildrenAk.Single()); - } - - if (state != EntityState.Detached) - { - var parent = context.ChangeTracker.Entries().Single().Entity; - - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(child.Parent); - Assert.Null(parent.ChildrenAk); + Assert.Same(child, child.Parent!.ChildrenAk.Single()); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.ChildrenAk.Single()); + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + Assert.Null(parent.ChildrenAk); + } + else + { + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.ChildrenAk.Single()); + } } } } + else + { + Assert.Null(child.Parent); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -1599,49 +1695,57 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_alternate_key( Assert.False(referenceEntry.IsLoaded); - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) - { - Assert.Null(single.Parent); // Explicitly detached - } - else + if (LazyLoadingEnabled) { - if (state == EntityState.Deleted) + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) { - Assert.Null(single.Parent); + Assert.Null(single.Parent); // Explicitly detached } else { - Assert.NotNull(single.Parent); - } - - Assert.True(referenceEntry.IsLoaded); + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + } + else + { + Assert.NotNull(single.Parent); + } - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + Assert.True(referenceEntry.IsLoaded); - Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - if (state != EntityState.Deleted) - { - Assert.Same(single, single.Parent!.SingleAk); - } - - if (state != EntityState.Detached) - { - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(single.Parent); - Assert.Null(parent.SingleAk); + Assert.Same(single, single.Parent!.SingleAk); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SingleAk); + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.SingleAk); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SingleAk); + } } } } + else + { + Assert.Null(single.Parent); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -1676,31 +1780,39 @@ public virtual void Lazy_load_one_to_one_reference_to_dependent_alternate_key( Assert.False(referenceEntry.IsLoaded); - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) - { - Assert.Null(parent.SingleAk); // Explicitly detached - } - else + if (LazyLoadingEnabled) { - Assert.NotNull(parent.SingleAk); + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(parent.SingleAk); // Explicitly detached + } + else + { + Assert.NotNull(parent.SingleAk); - Assert.True(referenceEntry.IsLoaded); + Assert.True(referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - Assert.Same(parent, parent.SingleAk.Parent); + Assert.Same(parent, parent.SingleAk.Parent); - if (state != EntityState.Detached) - { - var single = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(single, parent.SingleAk); - Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SingleAk); + Assert.Same(parent, single.Parent); + } } } + else + { + Assert.Null(parent.SingleAk); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -1735,15 +1847,23 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK_alterna Assert.False(referenceEntry.IsLoaded); - Assert.Null(child.Parent); + if (LazyLoadingEnabled) + { + Assert.Null(child.Parent); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); - Assert.Null(child.Parent); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Null(child.Parent); + } + else + { + Assert.Null(child.Parent); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -1778,16 +1898,24 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK_alternat Assert.False(referenceEntry.IsLoaded); - Assert.Null(single.Parent); + if (LazyLoadingEnabled) + { + Assert.Null(single.Parent); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); - Assert.Null(single.Parent); + Assert.Null(single.Parent); + } + else + { + Assert.Null(single.Parent); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -1820,24 +1948,32 @@ public virtual void Lazy_load_collection_shadow_fk(EntityState state, QueryTrack Assert.False(collectionEntry.IsLoaded); - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + if (LazyLoadingEnabled) { - Assert.Null(parent.ChildrenShadowFk); // Explicitly detached - } - else - { - Assert.NotNull(parent.ChildrenShadowFk); + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(parent.ChildrenShadowFk); // Explicitly detached + } + else + { + Assert.NotNull(parent.ChildrenShadowFk); - Assert.True(collectionEntry.IsLoaded); + Assert.True(collectionEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, parent.ChildrenShadowFk.Count()); - Assert.All(parent.ChildrenShadowFk.Select(e => e.Parent), p => Assert.Same(parent, p)); - } + Assert.Equal(2, parent.ChildrenShadowFk.Count()); + Assert.All(parent.ChildrenShadowFk.Select(e => e.Parent), p => Assert.Same(parent, p)); + } - Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); + } + else + { + Assert.Null(parent.ChildrenShadowFk); + Assert.False(collectionEntry.IsLoaded); + } } [ConditionalTheory] @@ -1868,56 +2004,64 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_shadow_fk( SetState(context, child, state, queryTrackingBehavior); - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) - { - Assert.Null(child.Parent); // Explicitly detached - } - else if (state == EntityState.Detached || queryTrackingBehavior != QueryTrackingBehavior.TrackAll) + if (LazyLoadingEnabled) { - Assert.Equal( - CoreStrings.CannotLoadDetachedShadow("Parent", "ChildShadowFk"), - Assert.Throws(() => child.Parent).Message); - } - else - { - var referenceEntry = context.Entry(child).Reference(e => e.Parent); - - Assert.False(referenceEntry.IsLoaded); - - if (state == EntityState.Deleted) + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) { - Assert.Null(child.Parent); + Assert.Null(child.Parent); // Explicitly detached } - else + else if (state == EntityState.Detached || queryTrackingBehavior != QueryTrackingBehavior.TrackAll) { - Assert.NotNull(child.Parent); + Assert.Equal( + CoreStrings.CannotLoadDetachedShadow("Parent", "ChildShadowFk"), + Assert.Throws(() => child.Parent).Message); } + else + { + var referenceEntry = context.Entry(child).Reference(e => e.Parent); - Assert.True(referenceEntry.IsLoaded); + Assert.False(referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + } + else + { + Assert.NotNull(child.Parent); + } - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.True(referenceEntry.IsLoaded); - if (state != EntityState.Deleted) - { - Assert.Same(child, child.Parent!.ChildrenShadowFk.Single()); - } + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) - { - Assert.Null(child.Parent); - Assert.Null(parent.ChildrenShadowFk); - } - else - { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.ChildrenShadowFk.Single()); + if (state != EntityState.Deleted) + { + Assert.Same(child, child.Parent!.ChildrenShadowFk.Single()); + } + + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + Assert.Null(parent.ChildrenShadowFk); + } + else + { + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.ChildrenShadowFk.Single()); + } } } + else + { + Assert.Null(child.Parent); + Assert.False(context.Entry(child).Reference(e => e.Parent).IsLoaded); + } } [ConditionalTheory] @@ -1948,56 +2092,64 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_shadow_fk( SetState(context, single, state, queryTrackingBehavior); - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) - { - Assert.Null(single.Parent); // Explicitly detached - } - else if (state == EntityState.Detached || queryTrackingBehavior != QueryTrackingBehavior.TrackAll) - { - Assert.Equal( - CoreStrings.CannotLoadDetachedShadow("Parent", "SingleShadowFk"), - Assert.Throws(() => single.Parent).Message); - } - else + if (LazyLoadingEnabled) { - var referenceEntry = context.Entry(single).Reference(e => e.Parent); - - Assert.False(referenceEntry.IsLoaded); - - if (state == EntityState.Deleted) + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) { - Assert.Null(single.Parent); + Assert.Null(single.Parent); // Explicitly detached } - else + else if (state == EntityState.Detached || queryTrackingBehavior != QueryTrackingBehavior.TrackAll) { - Assert.NotNull(single.Parent); + Assert.Equal( + CoreStrings.CannotLoadDetachedShadow("Parent", "SingleShadowFk"), + Assert.Throws(() => single.Parent).Message); } + else + { + var referenceEntry = context.Entry(single).Reference(e => e.Parent); - Assert.True(referenceEntry.IsLoaded); + Assert.False(referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + } + else + { + Assert.NotNull(single.Parent); + } - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.True(referenceEntry.IsLoaded); - if (state != EntityState.Deleted) - { - Assert.Same(single, single.Parent!.SingleShadowFk); - } + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) - { - Assert.Null(single.Parent); - Assert.Null(parent.SingleShadowFk); - } - else - { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SingleShadowFk); + if (state != EntityState.Deleted) + { + Assert.Same(single, single.Parent!.SingleShadowFk); + } + + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.SingleShadowFk); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SingleShadowFk); + } } } + else + { + Assert.Null(single.Parent); + Assert.False(context.Entry(single).Reference(e => e.Parent).IsLoaded); + } } [ConditionalTheory] @@ -2032,31 +2184,39 @@ public virtual void Lazy_load_one_to_one_reference_to_dependent_shadow_fk( Assert.False(referenceEntry.IsLoaded); - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + if (LazyLoadingEnabled) { - Assert.Null(parent.SingleShadowFk); // Explicitly detached - } - else - { - Assert.NotNull(parent.SingleShadowFk); + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(parent.SingleShadowFk); // Explicitly detached + } + else + { + Assert.NotNull(parent.SingleShadowFk); - Assert.True(referenceEntry.IsLoaded); + Assert.True(referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - Assert.Same(parent, parent.SingleShadowFk.Parent); + Assert.Same(parent, parent.SingleShadowFk.Parent); - if (state != EntityState.Detached) - { - var single = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(single, parent.SingleShadowFk); - Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SingleShadowFk); + Assert.Same(parent, single.Parent); + } } } + else + { + Assert.Null(parent.SingleShadowFk); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -2092,31 +2252,39 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK_shadow_ SetState(context, child, state, queryTrackingBehavior, isAttached: true); - if (state == EntityState.Detached) - { - Assert.Null(child.Parent); // Explicitly detached - } - else if (queryTrackingBehavior != QueryTrackingBehavior.TrackAll) + if (LazyLoadingEnabled) { - Assert.Equal( - CoreStrings.CannotLoadDetachedShadow("Parent", "ChildShadowFk"), - Assert.Throws(() => child.Parent).Message); - } - else - { - var referenceEntry = context.Entry(child).Reference(e => e.Parent); + if (state == EntityState.Detached) + { + Assert.Null(child.Parent); // Explicitly detached + } + else if (queryTrackingBehavior != QueryTrackingBehavior.TrackAll) + { + Assert.Equal( + CoreStrings.CannotLoadDetachedShadow("Parent", "ChildShadowFk"), + Assert.Throws(() => child.Parent).Message); + } + else + { + var referenceEntry = context.Entry(child).Reference(e => e.Parent); - Assert.False(referenceEntry.IsLoaded); + Assert.False(referenceEntry.IsLoaded); - Assert.Null(child.Parent); + Assert.Null(child.Parent); - Assert.True(referenceEntry.IsLoaded); + Assert.True(referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Single(context.ChangeTracker.Entries()); + Assert.Single(context.ChangeTracker.Entries()); + Assert.Null(child.Parent); + } + } + else + { Assert.Null(child.Parent); + Assert.False(context.Entry(child).Reference(e => e.Parent).IsLoaded); } } @@ -2153,32 +2321,40 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK_shadow_f SetState(context, single, state, queryTrackingBehavior, isAttached: true); - if (state == EntityState.Detached) - { - Assert.Null(single.Parent); - } - else if (queryTrackingBehavior != QueryTrackingBehavior.TrackAll) + if (LazyLoadingEnabled) { - Assert.Equal( - CoreStrings.CannotLoadDetachedShadow("Parent", "SingleShadowFk"), - Assert.Throws(() => single.Parent).Message); - } - else - { - var referenceEntry = context.Entry(single).Reference(e => e.Parent); + if (state == EntityState.Detached) + { + Assert.Null(single.Parent); + } + else if (queryTrackingBehavior != QueryTrackingBehavior.TrackAll) + { + Assert.Equal( + CoreStrings.CannotLoadDetachedShadow("Parent", "SingleShadowFk"), + Assert.Throws(() => single.Parent).Message); + } + else + { + var referenceEntry = context.Entry(single).Reference(e => e.Parent); - Assert.False(referenceEntry.IsLoaded); + Assert.False(referenceEntry.IsLoaded); - Assert.Null(single.Parent); + Assert.Null(single.Parent); - Assert.True(referenceEntry.IsLoaded); + Assert.True(referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Single(context.ChangeTracker.Entries()); + Assert.Single(context.ChangeTracker.Entries()); + Assert.Null(single.Parent); + } + } + else + { Assert.Null(single.Parent); + Assert.False(context.Entry(single).Reference(e => e.Parent).IsLoaded); } } @@ -2212,24 +2388,32 @@ public virtual void Lazy_load_collection_composite_key(EntityState state, QueryT Assert.False(collectionEntry.IsLoaded); - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) - { - Assert.Null(parent.ChildrenCompositeKey); // Explicitly detached - } - else + if (LazyLoadingEnabled) { - Assert.NotNull(parent.ChildrenCompositeKey); + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(parent.ChildrenCompositeKey); // Explicitly detached + } + else + { + Assert.NotNull(parent.ChildrenCompositeKey); - Assert.True(collectionEntry.IsLoaded); + Assert.True(collectionEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, parent.ChildrenCompositeKey.Count()); - Assert.All(parent.ChildrenCompositeKey.Select(e => e.Parent), p => Assert.Same(parent, p)); - } + Assert.Equal(2, parent.ChildrenCompositeKey.Count()); + Assert.All(parent.ChildrenCompositeKey.Select(e => e.Parent), p => Assert.Same(parent, p)); + } - Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); + } + else + { + Assert.Null(parent.ChildrenCompositeKey); + Assert.False(collectionEntry.IsLoaded); + } } [ConditionalTheory] @@ -2264,49 +2448,57 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_composite_key( Assert.False(referenceEntry.IsLoaded); - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + if (LazyLoadingEnabled) { - Assert.Null(child.Parent); // Explicitly detached - } - else - { - if (state == EntityState.Deleted) + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) { - Assert.Null(child.Parent); + Assert.Null(child.Parent); // Explicitly detached } else { - Assert.NotNull(child.Parent); - } - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + } + else + { + Assert.NotNull(child.Parent); + } - Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.True(referenceEntry.IsLoaded); - if (state != EntityState.Deleted) - { - Assert.Same(child, child.Parent!.ChildrenCompositeKey.Single()); - } + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - if (state != EntityState.Detached) - { - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(child.Parent); - Assert.Null(parent.ChildrenCompositeKey); + Assert.Same(child, child.Parent!.ChildrenCompositeKey.Single()); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.ChildrenCompositeKey.Single()); + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + Assert.Null(parent.ChildrenCompositeKey); + } + else + { + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.ChildrenCompositeKey.Single()); + } } } } + else + { + Assert.Null(child.Parent); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -2341,49 +2533,57 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_composite_key( Assert.False(referenceEntry.IsLoaded); - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) - { - Assert.Null(single.Parent); // Explicitly detached - } - else + if (LazyLoadingEnabled) { - if (state == EntityState.Deleted) + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) { - Assert.Null(single.Parent); + Assert.Null(single.Parent); // Explicitly detached } else { - Assert.NotNull(single.Parent); - } - - 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.Null(single.Parent); + } + else + { + Assert.NotNull(single.Parent); + } - if (state != EntityState.Deleted) - { - Assert.Same(single, single.Parent!.SingleCompositeKey); - } + Assert.True(referenceEntry.IsLoaded); - if (state != EntityState.Detached) - { - var parent = context.ChangeTracker.Entries().Single().Entity; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - if (state == EntityState.Deleted) + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) { - Assert.Null(single.Parent); - Assert.Null(parent.SingleCompositeKey); + Assert.Same(single, single.Parent!.SingleCompositeKey); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SingleCompositeKey); + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.SingleCompositeKey); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SingleCompositeKey); + } } } } + else + { + Assert.Null(single.Parent); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -2418,31 +2618,39 @@ public virtual void Lazy_load_one_to_one_reference_to_dependent_composite_key( Assert.False(referenceEntry.IsLoaded); - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + if (LazyLoadingEnabled) { - Assert.Null(parent.SingleCompositeKey); // Explicitly detached - } - else - { - Assert.NotNull(parent.SingleCompositeKey); + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(parent.SingleCompositeKey); // Explicitly detached + } + else + { + Assert.NotNull(parent.SingleCompositeKey); - Assert.True(referenceEntry.IsLoaded); + Assert.True(referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - Assert.Same(parent, parent.SingleCompositeKey.Parent); + Assert.Same(parent, parent.SingleCompositeKey.Parent); - if (state != EntityState.Detached) - { - var single = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(single, parent.SingleCompositeKey); - Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SingleCompositeKey); + Assert.Same(parent, single.Parent); + } } } + else + { + Assert.Null(parent.SingleCompositeKey); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -2477,15 +2685,23 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK_composi Assert.False(referenceEntry.IsLoaded); - Assert.Null(child.Parent); + if (LazyLoadingEnabled) + { + Assert.Null(child.Parent); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); - Assert.Null(child.Parent); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Null(child.Parent); + } + else + { + Assert.Null(child.Parent); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -2520,16 +2736,24 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK_composit Assert.False(referenceEntry.IsLoaded); - Assert.Null(single.Parent); + if (LazyLoadingEnabled) + { + Assert.Null(single.Parent); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); - Assert.Null(single.Parent); + Assert.Null(single.Parent); + } + else + { + Assert.Null(single.Parent); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -2584,27 +2808,35 @@ public virtual async Task Lazy_load_collection_full_loader_constructor_injection changeDetector.DetectChangesCalled = false; - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) - { - Assert.Null(await parent.LazyLoadChildren(async)); // Explicitly detached - } - else + if (LazyLoadingEnabled) { - Assert.NotNull(await parent.LazyLoadChildren(async)); + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(await parent.LazyLoadChildren(async)); // Explicitly detached + } + else + { + Assert.NotNull(await parent.LazyLoadChildren(async)); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.True(collectionEntry.IsLoaded); + Assert.True(collectionEntry.IsLoaded); - Assert.All(parent.Children.Select(e => e.Parent), p => Assert.Same(parent, p)); + Assert.All(parent.Children.Select(e => e.Parent), p => Assert.Same(parent, p)); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, parent.Children.Count()); - } + Assert.Equal(2, parent.Children.Count()); + } - Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); + } + else + { + Assert.Null(await parent.LazyLoadChildren(async)); + Assert.False(collectionEntry.IsLoaded); + } } [ConditionalTheory] @@ -2659,51 +2891,59 @@ public virtual async Task Lazy_load_many_to_one_reference_to_principal_full_load changeDetector.DetectChangesCalled = false; - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) - { - Assert.Null(await child.LazyLoadParent(async)); // Explicitly detached - } - else + if (LazyLoadingEnabled) { - if (state == EntityState.Deleted) + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) { - Assert.Null(await child.LazyLoadParent(async)); + Assert.Null(await child.LazyLoadParent(async)); // Explicitly detached } else { - Assert.NotNull(await child.LazyLoadParent(async)); - } - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); + if (state == EntityState.Deleted) + { + Assert.Null(await child.LazyLoadParent(async)); + } + else + { + Assert.NotNull(await child.LazyLoadParent(async)); + } - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.True(referenceEntry.IsLoaded); - if (state != EntityState.Deleted) - { - Assert.Same(child, child.Parent!.Children.Single()); - } + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - if (state != EntityState.Detached) - { - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(child.Parent); - Assert.Null(parent.Children); + Assert.Same(child, child.Parent!.Children.Single()); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.Children.Single()); + 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()); + } } } } + else + { + Assert.Null(await child.LazyLoadParent(async)); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -2758,51 +2998,59 @@ public virtual async Task Lazy_load_one_to_one_reference_to_principal_full_loade changeDetector.DetectChangesCalled = false; - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) - { - Assert.Null(await single.LazyLoadParent(async)); // Explicitly detached - } - else + if (LazyLoadingEnabled) { - if (state == EntityState.Deleted) + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) { - Assert.Null(await single.LazyLoadParent(async)); + Assert.Null(await single.LazyLoadParent(async)); // Explicitly detached } else { - Assert.NotNull(await single.LazyLoadParent(async)); - } - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); + if (state == EntityState.Deleted) + { + Assert.Null(await single.LazyLoadParent(async)); + } + else + { + Assert.NotNull(await single.LazyLoadParent(async)); + } - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.True(referenceEntry.IsLoaded); - if (state != EntityState.Deleted) - { - Assert.Same(single, single.Parent!.Single); - } + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - if (state != EntityState.Detached) - { - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(single.Parent); - Assert.Null(parent.Single); + Assert.Same(single, single.Parent!.Single); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.Single); + 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); + } } } } + else + { + Assert.Null(await single.LazyLoadParent(async)); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -2857,36 +3105,44 @@ public virtual async Task Lazy_load_one_to_one_reference_to_dependent_full_loade changeDetector.DetectChangesCalled = false; - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + if (LazyLoadingEnabled) { - Assert.Null(await parent.LazyLoadSingle(async)); // Explicitly detached - } - else - { - Assert.NotNull(await parent.LazyLoadSingle(async)); + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(await parent.LazyLoadSingle(async)); // Explicitly detached + } + else + { + Assert.NotNull(await parent.LazyLoadSingle(async)); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.True(referenceEntry.IsLoaded); + Assert.True(referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state != EntityState.Deleted) - { - Assert.Same(parent, parent.Single.Parent); - } + if (state != EntityState.Deleted) + { + Assert.Same(parent, parent.Single.Parent); + } - if (state != EntityState.Detached) - { - var single = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(single, parent.Single); - Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + Assert.Same(parent, single.Parent); + } } } + else + { + Assert.Null(await parent.LazyLoadSingle(async)); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -2941,17 +3197,25 @@ public virtual async Task Lazy_load_many_to_one_reference_to_principal_null_FK_f changeDetector.DetectChangesCalled = false; - Assert.Null(await child.LazyLoadParent(async)); + if (LazyLoadingEnabled) + { + Assert.Null(await child.LazyLoadParent(async)); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); - Assert.Null(child.Parent); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Null(child.Parent); + } + else + { + Assert.Null(await child.LazyLoadParent(async)); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -3006,18 +3270,26 @@ public virtual async Task Lazy_load_one_to_one_reference_to_principal_null_FK_fu changeDetector.DetectChangesCalled = false; - Assert.Null(await single.LazyLoadParent(async)); + if (LazyLoadingEnabled) + { + Assert.Null(await single.LazyLoadParent(async)); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); - Assert.Null(single.Parent); + Assert.Null(single.Parent); + } + else + { + Assert.Null(await single.LazyLoadParent(async)); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -3072,20 +3344,28 @@ public virtual async Task Lazy_load_collection_not_found_full_loader_constructor changeDetector.DetectChangesCalled = false; - if (state == EntityState.Detached) + if (LazyLoadingEnabled) { - Assert.Null(await parent.LazyLoadChildren(async)); // Explicitly detached + if (state == EntityState.Detached) + { + Assert.Null(await parent.LazyLoadChildren(async)); // Explicitly detached + } + else + { + Assert.Empty(await parent.LazyLoadChildren(async)); + Assert.False(changeDetector.DetectChangesCalled); + Assert.True(collectionEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Single(context.ChangeTracker.Entries()); + } } else { - Assert.Empty(await parent.LazyLoadChildren(async)); - Assert.False(changeDetector.DetectChangesCalled); - Assert.True(collectionEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Single(context.ChangeTracker.Entries()); + Assert.Null(await parent.LazyLoadChildren(async)); + Assert.False(collectionEntry.IsLoaded); } } @@ -3141,17 +3421,25 @@ public virtual async Task Lazy_load_many_to_one_reference_to_principal_not_found changeDetector.DetectChangesCalled = false; - Assert.Null(await child.LazyLoadParent(async)); + if (LazyLoadingEnabled) + { + Assert.Null(await child.LazyLoadParent(async)); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); - Assert.Null(child.Parent); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Null(child.Parent); + } + else + { + Assert.Null(await child.LazyLoadParent(async)); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -3206,18 +3494,26 @@ public virtual async Task Lazy_load_one_to_one_reference_to_principal_not_found_ changeDetector.DetectChangesCalled = false; - Assert.Null(await single.LazyLoadParent(async)); + if (LazyLoadingEnabled) + { + Assert.Null(await single.LazyLoadParent(async)); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); - Assert.Null(single.Parent); + Assert.Null(single.Parent); + } + else + { + Assert.Null(await single.LazyLoadParent(async)); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -3272,18 +3568,26 @@ public virtual async Task Lazy_load_one_to_one_reference_to_dependent_not_found_ changeDetector.DetectChangesCalled = false; - Assert.Null(await parent.LazyLoadSingle(async)); + if (LazyLoadingEnabled) + { + Assert.Null(await parent.LazyLoadSingle(async)); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Null(parent.Single); + Assert.Null(parent.Single); - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + } + else + { + Assert.Null(await parent.LazyLoadSingle(async)); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -3723,26 +4027,34 @@ public virtual void Lazy_load_collection_already_partially_loaded_full_loader_co RecordLog(); - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + if (LazyLoadingEnabled) { - Assert.False(collectionEntry.IsLoaded); // Explicitly detached - Assert.Equal(1, parent.Children.Count()); + 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); + Assert.All(parent.Children.Select(e => e.Parent), p => Assert.Same(parent, p)); + } + else + { + Assert.True(collectionEntry.IsLoaded); - context.ChangeTracker.LazyLoadingEnabled = false; + 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()); + // 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)); + Assert.All(parent.Children.Select(e => e.Parent), p => Assert.Same(parent, p)); + } + } + else + { + Assert.False(collectionEntry.IsLoaded); + Assert.Equal(1, parent.Children.Count()); } } @@ -3782,20 +4094,28 @@ public virtual void Lazy_load_collection_delegate_loader_constructor_injection( changeDetector.DetectChangesCalled = false; - Assert.NotNull(parent.Children); + if (LazyLoadingEnabled) + { + Assert.NotNull(parent.Children); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.True(collectionEntry.IsLoaded); + Assert.True(collectionEntry.IsLoaded); - Assert.All(parent.Children.Select(e => e.Parent), p => Assert.Same(parent, p)); + Assert.All(parent.Children.Select(e => e.Parent), p => Assert.Same(parent, p)); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, parent.Children.Count()); + Assert.Equal(2, parent.Children.Count()); - Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); + } + else + { + Assert.Null(parent.Children); + Assert.False(collectionEntry.IsLoaded); + } } [ConditionalTheory] @@ -3834,44 +4154,52 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_delegate_loader changeDetector.DetectChangesCalled = false; - if (state == EntityState.Deleted) - { - Assert.Null(child.Parent); - } - else + if (LazyLoadingEnabled) { - Assert.NotNull(child.Parent); - } - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + } + else + { + Assert.NotNull(child.Parent); + } - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.True(referenceEntry.IsLoaded); - if (state != EntityState.Deleted) - { - Assert.Same(child, child.Parent!.Children.Single()); - } + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - if (state != EntityState.Detached) - { - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(child.Parent); - Assert.Null(parent.Children); + Assert.Same(child, child.Parent!.Children.Single()); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.Children.Single()); + 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()); + } } } + else + { + Assert.Null(child.Parent); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -3910,44 +4238,52 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_delegate_loader_ changeDetector.DetectChangesCalled = false; - if (state == EntityState.Deleted) - { - Assert.Null(single.Parent); - } - else + if (LazyLoadingEnabled) { - Assert.NotNull(single.Parent); - } - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + } + else + { + Assert.NotNull(single.Parent); + } - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.True(referenceEntry.IsLoaded); - if (state != EntityState.Deleted) - { - Assert.Same(single, single.Parent!.Single); - } + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - if (state != EntityState.Detached) - { - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(single.Parent); - Assert.Null(parent.Single); + Assert.Same(single, single.Parent!.Single); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.Single); + 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); + } } } + else + { + Assert.Null(single.Parent); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -3986,28 +4322,36 @@ public virtual void Lazy_load_one_to_one_reference_to_dependent_delegate_loader_ changeDetector.DetectChangesCalled = false; - Assert.NotNull(parent.Single); + if (LazyLoadingEnabled) + { + Assert.NotNull(parent.Single); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.True(referenceEntry.IsLoaded); + Assert.True(referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state != EntityState.Deleted) - { - Assert.Same(parent, parent.Single.Parent); - } + if (state != EntityState.Deleted) + { + Assert.Same(parent, parent.Single.Parent); + } - if (state != EntityState.Detached) - { - var single = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(single, parent.Single); - Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + Assert.Same(parent, single.Parent); + } + } + else + { + Assert.Null(parent.Single); + Assert.False(referenceEntry.IsLoaded); } } @@ -4369,22 +4713,31 @@ public virtual void Lazy_load_collection_already_loaded_delegate_loader_construc changeDetector.DetectChangesCalled = false; + var originalLoadedState = collectionEntry.IsLoaded; + Assert.NotNull(parent.Children); Assert.False(changeDetector.DetectChangesCalled); - Assert.True(collectionEntry.IsLoaded); + if (LazyLoadingEnabled) + { + Assert.True(collectionEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + 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 - ? 4 - : 2, parent.Children.Count()); + // Note that when detached there is no identity resolution, so loading results in duplicates + Assert.Equal( + state == EntityState.Detached && queryTrackingBehavior != QueryTrackingBehavior.NoTrackingWithIdentityResolution + ? 4 + : 2, parent.Children.Count()); - Assert.All(parent.Children.Select(e => e.Parent), p => Assert.Same(parent, p)); + Assert.All(parent.Children.Select(e => e.Parent), p => Assert.Same(parent, p)); + } + else + { + Assert.Equal(originalLoadedState, collectionEntry.IsLoaded); + } } [ConditionalTheory] @@ -4655,17 +5008,25 @@ public virtual void Lazy_load_collection_already_partially_loaded_delegate_loade RecordLog(); - Assert.True(collectionEntry.IsLoaded); + if (LazyLoadingEnabled) + { + Assert.True(collectionEntry.IsLoaded); - context.ChangeTracker.LazyLoadingEnabled = false; + 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()); + // 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)); + Assert.All(parent.Children.Select(e => e.Parent), p => Assert.Same(parent, p)); + } + else + { + Assert.False(collectionEntry.IsLoaded); + Assert.Single(parent.Children); + } } [ConditionalTheory] @@ -4704,27 +5065,35 @@ public virtual void Lazy_load_collection_delegate_loader_property_injection( changeDetector.DetectChangesCalled = false; - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) - { - Assert.Null(parent.Children); // Explicitly detached - } - else - { - Assert.NotNull(parent.Children); + if (LazyLoadingEnabled) + { + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(parent.Children); // Explicitly detached + } + else + { + Assert.NotNull(parent.Children); + + Assert.False(changeDetector.DetectChangesCalled); - Assert.False(changeDetector.DetectChangesCalled); + Assert.True(collectionEntry.IsLoaded); - Assert.True(collectionEntry.IsLoaded); + Assert.All(parent.Children.Select(e => e.Parent), p => Assert.Same(parent, p)); - Assert.All(parent.Children.Select(e => e.Parent), p => Assert.Same(parent, p)); + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + Assert.Equal(2, parent.Children.Count()); + } - Assert.Equal(2, parent.Children.Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); + } + else + { + Assert.Null(parent.Children); + Assert.False(collectionEntry.IsLoaded); } - - Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] @@ -4763,51 +5132,59 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_delegate_loader changeDetector.DetectChangesCalled = false; - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + if (LazyLoadingEnabled) { - Assert.Null(child.Parent); // Explicitly detached - } - else - { - if (state == EntityState.Deleted) + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) { - Assert.Null(child.Parent); + Assert.Null(child.Parent); // Explicitly detached } else { - Assert.NotNull(child.Parent); - } - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + } + else + { + Assert.NotNull(child.Parent); + } - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.True(referenceEntry.IsLoaded); - if (state != EntityState.Deleted) - { - Assert.Same(child, child.Parent!.Children.Single()); - } + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - if (state != EntityState.Detached) - { - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(child.Parent); - Assert.Null(parent.Children); + Assert.Same(child, child.Parent!.Children.Single()); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.Children.Single()); + 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()); + } } } } + else + { + Assert.Null(child.Parent); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -4846,51 +5223,59 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_delegate_loader_ changeDetector.DetectChangesCalled = false; - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + if (LazyLoadingEnabled) { - Assert.Null(single.Parent); // Explicitly detached - } - else - { - if (state == EntityState.Deleted) + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) { - Assert.Null(single.Parent); + Assert.Null(single.Parent); // Explicitly detached } else { - Assert.NotNull(single.Parent); - } - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + } + else + { + Assert.NotNull(single.Parent); + } - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.True(referenceEntry.IsLoaded); - if (state != EntityState.Deleted) - { - Assert.Same(single, single.Parent!.Single); - } + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - if (state != EntityState.Detached) - { - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(single.Parent); - Assert.Null(parent.Single); + Assert.Same(single, single.Parent!.Single); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.Single); + 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); + } } } } + else + { + Assert.Null(single.Parent); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -4929,36 +5314,44 @@ public virtual void Lazy_load_one_to_one_reference_to_dependent_delegate_loader_ changeDetector.DetectChangesCalled = false; - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + if (LazyLoadingEnabled) { - Assert.Null(parent.Single); // Explicitly detached - } - else - { - Assert.NotNull(parent.Single); + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(parent.Single); // Explicitly detached + } + else + { + Assert.NotNull(parent.Single); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.True(referenceEntry.IsLoaded); + Assert.True(referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state != EntityState.Deleted) - { - Assert.Same(parent, parent.Single.Parent); - } + if (state != EntityState.Deleted) + { + Assert.Same(parent, parent.Single.Parent); + } - if (state != EntityState.Detached) - { - var single = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(single, parent.Single); - Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + Assert.Same(parent, single.Parent); + } } } + else + { + Assert.Null(parent.Single); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -4997,17 +5390,25 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK_delegat changeDetector.DetectChangesCalled = false; - Assert.Null(child.Parent); + if (LazyLoadingEnabled) + { + Assert.Null(child.Parent); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); - Assert.Null(child.Parent); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Null(child.Parent); + } + else + { + Assert.Null(child.Parent); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -5046,18 +5447,26 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK_delegate changeDetector.DetectChangesCalled = false; - Assert.Null(single.Parent); + if (LazyLoadingEnabled) + { + Assert.Null(single.Parent); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); - Assert.Null(single.Parent); + Assert.Null(single.Parent); + } + else + { + Assert.Null(single.Parent); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -5096,20 +5505,28 @@ public virtual void Lazy_load_collection_not_found_delegate_loader_property_inje changeDetector.DetectChangesCalled = false; - if (state == EntityState.Detached) + if (LazyLoadingEnabled) { - Assert.Null(parent.Children); // Explicitly detached + 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()); + } } else { - Assert.Empty(parent.Children); - Assert.False(changeDetector.DetectChangesCalled); - Assert.True(collectionEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Single(context.ChangeTracker.Entries()); + Assert.Null(parent.Children); + Assert.False(collectionEntry.IsLoaded); } } @@ -5149,17 +5566,25 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_not_found_deleg changeDetector.DetectChangesCalled = false; - Assert.Null(child.Parent); + if (LazyLoadingEnabled) + { + Assert.Null(child.Parent); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); - Assert.Null(child.Parent); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Null(child.Parent); + } + else + { + Assert.Null(child.Parent); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -5198,18 +5623,26 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_not_found_delega changeDetector.DetectChangesCalled = false; - Assert.Null(single.Parent); + if (LazyLoadingEnabled) + { + Assert.Null(single.Parent); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); - Assert.Null(single.Parent); + Assert.Null(single.Parent); + } + else + { + Assert.Null(single.Parent); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -5248,18 +5681,26 @@ public virtual void Lazy_load_one_to_one_reference_to_dependent_not_found_delega changeDetector.DetectChangesCalled = false; - Assert.Null(parent.Single); + if (LazyLoadingEnabled) + { + Assert.Null(parent.Single); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Null(parent.Single); + Assert.Null(parent.Single); - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + } + else + { + Assert.Null(parent.Single); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -5326,30 +5767,39 @@ public virtual void Lazy_load_collection_already_loaded_delegate_loader_property changeDetector.DetectChangesCalled = false; + var originalLoadedState = collectionEntry.IsLoaded; + Assert.NotNull(parent.Children); Assert.False(changeDetector.DetectChangesCalled); - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + if (LazyLoadingEnabled) { - Assert.False(collectionEntry.IsLoaded); // Explicitly detached - Assert.Equal(2, parent.Children.Count()); + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.False(collectionEntry.IsLoaded); // Explicitly detached + Assert.Equal(2, parent.Children.Count()); + } + else + { + Assert.True(collectionEntry.IsLoaded); + + RecordLog(); + 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 + ? 4 + : 2, parent.Children.Count()); + } + + Assert.All(parent.Children.Select(e => e.Parent), p => Assert.Same(parent, p)); } else { - Assert.True(collectionEntry.IsLoaded); - - RecordLog(); - 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 - ? 4 - : 2, parent.Children.Count()); + Assert.Equal(originalLoadedState, collectionEntry.IsLoaded); } - - Assert.All(parent.Children.Select(e => e.Parent), p => Assert.Same(parent, p)); } [ConditionalTheory] @@ -5618,7 +6068,7 @@ public virtual void Lazy_load_collection_already_partially_loaded_delegate_loade RecordLog(); - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + if (!LazyLoadingEnabled || (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll)) { Assert.False(collectionEntry.IsLoaded); // Explicitly detached Assert.Equal(1, parent.Children.Count()); @@ -5677,27 +6127,35 @@ public virtual void Lazy_load_collection_delegate_loader_with_state_property_inj changeDetector.DetectChangesCalled = false; - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) - { - Assert.Null(parent.Children); // Explicitly detached - } - else - { - Assert.NotNull(parent.Children); + if (LazyLoadingEnabled) + { + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(parent.Children); // Explicitly detached + } + else + { + Assert.NotNull(parent.Children); + + Assert.False(changeDetector.DetectChangesCalled); - Assert.False(changeDetector.DetectChangesCalled); + Assert.True(collectionEntry.IsLoaded); - Assert.True(collectionEntry.IsLoaded); + Assert.All(parent.Children.Select(e => e.Parent), p => Assert.Same(parent, p)); - Assert.All(parent.Children.Select(e => e.Parent), p => Assert.Same(parent, p)); + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + Assert.Equal(2, parent.Children.Count()); + } - Assert.Equal(2, parent.Children.Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); + } + else + { + Assert.Null(parent.Children); + Assert.False(collectionEntry.IsLoaded); } - - Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] @@ -5736,51 +6194,59 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_delegate_loader changeDetector.DetectChangesCalled = false; - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + if (LazyLoadingEnabled) { - Assert.Null(child.Parent); // Explicitly detached - } - else - { - if (state == EntityState.Deleted) + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) { - Assert.Null(child.Parent); + Assert.Null(child.Parent); // Explicitly detached } else { - Assert.NotNull(child.Parent); - } - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + } + else + { + Assert.NotNull(child.Parent); + } - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.True(referenceEntry.IsLoaded); - if (state != EntityState.Deleted) - { - Assert.Same(child, child.Parent!.Children.Single()); - } + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - if (state != EntityState.Detached) - { - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(child.Parent); - Assert.Null(parent.Children); + Assert.Same(child, child.Parent!.Children.Single()); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.Children.Single()); + 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()); + } } } } + else + { + Assert.Null(child.Parent); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -5819,51 +6285,59 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_delegate_loader_ changeDetector.DetectChangesCalled = false; - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + if (LazyLoadingEnabled) { - Assert.Null(single.Parent); // Explicitly detached - } - else - { - if (state == EntityState.Deleted) + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) { - Assert.Null(single.Parent); + Assert.Null(single.Parent); // Explicitly detached } else { - Assert.NotNull(single.Parent); - } - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + } + else + { + Assert.NotNull(single.Parent); + } - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.True(referenceEntry.IsLoaded); - if (state != EntityState.Deleted) - { - Assert.Same(single, single.Parent!.Single); - } + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - if (state != EntityState.Detached) - { - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(single.Parent); - Assert.Null(parent.Single); + Assert.Same(single, single.Parent!.Single); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.Single); + 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); + } } } } + else + { + Assert.Null(single.Parent); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -5902,36 +6376,44 @@ public virtual void Lazy_load_one_to_one_reference_to_dependent_delegate_loader_ changeDetector.DetectChangesCalled = false; - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + if (LazyLoadingEnabled) { - Assert.Null(parent.Single); // Explicitly detached - } - else - { - Assert.NotNull(parent.Single); + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(parent.Single); // Explicitly detached + } + else + { + Assert.NotNull(parent.Single); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.True(referenceEntry.IsLoaded); + Assert.True(referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state != EntityState.Deleted) - { - Assert.Same(parent, parent.Single.Parent); - } + if (state != EntityState.Deleted) + { + Assert.Same(parent, parent.Single.Parent); + } - if (state != EntityState.Detached) - { - var single = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(single, parent.Single); - Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + Assert.Same(parent, single.Parent); + } } } + else + { + Assert.Null(parent.Single); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -5970,17 +6452,25 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK_delegat changeDetector.DetectChangesCalled = false; - Assert.Null(child.Parent); + if (LazyLoadingEnabled) + { + Assert.Null(child.Parent); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); - Assert.Null(child.Parent); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Null(child.Parent); + } + else + { + Assert.Null(child.Parent); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -6019,18 +6509,26 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK_delegate changeDetector.DetectChangesCalled = false; - Assert.Null(single.Parent); + if (LazyLoadingEnabled) + { + Assert.Null(single.Parent); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); - Assert.Null(single.Parent); + Assert.Null(single.Parent); + } + else + { + Assert.Null(single.Parent); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -6069,20 +6567,28 @@ public virtual void Lazy_load_collection_not_found_delegate_loader_with_state_pr changeDetector.DetectChangesCalled = false; - if (state == EntityState.Detached) + if (LazyLoadingEnabled) { - Assert.Null(parent.Children); // Explicitly detached + 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()); + } } else { - Assert.Empty(parent.Children); - Assert.False(changeDetector.DetectChangesCalled); - Assert.True(collectionEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Single(context.ChangeTracker.Entries()); + Assert.Null(parent.Children); + Assert.False(collectionEntry.IsLoaded); } } @@ -6122,17 +6628,25 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_not_found_deleg changeDetector.DetectChangesCalled = false; - Assert.Null(child.Parent); + if (LazyLoadingEnabled) + { + Assert.Null(child.Parent); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); - Assert.Null(child.Parent); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Null(child.Parent); + } + else + { + Assert.Null(child.Parent); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -6171,18 +6685,26 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_not_found_delega changeDetector.DetectChangesCalled = false; - Assert.Null(single.Parent); + if (LazyLoadingEnabled) + { + Assert.Null(single.Parent); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); - Assert.Null(single.Parent); + Assert.Null(single.Parent); + } + else + { + Assert.Null(single.Parent); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -6221,18 +6743,26 @@ public virtual void Lazy_load_one_to_one_reference_to_dependent_not_found_delega changeDetector.DetectChangesCalled = false; - Assert.Null(parent.Single); + if (LazyLoadingEnabled) + { + Assert.Null(parent.Single); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Null(parent.Single); + Assert.Null(parent.Single); - Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + } + else + { + Assert.Null(parent.Single); + Assert.False(referenceEntry.IsLoaded); + } } [ConditionalTheory] @@ -6578,26 +7108,34 @@ public virtual void Lazy_load_collection_already_partially_loaded_delegate_loade RecordLog(); - if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + if (LazyLoadingEnabled) { - Assert.False(collectionEntry.IsLoaded); // Explicitly detached - Assert.Equal(1, parent.Children.Count()); + 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); + Assert.All(parent.Children.Select(e => e.Parent), p => Assert.Same(parent, p)); + } + else + { + Assert.True(collectionEntry.IsLoaded); - context.ChangeTracker.LazyLoadingEnabled = false; + 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()); + // 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)); + Assert.All(parent.Children.Select(e => e.Parent), p => Assert.Same(parent, p)); + } + } + else + { + Assert.False(collectionEntry.IsLoaded); + Assert.Equal(1, parent.Children.Count()); } } @@ -6608,7 +7146,17 @@ public virtual void Lazy_loading_uses_field_access_when_abstract_base_class_navi var product = context.Set().Single(); var deposit = product.Deposit; - Assert.NotNull(deposit); - Assert.Same(deposit, product.Deposit); + if (LazyLoadingEnabled) + { + Assert.NotNull(deposit); + Assert.Same(deposit, product.Deposit); + } + else + { + Assert.Null(deposit); + } } + + protected virtual bool LazyLoadingEnabled + => true; } diff --git a/test/EFCore.Specification.Tests/ManyToManyLoadTestBase.cs b/test/EFCore.Specification.Tests/ManyToManyLoadTestBase.cs index 17732d4f371..15a783fa90e 100644 --- a/test/EFCore.Specification.Tests/ManyToManyLoadTestBase.cs +++ b/test/EFCore.Specification.Tests/ManyToManyLoadTestBase.cs @@ -75,6 +75,7 @@ public virtual async Task Load_collection(EntityState state, QueryTrackingBehavi } else { + Assert.Null(left.TwoSkip); if (async) { await collectionEntry.LoadAsync(); @@ -86,7 +87,7 @@ public virtual async Task Load_collection(EntityState state, QueryTrackingBehavi } Assert.True(collectionEntry.IsLoaded); - foreach (var entityTwo in left.TwoSkip) + foreach (var entityTwo in left.TwoSkip!) { Assert.False(context.Entry(entityTwo).Collection(e => e.OneSkip).IsLoaded); } @@ -261,6 +262,7 @@ public virtual async Task Load_collection_already_loaded(EntityState state, bool } else { + Assert.Equal(4, left.ThreeSkipPayloadFull.Count); if (async) { await collectionEntry.LoadAsync(); @@ -583,6 +585,7 @@ public virtual void Load_collection_partially_loaded_no_tracking(QueryTrackingBe } else { + Assert.Single(left.ThreeSkipPayloadFull); if (queryTrackingBehavior == QueryTrackingBehavior.NoTrackingWithIdentityResolution) { collectionEntry.LoadWithIdentityResolution(); @@ -650,6 +653,7 @@ public virtual async Task Load_collection_untyped(EntityState state, bool async) } else { + Assert.Null(left.TwoSkip); if (async) { await navigationEntry.LoadAsync(); @@ -661,7 +665,7 @@ public virtual async Task Load_collection_untyped(EntityState state, bool async) } Assert.True(navigationEntry.IsLoaded); - foreach (var entityTwo in left.TwoSkip) + foreach (var entityTwo in left.TwoSkip!) { Assert.False(context.Entry((object)entityTwo).Collection("OneSkip").IsLoaded); } @@ -771,6 +775,7 @@ public virtual async Task Load_collection_not_found_untyped(EntityState state, b } else { + Assert.Null(left.TwoSkip); if (async) { await navigationEntry.LoadAsync(); @@ -786,7 +791,7 @@ public virtual async Task Load_collection_not_found_untyped(EntityState state, b RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Empty(left.TwoSkip); + Assert.Empty(left.TwoSkip!); } Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); @@ -883,6 +888,7 @@ public virtual async Task Load_collection_already_loaded_untyped(EntityState sta } else { + Assert.Equal(4, left.ThreeSkipPayloadFull.Count); if (async) { await navigationEntry.LoadAsync(); @@ -1025,6 +1031,7 @@ public virtual async Task Load_collection_composite_key(EntityState state, bool } else { + Assert.Null(left.ThreeSkipFull); if (async) { await collectionEntry.LoadAsync(); @@ -1036,7 +1043,7 @@ public virtual async Task Load_collection_composite_key(EntityState state, bool } Assert.True(collectionEntry.IsLoaded); - foreach (var entityTwo in left.ThreeSkipFull) + foreach (var entityTwo in left.ThreeSkipFull!) { Assert.False(context.Entry(entityTwo).Collection(e => e.CompositeKeySkipFull).IsLoaded); } diff --git a/test/EFCore.Sqlite.FunctionalTests/NonLoadingNavigationsManyToManyLoadSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/NonLoadingNavigationsManyToManyLoadSqliteTest.cs new file mode 100644 index 00000000000..d1106c05344 --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/NonLoadingNavigationsManyToManyLoadSqliteTest.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.TestModels.ManyToManyModel; + +namespace Microsoft.EntityFrameworkCore; + +public class NonLoadingNavigationsManyToManyLoadSqliteTest + : ManyToManyLoadTestBase +{ + public NonLoadingNavigationsManyToManyLoadSqliteTest(NonLoadingNavigationsManyToManyLoadSqliteFixture fixture) + : base(fixture) + { + } + + public class NonLoadingNavigationsManyToManyLoadSqliteFixture : ManyToManyLoadFixtureBase + { + protected override string StoreName + => "NonLoadingNavigationsManyToMany"; + + protected override ITestStoreFactory TestStoreFactory + => SqliteTestStoreFactory.Instance; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder).UseLazyLoadingProxies(); + + protected override IServiceCollection AddServices(IServiceCollection serviceCollection) + => base.AddServices(serviceCollection.AddEntityFrameworkProxies()); + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Entity( + b => + { + b.Navigation(e => e.Reference).EnableLazyLoading(false); + b.Navigation(e => e.Collection).EnableLazyLoading(false); + b.Navigation(e => e.TwoSkip).EnableLazyLoading(false); + b.Navigation(e => e.ThreeSkipPayloadFull).EnableLazyLoading(false); + b.Navigation(e => e.TwoSkipShared).EnableLazyLoading(false); + b.Navigation(e => e.ThreeSkipPayloadFullShared).EnableLazyLoading(false); + b.Navigation(e => e.JoinThreePayloadFullShared).EnableLazyLoading(false); + b.Navigation(e => e.SelfSkipPayloadLeft).EnableLazyLoading(false); + b.Navigation(e => e.JoinSelfPayloadLeft).EnableLazyLoading(false); + b.Navigation(e => e.SelfSkipPayloadRight).EnableLazyLoading(false); + b.Navigation(e => e.JoinSelfPayloadRight).EnableLazyLoading(false); + b.Navigation(e => e.BranchSkip).EnableLazyLoading(false); + }); + + modelBuilder.Entity( + b => + { + b.Navigation(e => e.TwoSkipShared).EnableLazyLoading(false); + b.Navigation(e => e.ThreeSkipFull).EnableLazyLoading(false); + b.Navigation(e => e.JoinThreeFull).EnableLazyLoading(false); + b.Navigation(e => e.RootSkipShared).EnableLazyLoading(false); + b.Navigation(e => e.LeafSkipFull).EnableLazyLoading(false); + b.Navigation(e => e.JoinLeafFull).EnableLazyLoading(false); + }); + } + } +} diff --git a/test/EFCore.Tests/ApiConsistencyTest.cs b/test/EFCore.Tests/ApiConsistencyTest.cs index 4484a680574..1ac67317e30 100644 --- a/test/EFCore.Tests/ApiConsistencyTest.cs +++ b/test/EFCore.Tests/ApiConsistencyTest.cs @@ -92,6 +92,20 @@ protected override void Initialize() typeof(IEntityTypeConfiguration<>).GetMethod(nameof(IEntityTypeConfiguration.Configure)) }; + public override Dictionary MetadataMethodNameTransformers { get; } = new() + { + { + typeof(IConventionNavigationBuilder).GetMethod( + nameof(IConventionNavigationBuilder.EnableLazyLoading), new[] { typeof(bool?), typeof(bool) })!, + "LazyLoadingEnabled" + }, + { + typeof(IConventionSkipNavigationBuilder).GetMethod( + nameof(IConventionSkipNavigationBuilder.EnableLazyLoading), new[] { typeof(bool?), typeof(bool) })!, + "LazyLoadingEnabled" + } + }; + public override HashSet UnmatchedMetadataMethods { get; } = new() { typeof(OwnedNavigationBuilder).GetMethod( diff --git a/test/EFCore.Tests/Metadata/Internal/InternalNavigationBuilderTest.cs b/test/EFCore.Tests/Metadata/Internal/InternalNavigationBuilderTest.cs index 6c0a34ae3dc..20989017f31 100644 --- a/test/EFCore.Tests/Metadata/Internal/InternalNavigationBuilderTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/InternalNavigationBuilderTest.cs @@ -154,6 +154,42 @@ public void Can_only_override_lower_or_equal_source_IsEagerLoaded() Assert.Null(metadata.GetIsEagerLoadedConfigurationSource()); } + [ConditionalFact] + public void Can_only_override_lower_or_equal_source_LazyLoadingEnabled() + { + var builder = CreateInternalNavigationBuilder(); + IConventionNavigation metadata = builder.Metadata; + + Assert.True(metadata.LazyLoadingEnabled); + Assert.Null(metadata.GetLazyLoadingEnabledConfigurationSource()); + + Assert.True(builder.CanSetLazyLoadingEnabled(lazyLoadingEnabled: false, ConfigurationSource.DataAnnotation)); + Assert.NotNull(builder.EnableLazyLoading(lazyLoadingEnabled: false, ConfigurationSource.DataAnnotation)); + + Assert.False(metadata.LazyLoadingEnabled); + Assert.Equal(ConfigurationSource.DataAnnotation, metadata.GetLazyLoadingEnabledConfigurationSource()); + + Assert.True(builder.CanSetLazyLoadingEnabled(lazyLoadingEnabled: false, ConfigurationSource.Convention)); + Assert.False(builder.CanSetLazyLoadingEnabled(lazyLoadingEnabled: true, ConfigurationSource.Convention)); + Assert.NotNull(builder.EnableLazyLoading(lazyLoadingEnabled: false, ConfigurationSource.Convention)); + Assert.Null(builder.EnableLazyLoading(lazyLoadingEnabled: true, ConfigurationSource.Convention)); + + Assert.False(metadata.LazyLoadingEnabled); + Assert.Equal(ConfigurationSource.DataAnnotation, metadata.GetLazyLoadingEnabledConfigurationSource()); + + Assert.True(builder.CanSetLazyLoadingEnabled(lazyLoadingEnabled: true, ConfigurationSource.DataAnnotation)); + Assert.NotNull(builder.EnableLazyLoading(lazyLoadingEnabled: true, ConfigurationSource.DataAnnotation)); + + Assert.True(metadata.LazyLoadingEnabled); + Assert.Equal(ConfigurationSource.DataAnnotation, metadata.GetLazyLoadingEnabledConfigurationSource()); + + Assert.True(builder.CanSetLazyLoadingEnabled(null, ConfigurationSource.DataAnnotation)); + Assert.NotNull(builder.EnableLazyLoading(null, ConfigurationSource.DataAnnotation)); + + Assert.True(metadata.LazyLoadingEnabled); + Assert.Null(metadata.GetLazyLoadingEnabledConfigurationSource()); + } + [ConditionalFact] public void Configuring_IsRequired_on_to_dependent_nonUnique_throws() { diff --git a/test/EFCore.Tests/Metadata/Internal/InternalSkipNavigationBuilderTest.cs b/test/EFCore.Tests/Metadata/Internal/InternalSkipNavigationBuilderTest.cs index 5f161db388f..fe6e7f51436 100644 --- a/test/EFCore.Tests/Metadata/Internal/InternalSkipNavigationBuilderTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/InternalSkipNavigationBuilderTest.cs @@ -262,6 +262,42 @@ public void Can_only_override_lower_or_equal_source_IsEagerLoaded() Assert.Null(metadata.GetIsEagerLoadedConfigurationSource()); } + [ConditionalFact] + public void Can_only_override_lower_or_equal_source_LazyLoadingEnabled() + { + var builder = CreateInternalSkipNavigationBuilder(); + IConventionSkipNavigation metadata = builder.Metadata; + + Assert.True(metadata.LazyLoadingEnabled); + Assert.Null(metadata.GetLazyLoadingEnabledConfigurationSource()); + + Assert.True(builder.CanSetLazyLoadingEnabled(lazyLoadingEnabled: false, ConfigurationSource.DataAnnotation)); + Assert.NotNull(builder.EnableLazyLoading(lazyLoadingEnabled: false, ConfigurationSource.DataAnnotation)); + + Assert.False(metadata.LazyLoadingEnabled); + Assert.Equal(ConfigurationSource.DataAnnotation, metadata.GetLazyLoadingEnabledConfigurationSource()); + + Assert.True(builder.CanSetLazyLoadingEnabled(lazyLoadingEnabled: false, ConfigurationSource.Convention)); + Assert.False(builder.CanSetLazyLoadingEnabled(lazyLoadingEnabled: true, ConfigurationSource.Convention)); + Assert.NotNull(builder.EnableLazyLoading(lazyLoadingEnabled: false, ConfigurationSource.Convention)); + Assert.Null(builder.EnableLazyLoading(lazyLoadingEnabled: true, ConfigurationSource.Convention)); + + Assert.False(metadata.LazyLoadingEnabled); + Assert.Equal(ConfigurationSource.DataAnnotation, metadata.GetLazyLoadingEnabledConfigurationSource()); + + Assert.True(builder.CanSetLazyLoadingEnabled(lazyLoadingEnabled: true, ConfigurationSource.DataAnnotation)); + Assert.NotNull(builder.EnableLazyLoading(lazyLoadingEnabled: true, ConfigurationSource.DataAnnotation)); + + Assert.True(metadata.LazyLoadingEnabled); + Assert.Equal(ConfigurationSource.DataAnnotation, metadata.GetLazyLoadingEnabledConfigurationSource()); + + Assert.True(builder.CanSetLazyLoadingEnabled(null, ConfigurationSource.DataAnnotation)); + Assert.NotNull(builder.EnableLazyLoading(null, ConfigurationSource.DataAnnotation)); + + Assert.True(metadata.LazyLoadingEnabled); + Assert.Null(metadata.GetLazyLoadingEnabledConfigurationSource()); + } + private InternalSkipNavigationBuilder CreateInternalSkipNavigationBuilder() { var modelBuilder = (InternalModelBuilder) diff --git a/test/EFCore.Tests/Metadata/Internal/SkipNavigationTest.cs b/test/EFCore.Tests/Metadata/Internal/SkipNavigationTest.cs index 5b2755a3a72..41348cdb38d 100644 --- a/test/EFCore.Tests/Metadata/Internal/SkipNavigationTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/SkipNavigationTest.cs @@ -45,6 +45,10 @@ public void Throws_when_model_is_readonly() CoreStrings.ModelReadOnly, Assert.Throws(() => navigation.SetIsEagerLoaded(null)).Message); + Assert.Equal( + CoreStrings.ModelReadOnly, + Assert.Throws(() => navigation.SetLazyLoadingEnabled(null)).Message); + Assert.Equal( CoreStrings.ModelReadOnly, Assert.Throws(() => navigation.SetPropertyAccessMode(null)).Message); diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs index d7a8c680297..e00358d5610 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs @@ -664,6 +664,9 @@ public override TestNavigationBuilder HasField(string fieldName) public override TestNavigationBuilder AutoInclude(bool autoInclude = true) => new GenericTestNavigationBuilder(NavigationBuilder.AutoInclude(autoInclude)); + public override TestNavigationBuilder EnableLazyLoading(bool lazyLoadingEnabled = true) + => new GenericTestNavigationBuilder(NavigationBuilder.EnableLazyLoading(lazyLoadingEnabled)); + public override TestNavigationBuilder IsRequired(bool required = true) => new GenericTestNavigationBuilder(NavigationBuilder.IsRequired(required)); } diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs index 8235121bb6f..e3e88baa4a3 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs @@ -695,6 +695,9 @@ public override TestNavigationBuilder HasField(string fieldName) public override TestNavigationBuilder AutoInclude(bool autoInclude = true) => new NonGenericTestNavigationBuilder(NavigationBuilder.AutoInclude(autoInclude)); + public override TestNavigationBuilder EnableLazyLoading(bool lazyLoadingEnabled = true) + => new NonGenericTestNavigationBuilder(NavigationBuilder.EnableLazyLoading(lazyLoadingEnabled)); + public override TestNavigationBuilder IsRequired(bool required = true) => new NonGenericTestNavigationBuilder(NavigationBuilder.IsRequired(required)); } diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs index dcd17556d7f..a1f17aed013 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs @@ -456,6 +456,7 @@ public abstract class TestNavigationBuilder public abstract TestNavigationBuilder UsePropertyAccessMode(PropertyAccessMode propertyAccessMode); public abstract TestNavigationBuilder HasField(string fieldName); public abstract TestNavigationBuilder AutoInclude(bool autoInclude = true); + public abstract TestNavigationBuilder EnableLazyLoading(bool lazyLoadingEnabled = true); public abstract TestNavigationBuilder IsRequired(bool required = true); }