Como agrupar dados por dois ou mais campos usando LINQ em C#?
\n\nOlá, amigo! Se você já escreveu consultas para bancos de dados em SQL, certamente se lembra da construção GROUP BY com várias colunas. Ela permite dividir os dados em grupos lógicos — por exemplo, agrupar vendas primeiro por cidade e, dentro dela, por categoria de produto. Em C#, para o mesmo propósito, usamos GroupBy do LINQ. Mas aí está o problema: o GroupBy padrão aceita apenas uma chave. O que fazer se precisar agrupar por várias colunas?
Não se preocupe! A solução é elegante e simples — criamos um tipo anônimo que combina os campos necessários em uma chave composta. Neste artigo, mostrarei não apenas a sintaxe básica, mas também exemplos reais com código que você poderá aplicar imediatamente em seus projetos. Vamos lá!
\n\nProblema: uma chave é muito pouco
\n\nImagine que você tem uma lista de alunos:
\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 = "Anna", Course = "Matemática", Grade = 85 },\n new Student { Name = "Boris", Course = "Matemática", Grade = 90 },\n new Student { Name = "Anna", Course = "Física", Grade = 78 },\n new Student { Name = "Boris", Course = "Física", Grade = 82 },\n new Student { Name = "Anna", Course = "Matemática", Grade = 95 }\n};\n\nVocê quer saber a nota média de cada aluno em cada curso. Se usar GroupBy apenas pelo nome, os dados da Anna em Matemática e Física se misturarão. Se usar apenas pelo curso, perderá a referência ao aluno. É necessário um agrupamento por duas colunas: Name + Course.
Solução: tipo anônimo como chave composta
\n\nEm C#, podemos criar um objeto temporário com os campos necessários diretamente no local:
\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} | Média: {item.AverageGrade:F2}");\n}\n\nSaída:
\nAnna | Matemática | Média: 90.00
\nBoris | Matemática | Média: 90.00
\nAnna | Física | Média: 78.00
\nBoris | Física | Média: 82.00
Observe: new { s.Name, s.Course } é o tipo anônimo. O LINQ compara automaticamente os objetos por todas as suas propriedades (igualdade de valor).
Trabalhando com IGrouping e coleções aninhadas
\n\nÀs vezes, não é necessário agregar os dados, mas sim manter todos os elementos originais dentro do grupo. Para isso, usamos a interface IGrouping<TKey, TElement>. Cada grupo contém uma chave e uma coleção de elementos:
var groups = students.GroupBy(s => new { s.Name, s.Course });\n\nforeach (var group in groups)\n{\n Console.WriteLine($"Aluno: {group.Key.Name}, Curso: {group.Key.Course}");\n foreach (var student in group)\n {\n Console.WriteLine($" Nota: {student.Grade}");\n }\n}\n\nEssa abordagem é ideal quando você precisa, por exemplo, gerar um relatório detalhado.
\n\nSintaxe query vs fluent — qual escolher?
\n\nO LINQ suporta dois estilos de escrita de consultas. Vamos ver ambos:
\n\n1. Fluent (sintaxe 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 (sintaxe de expressões semelhantes a SQL)
\nvar result = from student in students\n group student by new { student.Name, stud