Grouping with GroupBy and Common Aggregates in LINQ to Objects
Learn how to group in-memory collections with GroupBy and compute common aggregates like Count, Sum, Average, Min, and Max for reporting and interview-style problems in LINQ to Objects.
Learning outcome
By the end of this note, 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.
Execution and key equality
GroupBy is deferred. Groups retain first-key encounter order; items retain source order. A supplied comparer controls key equality; otherwise the default comparer applies.
In .NET 10 LINQ to Objects, starting enumeration builds a lookup from the source before yielding the first group. This buffers the grouped elements; deferred does not mean streaming here. See the runtime sources below.
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 — worked answers
- How do you find total order value per customer? Use the first query below: group by
Customer, then select the key and the sum ofAmount. Alice's three amounts are25.50 + 39.99 + 14.25 = 79.74; the other totals are Bob71.99and Cara28.70. - How do you return departments whose average salary exceeds a threshold? Group employees by department, project each department and its average salary, then filter those report rows by the threshold. Filtering employees first answers a different question. For salaries
40and100, the group average is70; filtering out40first changes it to100. - How does a composite key change the report? Grouping by customer makes three rows here. Grouping by year, month and category makes four rows. January Books combines orders 1, 2 and 4 even though they belong to different customers; February Books is a separate bucket.
- When is a manual dictionary accumulation worth considering?
GroupBykeeps the per-group members available, which is useful for inspection and several reports. If you need only a running total per customer, a dictionary of totals can store one total per customer instead. Compare maintainability and measured workload costs rather than claiming one approach is always faster. - How do you sort by group name, then total descending? Apply ascending name ordering to the report rows and descending total as the tie-breaker. In this customer-only report each name has one row, so that tie-breaker has no effect. The sample instead sorts by total descending, then customer name; its order is Alice, Bob, Cara.
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.
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);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.
Trace Alice's bucket: amounts 25.50, 39.99, 14.25 give count 3, total 79.74, average 26.58, minimum 14.25 and maximum 39.99. Bob's average is 71.99 / 2 = 35.995, displayed as 36.00. February Games combines 59.99 + 19.95 = 79.94.
The two queries answer different questions over the same seven input rows. The first creates three customer summaries; the second creates four month/category summaries. Their final ordering makes each printed report explicit.
Output: Group orders by customer and compute common aggregates
This display assumes an English-style decimal separator. F2 formats two fractional digits using the current culture; formatting rounds the display without changing the stored decimal value.
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
TechInterviewNotes Editorial Team · Updated 7 Oct 2026 ·