Reputation: 1764
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
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