Skip to main content

C# LINQ Grouping

LINQ grouping operations allow you to organize collections of data into groups based on specified criteria. This is similar to the GROUP BY clause in SQL, enabling powerful data analysis and transformation. In this tutorial, you'll learn how to use LINQ's grouping capabilities to organize your data effectively.

Introduction to LINQ Grouping

Grouping in LINQ is primarily accomplished using the GroupBy operator, which separates a collection into groups based on a key that you specify. Each group contains the elements that share the same key value.

The result of a GroupBy operation is a collection of IGrouping<TKey, TElement> objects, where:

  • TKey is the type of the key on which the data is grouped
  • TElement is the type of the elements in each group

Basic GroupBy Syntax

Let's look at the basic syntax of the GroupBy operator:

csharp
var groupedResult = collection.GroupBy(item => item.Property);

This returns a collection of groups, where each group contains all elements that share the same value for the specified property.

Simple Grouping Example

Let's start with a simple example grouping a list of students by their grade:

csharp
using System;
using System.Linq;
using System.Collections.Generic;

// Define a Student class
class Student
{
public string Name { get; set; }
public int Grade { get; set; }

public override string ToString()
{
return $"{Name} (Grade {Grade})";
}
}

class Program
{
static void Main()
{
// Create a list of students
List<Student> students = new List<Student>
{
new Student { Name = "Alice", Grade = 9 },
new Student { Name = "Bob", Grade = 10 },
new Student { Name = "Charlie", Grade = 9 },
new Student { Name = "Diana", Grade = 11 },
new Student { Name = "Eve", Grade = 10 },
new Student { Name = "Frank", Grade = 9 }
};

// Group students by grade
var groupedByGrade = students.GroupBy(s => s.Grade);

// Display each group
foreach (var gradeGroup in groupedByGrade)
{
Console.WriteLine($"Grade {gradeGroup.Key} students:");

foreach (var student in gradeGroup)
{
Console.WriteLine($" {student.Name}");
}
Console.WriteLine();
}
}
}

Output:

Grade 9 students:
Alice
Charlie
Frank

Grade 10 students:
Bob
Eve

Grade 11 students:
Diana

Understanding IGrouping<TKey, TElement>

The GroupBy operator returns a collection of IGrouping<TKey, TElement> objects. Each IGrouping object:

  1. Has a Key property that holds the value that was used to group the items
  2. Implements IEnumerable<TElement>, meaning you can iterate through the elements in each group

Working with Grouped Data

Once you have grouped data, you can perform various operations on each group:

Counting Elements in Groups

csharp
var studentCountByGrade = students
.GroupBy(s => s.Grade)
.Select(group => new
{
Grade = group.Key,
Count = group.Count()
});

foreach (var item in studentCountByGrade)
{
Console.WriteLine($"Grade {item.Grade}: {item.Count} students");
}

Output:

Grade 9: 3 students
Grade 10: 2 students
Grade 11: 1 student

Finding Maximum or Minimum in Groups

Let's add exam scores to our student model and find the highest score in each grade:

csharp
// Updated Student class
class Student
{
public string Name { get; set; }
public int Grade { get; set; }
public int ExamScore { get; set; }

public override string ToString()
{
return $"{Name} (Grade {Grade}, Score: {ExamScore})";
}
}

// In Main method
List<Student> students = new List<Student>
{
new Student { Name = "Alice", Grade = 9, ExamScore = 85 },
new Student { Name = "Bob", Grade = 10, ExamScore = 90 },
new Student { Name = "Charlie", Grade = 9, ExamScore = 92 },
new Student { Name = "Diana", Grade = 11, ExamScore = 88 },
new Student { Name = "Eve", Grade = 10, ExamScore = 95 },
new Student { Name = "Frank", Grade = 9, ExamScore = 80 }
};

var topScoresByGrade = students
.GroupBy(s => s.Grade)
.Select(group => new
{
Grade = group.Key,
TopStudent = group.OrderByDescending(s => s.ExamScore).First()
});

foreach (var item in topScoresByGrade)
{
Console.WriteLine($"Grade {item.Grade} top student: {item.TopStudent.Name} with score {item.TopStudent.ExamScore}");
}

Output:

Grade 9 top student: Charlie with score 92
Grade 10 top student: Eve with score 95
Grade 11 top student: Diana with score 88

Grouping with Multiple Keys

You can group items based on multiple properties by creating a composite key:

csharp
// Add a Section property to Student
class Student
{
public string Name { get; set; }
public int Grade { get; set; }
public string Section { get; set; }
public int ExamScore { get; set; }
}

// Group by both Grade and Section
var groupedByGradeAndSection = students.GroupBy(s => new { s.Grade, s.Section });

foreach (var group in groupedByGradeAndSection)
{
Console.WriteLine($"Grade {group.Key.Grade}, Section {group.Key.Section}:");

foreach (var student in group)
{
Console.WriteLine($" {student.Name}: {student.ExamScore}");
}
Console.WriteLine();
}

Using GroupBy with Query Syntax

LINQ's query syntax provides an alternative way to express grouping:

csharp
var groupedStudents = from student in students
group student by student.Grade into gradeGroup
select new
{
Grade = gradeGroup.Key,
Students = gradeGroup.ToList(),
AverageScore = gradeGroup.Average(s => s.ExamScore)
};

foreach (var group in groupedStudents)
{
Console.WriteLine($"Grade {group.Grade}: Average score = {group.AverageScore:F1}");
foreach (var student in group.Students)
{
Console.WriteLine($" {student.Name}: {student.ExamScore}");
}
}

Nested Grouping

You can create nested groups by chaining multiple GroupBy operations:

csharp
// Assuming students also have Department property
var departmentGradeGroups = students
.GroupBy(s => s.Department)
.Select(deptGroup => new
{
Department = deptGroup.Key,
GradeGroups = deptGroup.GroupBy(s => s.Grade)
.Select(gradeGroup => new
{
Grade = gradeGroup.Key,
Students = gradeGroup.ToList()
})
});

// Display the nested structure
foreach (var deptGroup in departmentGradeGroups)
{
Console.WriteLine($"Department: {deptGroup.Department}");

foreach (var gradeGroup in deptGroup.GradeGroups)
{
Console.WriteLine($" Grade {gradeGroup.Grade}:");

foreach (var student in gradeGroup.Students)
{
Console.WriteLine($" {student.Name}");
}
}
}

Real-World Example: Product Categories Analysis

Let's look at a more practical example analyzing product data:

csharp
class Product
{
public string Name { get; set; }
public string Category { get; set; }
public decimal Price { get; set; }
public int UnitsInStock { get; set; }
}

class Program
{
static void Main()
{
List<Product> products = new List<Product>
{
new Product { Name = "Apple", Category = "Fruit", Price = 1.2m, UnitsInStock = 50 },
new Product { Name = "Banana", Category = "Fruit", Price = 0.5m, UnitsInStock = 20 },
new Product { Name = "Carrot", Category = "Vegetable", Price = 0.8m, UnitsInStock = 30 },
new Product { Name = "Orange", Category = "Fruit", Price = 1.5m, UnitsInStock = 40 },
new Product { Name = "Potato", Category = "Vegetable", Price = 0.4m, UnitsInStock = 100 },
new Product { Name = "Milk", Category = "Dairy", Price = 2.5m, UnitsInStock = 15 },
new Product { Name = "Cheese", Category = "Dairy", Price = 4.5m, UnitsInStock = 10 }
};

// Get inventory value by category
var inventoryByCategory = products
.GroupBy(p => p.Category)
.Select(group => new
{
Category = group.Key,
TotalProducts = group.Count(),
TotalInventoryValue = group.Sum(p => p.Price * p.UnitsInStock),
AveragePrice = group.Average(p => p.Price)
})
.OrderByDescending(x => x.TotalInventoryValue);

Console.WriteLine("Inventory Analysis by Category:");
Console.WriteLine("-------------------------------");

foreach (var category in inventoryByCategory)
{
Console.WriteLine($"Category: {category.Category}");
Console.WriteLine($" Total Products: {category.TotalProducts}");
Console.WriteLine($" Average Price: ${category.AveragePrice:F2}");
Console.WriteLine($" Total Inventory Value: ${category.TotalInventoryValue:F2}");
Console.WriteLine();
}
}
}

Output:

Inventory Analysis by Category:
-------------------------------
Category: Vegetable
Total Products: 2
Average Price: $0.60
Total Inventory Value: $64.00

Category: Fruit
Total Products: 3
Average Price: $1.07
Total Inventory Value: $120.00

Category: Dairy
Total Products: 2
Average Price: $3.50
Total Inventory Value: $82.50

Best Practices for LINQ Grouping

  1. Immediate execution vs. deferred execution: Remember that calling ToList() or ToArray() on a group forces immediate execution, which can be useful for caching results.

  2. Memory considerations: Be aware that grouping operations can consume significant memory with large datasets, as the entire result set must be materialized.

  3. Check for empty groups: Always handle the possibility of empty groups in your code.

  4. Use meaningful key selectors: Choose keys that make logical sense for your data and intended analysis.

Summary

LINQ grouping operations provide powerful tools for organizing and analyzing collections of data. The GroupBy operator lets you:

  • Organize data into logical groups based on one or more properties
  • Perform aggregate operations like count, sum, average on each group separately
  • Create hierarchical data structures with nested groupings
  • Transform your data into more meaningful representations for analysis

Mastering LINQ grouping will significantly improve your data handling capabilities in C#, allowing you to write more concise and expressive code for data analysis tasks.

Exercises

  1. Create a list of people with names, ages, and cities. Group them by city and display the average age in each city.

  2. Create a list of file information (name, size, file extension). Group the files by extension and calculate the total size for each extension.

  3. Take a list of words and group them by their first letter, then display each group with a count of words in it.

  4. Advanced: Create a list of sales transactions with product, category, region, and amount. Group them first by region, then by category, and display the total sales for each region-category combination.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)