LINQ in Practice · Lesson 6

Grouping and summaries

Calculate one total for each category.

Grouping changes the unit of calculation

A sales report often needs one result per category rather than one result per input row. GroupBy partitions rows according to a key, after which an aggregate can summarize each partition. The sample chooses Category as the key and Amount as the value to sum. Grouping and aggregation are separate stages: grouping identifies which rows belong together, while the sum defines the specific fact reported about those rows. A category count, average, or largest sale would require a different aggregate.

Account for every row in its category total

The source has a books sale of twelve, a food sale of eight, and another books sale of five. The two books rows belong to one group, whose amount sum is seventeen. The food group contains one row and sums to eight. The final output is books:17 followed by food:8. Each input row contributes once to its group’s total, so the combined reported amount is twenty-five. Checking that arithmetic independently helps catch missing or double-counted rows before formatting distracts from the calculation.

group.Sum(x => x.Amount)

Key equality and display ordering are different choices

GroupBy receives StringComparer.Ordinal to define category-key equality. Under that policy, books and Books are different keys. The later OrderBy arranges groups for presentation; it does not change which rows were grouped together. The sample’s simple lowercase keys appear as books then food in the invariant browser environment. For a broader identifier report, an explicit comparer on the final order can make that separate ordering policy visible too. Never infer the grouping equality rule merely from how the displayed keys happen to be sorted.

Observed groups do not include absent categories

GroupBy creates groups for keys present in the source. It does not manufacture a toys category just because the application expects that category to exist. A report requiring every configured category needs a separate complete category list and a policy for categories with no matching rows. Missing observations, a zero total, and an unknown category are different cases. Decide which are valid rather than silently treating every absent group as a numeric zero without explaining how that result was obtained.

Practice: predict a revised summary

Add a food row with amount two and predict books seventeen and food ten. Then add a books row with a zero amount and explain why the row count changes while the books total does not. Finally, project both Count and Sum for each group so those two facts remain visible. These changes show why one total alone cannot prove how many rows were processed, and why a report’s output fields should be chosen according to the question being answered.

  • Verify the sum of group totals against the sum of accepted source rows.
  • If negative amounts are invalid, validate them before grouping rather than hoping aggregation will reveal the error.

Practice: make category identity explicit

As a separate experiment, add a row whose category is Books with a capital B. Count groups under the existing ordinal comparer, then compare a deliberate ordinal-ignore-case policy. Decide whether retaining the first observed spelling is acceptable for display or whether the application needs a canonical category label from a separate catalog. Do not normalize human-facing names casually; choose the equality rule for the actual identifier domain and keep display text as a distinct concern when necessary.

  • Test empty input and a category with only zero amounts as different cases.
  • Record expected group keys, row counts, and totals before inspecting formatted output.

Validate the report at each boundary

A trustworthy grouped report states which rows are admitted, how keys compare, what aggregate is calculated, and how results are ordered and formatted. The decimal amounts in this example are simple exact values; they do not define an external accounting or rounding policy. Larger ranges can still overflow a bounded numeric type, and missing categories require separate modeling. Treat the sample as a clear local calculation whose invariants can be checked, then extend only the boundaries the surrounding application actually needs.

  • Baseline totals are twelve plus five for books, and eight for food.
  • Ordinal grouping equality is explicit; group display order is a separate stage.
  • An empty source produces no observed groups, not a complete catalog of zero-valued categories.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var rows = new[] { new Sale("books", 12m), new Sale("food", 8m), new Sale("books", 5m) };
foreach (var group in rows.GroupBy(x => x.Category, StringComparer.Ordinal).OrderBy(x => x.Key))
    Console.WriteLine($"{group.Key}:{group.Sum(x => x.Amount)}");
record Sale(string Category, decimal Amount);

Expected output

books:17
food:8
Try this
A grouping’s Key identifies the category; its items hold the original rows.

Further reading