Better way to Sort a List by any property

2019-05-11 19:05发布

My method receives all DataTables parameters to sort table by column clicked. I call this method from controller of each page list. I'm looking for a better way to do this like a generic method for all types: string, int, decimal, double, bool (nullable or not). But I can't find it.

My current code:

public List<T> OrderingList<T>(List<T> list, DataTablesParam model)
{
    var iColumn = model.Order.FirstOrDefault().Column;
    var property = typeof(T).GetProperty(model.Columns.ToArray()[iColumn].Data);
    var param = Expression.Parameter(typeof(T));
    var final = Expression.Property(param, property);

    var isDirAsc = model.Order.FirstOrDefault().Dir.Equals("asc");

    if (property.PropertyType == typeof(string))
    {
        var lambda = Expression.Lambda<Func<T, string>>(final, param).Compile();
        return isDirAsc ? list.OrderBy(lambda).ToList() : list.OrderByDescending(lambda).ToList();
    }
    else if (property.PropertyType == typeof(int))
    {
        var lambda = Expression.Lambda<Func<T, int>>(final, param).Compile();
        return isDirAsc ? list.OrderBy(lambda).ToList() : list.OrderByDescending(lambda).ToList();
    }
    else if (property.PropertyType == typeof(bool))
    {
        var lambda = Expression.Lambda<Func<T, bool>>(final, param).Compile();
        return isDirAsc ? list.OrderBy(lambda).ToList() : list.OrderByDescending(lambda).ToList();
    }
    else if (property.PropertyType == typeof(decimal))
    {
        var lambda = Expression.Lambda<Func<T, decimal>>(final, param).Compile();
        return isDirAsc ? list.OrderBy(lambda).ToList() : list.OrderByDescending(lambda).ToList();
    }
    else if (property.PropertyType == typeof(double))
    {
        var lambda = Expression.Lambda<Func<T, double>>(final, param).Compile();
        return isDirAsc ? list.OrderBy(lambda).ToList() : list.OrderByDescending(lambda).ToList();
    }

    return list;
}

I want to do something like this: (But this code doesn't work)

public List<T> OrderingList<T>(List<T> list, DataTablesParam model)
{
    var iColumn = model.Order.FirstOrDefault().Column;
    var property = typeof(T).GetProperty(model.Columns.ToArray()[iColumn].Data);
    var param = Expression.Parameter(typeof(T));
    var final = Expression.Property(param, property);

    var isDirAsc = model.Order.FirstOrDefault().Dir.Equals("asc");

    var lambda = Expression.Lambda<Func<T, dynamic>>(final, param).Compile();
    return isDirAsc ? list.OrderBy(lambda).ToList() : list.OrderByDescending(lambda).ToList();
}

5条回答
狗以群分
2楼-- · 2019-05-11 19:30

Your suggested method almost works. You need to change two things in order to make it work:

public List<T> OrderingList<T>(List<T> list, DataTablesParam model)
{
    var iColumn = model.Order.FirstOrDefault().Column;
    var property = typeof(T).GetProperty(model.Columns.ToArray()[iColumn].Data);
    var param = Expression.Parameter(typeof(T), "p");
    Expression final = Expression.Property(param, property);

    // Boxing of value types
    if (property.PropertyType.IsValueType) {
        final = Expression.MakeUnary(ExpressionType.Convert, final, typeof(object));
    }

    var isDirAsc = model.Order.FirstOrDefault().Dir.Equals("asc");

    //                                     VVVVVV
    var lambda = Expression.Lambda<Func<T, object>>(final, param).Compile();
    return isDirAsc
        ? list.OrderBy(lambda).ToList()
        : list.OrderByDescending(lambda).ToList();
}
  1. Instead of using dynamic use object, since every type is an object.
  2. If you have a value type, you need a boxing operation, i.e. you must cast the value to object (object)i. This is done with a unary convert operation:

    Expression final = Expression.Property(param, property);
    if (property.PropertyType.IsValueType) {
        final = Expression.MakeUnary(ExpressionType.Convert, final, typeof(object));
    }
    

Note also that final is declared explicitly as Expression, since the expression type might change from property to unary expression.

查看更多
3楼-- · 2019-05-11 19:36

Beyond just not being very generic, your solution also requires a lot of extra memory because you're copying the list with LINQ. You can avoid this using List.Sort.

I would do:

static void SortBy<T>(List<T> list, MemberInfo member, bool desc)
{
    Comparison<T> cmp = BuildComparer<T>(member, desc);
    list.Sort(cmp);
}

static Comparison<T> BuildComparer<T>(MemberInfo member, bool desc)
{
    var left = Expression.Parameter(typeof(T));
    var right = Expression.Parameter(typeof(T));

    Expression cmp = Expression.Call(
        Expression.MakeMemberAccess(desc ? right : left, member),
        "CompareTo",
        Type.EmptyTypes,
        Expression.MakeMemberAccess(desc ? left : right, member));

    return Expression.Lambda<Comparison<T>>(cmp, left, right).Compile();
}
查看更多
smile是对你的礼貌
4楼-- · 2019-05-11 19:39

It's works fine for me: (Thanks @Poke)

https://stackoverflow.com/a/31393168/5112444

My final method:

private IEnumerable<T> Sort<T>(IEnumerable<T> list, string propertyName, bool isAsc)
{
    MethodInfo orderByMethod = typeof(Enumerable).GetMethods().First(mi => mi.Name == (isAsc ? "OrderBy" : "OrderByDescending") && mi.GetParameters().Length == 2);

    PropertyInfo pi = typeof(T).GetProperty(propertyName);
    MethodInfo orderBy = orderByMethod.MakeGenericMethod(typeof(T), pi.PropertyType);

    ParameterExpression param = Expression.Parameter(typeof(T));
    Delegate accessor = Expression.Lambda(Expression.Call(param, pi.GetGetMethod()), param).Compile();
    return (IEnumerable<T>)orderBy.Invoke(null, new object[] { list, accessor });
}
查看更多
太酷不给撩
5楼-- · 2019-05-11 19:42

I found a better way to do this. I had to do 3 steps:

1 - Add the package "Linq Dynamic" in project:

Install-Package System.Linq.Dynamic.Library

2 - Import the package in Class:

using System.Linq.Dynamic;

3 - Order list by the string name of property:

list.OrderBy(stringPropertyName);                 //asc
list.OrderBy(stringPropertyName + " descending"); //des

It work perfectly for me.

查看更多
欢心
6楼-- · 2019-05-11 19:43

You can just call the Enumerable.OrderBy method using reflection. That way, you don’t have to know the type at compile-time. To do that, you just need to get the method, and create a generic method using the property’s type:

private IEnumerable<T> Sort<T> (List<T> list, string propertyName)
{
    MethodInfo orderByMethod = typeof(Enumerable).GetMethods().First(mi => mi.Name == "OrderBy" && mi.GetParameters().Length == 2);

    PropertyInfo pi = typeof(T).GetProperty(propertyName);
    MethodInfo orderBy = orderByMethod.MakeGenericMethod(typeof(T), pi.PropertyType);

    ParameterExpression param = Expression.Parameter(typeof(T));
    Delegate accessor = Expression.Lambda(Expression.Property(param, pi), param).Compile();
    return (IEnumerable<T>)orderBy.Invoke(null, new object[] { lst, accessor });
}

Note that I abstracted out the stuff about your model to keep this method generic enough. It can basically sort by any property on a list by just specifying the property name. Your original method would then look like this:

public List<T> OrderingList<T>(List<T> list, DataTablesParam model)
{
    var iColumn = model.Order.FirstOrDefault().Column;
    string propertyName = model.Columns.ToArray()[iColumn].Data;

    return Sort(list, propertyName).ToList();
}
查看更多
登录 后发表回答