BLSully
BLSully

Reputation: 5939

Building Custom Expressions for Entity Framework (LINQ)

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

Upvotes: 3

Views: 1159

Answers (1)

NetMage
NetMage

Reputation: 26926

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.

Upvotes: 3

Related Questions