Commande LINQ dynamique sur IEnumerable <T> / IQueryable <T>


670

J'ai trouvé un exemple dans les exemples VS2008 pour Dynamic LINQ qui vous permet d'utiliser une chaîne de type SQL (par exemple OrderBy("Name, Age DESC"))pour la commande. Malheureusement, la méthode incluse ne fonctionne que sur IQueryable<T>. Existe-t-il un moyen d'activer cette fonctionnalité IEnumerable<T>?


1
La meilleure réponse à ce jour, à mon avis: la bibliothèque System.Linq.Dynamic.Core .
Shahin Dohan

Réponses:


905

Je suis juste tombé sur ce vieil homme ...

Pour ce faire sans la bibliothèque dynamique LINQ, vous avez juste besoin du code ci-dessous. Cela couvre les scénarios les plus courants, y compris les propriétés imbriquées.

Pour le faire fonctionner, IEnumerable<T>vous pouvez ajouter des méthodes d'encapsulation qui passent par AsQueryable- mais le code ci-dessous est la Expressionlogique de base nécessaire.

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

Edit: cela devient plus amusant si vous voulez mélanger cela avec dynamic- bien que notez que cela dynamicne s'applique qu'aux LINQ-to-Objects (les arbres d'expression pour les ORM, etc. ne peuvent pas vraiment représenter les dynamicrequêtes - MemberExpressionne le supportent pas). Mais voici un moyen de le faire avec LINQ-to-Objects. Notez que le choix de Hashtableest dû à une sémantique de verrouillage favorable:

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
Le meilleur putain de code que j'ai vu :) Je viens de résoudre un million de problèmes dans mon projet :)
sajidnizami

4
@Dave - vous devez commencer par IQueryable<T>, donc si vous avez quelque chose comme List<T>(qui est IEnumerable<T>), vous devrez peut-être utiliser AsQueryable()- par exemplevar sorted = someList.AsQueryable().OrderBy("Foo.Bar");
Marc Gravell

7
Avez-vous vu cela ... cela pourrait aider certaines personnes ... stackoverflow.com/questions/557819/… c'est une solution plus fortement typée.
anthonyv

28
@MGOwen, vous semblez mal comprendre la nature du code. Les 40 lignes sont les mêmes, que ce soit 40 lignes que vous placez quelque part dans votre projet, ou si ces lignes viennent (précompilées ou comme source) dans une bibliothèque externe. Cela aurait été assez étonnant si j'avais lié, en octobre 2008, à une bibliothèque sur nuget qui existait depuis décembre '11 (notamment parce que nuget n'existait pas alors non plus), mais l'essentiel "ce qu'il fait" est le même. En outre, vous utilisez l'expression «solution réelle» comme s'il existait une route unique convenue bien définie pour chaque question de codage: il n'y en a pas.
Marc Gravell

5
@MGOwen btw, la lib externe est 2296 lignes de code (non compris AssemblyInfo.cs); ce qui rend les 40 lignes ici assez raisonnables
Marc Gravell

231

Trop facile sans aucune complication:

  1. Ajoutez using System.Linq.Dynamic;en haut.
  2. Utilisation vehicles = vehicles.AsQueryable().OrderBy("Make ASC, Year DESC").ToList();

11
et d'où avez-vous obtenu le System.Linq.Dynamic?
Dementic

1
Fonctionne également lors de l'utilisation de linq avec MongoDB.
soupy1976

32
La réponse acceptée était peut-être la bonne réponse en 2008, mais c'est actuellement la réponse la plus simple et la plus correcte.
EL MOJO

1
C'est une manipulation vraiment bonne et simple, tellement de complexité en interne,
j'ai

5
Pour les gens du "futur", si vous utilisez un noyau dotnet, utilisez ceci: nuget.org/packages/System.Linq.Dynamic.Core
Rafael Merlin

78

J'ai trouvé la réponse. Je peux utiliser la .AsQueryable<>()méthode d'extension pour convertir ma liste en IQueryable, puis exécuter l'ordre dynamique par contre.


52
Veuillez fournir un exemple pour le reste d'entre nous.
MGOwen

54

Je suis juste tombé sur cette question.

En utilisant l'implémentation ApplyOrder de Marc ci-dessus, j'ai giflé ensemble une méthode d'extension qui gère les chaînes de type SQL comme:

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

Les détails peuvent être trouvés ici: http://aonnull.blogspot.com/2010/08/dynamic-sql-like-linq-orderby-extension.html


1
Très bien, ajoutez simplement une modification comme suit pour rendre le nom de propriété insensible à la casse: PropertyInfo pi = type.GetProperty (prop, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
Mrinal Kamboj

43

Je suppose que cela fonctionnerait d'utiliser la réflexion pour obtenir la propriété que vous souhaitez trier:

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

Notez que l'utilisation de la réflexion est considérablement plus lente que l'accès direct à la propriété, de sorte que les performances doivent être étudiées.


ça marche même? orderby ne veut pas de valeur mais un sélecteur lamba / delegate (Func <TSource, TKey> keySelector) ..
Davy Landman

2
J'ai essayé cet exemple avant de le poster, et oui, cela fonctionne.
Kjetil Watnedal

3
+1 C'est exactement ce que je cherchais! Cela fonctionnera très bien pour les problèmes de tri de page simples.
Andrew Siemer

Ça n'a pas marché pour moi. Suis-je en train de manquer quelque chose? Que devrait être "SomeProperty". J'ai essayé de donner le nom de la propriété ainsi que property.GetType (). J'ai IQueryable <> et non IEnumerable <>
SO User

2
@Alex Shkor: Comment êtes-vous censé trier les éléments sans regarder tous les éléments? Cependant, il existe de meilleures solutions dans d'autres réponses.
Kjetil Watnedal

19

S'appuyant simplement sur ce que les autres ont dit. J'ai trouvé que ce qui suit fonctionne assez bien.

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

12

J'ai trébuché sur cette question en recherchant des clauses de commande multiple de Linq et c'est peut-être ce que l'auteur recherchait

Voici comment procéder:

var query = pets.OrderBy(pet => pet.Name).ThenByDescending(pet => pet.Age);    

5
+1 a annulé le vote négatif par manque d'explication. Je pense également que l'auteur aurait pu être intéressé par plusieurs commandes. Même si dynamique était le mot clé, aucune raison de voter contre.
Jason Kleban

11

J'essayais de le faire, mais j'avais des problèmes avec la solution de Kjetil Watnedal parce que je n'utilise pas la syntaxe linq en ligne - je préfère la syntaxe de style méthode. Mon problème spécifique était d'essayer de faire un tri dynamique à l'aide d'une coutume IComparer.

Ma solution a fini comme ceci:

Étant donné une requête IQueryable comme ceci:

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

Et étant donné un argument de champ de tri au moment de l'exécution:

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

Le OrderBy dynamique ressemble à ceci:

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

Et cela utilise une petite méthode d'assistance appelée GetReflectedPropertyValue ():

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

Une dernière chose - j'ai mentionné que je voulais OrderByutiliser la coutume IComparer- parce que je voulais faire un tri naturel .

Pour ce faire, je modifie simplement le OrderBypour:

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

Voir cet article pour le code NaturalSortComparer().


5

Utiliser dynamique linq

Ajoutez simplement using System.Linq.Dynamic;

Et utilisez-le comme ceci pour commander toutes vos colonnes:

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

4

Vous pouvez l'ajouter:

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
}

La GetPropertyValuefonction est de la réponse de Kjetil Watnedal

La question serait pourquoi? Un tel tri lèverait des exceptions au moment de l'exécution, plutôt qu'au moment de la compilation (comme la réponse de D2VIANT).

Si vous avez affaire à Linq en Sql et que orderby est un arbre d'expression, il sera de toute façon converti en SQL pour exécution.


Le mehotod GetPropertyValue sera exécuté pour tous les éléments, c'est une mauvaise solution.
Alex Shkor

2
OrderByne maintenez pas la commande précédente !!
Amir Ismail

4

Voici quelque chose d'autre que j'ai trouvé intéressant. Si votre source est un DataTable, vous pouvez utiliser le tri dynamique sans utiliser 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;

référence: http://msdn.microsoft.com/en-us/library/bb669083.aspx (à l'aide de DataSetExtensions)

Voici une autre façon de le faire en le convertissant en DataView:

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

4

Grâce à Maarten ( interroger une collection utilisant un objet PropertyInfo dans LINQ ), j'ai obtenu cette solution:

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

Dans mon cas, je travaillais sur un "ColumnHeaderMouseClick" (WindowsForm) alors je viens de trouver la colonne spécifique appuyée et son PropertyInfo correspondant:

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

OU

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

(assurez-vous que les noms de vos colonnes correspondent aux propriétés de l'objet)

À votre santé


4

Après beaucoup de recherches, cela a fonctionné pour moi:

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

4

Vous pouvez convertir IEnumerable en IQueryable.

items = items.AsQueryable().OrderBy("Name ASC");

3

Une autre solution utilise la classe / interface suivante. Ce n'est pas vraiment dynamique, mais ça marche.

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

Cette réponse est une réponse aux commentaires qui nécessitent un exemple de la solution fournie par @John Sheehan - Runscope

Veuillez fournir un exemple pour le reste d'entre nous.

en DAL (Data Access Layer),

La version IEnumerable:

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

La version IQueryable

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

Vous pouvez maintenant utiliser la version IQueryable pour lier, par exemple GridView dans Asp.net et bénéficier du tri (vous ne pouvez pas trier en utilisant la version IEnumerable)

J'ai utilisé Dapper comme ORM et construit la version IQueryable et j'ai utilisé le tri dans GridView dans asp.net si facilement.


2

Installez d'abord Dynamic Tools -> NuGet Package Manager -> Package Manager Console

install-package System.Linq.Dynamic

Ajouter un espace de noms using System.Linq.Dynamic;

Vous pouvez maintenant utiliser OrderBy("Name, Age DESC")


Comment puis-je l'utiliser avec le tri des propriétés internes - comme OrderBy ("Branch.BranchName", "Descending")
devC

Cela fonctionne pour moi. Peut-être parce que la question a 10 ans, et cette méthode plus simple n'est venue que plus tard.
kosherjellyfish

1

Vous pouvez utiliser ceci:

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

Quelques années plus tard, je tombe sur cela; cela a fonctionné pour moi, comme un rêve. J'ai un tri dynamique sur 1 à 3 propriétés, et cela fonctionne comme un rêve. Facile à mettre en œuvre et sans tracas.
Bazïnga

0

Convertir la liste en IEnumerable ou Iquerable, ajouter en utilisant l'espace de noms System.LINQ.Dynamic, puis vous pouvez mentionner les noms de propriété dans une chaîne séparée par des virgules en méthode OrderBy qui vient par défaut de System.LINQ.Dynamic.


-3
var result1 = lst.OrderBy(a=>a.Name);// for ascending order. 
 var result1 = lst.OrderByDescending(a=>a.Name);// for desc order. 
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.