Skip to content
Search lessons, topics, tests…
Esc

    ↑ ↓ moveEnter openEsc close

    Module 1 · LINQ to Objects · Lesson 4 of 4

    Grouping with GroupBy and Common Aggregates in LINQ to Objects

    Watch

    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 Key representing 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 size
    • Sum(...) for totals
    • Average(...) for mean values
    • Min(...) / 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:

    1. Group the source sequence.
    2. Compute one or more aggregates per group.
    3. 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 GroupBy with Distinct: Distinct removes duplicates, while GroupBy collects all duplicates together.
    • Over-projecting too early: if you reduce your data before grouping, you may throw away fields needed for aggregation.
    • Assuming ordering: GroupBy does 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 edge cases: group correctness and cost

    The practice examples use LINQ to Objects and modern C# (C# 9 or later for top-level programs), with the usual System, System.Collections.Generic and System.Linq imports. Unless a question says otherwise, inputs are non-null and are not modified concurrently.

    • Choose equality as well as the key shape. A comparer such as StringComparer.OrdinalIgnoreCase can put differently cased identifiers in one group when the domain requires it. Include year as well as month in a multi-year monthly report.
    • LINQ to Objects groups follow the order in which their keys first appear; entries within a group retain source order. That is not sorting by key. Add explicit ordering for the report’s required key or aggregate order.
    • Standard GroupBy is deferred but buffers the source before returning its first complete group. Breaking after the first group does not avoid that work. During one grouping enumeration, Count and Sum operate on the stored groups rather than restarting the original source. Multiple aggregates can still cost additional work over group contents.
    • Filter at the required grain. To find customers whose combined spend exceeds a threshold, aggregate all relevant orders first and filter totals afterward. To choose the top three customers, calculate their full totals, order the report rows, then Take(3). Limiting orders first changes those totals.
    • Combine averages using total sum divided by total count when every original item should have equal weight. Averaging group averages equally gives small and large groups the same influence.
    • An empty input produces no groups. Missing categories are not manufactured with zero totals; start from an explicit category set when the report must display categories without activity.

    Interview drill

    Try answering these aloud before looking at code:

    1. How would you find the total order value per customer?
    2. How do you compute average salary per department and return only departments above a threshold?
    3. What is the difference between grouping by a single field and grouping by a composite key?
    4. When would you prefer GroupBy over manual dictionary accumulation?
    5. 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 GroupBy returns.
    • I can choose an appropriate grouping key.
    • I can apply Count, Sum, Average, Min, and Max to 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.
    • Min and Max show 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

    C#Runs
    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);

    Optional video

    kudvenkat · 10:28. Grouped reporting and per-group counts. The presentation uses older C# syntax. Watching is optional.

    Watch on YouTube.

    Receipts in labeled envelopes

    Picture sorting receipts into envelopes labeled by customer. The key decides which envelope receives each receipt; the equality rule decides whether differently written labels mean the same customer. Each envelope still contains all its receipts, so you can count them, total them or find their range.

    To know the first envelope is complete, you must inspect the remaining receipts too. The envelopes are not automatically alphabetized. A customer with no receipts does not acquire an empty envelope unless the report deliberately starts from a separate customer list.

    Grouping and reporting checklist

    • State the grain: one report row per which key or composite key?
    • Choose key equality explicitly when default comparison does not match the domain.
    • g.Key identifies a group; g.Count() counts its entries; g.Sum(...) totals its values.
    • Standard LINQ to Objects GroupBy is deferred but buffers before its first group.
    • Multiple aggregates read the resulting groups; they do not each restart the original source within that grouping enumeration.
    • Group order follows first-seen keys, not key sorting. Add the report’s ordering explicitly.
    • Filter totals after aggregation; take top report rows after calculating and ordering full totals.
    • Combine averages as total sum / total count. Empty input creates no groups.

    Practice

    Sign in to mark lessons done and keep your place in the course.Sign in