tschuege
tschuege

Reputation: 761

linq to entities dynamic where build from lambdas

i have a set of lambdas like this

t => t.FirstName
t => t.LastName
t => t.Profession

I would like to find a way to build an expression that can be used in a Where statement in Linq to Entities where these lambdas are compared to a value using string.contains

// a filter is definded by a lambda and the string to compare it with   
var filters = new Dictionary<Expression<Func<Person, string>>, string>();
filters.Add(t => t.FirstName, "Miller");
filters.Add(t => t.Profession, "Engineer");
var filterConstraints = BuildFilterExpression(t => t, filters);
Entities.Persons.Where(filterConstraints).ToList();

public static Expression<Func<TElement, bool>> BuildFilterExpression<TElement>(Dictionary<Expression<Func<TElement, string>>, string> constraints)
{
  List<Expression> expressions = new List<Expression>();

  var stringType = typeof(string);
  var containsMethod = stringType.GetMethod("Contains", new Type[] { stringType });

  foreach (var constraint in constraints)
  {
    var equalsExpression = (Expression)Expression.Call(constraint.Key.Body, containsMethod, Expression.Constant(constraint.Value, stringType));
    expressions.Add(equalsExpression);
  }

  var body = expressions.Aggregate((accumulate, equal) => Expression.And(accumulate, equal));

  ParameterExpression p = constraints.First().Key.Parameters.First();
  return Expression.Lambda<Func<TElement, bool>>(body, p);
}

I guess I'm doing something terribly wrong in building the expression tree because i get the following exception: Invalid operation exception - The parameter 't' was not bound in the specified LINQ to Entities query expression.

Does anyone know how to solve this problem?

Upvotes: 4

Views: 1797

Answers (2)

Servy
Servy

Reputation: 203814

You're actually really close. The issue is that parameter objects that have the same name and type, aren't technically "equal".

var b = Expression.Parameter(typeof(string), "p") == 
    Expression.Parameter(typeof(string), "p");
//b is false

So the parameter of the lambda that you create is the parameter of the first expression that you take as input. The parameters used in the body of all of the other expressions are different parameters, and they aren't given as parameters to the lambda, so the error is because of that.

The solution is actually fairly simple. You just need to replace all instances of all of the other parameters with the actual parameter that you want to use.

Here is a helper method (using a helper class) that takes all instances of one expression in some expression and replaces it with another:

public class ReplaceVisitor : ExpressionVisitor
{
    private readonly Expression from, to;
    public ReplaceVisitor(Expression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }
    public override Expression Visit(Expression node)
    {
        return node == from ? to : base.Visit(node);
    }
}

public static Expression Replace(this Expression expression,
    Expression searchEx, Expression replaceEx)
{
    return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
}

Now we just call that once on each body, replacing in a common parameter:

public static Expression<Func<TElement, bool>> BuildFilterExpression<TElement>(
    Dictionary<Expression<Func<TElement, string>>, string> constraints)
{
    List<Expression> expressions = new List<Expression>();

    var stringType = typeof(string);
    var containsMethod = stringType.GetMethod("Contains", new Type[] { stringType });

    var parameter = Expression.Parameter(typeof(TElement));

    foreach (var constraint in constraints)
    {
        var equalsExpression = (Expression)Expression.Call(
            constraint.Key.Body.Replace(constraint.Key.Parameters[0], parameter),
            containsMethod, Expression.Constant(constraint.Value, stringType));
        expressions.Add(equalsExpression);
    }

    var body = expressions.Aggregate((accumulate, equal) =>
        Expression.And(accumulate, equal));

    return Expression.Lambda<Func<TElement, bool>>(body, parameter);
}

Upvotes: 5

Aron
Aron

Reputation: 15772

Close. Unfortunately if you take a look inside each of your property lambdas, for instance..

t => t.FirstName 
t => t.LastName

You will find they are each of Expression.Property. However each of them has a different Expression.Parameter. You want to use a ExpressionVisitor to replace the PropertyExpression.Parameter with the same INSTANCE of Expression.Parameter AND use that with Expression.Lambda.

The Exception Invalid operation exception - The parameter 't' was not bound in the specified LINQ to Entities query expression. means you have ParameterExpressions in your lambda's body that aren't in the lambda's parameter array.

Upvotes: 2

Related Questions