关于C#:IEnumerable / IQueryable 上的动态LINQ OrderBy

Dynamic LINQ OrderBy on IEnumerable<T> / IQueryable<T>

我在VS2008的动态LINQ示例中发现了一个示例,它允许您使用类似SQL的字符串(例如OrderBy("Name, Age DESC"))进行排序。不幸的是,该方法仅适用于IQueryable。有没有办法在IEnumerable上获得这种功能?


只是偶然发现了这个老东西…

要在没有动态Linq库的情况下完成这项工作,您只需要下面的代码。这涵盖了最常见的场景,包括嵌套属性。

要使它与IEnumerable一起工作,您可以添加一些通过AsQueryable的包装方法,但下面的代码是所需的核心Expression逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public static IOrderedQueryable<T> OrderBy<T>(
    this IQueryable<T> source,
    string property)
{
    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);
        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;
}

编辑:如果你想把它和dynamic混合起来会更有趣,尽管注意dynamic只适用于linq to对象(orms等的表达式树不能真正代表dynamic查询,MemberExpression不支持它)。但这里有一种处理LinqTo对象的方法。注意,选择Hashtable是由于良好的锁语义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
using Microsoft.CSharp.RuntimeBinder;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Runtime.CompilerServices;
static class Program
{
    private static class AccessorCache
    {
        private static readonly Hashtable accessors = new Hashtable();

        private static readonly Hashtable callSites = new Hashtable();

        private static CallSite<Func<CallSite, object, object>> GetCallSiteLocked(
            string name)
        {
            var callSite = (CallSite<Func<CallSite, object, object>>)callSites[name];
            if(callSite == null)
            {
                callSites[name] = callSite = CallSite<Func<CallSite, object, object>>
                    .Create(Binder.GetMember(
                                CSharpBinderFlags.None,
                                name,
                                typeof(AccessorCache),
                                new CSharpArgumentInfo[] {
                                    CSharpArgumentInfo.Create(
                                        CSharpArgumentInfoFlags.None,
                                        null)
                                }));
            }
            return callSite;
        }

        internal static Func<dynamic,object> GetAccessor(string name)
        {
            Func<dynamic, object> accessor = (Func<dynamic, object>)accessors[name];
            if (accessor == null)
            {
                lock (accessors )
                {
                    accessor = (Func<dynamic, object>)accessors[name];
                    if (accessor == null)
                    {
                        if(name.IndexOf('.') >= 0) {
                            string[] props = name.Split('.');
                            CallSite<Func<CallSite, object, object>>[] arr
                                = Array.ConvertAll(props, GetCallSiteLocked);
                            accessor = target =>
                            {
                                object val = (object)target;
                                for (int i = 0; i < arr.Length; i++)
                                {
                                    var cs = arr[i];
                                    val = cs.Target(cs, val);
                                }
                                return val;
                            };
                        } else {
                            var callSite = GetCallSiteLocked(name);
                            accessor = target =>
                            {
                                return callSite.Target(callSite, (object)target);
                            };
                        }
                        accessors[name] = accessor;
                    }
                }
            }
            return accessor;
        }
    }

    public static IOrderedEnumerable<dynamic> OrderBy(
        this IEnumerable<dynamic> source,
        string property)
    {
        return Enumerable.OrderBy<dynamic, object>(
            source,
            AccessorCache.GetAccessor(property),
            Comparer<object>.Default);
    }

    public static IOrderedEnumerable<dynamic> OrderByDescending(
        this IEnumerable<dynamic> source,
        string property)
    {
        return Enumerable.OrderByDescending<dynamic, object>(
            source,
            AccessorCache.GetAccessor(property),
            Comparer<object>.Default);
    }

    public static IOrderedEnumerable<dynamic> ThenBy(
        this IOrderedEnumerable<dynamic> source,
        string property)
    {
        return Enumerable.ThenBy<dynamic, object>(
            source,
            AccessorCache.GetAccessor(property),
            Comparer<object>.Default);
    }

    public static IOrderedEnumerable<dynamic> ThenByDescending(
        this IOrderedEnumerable<dynamic> source,
        string property)
    {
        return Enumerable.ThenByDescending<dynamic, object>(
            source,
            AccessorCache.GetAccessor(property),
            Comparer<object>.Default);
    }

    static void Main()
    {
        dynamic a = new ExpandoObject(),
                b = new ExpandoObject(),
                c = new ExpandoObject();
        a.X ="abc";
        b.X ="ghi";
        c.X ="def";
        dynamic[] data = new[] {
            new { Y = a },
            new { Y = b },
            new { Y = c }
        };

        var ordered = data.OrderByDescending("Y.X").ToArray();
        foreach (var obj in ordered)
        {
            Console.WriteLine(obj.Y.X);
        }
    }
}


太容易了,没有任何并发症:

  • 在顶部加上using System.Linq.Dynamic;
  • 使用vehicles = vehicles.AsQueryable().OrderBy("Make ASC, Year DESC").ToList();

  • 我找到了答案。我可以使用.AsQueryable<>()扩展方法将我的列表转换为iqueryable,然后针对它运行动态顺序。


    刚刚偶然发现这个问题。

    使用上面Marc的applyOrder实现,我将处理类似SQL的字符串的扩展方法组合在一起,比如:

    1
    list.OrderBy("MyProperty DESC, MyOtherProperty ASC");

    有关详细信息,请访问:http://aonnull.blogspot.com/2010/08/dynamic-sql-like-linq-orderby-extension.html


    我想使用反射来获得你想要排序的任何属性都是可行的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    IEnumerable<T> myEnumerables
    var query=from enumerable in myenumerables
              where some criteria
              orderby GetPropertyValue(enumerable,"SomeProperty")
              select enumerable

    private static object GetPropertyValue(object obj, string property)
    {
        System.Reflection.PropertyInfo propertyInfo=obj.GetType().GetProperty(property);
        return propertyInfo.GetValue(obj, null);
    }

    请注意,使用反射比直接访问属性慢得多,因此必须研究性能。


    只是建立在别人所说的基础上。我发现下面的方法很有效。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    public static IEnumerable<T> OrderBy<T>(this IEnumerable<T> input, string queryString)
    {
        if (string.IsNullOrEmpty(queryString))
            return input;

        int i = 0;
        foreach (string propname in queryString.Split(','))
        {
            var subContent = propname.Split('|');
            if (Convert.ToInt32(subContent[1].Trim()) == 0)
            {
                if (i == 0)
                    input = input.OrderBy(x => GetPropertyValue(x, subContent[0].Trim()));
                else
                    input = ((IOrderedEnumerable<T>)input).ThenBy(x => GetPropertyValue(x, subContent[0].Trim()));
            }
            else
            {
                if (i == 0)
                    input = input.OrderByDescending(x => GetPropertyValue(x, subContent[0].Trim()));
                else
                    input = ((IOrderedEnumerable<T>)input).ThenByDescending(x => GetPropertyValue(x, subContent[0].Trim()));
            }
            i++;
        }

        return input;
    }


    我在查找linq multiple orderby子句时遇到了这个问题。也许这就是作者想要的

    以下是如何做到这一点:

    1
    var query = pets.OrderBy(pet => pet.Name).ThenByDescending(pet => pet.Age);


    我试图这样做,但是KjetilWatnedal的解决方案有问题,因为我不使用内联LINQ语法-我更喜欢方法样式语法。我的具体问题是尝试使用自定义IComparer进行动态排序。

    我的解决方案是这样的:

    给出这样一个IQueryable查询:

    1
    2
    List<DATA__Security__Team> teams = TeamManager.GetTeams();
    var query = teams.Where(team => team.ID < 10).AsQueryable();

    并给出一个运行时排序字段参数:

    1
    string SortField; // Set at run-time to"Name"

    动态orderby如下所示:

    1
    query = query.OrderBy(item => item.GetReflectedPropertyValue(SortField));

    它使用了一个名为getReflectedPropertyValue()的小助手方法:

    1
    2
    3
    4
    5
    public static string GetReflectedPropertyValue(this object subject, string field)
    {
        object reflectedValue = subject.GetType().GetProperty(field).GetValue(subject, null);
        return reflectedValue != null ? reflectedValue.ToString() :"";
    }

    最后一件事-我提到我希望OrderBy使用自定义IComparer,因为我希望进行自然排序。

    为此,我将OrderBy改为:

    1
    query = query.OrderBy(item => item.GetReflectedPropertyValue(SortField), new NaturalSortComparer<string>());

    有关NaturalSortComparer()的代码,请参阅本文。


    可以将IEnumerable转换为IQueryable。

    1
    items = items.AsQueryable().OrderBy("Name ASC");

    经过大量的搜索,这对我很有用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public static IEnumerable<TEntity> OrderBy<TEntity>(this IEnumerable<TEntity> source,
                                                        string orderByProperty, bool desc)
    {
        string command = desc ?"OrderByDescending" :"OrderBy";
        var type = typeof(TEntity);
        var property = type.GetProperty(orderByProperty);
        var parameter = Expression.Parameter(type,"p");
        var propertyAccess = Expression.MakeMemberAccess(parameter, property);
        var orderByExpression = Expression.Lambda(propertyAccess, parameter);
        var resultExpression = Expression.Call(typeof(Queryable), command,
                                               new[] { type, property.PropertyType },
                                               source.AsQueryable().Expression,
                                               Expression.Quote(orderByExpression));
        return source.AsQueryable().Provider.CreateQuery<TEntity>(resultExpression);
    }

    由于Maarten(使用Linq中的PropertyInfo对象查询集合),我得到了这个解决方案:

    1
    myList.OrderByDescending(x => myPropertyInfo.GetValue(x, null)).ToList();

    在我的例子中,我正在处理一个"ColumnHeaderMouseClick"(WindowsForm),所以刚找到按下的特定列及其对应的属性信息:

    1
    2
    3
    4
    5
    foreach (PropertyInfo column in (new Process()).GetType().GetProperties())
    {
        if (column.Name == dgvProcessList.Columns[e.ColumnIndex].Name)
        {}
    }

    1
    PropertyInfo column = (new Process()).GetType().GetProperties().Where(x => x.Name == dgvProcessList.Columns[e.ColumnIndex].Name).First();

    (请确保列名与对象属性匹配)

    干杯


    您可以添加它:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public static IEnumerable<T> OrderBy( this IEnumerable<T> input, string queryString) {
        //parse the string into property names
        //Use reflection to get and sort by properties
        //something like

        foreach( string propname in queryString.Split(','))
            input.OrderBy( x => GetPropertyValue( x, propname ) );

        // I used Kjetil Watnedal's reflection example
    }

    GetPropertyValue函数来自kjetil watnedal的答案。

    问题是为什么?任何这样的类型都会在运行时抛出异常,而不是在编译时抛出异常(如d2viant的答案)。

    如果您处理的是linq to sql,而orderby是一个表达式树,那么它将转换为sql以供执行。


    这里还有一些我觉得有趣的东西。如果源是数据表,则可以使用动态排序而不使用动态LINQ

    1
    2
    3
    4
    5
    6
    DataTable orders = dataSet.Tables["SalesOrderHeader"];
    EnumerableRowCollection<DataRow> query = from order in orders.AsEnumerable()
                                             orderby order.Field<DateTime>("OrderDate")
                                             select order;
    DataView view = query.AsDataView();
    bindingSource1.DataSource = view;

    参考:http://msdn.microsoft.com/en-us/library/bb669083.aspx(使用datasetensions)

    还有一种方法可以通过将其转换为数据视图来实现:

    1
    2
    3
    4
    5
    DataTable contacts = dataSet.Tables["Contact"];    
    DataView view = contacts.AsDataView();    
    view.Sort ="LastName desc, FirstName asc";    
    bindingSource1.DataSource = view;
    dataGridView1.AutoResizeColumns();

    另一种解决方案使用以下类/接口。它不是真正的动态的,但它是有效的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public interface IID
    {
        int ID
        {
            get; set;
        }
    }

    public static class Utils
    {
        public static int GetID<T>(ObjectQuery<T> items) where T:EntityObject, IID
        {
            if (items.Count() == 0) return 1;
            return items.OrderByDescending(u => u.ID).FirstOrDefault().ID + 1;
        }
    }

    此答案是对需要@john sheehan-runscope提供解决方案示例的注释的响应

    Please provide an example for the rest of us.

    在DAL(数据访问层)中,

    IEnumerable版本:

    1
    2
    3
    4
    5
    6
      public  IEnumerable<Order> GetOrders()
        {
          // i use Dapper to return IEnumerable<T> using Query<T>
          //.. do stuff
          return  orders  // IEnumerable<Order>
      }

    可查询版本

    1
    2
    3
    4
    5
    6
      public IQueryable<Order> GetOrdersAsQuerable()
        {
            IEnumerable<Order> qry= GetOrders();
            //use the built-in extension method  AsQueryable in  System.Linq namespace
            return qry.AsQueryable();            
        }

    现在您可以使用iQuery版本进行绑定,例如ASP.NET中的GridView和排序优势(不能使用IEnumerable版本进行排序)

    我使用dapper作为ORM,构建了iqueryable版本,并在ASP.NET中轻松地使用了GridView中的排序。


    第一次安装动态工具——>Nuget Package Manager——>Package Manager控制台

    1
    install-package System.Linq.Dynamic

    添加命名空间using System.Linq.Dynamic;

    现在您可以使用EDOCX1[1]


    将list转换为ienumerable或iquerable,使用system.linq.dynamic命名空间添加,然后可以将属性名用逗号分隔的字符串提到orderby方法,该方法默认来自system.linq.dynamic。


    使用动态linq

    加上using System.Linq.Dynamic;

    然后像这样对所有列进行排序:

    1
    2
    3
    string sortTypeStr ="ASC"; // or DESC
    string SortColumnName ="Age"; // Your column name
    query = query.OrderBy($"{SortColumnName} {sortTypeStr}");

    1
    2
    var result1 = lst.OrderBy(a=>a.Name);// for ascending order.
     var result1 = lst.OrderByDescending(a=>a.Name);// for desc order.