C # - código para ordenar por uma propriedade usando o nome da propriedade como uma string

93

Qual é a maneira mais simples de codificar em relação a uma propriedade em C # quando tenho o nome da propriedade como uma string? Por exemplo, quero permitir que o usuário ordene alguns resultados da pesquisa por uma propriedade de sua escolha (usando LINQ). Eles escolherão a propriedade "ordenar por" na IU - como um valor de string, é claro. Existe uma maneira de usar essa string diretamente como uma propriedade da consulta linq, sem ter que usar lógica condicional (if / else, switch) para mapear as strings para propriedades. Reflexão?

Logicamente, é o que eu gostaria de fazer:

query = query.OrderBy(x => x."ProductId");

Atualização: originalmente não especifiquei que estou usando Linq to Entities - parece que a reflexão (pelo menos a abordagem GetProperty, GetValue) não se traduz em L2E.

Jeremy
fonte
Acho que você teria que usar reflexão, e não tenho certeza se pode usar reflexão em uma expressão lambda ... bem, quase certamente não no Linq para SQL, mas talvez ao usar Linq em uma lista ou algo assim.
CodeRedick
@Telos: Não há razão para que você não possa usar reflexão (ou qualquer outra API) em um lambda. Se funcionará ou não se o código for avaliado como uma expressão e traduzido em outra coisa (como LINQ-to-SQL, como você sugere) é outra questão totalmente.
Adam Robinson
É por isso que postei um comentário em vez de uma resposta. ;) Principalmente usado para Linq2SQL ...
CodeRedick
1
Só tive que superar o mesmo problema .. veja minha resposta abaixo. stackoverflow.com/a/21936366/775114
Mark Powell

Respostas:

132

Eu ofereceria essa alternativa ao que todo mundo postou.

System.Reflection.PropertyInfo prop = typeof(YourType).GetProperty("PropertyName");

query = query.OrderBy(x => prop.GetValue(x, null));

Isso evita chamadas repetidas para a API de reflexão para obter a propriedade. Agora, a única chamada repetida é obter o valor.

Contudo

Eu recomendaria usar um em PropertyDescriptorvez disso, pois isso permitirá que TypeDescriptors personalizados sejam atribuídos ao seu tipo, tornando possível ter operações leves para recuperar propriedades e valores. Na ausência de um descritor personalizado, ele voltará a ser refletido de qualquer maneira.

PropertyDescriptor prop = TypeDescriptor.GetProperties(typeof(YourType)).Find("PropertyName");

query = query.OrderBy(x => prop.GetValue(x));

Para acelerar, confira o HyperDescriptorprojeto de Marc Gravel no CodeProject. Usei isso com grande sucesso; é um salva-vidas para vinculação de dados de alto desempenho e operações de propriedade dinâmica em objetos de negócios.

Adam Robinson
fonte
Observe que a invocação refletida (ou seja, GetValue) é a parte mais cara da reflexão. A recuperação de metadados (ou seja, GetProperty) é, na verdade, menos custosa (em uma ordem de magnitude), portanto, ao armazenar essa parte em cache, você não está realmente se salvando muito. Isso vai custar praticamente o mesmo de qualquer maneira, e esse custo será alto. Apenas algo a ser observado.
Jrista
1
@jrista: a invocação é o mais caro, com certeza. No entanto, "menos caro" não significa "grátis", ou mesmo perto disso. A recuperação de metadados leva uma quantidade não trivial de tempo, portanto, há uma vantagem em armazená-los em cache e nenhuma desvantagem (a menos que esteja faltando alguma coisa aqui). Na verdade, isso deveria realmente ser usado de PropertyDescriptorqualquer maneira (para levar em conta os descritores de tipo personalizados, o que poderia tornar a recuperação de valor uma operação leve).
Adam Robinson
Procurei por horas por algo assim para lidar com a classificação de um GridView ASP.NET programaticamente: PropertyDescriptor prop = TypeDescriptor.GetProperties (typeof (ScholarshipRequest)). Find (e.SortExpression, true);
Baxter
1
stackoverflow.com/questions/61635636/… Tive um problema com a reflexão que não funcionou no EfCore 3.1.3. Parece lançar um erro no EfCore 2 que precisa ser ativado para os avisos. Use a resposta de @Mark abaixo
armourshield
1
Recebo o seguinte: InvalidOperationException: A expressão LINQ 'DbSet <MyObject> .Where (t => t.IsMasterData) .OrderBy (t => t.GetType (). GetProperty ("Address"). GetValue (obj: t, index: null) .GetType ()) 'não pôde ser traduzido. Reescreva a consulta em um formato que possa ser traduzido ou alterne para a avaliação do cliente explicitamente inserindo uma chamada para AsEnumerable (), AsAsyncEnumerable (), ToList () ou ToListAsync ().
bbrinck
70

Estou um pouco atrasado para a festa, no entanto, espero que isso possa ajudar.

O problema de usar reflexão é que a árvore de expressão resultante quase certamente não será suportada por nenhum provedor Linq diferente do provedor .Net interno. Isso é bom para coleções internas, no entanto, não funcionará onde a classificação deve ser feita na origem (seja SQL, MongoDb etc.) antes da paginação.

O exemplo de código abaixo fornece métodos de extensão IQueryable para OrderBy e OrderByDescending e pode ser usado da seguinte forma:

query = query.OrderBy("ProductId");

Método de extensão:

public static class IQueryableExtensions 
{
    public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string propertyName)
    {
        return source.OrderBy(ToLambda<T>(propertyName));
    }

    public static IOrderedQueryable<T> OrderByDescending<T>(this IQueryable<T> source, string propertyName)
    {
        return source.OrderByDescending(ToLambda<T>(propertyName));
    }

    private static Expression<Func<T, object>> ToLambda<T>(string propertyName)
    {
        var parameter = Expression.Parameter(typeof(T));
        var property = Expression.Property(parameter, propertyName);
        var propAsObject = Expression.Convert(property, typeof(object));

        return Expression.Lambda<Func<T, object>>(propAsObject, parameter);            
    }
}

Atenciosamente, Mark.

Mark Powell
fonte
Excelente solução - eu estava procurando exatamente isso. Eu realmente preciso me aprofundar nas árvores de expressão. Ainda muito novato nisso. @Mark, alguma solução para fazer expressões aninhadas? Digamos que eu tenha um tipo T com uma propriedade "Sub" do tipo TSub que por sua vez tenha uma propriedade "Value". Agora eu gostaria de obter a expressão Expression <Func <T, object >> para a string "Sub.Value".
Simon Scheurer
5
Por que precisamos Expression.Convertconverter propertypara object? Estou recebendo um Unable to cast the type 'System.String' to type 'System.Object'. LINQ to Entities only supports casting EDM primitive or enumeration types.erro e a remoção parece funcionar.
ShuberFu
@Demodave se bem me lembro. var propAsObject = Expression.Convert(property, typeof(object));e apenas use propertyno lugar depropAsObject
ShuberFu
1
Ouro. Adaptado para um .Net Core 2.0.5.
Chris Amelinckx
2
Erro obtidoLINQ to Entities only supports casting EDM primitive or enumeration types
Mateusz Puwałowski
39

Gostei da resposta de @Mark Powell , mas como disse @ShuberFu , dá o erro LINQ to Entities only supports casting EDM primitive or enumeration types.

A remoção var propAsObject = Expression.Convert(property, typeof(object));não funcionou com propriedades que eram tipos de valor, como inteiro, pois não encaixaria implicitamente o int para o objeto.

Usando ideias de Kristofer Andersson e Marc Gravell , descobri uma maneira de construir a função Queryable usando o nome da propriedade e fazê-la funcionar com o Entity Framework. Eu também incluí um parâmetro opcional IComparer. Cuidado: O parâmetro IComparer não funciona com o Entity Framework e deve ser deixado de fora se estiver usando Linq para Sql.

O seguinte funciona com Entity Framework e Linq to Sql:

query = query.OrderBy("ProductId");

E @Simon Scheurer também funciona:

query = query.OrderBy("ProductCategory.CategoryId");

E se você não estiver usando o Entity Framework ou Linq to Sql, isso funciona:

query = query.OrderBy("ProductCategory", comparer);

Aqui está o código:

public static class IQueryableExtensions 
{    
public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> query, string propertyName, IComparer<object> comparer = null)
{
    return CallOrderedQueryable(query, "OrderBy", propertyName, comparer);
}

public static IOrderedQueryable<T> OrderByDescending<T>(this IQueryable<T> query, string propertyName, IComparer<object> comparer = null)
{
    return CallOrderedQueryable(query, "OrderByDescending", propertyName, comparer);
}

public static IOrderedQueryable<T> ThenBy<T>(this IOrderedQueryable<T> query, string propertyName, IComparer<object> comparer = null)
{
    return CallOrderedQueryable(query, "ThenBy", propertyName, comparer);
}

public static IOrderedQueryable<T> ThenByDescending<T>(this IOrderedQueryable<T> query, string propertyName, IComparer<object> comparer = null)
{
    return CallOrderedQueryable(query, "ThenByDescending", propertyName, comparer);
}

/// <summary>
/// Builds the Queryable functions using a TSource property name.
/// </summary>
public static IOrderedQueryable<T> CallOrderedQueryable<T>(this IQueryable<T> query, string methodName, string propertyName,
        IComparer<object> comparer = null)
{
    var param = Expression.Parameter(typeof(T), "x");

    var body = propertyName.Split('.').Aggregate<string, Expression>(param, Expression.PropertyOrField);

    return comparer != null
        ? (IOrderedQueryable<T>)query.Provider.CreateQuery(
            Expression.Call(
                typeof(Queryable),
                methodName,
                new[] { typeof(T), body.Type },
                query.Expression,
                Expression.Lambda(body, param),
                Expression.Constant(comparer)
            )
        )
        : (IOrderedQueryable<T>)query.Provider.CreateQuery(
            Expression.Call(
                typeof(Queryable),
                methodName,
                new[] { typeof(T), body.Type },
                query.Expression,
                Expression.Lambda(body, param)
            )
        );
}
}
David Specht
fonte
1
Nossa, cara, você é a Microsoft? :) Esse Aggregatefragmento é incrível! Ele cuida das visualizações virtuais criadas a partir do modelo EF Core com Join, já que uso propriedades como "T.Property". Caso contrário, Joinseria impossível fazer o pedido depois de produzir InvalidOperationExceptionou NullReferenceException. E eu preciso fazer o pedido DEPOIS Join, porque a maioria das consultas são constantes, os pedidos nas visualizações não são.
Harry
@DavidSpecht Estou aprendendo Árvores de Expressão, então tudo sobre elas agora é magia negra para mim ainda. Mas eu aprendo rapidamente, a janela interativa C # no VS ajuda muito.
Harry,
como usar isso?
Dat Nguyen
1
@Dat Nguyen Em vez de products.OrderBy(x => x.ProductId), você pode usarproducts.OrderBy("ProductId")
David Specht
12

Sim, eu não acho que haja outra maneira além da reflexão.

Exemplo:

query = query.OrderBy(x => x.GetType().GetProperty("ProductId").GetValue(x, null));
Alon Gubkin
fonte
Recebo o erro "LINQ to Entities does not recognize the method 'System.Object GetValue(System.Object)' method, and this method cannot be translated into a store expression."Alguma opinião ou conselho, por favor?
Florin Vîrdol
5
query = query.OrderBy(x => x.GetType().GetProperty("ProductId").GetValue(x, null));

Estou tentando lembrar a sintaxe exata de início, mas acho que está correto.

dkackman
fonte
2

A reflexão é a resposta!

typeof(YourType).GetProperty("ProductId").GetValue(theInstance);

Há muitas coisas que você pode fazer para armazenar em cache o PropertyInfo refletido, verificar se há strings incorretas, escrever sua função de comparação de consulta, etc., mas, no fundo, é isso que você faz.

Sebastian bom
fonte
2

Você pode usar o Linq dinâmico - confira este blog.

Verifique também esta postagem StackOverFlow ...

Partha Choudhury
fonte
Esta é a melhor resposta para mim
Demodave
2

Mais produtivo do que a extensão de reflexão para itens de pedido dinâmicos:

public static class DynamicExtentions
{
    public static object GetPropertyDynamic<Tobj>(this Tobj self, string propertyName) where Tobj : class
    {
        var param = Expression.Parameter(typeof(Tobj), "value");
        var getter = Expression.Property(param, propertyName);
        var boxer = Expression.TypeAs(getter, typeof(object));
        var getPropValue = Expression.Lambda<Func<Tobj, object>>(boxer, param).Compile();            
        return getPropValue(self);
    }
}

Exemplo:

var ordered = items.OrderBy(x => x.GetPropertyDynamic("ProductId"));

Você também pode precisar armazenar em cache lambas em conformidade (por exemplo, no Dicionário <>)

gdbdable
fonte
1

Além disso, as expressões dinâmicas podem resolver este problema. Você pode usar consultas baseadas em string por meio de expressões LINQ que poderiam ter sido construídas dinamicamente em tempo de execução.

var query = query
          .Where("Category.CategoryName == @0 and Orders.Count >= @1", "Book", 10)
          .OrderBy("ProductId")
          .Select("new(ProductName as Name, Price)");
ali-myousefi
fonte
0

Acho que podemos usar um nome de ferramenta poderoso Expression e, neste caso, usá-lo como um método de extensão da seguinte maneira:

public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string ordering, bool descending)
{
    var type = typeof(T);
    var property = type.GetProperty(ordering);
    var parameter = Expression.Parameter(type, "p");
    var propertyAccess = Expression.MakeMemberAccess(parameter, property);
    var orderByExp = Expression.Lambda(propertyAccess, parameter);
    MethodCallExpression resultExp = 
        Expression.Call(typeof(Queryable), (descending ? "OrderByDescending" : "OrderBy"), 
            new Type[] { type, property.PropertyType }, source.Expression, Expression.Quote(orderByExp));
    return (IOrderedQueryable<T>)source.Provider.CreateQuery<T>(resultExp);
}
Abolfazl
fonte