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);
 }
asked Feb 26, 2016 at 16:57
\$\endgroup\$
3
  • \$\begingroup\$ Why do you care that the expression tree looks bad? \$\endgroup\$ Commented Feb 26, 2016 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\$ Commented Feb 26, 2016 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\$ Commented Feb 26, 2016 at 20:06

1 Answer 1

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?));
answered Feb 26, 2016 at 19:57
\$\endgroup\$
2
  • \$\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\$ Commented Feb 26, 2016 at 20:09
  • \$\begingroup\$ In that case, I think Expression.Constant(rangeStart, typeof(int?)) should work. \$\endgroup\$ Commented Feb 26, 2016 at 20:11

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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.