If you’ve used LINQ you know that it’s very powerful and with a few lines of code you can achieve your goal in a concise and maintainable way. However, you can also easily create a bug if you don’t fully understand how it’s meant to work.
The aim of this post is to talk about 2 things (they’re simple concepts, but if you’re not aware of them, this should be useful):
- The difference between OrderBy and ThenBy
- Ordering by a boolean
Here’s a scenario I recently came across:
The data below needed be sorted as follows:
- date
- transaction type
- transaction detail type
Pretty straight forward using LINQ right? With one catch. All Return: Exchange transactions need to be displayed beneath any other Return transaction.
Current unsorted data:
Date | Transaction type | Transaction Detail Type |
---|---|---|
2015/05/22 | Purchase | Customer sale |
2015/05/22 | Return | Return / Exchange |
2015/05/21 | Purchase | Customer sale |
2015/05/22 | Return | Return / Partial |
2015/05/23 | Return | Return / Full |
2015/05/22 | Return | Return / Full |
Expected sort results:
Date | Transaction type | Transaction Detail Type |
---|---|---|
2015/05/21 | Purchase | Customer sale |
2015/05/22 | Purchase | Customer sale |
2015/05/22 | Return | Return / Full |
2015/05/22 | Return | Return / Partial |
2015/05/22 | Return | Return / Exchange |
2015/05/23 | Return | Return / Full |
The existing code does the following: Use multiple OrderBy statements and create a custom sort method that will manually sort the list and place Return: Exchange
before any other Return transaction.
var results = results.OrderBy(a => a.TransDate)
.OrderBy(a => a.TransType)
.OrderBy(a => TransactionDetailCustomSorting(a.TransDetailType))
.ToList();
private int TransactionDetailCustomSorting(string transType)
{
if (transType == null)
transType = "";
if (IsTransTypeOfType(transType, "Return: Full"))
return 0;
if (IsTransTypeOfType(transType, "Return: Partial"))
return 1;
if (IsTransTypeOfType(transType, "Return: Exchange"))
return 2;
return 3;
}
private bool IsTransTypeOfType(string transType, string type)
{
return transType.ToLowerInvariant().Contains(type);
}
At first glance, this looks like it would work, but there are a few problems with this approach:
- Chaining multiple OrderBy statements doesn’t work as expected. If you call OrderBy multiple times, it will effectively reorder the sequence completely each time, so the final call will effectively be the dominant one. This is how OrderBy was designed to work.
- By manually ordering the return items, you are bumping them to the top of the list i.e. you are in effect overwriting the previous sort order (date, transaction type, transaction detail type)
- It’s clunky
The proposed solution:
First we order the results according to our criteria:
var results = results.OrderBy(r => r.TransDate)
.ThenBy(r => r.TransType)
.ThenBy(r => r.TransDetailType)
.ToList();
Whilst reading up on OrderBy, I stumbled across ThenBy. ThenBy maintains the current sort order and builds on it, so chaining works as expected here.
To help you understand the difference between OrderBy and ThenBy:
myList.OrderBy(a).OrderBy(b).OrderBy(c)
is equivalent to
myList.OrderBy(c).ThenBy(b).ThenBy(a)
OrderBy provides the “most important” ordering projection. ThenBy is used to specify secondary/additional ordering projections. Think of it this way: OrderBy(…).ThenBy(…).ThenBy(…) allows you to create a single composite comparison for any two objects, and then sort the sequence once using that composite comparison.
Ok, so now that the data is sorted, how do we push all Return: Exchange
transactions below other Return transactions?
We sort by Boolean.
var results = results.OrderBy(r => r.TransDate)
.ThenBy(r => r.TransType)
.ThenBy(r => r.TransDetailType.ToLower() == "return: exchange")
.ThenBy(r => r.TransDetailType)
.ToList();
This will place all items that evaluate to false before the ones that evaluate to true (False comes before True). This means Return: Exchange
items will be placed after other return items.
We’ve managed to eliminate manual sorting, and a few unnecessary methods and replace them with a single, concise, readable LINQ statement which does exactly what we want. Pretty neat!