<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. C#
  3. Functional C# and LINQ
All lessons in this course

Lesson 42 of 60 15 minIntermediate

LINQ: grouping, joins and deferred execution

Group and summarise data, join two collections, flatten nested lists, and understand when a query actually runs.

Basic LINQ filters and sorts. Real reports need more: totals per profile, beams joined with their section data, all bolts from all connections. And to use LINQ well you must know when a query really executes.

Interactive

Grouping and deferred execution

GroupBy(b => b.)
var summary = beams.GroupBy(b => b.Profile)
.Select(g => new { g.Key, Count = g.Count(), TotalKg = g.Sum(b => b.Kg), MaxLen = g.Max(b => b.Length) });
Key = "IPE300"
B1B4B7
Count 3 · TotalKg 865 · MaxLen 8
Key = "IPE200"
B2B5B8
Count 3 · TotalKg 370 · MaxLen 9
Key = "HEA200"
B3B6
Count 2 · TotalKg 538 · MaxLen 7.2
Deferred execution
var list = new List<Beam>(beams);
var longOnes = list.Where(b => b.Length > 7); // only a recipe: nothing runs yet
list.Add(new Beam("B9", Length: 7.5));
Console.WriteLine(longOnes.Count());
Prints 4. Where only stores the recipe. It actually runs when Count() enumerates it, after B9 was added, so B9 is included. Each enumeration runs the query again.

GroupBy

Group.cs
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");
Output
IPE300  3 beams    865 kg
HEA200  2 beams    538 kg
IPE200  3 beams    370 kg

Each group 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

Join.cs
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)));
Output
B1: 300 mm deep | B2: 200 mm deep | B3: 190 mm deep

SelectMany: flattening

Flatten.cs
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()));
Output
6
M20, M16

Deferred execution

Most LINQ operators don’t do anything when you call them. 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, SelectManyToList, ToArray, ToDictionary, Count, Sum, Max, First, Any, foreach
Twice.cs
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 free

LINQ 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 for loop over an array avoids small allocations. Measure with a profiler before optimising.
  • Any() is better than Count() > 0: it stops at the first match.
Quick check

When does var q = list.Where(x => x > 5); filter the list?

Quick check

Which operator turns a list of lists into one flat sequence?

Practice

Given a list of (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
Solution.cs
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);
Output
Level 1: 659 kg
Level 2: 638 kg
Level 3: 476 kg

Remember

  • GroupBy groups by a key; aggregate each group with Count, Sum, Max…
  • Join matches two collections by key; SelectMany flattens 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.
0 of 60 lessons completed