Learning outcome
By the end of this lesson, you should be able to use GroupBy to organize in-memory data into logical buckets and then apply aggregates to each bucket for reporting. You will also know when a grouping is useful, how the grouping key affects results, and how to avoid common mistakes such as grouping too late or projecting too much data before aggregation.
Intuition
GroupBy is the LINQ tool for answering questions like:
- How many orders did each customer place?
- What is the total sales amount per category?
- Which department has the highest average salary?
Think of it as building a dictionary-like view of your data where each key points to a sequence of matching items. Once you have the groups, you can summarize them with aggregates.
A useful sequence is: group the input by a key, aggregate the items in each group, and project one report row per group.
This is especially important in LINQ to Objects, where grouping happens in memory against sequences already available in your process. That is different from database LINQ, where the provider may translate a query into SQL. Here, the examples focus only on LINQ to Objects.
Deep dive
GroupBy returns a sequence of IGrouping<TKey, TElement>. Each grouping has:
- a
Keyrepresenting the group value - the items that share that key
A common interview pattern is to group by a field and then immediately project the result into a report shape. That keeps the output compact and easy to read.
Typical aggregate operations include:
Count()for group sizeSum(...)for totalsAverage(...)for mean valuesMin(...)/Max(...)for extremes
A useful detail: the selector used in GroupBy determines the grouping key. If you group by department, every item with the same department ends up in the same bucket. If you need a composite key, you can group by an anonymous type or a tuple.
For reporting, a good flow is often:
- Group the source sequence.
- Compute one or more aggregates per group.
- Project to a small report DTO or anonymous type.
This is usually easier to reason about than manually maintaining nested dictionaries or counters.
Failure modes
Common mistakes with grouping and aggregates include:
- Grouping on the wrong field: if the key is too broad or too narrow, the report becomes misleading.
- Forgetting that groups are sequences: a group is not a single value; you still need aggregate methods or further projection.
- Confusing
GroupBywithDistinct:Distinctremoves duplicates, whileGroupBycollects all duplicates together. - Over-projecting too early: if you reduce your data before grouping, you may throw away fields needed for aggregation.
- Assuming ordering:
GroupBydoes not sort groups by default; sort explicitly if the report needs order. - Mixing database assumptions into LINQ to Objects: methods and behaviors that are safe in memory may not translate in a database provider, so keep the mental model clear.
Interview drill
Try answering these aloud before looking at code:
- How would you find the total order value per customer?
- How do you compute average salary per department and return only departments above a threshold?
- What is the difference between grouping by a single field and grouping by a composite key?
- When would you prefer
GroupByover manual dictionary accumulation? - How would you produce a sorted report by group name and then by total descending?
A strong answer usually mentions grouping first, then aggregating, then shaping the output.
Revision checklist
- I can explain what
GroupByreturns. - I can choose an appropriate grouping key.
- I can apply
Count,Sum,Average,Min, andMaxto each group. - I can project grouped results into a readable report shape.
- I can distinguish LINQ to Objects grouping from database query translation.
- I can spot when ordering must be added explicitly after grouping.
Production code
The following example groups a list of orders by customer, then computes several common aggregates per customer. It also shows a composite-key grouping by month and category to demonstrate that GroupBy can produce reporting slices beyond a single field.
Code walkthrough
The example starts with an in-memory list of Order records. That makes it pure LINQ to Objects, so the grouping and aggregations happen over local data.
The first query groups by Customer:
GroupBy(o => o.Customer)creates one grouping per customer name.Count()tells us how many orders each customer placed.Sum(o => o.Amount)gives the total spend.Average(o => o.Amount)gives the typical order size.MinandMaxshow the range of order amounts.
The second query uses a composite key:
GroupBy(o => new { o.OrderDate.Year, o.OrderDate.Month, o.Category })
This is useful when the report needs several dimensions, such as month and category. The anonymous object becomes the group key, and you can read its properties from g.Key.
Notice the pattern: grouping first, then aggregating, then sorting the final report. That pattern is a reliable interview answer and a practical production habit.
Output: Group orders by customer and compute common aggregates
Customer summary:
Alice: count=3, total=79.74, avg=26.58, min=14.25, max=39.99
Bob: count=2, total=71.99, avg=36.00, min=12.00, max=59.99
Cara: count=2, total=28.70, avg=14.35, min=8.75, max=19.95
Month/category summary:
2026-01 Books: count=3, total=51.75
2026-01 Games: count=1, total=39.99
2026-02 Books: count=1, total=8.75
2026-02 Games: count=2, total=79.94
Executable code examples
Group orders by customer and compute common aggregates
Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
var orders = new List<Order>
{
new(1, "Alice", "Books", new DateOnly(2026, 1, 3), 25.50m),
new(2, "Bob", "Books", new DateOnly(2026, 1, 5), 12.00m),
new(3, "Alice", "Games", new DateOnly(2026, 1, 8), 39.99m),
new(4, "Alice", "Books", new DateOnly(2026, 1, 9), 14.25m),
new(5, "Bob", "Games", new DateOnly(2026, 2, 1), 59.99m),
new(6, "Cara", "Books", new DateOnly(2026, 2, 2), 8.75m),
new(7, "Cara", "Games", new DateOnly(2026, 2, 2), 19.95m),
};
var perCustomer = orders
.GroupBy(o => o.Customer)
.Select(g => new
{
Customer = g.Key,
OrderCount = g.Count(),
TotalSpent = g.Sum(o => o.Amount),
AverageOrder = g.Average(o => o.Amount),
CheapestOrder = g.Min(o => o.Amount),
MostExpensiveOrder = g.Max(o => o.Amount)
})
.OrderByDescending(x => x.TotalSpent)
.ThenBy(x => x.Customer)
.ToList();
Console.WriteLine("Customer summary:");
foreach (var row in perCustomer)
{
Console.WriteLine($"{row.Customer}: count={row.OrderCount}, total={row.TotalSpent:F2}, avg={row.AverageOrder:F2}, min={row.CheapestOrder:F2}, max={row.MostExpensiveOrder:F2}");
}
Console.WriteLine();
var perMonthAndCategory = orders
.GroupBy(o => new { o.OrderDate.Year, o.OrderDate.Month, o.Category })
.Select(g => new
{
g.Key.Year,
g.Key.Month,
g.Key.Category,
Count = g.Count(),
Total = g.Sum(o => o.Amount)
})
.OrderBy(x => x.Year)
.ThenBy(x => x.Month)
.ThenBy(x => x.Category)
.ToList();
Console.WriteLine("Month/category summary:");
foreach (var row in perMonthAndCategory)
{
Console.WriteLine($"{row.Year}-{row.Month:D2} {row.Category}: count={row.Count}, total={row.Total:F2}");
}
public sealed record Order(int Id, string Customer, string Category, DateOnly OrderDate, decimal Amount);