Dynamic LINQ OrderBy auf IEnumerable <T> / IQueryable <T>

668

In den VS2008-Beispielen für Dynamic LINQ habe ich ein Beispiel gefunden , mit dem Sie eine SQL-ähnliche Zeichenfolge verwenden können (z OrderBy("Name, Age DESC")). B. zum Bestellen. Leider funktioniert die enthaltene Methode nur IQueryable<T>. Gibt es eine Möglichkeit, diese Funktionalität zu aktivieren IEnumerable<T>?

John Sheehan
quelle
1
Die beste Antwort ab diesem Datum meiner Meinung nach: System.Linq.Dynamic.Core Bibliothek.
Shahin Dohan

Antworten:

903

Bin gerade in diesen Oldie gestolpert ...

Um dies ohne die dynamische LINQ-Bibliothek zu tun, benötigen Sie nur den folgenden Code. Dies umfasst die häufigsten Szenarien, einschließlich verschachtelter Eigenschaften.

Um es zum Laufen zu bringen, können IEnumerable<T>Sie einige Wrapper-Methoden hinzufügen, die über AsQueryable- aber der folgende Code ist die ExpressionKernlogik, die benötigt wird.

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

Bearbeiten: Es macht mehr Spaß, wenn Sie dies mit mischen möchten dynamic- obwohl der Hinweis dynamicnur für LINQ-to-Objects gilt (Ausdrucksbäume für ORMs usw. können dynamicAbfragen nicht wirklich darstellen - unterstützt MemberExpressiondies nicht). Aber hier ist eine Möglichkeit, dies mit LINQ-to-Objects zu tun. Beachten Sie, dass die Auswahl von Hashtableauf eine günstige Sperrsemantik zurückzuführen ist:

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
quelle
109
Bester verdammter Code, den ich je gesehen habe :) Ich habe gerade eine Million Probleme in meinem Projekt gelöst :)
sajidnizami
4
@ Dave - Sie müssen mit beginnen IQueryable<T>, also wenn Sie etwas wie List<T>(was ist IEnumerable<T>) haben, müssen Sie möglicherweise AsQueryable()- zum Beispielvar sorted = someList.AsQueryable().OrderBy("Foo.Bar");
Marc Gravell verwenden
7
Haben Sie das gesehen ... es könnte einigen Leuten helfen ... stackoverflow.com/questions/557819/… es ist eine stärker typisierte Lösung.
Anthonyv
28
@MGOwen Sie scheinen die Natur des Codes falsch zu verstehen. Die 40 Zeilen sind gleich, unabhängig davon, ob es sich um 40 Zeilen handelt, die Sie irgendwo in Ihr Projekt eingefügt haben, oder ob diese Zeilen (vorkompiliert oder als Quelle) in einer externen Bibliothek enthalten sind. Es wäre ziemlich erstaunlich gewesen, wenn ich im Oktober 2008 eine Bibliothek auf Nuget verlinkt hätte, die seit Dezember 11 existiert (nicht zuletzt, weil Nuget damals auch nicht existierte), aber das grundlegende "was es tut" ist das Gleiche. Außerdem verwenden Sie den Ausdruck "tatsächliche Lösung" so, als gäbe es für jede Codierungsfrage einen genau definierten, vereinbarten Einzelweg: Dies gibt es nicht.
Marc Gravell
5
@MGOwen übrigens, die externe Bibliothek besteht aus 2296 Codezeilen (ohne AssemblyInfo.cs); was die 40 Zeilen hier ziemlich vernünftig aussehen lässt
Marc Gravell
231

Zu einfach ohne Komplikationen:

  1. using System.Linq.Dynamic;Oben hinzufügen .
  2. Verwenden vehicles = vehicles.AsQueryable().OrderBy("Make ASC, Year DESC").ToList();
Alaa Osta
quelle
11
und woher hast du das System.Linq.Dynamic?
Dementic
1
Funktioniert auch bei Verwendung von linq mit MongoDB.
suppe1976
32
Die akzeptierte Antwort war möglicherweise die richtige Antwort im Jahr 2008, aber derzeit ist dies die einfachste und korrekteste Antwort.
EL MOJO
1
Dies ist wirklich gute und einfache Handhabung, so viel Komplexität intern, liebte es
Mrinal Kamboj
5
Für die Menschen in der "Zukunft", wenn Sie Dotnet Core verwenden, verwenden Sie diese: nuget.org/packages/System.Linq.Dynamic.Core
Rafael Merlin
78

Ich habe die Antwort gefunden. Ich kann die .AsQueryable<>()Erweiterungsmethode verwenden, um meine Liste in IQueryable zu konvertieren und dann die dynamische Reihenfolge danach auszuführen.

John Sheehan
quelle
52
Bitte geben Sie ein Beispiel für den Rest von uns.
MGOwen
54

Bin gerade über diese Frage gestolpert.

Mit der ApplyOrder-Implementierung von Marc von oben habe ich eine Erweiterungsmethode zusammengestellt, die SQL-ähnliche Zeichenfolgen wie folgt verarbeitet:

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

Details finden Sie hier: http://aonnull.blogspot.com/2010/08/dynamic-sql-like-linq-orderby-extension.html

Adam Anderson
quelle
1
Tolles Zeug, fügen Sie einfach eine Änderung wie folgt hinzu, um die Groß- und Kleinschreibung des Eigenschaftsnamens nicht zu berücksichtigen: PropertyInfo pi = type.GetProperty (prop, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
Mrinal Kamboj
43

Ich denke, es würde funktionieren, Reflexion zu verwenden, um die Eigenschaft zu erhalten, nach der Sie sortieren möchten:

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

Beachten Sie, dass die Verwendung der Reflexion erheblich langsamer ist als der direkte Zugriff auf die Eigenschaft, sodass die Leistung untersucht werden muss.

Kjetil Watnedal
quelle
funktioniert das überhaupt orderby will keinen Wert, sondern einen Selektor lamba / delegate (Func <TSource, TKey> keySelector).
Davy Landman
2
Ich habe dieses Beispiel vor dem Posten ausprobiert und ja, es funktioniert.
Kjetil Watnedal
3
+1 Genau das habe ich gesucht! Dies eignet sich hervorragend für einfache Probleme beim Sortieren von Seiten.
Andrew Siemer
Das hat bei mir nicht funktioniert. Vermisse ich etwas Was soll "SomeProperty" sein? Ich habe versucht, den Eigenschaftsnamen sowie property.GetType () anzugeben. Ich habe IQueryable <> und nicht IEnumerable <>
SO User
2
@ Alex Shkor: Wie soll man die Elemente sortieren, ohne alle Elemente zu betrachten? In anderen Antworten gibt es jedoch bessere Lösungen.
Kjetil Watnedal
19

Bauen Sie einfach auf dem auf, was andere gesagt haben. Ich fand, dass das folgende ziemlich gut funktioniert.

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
quelle
12

Ich bin über diese Frage gestolpert und habe nach Linq-Multiple-Orderby-Klauseln gesucht. Vielleicht hat der Autor danach gesucht

So geht's:

var query = pets.OrderBy(pet => pet.Name).ThenByDescending(pet => pet.Age);    
InfoStatus
quelle
5
+1 hat die Abwertung wegen fehlender Erklärung abgesagt. Ich denke auch, dass der Autor an mehreren Bestellungen interessiert gewesen sein könnte. Auch wenn dynamisch war das Schlüsselwort, kein Grund zur Abwärts Stimme.
Jason Kleban
11

Ich habe versucht, dies zu tun, hatte aber Probleme mit der Lösung von Kjetil Watnedal, weil ich nicht die Inline-Linq-Syntax verwende - ich bevorzuge eine Syntax im Methodenstil. Mein spezielles Problem bestand darin, eine dynamische Sortierung mithilfe eines benutzerdefinierten Objekts durchzuführen IComparer.

Meine Lösung endete so:

Bei einer IQueryable-Abfrage wie folgt:

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

Und gegeben ein Laufzeitsortierfeldargument:

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

Das dynamische OrderBy sieht so aus:

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

Und das mit einer kleinen Hilfsmethode namens GetReflectedPropertyValue ():

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

Eine letzte Sache - ich erwähnte, dass ich die OrderBybenutzerdefinierte verwenden wollte IComparer- weil ich natürliche Sortierung durchführen wollte .

Um das zu tun, ändere ich einfach Folgendes OrderBy:

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

In diesem Beitrag finden Sie den Code für NaturalSortComparer().

James McCormack
quelle
5

Verwenden Sie dynamisch linq

einfach hinzufügen using System.Linq.Dynamic;

Und verwenden Sie es so, um alle Ihre Spalten zu bestellen:

string sortTypeStr = "ASC"; // or DESC
string SortColumnName = "Age"; // Your column name
query = query.OrderBy($"{SortColumnName} {sortTypeStr}");
Masoud Darvishian
quelle
4

Sie könnten es hinzufügen:

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
}

Die GetPropertyValueFunktion stammt aus der Antwort von Kjetil Watnedal

Das Problem wäre, warum? Eine solche Sortierung würde zur Laufzeit Ausnahmen auslösen und nicht zur Kompilierungszeit (wie die Antwort von D2VIANT).

Wenn Sie mit Linq to Sql arbeiten und das orderby ein Ausdrucksbaum ist, wird es ohnehin zur Ausführung in SQL konvertiert.

Keith
quelle
GetPropertyValue mehotod wird für alle Elemente ausgeführt, es ist eine schlechte Lösung.
Alex Shkor
2
OrderByBehalte die vorherige Bestellung nicht bei !!
Amir Ismail
4

Hier ist noch etwas, das ich interessant fand. Wenn Ihre Quelle eine DataTable ist, können Sie die dynamische Sortierung ohne Verwendung von Dynamic Linq verwenden

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;

Referenz: http://msdn.microsoft.com/en-us/library/bb669083.aspx (Verwenden von DataSetExtensions)

Hier ist eine weitere Möglichkeit, dies durch Konvertieren in eine DataView zu tun:

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

Dank Maarten ( Abfrage einer Sammlung mit dem PropertyInfo-Objekt in LINQ ) habe ich folgende Lösung erhalten:

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

In meinem Fall habe ich an einem "ColumnHeaderMouseClick" (WindowsForm) gearbeitet, also habe ich gerade die bestimmte Spalte gedrückt und ihre entsprechende PropertyInfo gefunden:

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

ODER

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

(Stellen Sie sicher, dass Ihre Spaltennamen mit den Objekteigenschaften übereinstimmen.)

Prost

Joaopintocruz
quelle
4

Nach langem Suchen hat das bei mir funktioniert:

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
quelle
4

Sie können die IEnumerable in IQueryable konvertieren.

items = items.AsQueryable().OrderBy("Name ASC");
Richard YS
quelle
3

Eine alternative Lösung verwendet die folgende Klasse / Schnittstelle. Es ist nicht wirklich dynamisch, aber es funktioniert.

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
quelle
2

Diese Antwort ist eine Antwort auf die Kommentare, die ein Beispiel für die von @John Sheehan - Runscope bereitgestellte Lösung benötigen

Bitte geben Sie ein Beispiel für den Rest von uns.

in DAL (Datenzugriffsschicht),

Die IEnumerable-Version:

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

Die IQueryable-Version

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

Jetzt können Sie die IQueryable-Version zum Binden verwenden, z. B. GridView in Asp.net, und Vorteile beim Sortieren erzielen (Sie können nicht mit der IEnumerable-Version sortieren).

Ich habe Dapper als ORM verwendet und eine IQueryable-Version erstellt und das Sortieren in GridView in asp.net so einfach verwendet.

M. Hassan
quelle
2

Installieren Sie zuerst Dynamic Tools -> NuGet Package Manager -> Package Manager Console

install-package System.Linq.Dynamic

In Namespace using System.Linq.Dynamic;

Jetzt können Sie verwenden OrderBy("Name, Age DESC")

Aminur Rahman
quelle
Wie kann ich es mit innerer Eigenschaftssortierung verwenden - wie OrderBy ("Branch.BranchName", "Descending")
devC
Das funktioniert bei mir. Vielleicht, weil die Frage 10 Jahre alt ist und diese einfachere Methode erst später kam.
Kosherjellyfish
1

Sie können dies verwenden:

        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();
}
k1Entwickler
quelle
Ein paar Jahre später stolpere ich darüber; Das funktionierte für mich wie ein Traum. Ich habe eine dynamische Sortierung nach 1 bis 3 Eigenschaften, und das funktioniert wie ein Traum. Einfach zu implementieren und problemlos.
Bazïnga
0

Konvertieren Sie die Liste in IEnumerable oder Iquerable, fügen Sie sie mit dem System.LINQ.Dynamic-Namespace hinzu. Anschließend können Sie die Eigenschaftsnamen in einer durch Kommas getrennten Zeichenfolge für die OrderBy-Methode angeben, die standardmäßig von System.LINQ.Dynamic stammt.

user145610
quelle
-3
var result1 = lst.OrderBy(a=>a.Name);// for ascending order. 
 var result1 = lst.OrderByDescending(a=>a.Name);// for desc order. 
Arindam
quelle