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

670

Encontré un ejemplo en VS2008 Ejemplos para Dynamic LINQ que le permite usar una cadena tipo sql (por ejemplo, OrderBy("Name, Age DESC"))para ordenar. Desafortunadamente, el método incluido solo funciona IQueryable<T>. ¿Hay alguna forma de activar esta funcionalidad IEnumerable<T>?

John Sheehan
fuente
1
La mejor respuesta a partir de esta fecha, en mi opinión: System.Linq.Dynamic.Core library.
Shahin Dohan

Respuestas:

905

Me topé con este viejo ...

Para hacer esto sin la biblioteca LINQ dinámica, solo necesita el código de la siguiente manera. Esto cubre los escenarios más comunes, incluidas las propiedades anidadas.

Para que funcione IEnumerable<T>, puede agregar algunos métodos de envoltura que funcionan AsQueryable, pero el código a continuación es la Expressionlógica central necesaria.

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

Editar: se vuelve más divertido si desea mezclar eso dynamic, aunque tenga en cuenta que dynamicsolo se aplica a LINQ-to-Objects (los árboles de expresión para ORM, etc., en realidad no pueden representar dynamicconsultas, MemberExpressionno lo admiten). Pero aquí hay una manera de hacerlo con LINQ-to-Objects. Tenga en cuenta que la elección de Hashtablese debe a una semántica de bloqueo 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);
        }
    }
}
Marc Gravell
fuente
109
La mejor pieza de código que he visto :) Acabo de resolver un millón de problemas en mi proyecto :)
sajidnizami
44
@Dave: debes comenzar IQueryable<T>, así que si tienes algo como List<T>(que es IEnumerable<T>), es posible que necesites usar AsQueryable(), por ejemplovar sorted = someList.AsQueryable().OrderBy("Foo.Bar");
Marc Gravell
77
¿Has visto esto ... podría ayudar a algunas personas ... stackoverflow.com/questions/557819/ ... es una solución más fuertemente tipada.
anthonyv
28
@ MGO, cuando pareces malinterpretar la naturaleza del código. Las 40 líneas son las mismas, sin importar si son 40 líneas las que colocaste en algún lugar de tu proyecto, o si esas líneas vienen (precompiladas o como fuente) en una biblioteca externa. Hubiera sido bastante sorprendente si me hubiera vinculado, en octubre de 2008, a una biblioteca en Nuget que ha existido desde diciembre '11 (sobre todo porque Nuget tampoco existía entonces), pero lo fundamental "lo que está haciendo" es lo mismo. Además, utiliza la frase "solución real" como si hubiera una ruta única acordada bien definida para cada pregunta de codificación: no la hay.
Marc Gravell
55
@ MGOwen por cierto, la lib externa es 2296 líneas de código (sin incluir AssemblyInfo.cs); lo que hace que las 40 líneas parezcan bastante razonables
Marc Gravell
231

Demasiado fácil sin ninguna complicación:

  1. Añadir using System.Linq.Dynamic;en la parte superior.
  2. Utilizar vehicles = vehicles.AsQueryable().OrderBy("Make ASC, Year DESC").ToList();
Alaa Osta
fuente
11
y ¿de dónde sacaste la System.Linq.Dynamicde?
Demencial
1
Funciona cuando se usa linq con MongoDB también.
soupy1976
32
La respuesta aceptada puede haber sido la respuesta correcta en 2008, pero actualmente esta es la respuesta más fácil y correcta ahora.
EL MOJO
1
Este es un manejo realmente bueno y simple, tanta complejidad internamente, me encantó
Mrinal Kamboj
55
Para las personas en el "futuro", si está utilizando dotnet core, use esto: nuget.org/packages/System.Linq.Dynamic.Core
Rafael Merlin
78

Encontré la respuesta. Puedo usar el .AsQueryable<>()método de extensión para convertir mi lista a IQueryable, luego ejecutar el orden dinámico en contra.

John Sheehan
fuente
52
Proporcione un ejemplo para el resto de nosotros.
MGOwen
54

Acabo de tropezar con esta pregunta.

Usando la implementación ApplyOrder de Marc desde arriba, combiné un método de Extensión que maneja cadenas similares a SQL como:

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

Los detalles se pueden encontrar aquí: http://aonnull.blogspot.com/2010/08/dynamic-sql-like-linq-orderby-extension.html

Adam Anderson
fuente
1
Excelente, simplemente agregue una modificación de la siguiente manera para que el nombre de la propiedad no distinga entre mayúsculas y minúsculas: PropertyInfo pi = type.GetProperty (prop, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
Mrinal Kamboj
43

Supongo que funcionaría usar la reflexión para obtener cualquier propiedad que desee ordenar:

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

Tenga en cuenta que el uso de la reflexión es considerablemente más lento que el acceso directo a la propiedad, por lo que el rendimiento debería investigarse.

Kjetil Watnedal
fuente
¿esto incluso funciona? orderby no quiere un valor sino un selector lamba / delegado (Func <TSource, TKey> keySelector) ..
Davy Landman
2
Intenté este ejemplo antes de publicarlo, y sí, funciona.
Kjetil Watnedal
3
+1 ¡Esto es exactamente lo que estaba buscando! Esto funcionará muy bien para problemas simples de clasificación de páginas.
Andrew Siemer el
Esto no funcionó para mí. ¿Me estoy perdiendo de algo? ¿Qué debería ser "SomeProperty"? Traté de dar el nombre de la propiedad, así como la propiedad. GetType (). Tengo IQueryable <> y no IEnumerable <>
Usuario SO
2
@ Alex Shkor: ¿Cómo se supone que debes clasificar los elementos sin mirar todos los elementos? Sin embargo, hay mejores soluciones en otras respuestas.
Kjetil Watnedal
19

Basándome en lo que otros han dicho. Descubrí que lo siguiente funciona bastante 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;
}
vdhant
fuente
12

He tropezado con esta pregunta buscando cláusulas de pedido múltiple de Linq y tal vez esto era lo que el autor estaba buscando

Aquí se explica cómo hacerlo:

var query = pets.OrderBy(pet => pet.Name).ThenByDescending(pet => pet.Age);    
InfoStatus
fuente
55
+1 canceló el voto negativo por falta de explicación. También creo que el autor podría haber estado interesado en múltiples pedidos. Incluso si dinámica fue la palabra clave, no hay razón para rechazar el voto.
Jason Kleban
11

Estaba tratando de hacer esto, pero tengo problemas con la solución de Kjetil Watnedal porque no uso la sintaxis linq en línea; prefiero la sintaxis de estilo de método. Mi problema específico fue tratar de hacer una ordenación dinámica usando una costumbre IComparer.

Mi solución terminó así:

Dada una consulta IQueryable como esta:

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

Y dado un argumento de campo de ordenación en tiempo de ejecución:

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

La dinámica OrderBy se ve así:

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

Y eso está usando un pequeño método auxiliar llamado GetReflectedPropertyValue ():

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

Una última cosa, mencioné que quería OrderByusar el personalizado IComparer, porque quería hacer la clasificación natural .

Para hacer eso, simplemente modifico el OrderBypara:

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

Ver esta publicación para el código de NaturalSortComparer().

James McCormack
fuente
5

Usar dinámico linq

solo agrega using System.Linq.Dynamic;

Y úsalo así para ordenar todas tus columnas:

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

Puedes agregarlo:

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 GetPropertyValuefunción es de la respuesta de Kjetil Watnedal.

El problema sería por qué? Cualquiera de estos tipos arrojaría excepciones en tiempo de ejecución, en lugar de tiempo de compilación (como la respuesta de D2VIANT).

Si se trata de Linq a Sql y el orden es un árbol de expresión, de todos modos se convertirá en SQL para su ejecución.

Keith
fuente
GetPropertyValue mehotod se ejecutará para todos los elementos, es una mala solución.
Alex Shkor
2
OrderBy¡no mantengas el orden anterior!
Amir Ismail
4

Aquí hay algo más que encontré interesante. Si su fuente es una DataTable, puede usar la ordenación dinámica sin usar 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;

referencia: http://msdn.microsoft.com/en-us/library/bb669083.aspx (Uso de DataSetExtensions)

Aquí hay una forma más de hacerlo convirtiéndolo en un DataView:

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

Gracias a Maarten ( Consulta una colección usando el objeto PropertyInfo en LINQ ) obtuve esta solución:

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

En mi caso, estaba trabajando en un "ColumnHeaderMouseClick" (WindowsForm), así que encontré la columna específica presionada y su correspondiente PropertyInfo:

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

O

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

(asegúrese de que sus nombres de columna coincidan con las propiedades del objeto)

Salud

joaopintocruz
fuente
4

Después de mucho buscar esto funcionó para mí:

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

Puede convertir el IEnumerable a IQueryable.

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

Una solución alternativa utiliza la siguiente clase / interfaz. No es realmente dinámico, pero funciona.

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

Esta respuesta es una respuesta a los comentarios que necesitan un ejemplo para la solución provista por @John Sheehan - Runscope

Proporcione un ejemplo para el resto de nosotros.

en DAL (capa de acceso a datos),

La versión IEnumerable:

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

La versión IQueryable

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

Ahora puede usar la versión IQueryable para enlazar, por ejemplo GridView en Asp.net y beneficiarse de la clasificación (no puede ordenar usando la versión IEnumerable)

Utilicé Dapper como ORM y construí la versión IQueryable y utilicé la clasificación en GridView en asp.net de manera muy fácil.

M.Hassan
fuente
2

Primero instale las herramientas dinámicas -> NuGet Package Manager -> Package Manager Console

install-package System.Linq.Dynamic

Agregar espacio de nombres using System.Linq.Dynamic;

Ahora puedes usar OrderBy("Name, Age DESC")

Aminur Rahman
fuente
¿Cómo puedo usarlo con la clasificación de propiedades internas, como OrderBy ("Branch.BranchName", "Descending")
DevC
Esto funciona para mi. Quizás porque la pregunta tiene 10 años, y este método más fácil solo llegó más tarde.
kosherjellyfish
1

Puedes usar esto:

        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
fuente
Un par de años después y me topé con esto; Esto funcionó para mí, como un sueño. Tengo una clasificación dinámica de 1 a 3 propiedades, y esto funciona como un sueño. Fácil de implementar y sin problemas.
Bazïnga
0

Convierta la Lista a IEnumerable o Iquerable, agregue usando el espacio de nombres System.LINQ.Dynamics, luego puede mencionar los nombres de propiedad en una cadena separada por comas al Método OrderBy que viene por defecto de System.LINQ.Dynamic.

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