3

I have the following method to build some custom EF queries to support a text filter which is very close to working, but I'm having a problem with the LEFT side of the assembled expression. When I use "Expression.Invoke" (first line of method body), I get an exception that The LINQ expression node type 'Invoke' is not supported in LINQ to Entities. which makes sense to me (I conceptually understand what's going on in the LINQ => SQL translation). So I figured the left side of the expression must need something more like the right side (i.e. using Expression.Constant) where all the 'preprocessing' is done so LINQ to Entities knows how to construct the left side of the expression.

But when I use the 2nd line (Expression.Property), I get an exception:

Instance property 'PropertyName' is not defined for type System.Func2[Proj.EntityFramework.DomainObject,System.Decimal]'

Which I understand.... much less.

Example call to the method in question:

return context.DomainObjects.Where(BuildExpression(l => l.PropertyName, "<200"));

So, I roughly get that I'm building the expression wrong and it's trying to pull the property name off the supplied expression rather than whatever EF needs to compile the SQL statement, but I'm a bit lost at this point.

private static Expression<Func<DomainObject, bool>> BuildExpression<TDest>(
    Expression<Func<DomainObject, TDest>> propertyexpression,
    string term
) where TDest : struct {
  //var property = Expression.Invoke(propertyexpression, propertyexpression.Parameters.ToArray());
  var property = Expression.Property(propertyexpression, ((MemberExpression)propertyexpression.Body).Member.Name);
  var parser = new ParsedSearchTerm<TDest>(term); // e.g. "<200" => { LowerBound = null, Operator = "<", UpperBound = 200 }

  Expression final = null;
  if (parser.HasLowerBound) {
    final = Expression.AndAlso(
      Expression.GreaterThanOrEqual(property, Expression.Constant(parser.LowerBound)),
      Expression.LessThanOrEqual(property, Expression.Constant(parser.UpperBound)));
  }
  else {
    switch (parser.Operator) {
      case "<":
        final = Expression.LessThanOrEqual(property, Expression.Constant(parser.UpperBound));
        break;
      case ">":
        final = Expression.GreaterThanOrEqual(property, Expression.Constant(parser.UpperBound));
        break;
      case "=":
        final = Expression.Equal(property, Expression.Constant(parser.UpperBound));
        break;
      case "!":
        final = Expression.Negate(Expression.Equal(property, Expression.Constant(parser.UpperBound)));
        break;
    }
  }

  return Expression.Lambda<Func<DomainObject, bool>>(final, propertyexpression.Parameters.ToArray());
}
BLSully
  • 5,929
  • 1
  • 27
  • 43
  • The code above shouldn't be able to give you that error message. Is the line you commented out responsible for that? – DavidG Dec 19 '18 at 21:00
  • 1
    `propertyexpression` is a `Func<>`: type `Func` does not have a property called `Member.Name` (=`PropertyName`). Just put in `var property = propertyexpression.Body;`. – NetMage Dec 19 '18 at 21:05
  • 1
    Note: I handled converting from `Expression.Invoke` to something supported by EF for my left outer join `IQueryable` implementation by using an extension method `Apply` that expanded a lambda body by replacing the parameters with the arguments using an `ExpressionVisitor` as in [this answer](https://stackoverflow.com/a/49418695/2557128) (you probably don't need the `PropagateNull` on your `Apply`). You just substitute `Apply` where you have `Expression.Invoke`. – NetMage Dec 19 '18 at 21:49
  • @NetMage your first comment was spot on. Can you add that as an answer and I'll accept? Can't believe it was that simple :-P – BLSully Dec 19 '18 at 21:51
  • I added the code for `Apply` as well... – NetMage Dec 19 '18 at 21:57

1 Answers1

3

To make your code that manually expands the Invoke into the lambda body, you need to use the body of the lambda parameter (propertyexpression) as the property value you want to test:

var property = propertyexpression.Body;

(I would rename propertyexpression to propertylambda - really, propertyexpression.Body is the property expression).

You could use your original lambda with EF if you replace Invoke with an extension that does an in-place expansion of the lambda body of the propertylambda with the arguments substituted in for the lambda parameters. I call it Apply.

Given some Expression extension methods:

public static class ExpressionExt {
    /// <summary>
    /// Replaces a sub-Expression with another Expression inside an Expression
    /// </summary>
    /// <param name="orig">The original Expression.</param>
    /// <param name="from">The from Expression.</param>
    /// <param name="to">The to Expression.</param>
    /// <returns>Expression with all occurrences of from replaced with to</returns>
    public static Expression Replace(this Expression orig, Expression from, Expression to) => new ReplaceVisitor(from, to).Visit(orig);

    public static Expression PropagateNull(this Expression orig) => new NullVisitor().Visit(orig);

    public static Expression Apply(this LambdaExpression e, params Expression[] args) {
        var b = e.Body;

        foreach (var pa in e.Parameters.Zip(args, (p, a) => (p, a)))
            b = b.Replace(pa.p, pa.a);

        return b.PropagateNull();
    }
}

and some ExpressionVisitor classes to do the changes:

/// <summary>
/// Standard ExpressionVisitor to replace an Expression with another in an Expression.
/// </summary>
public class ReplaceVisitor : ExpressionVisitor {
    readonly Expression from;
    readonly Expression to;

    public ReplaceVisitor(Expression from, Expression to) {
        this.from = from;
        this.to = to;
    }

    public override Expression Visit(Expression node) => node == from ? to : base.Visit(node);
}

/// <summary>
/// ExpressionVisitor to replace a null.member Expression with a null
/// </summary>
public class NullVisitor : System.Linq.Expressions.ExpressionVisitor {
    public override Expression Visit(Expression node) {
        if (node is MemberExpression nme && nme.Expression is ConstantExpression nce && nce.Value == null)
            return Expression.Constant(null, nce.Type.GetMember(nme.Member.Name).Single().GetMemberType());
        else
            return base.Visit(node);
    }
}

You can take any instance of Expression.Invoke(lambda,args) and replace it with Apply(lambda, args) and it will expand the lambda body in-line so EF will accept it.

NetMage
  • 26,163
  • 3
  • 34
  • 55
  • Thanks for the explanations. I'll have to think on that a bit. I'm brand new to the Expression namespace. And the renaming of the variable helped me realize why I should do it that way. Makes a lot more sense now. – BLSully Dec 19 '18 at 22:03
  • I would say combining can be complicated, hence the creation of libraries like LINQKit to make it easier, and more automated, and that `ExpressionVisitor` can be really powerful and just a few custom visitors can make hard things possible. – NetMage Dec 19 '18 at 22:08