LINQ: grouping, joins and deferred execution
Group and summarise data, join two collections, flatten nested lists, and understand when a query actually runs.
GroupBy
var byProfile = beams .GroupBy(b => b.Profile) .Select(g => new { Profile = g.Key, // the value grouped on Count = g.Count(), TotalKg = g.Sum(b => b.Kg) }) .OrderByDescending(x => x.TotalKg); foreach (var p in byProfile) Console.WriteLine($"{p.Profile,-7} {p.Count} beams {p.TotalKg,6} kg");IPE300 3 beams 865 kg HEA200 2 beams 538 kg IPE200 3 beams 370 kg
g is itself a sequence of the beams with that key, plus a Key property. ToDictionary and ToLookup are handy relatives: beams.ToDictionary(b => b.Mark) gives instant lookup by mark.
Join
var sections = new[]{ new { Name = "IPE200", DepthMm = 200 }, new { Name = "IPE300", DepthMm = 300 }, new { Name = "HEA200", DepthMm = 190 }}; var withDepth = beams.Join( sections, b => b.Profile, // key from the beam s => s.Name, // matching key from the section (b, s) => $"{b.Mark}: {s.DepthMm} mm deep"); Console.WriteLine(string.Join(" | ", withDepth.Take(3)));B1: 300 mm deep | B2: 200 mm deep | B3: 190 mm deep
SelectMany: flattening
var connections = new[]{ new { Id = "C1", Bolts = new[] { "M20", "M20", "M20", "M20" } }, new { Id = "C2", Bolts = new[] { "M16", "M16" } }}; var allBolts = connections.SelectMany(c => c.Bolts); // one flat sequenceConsole.WriteLine(allBolts.Count());Console.WriteLine(string.Join(", ", allBolts.Distinct()));6 M20, M16
Deferred execution
beams.Where(...) returns an object that remembers the query. The work happens only when something enumerates it: a foreach, ToList(), Count(), First() and so on. Toggle .ToList() in the diagram to see the difference.
| Deferred (lazy) | Immediate (runs now) |
|---|---|
Where, Select, OrderBy, Take, Skip, GroupBy, Join, SelectMany | ToList, ToArray, ToDictionary, Count, Sum, Max, First, Any, foreach |
var heavy = beams.Where(b => ExpensiveCheck(b)); // nothing runs yet int n = heavy.Count(); // runs ExpensiveCheck for every beamvar first = heavy.First(); // runs it AGAIN var cached = heavy.ToList(); // run once…int n2 = cached.Count; // …then reuse for freeLINQ and performance
LINQ is fast enough for almost all business code. Readability usually wins. In tight inner loops (millions of items per frame, like 3D rendering), a plain forloop over an array avoids small allocations. Measure with a profiler before optimising.Any()is better thanCount() > 0: it stops at the first match.
(string Profile, int Level, double Kg) tuples, print the total weight per level, sorted by level, as “Level 1: 659 kg”. Use the beams from the diagram: (IPE300,1,253) (IPE200,1,101) (HEA200,1,305) (IPE300,2,338) (IPE200,2,67) (HEA200,2,233) (IPE300,3,274) (IPE200,3,202).
Show the solution
var beams = new List<(string Profile, int Level, double Kg)>{ ("IPE300", 1, 253), ("IPE200", 1, 101), ("HEA200", 1, 305), ("IPE300", 2, 338), ("IPE200", 2, 67), ("HEA200", 2, 233), ("IPE300", 3, 274), ("IPE200", 3, 202)}; var perLevel = beams .GroupBy(b => b.Level) .OrderBy(g => g.Key) .Select(g => $"Level {g.Key}: {g.Sum(b => b.Kg)} kg"); foreach (string line in perLevel) Console.WriteLine(line);Level 1: 659 kg Level 2: 638 kg Level 3: 476 kg
Remember
GroupBygroups by a key; aggregate each group withCount,Sum,Max…Joinmatches two collections by key;SelectManyflattens nested ones.Most operators are deferred: they run when the query is enumerated. Call ToList()to run once and reuse; enumerating again re-runs the query.