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

670

Encontrei um exemplo no VS2008 Examples for Dynamic LINQ que permite que você use uma string do tipo sql (por exemplo, OrderBy("Name, Age DESC"))para fazer pedidos. Infelizmente, o método incluído apenas funciona IQueryable<T>. Existe alguma maneira de obter essa funcionalidade IEnumerable<T>?

John Sheehan
fonte
1
A melhor resposta a partir desta data, na minha opinião: biblioteca System.Linq.Dynamic.Core .
Shahin Dohan

Respostas:

905

Apenas tropeçou neste velho ...

Para fazer isso sem a biblioteca dinâmica LINQ, você só precisa do código abaixo. Isso abrange os cenários mais comuns, incluindo propriedades aninhadas.

Para fazê-lo funcionar, IEnumerable<T>você pode adicionar alguns métodos de wrapper, AsQueryablemas o código abaixo é a Expressionlógica principal necessária.

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: fica mais divertido se você quiser misturar isso dynamic- embora note que isso dynamicsó se aplica a LINQ-to-Objects (as árvores de expressão para ORMs etc não podem realmente representar dynamicconsultas - MemberExpressionnão é compatível). Mas aqui está uma maneira de fazer isso com o LINQ-to-Objects. Observe que a escolha de Hashtableé devido à semântica de bloqueio favorável:

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
fonte
109
Melhor peça maldita de código que eu vi :) Apenas resolvido um milhão de problemas no meu projeto :)
sajidnizami
4
@Dave - você precisa começar IQueryable<T>, portanto, se você tem algo como List<T>(o que é IEnumerable<T>), pode precisar usar AsQueryable()- por exemplovar sorted = someList.AsQueryable().OrderBy("Foo.Bar");
Marc Gravell
7
Você já viu isso ... isso pode ajudar algumas pessoas ... stackoverflow.com/questions/557819/… é uma solução mais fortemente tipada.
Anthonyv
28
@MGOwen parece que você não entende a natureza do código. As 40 linhas são as mesmas, não importa se são 40 linhas que você coloca em algum lugar do seu projeto ou se essas linhas vêm (pré-compiladas ou como fonte) em uma biblioteca externa. Teria sido incrível se eu tivesse vinculado, em outubro de 2008, a uma biblioteca de nuget que existe desde dez '11 (principalmente porque nuget também não existia), mas o fundamental "o que está fazendo" é o mesmo. Além disso, você usa a frase "solução real" como se houvesse uma rota única acordada e bem definida para todas as questões de codificação: não existe.
Marc Gravell
5
@MGOwen btw, a lib externa é 2296 linhas de código (não incluindo AssemblyInfo.cs); que kinda faz com que as 40 linhas aqui parecem bastante razoável
Marc Gravell
231

Muito fácil sem qualquer complicação:

  1. Adicione using System.Linq.Dynamic;no topo.
  2. Usar vehicles = vehicles.AsQueryable().OrderBy("Make ASC, Year DESC").ToList();
Alaa Osta
fonte
11
e de onde você tirou isso System.Linq.Dynamic?
Dementic
1
Funciona ao usar o linq com o MongoDB também.
soupy1976
32
A resposta aceita pode ter sido a resposta correta em 2008, mas atualmente é a resposta mais fácil e correta agora.
EL MOJO 24/10
1
Este é realmente bom e manuseio simples, tanta complexidade internamente, adorei
Mrinal Kamboj
5
Para as pessoas no "futuro", se você estiver usando núcleo dotnet, use este: nuget.org/packages/System.Linq.Dynamic.Core
Rafael Merlin
78

Eu encontrei a resposta. Posso usar o .AsQueryable<>()método de extensão para converter minha lista em IQueryable e executar a ordem dinâmica nela.

John Sheehan
fonte
52
Por favor, forneça um exemplo para o resto de nós.
MGOwen
54

Apenas tropeçou nesta questão.

Usando a implementação ApplyOrder de Marc de cima, juntei um método Extension que lida com seqüências semelhantes a SQL, como:

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

Detalhes podem ser encontrados aqui: http://aonnull.blogspot.com/2010/08/dynamic-sql-like-linq-orderby-extension.html

Adam Anderson
fonte
1
Ótimas coisas, basta adicionar uma modificação da seguinte maneira para tornar o nome da propriedade sem distinção entre maiúsculas e minúsculas: PropertyInfo pi = type.GetProperty (prop, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
Mrinal Kamboj 29/05
43

Eu acho que funcionaria usar a reflexão para obter qualquer propriedade que você deseja classificar:

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

Observe que o uso da reflexão é consideravelmente mais lento do que o acesso direto à propriedade, portanto, o desempenho precisaria ser investigado.

Kjetil Watnedal
fonte
isso funciona mesmo? não orderby não quer um valor, mas um seletor lamba / delegado (Func <TSource, TKey> keySelector) ..
Davy Landman
2
Eu tentei este exemplo antes de publicá-lo e, sim, ele funciona.
Kjetil Watnedal 28/10/08
3
+1 Isso é exatamente o que eu estava procurando! Isso funcionará muito bem para problemas simples de classificação de páginas.
precisa saber é o seguinte
Isso não funcionou para mim. Estou esquecendo de algo? O que deveria ser "SomeProperty". Eu tentei dar o nome da propriedade, bem como property.GetType (). Eu tenho IQueryable <> e não IEnumerable <>
SO User
2
@Alex Shkor: Como você deve classificar os elementos sem olhar para todos os elementos? No entanto, existem melhores soluções em outras respostas.
Kjetil Watnedal
19

Apenas construindo sobre o que os outros disseram. Eu descobri que o seguinte funciona muito bem.

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

Eu tropecei nesta pergunta procurando por várias cláusulas de ordenação do Linq e talvez fosse isso que o autor estava procurando

Veja como fazer isso:

var query = pets.OrderBy(pet => pet.Name).ThenByDescending(pet => pet.Age);    
InfoStatus
fonte
5
+1 cancelou o voto negativo devido à falta de explicação. Eu também acho que o autor pode estar interessado em vários pedidos. Mesmo que dinâmica fosse a palavra-chave, não havia razão para votar em baixa.
Jason Kleban
11

Eu estava tentando fazer isso, mas estava tendo problemas com a solução da Kjetil Watnedal porque não uso a sintaxe linq inline - prefiro a sintaxe no estilo de método. Meu problema específico estava em tentar fazer a classificação dinâmica usando um costume IComparer.

Minha solução acabou assim:

Dada uma consulta IQueryable da seguinte maneira:

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

E dado um argumento de campo de classificação em tempo de execução:

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

O dinâmico OrderBy tem a seguinte aparência:

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

E isso está usando um pequeno método auxiliar chamado GetReflectedPropertyValue ():

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

Uma última coisa - mencionei que queria OrderByusar personalizado IComparer- porque queria fazer a classificação Natural .

Para fazer isso, basta alterar o OrderBypara:

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

Veja esta postagem para o código de NaturalSortComparer().

James McCormack
fonte
5

Use dinâmico linq

basta adicionar using System.Linq.Dynamic;

E use-o assim para ordenar todas as suas colunas:

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

Você pode adicioná-lo:

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
}

A GetPropertyValuefunção é da resposta de Kjetil Watnedal

A questão seria por quê? Qualquer tipo desse lançaria exceções em tempo de execução, em vez de tempo de compilação (como a resposta do D2VIANT).

Se você estiver lidando com o Linq to Sql e o orderby for uma árvore de expressão, ele será convertido em SQL para execução de qualquer maneira.

Keith
fonte
GetPropertyValue mehotod será executado para todos os elementos, é uma solução ruim.
21412 Alex Shkor
2
OrderBynão mantenha a ordem anterior !!
Amir Ismail
4

Aqui está outra coisa que achei interessante. Se sua fonte for uma DataTable, você poderá usar a classificação dinâmica sem usar o 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;

referência: http://msdn.microsoft.com/en-us/library/bb669083.aspx (Using DataSetExtensions)

Aqui está mais uma maneira de fazer isso convertendo-o em um DataView:

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

Graças a Maarten ( consulte uma coleção usando o objeto PropertyInfo no LINQ ), obtive esta solução:

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

No meu caso, eu estava trabalhando em um "ColumnHeaderMouseClick" (WindowsForm), apenas encontrei a coluna específica pressionada e seu correspondente PropertyInfo:

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

(certifique-se de que os nomes da coluna correspondam às propriedades do objeto)

Felicidades

joaopintocruz
fonte
4

Depois de muita pesquisa, isso funcionou para mim:

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

Você pode converter o IEnumerable em IQueryable.

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

Uma solução alternativa usa a seguinte classe / interface. Não é verdadeiramente dinâmico, mas 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
fonte
2

Esta resposta é uma resposta aos comentários que precisam de um exemplo para a solução fornecida por @John Sheehan - Runscope

Por favor, forneça um exemplo para o resto de nós.

no DAL (camada de acesso a dados),

A versão IEnumerable:

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

A versão IQueryable

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

Agora você pode usar a versão IQueryable para vincular, por exemplo, GridView no Asp.net e se beneficiar da classificação (você não pode classificar usando a versão IEnumerable)

Usei o Dapper como ORM e criei a versão IQueryable e utilizei a classificação no GridView no asp.net tão fácil.

M.Hassan
fonte
2

Primeiro instale ferramentas dinâmicas -> Gerenciador de Pacotes NuGet -> Console do Gerenciador de Pacotes

install-package System.Linq.Dynamic

Adicionar espaço para nome using System.Linq.Dynamic;

Agora você pode usar OrderBy("Name, Age DESC")

Aminur Rahman
fonte
Como posso usá-lo com propriedade interior de classificação - como OrderBy ( "Branch.BranchName", "Decrescente")
devC
Isso funciona para mim. Talvez porque a pergunta tenha 10 anos e esse método mais fácil só tenha surgido mais tarde.
kosherjellyfish
1

Você pode usar isto:

        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
fonte
Alguns anos depois, tropeço nisso; isso funcionou para mim, como um sonho. Eu tenho uma classificação dinâmica em 1 a 3 propriedades, e isso funciona como um sonho. Fácil de implementar e sem complicações.
Bazïnga 22/03
0

Converta a lista em IEnumerable ou Iquerable, adicione usando o espaço para nome System.LINQ.Dynamic e, em seguida, você pode mencionar os nomes de propriedades na sequência separada por vírgulas no método OrderBy, que vem por padrão de System.LINQ.Dynamic.

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