Skip to content

Named query filters #36028

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -893,7 +893,7 @@ private void Create(IEntityType entityType, CSharpRuntimeAnnotationCodeGenerator
entityType.ShortName(), "Customize()", parameters.ClassName));
}

if (entityType.GetQueryFilter() != null)
if (entityType.GetDeclaredQueryFilters().Count > 0)
{
throw new InvalidOperationException(DesignStrings.CompiledModelQueryFilter(entityType.ShortName()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,6 @@ public RelationalQueryFilterRewritingConvention(
/// </summary>
protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; }

/// <inheritdoc />
public override void ProcessModelFinalizing(
IConventionModelBuilder modelBuilder,
IConventionContext<IConventionModelBuilder> context)
{
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes())
{
var queryFilter = entityType.GetQueryFilter();
if (queryFilter != null)
{
entityType.SetQueryFilter((LambdaExpression)DbSetAccessRewriter.Rewrite(modelBuilder.Metadata, queryFilter));
}
}
}

/// <inheritdoc />
protected class RelationalDbSetAccessRewritingExpressionVisitor : DbSetAccessRewritingExpressionVisitor
{
Expand Down
31 changes: 30 additions & 1 deletion src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2670,7 +2670,14 @@ public static IQueryable<TEntity> IgnoreAutoIncludes<TEntity>(
#region Query Filters

internal static readonly MethodInfo IgnoreQueryFiltersMethodInfo
= typeof(EntityFrameworkQueryableExtensions).GetTypeInfo().GetDeclaredMethod(nameof(IgnoreQueryFilters))!;
= typeof(EntityFrameworkQueryableExtensions).GetTypeInfo().GetDeclaredMethods(nameof(IgnoreQueryFilters))
.Where(info => info.GetParameters().Length == 1)
.First();

internal static readonly MethodInfo IgnoreNamedQueryFiltersMethodInfo
= typeof(EntityFrameworkQueryableExtensions).GetTypeInfo().GetDeclaredMethods(nameof(IgnoreQueryFilters))
.Where(info => info.GetParameters().Length == 2)
.First();

/// <summary>
/// Specifies that the current Entity Framework LINQ query should not have any model-level entity query filters applied.
Expand All @@ -2693,6 +2700,28 @@ public static IQueryable<TEntity> IgnoreQueryFilters<TEntity>(
arguments: source.Expression))
: source;

/// <summary>
/// Specifies that the current Entity Framework LINQ query should not have any model-level entity query filters applied.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-query-filters">EF Core query filters</see> for more information and examples.
/// </remarks>
/// <typeparam name="TEntity">The type of entity being queried.</typeparam>
/// <param name="source">The source query.</param>
/// <param name="filterKeys">The filter keys.</param>
/// <returns>A new query that will not apply any model-level entity query filters.</returns>
/// <exception cref="ArgumentNullException"><paramref name="source" /> is <see langword="null" />.</exception>
public static IQueryable<TEntity> IgnoreQueryFilters<TEntity>(
this IQueryable<TEntity> source, [NotParameterized] IReadOnlyCollection<string> filterKeys)
where TEntity : class
=> source.Provider is EntityQueryProvider
? source.Provider.CreateQuery<TEntity>(
Expression.Call(
instance: null,
method: IgnoreNamedQueryFiltersMethodInfo.MakeGenericMethod(typeof(TEntity)),
arguments: [source.Expression, Expression.Constant(filterKeys)]))
: source;

#endregion

#region Tracking
Expand Down
11 changes: 6 additions & 5 deletions src/EFCore/Infrastructure/ModelValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1151,21 +1151,22 @@ protected virtual void ValidateQueryFilters(
{
foreach (var entityType in model.GetEntityTypes())
{
if (entityType.GetQueryFilter() != null)
var queryFilters = entityType.GetDeclaredQueryFilters();
if (queryFilters.Count > 0)
{
if (entityType.BaseType != null)
{
throw new InvalidOperationException(
CoreStrings.BadFilterDerivedType(
entityType.GetQueryFilter(),
queryFilters.First().Expression,
entityType.DisplayName(),
entityType.GetRootType().DisplayName()));
}

if (entityType.IsOwned())
{
throw new InvalidOperationException(
CoreStrings.BadFilterOwnedType(entityType.GetQueryFilter(), entityType.DisplayName()));
CoreStrings.BadFilterOwnedType(queryFilters.First().Expression, entityType.DisplayName()));
}
}

Expand All @@ -1177,8 +1178,8 @@ protected virtual void ValidateQueryFilters(
.GetNavigations()
.FirstOrDefault(
n => n is { IsCollection: false, ForeignKey.IsRequired: true, IsOnDependent: true }
&& n.ForeignKey.PrincipalEntityType.GetRootType().GetQueryFilter() != null
&& n.ForeignKey.DeclaringEntityType.GetRootType().GetQueryFilter() == null);
&& n.ForeignKey.PrincipalEntityType.GetRootType().GetDeclaredQueryFilters().Count > 0
&& n.ForeignKey.DeclaringEntityType.GetRootType().GetDeclaredQueryFilters().Count == 0);

if (requiredNavigationWithQueryFilter != null)
{
Expand Down
14 changes: 14 additions & 0 deletions src/EFCore/Metadata/Builders/EntityTypeBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,20 @@ public virtual EntityTypeBuilder HasQueryFilter(LambdaExpression? filter)
return this;
}

/// <summary>
/// Specifies a LINQ predicate expression that will automatically be applied to any queries targeting
/// this entity type.
/// </summary>
/// <param name="filterKey">The filter key</param>
/// <param name="filter">The LINQ predicate expression.</param>
/// <returns>The same builder instance so that multiple configuration calls can be chained.</returns>
public virtual EntityTypeBuilder HasQueryFilter(string filterKey,LambdaExpression? filter)
{
Builder.HasQueryFilter(new QueryFilter(filterKey, filter));

return this;
}

/// <summary>
/// Configures an unnamed index on the specified properties.
/// If there is an existing unnamed index on the given
Expand Down
20 changes: 20 additions & 0 deletions src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,16 @@ public virtual EntityTypeBuilder<TEntity> Ignore(Expression<Func<TEntity, object
public new virtual EntityTypeBuilder<TEntity> HasQueryFilter(LambdaExpression? filter)
=> (EntityTypeBuilder<TEntity>)base.HasQueryFilter(filter);

/// <summary>
/// Specifies a LINQ predicate expression that will automatically be applied to any queries targeting
/// this entity type.
/// </summary>
/// <param name="filterKey">The filter key.</param>
/// <param name="filter">The LINQ predicate expression.</param>
/// <returns>The same builder instance so that multiple configuration calls can be chained.</returns>
public new virtual EntityTypeBuilder<TEntity> HasQueryFilter(string filterKey, LambdaExpression? filter)
=> (EntityTypeBuilder<TEntity>)base.HasQueryFilter(filterKey, filter);

/// <summary>
/// Specifies a LINQ predicate expression that will automatically be applied to any queries targeting
/// this entity type.
Expand All @@ -620,6 +630,16 @@ public virtual EntityTypeBuilder<TEntity> Ignore(Expression<Func<TEntity, object
public virtual EntityTypeBuilder<TEntity> HasQueryFilter(Expression<Func<TEntity, bool>>? filter)
=> (EntityTypeBuilder<TEntity>)base.HasQueryFilter(filter);

/// <summary>
/// Specifies a LINQ predicate expression that will automatically be applied to any queries targeting
/// this entity type.
/// </summary>
/// <param name="filterKey">The filter key.</param>
/// <param name="filter">The LINQ predicate expression.</param>
/// <returns>The same builder instance so that multiple configuration calls can be chained.</returns>
public virtual EntityTypeBuilder<TEntity> HasQueryFilter(string filterKey, Expression<Func<TEntity, bool>>? filter)
=> (EntityTypeBuilder<TEntity>)base.HasQueryFilter(filterKey, filter);

/// <summary>
/// Configures an unnamed index on the specified properties.
/// If there is an existing index on the given list of properties,
Expand Down
22 changes: 22 additions & 0 deletions src/EFCore/Metadata/Builders/IConventionEntityTypeBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,19 @@ bool CanHaveTrigger(
/// </returns>
IConventionEntityTypeBuilder? HasQueryFilter(LambdaExpression? filter, bool fromDataAnnotation = false);

/// <summary>
/// Specifies a LINQ predicate expression that will automatically be applied to any queries targeting
/// this entity type.
/// </summary>
/// <param name="filterKey">The filter key.</param>
/// <param name="filter">The LINQ predicate expression.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>
/// The same builder instance if the query filter was set,
/// <see langword="null" /> otherwise.
/// </returns>
IConventionEntityTypeBuilder? HasQueryFilter(string filterKey, LambdaExpression? filter, bool fromDataAnnotation = false);

/// <summary>
/// Returns a value indicating whether the given query filter can be set from the current configuration source.
/// </summary>
Expand All @@ -878,6 +891,15 @@ bool CanHaveTrigger(
/// <returns><see langword="true" /> if the given query filter can be set.</returns>
bool CanSetQueryFilter(LambdaExpression? filter, bool fromDataAnnotation = false);

/// <summary>
/// Returns a value indicating whether the given query filter can be set from the current configuration source.
/// </summary>
/// <param name="filterKey">The filter key.</param>
/// <param name="filter">The LINQ predicate expression.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns><see langword="true" /> if the given query filter can be set.</returns>
bool CanSetQueryFilter(string filterKey, LambdaExpression? filter, bool fromDataAnnotation = false);

/// <summary>
/// Configures the <see cref="ChangeTrackingStrategy" /> to be used for this entity type.
/// This strategy indicates how the context detects changes to properties for an instance of the entity type.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Metadata.Internal;

namespace Microsoft.EntityFrameworkCore.Metadata.Conventions;
Expand Down Expand Up @@ -43,10 +44,23 @@ public virtual void ProcessModelFinalizing(
{
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes())
{
var queryFilter = entityType.GetQueryFilter();
if (queryFilter != null)
var queryFilters = entityType.GetDeclaredQueryFilters();
foreach (var queryFilter in queryFilters)
{
entityType.SetQueryFilter((LambdaExpression)DbSetAccessRewriter.Rewrite(modelBuilder.Metadata, queryFilter));
if (queryFilter.Expression == null)
{
continue;
}

var expression = (LambdaExpression)DbSetAccessRewriter.Rewrite(modelBuilder.Metadata, queryFilter.Expression);
if (queryFilter.IsAnonymous)
{
entityType.SetQueryFilter(expression);
}
else
{
entityType.SetQueryFilter(queryFilter.Key, expression);
}
}
}
}
Expand Down
11 changes: 8 additions & 3 deletions src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs
Original file line number Diff line number Diff line change
Expand Up @@ -299,10 +299,15 @@ protected virtual void ProcessEntityTypeAnnotations(
}
}

if (annotations.TryGetValue(CoreAnnotationNames.QueryFilter, out var queryFilter))
if (annotations.TryGetValue(CoreAnnotationNames.QueryFilter, out var queryFilters) && queryFilters != null)
{
annotations[CoreAnnotationNames.QueryFilter] =
new QueryRootRewritingExpressionVisitor(runtimeEntityType.Model).Rewrite((Expression)queryFilter!);

var rewritingVisitor = new QueryRootRewritingExpressionVisitor(runtimeEntityType.Model);

annotations[CoreAnnotationNames.QueryFilter] = new QueryFilterCollection(
((QueryFilterCollection)queryFilters)
.Select(x => new RuntimeQueryFilter(x.Key, (LambdaExpression)rewritingVisitor.Rewrite(x.Expression!)))
);
}
}
}
Expand Down
20 changes: 18 additions & 2 deletions src/EFCore/Metadata/IConventionEntityType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ public interface IConventionEntityType : IReadOnlyEntityType, IConventionTypeBas
/// </summary>
bool IsKeyless { get; }

/// <summary>
/// Sets the query filter automatically applied to queries for this entity type.
/// </summary>
/// <param name="filterKey">The filter key.</param>
/// <param name="filter">The LINQ predicate expression.</param>
/// <returns>The configured filter.</returns>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>The configured filter.</returns>
IQueryFilter? SetQueryFilter(string filterKey, LambdaExpression? filter, bool fromDataAnnotation = false);

/// <summary>
/// Sets the LINQ expression filter automatically applied to queries for this entity type.
/// </summary>
Expand All @@ -51,11 +61,17 @@ public interface IConventionEntityType : IReadOnlyEntityType, IConventionTypeBas
LambdaExpression? SetQueryFilter(LambdaExpression? queryFilter, bool fromDataAnnotation = false);

/// <summary>
/// Returns the configuration source for <see cref="IReadOnlyEntityType.GetQueryFilter" />.
/// Returns the configuration source for <see cref="IReadOnlyEntityType.GetDeclaredQueryFilters" />.
/// </summary>
/// <returns>The configuration source for <see cref="IReadOnlyEntityType.GetQueryFilter" />.</returns>
/// <returns>The configuration source for <see cref="IReadOnlyEntityType.GetDeclaredQueryFilters" />.</returns>
ConfigurationSource? GetQueryFilterConfigurationSource();

/// <summary>
/// Returns the configuration source for <see cref="IReadOnlyEntityType.GetDeclaredQueryFilters" />.
/// </summary>
/// <returns>The configuration source for <see cref="IReadOnlyEntityType.GetDeclaredQueryFilters" />.</returns>
ConfigurationSource? GetQueryFilterConfigurationSource(string? filterKey);

/// <summary>
/// Sets the value indicating whether the discriminator mapping is complete.
/// </summary>
Expand Down
7 changes: 7 additions & 0 deletions src/EFCore/Metadata/IMutableEntityType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ public interface IMutableEntityType : IReadOnlyEntityType, IMutableTypeBase
/// <param name="queryFilter">The LINQ expression filter.</param>
void SetQueryFilter(LambdaExpression? queryFilter);

/// <summary>
/// Sets the query filter automatically applied to queries for this entity type.
/// </summary>
/// <param name="filterKey">The filter key.</param>
/// <param name="filter">The LINQ predicate expression.</param>
void SetQueryFilter(string filterKey, LambdaExpression? filter);

/// <summary>
/// Sets the value indicating whether the discriminator mapping is complete.
/// </summary>
Expand Down
28 changes: 28 additions & 0 deletions src/EFCore/Metadata/IQueryFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;

namespace Microsoft.EntityFrameworkCore.Metadata;

/// <summary>
/// Represents a query filter in a model.
/// </summary>
public interface IQueryFilter
{
/// <summary>
/// The LINQ expression of the filter.
/// </summary>
LambdaExpression? Expression { get; }

/// <summary>
/// The name of the filter.
/// </summary>
string? Key { get; }

/// <summary>
/// Indicates whether the query filter is anonymous.
/// </summary>
[MemberNotNullWhen(false, nameof(Key))]
bool IsAnonymous => Key == null;
}
14 changes: 14 additions & 0 deletions src/EFCore/Metadata/IReadOnlyEntityType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,26 @@ public interface IReadOnlyEntityType : IReadOnlyTypeBase
/// <returns>The data.</returns>
IEnumerable<IDictionary<string, object?>> GetSeedData(bool providerValues = false);

/// <summary>
/// Gets the query filters automatically applied to queries for this entity type.
/// </summary>
/// <returns>The query filters.</returns>
IReadOnlyCollection<IQueryFilter> GetDeclaredQueryFilters();

/// <summary>
/// Gets the LINQ expression filter automatically applied to queries for this entity type.
/// </summary>
/// <returns>The LINQ expression filter.</returns>
[Obsolete("Use GetDeclaredQueryFilters() instead.")]
LambdaExpression? GetQueryFilter();

/// <summary>
/// Retrieves the query filter associated with the specified key.
/// </summary>
/// <param name="filterKey">The key identifying the query filter to retrieve.</param>
/// <returns>The <see cref="IQueryFilter"/> associated with the specified key.</returns>
IQueryFilter? FindDeclaredQueryFilter(string? filterKey);

/// <summary>
/// Returns the value indicating whether the discriminator mapping is complete for this entity type.
/// </summary>
Expand Down
Loading