Extragorey
Extragorey

Reputation: 1764

OrderBy expression tree with dynamic field name, prioritising non-null values

I'm writing an extension method that takes a property name as a string and builds an expression tree to order by that column. It needs to support Linq-to-Entities as I'd like the order-by operation to be done in SQL. The method currently works as intended, however I'd now like to prioritise non-null values, i.e. always leave null values at the end of the collection, regardless of the order direction.

Here's the method I have so far:

public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string orderBy, bool ascending) {
    Type type = typeof(T);
    ParameterExpression parameter = Expression.Parameter(type, "p");
    PropertyInfo property = type.GetProperty(orderBy);
    if (property == null) {
        throw new ArgumentException("The given value did not match any available properties.", nameof(orderBy));
    }

    // Get the property accessor p.SortColumn
    Expression propertyAccess = Expression.MakeMemberAccess(parameter, property);

    // Get the lambda expression p => p.SortColumn
    LambdaExpression orderByExp = Expression.Lambda(propertyAccess, parameter);

    // Call the OrderBy(Descending) method with the above lambda expression
    MethodCallExpression resultExp = Expression.Call(typeof(Queryable),
        ascending ? "OrderBy" : "OrderByDescending", new[] { type, property.PropertyType },
        source.Expression, Expression.Quote(orderByExp));

    return (IOrderedQueryable<T>)source.Provider.CreateQuery<T>(resultExp);
}

If I didn't need to support Linq-to-Entities, I'd write something like this:

source.OrderBy(x => property.GetValue(x) == null).ThenBy/Descending(x => property.GetValue(x));

However, the Expression.Call()... syntax has thrown me as I don't fully understand what's happening behind the scenes there.

How would I go about prepending an OrderBy(value == null) operation to the expression tree?

Upvotes: 0

Views: 859

Answers (1)

TheGeneral
TheGeneral

Reputation: 81493

Just order by equal to null first

var result = list.OrderBy(x => x.Name == null).ThenBy(x => x.Name)

Example

public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string propertyName, bool ascending)
{
   var propertyInfo = typeof(T).GetProperty(propertyName) ?? throw new ArgumentException("The given value did not match any available properties.", nameof(propertyName));

   var parameterExpression = Expression.Parameter(typeof(T), "p");
   var propertyAccess = Expression.MakeMemberAccess(parameterExpression, propertyInfo);

   var orderByNullLambda = Expression.Lambda(
      Expression.Equal(propertyAccess, Expression.Constant(null)),
      parameterExpression);

   var resultExp = Expression.Call(
      typeof(Queryable),
      ascending ? "OrderBy" : "OrderByDescending",
      new[] {typeof(T), typeof(bool)},
      source.Expression,
      Expression.Quote(orderByNullLambda));

   var orderByPropertyLambda = Expression.Lambda(
      propertyAccess, 
      parameterExpression);

   var resultExp2 = Expression.Call(
      typeof(Queryable),
      ascending ? "ThenBy" : "ThenByDescending",
      new[] {typeof(T), propertyInfo.PropertyType},
      resultExp,
      Expression.Quote(orderByPropertyLambda));

   return (IOrderedQueryable<T>) source.Provider.CreateQuery<T>(resultExp2);
}

Usage

var list = new List<Bob>()
{
   new() {Name = "Bsd"}, 
   new() {Name = null}, 
   new() {Name = "asd"}
};

var result = list.AsQueryable().OrderBy("Name", true).ToList();

foreach (var item in result)
   Console.WriteLine(item.Name ?? "Null");

Result

asd
Bsd
Null

Upvotes: 1

Related Questions