C# 强类型动态 Linq 排序

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/557819/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-04 08:05:30  来源:igfitidea点击:

Strongly typed dynamic Linq sorting

c#.netlinq

提问by David

I'm trying to build some code for dynamically sorting a Linq IQueryable<>.

我正在尝试构建一些代码来动态排序 Linq IQueryable<>。

The obvious way is here, which sorts a list using a string for the field name
http://dvanderboom.wordpress.com/2008/12/19/dynamically-composing-linq-orderby-clauses/

显而易见的方法在这里,它使用字段名称的字符串对列表进行排序
http://dvanderboom.wordpress.com/2008/12/19/dynamically-composing-linq-orderby-clauses/

However I want one change - compile time checking of field names, and the ability to use refactoring/Find All References to support later maintenance. That means I want to define the fields as f=>f.Name, instead of as strings.

但是我想要一个更改 - 字段名称的编译时检查,以及使用重构/查找所有引用来支持以后维护的能力。这意味着我想将字段定义为 f=>f.Name,而不是字符串。

For my specific use I want to encapsulate some code that would decide which of a list of named "OrderBy" expressions should be used based on user input, without writing different code every time.

对于我的特定用途,我想封装一些代码,这些代码将根据用户输入决定应该使用哪些命名的“OrderBy”表达式列表,而无需每次都编写不同的代码。

Here is the gist of what I've written:

以下是我所写内容的要点:

var list = from m Movies select m; // Get our list

var sorter = list.GetSorter(...); // Pass in some global user settings object

sorter.AddSort("NAME", m=>m.Name);
sorter.AddSort("YEAR", m=>m.Year).ThenBy(m=>m.Year);

list = sorter.GetSortedList();

...
public class Sorter<TSource>
...
public static Sorter<TSource> GetSorter(this IQueryable<TSource> source, ...)

The GetSortedList function determines which of the named sorts to use, which results in a List object, where each FieldData contains the MethodInfo and Type values of the fields passed in AddSort:

GetSortedList 函数确定要使用的命名排序,这会产生一个 List 对象,其中每个 FieldData 包含在 AddSort 中传递的字段的 MethodInfo 和 Type 值:

public SorterItem<TSource> AddSort(Func<T, TKey> field)
{
   MethodInfo ... = field.Method;
   Type ... = TypeOf(TKey);
   // Create item, add item to diction, add fields to item's List<>
   // The item has the ThenBy method, which just adds another field to the List<>
}

I'm not sure if there is a way to store the entire field object in a way that would allow it be returned later (it would be impossible to cast, since it is a generic type)

我不确定是否有办法以允许稍后返回的方式存储整个字段对象(因为它是泛型类型,所以无法强制转换)

Is there a way I could adapt the sample code, or come up with entirely new code, in order to sort using strongly typed field names afterthey have been stored in some container and retrieved (losing any generic type casting)

有没有一种方法可以修改示例代码,或者提出全新的代码,以便在将强类型字段名称存储在某个容器中并检索使用强类型字段名称进行排序(丢失任何泛型类型转换)

采纳答案by David Wengier

The easiest way to do this would be to have your AddSort() function take an Expression<Func<Movie>> instead of just a Func. This allows your sort method to inspect the Expression to extract out the name of the property that you want to sort on. You can then store this name internally as a string, so storing is very easy and you can use the sorting algorithm you linked to, but you also get type safety and compile time checking for valid property names.

最简单的方法是让您的 AddSort() 函数采用 Expression<Func<Movie>> 而不仅仅是 Func。这允许您的排序方法检查表达式以提取您想要排序的属性的名称。然后您可以在内部将此名称存储为字符串,因此存储非常容易,您可以使用链接到的排序算法,但您还可以获得类型安全和编译时检查有效属性名称。

static void Main(string[] args)
{
    var query = from m in Movies select m;

    var sorter = new Sorter<Movie>();
    sorter.AddSort("NAME", m => m.Name);
}

class Sorter<T>
{
    public void AddSort(string name, Expression<Func<T, object>> func)
    {
        string fieldName = (func.Body as MemberExpression).Member.Name;
    }
}

In this case, i've used object as the return type of the func, because its easily automatically convertible, but you could implement that with different types, or generics, as appropriate, if you require more functionality. In this case, since the Expression is just there to be inspected, it doesn't really matter.

在这种情况下,我使用 object 作为 func 的返回类型,因为它很容易自动转换,但是如果需要更多功能,您可以使用不同的类型或泛型(视情况而定)来实现它。在这种情况下,由于表达式只是要检查的,所以并不重要。

The other possible way is to still take a Func, and store that in the dictionary itself. Then, when it comes to sorting, and you need to get the value to sort on, you can call something like:

另一种可能的方法是仍然采用 Func,并将其存储在字典本身中。然后,当涉及到排序时,您需要获取要排序的值,您可以调用如下代码:

// assuming a dictionary of fields to sort for, called m_fields
m_fields[fieldName](currentItem)

回答by Thomas Eyde

Bummer! I must learn how to read the specifications from end to end :-(

无赖!我必须学习如何从头到尾阅读规范:-(

However, now that I have spent too much time fooling around rather than working, I will post my results anyway hoping this will inspire people to read, think, understand (important) and then act. Or how to be too clever with generics, lambdas and funny Linq stuff.

然而,既然我花了太多时间在闲逛而不是工作,无论如何我都会发布我的结果,希望这能激励人们阅读、思考、理解(重要的)然后采取行动。或者如何在泛型、lambda 和有趣的 Linq 东西上变得过于聪明。

A neat trick I discovered during this exercise, are those private inner classes which derives from Dictionary. Their whole purpose is to remove all those angle brackets in order to improve readability.

我在这个练习中发现了一个巧妙的技巧,就是那些从Dictionary. 它们的全部目的是删除所有这些尖括号以提高可读性。

Oh, almost forgot the code:

哦,差点忘了代码:

UPDATE: Made the code generic and to use IQueryableinstead of IEnumerable

更新:使代码通用并使用IQueryable而不是IEnumerable

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;


namespace StackOverflow.StrongTypedLinqSort
{
    [TestFixture]
    public class SpecifyUserDefinedSorting
    {
        private Sorter<Movie> sorter;

        [SetUp]
        public void Setup()
        {
            var unsorted = from m in Movies select m;
            sorter = new Sorter<Movie>(unsorted);

            sorter.Define("NAME", m1 => m1.Name);
            sorter.Define("YEAR", m2 => m2.Year);
        }

        [Test]
        public void SortByNameThenYear()
        {
            var sorted = sorter.SortBy("NAME", "YEAR");
            var movies = sorted.ToArray();

            Assert.That(movies[0].Name, Is.EqualTo("A"));
            Assert.That(movies[0].Year, Is.EqualTo(2000));
            Assert.That(movies[1].Year, Is.EqualTo(2001));
            Assert.That(movies[2].Name, Is.EqualTo("B"));
        }

        [Test]
        public void SortByYearThenName()
        {
            var sorted = sorter.SortBy("YEAR", "NAME");
            var movies = sorted.ToArray();

            Assert.That(movies[0].Name, Is.EqualTo("B"));
            Assert.That(movies[1].Year, Is.EqualTo(2000));
        }

        [Test]
        public void SortByYearOnly()
        {
            var sorted = sorter.SortBy("YEAR");
            var movies = sorted.ToArray();

            Assert.That(movies[0].Name, Is.EqualTo("B"));
        }

        private static IQueryable<Movie> Movies
        {
            get { return CreateMovies().AsQueryable(); }
        }

        private static IEnumerable<Movie> CreateMovies()
        {
            yield return new Movie {Name = "B", Year = 1990};
            yield return new Movie {Name = "A", Year = 2001};
            yield return new Movie {Name = "A", Year = 2000};
        }
    }


    internal class Sorter<E>
    {
        public Sorter(IQueryable<E> unsorted)
        {
            this.unsorted = unsorted;
        }

        public void Define<P>(string name, Expression<Func<E, P>> selector)
        {
            firstPasses.Add(name, s => s.OrderBy(selector));
            nextPasses.Add(name, s => s.ThenBy(selector));
        }

        public IOrderedQueryable<E> SortBy(params string[] names)
        {
            IOrderedQueryable<E> result = null;

            foreach (var name in names)
            {
                result = result == null
                             ? SortFirst(name, unsorted)
                             : SortNext(name, result);
            }

            return result;
        }

        private IOrderedQueryable<E> SortFirst(string name, IQueryable<E> source)
        {
            return firstPasses[name].Invoke(source);
        }

        private IOrderedQueryable<E> SortNext(string name, IOrderedQueryable<E> source)
        {
            return nextPasses[name].Invoke(source);
        }

        private readonly IQueryable<E> unsorted;
        private readonly FirstPasses firstPasses = new FirstPasses();
        private readonly NextPasses nextPasses = new NextPasses();


        private class FirstPasses : Dictionary<string, Func<IQueryable<E>, IOrderedQueryable<E>>> {}


        private class NextPasses : Dictionary<string, Func<IOrderedQueryable<E>, IOrderedQueryable<E>>> {}
    }


    internal class Movie
    {
        public string Name { get; set; }
        public int Year { get; set; }
    }
}

回答by anthonyv

Based on what everyone has contributed I have come up with the following.

根据每个人的贡献,我想出了以下内容。

It provides bi-directional sorting as well solving the problem inside out. Meaning it didn't make much sense to me that a new Sorter need to be created for every unsorted list of a given type. Why can't this the unsorted list be passed into the sorter. This then means that we could create a signelton instance of the Sorter for our different types...

它提供双向排序以及从内到外解决问题。这意味着需要为给定类型的每个未排序列表创建一个新的排序器对我来说没有多大意义。为什么不能将未排序的列表传递到排序器中。这意味着我们可以为我们的不同类型创建一个 Sorter 的 signelton 实例......

Just an idea:

只是一个想法:

[TestClass]
public class SpecifyUserDefinedSorting
{
    private Sorter<Movie> sorter;
    private IQueryable<Movie> unsorted;

    [TestInitialize]
    public void Setup()
    {
        unsorted = from m in Movies select m;
        sorter = new Sorter<Movie>();
        sorter.Register("Name", m1 => m1.Name);
        sorter.Register("Year", m2 => m2.Year);
    }

    [TestMethod]
    public void SortByNameThenYear()
    {
        var instructions = new List<SortInstrcution>()
                               {
                                   new SortInstrcution() {Name = "Name"},
                                   new SortInstrcution() {Name = "Year"}
                               };
        var sorted = sorter.SortBy(unsorted, instructions);
        var movies = sorted.ToArray();

        Assert.AreEqual(movies[0].Name, "A");
        Assert.AreEqual(movies[0].Year, 2000);
        Assert.AreEqual(movies[1].Year, 2001);
        Assert.AreEqual(movies[2].Name, "B");
    }

    [TestMethod]
    public void SortByNameThenYearDesc()
    {
        var instructions = new List<SortInstrcution>()
                               {
                                   new SortInstrcution() {Name = "Name", Direction = SortDirection.Descending},
                                   new SortInstrcution() {Name = "Year", Direction = SortDirection.Descending}
                               };
        var sorted = sorter.SortBy(unsorted, instructions);
        var movies = sorted.ToArray();

        Assert.AreEqual(movies[0].Name, "B");
        Assert.AreEqual(movies[0].Year, 1990);
        Assert.AreEqual(movies[1].Name, "A");
        Assert.AreEqual(movies[1].Year, 2001);
        Assert.AreEqual(movies[2].Name, "A");
        Assert.AreEqual(movies[2].Year, 2000);
    }

    [TestMethod]
    public void SortByNameThenYearDescAlt()
    {
        var instructions = new List<SortInstrcution>()
                               {
                                   new SortInstrcution() {Name = "Name", Direction = SortDirection.Descending},
                                   new SortInstrcution() {Name = "Year"}
                               };
        var sorted = sorter.SortBy(unsorted, instructions);
        var movies = sorted.ToArray();

        Assert.AreEqual(movies[0].Name, "B");
        Assert.AreEqual(movies[0].Year, 1990);
        Assert.AreEqual(movies[1].Name, "A");
        Assert.AreEqual(movies[1].Year, 2000);
        Assert.AreEqual(movies[2].Name, "A");
        Assert.AreEqual(movies[2].Year, 2001);
    }

    [TestMethod]
    public void SortByYearThenName()
    {
        var instructions = new List<SortInstrcution>()
                               {
                                   new SortInstrcution() {Name = "Year"},
                                   new SortInstrcution() {Name = "Name"}
                               };
        var sorted = sorter.SortBy(unsorted, instructions); 
        var movies = sorted.ToArray();

        Assert.AreEqual(movies[0].Name, "B");
        Assert.AreEqual(movies[1].Year, 2000);
    }

    [TestMethod]
    public void SortByYearOnly()
    {
        var instructions = new List<SortInstrcution>()
                               {
                                   new SortInstrcution() {Name = "Year"} 
                               };
        var sorted = sorter.SortBy(unsorted, instructions); 
        var movies = sorted.ToArray();

        Assert.AreEqual(movies[0].Name, "B");
    }

    private static IQueryable<Movie> Movies
    {
        get { return CreateMovies().AsQueryable(); }
    }

    private static IEnumerable<Movie> CreateMovies()
    {
        yield return new Movie { Name = "B", Year = 1990 };
        yield return new Movie { Name = "A", Year = 2001 };
        yield return new Movie { Name = "A", Year = 2000 };
    }
}


public static class SorterExtension
{
    public static IOrderedQueryable<T> SortBy<T>(this IQueryable<T> source, Sorter<T> sorter, IEnumerable<SortInstrcution> instrcutions)
    {
        return sorter.SortBy(source, instrcutions);
    }
}

public class Sorter<TSource>
{
    private readonly FirstPasses _FirstPasses;
    private readonly FirstPasses _FirstDescendingPasses;
    private readonly NextPasses _NextPasses;
    private readonly NextPasses _NextDescendingPasses; 

    public Sorter()
    {
        this._FirstPasses = new FirstPasses();
        this._FirstDescendingPasses = new FirstPasses();
        this._NextPasses = new NextPasses();
        this._NextDescendingPasses = new NextPasses();
    }


    public void Register<TKey>(string name, Expression<Func<TSource, TKey>> selector)
    {
        this._FirstPasses.Add(name, s => s.OrderBy(selector));
        this._FirstDescendingPasses.Add(name, s => s.OrderByDescending(selector));
        this._NextPasses.Add(name, s => s.ThenBy(selector));
        this._NextDescendingPasses.Add(name, s => s.ThenByDescending(selector));
    }


    public IOrderedQueryable<TSource> SortBy(IQueryable<TSource> source, IEnumerable<SortInstrcution> instrcutions)
    {
        IOrderedQueryable<TSource> result = null;

        foreach (var instrcution in instrcutions) 
            result = result == null ? this.SortFirst(instrcution, source) : this.SortNext(instrcution, result); 

        return result;
    }

    private IOrderedQueryable<TSource> SortFirst(SortInstrcution instrcution, IQueryable<TSource> source)
    {
        if (instrcution.Direction == SortDirection.Ascending)
            return this._FirstPasses[instrcution.Name].Invoke(source);
        return this._FirstDescendingPasses[instrcution.Name].Invoke(source);
    }

    private IOrderedQueryable<TSource> SortNext(SortInstrcution instrcution, IOrderedQueryable<TSource> source)
    {
        if (instrcution.Direction == SortDirection.Ascending)
            return this._NextPasses[instrcution.Name].Invoke(source);
        return this._NextDescendingPasses[instrcution.Name].Invoke(source);
    }

    private class FirstPasses : Dictionary<string, Func<IQueryable<TSource>, IOrderedQueryable<TSource>>> { }

    private class NextPasses : Dictionary<string, Func<IOrderedQueryable<TSource>, IOrderedQueryable<TSource>>> { } 
}


internal class Movie
{
    public string Name { get; set; }
    public int Year { get; set; }
}

public class SortInstrcution
{
    public string Name { get; set; }

    public SortDirection Direction { get; set; }
}

public enum SortDirection   
{
    //Note I have created this enum because the one that exists in the .net 
    // framework is in the web namespace...
    Ascending,
    Descending
}

Note if you didn't want to have a dependency on SortInstrcution it wouldn't be that hard to change.

请注意,如果您不想依赖 SortInstrcution,则更改它并不难。

Hope this helps someone.

希望这可以帮助某人。

回答by Marty Spallone

I liked the work above - thank you very much! I took the liberty to add a couple things:

我喜欢上面的工作 - 非常感谢!我冒昧地添加了几件事:

  1. Added sort direction.

  2. Made registering and calling two different concerns.

  1. 添加排序方向。

  2. 注册和调用两个不同的问题。

Usage:

用法:

var censusSorter = new Sorter<CensusEntryVM>();
censusSorter.AddSortExpression("SubscriberId", e=>e.SubscriberId);
censusSorter.AddSortExpression("LastName", e => e.SubscriberId);

View.CensusEntryDataSource = censusSorter.Sort(q.AsQueryable(), 
    new Tuple<string, SorterSortDirection>("SubscriberId", SorterSortDirection.Descending),
    new Tuple<string, SorterSortDirection>("LastName", SorterSortDirection.Ascending))
    .ToList();



internal class Sorter<E>
{
    public Sorter()
    {
    }
    public void AddSortExpression<P>(string name, Expression<Func<E, P>> selector)
    {
        // Register all possible types of sorting for each parameter
        firstPasses.Add(name, s => s.OrderBy(selector));
        nextPasses.Add(name, s => s.ThenBy(selector));
        firstPassesDesc.Add(name, s => s.OrderByDescending(selector));
        nextPassesDesc.Add(name, s => s.OrderByDescending(selector));
    } 

    public IOrderedQueryable<E> Sort(IQueryable<E> list, 
                                     params Tuple<string, SorterSortDirection>[] names) 
    { 
        IOrderedQueryable<E> result = null; 
        foreach (var entry in names)
        {
            result = result == null 
                   ? SortFirst(entry.Item1, entry.Item2, list) 
                   : SortNext(entry.Item1, entry.Item2, result); 
        } 
        return result; 
    } 
    private IOrderedQueryable<E> SortFirst(string name, SorterSortDirection direction, 
                                           IQueryable<E> source) 
    { 
        return direction == SorterSortDirection.Descending
             ? firstPassesDesc[name].Invoke(source) 
             : firstPasses[name].Invoke(source);
    } 

    private IOrderedQueryable<E> SortNext(string name, SorterSortDirection direction, 
                                          IOrderedQueryable<E> source) 
    {
        return direction == SorterSortDirection.Descending
             ? nextPassesDesc[name].Invoke(source) 
             : nextPasses[name].Invoke(source); 
    }

    private readonly FirstPasses firstPasses = new FirstPasses(); 
    private readonly NextPasses nextPasses = new NextPasses();
    private readonly FirstPasses firstPassesDesc = new FirstPasses();
    private readonly NextPasses nextPassesDesc = new NextPasses();

    private class FirstPasses : Dictionary<string, Func<IQueryable<E>, IOrderedQueryable<E>>> { }
    private class NextPasses : Dictionary<string, Func<IOrderedQueryable<E>, IOrderedQueryable<E>>> { }
}