Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 874
tschuege Avatar asked Mar 21 '23 20:03

tschuege


1 Answers

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);
}
like image 147
Servy Avatar answered Mar 31 '23 23:03

Servy