2
\$\begingroup\$

I am creating a way for users to filter results based on start and end values. An example of this would be:

?RangeColumn=DisplayId&RangeStart=640,830,670&RangeEnd=650,850,680

In this case, the api would return items where the "DisplayId" is between 640-650 or 830-850 or 670-680. My solution works but the query looks pretty bad:

{t => ((((t.DisplayId >= { Item = 640 }.Item) AndAlso (t.DisplayId <= { Item = 650 }.Item)) OrElse ((t.DisplayId >= { Item = 830 }.Item) AndAlso (t.DisplayId <= { Item = 850 }.Item))) OrElse ((t.DisplayId >= { Item = 670 }.Item) AndAlso (t.DisplayId <= { Item = 680 }.Item)))}

The user can have an unlimited number of ranges so this could quickly get crazy really fast so I'm wondering if there is a better way for me to do what I am trying to do. Here are the methods that are relevant to the problem:

private static IQueryable<T> FilterOnRange<T>(Type entityType, RangeProperties rangeProperties, IQueryable<T> query)
    {
        var startCount = rangeProperties.RangeStart.Count();
        var endCount = rangeProperties.RangeEnd.Count();

        if (startCount > 0 || endCount > 0)
        {
            if (startCount != endCount && (startCount > 1 || endCount > 1))
                throw new RangeFilterException();
        }
        else
            return query;

        PropertyInfo pi = GetFilterPropertyInfo(entityType, rangeProperties.RangeColumnName);

        if (pi != null)
        {
            bool propertyIsNullableType = pi.PropertyType.IsGenericType && pi.PropertyType.GetGenericTypeDefinition().Equals(typeof(Nullable<>));

            bool isInteger = pi.PropertyType.Equals(typeof(int)) || (propertyIsNullableType && Nullable.GetUnderlyingType(pi.PropertyType).Equals(typeof(int)));
            bool isDateTimeOffset = pi.PropertyType.Equals(typeof(DateTimeOffset)) || (propertyIsNullableType && Nullable.GetUnderlyingType(pi.PropertyType).Equals(typeof(DateTimeOffset)));

            var maxCount = Math.Max(startCount, endCount);
            ParameterExpression argParam = Expression.Parameter(typeof(T), "t");
            if (isInteger)
            {
                if (maxCount == 1)
                {
                    string start = startCount > 0 ? rangeProperties.RangeStart.Single() : null;
                    string end = endCount > 0 ? rangeProperties.RangeEnd.Single() : null;
                    var lambda = RangeInt<T>(argParam, start, end, rangeProperties.RangeColumnName, propertyIsNullableType);
                    query = query.Where(lambda);
                }
                else
                {
                    Expression<Func<T, bool>> outerLambda = null;
                    for (int i = 0; i < maxCount; i++)
                    {
                        string start = rangeProperties.RangeStart[i];
                        string end = rangeProperties.RangeEnd[i];
                        var lambda1 = RangeInt<T>(argParam, start, end, rangeProperties.RangeColumnName, propertyIsNullableType);

                        Expression<Func<T, bool>> tempLambda = null;
                        i++;
                        if (i >= maxCount)
                        {
                            tempLambda = lambda1;
                        }
                        else
                        {
                            start = rangeProperties.RangeStart[i];
                            end = rangeProperties.RangeEnd[i];
                            var lambda2 = RangeInt<T>(argParam, start, end, rangeProperties.RangeColumnName, propertyIsNullableType);

                            var body = Expression.OrElse(lambda1.Body, lambda2.Body);
                            tempLambda = Expression.Lambda<Func<T, bool>>(body, lambda1.Parameters[0]);
                        }

                        if (outerLambda == null)
                            outerLambda = tempLambda;
                        else
                        {
                            var body = Expression.OrElse(outerLambda.Body, tempLambda.Body);
                            outerLambda = Expression.Lambda<Func<T, bool>>(body, outerLambda.Parameters[0]);
                        }
                    }

                    query = query.Where(outerLambda);
                }
            }
            else if (isDateTimeOffset)
            {
                if (maxCount == 1)
                {
                    string start = startCount > 0 ? rangeProperties.RangeStart.Single() : null;
                    string end = endCount > 0 ? rangeProperties.RangeEnd.Single() : null;
                    var lambda = RangeDateTime<T>(argParam, start, end, rangeProperties.RangeColumnName, propertyIsNullableType);
                    query = query.Where(lambda);
                }
                else
                {
                    Expression<Func<T, bool>> outerLambda = null;
                    for (int i = 0; i < maxCount; i++)
                    {
                        string start = rangeProperties.RangeStart[i];
                        string end = rangeProperties.RangeEnd[i];
                        var lambda1 = RangeDateTime<T>(argParam, start, end, rangeProperties.RangeColumnName, propertyIsNullableType);

                        Expression<Func<T, bool>> tempLambda = null;
                        i++;
                        if (i >= maxCount)
                        {
                            tempLambda = lambda1;
                        }
                        else
                        {
                            start = rangeProperties.RangeStart[i];
                            end = rangeProperties.RangeEnd[i];
                            var lambda2 = RangeDateTime<T>(argParam, start, end, rangeProperties.RangeColumnName, propertyIsNullableType);

                            var body = Expression.OrElse(lambda1.Body, lambda2.Body);
                            tempLambda = Expression.Lambda<Func<T, bool>>(body, lambda1.Parameters[0]);
                        }

                        if (outerLambda == null)
                            outerLambda = tempLambda;
                        else
                        {
                            var body = Expression.OrElse(outerLambda.Body, tempLambda.Body);
                            outerLambda = Expression.Lambda<Func<T, bool>>(body, outerLambda.Parameters[0]);
                        }
                    }

                    query = query.Where(outerLambda);
                }
            }
            else
                throw new InvalidOperationException("Only Datetime and Integer is supported for a range filter.");
        }

        return query;
    }

    private static Expression<Func<T, bool>> RangeInt<T>(ParameterExpression argParam, string start, string end, string columnName, bool propertyIsNullableType)
    {
        int? rangeStart = null;
        int tempRangeStart = int.MinValue;

        if (int.TryParse(start, out tempRangeStart))
            rangeStart = tempRangeStart;
        else
            rangeStart = int.MinValue;

        int? rangeEnd = null;
        int tempRangeEnd = int.MaxValue;

        if (int.TryParse(end, out tempRangeEnd))
            rangeEnd = tempRangeEnd;
        else
            rangeEnd = int.MaxValue;

        if (rangeEnd < rangeStart)
            throw new InvalidOperationException("RangeEnd must be greater than RangeStart.");

        Expression rangeProperty = Expression.Property(argParam, columnName);

        MemberExpression comparisonValue2 = null;
        if (propertyIsNullableType)
        {
            var item = new { Item = (int?)rangeStart };
            var comparisonValue = Expression.Constant(item);
            comparisonValue2 = Expression.MakeMemberAccess(comparisonValue, item.GetType().GetMember("Item").First());
        }
        else
        {
            var item = new { Item = (int)rangeStart };
            var comparisonValue = Expression.Constant(item);
            comparisonValue2 = Expression.MakeMemberAccess(comparisonValue, item.GetType().GetMember("Item").First());
        }
        Expression e1 = Expression.GreaterThanOrEqual(rangeProperty, comparisonValue2);

        MemberExpression comparisonValue4 = null;
        if (propertyIsNullableType)
        {
            var item2 = new { Item = (int?)rangeEnd };
            var comparisonValue3 = Expression.Constant(item2);
            comparisonValue4 = Expression.MakeMemberAccess(comparisonValue3, item2.GetType().GetMember("Item").First());
        }
        else
        {
            var item2 = new { Item = (int)rangeEnd };
            var comparisonValue3 = Expression.Constant(item2);
            comparisonValue4 = Expression.MakeMemberAccess(comparisonValue3, item2.GetType().GetMember("Item").First());
        }
        Expression e2 = Expression.LessThanOrEqual(rangeProperty, comparisonValue4);

        Expression e3 = Expression.AndAlso(e1, e2);

        return Expression.Lambda<Func<T, bool>>(e3, argParam);
    }

    private static Expression<Func<T, bool>> RangeDateTime<T>(ParameterExpression argParam, string start, string end, string columnName, bool propertyIsNullableType)
    {
        DateTimeOffset? rangeStart = null;
        DateTimeOffset tempRangeStart = DateTimeOffset.MinValue;

        if (DateTimeOffset.TryParse(start, out tempRangeStart))
            rangeStart = tempRangeStart;
        else
            rangeStart = DateTimeOffset.MinValue;

        DateTimeOffset? rangeEnd = null;
        DateTimeOffset tempRangeEnd = DateTimeOffset.MaxValue;

        if (DateTimeOffset.TryParse(end, out tempRangeEnd))
            rangeEnd = tempRangeEnd;
        else
            rangeEnd = DateTimeOffset.MaxValue;

        if (rangeEnd < rangeStart)
            throw new InvalidOperationException("RangeEnd must be greater than RangeStart.");

        Expression rangeProperty = Expression.Property(argParam, columnName);

        MemberExpression comparisonValue2 = null;
        if (propertyIsNullableType)
        {
            var item = new { Item = (DateTimeOffset?)rangeStart };
            var comparisonValue = Expression.Constant(item);
            comparisonValue2 = Expression.MakeMemberAccess(comparisonValue, item.GetType().GetMember("Item").First());
        }
        else
        {
            var item = new { Item = (DateTimeOffset)rangeStart };
            var comparisonValue = Expression.Constant(item);
            comparisonValue2 = Expression.MakeMemberAccess(comparisonValue, item.GetType().GetMember("Item").First());
        }
        Expression e1 = Expression.GreaterThanOrEqual(rangeProperty, comparisonValue2);

        MemberExpression comparisonValue4 = null;
        if (propertyIsNullableType)
        {
            var item2 = new { Item = (DateTimeOffset?)rangeEnd };
            var comparisonValue3 = Expression.Constant(item2);
            comparisonValue4 = Expression.MakeMemberAccess(comparisonValue3, item2.GetType().GetMember("Item").First());
        }
        else
        {
            var item2 = new { Item = (DateTimeOffset)rangeEnd };
            var comparisonValue3 = Expression.Constant(item2);
            comparisonValue4 = Expression.MakeMemberAccess(comparisonValue3, item2.GetType().GetMember("Item").First());
        }
        Expression e2 = Expression.LessThanOrEqual(rangeProperty, comparisonValue4);

        Expression e3 = Expression.AndAlso(e1, e2);

        return Expression.Lambda<Func<T, bool>>(e3, argParam);
    }
\$\endgroup\$
  • \$\begingroup\$ Why do you care that the expression tree looks bad? \$\endgroup\$ – svick Feb 26 '16 at 19:52
  • \$\begingroup\$ I just worry about performance. I don't really care how it looks as long as it performs the same I guess. \$\endgroup\$ – GBreen12 Feb 26 '16 at 20:01
  • \$\begingroup\$ Then you should look at the generated SQL, not the expression tree, that's what affects performance. Also, have you measured the performance? Does it meet your performance goals? \$\endgroup\$ – svick Feb 26 '16 at 20:06
2
\$\begingroup\$
var item = new { Item = (int?)rangeStart };
var comparisonValue = Expression.Constant(item);
comparisonValue2 = Expression.MakeMemberAccess(comparisonValue, item.GetType().GetMember("Item").First());

Creating the anonymous object and then accessing it in the expression doesn't give you anything. You can just use the value directly:

comparisonValue = Expression.Constant(rangeStart, typeof(int?));
\$\endgroup\$
  • \$\begingroup\$ If I change the line to Expression e1 = Expression.GreaterThanOrEqual(rangeProperty, Expression.Constant(rangeStart)); It throws an error if the field is nullable \$\endgroup\$ – GBreen12 Feb 26 '16 at 20:09
  • \$\begingroup\$ In that case, I think Expression.Constant(rangeStart, typeof(int?)) should work. \$\endgroup\$ – svick Feb 26 '16 at 20:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.