How to group data by two or more fields using LINQ in C#?
\n\nHello, friend! If you've ever written database queries in SQL, you probably remember the GROUP BY construct with multiple columns. It allows you to split data into logical buckets — for example, grouping sales first by city, and then within that by product category. In C#, the same purpose is served by GroupBy from LINQ. But here's the catch: the standard GroupBy accepts only one key. What if you need to group by multiple columns?
Don't worry! The solution is elegant and simple — we create an anonymous type that combines the required fields into a single composite key. In this article, I'll show you not only the basic syntax but also real code examples that you can immediately apply in your projects. Let's go!
\n\nThe Problem: One Key Is Too Little
\n\nImagine you have a list of students:
\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 = "Mathematics", Grade = 85 },\n new Student { Name = "Boris", Course = "Mathematics", Grade = 90 },\n new Student { Name = "Anna", Course = "Physics", Grade = 78 },\n new Student { Name = "Boris", Course = "Physics", Grade = 82 },\n new Student { Name = "Anna", Course = "Mathematics", Grade = 95 }\n};\n\nYou want to find the average grade of each student for each course. If you use GroupBy only by name, Anna's data for Mathematics and Physics will be mixed. If only by course, the link to the student is lost. You need grouping by two columns: Name + Course.
The Solution: Anonymous Type as a Composite Key
\n\nIn C#, we can create a temporary object with the required fields right on the spot:
\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} | Average grade: {item.AverageGrade:F2}");\n}\n\nOutput:
\nAnna | Mathematics | Average grade: 90.00
\nBoris | Mathematics | Average grade: 90.00
\nAnna | Physics | Average grade: 78.00
\nBoris | Physics | Average grade: 82.00
Notice: new { s.Name, s.Course } — this is the anonymous type. LINQ automatically compares objects by all their properties (value equality).
Working with IGrouping and Nested Collections
\n\nSometimes you don't need to aggregate data, but rather preserve all original elements within the group. For this, the IGrouping<TKey, TElement> interface is used. Each group contains a key and a collection of elements:
var groups = students.GroupBy(s => new { s.Name, s.Course });\n\nforeach (var group in groups)\n{\n Console.WriteLine($"Student: {group.Key.Name}, Course: {group.Key.Course}");\n foreach (var student in group)\n {\n Console.WriteLine($" Grade: {student.Grade}");\n }\n}\n\nThis approach is ideal when you need, for example, to output a detailed report.
\n\nQuery Syntax vs Fluent — Which to Choose?
\n\nLINQ supports two styles of writing queries. Let's look at both:
\n\n1. Fluent (method syntax)
\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 (SQL-like expression syntax)
\nvar result = from student in students\n group student by new { student.Name, stud