web-dev-qa-db-fra.com

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

Quelle est la manière la plus simple de coder une propriété en C # lorsque j'ai le nom de la propriété sous forme de chaîne? Par exemple, je veux permettre à l'utilisateur de commander certains résultats de recherche par une propriété de son choix (en utilisant LINQ). Ils choisiront la propriété "order by" dans l'interface utilisateur - comme une valeur de chaîne bien sûr. Existe-t-il un moyen d'utiliser cette chaîne directement en tant que propriété de la requête linq, sans avoir à utiliser la logique conditionnelle (if/else, switch) 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'avais pas spécifié à l'origine que j'utilisais Linq to Entities - il semble que la réflexion (au moins l'approche GetProperty, GetValue) ne se traduit pas en L2E.

69
Jeremy

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

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é consiste à obtenir la valeur.

Cependant

Je recommanderais plutôt d'utiliser un PropertyDescriptor, car cela permettra d'attribuer des TypeDescriptor personnalisés à votre type, ce qui permettra d'avoir 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 retombera de toute façon dans 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 projet HyperDescriptor de Marc Gravel sur CodeProject. J'ai utilisé cela avec beaucoup de succès; c'est un épargnant de vie pour la liaison de données hautes performances et les opérations de propriétés dynamiques sur les objets métier.

100
Adam Robinson

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

Le problème avec l'utilisation de la réflexion est que l'arbre d'expression résultant ne sera certainement pas pris en charge par les fournisseurs Linq autres que le fournisseur .Net interne. C'est bien pour les collections internes, mais cela ne fonctionnera pas lorsque le tri doit être fait à 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 suit:

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.

51
Mark Powell

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

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

En utilisant des 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 laissé de côté si vous utilisez Linq à Sql.

Les éléments suivants fonctionnent avec Entity Framework et Linq to Sql:

query = query.OrderBy("ProductId");

Et @ Simon Scheurer cela fonctionne aussi:

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)
            )
        );
}
}
23
David Specht

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

Exemple:

query = query.OrderBy(x => x.GetType().GetProperty("ProductId").GetValue(x, null));
12
Alon Gubkin
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.

5
dkackman

Vous pouvez utiliser Linq dynamique - consultez this blog.

Consultez également this StackOverFlow post ...

2
Partha Choudhury

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 réfléchi, vérifier les chaînes incorrectes, écrire votre fonction de comparaison de requête, etc., mais au fond, c'est ce que vous faites.

2
Sebastian Good

Plus productif que l'extension de réflexion aux articles 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 <>)

2
devi

Expressions dynamiques peut également 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)");
1
ali-myousefi