¿Cómo agrupar datos por dos o más campos con LINQ en C#?
\n\n¡Hola, amigo! Si alguna vez has escrito consultas a bases de datos en SQL, seguramente recuerdas la construcción GROUP BY con múltiples columnas. Permite dividir los datos en cestas lógicas — por ejemplo, agrupar ventas primero por ciudad y luego por categoría de producto. En C#, para el mismo propósito se utiliza GroupBy de LINQ. Pero aquí está el problema: el GroupBy estándar solo acepta una clave. ¿Qué hacer si necesitas agrupar por varias columnas?
¡No te asustes! La solución es elegante y simple: creamos un tipo anónimo que combina los campos necesarios en una clave compuesta. En este artículo te mostraré no solo la sintaxis básica, sino también ejemplos reales con código que podrás aplicar inmediatamente en tus proyectos. ¡Vamos allá!
\n\nProblema: una clave es demasiado poco
\n\nImagina que tienes una lista de estudiantes:
\n\npublic class Student\n{\n public string Name { get; set; }\n public string Course { get; set; }\n public int Grade { get; set; }\n}\n\nList<Student> students = new List<Student>\n{\n new Student { Name = "Ana", Course = "Matemáticas", Grade = 85 },\n new Student { Name = "Boris", Course = "Matemáticas", Grade = 90 },\n new Student { Name = "Ana", Course = "Física", Grade = 78 },\n new Student { Name = "Boris", Course = "Física", Grade = 82 },\n new Student { Name = "Ana", Course = "Matemáticas", Grade = 95 }\n};\n\nQuieres saber la calificación promedio de cada estudiante por cada curso. Si usas GroupBy solo por nombre, los datos de Ana en Matemáticas y Física se mezclarán. Si solo por curso, se pierde la relación con el estudiante. Necesitas una agrupación por dos columnas: Name + Course.
Solución: tipo anónimo como clave compuesta
\n\nEn C# podemos crear un objeto temporal con los campos necesarios directamente en el lugar:
\n\nvar grouped = students\n .GroupBy(s => new { s.Name, s.Course })\n .Select(g => new\n {\n g.Key.Name,\n g.Key.Course,\n AverageGrade = g.Average(st => st.Grade)\n });\n\nforeach (var item in grouped)\n{\n Console.WriteLine($"{item.Name} | {item.Course} | Promedio: {item.AverageGrade:F2}");\n}\n\nSalida:
\nAna | Matemáticas | Promedio: 90.00
\nBoris | Matemáticas | Promedio: 90.00
\nAna | Física | Promedio: 78.00
\nBoris | Física | Promedio: 82.00
Observa: new { s.Name, s.Course } — esto es el tipo anónimo. LINQ compara automáticamente los objetos por todas sus propiedades (igualdad de valor).
Trabajo con IGrouping y colecciones anidadas
\n\nA veces no necesitas agregar datos, sino conservar todos los elementos originales dentro del grupo. Para esto se utiliza la interfaz IGrouping<TKey, TElement>. Cada grupo contiene una clave y una colección de elementos:
var groups = students.GroupBy(s => new { s.Name, s.Course });\n\nforeach (var group in groups)\n{\n Console.WriteLine($"Estudiante: {group.Key.Name}, Curso: {group.Key.Course}");\n foreach (var student in group)\n {\n Console.WriteLine($" Calificación: {student.Grade}");\n }\n}\n\nEste enfoque es ideal cuando necesitas, por ejemplo, generar un informe detallado.
\n\nSintaxis query vs fluent — ¿cuál elegir?
\n\nLINQ admite dos estilos de escritura de consultas. Veamos ambos:
\n\n1. Fluent (sintaxis de método)
\nvar result = students\n .GroupBy(s => new { s.Name, s.Course })\n .Select(g => new { g.Key.Name, g.Key.Course, Count = g.Count() });\n\n2. Query (sintaxis de expresiones similares a SQL)
\nvar result = from student in students\n group student by new { student.Name, stud