Generic multi-property sorting using LINQ and Reflection

Recently I was tasked with the creation of a summary grid for a list of data objects.  All of the objects would be retrieved when the form containing the list was loaded, and there would be a default multi-column sort applied to the list.  After the initial load, more items could be added to the list, and the current sort would need to be applied.

There was some discussion amongst the team about how and where to sort.  Do we perform our ordering in the SQL call?  Quick, but only works on that initial load.  Do we simply rely on the Infragistics grids we’re using?  They do multi-column sorting but as soon as you add multiple bands to the grid (for master/detail views) the sort no longer works, though why I couldn’t even begin to take a guess at.  That’s when I remembered the OrderBy and ThenBy LINQ extension methods provided in the 3.5 Framework.

OrderBy, ThenBy and their reverse order cousins, OrderByDescending and ThenByDescending all take lambda expressions to define their sort order.  Usually a property is chosen as the sort candidate and the line would look something like this:

dateList.OrderBy(d => d.Year);

However, we could be sorting by any property in our object and multiple properties to boot.  Creating a big list of OrderBy and ThenBy statements for each property would simply be madness.  A better method would be to have a string key for the column which matches the property name and use Reflection to identify the property.  A dictionary of keys and their matching sort direction could be iterated over to provide multi-property sorting by storing the IOrderedEnumerable<T> object returned from the initial OrderBy/Descending call to gain access to ThenBy/Descending.  After encapsulating the whole thing in its own class, I ended up with this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Reflection;

namespace ESC.SV.UI.Win.RATA.Helpers
{
	public static class ListSorter
	{
		public enum SortingOrder
		{
			Ascending,
			Descending,
		};

		public static IOrderedEnumerable<T> Sort<T>(IEnumerable<T> toSort, Dictionary<string, SortingOrder> sortOptions)
		{
			IOrderedEnumerable<T> orderedList = null;

			foreach (KeyValuePair<string, SortingOrder> entry in sortOptions)
			{
				if (orderedList != null)
				{
					if (entry.Value == SortingOrder.Ascending)
					{
						orderedList = orderedList.ApplyOrder<T>(entry.Key, "ThenBy");
					}
					else
					{
						orderedList = orderedList.ApplyOrder<T>(entry.Key, "ThenByDescending");
					}
				}
				else
				{
					if (entry.Value == SortingOrder.Ascending)
					{
						orderedList = toSort.ApplyOrder<T>(entry.Key, "OrderBy");
					}
					else
					{
						orderedList = toSort.ApplyOrder<T>(entry.Key, "OrderByDescending");
					}
				}
			}

			return orderedList;
		}

		private static IOrderedEnumerable<T> ApplyOrder<T>(this IEnumerable<T> source, string property, string methodName)
		{
			ParameterExpression param = Expression.Parameter(typeof(T), "x");
			Expression expr = param;
			foreach (string prop in property.Split('.'))
			{
				// use reflection (not ComponentModel) to mirror LINQ
				expr = Expression.PropertyOrField(expr, prop);
			}
			Type delegateType = typeof(Func<,>).MakeGenericType(typeof(T), expr.Type);
			LambdaExpression lambda = Expression.Lambda(delegateType, expr, param);

			MethodInfo mi = typeof(Enumerable).GetMethods().Single(
					method => method.Name == methodName
							&& method.IsGenericMethodDefinition
							&& method.GetGenericArguments().Length == 2
							&& method.GetParameters().Length == 2)
					.MakeGenericMethod(typeof(T), expr.Type);
			return (IOrderedEnumerable<T>)mi.Invoke(null, new object[] { source, lambda.Compile() });
		}
	}
}

Edit: Thanks to Karl for pointing out earlier that the original version didn’t work.  The values of the properties being sorted by were not getting extracted through the lambda expression. I have updated the code above to fix this issue and extended the functionality to include nested property sorting, so you can sort on something like “Name.FirstName” for an object. Thanks to this post for pointing me in the right direction.  I had to call .Compile()  on the lambda expression before I wouldn’t receive a runtime error.

Advertisements
Explore posts in the same categories: .NET, C#, LINQ

7 Comments on “Generic multi-property sorting using LINQ and Reflection”

  1. Karl Shea Says:

    Except that it doesn’t work. Copied it directly, and the list it’s returning is in the original state.

  2. omeganot Says:

    Yes, my apologies. Took me forever to sort it out, because simply retrieving the value of the property through GetValue() didn’t sort quite right. Try the new code!

  3. Karl Shea Says:

    That’s great, works perfectly 🙂

    I was working on it a bit yesterday and was headed in the right direction, but starting to get stumped. Now I see where I was going wrong. Thanks for the code!

  4. Victor Says:

    Very nice post, great job!

  5. Jonathan Says:

    If you use the Queryable type’s Ordering methods, you don’t have to compile your lambda. … see snippet below (hope it formats well):

    MethodInfo mi = typeof(Queryable).GetMethods().Single(
    method => method.Name == methodName
    && method.IsGenericMethodDefinition
    && method.GetGenericArguments().Length == 2
    && method.GetParameters().Length == 2)
    .MakeGenericMethod(typeof(T), expr.Type);
    return (IOrderedQueryable)mi.Invoke(null, new object[] { source, lambda });

    • Rich Says:

      Very interesting, I had not tried that before. I’ll have to check, but it makes me wonder if the lambda would require compilation anyway, since the format is identical other than the type changing from IOrderedEnumerable to IOrderedQueryable. However, that there is a major change in itself, you’re no longer returning an IOrderedEnumerable.

  6. black tea Says:

    Hello my friend! I wish to say that this post is awesome, great
    written and come with almost all important infos. I would like to look extra posts like
    this .


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


%d bloggers like this: