Construire l'arborescence d'expression de GroupBy avec plusieurs champs

c# expression-trees lambda linq

Question

Pour générer dynamiquement une expression GroupBy, j'essaie de créer un arbre d'expression Linq. Les champs à regrouper sont dynamiques et leur nombre peut être différent.

J'utilise ce code:

string[] fields = {"Name", "Test_Result"};
Type studentType = typeof(Student);

var itemParam = Expression.Parameter(studentType, "x");

var addMethod = typeof(Dictionary<string, object>).GetMethod(
    "Add", new[] { typeof(string), typeof(object) });
var selector = Expression.ListInit(
        Expression.New(typeof(Dictionary<string,object>)),
        fields.Select(field => Expression.ElementInit(addMethod,
            Expression.Constant(field),
            Expression.Convert(
                Expression.PropertyOrField(itemParam, field),
                typeof(object)
            )
        )));
var lambda = Expression.Lambda<Func<Student, Dictionary<string,object>>>(
    selector, itemParam);

Le code est copié à partir de ce post (Merci Mark Gravel!).

Il se termine avec ...

var currentItemFields = students.Select(lambda.Compile());

... dont je m'attendais à pouvoir le changer en ...

var currentItemFields = students.GroupBy(lambda.Compile());

J'ai supposé que l'expression lambda n'était rien de plus que ...

var currentItemFields = students.GroupBy(o => new { o.Name, o.Test_Result });

... mais malheureusement, cela ne semble pas être le cas. GroupBy avec un lambda dynamique ne donne aucune exception, il ne regroupe rien et renvoie tous les éléments

Qu'est-ce que je fais mal ici? Toute aide serait appréciée. Merci d'avance.

Réponse acceptée

Cette expression lambda construit un dictionnaire de champs de regroupement.
Dictionary<TKey, TValue> pas Equals() et GetHashCode() , il les regroupe donc par égalité de référence.
Comme vous retournez toujours un nouveau dictionnaire, chaque élément a son propre groupe.

Vous devez le modifier pour créer un type qui implémente correctement Equals() et GetHashCode() pour l'égalité des valeurs.
Normalement, le compilateur génère un type anonyme. Cependant, vous ne pouvez pas le faire ici car vous ne connaissez pas la signature de type au moment de la compilation.
Au lieu de cela, vous pouvez construire un Tuple<...> :

Expression.New(
    Type.GetType("System.Tuple`" + fields.Length)
        .MakeGenericType(fields.Select(studentType.GetProperty), 
    fields.Select(f => Expression.PropertyOrField(itemParam, f))
)

Réponse populaire

Cet article montre une fonction d'expression qui peut être utilisée à la fois pour Select et GroupBy. J'espère que ça aide les autres!

public Expression<Func<TItem, object>> GroupByExpression<TItem>(string[] propertyNames)
{
    var properties = propertyNames.Select(name => typeof(TItem).GetProperty(name)).ToArray();
    var propertyTypes = properties.Select(p => p.PropertyType).ToArray();
    var tupleTypeDefinition = typeof(Tuple).Assembly.GetType("System.Tuple`" + properties.Length);
    var tupleType = tupleTypeDefinition.MakeGenericType(propertyTypes);
    var constructor = tupleType.GetConstructor(propertyTypes);
    var param = Expression.Parameter(typeof(TItem), "item");
    var body = Expression.New(constructor, properties.Select(p => Expression.Property(param, p)));
    var expr = Expression.Lambda<Func<TItem, object>>(body, param);
    return expr;
}  

Pour être appelé comme ça:

var lambda = GroupByExpression<Student>(fields);
var currentItemFields = students.GroupBy(lambda.Compile());



Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi