Organizing Data by Category
Imagine you're building a sales dashboard that needs to show revenue by region, products by category, or customers by signup month. You have a flat list of data but need it organized into logical groups. Manually looping through collections and building dictionaries is tedious and error-prone.
LINQ's group by keywords let you organize collections into categories with a single expression. You specify a key selector—how items should be grouped—and LINQ handles the rest. The into keyword extends this by letting you query the groups themselves, filtering or transforming them before final projection.
You'll learn basic grouping syntax, how to use into for query continuation, and patterns for aggregating group data. By the end, you'll transform raw data into organized summaries without writing manual grouping logic.
Basic group by Syntax
The group clause takes two parts: what to group (the element) and by what key. The result is an IEnumerable<IGrouping<TKey, TElement>>—a sequence of groups where each group has a Key property and contains all elements matching that key. You can iterate groups directly or project them into a more useful shape.
Without LINQ, you'd manually loop through items, check if a key exists in a dictionary, and add items to lists. Group by handles all of this in one expression, making your code declarative and easier to read. The compiler translates it to efficient GroupBy method calls.
Here's basic grouping in action:
var products = new List<Product>
{
new("Laptop", "Electronics", 999),
new("Mouse", "Electronics", 25),
new("Desk", "Furniture", 350),
new("Chair", "Furniture", 200),
new("Keyboard", "Electronics", 75)
};
// Group by category
var byCategory = from p in products
group p by p.Category;
foreach (var group in byCategory)
{
Console.WriteLine($"{group.Key}:");
foreach (var product in group)
{
Console.WriteLine($" {product.Name} - ${product.Price}");
}
}
// Method syntax equivalent
var byCategoryMethod = products.GroupBy(p => p.Category);
record Product(string Name, string Category, decimal Price);
The query groups products by their Category property. Each group is an IGrouping with a Key (the category name) and a sequence of Product objects in that category. You can enumerate the groups and then enumerate items within each group. The method syntax version produces identical results—choose based on readability.
Query Continuation with into
The into keyword creates a range variable for the grouped results, letting you continue querying. After group by into, you can apply where clauses to filter groups, orderby to sort them, or select to project them. This is powerful for filtering aggregates—like showing only categories with more than 3 items or sorting groups by their count.
Without into, your group query ends immediately after grouping. With into, you can chain additional operations on the groups themselves before projecting final results. This eliminates the need for separate filtering steps after grouping.
Using into for continued querying:
var sales = new List<Sale>
{
new("North", "Q1", 15000),
new("North", "Q2", 18000),
new("South", "Q1", 12000),
new("West", "Q1", 9000),
new("West", "Q2", 11000),
new("West", "Q3", 13000)
};
// Group by region, filter groups with 2+ sales, project summary
var regionSummary = from s in sales
group s by s.Region into regionGroup
where regionGroup.Count() >= 2
orderby regionGroup.Key
select new
{
Region = regionGroup.Key,
TotalSales = regionGroup.Sum(x => x.Amount),
QuarterCount = regionGroup.Count()
};
foreach (var summary in regionSummary)
{
Console.WriteLine($"{summary.Region}: ${summary.TotalSales:N0} " +
$"across {summary.QuarterCount} quarters");
}
record Sale(string Region, string Quarter, decimal Amount);
After grouping by Region, into creates regionGroup as a variable representing each group. We then filter groups with at least 2 sales, sort by region name, and project a summary with total sales and count. The where clause operates on groups (not individual sales), and orderby sorts the groups. This produces clean, aggregated results in one query.
Projecting Group Results
Often you don't want the raw IGrouping objects—you want summaries or specific shapes. You can project groups inline or use into with a final select. Common projections include aggregates like Count, Sum, Average, or extracting specific fields from the key and elements.
Projecting during grouping creates anonymous types or records with exactly the fields you need. This is cleaner than iterating groups later and manually building result objects. LINQ handles the iteration and transformation in one pass.
Different projection patterns:
var orders = new List<Order>
{
new(1, "Alice", 50),
new(2, "Bob", 75),
new(3, "Alice", 30),
new(4, "Charlie", 100),
new(5, "Bob", 45)
};
// Direct projection without into
var customerTotals = from o in orders
group o.Amount by o.Customer into g
select new { Customer = g.Key, Total = g.Sum() };
// Projection with detailed group info
var customerDetails = from o in orders
group o by o.Customer into g
select new
{
Customer = g.Key,
OrderCount = g.Count(),
TotalAmount = g.Sum(x => x.Amount),
AvgOrder = g.Average(x => x.Amount),
Orders = g.Select(x => x.Id).ToList()
};
foreach (var detail in customerDetails)
{
Console.WriteLine($"{detail.Customer}: {detail.OrderCount} orders, " +
$"${detail.TotalAmount} total (avg ${detail.AvgOrder:F2})");
}
record Order(int Id, string Customer, decimal Amount);
The first query projects just customer and total, grouping amounts directly. The second creates a detailed summary with count, sum, average, and a list of order IDs. Each IGrouping supports all LINQ operators—Count, Sum, Average, Select, Where, etc. You can build rich summaries without leaving the query expression.
Nested and Multi-Level Grouping
Sometimes you need grouping within groups—like products by category and then by price range, or sales by year and then by quarter. You can nest group clauses or use composite keys. For simple two-level grouping, use an anonymous type as the key.
Nested grouping creates a hierarchy of IGrouping objects. You can iterate the outer groups, then inner groups, or project them into nested structures. This is powerful for creating hierarchical reports or drill-down data.
Multi-level grouping examples:
var transactions = new List<Transaction>
{
new(2024, "Q1", "North", 5000),
new(2024, "Q1", "South", 3000),
new(2024, "Q2", "North", 6000),
new(2024, "Q2", "South", 4000),
new(2025, "Q1", "North", 5500)
};
// Composite key grouping
var byYearQuarter = from t in transactions
group t by new { t.Year, t.Quarter } into g
select new
{
Period = $"{g.Key.Year} {g.Key.Quarter}",
Total = g.Sum(x => x.Amount)
};
// Nested grouping with projection
var hierarchical = from t in transactions
group t by t.Year into yearGroup
select new
{
Year = yearGroup.Key,
Quarters = from q in yearGroup
group q by q.Quarter into qGroup
select new
{
Quarter = qGroup.Key,
Regions = from r in qGroup
group r by r.Region into rGroup
select new
{
Region = rGroup.Key,
Total = rGroup.Sum(x => x.Amount)
}
}
};
record Transaction(int Year, string Quarter, string Region, decimal Amount);
The first query groups by a composite key (both year and quarter), producing flat groups. The second creates a three-level hierarchy: year → quarter → region. Each level uses group...into with a nested query. This builds a tree structure perfect for hierarchical reports or JSON serialization.
Choosing Your Grouping Approach
Query syntax vs method syntax: Use query syntax for complex queries with multiple clauses. It reads naturally for group-filter-project workflows. Use method syntax (GroupBy) for simple grouping or when chaining with other method calls. Both produce identical compiled code.
Projection timing: Project groups immediately if you know the final shape. Use into when you need to filter or sort groups before projection. Immediate projection is cleaner when you don't need group filtering.
Memory considerations: Grouping loads all data into memory to organize it. For large datasets with database sources, ensure grouping happens in SQL (via IQueryable) rather than in-memory. Check the generated SQL to verify group by translates to the database.
Alternative: ToDictionary vs GroupBy: If you need fast key lookup after grouping, consider ToDictionary or ToLookup. Lookup preserves multi-valued keys like GroupBy but returns a dictionary-like structure. This is faster for repeated key-based access than iterating groups.
Try It Yourself
Build a console app that groups sample data and produces summary reports using group, by, and into.
Steps
- Init:
dotnet new console -n GroupByDemo
- Open:
cd GroupByDemo
- Replace Program.cs with the code shown below
- Execute:
dotnet run
var employees = new List<Employee>
{
new("Alice", "Engineering", 85000),
new("Bob", "Engineering", 92000),
new("Carol", "Sales", 75000),
new("Dave", "Sales", 68000),
new("Eve", "Engineering", 95000),
new("Frank", "Marketing", 72000)
};
// Group by department with summary
var deptSummary = from e in employees
group e by e.Department into dept
where dept.Count() >= 2
orderby dept.Key
select new
{
Department = dept.Key,
EmployeeCount = dept.Count(),
TotalSalary = dept.Sum(x => x.Salary),
AvgSalary = dept.Average(x => x.Salary)
};
Console.WriteLine("=== Department Summary (2+ employees) ===\n");
foreach (var dept in deptSummary)
{
Console.WriteLine($"{dept.Department}:");
Console.WriteLine($" Employees: {dept.EmployeeCount}");
Console.WriteLine($" Total Salary: ${dept.TotalSalary:N0}");
Console.WriteLine($" Avg Salary: ${dept.AvgSalary:N0}\n");
}
record Employee(string Name, string Department, decimal Salary);
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
What you'll see
=== Department Summary (2+ employees) ===
Engineering:
Employees: 3
Total Salary: $272,000
Avg Salary: $90,667
Sales:
Employees: 2
Total Salary: $143,000
Avg Salary: $71,500
Try modifying the where clause to show all departments, or change orderby to sort by average salary descending.