Сильно типизированная динамическая сортировка Linq - PullRequest
25 голосов
/ 17 февраля 2009

Я пытаюсь создать код для динамической сортировки Linq IQueryable <>.

Здесь очевидный способ сортировки списка с использованием строки для имени поля
http://dvanderboom.wordpress.com/2008/12/19/dynamically-composing-linq-orderby-clauses/

Однако мне нужно одно изменение - проверка времени компиляции имен полей и возможность использовать рефакторинг / Найти все ссылки для поддержки последующего обслуживания. Это означает, что я хочу определить поля как f => f.Name, а не как строки.

Для моего конкретного использования я хочу инкапсулировать некоторый код, который бы решал, какое из списка именованных выражений "OrderBy" должно использоваться на основе пользовательского ввода, без написания разного кода каждый раз.

Вот суть того, что я написал:

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, ...)

Функция GetSortedList определяет, какой из именованных видов использовать, что приводит к созданию объекта List, где каждый FieldData содержит значения MethodInfo и Type полей, переданных в AddSort:

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

Я не уверен, есть ли способ сохранить весь объект поля таким образом, чтобы он мог быть возвращен позже (это было бы невозможно привести, поскольку это универсальный тип)

Есть ли способ, которым я мог бы адаптировать пример кода или придумать совершенно новый код, чтобы сортировать, используя строго типизированные имена полей после они были сохранены в каком-либо контейнере и извлечены (потеряв универсальный тип литья)

Ответы [ 4 ]

17 голосов
/ 18 февраля 2009

Самый простой способ сделать это состоит в том, чтобы ваша функция AddSort () принимала выражение > вместо простого 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;
    }
}

В этом случае я использовал объект в качестве возвращаемого типа функции, потому что он легко конвертируется, но вы можете реализовать это с другими типами или обобщениями, в зависимости от ситуации, если вам требуется больше функциональности. В этом случае, поскольку выражение только для проверки, это не имеет значения.

Другой возможный способ - взять Func и сохранить его в самом словаре. Затем, когда дело доходит до сортировки, и вам нужно получить значение для сортировки, вы можете вызвать что-то вроде:

// assuming a dictionary of fields to sort for, called m_fields
m_fields[fieldName](currentItem)
8 голосов
/ 08 мая 2010

Исходя из того, что каждый внес вклад, я пришел к следующему.

Обеспечивает двунаправленную сортировку, а также решение проблемы наизнанку. Это означало, что для меня не имело особого смысла, что новый Сортировщик должен быть создан для каждого несортированного списка данного типа. Почему этот несортированный список нельзя передать в сортировщик. Это означает, что мы могли бы создать экземпляр сортировщика Signelton для наших различных типов ...

Просто идея:

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

Обратите внимание, что если вы не хотите зависеть от SortInstrcution, это не так сложно изменить.

Надеюсь, это кому-нибудь поможет.

8 голосов
/ 18 февраля 2009

Облом! Я должен научиться читать спецификации от начала до конца: - (

Однако теперь, когда я потратил слишком много времени на то, чтобы дурачиться, а не работать, я все равно опубликую свои результаты, надеясь, что это вдохновит людей читать, думать, понимать (важно), а затем действовать. Или как быть слишком умным с дженериками, лямбдами и забавными вещами Linq.

Аккуратный трюк, который я обнаружил во время этого упражнение, являются ли они частным внутренним классы, которые происходят от Dictionary. Вся их цель состоит в том, чтобы удалить все эти угловые скобки для того, чтобы улучшить читаемость.

О, почти забыл код:

ОБНОВЛЕНИЕ: Сделан общий код и использовать 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; }
    }
}
3 голосов
/ 10 августа 2011

Мне понравилась работа выше - большое спасибо! Я позволил себе добавить пару вещей:

  1. Добавлено направление сортировки.

  2. Сделана регистрация и вызов двух разных проблем.

Использование:

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>>> { }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...