逐步构建 lambda 表达式时出现问题

Issue when building lambda expression progressively

我正在尝试以这种方式逐步构建 lambda 表达式:

public class PropertySearchFilter
{
    public virtual Expression<Func<T,bool>> GetSearchFilter<T>(SearchFilterModel filterModelModel) where T: Property
    {
        Expression<Func<T, bool>> combinedFilter = null;
        Expression<Func<T, bool>>? countryFilter = filterModelModel.CountryId.HasValue ? x => x.CountryId == filterModelModel.CountryId.GetValueOrDefault() : null;
        Expression<Func<T, bool>>? cityFilter = filterModelModel.CityId.HasValue ? x => x.CityId == filterModelModel.CityId.GetValueOrDefault() : null;
        Expression<Func<T, bool>>? categoryFilter = filterModelModel.CategoryId.HasValue ? x => x.CategoryId == filterModelModel.CategoryId.GetValueOrDefault() : null;
        Expression<Func<T, bool>>? transactionTypeFilter = filterModelModel.TransactionTypeId.HasValue
            ? x => x.TransactionTypeId == filterModelModel.TransactionTypeId.GetValueOrDefault()
            : null;

        Expression<Func<T, bool>>? publicFilter = filterModelModel.IsPublic.HasValue ? x => x.IsPublic == filterModelModel.IsPublic.GetValueOrDefault() : null;
        Expression<Func<T, bool>>? createdByFilter = !string.IsNullOrEmpty(filterModelModel.CreatedBy) ? x => x.CreatedBy == filterModelModel.CreatedBy : null;
        Expression<Func<T, bool>>? minimumPriceFilter =
            filterModelModel.MinimumPrice.HasValue ? x => x.Price >= filterModelModel.MinimumPrice.GetValueOrDefault() : null;
        Expression<Func<T, bool>>? maximumPriceFilter = filterModelModel.MaximumPrice.HasValue ? x => x.Price <= filterModelModel.MaximumPrice.GetValueOrDefault() : null;

        if (countryFilter != null)
            combinedFilter = countryFilter;
        if (cityFilter != null)
        {
            combinedFilter = combinedFilter.And(cityFilter);
        }

        if (categoryFilter != null)
        {
            combinedFilter = combinedFilter.And(categoryFilter);
        }

        if (transactionTypeFilter != null)
        {
            combinedFilter = combinedFilter.And(transactionTypeFilter);
        }

        if (publicFilter != null)
        {
            combinedFilter = combinedFilter.And(publicFilter);
        }

        if (createdByFilter != null)
        {
            combinedFilter = combinedFilter.And(createdByFilter);
        }

        if (minimumPriceFilter != null)
        {
            combinedFilter = combinedFilter.And(minimumPriceFilter);
        }

        if (maximumPriceFilter != null)
        {
            combinedFilter = combinedFilter.And(maximumPriceFilter);
        }

        return combinedFilter;
    }
}

这是扩展方法:

public static class ExpressionExtensionsMethods
{
    public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
    {
        if (left == null) return right;
        var and = Expression.AndAlso(left.Body, right.Body);
        return Expression.Lambda<Func<T, bool>>(and, left.Parameters.Single());
    }

    public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
    {
        if (left == null) return right;
        var and = Expression.OrElse(left.Body, right.Body);
        return Expression.Lambda<Func<T, bool>>(and, left.Parameters.Single());
    }
}

我收到以下错误:

System.InvalidOperationException: The LINQ expression 'x' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.VisitParameter(ParameterExpression parameterExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.VisitMember(MemberExpression memberExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.VisitBinary(BinaryExpression binaryExpression)
   at Microsoft.EntityFrameworkCore.SqlServer.Query.Internal.SqlServerSqlTranslatingExpressionVisitor.VisitBinary(BinaryExpression binaryExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.VisitBinary(BinaryExpression binaryExpression)
   at Microsoft.EntityFrameworkCore.SqlServer.Query.Internal.SqlServerSqlTranslatingExpressionVisitor.VisitBinary(BinaryExpression binaryExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.TranslateInternal(Expression expression)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.Translate(Expression expression)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateExpression(Expression expression)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateLambdaExpression(ShapedQueryExpression shapedQueryExpression, LambdaExpression lambdaExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateWhere(ShapedQueryExpression source, LambdaExpression predicate)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass12_0`1.<ExecuteAsync>b__0()
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1.GetAsyncEnumerator(CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.ConfiguredCancelableAsyncEnumerable`1.GetAsyncEnumerator()
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
   at Meerkat.Application.Persistence.GenericRepository`1.GetPageAsync(Int32 pageNumber, Int32 pageSize, Expression`1 filter, Func`2 orderBy, String includeProperties) in C:\Azure DevOps\Meerkat Back-end\Meerkat.Service\Meerkat.Application\Persistence\GenericRepository.cs:line 100
   at Meerkat.Application.Facades.RealEstateFacade.GetPropertiesAsync(Int32 pageNumber, Int32 pageSize, SearchFilterModel filterModel, String includeProperties) in C:\Azure DevOps\Meerkat Back-end\Meerkat.Service\Meerkat.Application\Facades\RealEstateFacade.cs:line 126
   at Meerkat.Application.Facades.RealEstateFacade.GetPropertiesAsync(SearchFilterModel filterModel, String includeProperties, Int32 pageNumber, Int32 propertiesPerPage) in C:\Azure DevOps\Meerkat Back-end\Meerkat.Service\Meerkat.Application\Facades\RealEstateFacade.cs:line 41
   at Meerkat.WebService.Controllers.RealEstateController.GetProperties(String filter) in C:\Azure DevOps\Meerkat Back-end\Meerkat.Service\Meerkat.WebService\Controllers\RealEstateController.cs:line 34
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Localization.RequestLocalizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

我错过了什么?

感谢您的帮助。

LINQ Expressions 是对象树,而不是要编译的文本集合。虽然来自源函数表达式的参数在外部看起来可能相同,但实际上它们是不同的对象,只是恰好具有相同的属性。因此,当您组合两个函数表达式并从其中一个中抛出参数时,您会得到一个 Expression,它不包含所有信息。

为了使这一点更明显,假设您要将 a => a.Name == "test" 添加到 b => b.Age > 0。您的代码将生成等同于 a => a.Name == "test" && b.Age > 0... 的 LINQ 表达式,这会在混合中留下一个未知对象 b。即使您更改了源表达式中的名称,它仍然是一个未知对象。

幸运的是,我们可以使用 ExpressionVisitor 来解决这个问题。这是我在类似情况下使用的一个:

class ExpressionReplacer : ExpressionVisitor
{
    private readonly Expression From;
    private readonly Expression To;
    
    private ExpressionReplacer(Expression from, Expression to)
    {
        From = from;
        To = to;
    }
    
    public override Expression Visit(Expression node)
    {
        if (ReferenceEquals(node, From))
            return To;
        return base.Visit(node);
    }
    
    public static T Replace<T>(T target, Expression from, Expression to)
        where T : Expression
    {
        var replacer = new ExpressionReplacer(from, to);
        return (T)replacer.Visit(target);
    }
}

您可以在扩展方法中使用它来更改正在组合的函数之一的主体,以使用正确的参数实例,如下所示:

public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
{
    if (left == null) return right;
    var right_body = ExpressionReplacer.Replace(right.Body, right.Parameters[0], left.Parameters[0]);
    var and = Expression.AndAlso(left.Body, right_body);
    return Expression.Lambda<Func<T, bool>>(and, left.Parameters[0]);
}

LINQ 表达式有趣又有趣,你可以用它们做一些非常有用的事情,表达式访问者都是乐趣的一部分。