4

I need to reduce an expression

Expression<Func<TQueryResult, TParam, bool>>

to

Expression<Func<TQueryResult, bool>>

by injecting TParam value as a constant into the expression.


Concrete example:

protected IQueryable<TQueryResult> AddQueryFilter<TQueryResult, TParam>(IQueryable<TQueryResult> query, Expression<Func<TQueryResult, TParam, bool>> exp,  TParam param)
{
    object obj = param;

    if (obj is string)
    {
        if (!string.IsNullOrWhiteSpace((string) obj))
        {
            var reducedExp = new Expression<Func<TQueryResult, bool>>()
            // ...
            // the magic that I need to inject param value
            //..
            return query.Where(reducedExp);
        }
    }
    else if (obj is DateTime)
    {
        //... return query.Where(reducedExp); 
    }
    else
        throw new InvalidOperationException("Param type not supported");

    return query;
}

//usage

var qr = Manager.Invoices.Query;
qr = AddQueryFilter(qr, (invoice, value) => value == invoice.Number, numberEdit.Text);
qr = AddQueryFilter(qr, (invoice, value) => value == invoice.Date, dateEdit.Date);
qr = AddQueryFilter(qr, (invoice, value) => invoice.Description.Contains(value), descEdit.Text);            
Boris B.
  • 4,933
  • 1
  • 28
  • 59

2 Answers2

2

Try:

protected static IQueryable<TQueryResult> AddQueryFilter<TQueryResult, TParam>(
    IQueryable<TQueryResult> query, Expression<Func<TQueryResult, TParam, bool>> exp, TParam param)
{

    var rewriter = new ExpressionRewriter();
    rewriter.Subst(exp.Parameters[1], Expression.Constant(param, typeof(TParam)));
    var body = rewriter.Apply(exp.Body);
    var lambda = Expression.Lambda<Func<TQueryResult, bool>>(body, exp.Parameters[0]);
    return query.Where(lambda);
}

using ExpressionRewriter from this answer.

Community
  • 1
  • 1
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • It works for `invoice.Number == value`; also works for `invoice.Number.ToLower() == value.ToLower()`; but does not work for `invoice.Number.Contains(value)`, it fails in ExpressionRewriter.Walk { case ExpressionType.Call: } - "Static method requires null instance, non-static method requires non-null instance. Parameter name: method" – Boris B. Feb 01 '11 at 13:24
  • @Boris - odd; that isn't a static method - I can repro, will investigate a little later – Marc Gravell Feb 01 '11 at 14:08
  • @Boris - found and fixed; see the change to `Walk` around `ExpressionType.Parameter` – Marc Gravell Feb 01 '11 at 14:23
  • Thank you, it seems to work now. I was hoping to understand the solution to this, but this seems somewhat over my head :) – Boris B. Feb 01 '11 at 15:20
  • @Boris expression trees take a bit of brain-ache to appreciate, for sure. Very powerful when you grok them fully, though – Marc Gravell Feb 01 '11 at 15:21
0

I hope somebody still can be searching for this topic, as me, in fact, so I'd like to suggest the following possibility.

Since .NET 4.0 has been released, you can use built-in expression tree visitors.

Here's an exapmple, which implements required functionality:

private class ExpressionConstantInjector<T, TConstant> : ExpressionVisitor
{
    private readonly TConstant toInject;
    private readonly ParameterExpression targetParam;

    public EntityExpressionListInjector(TConstant toInject)
    {
        this.toInject = toInject;
        targetParam = Expression.Parameter(typeof(T), "a");
    }

    public Expression<Func<T, bool>> Inject(Expression<Func<T, TConstant, bool>> source)
    {
        return (Expression<Func<T, bool>>) VisitLambda(source);
    }

    protected override Expression VisitLambda<T1>(Expression<T1> node)
    {
        return Expression.Lambda(Visit(node.Body), targetParam);
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        if (node.Type == typeof (TConstant))
            return Expression.Constant(toInject);
        return targetParam;
    }
}

Usage:

Expression<Func<Entity, List<int>, bool>> expression = (e, ids) => ids.Contains(e.Id);

var filterExpression 
    = new ExpressionConstantInjector<Entity, List<int>>(new List<int>{1, 2, 3})
    .Inject(expression); 
// filterExpression is like a => (1, 2, 3).Contains(a.Id) 
// filterExpression can be passed to EF IQueryables.

This solution is very local, not really reusable, but quiet simple (nah).

To be honest, [].Contains(id) is the only case I've tested. But I think it works.

mazharenko
  • 565
  • 5
  • 11