The Operators That Do Real Work
GroupBy, Join/GroupJoin, Aggregate, Zip/Chunk, and the set operations โ with the shapes they return.
GroupBy produces groups of pairs
GroupBy(keySel) yields IGrouping<TKey, TElement> โ a Key plus the
members in encounter order. Counts, tops, and pivots all fall out:
var byCity = people.GroupBy(p => p.City);
foreach (var g in byCity)
Console.WriteLine($"{g.Key}: {g.Count()}");
var topOfEach = people
.GroupBy(p => p.City)
.Select(g => g.OrderByDescending(p => p.Score).First());
ToLookup(keySel) is GroupBy's eager sibling: one pass, then O(1)
indexing by key, and missing keys yield EMPTY groups instead of throwing.
Join and GroupJoin
Join pairs inner+outer on keys (flat rows); GroupJoin produces one
outer item with a group of matches โ the shape of "customers with their
orders, including zero-order customers":
var q = customers.GroupJoin(orders,
c => c.Id, o => o.CustomerId,
(c, cs) => (c.Name, Count: cs.Count()));
Left outer join = GroupJoin + SelectMany with an empty-group default.
Aggregate: fold a sequence into one value
int maxGap = diffs.Aggregate((acc, next) => Math.Max(acc, next));
Seed form Aggregate(seed, func, resultSelector) builds arbitrary results.
Prefer a plain foreach when the fold needs explanation โ Aggregate is not a
flex.
Windowing and sets
Zip(second)pairs element-wise (stops at shorter).Chunk(size)splits into fixed-size batches (.NET 6+).Distinct/Union/Intersect/Exceptcompare by DEFAULT equality โ pass a comparer or project first for value semantics.
Check your understanding
- GroupBy vs ToLookup โ which is lazy? (GroupBy; ToLookup runs immediately.)
- Which join keeps customers with zero orders? (GroupJoin.)