2016-01-26 1 views
0

En utilisant des arbres d'expression, je devrais construire un GroupBy d'une manière générique. La méthode statique que je vais utiliser est la suivante:Expression.Call GroupBy puis Select et Count()?

public static IQueryable<Result> GroupBySelector<TSource>(this IQueryable<TSource> source, String coloumn) 
{ 

    //Code here 

} 

La classe Résultat a deux propriétés:

public string Value { get; set; } 
public int Count { get; set; } 

Fondamentalement, je voudrais construire la requête Linq suivante par des arbres d'expression:

query.GroupBy(s => s.Country).Select(p => new 
       { 
        Value = p.Key, 
        Count = p.Count() 
       } 
      ) 

Comment l'implémenteriez-vous?

Répondre

5

Regarder:

query.GroupBy(s => s.Country).Select(p => new 
    { 
    Value = p.Key, 
    Count = p.Count() 
    } 
); 

Pour correspondre à la signature de IQueryable<Result> ce que vous avez réellement besoin ici est:

query.GroupBy(s => s.Country).Select(p => new 
    Result{ 
    Value = p.Key, 
    Count = p.Count() 
    } 
); 

Maintenant, le Select peut travailler avec tout IQueryable<IGrouping<string, TSource>> comme il est. C'est seulement le GroupBy qui a besoin de nous pour utiliser des arbres d'expression.

Notre tâche ici est de commencer avec un type et une chaîne qui représente une propriété (elle-même renvoie une chaîne) et créer un Expression<Func<TSource, string>> qui représente l'obtention de la valeur de cette propriété.

Donc, nous allons produire le peu simple de la méthode d'abord:

public static IQueryable<Result> GroupBySelector<TSource>(this IQueryable<TSource> source, string column) 
{ 
    Expression<Func<TSource, string>> keySelector = //Build tree here. 

    return source.GroupBy(keySelector).Select(p => new Result{Value = p.Key, Count = p.Count()}); 
} 

D'accord. Comment construire l'arbre.

Nous allons avoir besoin d'un lambda qui a un paramter de type TSource:

var param = Expression.Parameter(typeof(TSource)); 

Nous allons avoir besoin d'obtenir la propriété dont le nom correspond column:

Expression.Property(param, column); 

Et la seule logique nécessaire dans le lambda est simplement d'accéder à cette propriété:

Expression<Func<TSource, string>> keySelector = Expression.Lambda<Func<TSource, string>> 
(
    Expression.Property(param, column), 
    param 
); 

Mettre tous ensemble:

public static IQueryable<Result> GroupBySelector<TSource>(this IQueryable<TSource> source, String column) 
{ 
    var param = Expression.Parameter(typeof(TSource)); 
    Expression<Func<TSource, string>> keySelector = Expression.Lambda<Func<TSource, string>> 
    (
     Expression.Property(param, column), 
     param 
    ); 
    return source.GroupBy(keySelector).Select(p => new Result{Value = p.Key, Count = p.Count()}); 
} 

A propos de la seule chose qui reste est l'exception de manipulation, que je ne normalement pas inclus dans une réponse, mais une partie de cela vaut la peine de prêter attention à.

d'abord l'hypothèse nulle évidente et les chèques vides:

public static IQueryable<Result> GroupBySelector<TSource>(this IQueryable<TSource> source, String column) 
{ 
    if (source == null) throw new ArgumentNullException("source"); 
    if (column == null) throw new ArgumentNullException("column"); 
    if (column.Length == 0) throw new ArgumentException("column"); 
    var param = Expression.Parameter(typeof(TSource)); 
    Expression<Func<TSource, string>> keySelector = Expression.Lambda<Func<TSource, string>> 
    (
     Expression.Property(param, column), 
     param 
    ); 
    return source.GroupBy(keySelector).Select(p => new Result{Value = p.Key, Count = p.Count()}); 
} 

Maintenant, nous allons examiner ce qui se passe si nous passons une chaîne pour column qui ne correspond pas à une propriété de TSource. Nous obtenons un ArgumentException avec le message Instance property '[Whatever you asked for]' is not defined for type '[Whatever the type is]'. C'est à peu près ce que nous voulons dans ce cas, donc pas de problème.

Si toutefois nous passions une chaîne qui identifiait une propriété mais où cette propriété n'était pas de type string, nous obtiendrions quelque chose comme "Expression of type 'System.Int32' cannot be used for return type 'System.String'". Ce n'est pas terrible, mais ce n'est pas génial non plus. Soyons plus explicite:

public static IQueryable<Result> GroupBySelector<TSource>(this IQueryable<TSource> source, String column) 
{ 
    if (source == null) throw new ArgumentNullException("source"); 
    if (column == null) throw new ArgumentNullException("column"); 
    if (column.Length == 0) throw new ArgumentException("column"); 
    var param = Expression.Parameter(typeof(TSource)); 
    var prop = Expression.Property(param, column); 
    if (prop.Type != typeof(string)) throw new ArgumentException("'" + column + "' identifies a property of type '" + prop.Type + "', not a string property.", "column"); 
    Expression<Func<TSource, string>> keySelector = Expression.Lambda<Func<TSource, string>> 
    (
     prop, 
     param 
    ); 
    return source.GroupBy(keySelector).Select(p => new Result{Value = p.Key, Count = p.Count()}); 
} 

Si cette méthode était ci-dessus interne serait peut-être trop tuer, mais si elle était publique l'info serait bien utile si vous êtes venu à déboguer.

+0

Vous avez mon vote, j'ai posté une solution assez similaire qui a été downvoted sans raison, mais de toute façon votre réponse est meilleure que la mienne;) – octavioccl

+0

Whaw - c'est un tutoriel :) :) –

+0

@IvanStoev même des questions qui semblent très étroites à Le premier coup d'œil peut offrir des opportunités d'élucidation. –