IEnumerable<T> / IQueryable<T> 上的动态LINQ OrderBy

749

我在VS2008的示例中找到了一个关于动态LINQ的例子,允许你使用类似SQL语句的字符串(例如OrderBy("Name, Age DESC"))进行排序。不幸的是,该方法只适用于IQueryable<T>。是否有办法在IEnumerable<T>上实现这个功能呢?

24个回答

972

刚刚偶然找到了这个老的代码...

如果不使用动态LINQ库,您只需要使用下面的代码即可实现此功能。这将包括大多数常见情况,包括嵌套属性。

为了让它与IEnumerable<T>一起工作,您可以添加一些通过AsQueryable进行的包装器方法 - 但是下面的代码是所需的核心Expression逻辑。

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-Objects(OR​​M等的表达式树实际上无法表示dynamic查询 - MemberExpression不支持它)。但是这里有一种使用LINQ-to-Objects的方法。请注意,选择Hashtable是由于其有利的锁定语义:

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);
        }
    }
}

119
我见过的最好的代码!刚刚解决了我项目中的一百万个问题 :) - sajidnizami
4
@Dave - 你需要从IQueryable<T>开始,所以如果你有像List<T>(它是IEnumerable<T>)这样的东西,你可能需要使用AsQueryable() - 例如 var sorted = someList.AsQueryable().OrderBy("Foo.Bar");。注意不要改变原意。 - Marc Gravell
7
你看过这个吗?它可能会帮助到一些人:https://dev59.com/QXRB5IYBdhLWcg3wpoxm#2794039 这是一个更加强类型化的解决方案。 - anthonyv
34
@MGOwen,您似乎误解了代码的本质。无论您在项目中放置40行代码还是这些代码在外部库中(编译前或作为源代码),这40行代码都是相同的。如果我在2008年10月链接到了一个自2011年12月以来一直存在的nuget库,那将是非常惊人的(主要原因是当时nuget还不存在),但基本的“它在做什么”是相同的。此外,您使用“实际解决方案”一词,仿佛每个编码问题都有某种明确定义的共识单一路线:事实并非如此。 - Marc Gravell
8
@MGOwen 顺便说一下,这个外部库有2296行代码(不包括AssemblyInfo.cs);这使得这里的40行代码看起来相当合理。 - Marc Gravell
显示剩余34条评论

296

太容易了,没有任何复杂性:

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

编辑:为节省时间,System.Linq.Dynamic.Core(System.Linq.Dynamic已弃用)程序集不是框架的一部分,但可以从nuget安装:System.Linq.Dynamic.Core


13
你从哪里得到了 System.Linq.Dynamic - Rafael Herscovici
40
接受的答案可能在2008年是正确的,但目前这是最简单、最正确的答案。 - EL MOJO
19
对于“未来”的人们,如果您正在使用dotnet core,请使用此链接:https://www.nuget.org/packages/System.Linq.Dynamic.Core - Rafael Merlin
4
对于像我一样寻找相同解决方案的任何人 - 这适用于对“嵌套”对象进行排序,例如vehicles.AsQueryable().OrderBy("Tire.Size").ToList();,其中Tire是属于Vehicle的对象。 - neilsimp1
6
@RafaelMerlin,同时命名空间现在是System.Linq.Dynamic.Core。 - Edwin Stoteler
显示剩余6条评论

60

1
很棒的东西,只需添加以下修改即可使属性名称不区分大小写:PropertyInfo pi = type.GetProperty(prop,BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); - Mrinal Kamboj

49

我想使用反射来获取您想要排序的任何属性应该是可行的:

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);
}

请注意,使用反射比直接访问属性要慢得多,因此需要进行性能调查。


这个能用吗?orderby 不需要一个值,而是一个选择器 lambda/delegate (Func<TSource, TKey> keySelector)。 - Davy Landman
2
在发布之前,我确实尝试过这个例子,是的,它可以工作。 - Kjetil Watnedal
3
+1 这正是我正在寻找的!这对于简单的页面排序问题非常有效。 - Andrew Siemer
这对我没用。我有什么遗漏吗?“SomeProperty”应该是什么?我尝试过给出属性名称以及property.GetType()。我有IQueryable<>而不是IEnumerable<>。 - Rashmi Pandit
GetPropertyValue方法会被所有元素执行,这是一个不好的解决方案。 - Alex Shkor
2
@Alex Shkor:你怎么可能在不查看所有元素的情况下对它们进行排序?然而,在其他答案中有更好的解决方案。 - Kjetil Watnedal

23

在其他人的基础上建立。我发现以下方法非常有效。

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;
}

14

我试着做这件事,但是在使用Kjetil Watnedal的解决方案时遇到了问题,因为我不使用内联linq语法 - 我更喜欢方法风格的语法。我的具体问题是尝试使用自定义IComparer进行动态排序。

我的解决方案最终变成了这样:

给定一个IQueryable查询,如下所示:

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

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

string SortField; // Set at run-time to "Name"
动态的 OrderBy 如下所示:
query = query.OrderBy(item => item.GetReflectedPropertyValue(SortField));

这是使用一个名为 GetReflectedPropertyValue() 的小助手方法实现的:

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

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

为了做到这一点,我只需修改OrderBy如下:

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

查看此帖子,获取NaturalSortComparer()的代码。


9

我在寻找Linq多重排序子句时遇到了这个问题,也许这就是作者所寻找的。

以下是实现方法:

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

6
由于缺乏解释,+1 取消了下投票。我认为作者可能对多个 order-by 感兴趣。即使 dynamic 是关键字,也没有理由进行下投票。 - Jason Kleban

9

在进行了大量搜索后,以下方法对我有效:

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);
}

我想要使用的确切解决方案是... - imdadhusen

9

使用动态 linq

只需添加 using System.Linq.Dynamic;

然后像这样使用它来对所有列进行排序:

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

那么,来个现实生活中的例子,带上几个连接怎么样? - Lawrence Thurman

6

首先安装动态工具 --> NuGet程序包管理器 --> 包管理器控制台

install-package System.Linq.Dynamic

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

现在你可以使用 OrderBy("Name, Age DESC")


我该如何使用它进行内部属性排序 - 比如OrderBy("Branch.BranchName","Descending")? - devC
这对我有效。也许是因为这个问题已经有10年了,而这种更简单的方法只是后来才出现的。 - kosherjellyfish

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