Introduction:
As software developers, we often find ourselves using LINQ methods to manipulate data with ease and fluency. However, when it comes to sorting data, we encounter a small inconvenience. The methods for ordering data in ascending and descending order in LINQ are different, and we have to resort to using if statements with OrderBy and OrderByDescending calls. In this blog post, we’ll explore a custom solution that allows us to simplify the sorting process by writing a method that takes the order as a parameter, providing greater flexibility and readability to our code.
The Limitation of Default LINQ Sorting:
By default, LINQ provides separate methods for ordering data in ascending (OrderBy) and descending (OrderByDescending) order. This can lead to repetitive code and reduced code readability when we need to dynamically determine the sorting order based on conditions. The common approach involves using conditional statements to choose between the two methods, resulting in less elegant code.
Lets take a sample code snippet below that I am seeing a lot.
var users = new List<User>();
if(model.AscendingDirection)
{
List<User> sortedUsers = _dbContext.AspUser
.Include(u => u.Groups)
.OrderBy(u => u.FirstName)
.ToList()
users.AddRange(sortedUsers);
}
else
{
List<User> sortedUsers = _dbContext.AspUser
.Include(u => u.Groups)
.OrderByDescending(u => u.FirstName)
.ToList()
users.AddRange(sortedUsers);
}
return users;
The issue with the above code is that its WET not DRY see(https://en.wikipedia.org/wiki/Don%27t_repeat_yourself ).
In software development DRY stands for “Don’t Repeat Yourself.” while WET stands for “We Enjoy Typing.” ( I know this just sounds like some bad “dad joke”. Hence, I am including Wikipedia link just to let you know that I am not making this up. (smile) )
We want our code DRY.(i.e without duplication). This means among other things, if we have to change code in the future, we make changes in minimal place(s). So, yeah I am talking about that users.AddRange(sortedUsers); call that needs to out of if/else block before return. but I am not just talking about that.
What do I mean?
Well there are some logic that is duplicated while generating query for sortedUsers in both if and else block. (Include and then include).
Well what can we do here?
At the very least we can do this.
var userQuery = _dbContext.AspUser
.Include(u => u.Groups);
if(model.AscendingDirection)
{
userQuery = userQuery.OrderBy(u => u.FirstName)
}
else
{
userQuery = userQuery.OrderByDescending(u => u.FirstName)
}
var users = userQuery.ToList();
Well, I “DRY”ied my code. But is this the best we can do? Can we do better?
Yes we can. We all love our linq methods and its fluentness. But the problem in this case is that the method to order by ascending and descending are different. Or We do not have a boolean flag that we can set to make it order by ascending vs descending. That is why we are forced to use that ugly if statement with OrderBy and OrderByDescending calls.
But Hey!! we are professional software developers. Aren’t we? We write big custom solutions for our client and work hard for them. Can’t we, for once, work hard for ourselves and write that method that takes order as parameter and sort it ourselves?
lets try that. Here are some of my examples.
Public static class QueryableExtensions
{
//flag with boolean
public static IOrderedQueryable<TSource> OrderByWithDirection<TSource,TKey>(
this IQueryable<TSource> source,
Expression<Func<TSource, TKey>> keySelector,
bool ascending = true
)
{
return ascending ? source.OrderBy(keySelector)
: source.OrderByDescending(keySelector);
}
//OR
//string as flag
public static IOrderedQueryable<TSource> OrderByWithDirection<TSource,TKey>(
this IQueryable<TSource> source,
Expression<Func<TSource, TKey>> keySelector,
string order = "asc"
)
{
order = order.ToLower();
if (order != "asc" && order != "desc")
throw new ArgumentException($"{nameof(order)} can be either 'asc' or 'desc'.");
return source.OrderByWithDirection(keySelector, order == "asc");
}
}
Ok now using that extension method. we can rewrite our code like this.
return _dbcontext.AspUser
.Include(u => u.Groups);
.OrderByWithDirection(u => u.FirstName, model.AscendingDirection)
.ToList();
Not only did we made our code shorter and easier to understand, we now have a reusable extension method that can be used in many other places. So WIN! WIN!!
BONUS
Why stop there? Well good I solved that problem but what if I want to sort by multiple column. You might remember that there are 2 other methods that we need to call if we want to sort by more than one column. “ThenBy” https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.thenby?view=netcore-2.2 and “ThenByDescending” https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.thenbydescending?view=netcore-2.2
Well we can write another set of extension methods for those scenarios. The method looks very much similar to the above method, the only difference is that we need to extend that method on IOrderedQueryable<T> instead on IQueryable<T>. You can try it ourself. hint: following will be your method signature for first version.
public static IOrderedQueryable<TSource> ThenByWithDirection<TSource,TKey>(
this IOrderedQueryable<TSource> source,
Expression<Func<TSource, TKey>> keySelector,
bool ascending = true
)
What else?
Well I see that we have to pass in lamda expression to the linq method.
Like OrderBy(c => c.SomeProperty). However, if I want to control sorting from UI, I will probably end up getting string or column number from UI. Can we make the method work By string.
Yes Absolutly. Following extension method will give you lamda expression to select property from poco object.
public static Expression<Func<T, object>> PropertySelectorFrom<T>(this string propertyName)
{
ParameterExpression parameter = Expression.Parameter(typeof(T));
UnaryExpression body = Expression.Convert(
Expression.PropertyOrField(parameter, propertyName), typeof(object));
return Expression.Lambda<Func<T, object>>(body, parameter);
}
This is how we use it.
// assuming model.SortColumn = "FirstName"
return _dbContext.AspUser
.Include(u => u.Groups);
.OrderByWithDirection(model.SortColumn.PropertySelectorFrom<User>(), model.AscendingDirection)
.ToList();
The model.SortColumn.PropertySelectorFrom<User>() is equivalent to u => u.FirstName lamda expression but generated dynamically.
I have a little complex scenario. What if I have multiple order column and direction that comes as string from UI. example: my model is.
var model = new
{
OrderBy = new[] { "Email" , "FirstName", "LastName" },
OrderDirection = new[] { "asc" , "desc" , "asc" }
};
We can write following extension method.
public static IOrderedQueryable<TSource> ChainedOrderBy<TSource>(
this IQueryable<TSource> source, string [] orderBy, string [] orderDirection)
{
if (orderBy == null)
throw new ArgumentNullException(nameof(orderBy));
if (orderDirection == null)
throw new ArgumentNullException(nameof(orderDirection));
if (!orderBy.Any())
throw new ArgumentException($"{nameof(orderBy)} should not be empty.");
if (!orderDirection.Any())
throw new ArgumentException($"{nameof(orderDirection)} should not be empty.");
if (orderBy.Length != orderDirection.Length)
throw new ArgumentException(
$"size of {nameof(orderBy)} and {nameof(orderDirection)} must be equal.");
var orderSettings = orderBy
.Zip(orderDirection, (o, d) => new {orderBy = o, orderDirection = d})
.ToArray();
return orderSettings
.Skip(1)
.Aggregate(
source.OrderByWithDirection(
orderSettings[0].orderBy.PropertySelectorFrom<TSource>(),
orderSettings[0].orderDirection
),
(current, orderSetting) =>
current.ThenByWithDirection(
orderSetting.orderBy.PropertySelectorFrom<TSource>(),
orderSetting.orderDirection
)
);
}
then our query can be.
return _Dbcontext.AspUser
.Include(u => u.Groups);
.ChainedOrderBy(model.OrderBy, model.OrderDirection)
.ToList();
Well thats all!!
Conclusion:
LINQ is a powerful tool for data manipulation in C#, but it can sometimes introduce challenges when it comes to sorting data dynamically. By creating a custom extension method that handles the sorting order based on a parameter, we can simplify our code, enhance readability, and eliminate the need for repetitive if statements. With this custom solution, we regain control over our code and optimize our development experience.