Como especifico o argumento Linq OrderBy dinamicamente?

94

Como especifico o argumento passado para orderbyusar um valor que considero como parâmetro?

Ex:

List<Student> existingStudends = new List<Student>{ new Student {...}, new Student {...}}

Implementação atual:

List<Student> orderbyAddress = existingStudends.OrderBy(c => c.Address).ToList();

Em vez de c.Address, como posso tomar isso como parâmetro?

Exemplo

 string param = "City";
 List<Student> orderbyAddress = existingStudends.OrderByDescending(c => param).ToList();
Sreedhar
fonte
4
Você pode estar procurando Dynamic Linq: weblogs.asp.net/scottgu/archive/2008/01/07/…
BrokenGlass 01 de
@Nev_Rahd: Tentei esclarecer um pouco a questão. Além disso, OrderByé um recurso do Linq e está IEnumerableativado, não um recurso específico para List. Sinta-se à vontade para reverter a edição ou alterá-la ainda mais :)
Merlyn Morgan-Graham
Possível duplicata de Dynamic LINQ OrderBy em IEnumerable <T>
Michael Freidgeim

Respostas:

129

Aqui está uma possibilidade usando reflexão ...

var param = "Address";    
var propertyInfo = typeof(Student).GetProperty(param);    
var orderByAddress = items.OrderBy(x => propertyInfo.GetValue(x, null));
codeConcussion
fonte
3
Mas isso é verdade quando se trata de expressões do Linq interpretadas por provedores, como Entity Framework (sql server, ou outro) ??
a.boussema
2
@vijay - use o ThenBymétodo .
codeConcussion 01 de
7
Quando tento fazer isso, recebo o erro: LINQ to Entities não reconhece o método 'System.Object GetValue (System.Object, System.Object [])' e esse método não pode ser convertido em uma expressão de armazenamento. Esta resposta se aplica apenas ao Linq To SQL?
philreed
4
Nenhum erro com .AsEnumerable (): var orderByAddress = items.AsEnumerable (). OrderBy (x => propertyInfo.GetValue (x, null));
César de
1
Como posso decidir dinamicamente pedir por asc ou desc
Hitesh Modha
123

Você pode usar um pouco de reflexão para construir a árvore de expressão da seguinte maneira (este é um método de extensão):

public static IQueryable<TEntity> OrderBy<TEntity>(this IQueryable<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[] { type, property.PropertyType },
                                   source.Expression, Expression.Quote(orderByExpression));
     return source.Provider.CreateQuery<TEntity>(resultExpression);
}

orderByPropertyé o nome da propriedade pela qual você deseja ordenar e, se for verdadeiro como parâmetro para desc, a classificação será feita em ordem decrescente; caso contrário, classificará em ordem crescente.

Agora você deve ser capaz de fazer existingStudents.OrderBy("City",true);ouexistingStudents.OrderBy("City",false);

Icaro
fonte
10
Essa resposta é incrível e muito melhor do que a resposta de reflexão. Isso realmente funciona com outros provedores como estrutura de entidade.
Sam
2
Eu votaria a favor dez vezes se pudesse !!! Onde você aprende a escrever um método de extensão como este ?? !!
Jach
3
Isso deve retornar um IOrderedQueryable, assim como o OrderBy integrado? Dessa forma, você pode ligar para .ThenBy.
Patrick Szalapski
4
Isso parece não funcionar mais ao usar o EFCore 3.0, estou recebendo um erro de tempo de execução em que não é possível traduzir a consulta.
Mildan
3
Sim, @Mildan, isso quebra em 3.0 e 3.1 para mim também. com o erro ~ "não consigo traduzir". Eu uso Pomelo para MySQL se isso for relevante. O problema é a Expressão. SE você codificar manualmente a expressão, ela funciona. Portanto, em vez de Lambda.Expression (), apenas forneça algo como: LambdaExpression orderByExp1 = (Expression <Func <AgencySystemBiz, string >>) (x => x.Name);
Menace
10

Para expandir a resposta de @Icarus : se você quiser que o tipo de retorno do método de extensão seja um IOrderedQueryable em vez de um IQueryable, você pode simplesmente converter o resultado da seguinte maneira:

public static IOrderedQueryable<TEntity> OrderBy<TEntity>(this IQueryable<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[] { type, property.PropertyType },
        source.Expression, Expression.Quote(orderByExpression));
    return (IOrderedQueryable<TEntity>)source.Provider.CreateQuery<TEntity>(resultExpression);
}
Ciaran
fonte
2
Parece que outras respostas não eram apropriadas para Entity Framework. Esta é uma solução perfeita para EF, pois Linq to Entities não oferece suporte a GetProperty, GetValue
Bill
1
Este método parece falhar para mim no 3.0 e 3.1 (funcionou no 2.2). Eu uso Pomelo para MySql, então isso pode ser relevante. Existe uma solução alternativa, mas é feio. Veja meu comentário acima.
Ameaça
Isso funcionou para mim no EF 3.0. No entanto, você deve alterar a seguinte linha para que o front-end não precise corresponder à distinção
Rei Arthur III
Ainda está otimizado para o Core 3.1?
Chris Go
8

1) Instale System.Linq.Dynamic

2) Adicione o seguinte código

public static class OrderUtils
{
    public static string ToStringForOrdering<T, TKey>(this Expression<Func<T, TKey>> expression, bool isDesc = false)
    {
        var str = expression.Body.ToString();
        var param = expression.Parameters.First().Name;
        str = str.Replace("Convert(", "(").Replace(param + ".", "");
        return str + (isDesc ? " descending" : "");
    }
}

3) Escreva sua chave para selecionar a função Lambda

public static class SortHelper
{
    public static Expression<Func<UserApp, object>> UserApp(string orderProperty)
    {
        orderProperty = orderProperty?.ToLowerInvariant();
        switch (orderProperty)
        {
            case "firstname":
                return x => x.PersonalInfo.FirstName;
            case "lastname":
                return x => x.PersonalInfo.LastName;
            case "fullname":
                return x => x.PersonalInfo.FirstName + x.PersonalInfo.LastName;
            case "email":
                return x => x.Email;

        }
    }
}

4) Use seus ajudantes

Dbset.OrderBy(SortHelper.UserApp("firstname").ToStringForOrdering())

5) Você pode usá-lo com paginação ( PagedList )

public virtual  IPagedList<T> GetPage<TOrder>(Page page, Expression<Func<T, bool>> where, Expression<Func<T, TOrder>> order, bool isDesc = false,
      params Expression<Func<T, object>>[] includes)
    {
        var orderedQueryable = Dbset.OrderBy(order.ToStringForOrdering(isDesc));
        var query = orderedQueryable.Where(where).GetPage(page);
        query = AppendIncludes(query, includes);

        var results = query.ToList();
        var total =  Dbset.Count(where);

        return new StaticPagedList<T>(results, page.PageNumber, page.PageSize, total);
    }

Explicação

System.Linq.Dynamic nos permite definir o valor da string no método OrderBy. Mas dentro dessa extensão, a string será analisada em Lambda. Portanto, pensei que funcionaria se analisássemos Lambda em string e o entregássemos ao método OrderBy. E funciona!

Igor Valikovsky
fonte
6
   private Func<T, object> GetOrderByExpression<T>(string sortColumn)
    {
        Func<T, object> orderByExpr = null;
        if (!String.IsNullOrEmpty(sortColumn))
        {
            Type sponsorResultType = typeof(T);

            if (sponsorResultType.GetProperties().Any(prop => prop.Name == sortColumn))
            {
                System.Reflection.PropertyInfo pinfo = sponsorResultType.GetProperty(sortColumn);
                orderByExpr = (data => pinfo.GetValue(data, null));
            }
        }
        return orderByExpr;
    }

    public List<T> OrderByDir<T>(IEnumerable<T> source, string dir, Func<T, object> OrderByColumn)
    {
        return dir.ToUpper() == "ASC" ? source.OrderBy(OrderByColumn).ToList() : source.OrderByDescending(OrderByColumn).ToList();``
    }

 // Call the code like below
        var orderByExpression= GetOrderByExpression<SearchResultsType>(sort);

    var data = OrderByDir<SponsorSearchResults>(resultRecords, SortDirectionString, orderByExpression);    
Rakesh Suryawanshi
fonte
Brilhante! Exatamente o que eu precisava.
Brandon Griffin de
5

Aqui está algo que inventei para lidar com uma descendente condicional. Você pode combinar isso com outros métodos de geração de keySelectorfunção dinamicamente.

    public static IOrderedQueryable<TSource> OrderBy<TSource, TKey>(this IQueryable<TSource> source,
            System.Linq.Expressions.Expression<Func<TSource, TKey>> keySelector,
            System.ComponentModel.ListSortDirection sortOrder
            )
    {
        if (sortOrder == System.ComponentModel.ListSortDirection.Ascending)
            return source.OrderBy(keySelector);
        else
            return source.OrderByDescending(keySelector);
    }

Uso:

//imagine this is some parameter
var direction = System.ComponentModel.ListSortDirection.Ascending;
query = query.OrderBy(ec => ec.MyColumnName, direction);

Observe que isso permite que você encadeie essa .OrderByextensão com um novo parâmetro em qualquer IQueryable.

// perhaps passed in as a request of user to change sort order
// var direction = System.ComponentModel.ListSortDirection.Ascending;
query = context.Orders
        .Where(o => o.Status == OrderStatus.Paid)
        .OrderBy(ec => ec.OrderPaidUtc, direction);
AaronLS
fonte
3

Isso não deixa você passar por um string, como pediu em sua pergunta, mas ainda pode funcionar para você.

O OrderByDescendingmétodo leva um Func<TSource, TKey>, então você pode reescrever sua função desta maneira:

List<Student> QueryStudents<TKey>(Func<Student, TKey> orderBy)
{
    return existingStudents.OrderByDescending(orderBy).ToList();
}

Existem outras sobrecargas para OrderByDescendingtambém que levam a Expression<Func<TSource, TKey>>, e / ou a IComparer<TKey>. Você também pode olhar para eles e ver se eles fornecem algo útil.

Merlyn Morgan-Graham
fonte
Isso não funciona porque você não define o tipo de TKey. Você deve alterar seu <T> para ter <TKey>.
Patrick Desjardins
Isso foi exatamente o que funcionou para mim! Eu queria uma função que ordenasse uma lista em ordem crescente ou decrescente, dependendo de um valor bool aprovado. Seu código funcionou muito bem com alguns ajustes!
Joe Gayetty
LINQ em ação: IEnumerable <Book> CustomSort <TKey> (seletor Func <Book, TKey>, Booleano ascendente) {IEnumerable <Book> books = SampleData.Books; retorno ascendente? books.OrderBy (seletor): books.OrderByDescending (seletor); }
Leszek P
1

A única solução que funcionou para mim foi postada aqui https://gist.github.com/neoGeneva/1878868 por neoGeneva.

Vou postar novamente o código dele porque funciona bem e não gostaria que ele se perdesse nas interwebs!

    public static IQueryable<T> OrderBy<T>(this IQueryable<T> source, string sortExpression)
    {
        if (source == null)
            throw new ArgumentNullException("source", "source is null.");

        if (string.IsNullOrEmpty(sortExpression))
            throw new ArgumentException("sortExpression is null or empty.", "sortExpression");

        var parts = sortExpression.Split(' ');
        var isDescending = false;
        var propertyName = "";
        var tType = typeof(T);

        if (parts.Length > 0 && parts[0] != "")
        {
            propertyName = parts[0];

            if (parts.Length > 1)
            {
                isDescending = parts[1].ToLower().Contains("esc");
            }

            PropertyInfo prop = tType.GetProperty(propertyName);

            if (prop == null)
            {
                throw new ArgumentException(string.Format("No property '{0}' on type '{1}'", propertyName, tType.Name));
            }

            var funcType = typeof(Func<,>)
                .MakeGenericType(tType, prop.PropertyType);

            var lambdaBuilder = typeof(Expression)
                .GetMethods()
                .First(x => x.Name == "Lambda" && x.ContainsGenericParameters && x.GetParameters().Length == 2)
                .MakeGenericMethod(funcType);

            var parameter = Expression.Parameter(tType);
            var propExpress = Expression.Property(parameter, prop);

            var sortLambda = lambdaBuilder
                .Invoke(null, new object[] { propExpress, new ParameterExpression[] { parameter } });

            var sorter = typeof(Queryable)
                .GetMethods()
                .FirstOrDefault(x => x.Name == (isDescending ? "OrderByDescending" : "OrderBy") && x.GetParameters().Length == 2)
                .MakeGenericMethod(new[] { tType, prop.PropertyType });

            return (IQueryable<T>)sorter
                .Invoke(null, new object[] { source, sortLambda });
        }

        return source;
    }
user1477388
fonte
1
  • Adicione o pacote nugget Dynamite ao seu código

  • Adicione o namespace Dynamite.Extensions Ex: using Dynamite.Extensions;

  • Dê Ordem por consulta como qualquer consulta SQL Ex .: students.OrderBy ("City DESC, Address"). ToList ();

Sreeja SJ
fonte
1

Para estender a resposta de @Icarus: se você quiser classificar por dois campos, eu poderia executar a seguinte função (para um campo a resposta de Icarius funciona muito bem).

public static IQueryable<T> OrderByDynamic<T>(this IQueryable<T> q, string SortField1, string SortField2, bool Ascending)
        {
            var param = Expression.Parameter(typeof(T), "p");
            var body = GetBodyExp(SortField1, SortField2, param);
            var exp = Expression.Lambda(body, param);

            string method = Ascending ? "OrderBy" : "OrderByDescending";
            Type[] types = new Type[] { q.ElementType, exp.Body.Type };
            var mce = Expression.Call(typeof(Queryable), method, types, q.Expression, exp);
            return q.Provider.CreateQuery<T>(mce);
        }

Esta é a função que o corpo retorna para a expressão lambda, ela trabalha com string e int, mas basta adicionar mais tipos para que funcione de acordo com a necessidade de cada programador

public static NewExpression GetBodyExp(string field1, string field2, ParameterExpression Parametro)
        {    
            // SE OBTIENE LOS NOMBRES DE LOS TIPOS DE VARIABLE 
            string TypeName1 = Expression.Property(Parametro, field1).Type.Name;
            string TypeName2 = Expression.Property(Parametro, field2).Type.Name;

            // SE DECLARA EL TIPO ANONIMO SEGUN LOS TIPOS DE VARIABLES
            Type TypeAnonymous = null;
            if (TypeName1 == "String")
            {
                string var1 = "0";
                if (TypeName2 == "Int32")
                {
                    int var2 = 0;
                    var example = new { var1, var2 };
                    TypeAnonymous = example.GetType();
                }

                if (TypeName2 == "String")
                {
                    string var2 = "0";
                    var example = new { var1, var2 };
                    TypeAnonymous = example.GetType();
                }    
            }    

            if (TypeName1 == "Int32")
            {
                int var1 = 0;
                if (TypeName2 == "Int32")
                {
                    string var2 = "0";
                    var example = new { var1, var2 };
                    TypeAnonymous = example.GetType();
                }

                if (TypeName2 == "String")
                {
                    string var2 = "0";
                    var example = new { var1, var2 };
                    TypeAnonymous = example.GetType();
                }    
            }

            //se declaran los TIPOS NECESARIOS PARA GENERAR EL BODY DE LA EXPRESION LAMBDA
            MemberExpression[] args = new[] { Expression.PropertyOrField(Parametro, field1), Expression.PropertyOrField(Parametro, field2) };
            ConstructorInfo CInfo = TypeAnonymous.GetConstructors()[0];
            IEnumerable<MemberInfo> a = TypeAnonymous.GetMembers().Where(m => m.MemberType == MemberTypes.Property);

            //BODY 
            NewExpression body = Expression.New(CInfo, args, TypeAnonymous.GetMembers().Where(m => m.MemberType == MemberTypes.Property));

            return body;
        }

para usá-lo é feito o seguinte

IQueryable<MyClass> IqMyClass= context.MyClass.AsQueryable();
List<MyClass> ListMyClass= IqMyClass.OrderByDynamic("UserName", "IdMyClass", true).ToList();

se houver uma maneira melhor de fazer isso, seria ótimo se eles compartilhassem

Consegui resolver isso graças a: Como posso fazer uma expressão lambda de propriedades múltiplas com Linq

Ruben Hidalgo
fonte
-2

Estou muito atrasado para a festa, mas nenhuma dessas soluções funcionou para mim. Eu estava ansioso para experimentar o System.Linq.Dynamic, mas não consegui encontrar no Nuget, talvez depreciado? De qualquer jeito...

Aqui está uma solução que encontrei. Eu precisava usar dinamicamente uma mistura de OrderBy , OrderByDescending e OrderBy> ThenBy .

Simplesmente criei um método de extensão para o meu objeto de lista, um pouco hacky eu sei ... Eu não recomendaria isso se fosse algo que eu estivesse fazendo muito, mas é bom para um único.

List<Employee> Employees = GetAllEmployees();

foreach(Employee oEmployee in Employees.ApplyDynamicSort(eEmployeeSort))
{
    //do stuff
}

public static IOrderedEnumerable<Employee> ApplyDynamicSort(this List<Employee> lEmployees, Enums.EmployeeSort eEmployeeSort)
{
    switch (eEmployeeSort)
    {
        case Enums.EmployeeSort.Name_ASC:
            return lEmployees.OrderBy(x => x.Name);
        case Enums.EmployeeSort.Name_DESC:
            return lEmployees.OrderByDescending(x => x.Name);
        case Enums.EmployeeSort.Department_ASC_Salary_DESC:
            return lEmployees.OrderBy(x => x.Department).ThenByDescending(y => y.Salary);
        default:
            return lEmployees.OrderBy(x => x.Name);
    }
}
Nicolay
fonte