.Net Core:如何将多个过滤器表达式组合成一个

.Net Core: How to combine multiple filter expressions into a single one

我有一个基础过滤器 class,它负责检查实体集合是否匹配其子 classes 中定义的过滤条件:

public abstract class FilterInputBase<TEntity> : IFilterInput<TEntity>
{
    public Expression<Func<TEntity, bool>> MatchesFilter()
    {
        var input = Expression.Variable(typeof(TEntity), "entity");
        
        var expressions = GetFilterExpressions().ToList();
        if(expressions.Count == 0)
            expressions.Add(e => true);
        
        // checks if the input satisfies all the filter conditions
        var resultExpression = expressions.Aggregate(
            (l, r) => Expression.Lambda<Func<TEntity, bool>>(
                Expression.AndAlso(Expression.Invoke(l, input), Expression.Invoke(r, input)), input));
        
        return resultExpression;
    }

    /// <summary>
    /// Returns a list of filter conditions converted into Expressions
    /// </summary>
    /// <returns></returns>
    protected abstract IEnumerable<Expression<Func<TEntity, bool>>> GetFilterExpressions();
}

此 class 用作:

public static IQueryable<TEntity> Filter<TEntity>(this IQueryable<TEntity> query, IFilterInput<TEntity> filterInput) => 
        query.Where(filterInput?.MatchesFilter() ?? (x => true));

顾名思义,GetFilterExpressions方法returns基于过滤参数的过滤表达式集合。这些筛选条件在 MatchesFilter 方法中聚合以形成单个筛选表达式:resultExpression.

此解决方案适用于最多两个过滤器表达式。现在我需要有两个以上的表达式。但是如果有更多的表达式,查询将在第二次尝试时失败。它适用于第一个 运行。添加了更多表达式后出现以下错误:

"message": "An item with the same key has already been added. Key: entity",
    "stackTrace": "   at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)\n   at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)\n   at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.CompareLambda(LambdaExpression a, LambdaExpression b)\n   at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.Compare(Expression left, Expression right)\n   at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.CompareInvocation(InvocationExpression a, InvocationExpression b)\n   at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.Compare(Expression left, Expression right)\n   at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.CompareBinary(BinaryExpression a, BinaryExpression b)\n   at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.Compare(Expression left, Expression right)\n   at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.CompareLambda(LambdaExpression a, LambdaExpression b)\n   at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.Compare(Expression left, Expression right)\n   at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.CompareUnary(UnaryExpression a, UnaryExpression b)\n   at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.Compare(Expression left, Expression right)\n   at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.CompareExpressionList(IReadOnlyList`1 a, IReadOnlyList`1 b)\n   at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.CompareMethodCall(MethodCallExpression a, MethodCallExpression b)\n   at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.Compare(Expression left, Expression right)\n   at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.CompareExpressionList(IReadOnlyList`1 a, IReadOnlyList`1 b)\n   at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.CompareMethodCall(MethodCallExpression a, MethodCallExpression b)\n   at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.ExpressionComparer.Compare(Expression left, Expression right)\n   at Microsoft.EntityFrameworkCore.Query.ExpressionEqualityComparer.Equals(Expression x, Expression y)\n   at Microsoft.EntityFrameworkCore.Query.CompiledQueryCacheKeyGenerator.CompiledQueryCacheKey.Equals(CompiledQueryCacheKey other)\n   at Microsoft.EntityFrameworkCore.Query.RelationalCompiledQueryCacheKeyGenerator.RelationalCompiledQueryCacheKey.Equals(RelationalCompiledQueryCacheKey other)\n   at Microsoft.EntityFrameworkCore.SqlServer.Query.Internal.SqlServerCompiledQueryCacheKeyGenerator.SqlServerCompiledQueryCacheKey.Equals(SqlServerCompiledQueryCacheKey other)\n   at Microsoft.EntityFrameworkCore.SqlServer.Query.Internal.SqlServerCompiledQueryCacheKeyGenerator.SqlServerCompiledQueryCacheKey.Equals(Object obj)\n   at System.Collections.Concurrent.ConcurrentDictionary`2.TryGetValueInternal(TKey key, Int32 hashcode, TValue& value)\n   at System.Collections.Concurrent.ConcurrentDictionary`2.TryGetValue(TKey key, TValue& value)\n   at Microsoft.Extensions.Caching.Memory.MemoryCache.TryGetValue(Object key, Object& result)\n   at Microsoft.Extensions.Caching.Memory.CacheExtensions.TryGetValue[TItem](IMemoryCache cache, Object key, TItem& value)\n   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)\n   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)\n   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)\n   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1.GetAsyncEnumerator(CancellationToken cancellationToken)\n   at System.Runtime.CompilerServices.ConfiguredCancelableAsyncEnumerable`1.GetAsyncEnumerator()\n   at HotChocolate.Types.Pagination.QueryableOffsetPagingHandler`1.ExecuteQueryableAsync(IQueryable`1 queryable, CancellationToken cancellationToken)\n   at HotChocolate.Types.Pagination.QueryableOffsetPagingHandler`1.ResolveAsync(IResolverContext context, IQueryable`1 queryable, OffsetPagingArguments arguments)\n   at HotChocolate.Types.Pagination.OffsetPagingHandler.HotChocolate.Types.Pagination.IPagingHandler.SliceAsync(IResolverContext context, Object source)\n   at HotChocolate.Types.Pagination.PagingMiddleware.InvokeAsync(IMiddlewareContext context)\n   at HotChocolate.Utilities.MiddlewareCompiler`1.ExpressionHelper.AwaitTaskHelper(Task task)\n   at HotChocolate.Data.ToListMiddleware`1.InvokeAsync(IMiddlewareContext context)\n   at HotChocolate.Types.EntityFrameworkObjectFieldDescriptorExtensions.<>c__DisplayClass2_1`1.<<UseDbContext>b__4>d.MoveNext()\n--- End of stack trace from previous location where exception was thrown ---\n   at HotChocolate.Types.EntityFrameworkObjectFieldDescriptorExtensions.<>c__DisplayClass2_1`1.<<UseDbContext>b__4>d.MoveNext()\n--- End of stack trace from previous location where exception was thrown ---\n   at HotChocolate.AspNetCore.Authorization.AuthorizeMiddleware.InvokeAsync(IDirectiveContext context)\n   at HotChocolate.Utilities.MiddlewareCompiler`1.ExpressionHelper.AwaitTaskHelper(Task task)\n   at HotChocolate.Execution.Processing.ResolverTask.ExecuteResolverPipelineAsync(CancellationToken cancellationToken)\n   at HotChocolate.Execution.Processing.ResolverTask.TryExecuteAsync(CancellationToken cancellationToken)"

我认为 expressions.Aggregate() 可能不是正确的方法。有没有更好的方法来解决这个问题?另外,我需要清除任何缓存的词典吗?

你可以做的是递归地创建 BinaryExpressions,然后用更多的 BinaryExpressions 包裹它们

例如您有以下内容:

x => x > 1 
x => x == 2
x => x < 3
x => x != 4

我们基本上采用最后两个表达式并从中创建一个二进制表达式

   var lastExpression = Expression.MakeBinary(ExpressionType.AndAlso,
                        expression3, expression4);

然后你加上第二个和第一个

   lastExpression = Expression.MakeBinary(ExpressionType.AndAlso, 
                    expression2, lastExpression);

   lastExpression = Expression.MakeBinary(ExpressionType.AndAlso, 
                    expression1, lastExpression);

这样您将创建类似于以下表达式的内容:

x => (x > 1 && (x == 2 && (x < 3 && x != 4)))

你只需要调用它

Expression.invoke(lastExpression, input);

如果有人遇到类似问题,这是我想出的对我有效的解决方案。

    public Expression<Func<TEntity, bool>> MatchesFilter()
    {
        var expressions = GetFilterExpressions().ToList();

        if (expressions.Count <= 1)
            return expressions.FirstOrDefault() ??  (x => true);

        var input = Expression.Parameter(typeof(TEntity), "x");
        
        var resultExpression = Expression.AndAlso(Expression.Invoke(expressions[0], input), Expression.Invoke(expressions[1], input));
        resultExpression = expressions.Skip(2).Aggregate(resultExpression, (current, expression) =>
            Expression.AndAlso(current, Expression.Invoke(expression, input)));

        return Expression.Lambda<Func<TEntity, bool>>(resultExpression, input);
    }