6

I have a DateRange class that I'd like to apply to an IQueryable as a where predicate, automatically using the begin and end dates and automatically using an open or closed interval.

public class DateRange
{
    public DateTime? BeginDate { get; set; }
    public DateTime? EndDate { get; set; }

    public bool BeginInclusive { get; set; }
    public bool EndInclusive { get; set; }

    public DateRange()
    {
        BeginInclusive = true;
        EndInclusive = false;
    }

    public IQueryable<T> Apply<T>( IQueryable<T> source, Expression<Func<T,DateTime>> dateField )
    {
        var result = source;
        if (BeginDate.HasValue)
        {
            if (BeginInclusive)
                result = result.Where( x => dateField >= BeginDate ); //does not compile
            else
                result = result.Where( x => dateField > BeginDate ); //does not compile
        }
        if (EndDate.HasValue)
        {
            if (EndInclusive)
                result = result.Where( x => dateField <= EndDate ); //does not compile
            else
                result = result.Where( x => dateField < EndDate ); //does not compile
        }
        return result;
    }
}

And I want to call it like this, DateField is any DateTime property of T.

DateRange d;
IQueryable<T> q;
q = d.Apply( q, x => x.DateField );

So I want to pass a member expression to the Apply method, and have it apply an appropriate where clause to the result set, but I cannot figure out how to get the dateField member expression embedded in the where predicate's expression. See lines "do not compile" in class above. I need to transform dateField somehow or build the predicate expression some other way, but I have no idea how to do so.

4
  • 1
    You'll have to hand-craft dateField >= BeginDate using Expression class methods. Commented Jul 21, 2016 at 15:10
  • I'm not sure how to do that. I don't work with the Expression class itself much. I've used LinqKit's PredicateBuilder, but it's not much help in this situation. The final 'where' predicate must work in Linq-to-Entities too. Commented Jul 21, 2016 at 15:12
  • How's this look: var expr = Expression.GreaterThanOrEqual( dateField.Body, Expression.Constant( BeginDate ) ); result = result.Where( Expression.Lambda<Func<T, bool>>( expr ) ); Commented Jul 21, 2016 at 15:20
  • I'm pretty sure you need to pass parameters to Expression.Lambda. Your code throws an exception when I try it. Check out my answer. Commented Jul 21, 2016 at 15:22

3 Answers 3

18

What you're looking to do here is to compose expressions; you're trying to apply one expression to the result of another. You can actually write a method to do that:

public static Expression<Func<TSource, TResult>> Compose<TSource, TIntermediate, TResult>(
    this Expression<Func<TSource, TIntermediate>> first,
    Expression<Func<TIntermediate, TResult>> second)
{
    var param = Expression.Parameter(typeof(TSource));
    var intermediateValue = first.Body.ReplaceParameter(first.Parameters[0], param);
    var body = second.Body.ReplaceParameter(second.Parameters[0], intermediateValue);
    return Expression.Lambda<Func<TSource, TResult>>(body, param);
}

It uses the following method to replace the parameter of an expression with an expression.

public static Expression ReplaceParameter(this Expression expression,
    ParameterExpression toReplace,
    Expression newExpression)
{
    return new ParameterReplaceVisitor(toReplace, newExpression)
        .Visit(expression);
}
public class ParameterReplaceVisitor : ExpressionVisitor
{
    private ParameterExpression from;
    private Expression to;
    public ParameterReplaceVisitor(ParameterExpression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }
    protected override Expression VisitParameter(ParameterExpression node)
    {
        return node == from ? to : node;
    }
}

This allows you to write your code as:

public IQueryable<T> Apply<T>(IQueryable<T> source, 
    Expression<Func<T, DateTime>> dateField)
{
    var result = source;
    if (BeginDate.HasValue)
    {
        if (BeginInclusive)
            result = result.Where(dateField.Compose(date => date >= BeginDate));
        else
            result = result.Where(dateField.Compose(date => date > BeginDate));
    }
    if (EndDate.HasValue)
    {
        if (EndInclusive)
            result = result.Where(dateField.Compose(date => date <= EndDate));
        else
            result = result.Where(dateField.Compose(date => date < EndDate));
    }
    return result;
}
Sign up to request clarification or add additional context in comments.

3 Comments

Thanks. You saved me hour or even more on implementing it myself. It's a shame BCL doesn't provide this method
thanks for answer ! I'm getting a pretty poor performances tho. Exp.Compose(x=>x*-1) : Around 25ms. Any reason for that ?
Never mind.... I had a very long diagnostic session, restarting project gets me to around 2ms
0

You'll have to hand-craft dateField >= BeginDate using Expression class methods.

(...)
    if (BeginInclusive)
    {
        var greaterOrEqual =
            Expression.Lambda<Func<T, bool>>(
                Expression.GreaterThanOrEqual(
                    dateField.Body,
                    Expression.Constant(BeginDate)),
                dateField.Parameters);

        result = result.Where(greaterOrEqual);
    }
(...)

Similarly for the other cases.

2 Comments

Awesome, that's exactly what I came up with. I'm going to post another answer with the final code too so people can just use it. I just need to confirm it all works with Linq-to-Entities as expected.
I am trying using IEnumerable<T> for my own learning, but i am unable to have it compile, can you see what i am misssing ? public IEnumerable<T> Apply<T>(IEnumerable<T> source, Func<IEnumerable<T>, DateTime, bool> dateField) { var result = source; return result.Where(x=>dateField(source,x)); }
0

Here's the updated Apply method created after figuring this out.

    public IQueryable<T> Apply<T>( IQueryable<T> source, Expression<Func<T,DateTime>> dateField )
    {
        Expression predicate;
        if (BeginDate.HasValue)
        {
            if (BeginInclusive)
                predicate = Expression.GreaterThanOrEqual( dateField.Body, Expression.Constant( BeginDate, typeof(DateTime) ) );
            else
                predicate = Expression.GreaterThan( dateField.Body, Expression.Constant( BeginDate, typeof(DateTime) ) );
            source = source.Where( Expression.Lambda<Func<T, bool>>( predicate ) );
        }
        if (EndDate.HasValue)
        {
            if (EndInclusive)
                predicate = Expression.LessThanOrEqual( dateField.Body, Expression.Constant( EndDate, typeof(DateTime) ) );
            else
                predicate = Expression.LessThan( dateField.Body, Expression.Constant( EndDate, typeof(DateTime) ) );
            source = source.Where( Expression.Lambda<Func<T, bool>>( predicate ) );
        }
        return source;
    }

Next, I'll transform it into an extension method, so it can be used like:

DateRange range;
IQueryable<T> q;
q = q.WhereInDateRange( range, x => x.DateField );

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.