diff --git a/src/EFCore.Relational/Metadata/Conventions/RelationalQueryFilterRewritingConvention.cs b/src/EFCore.Relational/Metadata/Conventions/RelationalQueryFilterRewritingConvention.cs index f907332c06b..f009ff38bde 100644 --- a/src/EFCore.Relational/Metadata/Conventions/RelationalQueryFilterRewritingConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/RelationalQueryFilterRewritingConvention.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; @@ -34,10 +35,17 @@ public override void ProcessModelFinalizing( { foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) { - var queryFilter = entityType.GetQueryFilter(); - if (queryFilter != null) + var queryFilters = entityType.GetQueryFilters(); + if (queryFilters != null) { - entityType.SetQueryFilter((LambdaExpression)DbSetAccessRewriter.Rewrite(modelBuilder.Metadata, queryFilter)); + var rewritedQueryfilters = new Dictionary(); + foreach (var queryfilter in queryFilters) + { + rewritedQueryfilters.Add(queryfilter.Key, + (LambdaExpression)DbSetAccessRewriter.Rewrite(modelBuilder.Metadata, queryfilter.Value)); + } + + entityType.SetQueryFilters(rewritedQueryfilters); } #pragma warning disable CS0618 // Type or member is obsolete diff --git a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs index 9719040342a..524b48dcdc5 100644 --- a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs +++ b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs @@ -2396,6 +2396,36 @@ source.Provider is EntityQueryProvider : source; } + internal static readonly MethodInfo IgnoreQueryFilterMethodInfo + = typeof(EntityFrameworkQueryableExtensions) + .GetRequiredDeclaredMethod(nameof(IgnoreQueryFilter)); + + /// + /// Specifies that the current Entity Framework LINQ query should not have a specific model-level entity query filters applied. + /// + /// The type of entity being queried. + /// The source query. + /// The name of the query filter that should not be applied. + /// A new query that will not apply the specified model-level entity query filters. + /// is . + public static IQueryable IgnoreQueryFilter( + this IQueryable source, [NotParameterized] string name) + where TEntity : class + { + Check.NotNull(source, nameof(source)); + Check.NotEmpty(name, nameof(name)); + + return + source.Provider is EntityQueryProvider + ? source.Provider.CreateQuery( + Expression.Call( + instance: null, + method: IgnoreQueryFilterMethodInfo.MakeGenericMethod(typeof(TEntity)), + arg0: source.Expression, + arg1: Expression.Constant(name))) + : source; + } + #endregion #region Tracking diff --git a/src/EFCore/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs index 1b3b969c06d..38d3e8e89bd 100644 --- a/src/EFCore/Infrastructure/ModelValidator.cs +++ b/src/EFCore/Infrastructure/ModelValidator.cs @@ -933,13 +933,13 @@ protected virtual void ValidateQueryFilters( foreach (var entityType in model.GetEntityTypes()) { - if (entityType.GetQueryFilter() != null) + if (entityType.GetQueryFilters() != null) { if (entityType.BaseType != null) { throw new InvalidOperationException( CoreStrings.BadFilterDerivedType( - entityType.GetQueryFilter(), + entityType.GetQueryFilters(), entityType.DisplayName(), entityType.GetRootType().DisplayName())); } @@ -947,7 +947,7 @@ protected virtual void ValidateQueryFilters( if (entityType.IsOwned()) { throw new InvalidOperationException( - CoreStrings.BadFilterOwnedType(entityType.GetQueryFilter(), entityType.DisplayName())); + CoreStrings.BadFilterOwnedType(entityType.GetQueryFilters(), entityType.DisplayName())); } } @@ -956,8 +956,8 @@ protected virtual void ValidateQueryFilters( n => !n.IsCollection && n.ForeignKey.IsRequired && n.IsOnDependent - && n.ForeignKey.PrincipalEntityType.GetQueryFilter() != null - && n.ForeignKey.DeclaringEntityType.GetQueryFilter() == null).FirstOrDefault(); + && n.ForeignKey.PrincipalEntityType.GetQueryFilters() != null + && n.ForeignKey.DeclaringEntityType.GetQueryFilters() == null).FirstOrDefault(); if (requiredNavigationWithQueryFilter != null) { diff --git a/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs b/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs index 6e730e19cdd..1a65a91e372 100644 --- a/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs @@ -247,8 +247,18 @@ public virtual EntityTypeBuilder Ignore(string propertyName) /// The LINQ predicate expression. /// The same builder instance so that multiple configuration calls can be chained. public virtual EntityTypeBuilder HasQueryFilter(LambdaExpression? filter) + => HasQueryFilter("", filter); + + /// + /// Specifies a named LINQ predicate expression that will automatically be applied to any queries targeting + /// this entity type. + /// + /// The name of the LINQ predicate expression. + /// The LINQ predicate expression. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual EntityTypeBuilder HasQueryFilter(string name, LambdaExpression? filter) { - Builder.HasQueryFilter(filter, ConfigurationSource.Explicit); + Builder.HasQueryFilter(name, filter, ConfigurationSource.Explicit); return this; } diff --git a/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs b/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs index 96b847bfa18..6b37a6ab555 100644 --- a/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs +++ b/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs @@ -218,7 +218,7 @@ public virtual EntityTypeBuilder Ignore(Expression The LINQ predicate expression. /// The same builder instance so that multiple configuration calls can be chained. public new virtual EntityTypeBuilder HasQueryFilter(LambdaExpression? filter) - => (EntityTypeBuilder)base.HasQueryFilter(filter); + => (EntityTypeBuilder)base.HasQueryFilter("", filter); /// /// Specifies a LINQ predicate expression that will automatically be applied to any queries targeting @@ -227,7 +227,27 @@ public virtual EntityTypeBuilder Ignore(Expression The LINQ predicate expression. /// The same builder instance so that multiple configuration calls can be chained. public virtual EntityTypeBuilder HasQueryFilter(Expression>? filter) - => (EntityTypeBuilder)base.HasQueryFilter(filter); + => (EntityTypeBuilder)base.HasQueryFilter("", filter); + + /// + /// Specifies a named LINQ predicate expression that will automatically be applied to any queries targeting + /// this entity type. + /// + /// The name of the LINQ predicate expression. + /// The LINQ predicate expression. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual EntityTypeBuilder HasQueryFilter(string name, LambdaExpression? filter) + => (EntityTypeBuilder)base.HasQueryFilter(name, filter); + + /// + /// Specifies a named LINQ predicate expression that will automatically be applied to any queries targeting + /// this entity type. + /// + /// The name of the LINQ predicate expression. + /// The LINQ predicate expression. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual EntityTypeBuilder HasQueryFilter(string name, Expression>? filter) + => (EntityTypeBuilder)base.HasQueryFilter(name, filter); /// /// Configures a query used to provide data for a keyless entity type. diff --git a/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs b/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs index 004a6390953..0cf11274184 100644 --- a/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs +++ b/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs @@ -246,10 +246,16 @@ protected virtual void ProcessEntityTypeAnnotations( annotations.Remove(CoreAnnotationNames.NavigationAccessMode); annotations.Remove(CoreAnnotationNames.DiscriminatorProperty); - if (annotations.TryGetValue(CoreAnnotationNames.QueryFilter, out var queryFilter)) + if (annotations.TryGetValue(CoreAnnotationNames.QueryFilter, out var queryFilters)) { - annotations[CoreAnnotationNames.QueryFilter] = - new QueryRootRewritingExpressionVisitor(runtimeEntityType.Model).Rewrite((Expression)queryFilter!); + var result = new Dictionary(); + foreach(var queryFilter in (Dictionary)queryFilters!) + { + result.Add(queryFilter.Key, + (LambdaExpression)new QueryRootRewritingExpressionVisitor(runtimeEntityType.Model).Rewrite(queryFilter.Value!)); + + } + annotations[CoreAnnotationNames.QueryFilter] = result; } #pragma warning disable CS0612 // Type or member is obsolete diff --git a/src/EFCore/Metadata/IConventionEntityType.cs b/src/EFCore/Metadata/IConventionEntityType.cs index f8ceed81b8e..d4ebb31c4b6 100644 --- a/src/EFCore/Metadata/IConventionEntityType.cs +++ b/src/EFCore/Metadata/IConventionEntityType.cs @@ -83,11 +83,34 @@ public interface IConventionEntityType : IReadOnlyEntityType, IConventionTypeBas LambdaExpression? SetQueryFilter(LambdaExpression? queryFilter, bool fromDataAnnotation = false); /// - /// Returns the configuration source for . + /// Sets a named LINQ expression filter automatically applied to queries for this entity type. /// - /// The configuration source for . + /// The name of the expression filter. + /// The LINQ expression filter. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured filter. + LambdaExpression? SetQueryFilter(string name, LambdaExpression? queryFilter, bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . ConfigurationSource? GetQueryFilterConfigurationSource(); + /// + /// Sets the LINQ expression filters automatically applied to queries for this entity type. + /// + /// The LINQ expression filters. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured filters. + Dictionary? SetQueryFilters(Dictionary? queryFilters, bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetQueryFiltersConfigurationSource(); + /// /// Returns the property that will be used for storing a discriminator value. /// diff --git a/src/EFCore/Metadata/IReadOnlyEntityType.cs b/src/EFCore/Metadata/IReadOnlyEntityType.cs index da0ea7064ca..8969976d533 100644 --- a/src/EFCore/Metadata/IReadOnlyEntityType.cs +++ b/src/EFCore/Metadata/IReadOnlyEntityType.cs @@ -68,6 +68,19 @@ public interface IReadOnlyEntityType : IReadOnlyTypeBase /// The LINQ expression filter. LambdaExpression? GetQueryFilter(); + /// + /// Gets a named LINQ expression filter automatically applied to queries for this entity type. + /// + /// The name of the LINQ expression. + /// The LINQ expression filter. + LambdaExpression? GetQueryFilter(string name); + + /// + /// Gets the LINQ expression filters automatically applied to queries for this entity type. + /// + /// The LINQ expression filters. + IDictionary? GetQueryFilters(); + /// /// Returns the property that will be used for storing a discriminator value. /// diff --git a/src/EFCore/Metadata/Internal/EntityType.cs b/src/EFCore/Metadata/Internal/EntityType.cs index 2fee460530b..ed46ec17d9a 100644 --- a/src/EFCore/Metadata/Internal/EntityType.cs +++ b/src/EFCore/Metadata/Internal/EntityType.cs @@ -3180,6 +3180,15 @@ public virtual ChangeTrackingStrategy GetChangeTrackingStrategy() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual LambdaExpression? SetQueryFilter(LambdaExpression? queryFilter, ConfigurationSource configurationSource) + => SetQueryFilter("", queryFilter, 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 LambdaExpression? SetQueryFilter(string name, LambdaExpression? queryFilter, ConfigurationSource configurationSource) { var errorMessage = CheckQueryFilter(queryFilter); if (errorMessage != null) @@ -3187,7 +3196,52 @@ public virtual ChangeTrackingStrategy GetChangeTrackingStrategy() throw new InvalidOperationException(errorMessage); } - return (LambdaExpression?)SetOrRemoveAnnotation(CoreAnnotationNames.QueryFilter, queryFilter, configurationSource)?.Value; + var queryFilters = (Dictionary?)FindAnnotation(CoreAnnotationNames.QueryFilter)?.Value + ?? new(); + + if(queryFilter == null && queryFilters.ContainsKey(name)) + { + queryFilters.Remove(name); + } + else if (queryFilter != null && !queryFilters.ContainsKey(name)) + { + queryFilters.Add(name, queryFilter); + } + else + { + queryFilters[name] = queryFilter!; + } + + if(queryFilters.Count == 0) + { + queryFilters = null; + } + + SetOrRemoveAnnotation(CoreAnnotationNames.QueryFilter, queryFilters, configurationSource); + return GetQueryFilter(name); + } + + /// + /// 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 Dictionary? SetQueryFilters(Dictionary? queryFilters, ConfigurationSource configurationSource) + { + if (queryFilters != null) + { + foreach (var queryFilter in queryFilters) + { + var errorMessage = CheckQueryFilter(queryFilter.Value); + if (errorMessage != null) + { + throw new InvalidOperationException(errorMessage); + } + } + } + + return (Dictionary?)SetOrRemoveAnnotation(CoreAnnotationNames.QueryFilter, queryFilters, configurationSource)?.Value; } /// @@ -3216,7 +3270,25 @@ public virtual ChangeTrackingStrategy GetChangeTrackingStrategy() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual LambdaExpression? GetQueryFilter() - => (LambdaExpression?)this[CoreAnnotationNames.QueryFilter]; + => GetQueryFilter(""); + + /// + /// 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 LambdaExpression? GetQueryFilter(string name) + { + var queryFilters = (Dictionary?)this[CoreAnnotationNames.QueryFilter]; + + if(queryFilters != null && name != null && queryFilters.ContainsKey(name)) + { + return queryFilters[name]; + } + + return null; + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -3227,6 +3299,24 @@ public virtual ChangeTrackingStrategy GetChangeTrackingStrategy() public virtual ConfigurationSource? GetQueryFilterConfigurationSource() => FindAnnotation(CoreAnnotationNames.QueryFilter)?.GetConfigurationSource(); + /// + /// 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 IDictionary? GetQueryFilters() + => (Dictionary?)this[CoreAnnotationNames.QueryFilter]; + + /// + /// 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 ConfigurationSource? GetQueryFiltersConfigurationSource() + => FindAnnotation(CoreAnnotationNames.QueryFilter)?.GetConfigurationSource(); + /// /// 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 @@ -3660,6 +3750,26 @@ void IMutableEntityType.SetQueryFilter(LambdaExpression? queryFilter) LambdaExpression? IConventionEntityType.SetQueryFilter(LambdaExpression? queryFilter, bool fromDataAnnotation) => SetQueryFilter(queryFilter, 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] + LambdaExpression? IConventionEntityType.SetQueryFilter(string name, LambdaExpression? queryFilter, bool fromDataAnnotation) + => SetQueryFilter(name, queryFilter, 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] + Dictionary? IConventionEntityType.SetQueryFilters(Dictionary? queryFilters, bool fromDataAnnotation) + => SetQueryFilters(queryFilters, 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/InternalEntityTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs index 94591ffcb61..d5205f4ce46 100644 --- a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs @@ -1343,12 +1343,13 @@ private bool CanIgnore(string name, ConfigurationSource configurationSource, boo /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual InternalEntityTypeBuilder? HasQueryFilter( + string name, LambdaExpression? filter, ConfigurationSource configurationSource) { - if (CanSetQueryFilter(filter, configurationSource)) + if (CanSetQueryFilter(name, filter, configurationSource)) { - Metadata.SetQueryFilter(filter, configurationSource); + Metadata.SetQueryFilter(name, filter, configurationSource); return this; } @@ -1362,9 +1363,9 @@ private bool CanIgnore(string name, ConfigurationSource configurationSource, boo /// 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 CanSetQueryFilter(LambdaExpression? filter, ConfigurationSource configurationSource) + public virtual bool CanSetQueryFilter(string name, LambdaExpression? filter, ConfigurationSource configurationSource) => configurationSource.Overrides(Metadata.GetQueryFilterConfigurationSource()) - || Metadata.GetQueryFilter() == filter; + || Metadata.GetQueryFilter(name) == filter; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -5205,7 +5206,7 @@ bool IConventionEntityTypeBuilder.CanRemoveSkipNavigation(IReadOnlySkipNavigatio /// [DebuggerStepThrough] IConventionEntityTypeBuilder? IConventionEntityTypeBuilder.HasQueryFilter(LambdaExpression? filter, bool fromDataAnnotation) - => HasQueryFilter(filter, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + => HasQueryFilter("", filter, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -5215,7 +5216,7 @@ bool IConventionEntityTypeBuilder.CanRemoveSkipNavigation(IReadOnlySkipNavigatio /// [DebuggerStepThrough] bool IConventionEntityTypeBuilder.CanSetQueryFilter(LambdaExpression? filter, bool fromDataAnnotation) - => CanSetQueryFilter(filter, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + => CanSetQueryFilter("", filter, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/Metadata/RuntimeEntityType.cs b/src/EFCore/Metadata/RuntimeEntityType.cs index 6fb93c7dac8..0557974911c 100644 --- a/src/EFCore/Metadata/RuntimeEntityType.cs +++ b/src/EFCore/Metadata/RuntimeEntityType.cs @@ -817,7 +817,29 @@ ChangeTrackingStrategy IReadOnlyEntityType.GetChangeTrackingStrategy() /// [DebuggerStepThrough] LambdaExpression? IReadOnlyEntityType.GetQueryFilter() - => (LambdaExpression?)this[CoreAnnotationNames.QueryFilter]; + => this.GetQueryFilter(""); + + /// + [DebuggerStepThrough] + LambdaExpression? IReadOnlyEntityType.GetQueryFilter(string name) + => this.GetQueryFilter(name); + + private LambdaExpression? GetQueryFilter(string name) + { + var queryFilters = (Dictionary?)this[CoreAnnotationNames.QueryFilter]; + + if (queryFilters != null && name != null && queryFilters.ContainsKey(name)) + { + return queryFilters[name]; + } + + return null; + } + + /// + [DebuggerStepThrough] + IDictionary? IReadOnlyEntityType.GetQueryFilters() + => (Dictionary?)this[CoreAnnotationNames.QueryFilter]; /// [DebuggerStepThrough] diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index e56ce6d0e83..307171ce0e3 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -155,7 +155,7 @@ public static string BadDependencyRegistration(object? dependenciesType) dependenciesType); /// - /// The filter expression '{filter}' cannot be specified for entity type '{entityType}'. A filter may only be applied to the root entity type '{rootType}'. + /// Filter expressions cannot be specified for entity type '{entityType}'. Filter may only be applied to the root entity type '{rootType}'. /// public static string BadFilterDerivedType(object? filter, object? entityType, object? rootType) => string.Format( @@ -171,7 +171,7 @@ public static string BadFilterExpression(object? filter, object? entityType, obj filter, entityType, clrType); /// - /// The filter expression '{filter}' cannot be specified for owned entity type '{entityType}'. A filter may only be applied to an entity type that is not owned. + /// Filter expressions cannot be specified for owned entity type '{entityType}'. Filter may only be applied to an entity type that is not owned. /// public static string BadFilterOwnedType(object? filter, object? entityType) => string.Format( diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 2023e55ea9e..9b2427b2b06 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -166,13 +166,13 @@ The service dependencies type '{dependenciesType}' has been registered incorrectly in the service collection. Service dependencies types must only be registered by Entity Framework or database providers. - The filter expression '{filter}' cannot be specified for entity type '{entityType}'. A filter may only be applied to the root entity type '{rootType}'. + Filter expressions cannot be specified for entity type '{entityType}'. Filter may only be applied to the root entity type '{rootType}'. The filter expression '{filter}' specified for entity type '{entityType}' is invalid. The expression must accept a single parameter of type '{clrType}' and return bool. - The filter expression '{filter}' cannot be specified for owned entity type '{entityType}'. A filter may only be applied to an entity type that is not owned. + Filter expressions cannot be specified for owned entity type '{entityType}'. Filters may only be applied to an entity type that is not owned. The type '{givenType}' cannot be used as a value comparer because it does not inherit from '{expectedType}'. Make sure to inherit value comparers from '{expectedType}'. diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs index a31267774de..430300079d3 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs @@ -60,7 +60,7 @@ private static readonly PropertyInfo _queryContextContextPropertyInfo private readonly ParameterExtractingExpressionVisitor _parameterExtractingExpressionVisitor; private readonly HashSet _nonCyclicAutoIncludeEntityTypes; - private readonly Dictionary _parameterizedQueryFilterPredicateCache + private readonly Dictionary> _parameterizedQueryFiltersPredicateCache = new(); private readonly Parameters _parameters = new(); @@ -1411,44 +1411,58 @@ private Expression ApplyQueryFilter(IEntityType entityType, NavigationExpansionE { var sequenceType = navigationExpansionExpression.Type.GetSequenceType(); var rootEntityType = entityType.GetRootType(); - var queryFilter = rootEntityType.GetQueryFilter(); - if (queryFilter != null) + var queryFilters = rootEntityType.GetQueryFilters(); + if (queryFilters != null) { - if (!_parameterizedQueryFilterPredicateCache.TryGetValue(rootEntityType, out var filterPredicate)) + if (!_parameterizedQueryFiltersPredicateCache.TryGetValue(rootEntityType, out var entityTypeQueryFilterCache)) { - filterPredicate = queryFilter; - filterPredicate = (LambdaExpression)_parameterExtractingExpressionVisitor.ExtractParameters(filterPredicate); - filterPredicate = (LambdaExpression)_queryTranslationPreprocessor.NormalizeQueryableMethod(filterPredicate); - - // We need to do entity equality, but that requires a full method call on a query root to properly flow the - // entity information through. Construct a MethodCall wrapper for the predicate with the proper query root. - var filterWrapper = Expression.Call( - QueryableMethods.Where.MakeGenericMethod(rootEntityType.ClrType), - new QueryRootExpression(rootEntityType), - filterPredicate); - filterPredicate = filterWrapper.Arguments[1].UnwrapLambdaFromQuote(); - - _parameterizedQueryFilterPredicateCache[rootEntityType] = filterPredicate; - } + entityTypeQueryFilterCache = new(); + + foreach (var queryFilter in queryFilters) + { + var filterPredicate = queryFilter.Value; + filterPredicate = (LambdaExpression)_parameterExtractingExpressionVisitor.ExtractParameters(filterPredicate); + filterPredicate = (LambdaExpression)_queryTranslationPreprocessor.NormalizeQueryableMethod(filterPredicate); + + // We need to do entity equality, but that requires a full method call on a query root to properly flow the + // entity information through. Construct a MethodCall wrapper for the predicate with the proper query root. + var filterWrapper = Expression.Call( + QueryableMethods.Where.MakeGenericMethod(rootEntityType.ClrType), + new QueryRootExpression(rootEntityType), + filterPredicate); + filterPredicate = filterWrapper.Arguments[1].UnwrapLambdaFromQuote(); + + entityTypeQueryFilterCache.Add(queryFilter.Key, filterPredicate); + } - filterPredicate = - (LambdaExpression)new SelfReferenceEntityQueryableRewritingExpressionVisitor(this, entityType).Visit( - filterPredicate); + _parameterizedQueryFiltersPredicateCache[rootEntityType] = entityTypeQueryFilterCache; + } - // if we are constructing EntityQueryable of a derived type, we need to re-map filter predicate to the correct derived type - var filterPredicateParameter = filterPredicate.Parameters[0]; - if (filterPredicateParameter.Type != sequenceType) + Expression filteredResult = navigationExpansionExpression; + foreach (var queryFilter in entityTypeQueryFilterCache) { - var newFilterPredicateParameter = Expression.Parameter(sequenceType, filterPredicateParameter.Name); - var newFilterPredicateBody = ReplacingExpressionVisitor.Replace( - filterPredicateParameter, newFilterPredicateParameter, filterPredicate.Body); - filterPredicate = Expression.Lambda(newFilterPredicateBody, newFilterPredicateParameter); - } + if (!_queryCompilationContext.IgnoredQueryFilters.Contains(queryFilter.Key)) + { + var filterPredicate = + (LambdaExpression)new SelfReferenceEntityQueryableRewritingExpressionVisitor(this, entityType).Visit( + queryFilter.Value); + + // if we are constructing EntityQueryable of a derived type, we need to re-map filter predicate to the correct derived type + var filterPredicateParameter = filterPredicate.Parameters[0]; + if (filterPredicateParameter.Type != sequenceType) + { + var newFilterPredicateParameter = Expression.Parameter(sequenceType, filterPredicateParameter.Name); + var newFilterPredicateBody = ReplacingExpressionVisitor.Replace( + filterPredicateParameter, newFilterPredicateParameter, filterPredicate.Body); + filterPredicate = Expression.Lambda(newFilterPredicateBody, newFilterPredicateParameter); + } - var filteredResult = Expression.Call( - QueryableMethods.Where.MakeGenericMethod(sequenceType), - navigationExpansionExpression, - filterPredicate); + filteredResult = Expression.Call( + QueryableMethods.Where.MakeGenericMethod(sequenceType), + filteredResult, + filterPredicate); + } + } return Visit(filteredResult); } diff --git a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs index 2e3831d7126..486dc0d2825 100644 --- a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs @@ -211,6 +211,14 @@ private void VerifyReturnType(Expression expression, ParameterExpression lambdaP return visitedExpression; } + if (genericMethodDefinition == EntityFrameworkQueryableExtensions.IgnoreQueryFilterMethodInfo) + { + var visitedExpression = Visit(methodCallExpression.Arguments[0]); + _queryCompilationContext.AddIgnoredQueryFilters(methodCallExpression.Arguments[1].GetConstantValue()); + + return visitedExpression; + } + if (genericMethodDefinition == EntityFrameworkQueryableExtensions.IgnoreQueryFiltersMethodInfo) { var visitedExpression = Visit(methodCallExpression.Arguments[0]); diff --git a/src/EFCore/Query/QueryCompilationContext.cs b/src/EFCore/Query/QueryCompilationContext.cs index c12eacf4bc5..c34e8453073 100644 --- a/src/EFCore/Query/QueryCompilationContext.cs +++ b/src/EFCore/Query/QueryCompilationContext.cs @@ -136,6 +136,11 @@ public virtual bool IsTracking /// public virtual bool IgnoreQueryFilters { get; internal set; } + /// + /// The set of named query filters that are ignored in this query. + /// + public virtual ISet IgnoredQueryFilters { get; } = new HashSet(); + /// /// A value indicating whether eager loaded navigations are ignored in this query. /// @@ -167,6 +172,17 @@ public virtual void AddTag(string tag) Tags.Add(tag); } + /// + /// Adds a query filter the should be ignored . + /// + /// The name of the query filter to ignore. + public virtual void AddIgnoredQueryFilters(string name) + { + Check.NotEmpty(name, nameof(name)); + + IgnoredQueryFilters.Add(name); + } + /// /// Creates the query executor func which gives results for this query. /// diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs index d1ef01dfc86..cb70476420c 100644 --- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs @@ -568,6 +568,14 @@ public LambdaExpression GetQueryFilter() { throw new NotImplementedException(); } + public LambdaExpression GetQueryFilter(string name) + { + throw new NotImplementedException(); + } + public IDictionary GetQueryFilters() + { + throw new NotImplementedException(); + } public IEnumerable GetReferencingForeignKeys() => throw new NotImplementedException(); diff --git a/test/EFCore.Specification.Tests/Query/NorthwindQueryFiltersQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindQueryFiltersQueryTestBase.cs index 013940786a2..d2673b6123f 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindQueryFiltersQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindQueryFiltersQueryTestBase.cs @@ -163,6 +163,31 @@ public virtual Task Include_query_opt_out(bool async) entryCount: 921); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Include_named_query_opt_out(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .IgnoreQueryFilter("HasOrder") + .IgnoreQueryFilter("Quantity"), + entryCount: 2155); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Include_single_named_query_opt_out(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .IgnoreQueryFilter("HasOrder"), + ss => ss.Set() + .Where(od => od.Quantity > 50), + entryCount: 159); + } + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Included_many_to_one_query2(bool async) diff --git a/test/EFCore.Specification.Tests/TestModels/Northwind/NorthwindContext.cs b/test/EFCore.Specification.Tests/TestModels/Northwind/NorthwindContext.cs index 5bff4b970ac..b705f0a0e18 100644 --- a/test/EFCore.Specification.Tests/TestModels/Northwind/NorthwindContext.cs +++ b/test/EFCore.Specification.Tests/TestModels/Northwind/NorthwindContext.cs @@ -92,7 +92,8 @@ public void ConfigureFilters(ModelBuilder modelBuilder) modelBuilder.Entity().HasQueryFilter(c => c.CompanyName.StartsWith(TenantPrefix)); modelBuilder.Entity().HasQueryFilter(o => o.Customer != null && o.Customer.CompanyName != null); - modelBuilder.Entity().HasQueryFilter(od => od.Order != null && EF.Property(od, "Quantity") > _quantity); + modelBuilder.Entity().HasQueryFilter("HasOrder", od => od.Order != null); + modelBuilder.Entity().HasQueryFilter("Quantity", od => EF.Property(od, "Quantity") > _quantity); modelBuilder.Entity().HasQueryFilter(e => e.Address.StartsWith("A")); modelBuilder.Entity().HasQueryFilter(p => ClientMethod(p)); modelBuilder.Entity().HasQueryFilter(cq => cq.CompanyName.StartsWith(SearchTerm)); diff --git a/test/EFCore.Specification.Tests/TestModels/QueryFilterFuncletizationContext.cs b/test/EFCore.Specification.Tests/TestModels/QueryFilterFuncletizationContext.cs index 72413836e1d..efbc618e989 100644 --- a/test/EFCore.Specification.Tests/TestModels/QueryFilterFuncletizationContext.cs +++ b/test/EFCore.Specification.Tests/TestModels/QueryFilterFuncletizationContext.cs @@ -46,7 +46,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().HasQueryFilter(e => e.Tenant == GetFlag().GetId()); modelBuilder.Entity().HasQueryFilter(x => x.IsEnabled == Property && (Tenant + GetId() > 0)); modelBuilder.Entity() - .HasQueryFilter(x => !x.IsDeleted && (IsModerated == null || IsModerated == x.IsModerated)); + .HasQueryFilter("SoftDelete", x => !x.IsDeleted); + modelBuilder.Entity() + .HasQueryFilter(x => IsModerated == null || IsModerated == x.IsModerated); modelBuilder.Entity() .HasQueryFilter(p => Dependents.Any(d => d.PrincipalSetFilterId == p.Id)); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindQueryFiltersQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindQueryFiltersQuerySqlServerTest.cs index 044d87f8f37..b59b5dccb72 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindQueryFiltersQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindQueryFiltersQuerySqlServerTest.cs @@ -146,12 +146,34 @@ FROM [Customers] AS [c] ORDER BY [c].[CustomerID], [o].[OrderID]"); } + public override async Task Include_named_query_opt_out(bool async) + { + await base.Include_named_query_opt_out(async); + + AssertSql( + @"SELECT [o].[OrderID], [o].[ProductID], [o].[Discount], [o].[Quantity], [o].[UnitPrice] +FROM [Order Details] AS [o]"); + } + + public override async Task Include_single_named_query_opt_out(bool async) + { + await base.Include_single_named_query_opt_out(async); + + AssertSql( + @"@__ef_filter___quantity_0='50' + +SELECT [o].[OrderID], [o].[ProductID], [o].[Discount], [o].[Quantity], [o].[UnitPrice] +FROM [Order Details] AS [o] +WHERE [o].[Quantity] > @__ef_filter___quantity_0"); + } + + public override async Task Included_many_to_one_query(bool async) { await base.Included_many_to_one_query(async); AssertSql( - @"@__ef_filter__TenantPrefix_0='B' (Size = 4000) + @"@__ef_filter__TenantPrefix_0 ='B' (Size = 4000) SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region] FROM [Orders] AS [o]