1

I'm trying to combine two lambda expressions to build something with an OR-clause, but it fails with the following exception message:

variable 'foo' of type 'Foo' referenced from scope '', but it is not defined.

Why, and how do I fix it?

Here's a failing code sample, based on Marc Gravell's answer to the question linked above:

static Expression<Func<T, bool>> Or<T>(Expression<Func<T, bool>> a, Expression<Func<T, bool>> b)
    => Expression.Lambda<Func<T, bool>>(Expression.OrElse(a.Body, b.Body), b.Parameters);

static Expression<Func<Foo, bool>> BarMatches(Bar bar) => foo => foo.Bar.Value == bar.Value;
static Expression<Func<Foo, bool>> BazMatches(Baz baz) => foo => foo.Baz.Value == baz.Value;

// sample usage (see below): foos.Where(Or(MatchesBar(bar), MatchesBaz(baz)))

void Main()
{
    var foos = new[]
    {
        new Foo
        {
            Bar = new Bar
            {
                Value = "bar"
            },
            Baz = new Baz
            {
                Value = "baz"
            }
        },
        new Foo
        {
            Bar = new Bar
            {
                Value = "not matching"
            },
            Baz = new Baz
            {
                Value = "baz"
            }
        }
    }.AsQueryable();

    var bar = new Bar { Value = "bar" };
    var baz = new Baz { Value = "baz" };

    Console.WriteLine(foos.Where(Or(BarMatches(bar), BazMatches(baz))).Count());
}


// Define other methods and classes here
class Foo
{
    public Bar Bar { get; set; }
    public Baz Baz { get; set; }
}

class Bar
{
    public string Value { get; set; }
}

class Baz
{
    public string Value { get; set; }
}
Tomas Aschan
  • 58,548
  • 56
  • 243
  • 402
  • 2
    That same asnwer you linked explains this. Answer starts: "but the problem is the parameters; are you working with the same ParameterExpression in expr1 and expr2? If so, it is easier". You are not using the same parameters - so this code is not applicable to your case and you need to read that answer futher. – Evk May 30 '18 at 13:17
  • @Evk, Thanks: I was trying to avoid `Expression.Invoke` because I was worried the EF Linq provider would not be able to cope - turns out it works just fine. If you want to write that as an answer instead, I can accept it and give you some rep for it :) – Tomas Aschan May 30 '18 at 14:52
  • If you are using EF Core, I'd ensure that it really translates that to SQL and does not just invoke in memory. But I think the real solution to this problem is expression visitor approach, as mentioned again a bit further in that answer :) Or third party libraries like LinqKit, which do that for you. – Evk May 30 '18 at 15:57

1 Answers1

-1

The problem is that the parameter (foo) is not the same for BarMatches and BazMatches. Therefore you need to unify the parameters so that the Or-ed expressions use the same one. This can be done with an expression replacer (stolen from this answer):

static TExpr ReplaceExpressions<TExpr>(TExpr expression,
                                              Expression orig,
                                              Expression replacement)
where TExpr : Expression 
{
    var replacer = new ExpressionReplacer(orig, replacement);
    return replacer.VisitAndConvert(expression, "ReplaceExpressions");
}

private class ExpressionReplacer : ExpressionVisitor
{
    private readonly Expression From;
    private readonly Expression To;

    public ExpressionReplacer(Expression from, Expression to) {
        From = from;
        To = to;
    }

    public override Expression Visit(Expression node) {
        if (node == From) {
            return To;
        }
        return base.Visit(node);
    }
}

This class will replace all instances of one expression with another. We can now use this to create a unified expression:

static Expression<Func<T, bool>> Or<T>(Expression<Func<T, bool>> a, Expression<Func<T, bool>> b) {
    // We know Parameters is exactly of length 1.
    // If you had multiple parameters you would need to invoke this for each parameter
    var replaced = ReplaceExpressions(a.Body, a.Parameters[0], b.Parameters[0]);
    return Expression.Lambda<Func<T, bool>>(Expression.OrElse(replaced, b.Body), b.Parameters);
}

While this code will do what you want, I suggest using a library that does this and much more: LinqKIT

felipe
  • 662
  • 4
  • 16