C # - code à ordonner par une propriété en utilisant le nom de la propriété comme chaîne

92

Quel est le moyen le plus simple de coder sur une propriété en C # lorsque j'ai le nom de la propriété sous forme de chaîne? Par exemple, je souhaite autoriser l'utilisateur à ordonner certains résultats de recherche par une propriété de son choix (en utilisant LINQ). Ils choisiront la propriété «Trier par» dans l'interface utilisateur - comme valeur de chaîne bien sûr. Existe-t-il un moyen d'utiliser cette chaîne directement comme propriété de la requête linq, sans avoir à utiliser la logique conditionnelle (if / else, commutateur) pour mapper les chaînes aux propriétés. Réflexion?

Logiquement, c'est ce que j'aimerais faire:

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

Mise à jour: Je n'ai pas spécifié à l'origine que j'utilise Linq to Entities - il semble que la réflexion (au moins l'approche GetProperty, GetValue) ne se traduit pas en L2E.

Jérémie
la source
Je pense que vous devriez utiliser la réflexion, et je ne suis pas sûr que vous puissiez utiliser la réflexion dans une expression lambda ... enfin, presque certainement pas dans Linq to SQL mais peut-être lors de l'utilisation de Linq contre une liste ou quelque chose.
CodeRedick
@Telos: Il n'y a aucune raison pour que vous ne puissiez pas utiliser la réflexion (ou toute autre API) dans un lambda. Que cela fonctionne ou non si le code est évalué en tant qu'expression et traduit en autre chose (comme LINQ-to-SQL, comme vous le suggérez) est une toute autre question.
Adam Robinson
C'est pourquoi j'ai posté un commentaire au lieu d'une réponse. ;) Principalement utilisé pour Linq2SQL ...
CodeRedick
1
Juste eu à surmonter le même problème .. voir ma réponse ci-dessous. stackoverflow.com/a/21936366/775114
Mark Powell

Réponses:

129

Je proposerais cette alternative à ce que tout le monde a publié.

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

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

Cela évite les appels répétés à l'API de réflexion pour obtenir la propriété. Maintenant, le seul appel répété est d'obtenir la valeur.

pourtant

Je recommanderais PropertyDescriptorplutôt d' utiliser un , car cela permettra d' TypeDescriptorattribuer des s personnalisés à votre type, ce qui rendra possible des opérations légères pour récupérer les propriétés et les valeurs. En l'absence d'un descripteur personnalisé, il reviendra de toute façon à la réflexion.

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

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

Quant à l'accélérer, consultez le HyperDescriptorprojet de Marc Gravel sur CodeProject. Je l'ai utilisé avec beaucoup de succès; c'est une bouée de sauvetage pour la liaison de données hautes performances et les opérations de propriétés dynamiques sur les objets métier.

Adam Robinson
la source
Notez que l'invocation réfléchie (c'est-à-dire GetValue) est la partie la plus coûteuse de la réflexion. La récupération de métadonnées (c'est-à-dire GetProperty) est en fait moins coûteuse (d'un ordre de grandeur), donc en mettant en cache cette partie, vous ne vous économisez pas vraiment beaucoup. Cela va coûter à peu près la même chose de toute façon, et ce coût va être lourd. Juste quelque chose à noter.
jrista
1
@jrista: l'invocation est la plus coûteuse, bien sûr. Cependant, «moins cher» ne veut pas dire «gratuit», ni même à proximité. La récupération des métadonnées prend un temps non négligeable, il y a donc un avantage à la mettre en cache et aucun inconvénient (sauf si je manque quelque chose ici). En vérité, cela devrait vraiment utiliser un de PropertyDescriptortoute façon (pour tenir compte des descripteurs de type personnalisés, ce qui pourrait faire de la récupération de valeur une opération légère).
Adam Robinson
Recherche pendant des heures quelque chose comme ceci pour gérer le tri d'un GridView ASP.NET par programme: PropertyDescriptor prop = TypeDescriptor.GetProperties (typeof (ScholarshipRequest)). Find (e.SortExpression, true);
Baxter
1
stackoverflow.com/questions/61635636/ ... J'ai eu un problème de réflexion, cela n'a pas fonctionné dans EfCore 3.1.3. Il semble générer une erreur dans EfCore 2 qui doit être activée pour les avertissements. Utilisez la réponse de @Mark ci
armourshield
1
Je reçois ce qui suit: InvalidOperationException: L'expression LINQ 'DbSet <MyObject> .Where (t => t.IsMasterData) .OrderBy (t => t.GetType (). GetProperty ("Address"). GetValue (obj: t, index: null) .GetType ()) 'n'a pas pu être traduit. Réécrivez la requête sous une forme qui peut être traduite ou basculez explicitement vers l'évaluation du client en insérant un appel à AsEnumerable (), AsAsyncEnumerable (), ToList () ou ToListAsync ().
bbrinck
67

Je suis un peu en retard à la fête, cependant, j'espère que cela peut être utile.

Le problème avec l'utilisation de la réflexion est que l'arborescence d'expressions résultante ne sera presque certainement pas prise en charge par les fournisseurs Linq autres que le fournisseur .Net interne. Cela convient aux collections internes, mais cela ne fonctionnera pas lorsque le tri doit être effectué à la source (que ce soit SQL, MongoDb, etc.) avant la pagination.

L'exemple de code ci-dessous fournit des méthodes d'extension IQueryable pour OrderBy et OrderByDescending, et peut être utilisé comme ceci:

query = query.OrderBy("ProductId");

Méthode d'extension:

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

Cordialement, Mark.

Mark Powell
la source
Excellente solution - je cherchais exactement cela. J'ai vraiment besoin de fouiller dans les arbres d'expressions. Encore très novice à ça. @Mark, une solution pour faire des expressions imbriquées? Disons que j'ai un type T avec une propriété "Sub" de type TSub qui a elle-même une propriété "Value". Maintenant, je voudrais obtenir l'expression Expression <Func <T, object >> pour la chaîne "Sub.Value".
Simon Scheurer
4
Pourquoi avons-nous besoin Expression.Convertde convertir propertyà object? J'obtiens une Unable to cast the type 'System.String' to type 'System.Object'. LINQ to Entities only supports casting EDM primitive or enumeration types.erreur et sa suppression semble fonctionner.
ShuberFu
@Demodave si je me souviens bien. var propAsObject = Expression.Convert(property, typeof(object));et utilisez simplement propertyà la place depropAsObject
ShuberFu
Or. Adapté pour un .Net Core 2.0.5.
Chris Amelinckx
2
Got errorLINQ to Entities only supports casting EDM primitive or enumeration types
Mateusz Puwałowski
35

J'ai aimé la réponse de @Mark Powell , mais comme @ShuberFu l'a dit, cela donne l'erreur LINQ to Entities only supports casting EDM primitive or enumeration types.

La suppression var propAsObject = Expression.Convert(property, typeof(object));ne fonctionnait pas avec les propriétés qui étaient des types valeur, tels que des entiers, car elle ne mettrait pas implicitement l'int en objet.

En utilisant les idées de Kristofer Andersson et Marc Gravell, j'ai trouvé un moyen de construire la fonction Queryable en utilisant le nom de la propriété et de la faire fonctionner avec Entity Framework. J'ai également inclus un paramètre IComparer facultatif. Attention: le paramètre IComparer ne fonctionne pas avec Entity Framework et doit être omis si vous utilisez Linq to Sql.

Ce qui suit fonctionne avec Entity Framework et Linq to Sql:

query = query.OrderBy("ProductId");

Et @Simon Scheurer cela fonctionne également:

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

Et si vous n'utilisez pas Entity Framework ou Linq to Sql, cela fonctionne:

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

Voici le code:

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
la source
Décidément, mec, êtes-vous Microsoft? :) Ce Aggregatefragment est génial! Il prend en charge les vues virtuelles créées à partir du modèle EF Core avec Join, puisque j'utilise des propriétés telles que "T.Property". Sinon, commander après Joinserait impossible de produire soit InvalidOperationExceptionou NullReferenceException. Et je dois commander APRÈS Join, car la plupart des requêtes sont constantes, les ordres dans les vues ne le sont pas.
Harry
@Harry. Merci, mais je ne peux vraiment pas prendre trop de crédit pour le Aggregatefragment. Je crois que c'était une combinaison du code de Marc Gravell et d'une recommandation intellisense. :)
David Specht
@DavidSpecht J'apprends juste les arbres d'expression, donc tout à leur sujet est maintenant de la magie noire pour moi. Mais j'apprends vite, la fenêtre interactive C # dans VS aide beaucoup.
Harry
comment l'utiliser?
Dat Nguyen
@Dat Nguyen Au lieu de products.OrderBy(x => x.ProductId), vous pouvez utiliserproducts.OrderBy("ProductId")
David Specht
12

Oui, je ne pense pas qu'il y ait d'autre moyen que la réflexion.

Exemple:

query = query.OrderBy(x => x.GetType().GetProperty("ProductId").GetValue(x, null));
Alon Gubkin
la source
Je reçois l'erreur "LINQ to Entities does not recognize the method 'System.Object GetValue(System.Object)' method, and this method cannot be translated into a store expression."Des pensées ou des conseils, s'il vous plaît?
Florin Vîrdol
5
query = query.OrderBy(x => x.GetType().GetProperty("ProductId").GetValue(x, null));

Essayer de rappeler la syntaxe exacte du haut de ma tête, mais je pense que c'est correct.

Dkackman
la source
2

La réflexion est la réponse!

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

Il y a beaucoup de choses que vous pouvez faire pour mettre en cache le PropertyInfo reflété, vérifier les mauvaises chaînes, écrire votre fonction de comparaison de requêtes, etc., mais au fond, c'est ce que vous faites.

Sébastien Bon
la source
2

Vous pouvez utiliser Linq dynamique - consultez ce blog.

Consultez également cet article de StackOverFlow ...

Partha Choudhury
la source
C'est la meilleure réponse pour moi
Demodave
2

Plus productif que l'extension de réflexion aux éléments de commande dynamiques:

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

Exemple:

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

Vous devrez peut-être également mettre en cache les lambas conformes (par exemple dans le dictionnaire <>)

gdbdable
la source
1

Aussi expressions dynamique peut résoudre ce problème. Vous pouvez utiliser des requêtes basées sur des chaînes via des expressions LINQ qui auraient pu être construites dynamiquement au moment de l'exécution.

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

Je pense que nous pouvons utiliser un nom d'outil puissant Expression et dans ce cas, l'utiliser comme méthode d'extension comme suit:

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
la source