动态使用LINQ进行排序

7

我有一组CLR对象。对象的类定义具有三个属性:FirstName,LastName,BirthDate。

我有一个字符串,反映了集合应该按其排序的属性的名称。此外,我还有一个排序方向。如何动态地将此排序信息应用于我的集合?请注意,排序可以是多层的,因此例如我可以先按LastName排序,然后再按FirstName排序。

目前,我正在尝试以下操作,但没有成功:

var results = myCollection.OrderBy(sortProperty);

然而,我收到了一条消息,内容为:...不包含名为'OrderBy'的定义,而最佳扩展方法超载...存在一些无效的参数。
9个回答

10
好的,我的争论与SLaks在他的评论中已经迫使我提出一个答案 :)

我假设你只需要支持LINQ to Objects。这是一些需要添加大量验证但仍能正常工作的代码:

// We want the overload which doesn't take an EqualityComparer.
private static MethodInfo OrderByMethod = typeof(Enumerable)
    .GetMethods(BindingFlags.Public | BindingFlags.Static)
    .Where(method => method.Name == "OrderBy" 
           && method.GetParameters().Length == 2)
    .Single();

public static IOrderedEnumerable<TSource> OrderByProperty<TSource>(
    this IEnumerable<TSource> source,
    string propertyName) 
{
    // TODO: Lots of validation :)
    PropertyInfo property = typeof(TSource).GetProperty(propertyName);
    MethodInfo getter = property.GetGetMethod();
    Type propType = property.PropertyType;
    Type funcType = typeof(Func<,>).MakeGenericType(typeof(TSource), propType);
    Delegate func = Delegate.CreateDelegate(funcType, getter);
    MethodInfo constructedMethod = OrderByMethod.MakeGenericMethod(
        typeof(TSource), propType);
    return (IOrderedEnumerable<TSource>) constructedMethod.Invoke(null,
        new object[] { source, func });
}

测试代码:

string[] foo = new string[] { "Jon", "Holly", "Tom", "William", "Robin" };

foreach (string x in foo.OrderByProperty("Length"))
{
    Console.WriteLine(x);
}

输出:

Jon
Tom
Holly
Robin
William

它甚至返回一个IOrderedEnumerable<TSource>,因此您可以像平常一样链接ThenBy子句 :)


顾客.OrderByProperty("LastName").ThenByProperty("FirstName") 或者顾客.OrderByProperty("LastName", "FirstName")(其中这些是 params string[])怎么样?因为它似乎是他的要求的一部分。在这个解决方案中可行吗? - Anthony Pegram
@Anthony:当然,你需要编写更多的代码来实现这个功能,但是这是可能的 - 并且看起来很像上面的代码。 - Jon Skeet
好的。作为一个对这个话题一窍不通的人,你是否有理由更喜欢反射而不是@SLaks使用表达式树?(以防将来我遇到这样的需求。) - Anthony Pegram
这个在使用几千行的企业应用中安全吗?我已经让它工作了,我还在Jon's的解决方案中添加了一个“OrderByPropertyDescending”。不过我担心"TODO: Lots of validation"。我只需要用字符串作为列名参数对IEnumerable进行排序 - 没有更花哨的需求。我对LINQ很陌生,也许我误解了关于验证的评论。 - notAnonymousAnymore
@user982119:是的,使用它处理数千行数据没有问题。验证只需要检查指定的属性是否存在、可读等。此验证仅会执行一次,而不是每次比较都执行。 - Jon Skeet
显示剩余4条评论

7
您需要构建一个表达式树并将其传递给OrderBy。它看起来应该像这样:

表达式树

var param = Expression.Parameter(typeof(MyClass));
var expression = Expression.Lambda<Func<MyClass, PropertyType>>(
    Expression.Property(param, sortProperty),
    param
);

或者,您可以使用Dynamic LINQ,这将使您的代码保持原样。


我该如何构建表达式树? - user564042
鉴于 OP 拥有“一组 CLR 对象”,我怀疑他并不需要 IQueryable 解决方案。 - Jon Skeet
@Jon:那又怎样?你仍然需要一个表达式树来动态排序。这意味着他需要调用“编译”。 - SLaks
@SLaks:我不同意。我认为完全可以在没有表达式树的情况下动态排序。 - Jon Skeet
如果 sortProperty 的类型不固定,那么这个解决方案(或 Jon 的假设解决方案)会有多大的适用性? - Anthony Pegram
@Anthony:我的解决方案很容易修改以生成对象。Jon的解决方案可以直接使用,而且速度更快。 - SLaks

1
protected void sort_grd(object sender, GridViewSortEventArgs e)
    {
        if (Convert.ToBoolean(ViewState["order"]) == true)
        {
            ViewState["order"] = false;

        }
        else
        {
            ViewState["order"] = true;
        }
        ViewState["SortExp"] = e.SortExpression;
        dataBind(Convert.ToBoolean(ViewState["order"]), e.SortExpression);
    }

public void dataBind(bool ord, string SortExp)
    {
        var db = new DataClasses1DataContext(); //linq to sql class
        var Name = from Ban in db.tbl_Names.AsEnumerable()
                         select new
                         {
                             First_Name = Ban.Banner_Name,
                             Last_Name = Ban.Banner_Project
                         };
        if (ord)
        {
            Name = BannerName.OrderBy(q => q.GetType().GetProperty(SortExp).GetValue(q, null));
        }
        else
        {
            Name = BannerName.OrderByDescending(q => q.GetType().GetProperty(SortExp).GetValue(q, null));
        }
        grdSelectColumn.DataSource = Name ;
        grdSelectColumn.DataBind();
    }

0

0

我误解了,我已经更新我的答案,建议使用Dynamic Linq库。 - Peter

0

你可以使用 Linq 来实现这个功能

var results = from c in myCollection
    orderby c.SortProperty
    select c;

问题是,我的sortProperty是一个字符串。我该如何使用这样的字符串进行排序? - user564042
2
啊,我明白了。在这里找到了另一篇帖子可能会有所帮助...https://dev59.com/fVLTa4cB1Zd3GeqPXyui - WraithNath

0

对于动态排序,您可以评估字符串,例如:

List<MyObject> foo = new List<MyObject>();
string sortProperty = "LastName";
var result = foo.OrderBy(x =>
                {
                if (sortProperty == "LastName")
                    return x.LastName;
                else
                    return x.FirstName;
                });

如需更通用的解决方案,请参见此 SO 帖子:强类型动态 Linq 排序


同意,这不是一个非常通用的解决方案,但解决了这个特定的问题。 - BrokenGlass

0
你需要使用反射来获取 PropertyInfo,然后使用它来构建表达式树。类似这样的代码:
var entityType = typeof(TEntity);
var prop = entityType.GetProperty(sortProperty);
var param = Expression.Parameter(entityType, "x");
var access = Expression.Lambda(Expression.MakeMemberAccess(param, prop), param);

var ordered = (IOrderedQueryable<TEntity>) Queryable.OrderBy(
    myCollection, 
    (dynamic) access);

0

您实际上可以使用您原来的代码行

var results = myCollection.OrderBy(sortProperty);

只需使用System.Linq.Dynamic库即可轻松实现。

如果您遇到编译器错误(例如无法转换或不包含定义...),则可能需要像这样操作:

var results = myCollection.AsQueryable().OrderBy(sortProperty);

不需要任何表达式树或数据绑定。


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