Home > Net >  Issue when building lambda expression progressively
Issue when building lambda expression progressively

Time:04-04

I am trying to build a lambda expression progressively in this manner:

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;
    }
}

Here's the extensionMethods:

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());
    }
}

And I am getting the following error:

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)

What I am missing?

Thanks for any help.

CodePudding user response:

LINQ Expressions are trees of objects, not collections of text to be compiled. While the parameters from the source function expressions may look the same on the outside they are in fact different objects that just happen to have the same properties. So when you combine two function expressions and throw out the parameter(s) from one of them you're left with an Expression that doesn't have all the information.

To make this more obvious, imagine you're adding a => a.Name == "test" to b => b.Age > 0. Your code will produce a LINQ expression equivalent to a => a.Name == "test" && b.Age > 0... which leaves an unknown object b in the mix. Even if you changed the name in the source expression it would still be an unknown object.

Fortunately we can use an ExpressionVisitor to fix this up for us. Here's one I've used in similar situations:

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);
    }
}

You can use that in your extension methods to change the body of one of the functions being combined to use the correct parameter instance like this:

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 expressions are fun and interesting and you can do some very useful things with them, and expression visitors are all part of the fun.

  • Related