Dynamiczne zamówienie LINQ przez IEnumerable <T> / IQueryable <T>

668

Znalazłem przykład w VS2008 Przykłady dla dynamicznego LINQ, który pozwala na użycie łańcucha podobnego do sql (np. OrderBy("Name, Age DESC"))Do zamawiania. Niestety, dołączona metoda działa tylko na IQueryable<T>. Czy jest jakiś sposób na włączenie tej funkcjonalności IEnumerable<T>?

John Sheehan
źródło
1
Najlepsza odpowiedź na ten dzień, moim zdaniem: biblioteka System.Linq.Dynamic.Core .
Shahin Dohan,

Odpowiedzi:

904

Właśnie natknąłem się na tego staruszka ...

Aby to zrobić bez dynamicznej biblioteki LINQ, wystarczy kod jak poniżej. Dotyczy to najczęstszych scenariuszy, w tym właściwości zagnieżdżonych.

Aby to działało, IEnumerable<T>możesz dodać kilka metod otoki, które przechodzą AsQueryable- ale poniższy kod jest podstawową Expressionlogiką.

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

Edycja: staje się bardziej zabawna, jeśli chcesz to mieszać dynamic- chociaż uwaga dynamicdotyczy tylko LINQ-do-Obiektów (drzewa wyrażeń dla ORM itp. Nie mogą tak naprawdę reprezentować dynamiczapytań - MemberExpressionnie obsługuje tego). Ale oto sposób na zrobienie tego za pomocą LINQ-to-Objects. Zauważ, że wybór Hashtablewynika z korzystnej semantyki blokowania:

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);
        }
    }
}
Marc Gravell
źródło
109
Najlepszy cholerny fragment kodu, jaki widziałem :) Właśnie rozwiązałem milion problemów w moim projekcie :)
sajidnizami
4
@Dave - musisz zacząć IQueryable<T>, więc jeśli masz coś takiego List<T>(co jest IEnumerable<T>), możesz potrzebować użyć AsQueryable()- na przykładvar sorted = someList.AsQueryable().OrderBy("Foo.Bar");
Marc Gravell
7
Widziałeś to ... może pomóc niektórym ludziom ... stackoverflow.com/questions/557819/ ... jest to bardziej typowe rozwiązanie.
Anthonyv
28
@MGOWydajesz się, że źle rozumiesz naturę kodu. 40 linii jest takich samych, bez względu na to, czy jest to 40 linii, które umieścisz gdzieś w swoim projekcie, czy te linie są (wstępnie skompilowane lub jako źródło) w bibliotece zewnętrznej. Byłoby całkiem niesamowite, gdybym połączył link w październiku 2008 r. Z biblioteką na nugecie, która istniała od grudnia '11 (nie tylko dlatego, że nuget też wtedy nie istniał), ale fundamentalne „to, co robi” to to samo. Używasz również wyrażenia „rzeczywiste rozwiązanie”, tak jakby istniała dobrze określona, ​​uzgodniona jedna droga do każdego pytania kodującego: nie ma.
Marc Gravell
5
@MGOwen btw, zewnętrzna biblioteka ma 2296 linii kodu (nie licząc AssemblyInfo.cs); co sprawia, że ​​40 linii tutaj wygląda całkiem rozsądnie
Marc Gravell
231

Zbyt łatwe bez komplikacji:

  1. Dodaj using System.Linq.Dynamic;u góry.
  2. Posługiwać się vehicles = vehicles.AsQueryable().OrderBy("Make ASC, Year DESC").ToList();
Alaa Osta
źródło
11
i skąd masz System.Linq.Dynamic?
Demencja
1
Działa również przy korzystaniu z linq z MongoDB.
soupy1976,
32
Przyjęta odpowiedź mogła być poprawną odpowiedzią w 2008 roku, ale obecnie jest to najłatwiejsza, najbardziej poprawna odpowiedź teraz.
EL MOJO
1
To naprawdę dobra i prosta obsługa, tyle złożoności wewnętrznie, bardzo mi się podobało
Mrinal Kamboj
5
Dla ludzi w „przyszłości”, jeśli używasz dotnet core, użyj tego: nuget.org/packages/System.Linq.Dynamic.Core
Rafael Merlin
78

Znalazłem odpowiedź Mogę użyć .AsQueryable<>()metody rozszerzenia do przekonwertowania mojej listy na IQueryable, a następnie uruchomić przeciwko niej dynamiczną kolejność.

John Sheehan
źródło
52
Podaj przykład dla reszty z nas.
MGOwen
54

Natknąłem się na to pytanie.

Korzystając z implementacji MarcOr ApplyOrder z góry, spoliczniłem metodę Extension, która obsługuje ciągi podobne do SQL, takie jak:

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

Szczegóły można znaleźć tutaj: http://aonnull.blogspot.com/2010/08/dynamic-sql-like-linq-orderby-extension.html

Adam Anderson
źródło
1
Świetne rzeczy, po prostu dodaj następującą modyfikację, aby nazwa właściwości nie rozróżniała wielkości liter: PropertyInfo pi = type.GetProperty (prop, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
Mrinal Kamboj
43

Wydaje mi się, że warto użyć refleksji, aby uzyskać dowolną właściwość, na której chcesz sortować:

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

Zauważ, że użycie odbicia jest znacznie wolniejsze niż bezpośredni dostęp do właściwości, więc należałoby zbadać wydajność.

Kjetil Watnedal
źródło
czy to w ogóle działa? orderby nie chce wartości, ale selektor lamba / delegate (Func <TSource, TKey> keySelector) ..
Davy Landman
2
Wypróbowałem ten przykład przed opublikowaniem go i tak, działa.
Kjetil Watnedal
3
+1 Właśnie tego szukałem! Działa to doskonale w przypadku prostych problemów z sortowaniem stron.
Andrew Siemer,
To nie działało dla mnie. Czy coś brakuje? Czym powinna być „SomeProperty”. Próbowałem podać nazwę właściwości oraz property.GetType (). Mam IQueryable <> i nie IEnumerable <>
SO Użytkownik
2
@Alex Shkor: Jak masz sortować elementy bez patrzenia na wszystkie elementy? Istnieją jednak lepsze rozwiązania w innych odpowiedziach.
Kjetil Watnedal
19

Opierając się na tym, co powiedzieli inni. Odkryłem, że następujące działa całkiem dobrze.

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
źródło
12

Natknąłem się na to pytanie, szukając wielokrotnych klauzul porządkowych Linq i być może tego szukał autor

Oto jak to zrobić:

var query = pets.OrderBy(pet => pet.Name).ThenByDescending(pet => pet.Age);    
InfoStatus
źródło
5
+1 anulowało głosowanie w dół z powodu braku wyjaśnienia. Myślę też, że autor mógł być zainteresowany wieloma zamówieniami. Nawet jeśli dynamiczny był kluczowym słowem, nie ma powodu do zahamowania głosowania.
Jason Kleban
11

Próbowałem to zrobić, ale miałem problemy z rozwiązaniem Kjetil Watnedal, ponieważ nie używam wbudowanej składni linq - wolę składnię w stylu metody. Moim specyficznym problemem była próba dynamicznego sortowania przy użyciu niestandardowego IComparer.

Moje rozwiązanie skończyło się tak:

Biorąc pod uwagę zapytanie IQueryable:

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

I biorąc pod uwagę argument pola sortowania w czasie wykonywania:

string SortField; // Set at run-time to "Name"

Dynamiczny OrderBy wygląda tak:

query = query.OrderBy(item => item.GetReflectedPropertyValue(SortField));

I to przy użyciu małej metody pomocniczej o nazwie GetReflectedPropertyValue ():

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

I ostatnia rzecz - wspomniałem, że chcę OrderByużyć niestandardowego IComparer- ponieważ chciałem dokonać naturalnego sortowania .

Aby to zrobić, po prostu zmieniam na OrderBy:

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

Zobacz ten post dla kodu dla NaturalSortComparer().

James McCormack
źródło
5

Użyj dynamicznego linq

poprostu dodaj using System.Linq.Dynamic;

I użyj go w ten sposób, aby uporządkować wszystkie kolumny:

string sortTypeStr = "ASC"; // or DESC
string SortColumnName = "Age"; // Your column name
query = query.OrderBy($"{SortColumnName} {sortTypeStr}");
Masoud Darvishian
źródło
4

Możesz to dodać:

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
}

Ta GetPropertyValuefunkcja pochodzi z odpowiedzi Kjetila Watnedala

Problemem byłoby dlaczego? Każdy tego rodzaju generowałby wyjątki w czasie wykonywania, a nie kompilował (jak odpowiedź D2VIANT).

Jeśli masz do czynienia z Linq do Sql, a kolejność jest drzewem wyrażeń, to i tak zostanie przekonwertowane na SQL.

Keith
źródło
MePod GetPropertyValue zostanie wykonany dla wszystkich elementów, to złe rozwiązanie.
Alex Shkor
2
OrderBynie utrzymuj poprzedniego zamówienia !!
Amir Ismail
4

Oto coś, co uznałem za interesujące. Jeśli źródłem jest DataTable, możesz użyć sortowania dynamicznego bez użycia 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;

odniesienie: http://msdn.microsoft.com/en-us/library/bb669083.aspx (Korzystanie z DataSetExtensions)

Oto jeszcze jeden sposób, aby to zrobić, przekształcając go w DataView:

DataTable contacts = dataSet.Tables["Contact"];    
DataView view = contacts.AsDataView();    
view.Sort = "LastName desc, FirstName asc";    
bindingSource1.DataSource = view;
dataGridView1.AutoResizeColumns();
Sameer Alibhai
źródło
4

Dzięki Maarten ( zapytanie kolekcji przy użyciu obiektu PropertyInfo w LINQ ) otrzymałem to rozwiązanie:

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

W moim przypadku pracowałem nad „ColumnHeaderMouseClick” (WindowsForm), więc właśnie znalazłem konkretną kolumnę wciśniętą i odpowiadającą jej właściwość PropertyInfo:

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

LUB

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

(pamiętaj, aby Nazwy kolumn pasowały do ​​właściwości obiektu)

Twoje zdrowie

joaopintocruz
źródło
4

Po wielu poszukiwaniach zadziałało to dla mnie:

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
źródło
4

Możesz przekonwertować IEnumerable na IQueryable.

items = items.AsQueryable().OrderBy("Name ASC");
Richard YS
źródło
3

Alternatywne rozwiązanie wykorzystuje następującą klasę / interfejs. Nie jest tak naprawdę dynamiczny, ale działa.

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;
    }
}
Mike Christiansen
źródło
2

Ta odpowiedź jest odpowiedzią na komentarze, które potrzebują przykładu rozwiązania dostarczonego przez @John Sheehan - Runscope

Podaj przykład dla reszty z nas.

w DAL (warstwa dostępu do danych),

Wersja IEnumerable:

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

Wersja IQueryable

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

Teraz możesz używać wersji IQueryable do wiązania, na przykład GridView w Asp.net i korzystać z sortowania (nie można sortować przy użyciu wersji IEnumerable)

Użyłem Dappera jako ORM i zbudowałem wersję IQueryable i tak łatwo posortowałem w GridView w asp.net.

M.Hassan
źródło
2

Najpierw zainstaluj narzędzia dynamiczne -> Menedżer pakietów NuGet -> Konsola menedżera pakietów

install-package System.Linq.Dynamic

Dodaj przestrzeń nazw using System.Linq.Dynamic;

Teraz możesz użyć OrderBy("Name, Age DESC")

Aminur Rahman
źródło
Jak mogę go używać z wewnętrznym sortowaniem właściwości - jak OrderBy („Branch.BranchName”, „Descending”)
devC
To działa dla mnie. Być może dlatego, że pytanie ma 10 lat, a ta łatwiejsza metoda pojawiła się dopiero później.
kosherjellyfish
1

Możesz użyć tego:

        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
źródło
Kilka lat później natknęłam się na to; to działało dla mnie jak sen. Mam dynamiczne sortowanie według 1 do 3 właściwości, a to działa jak sen. Łatwy do wdrożenia i bezproblemowy.
Bazïnga
0

Konwertuj listę na IEnumerable lub Iquerable, dodaj używając System.LINQ.Dynamic przestrzeni nazw, a następnie możesz wspomnieć nazwy właściwości w łańcuchu oddzielonym przecinkami do metody OrderBy, która domyślnie pochodzi z System.LINQ.Dynamic.

użytkownik145610
źródło
-3
var result1 = lst.OrderBy(a=>a.Name);// for ascending order. 
 var result1 = lst.OrderByDescending(a=>a.Name);// for desc order. 
Arindam
źródło