0

I want to combile two expressions :

public Expression<Func<AnyType,bool>> BuildExpression(int id, string filtreA, string filtreB)
    {
        Expression<Func<AnyType, bool>> extraExpression = null;
        Expression<Func<AnyType, bool>> expression = null;

        expression = a => a.ID == id;

        var fullExpression = expression.Body;
      
        if (!String.IsNullOrEmpty(filtreA))
        {
            extraExpression = a => a.TYPE == filtreA;
            fullExpression = Expression.AndAlso(fullExpression, extraExpression.Body);
        }
         if (!String.IsNullOrEmpty(filtreB))
        {
            extraExpression = a => a.TYPE == filtreB;
            fullExpression = Expression.AndAlso(fullExpression, extraExpression.Body);
        }
    
        expression = Expression.Lambda<Func<AnyType, bool>>(fullExpression, expression.Parameters[0]);
        }

        return expression;
    }

    //I want my final expression to be a.ID == id && a.TYPE == filtreB for example

The problem is that I get the following error : System.InvalidOperationException. Le paramètre 'a' n'est pas dans la portée.. It only happens if enter in one of my ifs.

Does anyone know how I can handle this ? Thanks

ps: My question il similar to this post but it looks like the solution doesn't work anymore : Combining two expressions (Expression<Func<T, bool>>)

  • What is `AnyType`? – Patrick Roberts Oct 15 '20 at 15:36
  • on which line is the error? – Pac0 Oct 15 '20 at 15:39
  • It could be anytype, string for example. But it's the same everywhere – Simon Descamps Oct 15 '20 at 15:40
  • I get the error after when I do a searchQuery to my database – Simon Descamps Oct 15 '20 at 15:41
  • When I return expression I do have an expression returned which looks normal and contains my a.TYPE == filtreB. The problem is that the "a" parameter seems to not exist ( or to be different ) for my a.TYPE – Simon Descamps Oct 15 '20 at 15:45
  • The solution is working if you use it directly `public Expression> BuildExpression(int id, string filtreA, string filtreB) { Expression> expression = a => a.ID == id; if (!String.IsNullOrEmpty(filtreA)) expression = expression.AndAlso(a => a.TYPE == filtreA); if (!String.IsNullOrEmpty(filtreB)) expression = expression.AndAlso(a => a.TYPE == filtreB); return expression; }` with extension from the answer works – Selvin Oct 15 '20 at 16:12

2 Answers2

1

You have missed parameters replacement. It is a tricky with visitors. Trying to simplify your life, I would suggest to use popular library LINQKit and rewrite your expression building.

public Expression<Func<AnyType, bool>> BuildExpression(int id, string filtreA, string filtreB)
{
    var predicate = PredicateBuilder.New<AnyType>(true);

    predicate = predicate.And(a => a.ID == id);

    if (!string.IsNullOrEmpty(filtreA))
    {
        predicate = predicate.And(a => a.TYPE == filtreA);
    }

    if (!string.IsNullOrEmpty(filtreB))
    {
        predicate = predicate.And(a => a.TYPE == filtreB);
    }
    
    return predicate;
}
Svyatoslav Danyliv
  • 21,911
  • 3
  • 16
  • 32
  • It works ! I didn't know that I already had `PredicateBuilder` class in my project. I dont instantiate it like you ( I do `var predicate = PredicateBuilder.True();` ). Thank you! – Simon Descamps Oct 16 '20 at 07:58
1

Each parameter is a different ParameterExpression instance. This problem would be more obvious if you had defined each expression with different parameter names;

expression = a => a.ID == id;
// ...
extraExpression = b => b.TYPE == filtreA;
// ...
extraExpression = c => c.TYPE == filtreB;

Note that entity framework core 2.1 and earlier didn't care if your parameters were different, and would seem to look through .Invoke operations. But since version 3, with an internal rewrite of the expression compiler, neither of those are supported. Hence why earlier examples may not work anymore.

An expression visitor to swap parameter expressions isn't too complicated;

public class MapParameters : ExpressionVisitor
{
    private readonly Dictionary<ParameterExpression, ParameterExpression> mapping;
    public MapParameters(IEnumerable<ParameterExpression> before, IEnumerable<ParameterExpression> after)
    {
        this.mapping = new Dictionary<ParameterExpression, ParameterExpression>(
            before
                .Zip(after)
                .Select(p => KeyValuePair.Create(p.First,p.Second))
        );
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        if (mapping.TryGetValue(node, out var replace))
            return replace;
        return base.VisitParameter(node);
    }
}

Expression.Lambda<Func<AnyType, bool>>(
    Expression.AndAlso(
        expression.Body,
        new MapParameters(
            extraExpression.Parameters,
            expression.Parameters
        ).Visit(extraExpression.Body)
    ),
    expression.Parameters);

Also note that if you are using this to build a single expression for IQueryable<T>.Where(...). You could consider calling .Where(...) twice, which would achieve the same end result.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Jeremy Lakeman
  • 9,515
  • 25
  • 29
  • Ok thank you, that is why id didn't work. I will try this solution and give you a feedback. Where/How do you call `.Where(...)` twice ? – Simon Descamps Oct 16 '20 at 07:07
  • There is no methode `Zip` that accepts this parameter. – Simon Descamps Oct 16 '20 at 07:24
  • Worked for me, dunno which .net version you have though; https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.zip?view=netcore-3.1#System_Linq_Enumerable_Zip__2_System_Collections_Generic_IEnumerable___0__System_Collections_Generic_IEnumerable___1__ – Jeremy Lakeman Oct 16 '20 at 08:22