Skip to main content

.NET LINQ Grouping

In this tutorial, you'll learn how to organize and structure data using LINQ's powerful grouping capabilities. Grouping operations allow you to arrange data into buckets based on specified key values, similar to the GROUP BY clause in SQL.

Introduction to LINQ Grouping

When working with collections of data, we often need to organize items into groups based on common characteristics. For example, you might want to group a list of employees by department, products by category, or expenses by month. LINQ provides elegant ways to perform these grouping operations through methods like GroupBy and related operations.

LINQ grouping transforms a flat collection into a hierarchical structure where items are organized into groups, each identified by a key value.

Basic Grouping with GroupBy

The primary method for grouping data in LINQ is GroupBy. This method creates a collection of IGrouping<TKey, TElement> objects, where each IGrouping contains a key and a collection of elements associated with that key.

Simple GroupBy Example

Let's start with a simple example where we group a list of numbers by their remainder when divided by 3:

csharp
using System;
using System.Linq;

public class Program
{
public static void Main()
{
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Group numbers by their remainder when divided by 3
var groups = numbers.GroupBy(n => n % 3);

// Display each group and its elements
foreach (var group in groups)
{
Console.WriteLine($"Numbers with remainder {group.Key} when divided by 3:");
foreach (var number in group)
{
Console.Write($"{number} ");
}
Console.WriteLine("\n");
}
}
}

Output:

Numbers with remainder 1 when divided by 3:
1 4 7 10

Numbers with remainder 2 when divided by 3:
2 5 8

Numbers with remainder 0 when divided by 3:
3 6 9

In this example:

  • We group the numbers based on their remainder when divided by 3
  • Each group has a key (the remainder value: 0, 1, or 2)
  • Each group contains numbers that satisfy the grouping condition

Grouping Complex Objects

Grouping becomes even more useful when working with complex objects. Let's look at a more realistic example with a collection of product objects:

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

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

public class Program
{
public static void Main()
{
List<Product> products = new List<Product>
{
new Product { Name = "Apple", Category = "Fruit", Price = 0.99m },
new Product { Name = "Banana", Category = "Fruit", Price = 0.59m },
new Product { Name = "Carrot", Category = "Vegetable", Price = 0.29m },
new Product { Name = "Orange", Category = "Fruit", Price = 0.79m },
new Product { Name = "Broccoli", Category = "Vegetable", Price = 1.29m },
new Product { Name = "Chicken", Category = "Meat", Price = 5.99m },
new Product { Name = "Beef", Category = "Meat", Price = 7.99m }
};

// Group products by category
var productsByCategory = products.GroupBy(p => p.Category);

// Display products by category
foreach (var category in productsByCategory)
{
Console.WriteLine($"Category: {category.Key}");
Console.WriteLine($"Product count: {category.Count()}");

foreach (var product in category)
{
Console.WriteLine($" - {product.Name}: ${product.Price}");
}
Console.WriteLine();
}
}
}

Output:

Category: Fruit
Product count: 3
- Apple: $0.99
- Banana: $0.59
- Orange: $0.79

Category: Vegetable
Product count: 2
- Carrot: $0.29
- Broccoli: $1.29

Category: Meat
Product count: 2
- Chicken: $5.99
- Beef: $7.99

This example groups products by their category, creating a hierarchical structure that makes it easy to process or display related items together.

GroupBy with Element Selection

The GroupBy method allows you to transform each element when creating groups using an element selector:

csharp
// GroupBy with element selection
var productNamesByCategory = products.GroupBy(
keySelector: p => p.Category, // Key to group by
elementSelector: p => p.Name // Element transformation
);

foreach (var category in productNamesByCategory)
{
Console.WriteLine($"Category: {category.Key}");
Console.WriteLine(string.Join(", ", category));
Console.WriteLine();
}

Output:

Category: Fruit
Apple, Banana, Orange

Category: Vegetable
Carrot, Broccoli

Category: Meat
Chicken, Beef

In this version, we're only taking the product names for each group, which simplifies the output.

GroupBy with Result Selection

LINQ's GroupBy offers an overload that lets you create a custom result for each group:

csharp
var categorySummary = products.GroupBy(
p => p.Category,
(key, group) => new {
Category = key,
Count = group.Count(),
Average = group.Average(p => p.Price),
Total = group.Sum(p => p.Price)
}
);

foreach (var summary in categorySummary)
{
Console.WriteLine($"Category: {summary.Category}");
Console.WriteLine($"Count: {summary.Count}");
Console.WriteLine($"Average price: ${summary.Average:F2}");
Console.WriteLine($"Total value: ${summary.Total:F2}");
Console.WriteLine();
}

Output:

Category: Fruit
Count: 3
Average price: $0.79
Total value: $2.37

Category: Vegetable
Count: 2
Average price: $0.79
Total value: $1.58

Category: Meat
Count: 2
Average price: $6.99
Total value: $13.98

This powerful feature allows you to compute aggregate statistics for each group in a single LINQ query.

Creating Groups with ToLookup

While GroupBy executes when you enumerate the results, LINQ offers another grouping method called ToLookup that performs immediate grouping and stores the results for quick lookup:

csharp
// Create a lookup
var productLookup = products.ToLookup(p => p.Category);

// Retrieve items by key
Console.WriteLine("Fruits:");
foreach (var item in productLookup["Fruit"])
{
Console.WriteLine($" - {item.Name}: ${item.Price}");
}

Console.WriteLine("\nVegetables:");
foreach (var item in productLookup["Vegetable"])
{
Console.WriteLine($" - {item.Name}: ${item.Price}");
}

// Check if a key exists
string searchCategory = "Electronics";
if (productLookup[searchCategory].Any())
{
Console.WriteLine($"\nFound {searchCategory} products");
}
else
{
Console.WriteLine($"\nNo {searchCategory} products found");
}

Output:

Fruits:
- Apple: $0.99
- Banana: $0.59
- Orange: $0.79

Vegetables:
- Carrot: $0.29
- Broccoli: $1.29

No Electronics products found

ToLookup is useful when you need to access groups multiple times throughout your code, as it computes the grouping only once.

Practical Example: Sales Analysis

Let's look at a real-world application of LINQ grouping for analyzing sales data:

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

public class SalesRecord
{
public DateTime Date { get; set; }
public string Product { get; set; }
public string Region { get; set; }
public decimal Amount { get; set; }
}

public class Program
{
public static void Main()
{
// Sample sales data
List<SalesRecord> sales = new List<SalesRecord>
{
new SalesRecord { Date = new DateTime(2023, 1, 15), Product = "Laptop", Region = "North", Amount = 1200 },
new SalesRecord { Date = new DateTime(2023, 1, 20), Product = "Phone", Region = "South", Amount = 800 },
new SalesRecord { Date = new DateTime(2023, 2, 5), Product = "Tablet", Region = "East", Amount = 500 },
new SalesRecord { Date = new DateTime(2023, 2, 10), Product = "Laptop", Region = "West", Amount = 1300 },
new SalesRecord { Date = new DateTime(2023, 2, 15), Product = "Phone", Region = "North", Amount = 750 },
new SalesRecord { Date = new DateTime(2023, 3, 3), Product = "Laptop", Region = "South", Amount = 1250 },
new SalesRecord { Date = new DateTime(2023, 3, 12), Product = "Tablet", Region = "East", Amount = 550 },
new SalesRecord { Date = new DateTime(2023, 3, 20), Product = "Phone", Region = "West", Amount = 820 },
};

// Group by month and calculate sales stats
var monthlySales = sales
.GroupBy(s => s.Date.Month)
.Select(g => new
{
Month = new DateTime(2023, g.Key, 1).ToString("MMMM"),
TotalSales = g.Sum(s => s.Amount),
AverageSale = g.Average(s => s.Amount),
NumberOfSales = g.Count()
})
.OrderBy(m => m.Month);

Console.WriteLine("Monthly Sales Report:");
foreach (var month in monthlySales)
{
Console.WriteLine($"{month.Month}:");
Console.WriteLine($" Total Sales: ${month.TotalSales}");
Console.WriteLine($" Average Sale: ${month.AverageSale:F2}");
Console.WriteLine($" Number of Sales: {month.NumberOfSales}");
Console.WriteLine();
}

// Group by product and region to see performance
var productByRegion = sales
.GroupBy(s => new { s.Product, s.Region })
.Select(g => new
{
Product = g.Key.Product,
Region = g.Key.Region,
TotalSales = g.Sum(s => s.Amount),
Count = g.Count()
})
.OrderByDescending(x => x.TotalSales);

Console.WriteLine("Product Performance by Region:");
foreach (var item in productByRegion)
{
Console.WriteLine($"{item.Product} in {item.Region}: ${item.TotalSales} ({item.Count} sales)");
}
}
}

Output:

Monthly Sales Report:
January:
Total Sales: $2000
Average Sale: $1000.00
Number of Sales: 2

February:
Total Sales: $2550
Average Sale: $850.00
Number of Sales: 3

March:
Total Sales: $2620
Average Sale: $873.33
Number of Sales: 3

Product Performance by Region:
Laptop in North: $1200 (1 sales)
Laptop in West: $1300 (1 sales)
Laptop in South: $1250 (1 sales)
Phone in West: $820 (1 sales)
Phone in South: $800 (1 sales)
Phone in North: $750 (1 sales)
Tablet in East: $550 (1 sales)
Tablet in East: $500 (1 sales)

This example demonstrates how LINQ grouping can transform raw sales data into meaningful reports and insights.

Grouping with Multiple Keys

Sometimes you need to group by multiple properties. LINQ allows you to do this by creating composite keys using anonymous types:

csharp
// Group by both product type and region
var groupedByProductAndRegion = sales
.GroupBy(s => new { s.Product, s.Region })
.Select(g => new {
Product = g.Key.Product,
Region = g.Key.Region,
Total = g.Sum(s => s.Amount),
Count = g.Count()
});

Console.WriteLine("\nSales by Product and Region:");
foreach (var group in groupedByProductAndRegion)
{
Console.WriteLine($"{group.Product} in {group.Region}: ${group.Total} ({group.Count} sales)");
}

Summary

LINQ grouping operations provide powerful tools for organizing and summarizing data in your .NET applications:

  • Use GroupBy to organize a collection into groups based on a key
  • Each group consists of a key and a collection of elements
  • You can transform elements or customize results during grouping
  • ToLookup provides immediate grouping with efficient key-based retrieval
  • Grouping supports multiple keys through composite key objects
  • Combine grouping with aggregation to create powerful data analyses

Grouping is one of LINQ's most valuable features for working with data, allowing you to structure collections in a way that makes them easier to process and analyze. By mastering LINQ grouping operations, you'll be able to transform flat data into rich, hierarchical structures that better represent the relationships within your data.

Additional Resources and Exercises

Resources

Exercises

  1. Student Grades: Create a student class with Name, Subject, and Grade properties. Group students by subject and calculate the average grade for each subject.

  2. File Organization: Create a program that takes a list of filenames and groups them by extension. Display how many files of each type exist.

  3. Word Frequency: Take a string of text, split it into words, and group by the word. Count how many times each word appears.

  4. Advanced Sales Analysis: Extend the sales analysis example to group by quarter and year, and show which product had the highest sales in each time period.

  5. Nested Grouping: Create a nested grouping that first groups data by year, then by month, showing total sales for each month in each year.

Remember that mastering LINQ grouping is about understanding both the syntax and when and how to apply it to your specific data needs. Happy coding!



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