Search code examples
c#linqentity-frameworkparameter-passingexpression-trees

Use the same lambda parameter across multiple filters


I am writing a class that allows users to search for entities based on a search term, working against Entity Framework under the hood. Every searchable field is registered with the class. For string fields, the search simply calls String.Contains. For look-up values, a call out to the database lookup table is needed.

For example, this is how to register a simple string filter:

Searcher<Account> searcher = new Searcher<Account>();
searcher.AddFilterMapping("Name", e => e.AccountName);

And this is what it looks like to register a lookup value:

searcher.AddFilterMapping("Type", e => context.AccountTypes.Where(t => t.Id == e.AccountTypeId).Select(t => t.Description).FirstOrDefault());

The context in the code above is the Entity Framework context. Now, ideally, someone will specify a search term like so:

searcher.SearchTerm = searchTerm;

The search should then loop through the filter mappings (lambda expressions) and update their lambda expressions to apply the String.Contains. I was able to write some pretty simply code to add the method call.

MethodInfo containsMethod = typeof(String).GetMethod("Contains");
ConstantExpression searchTermExpression = Expression.Constant(searchTerm);
foreach (Expression<Func<TEntity, string>> selector in selectors)
{
    Expression converter = selector.Body;
    MethodCallExpression containsExpression = Expression.Call(converter, containsMethod, searchTermExpression);
    LambdaExpression lambdaExpression = Expression.Lambda<Func<TEntity, bool>>(containsExpression, selector.Parameters);
    // Then apply the filter to the IQueryable<TEntity> via Where
}

This works fine if all I am doing is applying each filter, one after the other. However, I want to filter using an OR instead of an AND. In that case, I want a single lambda expression. I am simply combining the lambda bodies using Expression.Or.

The problem is the ParameterExpression is different for each expression tree passed to the AddFilterMapping. I get errors along the lines of "The parameter 'e' was not bound in the LINQ to Entity expression." I am pretty sure it is because they are completely different parameter expressions, even if they have the same name.

Is there a way to make sure the same ParameterExpression is shared across all of my lambda expressions?


Solution

  • You can use the following method to replace all instances of one expression with another:

    public static Expression Replace(this Expression expression,
        Expression searchEx, Expression replaceEx)
    {
        return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
    }
    internal 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);
        }
    }
    

    Using this you can create one parameter expression to use for your lambda and then replace all instance of the parameter in the bodies of the lambdas that you have with the new parameter that you create.