From da5ed2e2d89bd2c5cc59c759b5eb19d9951d6202 Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Mon, 6 Jul 2020 21:05:52 -0700 Subject: [PATCH] Model level eager loading configuration (#19355) IsEagerLoaded API on NavigationBuilder to mark a navigation as eagar loaded IgnoreEagerLoadedNavigations API on query to ignore model based config Resolves #21540 --- .../EntityFrameworkQueryableExtensions.cs | 33 ++++ .../Builders/IConventionForeignKeyBuilder.cs | 27 ---- .../Builders/IConventionNavigationBuilder.cs | 20 +++ .../IConventionSkipNavigationBuilder.cs | 20 +++ .../Metadata/Builders/NavigationBuilder.cs | 23 ++- .../NavigationEagerLoadingConvention.cs | 2 +- .../Internal/InternalForeignKeyBuilder.cs | 78 +-------- .../Internal/InternalNavigationBuilder.cs | 66 +++++--- .../Internal/InternalPropertyBaseBuilder`.cs | 8 +- .../Internal/InternalSkipNavigationBuilder.cs | 42 +++++ .../Metadata/Internal/SkipNavigation.cs | 9 ++ .../NavigationExpandingExpressionVisitor.cs | 7 +- ...yableMethodNormalizingExpressionVisitor.cs | 8 + src/EFCore/Query/QueryCompilationContext.cs | 4 + .../Query/QueryBugsTest.cs | 151 +++++++++++++++++- .../Metadata/Internal/EntityTypeTest.cs | 2 +- .../Internal/InternalNavigationBuilderTest.cs | 36 +++++ .../InternalSkipNavigationBuilderTest.cs | 36 +++++ 18 files changed, 436 insertions(+), 136 deletions(-) diff --git a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs index 10de3e8f741..e3b92d4b004 100644 --- a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs +++ b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs @@ -2655,6 +2655,39 @@ source.Provider is EntityQueryProvider #endregion + #region Eager loaded navigations + + internal static readonly MethodInfo IgnoreEagerLoadedNavigationsMethodInfo + = typeof(EntityFrameworkQueryableExtensions) + .GetTypeInfo().GetDeclaredMethod(nameof(IgnoreEagerLoadedNavigations)); + + /// + /// Specifies that the current Entity Framework LINQ query should not have any + /// model-level eager loaded navigations applied. + /// + /// The type of entity being queried. + /// The source query. + /// + /// A new query that will not apply any model-level eager loaded navigations. + /// + public static IQueryable IgnoreEagerLoadedNavigations( + [NotNull] this IQueryable source) + where TEntity : class + { + Check.NotNull(source, nameof(source)); + + return + source.Provider is EntityQueryProvider + ? source.Provider.CreateQuery( + Expression.Call( + instance: null, + method: IgnoreEagerLoadedNavigationsMethodInfo.MakeGenericMethod(typeof(TEntity)), + arguments: source.Expression)) + : source; + } + + #endregion + #region Query Filters internal static readonly MethodInfo IgnoreQueryFiltersMethodInfo diff --git a/src/EFCore/Metadata/Builders/IConventionForeignKeyBuilder.cs b/src/EFCore/Metadata/Builders/IConventionForeignKeyBuilder.cs index 71567e3aeca..d89a93fdae8 100644 --- a/src/EFCore/Metadata/Builders/IConventionForeignKeyBuilder.cs +++ b/src/EFCore/Metadata/Builders/IConventionForeignKeyBuilder.cs @@ -389,33 +389,6 @@ IConventionForeignKeyBuilder UsePropertyAccessMode( bool CanSetPropertyAccessMode( PropertyAccessMode? propertyAccessMode, bool pointsToPrincipal, bool fromDataAnnotation = false); - /// - /// Configures whether this navigation should be eager loaded by default. - /// - /// A value indicating whether this navigation should be eager loaded by default. - /// - /// A value indicating whether the navigation is on the dependent type pointing to the principal type. - /// - /// Indicates whether the configuration was specified using a data annotation. - /// - /// The same builder instance if the configuration was applied, - /// otherwise. - /// - IConventionForeignKeyBuilder IsEagerLoaded( - bool? eagerLoaded, bool pointsToPrincipal, bool fromDataAnnotation = false); - - /// - /// Returns a value indicating whether this navigation can be configured as should be eager loaded by default - /// from the current configuration source. - /// - /// A value indicating whether this navigation should be eager loaded by default. - /// - /// A value indicating whether the navigation is on the dependent type pointing to the principal type. - /// - /// Indicates whether the configuration was specified using a data annotation. - /// if this navigation can be configured as should be eager loaded by default. - bool CanSetIsEagerLoaded(bool? eagerLoaded, bool pointsToPrincipal, bool fromDataAnnotation = false); - /// /// Configures whether this is a required relationship (i.e. whether none the foreign key properties can /// be assigned ). diff --git a/src/EFCore/Metadata/Builders/IConventionNavigationBuilder.cs b/src/EFCore/Metadata/Builders/IConventionNavigationBuilder.cs index d973fb726ef..f27924c7deb 100644 --- a/src/EFCore/Metadata/Builders/IConventionNavigationBuilder.cs +++ b/src/EFCore/Metadata/Builders/IConventionNavigationBuilder.cs @@ -60,5 +60,25 @@ public interface IConventionNavigationBuilder : IConventionAnnotatableBuilder /// otherwise. /// IConventionNavigationBuilder UsePropertyAccessMode(PropertyAccessMode? propertyAccessMode, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether this navigation can be configured to be eager loaded in a query + /// from the current configuration source. + /// + /// A value indicating whether the navigation should be eager loaded. + /// Indicates whether the configuration was specified using a data annotation. + /// if eager loaded can be set for this navigation. + bool CanSetIsEagerLoaded(bool? eagerLoaded, bool fromDataAnnotation = false); + + /// + /// Configures this navigation to be eager loaded in a query + /// + /// A value indicating whether the navigation should be eager loaded. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + IConventionNavigationBuilder IsEagerLoaded(bool? eagerLoaded, bool fromDataAnnotation = false); } } diff --git a/src/EFCore/Metadata/Builders/IConventionSkipNavigationBuilder.cs b/src/EFCore/Metadata/Builders/IConventionSkipNavigationBuilder.cs index 876b74c25e5..25a90a11ef6 100644 --- a/src/EFCore/Metadata/Builders/IConventionSkipNavigationBuilder.cs +++ b/src/EFCore/Metadata/Builders/IConventionSkipNavigationBuilder.cs @@ -129,5 +129,25 @@ public interface IConventionSkipNavigationBuilder : IConventionAnnotatableBuilde /// Indicates whether the configuration was specified using a data annotation. /// if the can be set for this property. bool CanSetInverse([CanBeNull] IConventionSkipNavigation inverse, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether this navigation can be configured to be eager loaded in a query + /// from the current configuration source. + /// + /// A value indicating whether the navigation should be eager loaded. + /// Indicates whether the configuration was specified using a data annotation. + /// if eager loaded can be set for this navigation. + bool CanSetIsEagerLoaded(bool? eagerLoaded, bool fromDataAnnotation = false); + + /// + /// Configures this navigation to be eager loaded in a query + /// + /// A value indicating whether the navigation should be eager loaded. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + IConventionSkipNavigationBuilder IsEagerLoaded(bool? eagerLoaded, bool fromDataAnnotation = false); } } diff --git a/src/EFCore/Metadata/Builders/NavigationBuilder.cs b/src/EFCore/Metadata/Builders/NavigationBuilder.cs index bae49811769..c15ac298b5b 100644 --- a/src/EFCore/Metadata/Builders/NavigationBuilder.cs +++ b/src/EFCore/Metadata/Builders/NavigationBuilder.cs @@ -104,9 +104,7 @@ public virtual NavigationBuilder UsePropertyAccessMode(PropertyAccessMode proper } /// - /// - /// Sets a backing field to use for this navigation property. - /// + /// Sets a backing field to use for this navigation property. /// /// The name of the field to use for this navigation property. /// The same builder instance so that multiple configuration calls can be chained. @@ -124,6 +122,25 @@ public virtual NavigationBuilder HasField([CanBeNull] string fieldName) return this; } + /// + /// Configures whether this navigation should be eager loaded in a query. + /// + /// A value indicating if the navigation should be eager loaded. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual NavigationBuilder IsEagerLoaded(bool eagerLoaded = true) + { + if (InternalNavigationBuilder != null) + { + InternalNavigationBuilder.IsEagerLoaded(eagerLoaded, ConfigurationSource.Explicit); + } + else + { + InternalSkipNavigationBuilder.IsEagerLoaded(eagerLoaded, ConfigurationSource.Explicit); + } + + return this; + } + /// /// The internal builder being used to configure the skip navigation. /// diff --git a/src/EFCore/Metadata/Conventions/NavigationEagerLoadingConvention.cs b/src/EFCore/Metadata/Conventions/NavigationEagerLoadingConvention.cs index 979175cc623..795241103b3 100644 --- a/src/EFCore/Metadata/Conventions/NavigationEagerLoadingConvention.cs +++ b/src/EFCore/Metadata/Conventions/NavigationEagerLoadingConvention.cs @@ -34,7 +34,7 @@ public NavigationEagerLoadingConvention([NotNull] ProviderConventionSetBuilderDe public virtual void ProcessForeignKeyOwnershipChanged( IConventionForeignKeyBuilder relationshipBuilder, IConventionContext context) { - relationshipBuilder.Metadata.PrincipalToDependent?.SetIsEagerLoaded(relationshipBuilder.Metadata.IsOwnership); + relationshipBuilder.Metadata.PrincipalToDependent?.Builder.IsEagerLoaded(relationshipBuilder.Metadata.IsOwnership); } } } diff --git a/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs b/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs index 0376d3782a2..bcff2e604ed 100644 --- a/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs @@ -1075,53 +1075,6 @@ public virtual bool CanSetPropertyAccessMode( || navigation.GetPropertyAccessMode() == propertyAccessMode); } - /// - /// 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 InternalForeignKeyBuilder IsEagerLoaded( - bool? eagerLoaded, - bool pointsToPrincipal, - ConfigurationSource configurationSource) - { - var navigation = pointsToPrincipal ? Metadata.DependentToPrincipal : Metadata.PrincipalToDependent; - if (navigation == null) - { - throw new InvalidOperationException( - CoreStrings.NoNavigation( - pointsToPrincipal ? Metadata.DeclaringEntityType.DisplayName() : Metadata.PrincipalEntityType.DisplayName(), - Metadata.Properties.Format())); - } - - if (CanSetIsEagerLoaded(eagerLoaded, pointsToPrincipal, configurationSource)) - { - navigation.SetIsEagerLoaded(eagerLoaded, configurationSource); - - 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 - /// 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 CanSetIsEagerLoaded( - bool? eagerLoaded, - bool pointsToPrincipal, - ConfigurationSource? configurationSource) - { - IConventionNavigation navigation = pointsToPrincipal ? Metadata.DependentToPrincipal : Metadata.PrincipalToDependent; - return navigation != null - && (configurationSource.Overrides(navigation.GetIsEagerLoadedConfigurationSource()) - || navigation.IsEagerLoaded == 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 @@ -2887,10 +2840,10 @@ private InternalForeignKeyBuilder GetOrCreateRelationshipBuilder( foreach (var relationshipWithResolution in resolvableRelationships) { - var resolvableRelationship = relationshipWithResolution.Item1; - var sameConfigurationSource = relationshipWithResolution.Item2; - var resolution = relationshipWithResolution.Item3; - var inverseNavigationRemoved = relationshipWithResolution.Item4; + var resolvableRelationship = relationshipWithResolution.Builder; + var sameConfigurationSource = relationshipWithResolution.SameConfigurationSource; + var resolution = relationshipWithResolution.Resolution; + var inverseNavigationRemoved = relationshipWithResolution.InverseNavigationShouldBeRemoved; if (sameConfigurationSource && configurationSource == ConfigurationSource.Explicit && inverseNavigationRemoved) @@ -4341,29 +4294,6 @@ bool IConventionForeignKeyBuilder.CanSetPropertyAccessMode( pointsToPrincipal, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - [DebuggerStepThrough] - IConventionForeignKeyBuilder IConventionForeignKeyBuilder.IsEagerLoaded( - bool? eagerLoaded, bool pointsToPrincipal, bool fromDataAnnotation) - => IsEagerLoaded( - eagerLoaded, pointsToPrincipal, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - [DebuggerStepThrough] - bool IConventionForeignKeyBuilder.CanSetIsEagerLoaded(bool? eagerLoaded, bool pointsToPrincipal, bool fromDataAnnotation) - => CanSetIsEagerLoaded( - eagerLoaded, pointsToPrincipal, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore/Metadata/Internal/InternalNavigationBuilder.cs b/src/EFCore/Metadata/Internal/InternalNavigationBuilder.cs index 2e20f800ce7..acd13b176aa 100644 --- a/src/EFCore/Metadata/Internal/InternalNavigationBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalNavigationBuilder.cs @@ -32,7 +32,7 @@ public InternalNavigationBuilder([NotNull] Navigation metadata, [NotNull] Intern /// 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 new InternalNavigationBuilder HasField([CanBeNull] string fieldName, ConfigurationSource configurationSource) + public new virtual InternalNavigationBuilder HasField([CanBeNull] string fieldName, ConfigurationSource configurationSource) => (InternalNavigationBuilder)base.HasField(fieldName, configurationSource); /// @@ -41,7 +41,7 @@ public InternalNavigationBuilder([NotNull] Navigation metadata, [NotNull] Intern /// 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 new InternalNavigationBuilder UsePropertyAccessMode( + public new virtual InternalNavigationBuilder UsePropertyAccessMode( PropertyAccessMode? propertyAccessMode, ConfigurationSource configurationSource) => (InternalNavigationBuilder)base.UsePropertyAccessMode(propertyAccessMode, configurationSource); @@ -51,10 +51,12 @@ public InternalNavigationBuilder([NotNull] Navigation metadata, [NotNull] Intern /// 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. /// - IConventionNavigation IConventionNavigationBuilder.Metadata + public virtual bool CanSetIsEagerLoaded(bool? eagerLoaded, ConfigurationSource configurationSource) { - [DebuggerStepThrough] - get => Metadata; + IConventionNavigation conventionNavigation = Metadata; + + return configurationSource.Overrides(conventionNavigation.GetIsEagerLoadedConfigurationSource()) + || conventionNavigation.IsEagerLoaded == eagerLoaded; } /// @@ -63,41 +65,59 @@ IConventionNavigation IConventionNavigationBuilder.Metadata /// 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 IsEagerLoaded(bool? eagerLoaded, ConfigurationSource configurationSource) + { + if (CanSetIsEagerLoaded(eagerLoaded, configurationSource)) + { + Metadata.SetIsEagerLoaded(eagerLoaded, configurationSource); + + return this; + } + + return null; + } + + IConventionNavigation IConventionNavigationBuilder.Metadata + { + [DebuggerStepThrough] + get => Metadata; + } + + /// + [DebuggerStepThrough] bool IConventionNavigationBuilder.CanSetPropertyAccessMode(PropertyAccessMode? propertyAccessMode, bool fromDataAnnotation) => CanSetPropertyAccessMode( propertyAccessMode, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// + /// + [DebuggerStepThrough] IConventionNavigationBuilder IConventionNavigationBuilder.UsePropertyAccessMode( PropertyAccessMode? propertyAccessMode, bool fromDataAnnotation) => UsePropertyAccessMode( propertyAccessMode, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// + /// + [DebuggerStepThrough] bool IConventionNavigationBuilder.CanSetField(string fieldName, bool fromDataAnnotation) => CanSetField( fieldName, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// + /// + [DebuggerStepThrough] IConventionNavigationBuilder IConventionNavigationBuilder.HasField(string fieldName, bool fromDataAnnotation) => HasField( fieldName, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionNavigationBuilder.CanSetIsEagerLoaded(bool? eagerLoaded, bool fromDataAnnotation) + => CanSetIsEagerLoaded(eagerLoaded, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionNavigationBuilder IConventionNavigationBuilder.IsEagerLoaded(bool? eagerLoaded, bool fromDataAnnotation) + => IsEagerLoaded(eagerLoaded, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); } } diff --git a/src/EFCore/Metadata/Internal/InternalPropertyBaseBuilder`.cs b/src/EFCore/Metadata/Internal/InternalPropertyBaseBuilder`.cs index 634a64dd2e9..0940ec2c78f 100644 --- a/src/EFCore/Metadata/Internal/InternalPropertyBaseBuilder`.cs +++ b/src/EFCore/Metadata/Internal/InternalPropertyBaseBuilder`.cs @@ -35,8 +35,7 @@ public InternalPropertyBaseBuilder([NotNull] TPropertyBase metadata, [NotNull] I /// public virtual InternalPropertyBaseBuilder HasField([CanBeNull] string fieldName, ConfigurationSource configurationSource) { - if (Metadata.FieldInfo?.GetSimpleMemberName() == fieldName - || configurationSource.Overrides(Metadata.GetFieldInfoConfigurationSource())) + if (CanSetField(fieldName, configurationSource)) { Metadata.SetField(fieldName, configurationSource); @@ -63,11 +62,12 @@ public virtual bool CanSetField([CanBeNull] string fieldName, ConfigurationSourc var fieldInfo = PropertyBase.GetFieldInfo( fieldName, Metadata.DeclaringType, Metadata.Name, - shouldThrow: false); + shouldThrow: configurationSource == ConfigurationSource.Explicit); + return fieldInfo != null && PropertyBase.IsCompatible( fieldInfo, Metadata.ClrType, Metadata.DeclaringType.ClrType, Metadata.Name, - shouldThrow: false); + shouldThrow: configurationSource == ConfigurationSource.Explicit); } return Metadata.FieldInfo?.GetSimpleMemberName() == fieldName; diff --git a/src/EFCore/Metadata/Internal/InternalSkipNavigationBuilder.cs b/src/EFCore/Metadata/Internal/InternalSkipNavigationBuilder.cs index fccd17f0eaf..64c3051181b 100644 --- a/src/EFCore/Metadata/Internal/InternalSkipNavigationBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalSkipNavigationBuilder.cs @@ -295,6 +295,38 @@ public virtual InternalSkipNavigationBuilder Attach( return newSkipNavigationBuilder; } + /// + /// 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 CanSetIsEagerLoaded(bool? eagerLoaded, ConfigurationSource configurationSource) + { + IConventionSkipNavigation conventionNavigation = Metadata; + + return configurationSource.Overrides(conventionNavigation.GetIsEagerLoadedConfigurationSource()) + || conventionNavigation.IsEagerLoaded == 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 virtual InternalSkipNavigationBuilder IsEagerLoaded(bool? eagerLoaded, ConfigurationSource configurationSource) + { + if (CanSetIsEagerLoaded(eagerLoaded, configurationSource)) + { + Metadata.SetIsEagerLoaded(eagerLoaded, configurationSource); + + return this; + } + + return null; + } + IConventionSkipNavigation IConventionSkipNavigationBuilder.Metadata { [DebuggerStepThrough] get => Metadata; @@ -375,5 +407,15 @@ bool IConventionSkipNavigationBuilder.CanSetInverse( => CanSetInverse( (SkipNavigation)inverse, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionSkipNavigationBuilder.CanSetIsEagerLoaded(bool? eagerLoaded, bool fromDataAnnotation) + => CanSetIsEagerLoaded(eagerLoaded, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionSkipNavigationBuilder IConventionSkipNavigationBuilder.IsEagerLoaded(bool? eagerLoaded, bool fromDataAnnotation) + => IsEagerLoaded(eagerLoaded, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); } } diff --git a/src/EFCore/Metadata/Internal/SkipNavigation.cs b/src/EFCore/Metadata/Internal/SkipNavigation.cs index a37efbea688..b8b505f63a5 100644 --- a/src/EFCore/Metadata/Internal/SkipNavigation.cs +++ b/src/EFCore/Metadata/Internal/SkipNavigation.cs @@ -285,6 +285,15 @@ public virtual SkipNavigation SetInverse([CanBeNull] SkipNavigation inverse, Con public virtual void UpdateInverseConfigurationSource(ConfigurationSource configurationSource) => _inverseConfigurationSource = _inverseConfigurationSource.Max(configurationSource); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual void SetIsEagerLoaded(bool? eagerLoaded, ConfigurationSource configurationSource) + => this.SetOrRemoveAnnotation(CoreAnnotationNames.EagerLoaded, eagerLoaded, configurationSource); + /// /// Runs the conventions when an annotation was set or removed. /// diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs index c396ed2c670..a716b7567e5 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs @@ -1671,7 +1671,7 @@ private string GetParameterName(string prefix) return uniqueName; } - private static void PopulateEagerLoadedNavigations(IncludeTreeNode includeTreeNode) + private void PopulateEagerLoadedNavigations(IncludeTreeNode includeTreeNode) { var entityType = includeTreeNode.EntityType; var outboundNavigations @@ -1682,6 +1682,11 @@ var outboundNavigations .Concat(entityType.GetDerivedSkipNavigations()) .Where(n => n.IsEagerLoaded); + if (_queryCompilationContext.IgnoreEagerLoadedNavigations) + { + outboundNavigations = outboundNavigations.Where(n => n is INavigation navigation && navigation.ForeignKey.IsOwnership); + } + foreach (var navigation in outboundNavigations) { var addedIncludeTreeNode = includeTreeNode.AddNavigation(navigation); diff --git a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs index b8aaad6ef41..a7f76cb0804 100644 --- a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs @@ -166,6 +166,14 @@ private Expression ExtractQueryMetadata(MethodCallExpression methodCallExpressio return visitedExpression; } + if (genericMethodDefinition == EntityFrameworkQueryableExtensions.IgnoreEagerLoadedNavigationsMethodInfo) + { + var visitedExpression = Visit(methodCallExpression.Arguments[0]); + _queryCompilationContext.IgnoreEagerLoadedNavigations = true; + + return visitedExpression; + } + return null; } diff --git a/src/EFCore/Query/QueryCompilationContext.cs b/src/EFCore/Query/QueryCompilationContext.cs index 07098a04fac..f3ba576e24e 100644 --- a/src/EFCore/Query/QueryCompilationContext.cs +++ b/src/EFCore/Query/QueryCompilationContext.cs @@ -119,6 +119,10 @@ public QueryCompilationContext( /// public virtual bool IgnoreQueryFilters { get; internal set; } /// + /// A value indicating whether eager loaded navigations are ignored in this query. + /// + public virtual bool IgnoreEagerLoadedNavigations { get; internal set; } + /// /// The set of tags applied to this query. /// public virtual ISet Tags { get; } = new HashSet(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs index e3dea35c976..f936416b0bb 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs @@ -6391,8 +6391,8 @@ public virtual async Task Max_in_multi_level_nested_subquery() }) .SingleAsync(); - AssertSql( - @"SELECT [t0].[Id], [t1].[Id], [t1].[Id0], [t1].[Id1], [t1].[IsPastTradeDeadline] + AssertSql( + @"SELECT [t0].[Id], [t1].[Id], [t1].[Id0], [t1].[Id1], [t1].[IsPastTradeDeadline] FROM ( SELECT TOP(2) [t].[Id] FROM [Trades] AS [t] @@ -7608,6 +7608,153 @@ public BugContext21355(DbContextOptions options) #endregion + #region Issue21540 + + [ConditionalFact] + public virtual void Can_eager_loaded_navigation_from_model() + { + using (CreateDatabase21540()) + { + using var context = new MyContext21540(_options); + var query = context.Parents.AsNoTracking().ToList(); + + var result = Assert.Single(query); + Assert.NotNull(result.OwnedReference); + Assert.NotNull(result.Reference); + Assert.NotNull(result.Collection); + Assert.Equal(2, result.Collection.Count); + Assert.NotNull(result.SkipOtherSide); + Assert.Single(result.SkipOtherSide); + + AssertSql( + @"SELECT [p].[Id], [p].[OwnedReference_Id], [r].[Id], [r].[ParentId], [c].[Id], [c].[ParentId], [t].[Id], [t].[ParentId], [t].[OtherSideId] +FROM [Parents] AS [p] +LEFT JOIN [Reference21540] AS [r] ON [p].[Id] = [r].[ParentId] +LEFT JOIN [Collection21540] AS [c] ON [p].[Id] = [c].[ParentId] +LEFT JOIN ( + SELECT [o].[Id], [j].[ParentId], [j].[OtherSideId] + FROM [JoinEntity21540] AS [j] + INNER JOIN [OtherSide21540] AS [o] ON [j].[OtherSideId] = [o].[Id] +) AS [t] ON [p].[Id] = [t].[ParentId] +ORDER BY [p].[Id], [r].[Id], [c].[Id], [t].[ParentId], [t].[OtherSideId], [t].[Id]"); + } + } + + [ConditionalFact] + public virtual void Can_ignore_eager_loaded_navigation_from_model() + { + using (CreateDatabase21540()) + { + using var context = new MyContext21540(_options); + var query = context.Parents.AsNoTracking().IgnoreEagerLoadedNavigations().ToList(); + + var result = Assert.Single(query); + Assert.NotNull(result.OwnedReference); + Assert.Null(result.Reference); + Assert.Null(result.Collection); + Assert.Null(result.SkipOtherSide); + + AssertSql( + @"SELECT [p].[Id], [p].[OwnedReference_Id] +FROM [Parents] AS [p]"); + } + } + + private class Parent21540 + { + public int Id { get; set; } + public Reference21540 Reference { get; set; } + public Owned21540 OwnedReference { get; set; } + public List Collection { get; set; } + public List SkipOtherSide { get; set; } + } + + private class JoinEntity21540 + { + public int ParentId { get; set; } + public Parent21540 Parent { get; set; } + public int OtherSideId { get; set; } + public OtherSide21540 OtherSide { get; set; } + } + + private class OtherSide21540 + { + public int Id { get; set; } + public List SkipParent { get; set; } + } + + private class Reference21540 + { + public int Id { get; set; } + public int ParentId { get; set; } + public Parent21540 Parent { get; set; } + } + + private class Owned21540 + { + public int Id { get; set; } + } + + private class Collection21540 + { + public int Id { get; set; } + public int ParentId { get; set; } + public Parent21540 Parent { get; set; } + } + + private class MyContext21540 : DbContext + { + public DbSet Parents { get; set; } + + public MyContext21540(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasMany(e => e.SkipOtherSide).WithMany(e => e.SkipParent) + .UsingEntity( + e => e.HasOne(i => i.OtherSide).WithMany().HasForeignKey(e => e.OtherSideId), + e => e.HasOne(i => i.Parent).WithMany().HasForeignKey(e => e.ParentId)) + .HasKey(e => new { e.ParentId, e.OtherSideId }); + modelBuilder.Entity().OwnsOne(e => e.OwnedReference); + + modelBuilder.Entity().Navigation(e => e.Reference).IsEagerLoaded(); + modelBuilder.Entity().Navigation(e => e.Collection).IsEagerLoaded(); + modelBuilder.Entity().Navigation(e => e.SkipOtherSide).IsEagerLoaded(); + } + } + + private SqlServerTestStore CreateDatabase21540() + => CreateTestStore( + () => new MyContext21540(_options), + context => + { + var joinEntity = new JoinEntity21540 + { + OtherSide = new OtherSide21540(), + Parent = new Parent21540 + { + Reference = new Reference21540(), + OwnedReference = new Owned21540(), + Collection = new List + { + new Collection21540(), + new Collection21540(), + } + } + }; + + context.AddRange(joinEntity); + + context.SaveChanges(); + + ClearLog(); + }); + + #endregion + private DbContextOptions _options; private SqlServerTestStore CreateTestStore( diff --git a/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs b/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs index 60327e5777d..4bb8fe0b1f0 100644 --- a/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs @@ -2406,7 +2406,7 @@ public void Indexes_for_owned_collection_types_are_calculated_correctly() Assert.Equal((1, 1, 1, 1, 1), indexes["Id"]); Assert.Equal((2, -1, 2, -1, -1), indexes[nameof(ChildEntity.Name)]); - Dictionary GetIndexes(IEnumerable properties) + static Dictionary GetIndexes(IEnumerable properties) => properties.ToDictionary( p => p.Name, p => diff --git a/test/EFCore.Tests/Metadata/Internal/InternalNavigationBuilderTest.cs b/test/EFCore.Tests/Metadata/Internal/InternalNavigationBuilderTest.cs index 10d03b38843..7f78b1e3d2a 100644 --- a/test/EFCore.Tests/Metadata/Internal/InternalNavigationBuilderTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/InternalNavigationBuilderTest.cs @@ -83,6 +83,42 @@ public void Can_only_override_lower_or_equal_source_PropertyAccessMode() Assert.Null(metadata.GetPropertyAccessModeConfigurationSource()); } + [ConditionalFact] + public void Can_only_override_lower_or_equal_source_IsEagerLoaded() + { + var builder = CreateInternalNavigationBuilder(); + IConventionNavigation metadata = builder.Metadata; + + Assert.False(metadata.IsEagerLoaded); + Assert.Null(metadata.GetIsEagerLoadedConfigurationSource()); + + Assert.True(builder.CanSetIsEagerLoaded(eagerLoaded: true, ConfigurationSource.DataAnnotation)); + Assert.NotNull(builder.IsEagerLoaded(eagerLoaded: true, ConfigurationSource.DataAnnotation)); + + Assert.True(metadata.IsEagerLoaded); + Assert.Equal(ConfigurationSource.DataAnnotation, metadata.GetIsEagerLoadedConfigurationSource()); + + Assert.True(builder.CanSetIsEagerLoaded(eagerLoaded: true, ConfigurationSource.Convention)); + Assert.False(builder.CanSetIsEagerLoaded(eagerLoaded: false, ConfigurationSource.Convention)); + Assert.NotNull(builder.IsEagerLoaded(eagerLoaded: true, ConfigurationSource.Convention)); + Assert.Null(builder.IsEagerLoaded(eagerLoaded: false, ConfigurationSource.Convention)); + + Assert.True(metadata.IsEagerLoaded); + Assert.Equal(ConfigurationSource.DataAnnotation, metadata.GetIsEagerLoadedConfigurationSource()); + + Assert.True(builder.CanSetIsEagerLoaded(eagerLoaded: false, ConfigurationSource.DataAnnotation)); + Assert.NotNull(builder.IsEagerLoaded(eagerLoaded: false, ConfigurationSource.DataAnnotation)); + + Assert.False(metadata.IsEagerLoaded); + Assert.Equal(ConfigurationSource.DataAnnotation, metadata.GetIsEagerLoadedConfigurationSource()); + + Assert.True(builder.CanSetIsEagerLoaded(null, ConfigurationSource.DataAnnotation)); + Assert.NotNull(builder.IsEagerLoaded(null, ConfigurationSource.DataAnnotation)); + + Assert.False(metadata.IsEagerLoaded); + Assert.Null(metadata.GetIsEagerLoadedConfigurationSource()); + } + private InternalNavigationBuilder CreateInternalNavigationBuilder() { var modelBuilder = (InternalModelBuilder) diff --git a/test/EFCore.Tests/Metadata/Internal/InternalSkipNavigationBuilderTest.cs b/test/EFCore.Tests/Metadata/Internal/InternalSkipNavigationBuilderTest.cs index 9de0e8758c7..ff448e08cb6 100644 --- a/test/EFCore.Tests/Metadata/Internal/InternalSkipNavigationBuilderTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/InternalSkipNavigationBuilderTest.cs @@ -198,6 +198,42 @@ public void Can_only_override_lower_or_equal_source_Inverse() Assert.Null(inverse.GetInverseConfigurationSource()); } + [ConditionalFact] + public void Can_only_override_lower_or_equal_source_IsEagerLoaded() + { + var builder = CreateInternalSkipNavigationBuilder(); + IConventionSkipNavigation metadata = builder.Metadata; + + Assert.False(metadata.IsEagerLoaded); + Assert.Null(metadata.GetIsEagerLoadedConfigurationSource()); + + Assert.True(builder.CanSetIsEagerLoaded(eagerLoaded: true, ConfigurationSource.DataAnnotation)); + Assert.NotNull(builder.IsEagerLoaded(eagerLoaded: true, ConfigurationSource.DataAnnotation)); + + Assert.True(metadata.IsEagerLoaded); + Assert.Equal(ConfigurationSource.DataAnnotation, metadata.GetIsEagerLoadedConfigurationSource()); + + Assert.True(builder.CanSetIsEagerLoaded(eagerLoaded: true, ConfigurationSource.Convention)); + Assert.False(builder.CanSetIsEagerLoaded(eagerLoaded: false, ConfigurationSource.Convention)); + Assert.NotNull(builder.IsEagerLoaded(eagerLoaded: true, ConfigurationSource.Convention)); + Assert.Null(builder.IsEagerLoaded(eagerLoaded: false, ConfigurationSource.Convention)); + + Assert.True(metadata.IsEagerLoaded); + Assert.Equal(ConfigurationSource.DataAnnotation, metadata.GetIsEagerLoadedConfigurationSource()); + + Assert.True(builder.CanSetIsEagerLoaded(eagerLoaded: false, ConfigurationSource.DataAnnotation)); + Assert.NotNull(builder.IsEagerLoaded(eagerLoaded: false, ConfigurationSource.DataAnnotation)); + + Assert.False(metadata.IsEagerLoaded); + Assert.Equal(ConfigurationSource.DataAnnotation, metadata.GetIsEagerLoadedConfigurationSource()); + + Assert.True(builder.CanSetIsEagerLoaded(null, ConfigurationSource.DataAnnotation)); + Assert.NotNull(builder.IsEagerLoaded(null, ConfigurationSource.DataAnnotation)); + + Assert.False(metadata.IsEagerLoaded); + Assert.Null(metadata.GetIsEagerLoadedConfigurationSource()); + } + private InternalSkipNavigationBuilder CreateInternalSkipNavigationBuilder() { var modelBuilder = (InternalModelBuilder)