Sort List with string property names

For .NET 3.5 and later we can use the System.Linq.Dynamic library available for download from NuGet

If we have a collection of Car objects with properties Make, Model, Year and Price...

class Car
{
    public string Make { get; set; }
    public string Model { get; set; }
    public int Year { get; set; }
    public decimal Price { get; set; }
}

...this will sort the collection on multiple properties using lambda expression.

cars.OrderBy(c => c.Make)
    .ThenBy(c => c.Model)
    .ThenByDescending(c => c.Year)
    .ThenByDescending(c => c.Price);

I prefer doing this instead.


// prefix property name with a dash for reverse sort
cars.AsQueryable()
    .OrderBy("Make,Model,-Year,-Price");

// sort on one property
cars.AsQueryable()
    .OrderBy("Year");

// array of properties
cars.AsQueryable()
    .OrderBy(new string[] {"Year", "Price"});

It is made possible by dynamic lambda expressions. Copy the code below to a static class in your project. I have mine at Helpers/Extension.cs.


public static class Extensions
{
    public static IOrderedQueryable<T> OrderBy<T>(
        this IQueryable<T> source, 
        string property)
    {
        if (property.IndexOf(',') != -1)
        {
            return OrderBy<T>(
                   source, 
                   property.Split(','));
        }
        else if (property.StartsWith("-"))
        {
            return ApplyOrder<T>(
                   source, 
                   property.Substring(1).Trim(),
                   "OrderByDescending");
        }
        else
        {
            return ApplyOrder<T>(
                   source, 
                   property, 
                   "OrderBy");
        }
    }

    public static IOrderedQueryable<T> OrderByDescending<T>(
        this IQueryable<T> source,
        string property)
    {
        return ApplyOrder<T>(
               source, 
               property, 
               "OrderByDescending");
    }

    public static IOrderedQueryable<T> ThenBy<T>(
        this IOrderedQueryable<T> source, 
        string property)
    {
        return ApplyOrder<T>(
               source,
               property,
               "ThenBy");
    }

    public static IOrderedQueryable<T> ThenByDescending<T>(
        this IOrderedQueryable<T> source,
        string property)
    {
        return ApplyOrder<T>(
               source, 
               property, 
               "ThenByDescending");
    }

    static IOrderedQueryable<T> ApplyOrder<T>(
        IQueryable<T> source,
        string property,
        string methodName)
    {
        string[] props = property.Split('.');
        Type type = typeof(T);
        ParameterExpression arg = Expression.Parameter(type, "x");
        Expression expr = arg;

        foreach (string prop in props)
        {
            // use reflection (not ComponentModel) to mirror LINQ
            PropertyInfo pi = type.GetProperty(
                    prop, 
                    (BindingFlags.NonPublic
                     | BindingFlags.Public
                     | BindingFlags.Instance);

            // raise error if property is not found
            if (pi == null)
            {
                throw new ArgumentException(string.Format(
                    "Sort Error. Property '{0}' not found on type {1}",
                    prop, type.FullName));
            }

            expr = Expression.Property(expr, pi);
            type = pi.PropertyType;
        }

        Type delegateType = typeof(Func<,>).MakeGenericType(typeof(T), type);

        LambdaExpression lambda = Expression.Lambda(delegateType, expr, arg);

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

        return (IOrderedQueryable<T>)result;
    }

    public static IOrderedQueryable<T> OrderBy<T>(
        this IQueryable<T> source,
        IEnumerable<string> properties)
    {
        // iterate the properties and sort
        IOrderedQueryable<T> sortedData = null;

        foreach (string prop in properties)
        {
            string propName = prop.Trim();
            bool ascending = true;

            if (string.IsNullOrEmpty(propName))
            {
                continue;
            }

            if (propName.StartsWith("-"))
            {
                ascending = false;
                propName = prop.Substring(1).Trim();
            }

            // first iteration
            if (sortedData == null)
            {
                if (ascending)
                {
                    sortedData = source.AsQueryable().OrderBy(propName);
                }
                else
                {
                    sortedData = source.AsQueryable().OrderByDescending(propName);
                }
            }
            else
            {
                // subsequent iterations...
                if (ascending)
                {
                    sortedData = sortedData.ThenBy(propName);
                }
                else
                {
                    sortedData = sortedData.ThenByDescending(propName);
                }
            }
        }

        return sortedData;
    }
}

Related:

http://stackoverflow.com/questions/41244/dynamic-linq-orderby-on-ienumerablet/233505#233505