Thursday, February 03, 2011

Dynamic sorting of lists using Lambda Expressions

Lambda expressions provide a simple method by which you can sort an IEnumerable object by just providing the property on which to sort via a string parameter.

Here is the code:

public static List<T> Sort<T>(IEnumerable<T> list, string sortField, SortDirection sortDirection)
{
var param = Expression.Parameter(typeof(T), string.Empty);

//normally one would use Expression.Property(param, sortField), but that doesnt work
//when working with interfaces where the sortField is defined on a base interface.
//so instead we search for the Property through our own GetProperty method and use it to build the
//Expression property
PropertyInfo propertyInfo = GetProperty(typeof (T), sortField);
var property = Expression.Property(param, propertyInfo);

var lambda = Expression.Lambda<Func<T, object>>(Expression.Convert(property, typeof(object)), param);

var returnList;
if (sortDirection == SortDirection.Ascending)
returnList = list.AsQueryable().OrderBy(lambda).ToList();
else
returnList = list.AsQueryable().OrderByDescending(lambda).ToList();

return returnList;
}

///
// Allows you to get the PropertyInfo for a property defined on the provided type
// Code allows you to find the property even from base interfaces (Type.GetProperty does not return
// properties from base interfaces
// if you wish to look for a specific property defined on a certain interface, then provide the
// propertyName as 'interface.propertyname'
///
private static PropertyInfo GetProperty(Type type, string propertyName)
{
string typeName = string.Empty;
if (propertyName.Contains("."))
{
//name was specified with typename - so pull out the typename
typeName = propertyName.Substring(0, propertyName.IndexOf("."));
propertyName = propertyName.Substring(propertyName.IndexOf(".")+1);
}

PropertyInfo prop = type.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public);
if (prop == null)
{
var baseTypesAndInterfaces = new List<Type>();
if (type.BaseType != null) baseTypesAndInterfaces.Add(type.BaseType);
baseTypesAndInterfaces.AddRange(type.GetInterfaces());
foreach (Type t in baseTypesAndInterfaces)
{
prop = GetProperty(t, propertyName);
if (prop != null)
{
if (!string.IsNullOrEmpty(typeName) && t.Name != typeName)
continue; //keep looking as the typename was not found
break;
}
}
}
return prop;
}

Some notes about the code above:

  1. To create an property expression using the Expression.Property method worked fine when I was using classes. But the minute I started using interfaces where the property was defined on a base interface, Expression.Property began to fail.
    1. The reason for the failure was that .Net by default does not return properties defined on base interfaces via the call Type.GetProperty. Hence Expression.Property fails too.
  2. Instead, I implemented a custom method that would walk up the entire hierarchy of the interface/class and attempt to find a property with the provided name. The PropertyInfo returned by the method is then used to create the Expression property.

To call the above method:

var sortedList = Sort<MyClassOrInterface>(aListContainingObjectsOfTypeMyClassOrInterface, “APropertyDefinedOnMyInterfaceOrClass”, SortDirection.Ascending);

The above method works well when you need to sort based on a property that is defined by a string. (Works great when you have to work with Asp.Net grids where the sorting field is defined by name).

But if you instead need a strongly typed method of sorting you can use the following code:

public List<T> Sort<T,T1>(IEnumerable<T> list,
Func<T, T1> sorter, SortDirection direction)
{
List<T> returnList = null;
if (direction == SortDirection.Ascending)
returnList = list.OrderBy(sorter).ToList();
else
returnList = list.OrderByDescending(sorter).ToList();
return returnList;
}

And to call the above method use the following code: (Where C is the name of a class/interface and Name is a property defined on that class/interface and cList is a list).

List<C> sorted = Sort<C,string>(cList, c => c.Name, SortDirection.Ascending);

No comments: