Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trying to create a morethan, equal or greaterthan dynamic filter for dates in linq

Tags:

c#

lambda

linq

I have been trying to create a expression tree filter for Linq that take 2 dates and a string of possible values { "lessthan", "equals", "morethan" }. I wish to format the call to be something like Query.Where(CompareDates(x => x.left, right, "less than"));

I have the code:

        public static IQueryable<TSource> CompareDates<TSource>(
            this IQueryable<TSource> source, 
            Expression<Func<TSource, DateTime?>> left, 
            DateTime? right, 
            string equality)
        {
            if (right == null || string.IsNullOrWhiteSpace(equality))
                return source;

            var p = left.Parameters.Single();
            Expression member = p;

            Expression leftExpression = Expression.Property(member, "left");
            Expression rightParameter = Expression.Constant(right, typeof(DateTime));

            BinaryExpression BExpression = null;
            switch (equality)
            {
                case "lessthan":
                    BExpression = Expression.LessThan(leftExpression, rightParameter);
                    break;
                case "equal":
                    BExpression = Expression.Equal(leftExpression, rightParameter);
                    break;
                case "morethan":
                    BExpression = Expression.GreaterThan(leftExpression, rightParameter);
                    break;
                default:
                    throw new Exception(String.Format("Equality {0} not recognised.", equality));
            }

            return source.Where(Expression.Lambda<Func<TSource, bool>>(BExpression, p));
        }

Unfortunately it is producing an error of "System.ArgumentException: Instance property 'left' is not defined for type 'Model' at System.Linq.Expressions.Expression.Property(Expression expression, String propertyName) at SARRestAPI.Extensions.Expressions.CompareDates[TSource](IQueryable1 source, Expression1 src, DateTime supplied, String equality)"

Anyone have an ideas why this is happening?

like image 540
David Hubner Avatar asked Jul 17 '19 09:07

David Hubner


1 Answers

Here we go; what you want to do is use the .Body of the incoming selector, not look for .left. Meaning, given an input selector of x => x.Foo.Bar.Blap, a constant, and a comparison, you want to construct something like x => x.Foo.Bar.Blap < someValue, by reusing both the x (the parameter, which you're already doing) and the body (x.Foo.Bar.Blap).

In code (note this works for any TValue, not just DateTime):

public enum Comparison
{
    Equal,
    NotEqual,
    LessThan,
    LessThanOrEqual,
    GreaterThan,
    GreaterThanOrEqual
}
public static IQueryable<TSource> Compare<TSource, TValue>(
        this IQueryable<TSource> source,
        Expression<Func<TSource, TValue>> selector,
        TValue value,
        Comparison comparison)
{
    Expression left = selector.Body;
    Expression right = Expression.Constant(value, typeof(TValue));

    BinaryExpression body;
    switch (comparison)
    {
        case Comparison.LessThan:
            body = Expression.LessThan(left, right);
            break;
        case Comparison.LessThanOrEqual:
            body = Expression.LessThanOrEqual(left, right);
            break;
        case Comparison.Equal:
            body = Expression.Equal(left, right);
            break;
        case Comparison.NotEqual:
            body = Expression.NotEqual(left, right);
            break;
        case Comparison.GreaterThan:
            body = Expression.GreaterThan(left, right);
            break;
        case Comparison.GreaterThanOrEqual:
            body = Expression.GreaterThanOrEqual(left, right);
            break;
        default:
            throw new ArgumentOutOfRangeException(nameof(comparison));
    }

    return source.Where(Expression.Lambda<Func<TSource, bool>>(body, selector.Parameters));
}

Example usage (here using LINQ-to-Objects, but it should work for other LINQ backends, too):

var arr = new[] { new { X = 11 }, new { X = 12 }, new { X = 13 }, new { X = 14 } };
var source = arr.AsQueryable();

var filtered = source.Compare(x => x.X, 12, Comparison.GreaterThan);
foreach (var item in filtered)
{
    Console.WriteLine(item.X); // 13 and 14
}

Note that with C# vCurrent you can do:

var body = comparison switch
{
    Comparison.LessThan => Expression.LessThan(left, right),
    Comparison.LessThanOrEqual => Expression.LessThanOrEqual(left, right),
    Comparison.Equal => Expression.Equal(left, right),
    Comparison.NotEqual => Expression.NotEqual(left, right),
    Comparison.GreaterThan => Expression.GreaterThan(left, right),
    Comparison.GreaterThanOrEqual => Expression.GreaterThanOrEqual(left, right),
    _ => throw new ArgumentOutOfRangeException(nameof(comparison)),
};
return source.Where(Expression.Lambda<Func<TSource, bool>>(body, selector.Parameters));

which you might fine preferable.

like image 134
Marc Gravell Avatar answered Nov 11 '22 01:11

Marc Gravell