5

I am writing a list sorting extension method. My input is the list and a string with property name and sort direction. This string can have multiple properties like so: "Name ASC, Date DESC" etc.

I already implemented the string parsing and used reflection to get the property itself from the string, but what I am stuck on now is how do I dynamically chain the orderby methods.

something like: _list.orderBy(x=>x.prop1).thenBy(x=>x.prop2) etc.

Is there any way to build this dynamically?

Bhavik Ambani
  • 6,357
  • 14
  • 52
  • 84
Elad Lachmi
  • 10,140
  • 11
  • 69
  • 127

3 Answers3

9

Use reflection to get from the string property names to a PropertyInfo's. You can then build an expression tree using the PropertyInfo's to dynamically construct all the orderbys. Once you have the expression tree, compile it to a delegate, (say Func, IEnumerable>) Pass in your _list parameter to this delegate and it will give you the ordered result as another enumerable.

To get the reflection information for the generic method on Enumerable, have a look at the answer on this post: Get a generic method without using GetMethods

public static class Helper
{
    public static IEnumerable<T> BuildOrderBys<T>(
        this IEnumerable<T> source,
        params SortDescription[] properties)
    {
        if (properties == null || properties.Length == 0) return source;

        var typeOfT = typeof (T);

        Type t = typeOfT;

        IOrderedEnumerable<T> result = null;
        var thenBy = false;

        foreach (var item in properties
            .Select(prop => new {PropertyInfo = t.GetProperty(prop.PropertyName), prop.Direction}))
        {
            var oExpr = Expression.Parameter(typeOfT, "o");
            var propertyInfo = item.PropertyInfo;
            var propertyType = propertyInfo.PropertyType;
            var isAscending = item.Direction == ListSortDirection.Ascending;

            if (thenBy)
            {
                var prevExpr = Expression.Parameter(typeof (IOrderedEnumerable<T>), "prevExpr");
                var expr1 = Expression.Lambda<Func<IOrderedEnumerable<T>, IOrderedEnumerable<T>>>(
                    Expression.Call(
                        (isAscending ? thenByMethod : thenByDescendingMethod).MakeGenericMethod(typeOfT, propertyType),
                        prevExpr,
                        Expression.Lambda(
                            typeof (Func<,>).MakeGenericType(typeOfT, propertyType),
                            Expression.MakeMemberAccess(oExpr, propertyInfo),
                            oExpr)
                        ),
                    prevExpr)
                    .Compile();

                result = expr1(result);
            }
            else
            {
                var prevExpr = Expression.Parameter(typeof (IEnumerable<T>), "prevExpr");
                var expr1 = Expression.Lambda<Func<IEnumerable<T>, IOrderedEnumerable<T>>>(
                    Expression.Call(
                        (isAscending ? orderByMethod : orderByDescendingMethod).MakeGenericMethod(typeOfT, propertyType),
                        prevExpr,
                        Expression.Lambda(
                            typeof (Func<,>).MakeGenericType(typeOfT, propertyType),
                            Expression.MakeMemberAccess(oExpr, propertyInfo),
                            oExpr)
                        ),
                    prevExpr)
                    .Compile();

                result = expr1(source);
                thenBy = true;
            }
        }
        return result;
    }

    private static MethodInfo orderByMethod =
        MethodOf(() => Enumerable.OrderBy(default(IEnumerable<object>), default(Func<object, object>)))
            .GetGenericMethodDefinition();

    private static MethodInfo orderByDescendingMethod =
        MethodOf(() => Enumerable.OrderByDescending(default(IEnumerable<object>), default(Func<object, object>)))
            .GetGenericMethodDefinition();

    private static MethodInfo thenByMethod =
        MethodOf(() => Enumerable.ThenBy(default(IOrderedEnumerable<object>), default(Func<object, object>)))
            .GetGenericMethodDefinition();

    private static MethodInfo thenByDescendingMethod =
        MethodOf(() => Enumerable.ThenByDescending(default(IOrderedEnumerable<object>), default(Func<object, object>)))
            .GetGenericMethodDefinition();

    public static MethodInfo MethodOf<T>(Expression<Func<T>> method)
    {
        MethodCallExpression mce = (MethodCallExpression) method.Body;
        MethodInfo mi = mce.Method;
        return mi;
    }
}

public static class Sample
{
    private static void Main()
    {
      var data = new List<Customer>
        {
          new Customer {ID = 3, Name = "a"},
          new Customer {ID = 3, Name = "c"},
          new Customer {ID = 4},
          new Customer {ID = 3, Name = "b"},
          new Customer {ID = 2}
        };

      var result = data.BuildOrderBys(
        new SortDescription("ID", ListSortDirection.Ascending),
        new SortDescription("Name", ListSortDirection.Ascending)
        ).Dump();
    }
}

public class Customer
{
    public int ID { get; set; }
    public string Name { get; set; }
}

The result of the sample as shown in LinqPad

enter image description here

Community
  • 1
  • 1
base2
  • 875
  • 8
  • 12
  • In my sample I recursively build an expression and compile per property, but you can construct the entire expression tree with Expression.Block and then only compile once. – base2 Mar 12 '12 at 08:59
  • 1
    You might also want to expand on this sample to include a bool[] ascending parameter if you want to make some properties sort descending. – base2 Mar 12 '12 at 10:14
  • Your solution looks very good. Thank you. Just one thing, do you have any ideas on integrating support for ASC and DESC sorting? I added two more MethodInfo, but I can't find how to move forward from that. – Elad Lachmi Mar 12 '12 at 10:17
  • 2
    I updated the sample as per your request. The properties parameter is now a System.ComponentModel.SortDescription[]. It contains both the property name and an order direction. – base2 Mar 12 '12 at 10:48
2

I am not sure how you add the order by via reflection (too lazy to check) but here is the basic idea in kind of pseudocode:

var query = list.OrderBy(properties.First());
bool first = true;
foreach(var property in properties.Skip(1))
{
    query = query.ThenBy(property);
}
Stilgar
  • 20,008
  • 6
  • 58
  • 93
1

You can use something like this:

var query = _list.OrderBy(x=>x.prop1);
if (shouldOrderByProp2Too)
  query = query.ThenBy(x=>x.prop2);
if (shouldOrderByProp3Too)
  query = query.ThenBy(x=>x.prop3);
// ...
// then use query the way you had your code

For your comment: In this case I would go an implement IComparable on the objects in your list and then just sort it / or use Compareres that dynamically check for this - the only other thing I can think of is doing this with reflection and dynamic-invoke ... not a thing you want to do if not really neccessary ;)

Carsten
  • 49,407
  • 9
  • 85
  • 111