Динамический LINQ OrderBy для IEnumerable <T> / IQueryable <T>

670

Я нашел пример в VS2008 Примеры для динамического LINQ, который позволяет использовать sql-подобную строку (например, OrderBy("Name, Age DESC"))для упорядочивания. К сожалению, включенный метод работает только на IQueryable<T>. Есть ли способ включить эту функцию IEnumerable<T>?

Джон Шиэн
источник
1
На мой взгляд, лучший ответ на эту дату: библиотека System.Linq.Dynamic.Core .
Шахин Дохан,

Ответы:

905

Просто наткнулся на этого старичка ...

Чтобы сделать это без динамической библиотеки 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 (деревья выражений для ORM и т. Д. Не могут действительно представлять 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);
        }
    }
}
Марк Гравелл
источник
109
Лучший чертов кусок кода, который я видел :) Только что решил миллион проблем в моем проекте :)
sajidnizami
4
@ Dave - вы должны начать с IQueryable<T>, так что если у вас есть что - то подобное List<T>(что IEnumerable<T>) вам может понадобиться использование AsQueryable()- напримерvar sorted = someList.AsQueryable().OrderBy("Foo.Bar");
Марк Gravell
7
Вы видели это ... это могло бы помочь некоторым людям ... stackoverflow.com/questions/557819/… это более строго типизированное решение.
Антониев
28
@MGO, кажется, вы неправильно понимаете природу кода. 40 строк одинаковы, независимо от того, что это 40 строк, которые вы поместили где-то в вашем проекте, или эти строки (предварительно скомпилированные или как исходные) во внешней библиотеке. Было бы довольно удивительно, если бы я связал в октябре '08 с библиотекой nuget, которая существует с декабря '11 (не в последнюю очередь потому, что тогда и не существовало nuget), но фундаментальное «что он делает» это то же. Кроме того, вы используете фразу «фактическое решение», как будто есть какой-то четко определенный согласованный единый путь к каждому вопросу кодирования: нет.
Марк Гравелл
5
@MGOwen между прочим, внешняя библиотека содержит 2296 строк кода (не включая AssemblyInfo.cs); из-за чего эти 40 строк выглядят вполне разумно
Марк Грэвелл
231

Слишком легко без каких-либо осложнений:

  1. Добавьте using System.Linq.Dynamic;вверху.
  2. использование vehicles = vehicles.AsQueryable().OrderBy("Make ASC, Year DESC").ToList();
Алаа Оста
источник
11
а откуда ты взял System.Linq.Dynamic?
Dementic
1
Работает и при использовании linq с MongoDB.
souy1976
32
Принятый ответ, возможно, был правильным ответом в 2008 году, но в настоящее время это самый простой, самый правильный ответ сейчас.
EL MOJO
1
Это действительно хорошая и простая обработка, такая внутренняя сложность, она мне понравилась
Мринал Камбой
5
Для людей в «будущем», если вы используете ядро ​​dotnet, используйте это: nuget.org/packages/System.Linq.Dynamic.Core
Рафаэль Мерлин
78

Я нашел ответ. Я могу использовать .AsQueryable<>()метод расширения, чтобы преобразовать мой список в IQueryable, а затем запустить динамический порядок для него.

Джон Шиэн
источник
52
Пожалуйста, предоставьте пример для остальных из нас.
MGOwen
54

Просто наткнулся на этот вопрос.

Используя вышеописанную реализацию ApplyOrder от Marc, я собрал метод Extension, который обрабатывает подобные SQL строки:

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

Подробности можно найти здесь: http://aonnull.blogspot.com/2010/08/dynamic-sql-like-linq-orderby-extension.html

Адам Андерсон
источник
1
Отличная вещь, просто добавьте модификацию следующим образом, чтобы сделать имя свойства нечувствительным к регистру: PropertyInfo pi = type.GetProperty (prop, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
Мринал Камбой
43

Я полагаю, будет полезно использовать отражение, чтобы получить любое свойство, по которому вы хотите отсортировать:

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 не хочет значения, но селектор lamba / делегат (Func <TSource, TKey> keySelector) ..
Дэви Лэндман
2
Я попробовал этот пример перед публикацией, и да, он работает.
Кжетил Ватнедал
3
+1 Это именно то, что я искал! Это отлично подойдет для простых задач сортировки страниц.
Эндрю Симер
Это не сработало для меня. Я что-то пропустил? Каким должен быть "SomeProperty". Я попытался дать имя свойства, а также property.GetType (). У меня есть IQueryable <>, а не IEnumerable <>
ТАК пользователь
2
@ Алекс Шкор: Как вы должны сортировать элементы, не глядя на все элементы? Однако в других ответах есть лучшие решения.
Кжетил Ватнедал
19

Просто опираясь на то, что сказали другие. Я обнаружил, что следующее работает довольно хорошо.

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;
}
vdhant
источник
12

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

Вот как это сделать:

var query = pets.OrderBy(pet => pet.Name).ThenByDescending(pet => pet.Age);    
InfoStatus
источник
5
+1 отменил понижающее голосование из-за отсутствия объяснения. Я также думаю, что автор, возможно, был заинтересован в нескольких заказах. Даже если динамическое было ключевым словом, нет причин для отрицательного голосования.
Джейсон Клебан
11

Я пытался сделать это, но у меня возникли проблемы с решением 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() : "";
}

И последнее - я упомянул, что хочу OrderByиспользовать пользовательские настройки IComparer- потому что я хотел сделать естественную сортировку .

Для этого я просто изменяю на OrderBy:

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

Смотрите этот пост для кода для NaturalSortComparer().

Джеймс МакКормак
источник
5

Используйте динамический linq

просто добавь using System.Linq.Dynamic;

И используйте это так, чтобы упорядочить все ваши столбцы:

string sortTypeStr = "ASC"; // or DESC
string SortColumnName = "Age"; // Your column name
query = query.OrderBy($"{SortColumnName} {sortTypeStr}");
Масуд Дарвишян
источник
4

Вы можете добавить это:

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Функция от ответа Кьетил Watnedal в

Вопрос был бы почему? Любая такая сортировка будет генерировать исключения во время выполнения, а не во время компиляции (как ответ D2VIANT).

Если вы имеете дело с Linq to Sql, а orderby - это дерево выражений, оно все равно будет преобразовано в SQL для выполнения.

Кит
источник
Mehotod GetPropertyValue будет выполнен для всех элементов, это плохое решение.
Алекс Шкор
2
OrderByне поддерживать предыдущий порядок!
Амир Исмаил
4

Вот еще кое-что, что я нашел интересным. Если ваш источник - DataTable, вы можете использовать динамическую сортировку без использования Dynamic Linq.

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 (Использование DataSetExtensions)

Вот еще один способ сделать это, преобразовав его в DataView:

DataTable contacts = dataSet.Tables["Contact"];    
DataView view = contacts.AsDataView();    
view.Sort = "LastName desc, FirstName asc";    
bindingSource1.DataSource = view;
dataGridView1.AutoResizeColumns();
Самир Алибхай
источник
4

Благодаря Maarten ( Запрос коллекции с использованием объекта PropertyInfo в LINQ ) я получил это решение:

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

В моем случае я работал над «ColumnHeaderMouseClick» (WindowsForm), поэтому просто нашел конкретный нажатой столбец и его соответствующий PropertyInfo:

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

ИЛИ

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

(убедитесь, что имена столбцов совпадают со свойствами объекта)

ура

joaopintocruz
источник
4

После долгих поисков это сработало для меня:

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);
}
Sanchitos
источник
4

Вы можете конвертировать IEnumerable в IQueryable.

items = items.AsQueryable().OrderBy("Name ASC");
Ричард Ю.С.
источник
3

Альтернативное решение использует следующий класс / интерфейс. Это не совсем динамично, но работает.

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;
    }
}
Майк Кристиансен
источник
2

Этот ответ является ответом на комментарии, которым нужен пример решения, предоставленного @John Sheehan - Runscope

Пожалуйста, предоставьте пример для остальных из нас.

в DAL (уровень доступа к данным),

IEnumerable версия:

  public  IEnumerable<Order> GetOrders()
    {
      // i use Dapper to return IEnumerable<T> using Query<T>
      //.. do stuff
      return  orders  // IEnumerable<Order>
  }

IQueryable версия

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

Теперь вы можете использовать версию IQueryable для привязки, например, GridView в Asp.net и использовать преимущества для сортировки (вы не можете сортировать, используя версию IEnumerable)

Я использовал Dapper в качестве ORM и собрал версию IQueryable и использовал сортировку в GridView в asp.net так легко.

M.Hassan
источник
2

Сначала установите Dynamic Tools -> Диспетчер пакетов NuGet -> Консоль диспетчера пакетов

install-package System.Linq.Dynamic

Добавить пространство имен using System.Linq.Dynamic;

Теперь вы можете использовать OrderBy("Name, Age DESC")

Аминур Рахман
источник
Как я могу использовать это с внутренней сортировкой свойств - как OrderBy ("Branch.BranchName", "Descending")
devC
Это работает для меня. Возможно, потому что вопрос 10 лет, и этот более простой метод пришел позже.
кошерная медуза
1

Вы можете использовать это:

        public List<Book> Books(string orderField, bool desc, int skip, int take)
{
    var propertyInfo = typeof(Book).GetProperty(orderField);

    return _context.Books
        .Where(...)
        .OrderBy(p => !desc ? propertyInfo.GetValue(p, null) : 0)
        .ThenByDescending(p => desc ? propertyInfo.GetValue(p, null) : 0)
        .Skip(skip)
        .Take(take)
        .ToList();
}
k1developer
источник
Через пару лет я наткнулся на это; это сработало для меня, как во сне. У меня есть динамическая сортировка от 1 до 3 свойств, и это работает как сон. Легко реализовать и без проблем.
Базинга
0

Преобразуйте List в IEnumerable или Iquerable, добавьте с помощью пространства имен System.LINQ.Dynamic, после чего вы можете упомянуть имена свойств в строке, разделенной запятой, в метод OrderBy, который по умолчанию поставляется из System.LINQ.Dynamic.

user145610
источник
-3
var result1 = lst.OrderBy(a=>a.Name);// for ascending order. 
 var result1 = lst.OrderByDescending(a=>a.Name);// for desc order. 
Arindam
источник