Deferred, Immediate, and the Multiple-Enumeration Trap
When a query runs, what re-runs it, and the materialization discipline that keeps sources honest.
Two execution modes, one rule of thumb
Most operators are DEFERRED โ they record a plan. Count(), ToList(),
ToArray(), ToDictionary(), First(), Sum(), Any() are IMMEDIATE โ
they pull. Composition chains stay lazy until one of those fires:
var big = nums.Where(n => n > 10).Select(n => n * 2); // no work yet
var total = big.Sum(); // NOW the pipeline runs
Sources matter: an array re-iterates cheaply, but a query built on a method with side effects re-triggers them on EVERY enumeration.
The multiple-enumeration bug
var expensive = LoadAndParse(); // IEnumerable โ lazy
if (expensive.Any()) // pass 1
Render(expensive.Where(x => x.Ok)); // pass 2: LoadAndParse again!
Fix: materialize once at the boundary โ var data = LoadAndParse().ToList();
โ then query the list freely. Materialize EAGER data sparingly (memory),
LAZY side-effecting sources always.
Captured variables: the plan is not a snapshot
A deferred query closes over VARIABLES, so changing them before execution changes the result:
int threshold = 10;
var q = nums.Where(n => n > threshold);
threshold = 100; // q now uses 100 โ queries see current state
This is the same capture semantics as lambdas (Module 3) applied to plans.
IEnumerable vs IQueryable (concept)
IEnumerable<T> composes delegates; filtering happens in memory.
IQueryable<T> composes EXPRESSION TREES a provider can translate (to SQL).
You use IQueryable against a database in Advanced/data work โ but the
discipline here (look at the source, know when it runs) is the same.
Check your understanding
- Which operator makes the whole chain run? (Any immediate operator.)
- Why materialize a side-effecting source once? (Every enumeration re-runs the side effects.)