C #: código para ordenar por propiedad usando el nombre de la propiedad como una cadena

92

¿Cuál es la forma más sencilla de codificar una propiedad en C # cuando tengo el nombre de la propiedad como una cadena? Por ejemplo, quiero permitir que el usuario ordene algunos resultados de búsqueda por una propiedad de su elección (usando LINQ). Elegirán la propiedad "ordenar por" en la interfaz de usuario, como un valor de cadena, por supuesto. ¿Hay alguna manera de usar esa cadena directamente como una propiedad de la consulta linq, sin tener que usar lógica condicional (if / else, switch) para asignar las cadenas a las propiedades? ¿Reflexión?

Lógicamente, esto es lo que me gustaría hacer:

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

Actualización: originalmente no especifiqué que estoy usando Linq para entidades; parece que la reflexión (al menos el enfoque GetProperty, GetValue) no se traduce a L2E.

Jeremy
fuente
Creo que tendrías que usar la reflexión, y no estoy seguro de que puedas usar la reflexión en una expresión lambda ... bueno, es casi seguro que no en Linq a SQL, pero tal vez cuando uses Linq contra una lista o algo.
CodeRedick
@Telos: No hay ninguna razón por la que no pueda usar la reflexión (o cualquier otra API) en una lambda. Si funcionará o no si el código se evalúa como una expresión y se traduce a otra cosa (como LINQ-to-SQL, como sugiere) es otra cuestión completamente distinta.
Adam Robinson
Es por eso que publiqué un comentario en lugar de una respuesta. ;) Usado principalmente para Linq2SQL ...
CodeRedick
1
Solo tuve que superar el mismo problema ... mira mi respuesta a continuación. stackoverflow.com/a/21936366/775114
Mark Powell

Respuestas:

129

Ofrecería esta alternativa a lo que todos los demás han publicado.

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

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

Esto evita llamadas repetidas a la API de reflexión para obtener la propiedad. Ahora la única llamada repetida es obtener el valor.

sin embargo

Yo recomendaría usar un PropertyDescriptoren su lugar, ya que esto permitirá que los correos TypeDescriptorelectrónicos personalizados sean asignados a su tipo, haciendo posible tener operaciones ligeras para recuperar propiedades y valores. En ausencia de un descriptor personalizado, volverá a la reflexión de todos modos.

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

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

En cuanto a acelerarlo, consulte el HyperDescriptorproyecto de Marc Gravel en CodeProject. He usado esto con gran éxito; es un salvavidas para el enlace de datos de alto rendimiento y las operaciones de propiedad dinámicas en los objetos comerciales.

Adam Robinson
fuente
Tenga en cuenta que la invocación reflejada (es decir, GetValue) es la parte más costosa de la reflexión. La recuperación de metadatos (es decir, GetProperty) es en realidad menos costosa (en un orden de magnitud), por lo que al almacenar en caché esa parte, realmente no se ahorra tanto. Esto costará más o menos lo mismo de cualquier manera, y ese costo será alto. Solo algo para tener en cuenta.
jrista
1
@jrista: la invocación es la más costosa, sin duda. Sin embargo, "menos costoso" no significa "gratis", ni siquiera cerca de él. La recuperación de metadatos lleva una cantidad de tiempo no trivial, por lo que almacenarlos en caché tiene una ventaja y no hay desventajas (a menos que me falte algo aquí). En realidad, esto debería ser de PropertyDescriptortodos modos (para tener en cuenta los descriptores de tipo personalizados, lo que podría hacer que la recuperación de valores sea una operación liviana).
Adam Robinson
Busqué durante horas algo como esto para manejar la clasificación de un ASP.NET GridView mediante programación: PropertyDescriptor prop = TypeDescriptor.GetProperties (typeof (ScholarshipRequest)). Find (e.SortExpression, true);
Baxter
1
stackoverflow.com/questions/61635636/… Tuve un problema con la reflexión que no funcionó en EfCore 3.1.3. Parece arrojar un error en EfCore 2 que debe activarse para las advertencias. Use la respuesta de @Mark a continuación
armourshield
1
Recibo lo siguiente: InvalidOperationException: La expresión LINQ 'DbSet <MyObject> .Where (t => t.IsMasterData) .OrderBy (t => t.GetType (). GetProperty ("Dirección"). GetValue (obj: t, index: null) .GetType ()) 'no se pudo traducir. Reescriba la consulta en un formulario que pueda traducirse o cambie a la evaluación del cliente explícitamente insertando una llamada a AsEnumerable (), AsAsyncEnumerable (), ToList () o ToListAsync ().
bbrinck
67

Llego un poco tarde a la fiesta, sin embargo, espero que esto pueda ser de ayuda.

El problema con el uso de la reflexión es que es casi seguro que el árbol de expresión resultante no será compatible con ningún otro proveedor de Linq que no sea el proveedor interno de .Net. Esto está bien para colecciones internas, sin embargo, esto no funcionará cuando la clasificación se realice en la fuente (ya sea SQL, MongoDb, etc.) antes de la paginación.

El siguiente ejemplo de código proporciona métodos de extensión IQueryable para OrderBy y OrderByDescending, y se puede usar así:

query = query.OrderBy("ProductId");

Método de extensión:

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

Saludos, Mark.

Mark Powell
fuente
Excelente solución: estaba buscando exactamente eso. Realmente necesito profundizar en los árboles de expresión. Aún soy muy novato en eso. @Mark, ¿alguna solución para hacer expresiones anidadas? Digamos que tengo un tipo T con una propiedad "Sub" de tipo TSub que a su vez tiene una propiedad "Valor". Ahora me gustaría obtener la expresión Expression <Func <T, object >> para la cadena "Sub.Value".
Simon Scheurer
4
¿Por qué necesitamos Expression.Convertconvertir propertya object? Recibo un Unable to cast the type 'System.String' to type 'System.Object'. LINQ to Entities only supports casting EDM primitive or enumeration types.error y parece que la eliminación funciona.
ShuberFu
@Demodave si recuerdo correctamente. var propAsObject = Expression.Convert(property, typeof(object));y usar propertyen lugar depropAsObject
ShuberFu
Oro. Adaptado para .Net Core 2.0.5.
Chris Amelinckx
2
Tengo un errorLINQ to Entities only supports casting EDM primitive or enumeration types
Mateusz Puwałowski
35

Me gustó la respuesta de @Mark Powell , pero como dijo @ShuberFu , da el error LINQ to Entities only supports casting EDM primitive or enumeration types.

La eliminación var propAsObject = Expression.Convert(property, typeof(object));no funcionó con propiedades que fueran tipos de valor, como integer, ya que no enmarcaría implícitamente el int al objeto.

Usando ideas de Kristofer Andersson y Marc Gravell , encontré una manera de construir la función Queryable usando el nombre de la propiedad y hacer que aún funcione con Entity Framework. También incluí un parámetro opcional IComparer. Precaución: El parámetro IComparer no funciona con Entity Framework y debe omitirse si se usa Linq para Sql.

Lo siguiente funciona con Entity Framework y Linq to Sql:

query = query.OrderBy("ProductId");

Y @Simon Scheurer esto también funciona:

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

Y si no está utilizando Entity Framework o Linq to Sql, esto funciona:

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

Aquí está el 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
fuente
Dios, hombre, ¿eres Microsoft? :) ¡Ese Aggregatefragmento es asombroso! Se encarga de las vistas virtuales creadas a partir del modelo EF Core con Join, ya que yo uso propiedades como "T.Property". De lo contrario, ordenar después Joinsería imposible producir InvalidOperationExceptiono bien NullReferenceException. Y necesito hacer un pedido DESPUÉS Join, porque la mayoría de las consultas son constantes, los pedidos en las vistas no lo son.
Harry
@Harry. Gracias, pero realmente no puedo tomarme demasiado crédito por el Aggregatefragmento. Creo que fue una combinación del código de Marc Gravell y una recomendación intellisense. :)
David Specht
@DavidSpecht Estoy aprendiendo árboles de expresión, así que todo sobre ellos ahora es magia negra para mí. Pero aprendo rápidamente, la ventana interactiva de C # en VS ayuda mucho.
Harry
¿como usar esto?
Dat Nguyen
@Dat Nguyen En lugar de products.OrderBy(x => x.ProductId), podrías usarproducts.OrderBy("ProductId")
David Specht
12

Sí, no creo que haya otra forma que la Reflexión.

Ejemplo:

query = query.OrderBy(x => x.GetType().GetProperty("ProductId").GetValue(x, null));
Alon Gubkin
fuente
Recibo el error ¿ "LINQ to Entities does not recognize the method 'System.Object GetValue(System.Object)' method, and this method cannot be translated into a store expression."Alguna idea o consejo, por favor?
Florin Vîrdol
5
query = query.OrderBy(x => x.GetType().GetProperty("ProductId").GetValue(x, null));

Intento recordar la sintaxis exacta de la parte superior de mi cabeza, pero creo que es correcto.

dkackman
fuente
2

¡La reflexión es la respuesta!

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

Hay muchas cosas que puede hacer para almacenar en caché la PropertyInfo reflejada, verificar cadenas defectuosas, escribir su función de comparación de consultas, etc., pero en el fondo, esto es lo que hace.

Sebastián bueno
fuente
2

Puede utilizar Linq dinámico: consulte este blog.

También consulte esta publicación de StackOverFlow ...

Partha Choudhury
fuente
Esta es la mejor respuesta para mí
Demodave
2

Más productivo que la extensión de reflexión a artículos 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);
    }
}

Ejemplo:

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

También es posible que necesite almacenar en caché lambas cumplidas (por ejemplo, en el Diccionario <>)

gdbdable
fuente
1

También las Expresiones Dinámicas pueden resolver este problema. Puede utilizar consultas basadas en cadenas mediante expresiones LINQ que podrían haberse construido dinámicamente en tiempo de ejecución.

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

Creo que podemos usar una poderosa herramienta llamada Expresión y, en este caso, usarla como un método de extensión de la siguiente manera:

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
fuente