Construye el árbol de expresiones GroupBy con múltiples campos

c# expression-trees lambda linq

Pregunta

Para generar dinámicamente una expresión GroupBy, estoy tratando de construir un árbol de expresiones Linq. Los campos para agrupar son dinámicos y pueden diferir en número.

Yo uso este código:

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

El código se copia de este post (Gracias Mark Gravel!).

Finaliza con ...

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

... de lo que esperaba poder cambiarlo a ...

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

Asumí que la expresión lambda no es más que ...

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

... pero desafortunadamente ese no parece ser el caso. El GroupBy con un lambda dinámico no da ninguna excepción, simplemente no agrupa nada y devuelve todos los elementos.

¿Qué estoy haciendo mal aquí? Cualquier ayuda sería apreciada. Gracias por adelantado.

Respuesta aceptada

Esa expresión lambda construye un diccionario de campos de agrupación.
Dictionary<TKey, TValue> no implementa Equals() y GetHashCode() , por lo que los agrupa por igualdad de referencia.
Como siempre devuelve un nuevo diccionario, cada elemento obtiene su propio grupo.

Debe cambiarlo para crear un tipo que implemente correctamente Equals() y GetHashCode() para la igualdad de valores.
Normalmente, el compilador generará un tipo anónimo. Sin embargo, no puede hacerlo aquí ya que no conoce la firma de tipo en tiempo de compilación.
En su lugar, puedes construir un Tuple<...> :

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

Respuesta popular

Esta publicación muestra una función de expresión que se puede usar tanto para Select como para GroupBy. Espero que ayude a otros!

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

Para ser llamado así:

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


Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow