NML

LINQ OrderBy – A quick tip

By

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):

  1. The difference between OrderBy and ThenBy
  2. Ordering by a boolean

Here’s a scenario I recently came across:

The data below needed be sorted as follows:

  1. date
  2. transaction type
  3. 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:

  1. 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.
  2. 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)
  3. 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!