动态链接List<T> orderby

5
我正在编写一个列表排序扩展方法。 我的输入是列表和一个包含属性名称和排序方向的字符串。 这个字符串可以有多个属性,例如:"Name ASC, Date DESC"等。
我已经实现了字符串解析并使用反射从字符串中获取属性本身,但现在我卡在了如何动态链接orderby方法上。
类似这样的东西: _list.orderBy(x=>x.prop1).thenBy(x=>x.prop2) 等等。
是否有任何方法可以动态构建它?

2
请参见:https://dev59.com/nnVD5IYBdhLWcg3wQJKT#233505 - Gert Arnold
3个回答

9
使用反射来获取字符串属性名称到PropertyInfo的映射。然后使用PropertyInfo构建表达式树,动态构造所有的排序方式。一旦有了表达式树,将其编译为委托(例如Func, IEnumerable>),将_list参数传递给该委托,它将作为另一个可枚举对象给出排序结果。
要获取Enumerable上的泛型方法的反射信息,请查看此帖子中的答案: 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; }
}

示例在 LinqPad 中的结果如下所示:

enter image description here


在我的示例中,我递归地构建表达式并按属性编译,但您可以使用Expression.Block构建整个表达式树,然后仅编译一次。 - base2
1
如果您想使某些属性按降序排序,您可能还需要扩展此示例以包括一个bool[] ascending参数。 - base2
你的解决方案看起来非常好。谢谢。只有一件事,你有没有关于集成支持ASC和DESC排序的想法?我添加了两个额外的MethodInfo,但我找不到如何继续下去。 - Elad Lachmi
2
我已根据你的要求更新了示例。属性参数现在是System.ComponentModel.SortDescription[]类型,其中包括属性名称和排序方向。 - base2

3
我不确定如何通过反射添加排序,但以下是伪代码的基本思路:

我不确定如何通过反射添加排序(懒得去检查),但以下是伪代码的基本思路:

var query = list.OrderBy(properties.First());
bool first = true;
foreach(var property in properties.Skip(1))
{
    query = query.ThenBy(property);
}

1
你可以像这样使用:
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

针对您的评论: 在这种情况下,我会在列表中的对象上实现IComparable,然后只需对其进行排序/或使用动态检查此项的比较器 - 我能想到的唯一其他方法是使用反射和动态调用来完成...如果不是真正必要的话,这不是您想做的事情;)

1
谢谢您的回答,但如果可能的话,我更喜欢一种适用于任何N个属性的方法。 - Elad Lachmi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接