Skip to content
Preptima
mediumCodingConceptMidSenior

You build a LINQ query, then change the list it came from, then enumerate it. What do you get?

The results reflect the list and the captured variables as they are at enumeration time, not at query construction, because a LINQ-to-objects query is an iterator holding references rather than a snapshot. Enumerating it twice runs the whole chain twice.

5 min readUpdated 2026-07-29Target archetype: Big Tech, Enterprise Captive, Product Startup
Practice answering out loud

What the interviewer is scoring

  • Does the candidate say the query holds a reference to the source rather than a copy of it
  • Whether captured variables are recognised as live at enumeration time rather than snapshotted at construction
  • That re-enumeration is described as re-running the chain, with either a cost or a side-effect consequence
  • Whether the candidate can name an operator that buffers the sequence and one that streams it
  • Does the answer give a rule for where to materialise rather than sprinkling ToList defensively

Answer

Building a query executes nothing

names.Where(predicate) does not filter anything. It constructs an object that implements IEnumerable<T> and remembers two things: the source sequence and the delegate. No element is read, no predicate is invoked, and no allocation proportional to the data is made. The work happens when something calls GetEnumerator and starts pulling — a foreach, a ToList, a Count, an await foreach, or passing it to a method that enumerates.

That much most people know. The consequence that catches them out is what "remembers the source" means. It is a reference to the list, not a copy of it, and the delegate closes over variables rather than over their values at the moment you wrote the lambda. So the query is a recipe, and the ingredients are looked up fresh each time you cook.

var names = new List<string> { "ann", "bob" };
var threshold = 3;

// Constructs an iterator. Nothing is filtered, nothing is copied.
var shortNames = names.Where(n => n.Length <= threshold);

names.Add("charlie");
threshold = 7;

foreach (var n in shortNames) Console.WriteLine(n);
// ann, bob, charlie

Trace it. At the point the query is written the list holds two items and threshold is 3, so the intuitive answer is ann and bob. But enumeration happens after both mutations, against a list of three items and a threshold of 7. ann and bob are three characters, charlie is exactly seven, and all three pass. The query you wrote would have returned two items; the query you ran returned three, including one added after the fact and one that only qualifies under a threshold changed after the fact.

Every enumeration runs the whole chain again

Because there is no stored result, each enumeration re-executes every stage. Three consequences matter in real code.

The first is cost. if (query.Any()) { var first = query.First(); } enumerates the source twice, and a chain ending in an OrderBy sorts twice. Where each stage is cheap this is invisible; where a stage calls a method that hits a network or a disk it is a duplicated round trip per enumeration.

The second is side effects. A Select whose projection writes a log line, increments a counter, or mutates the object it is projecting will do that once per enumeration. Two enumerations produce two log lines per element and an unexplained doubled metric, which is a genuinely hard bug to see from reading the code because nothing in the code says "twice".

The third is instability. Two enumerations of the same query over a source someone else is mutating can legitimately produce different results, so a method that validates a query and then returns it has validated nothing. Worse, mutating the source during enumeration throws: List<T>'s enumerator tracks a version stamp, so foreach (var n in shortNames) names.Remove(n); fails with InvalidOperationException on the iteration after the removal rather than doing something quietly wrong.

Streaming operators and buffering operators

The distinction is worth having ready, because it decides whether a query can work over an infinite or expensive sequence. Where, Select, Take, Skip and Cast are streaming: they pull one element, do their work, and yield, so memory is constant and the first result appears before the source has been fully read.

OrderBy, GroupBy, Reverse, ToList, ToDictionary and Distinct cannot behave that way. Sorting requires knowing every element before the first one can be emitted, so OrderBy reads and buffers the entire source on the first MoveNext. That is why query.OrderBy(x => x.Name).First() still materialises the whole sequence, and why chaining Take(10) after an OrderBy limits what you receive rather than what is read. Getting this right in an interview separates someone who has read the operator remarks from someone who has assumed laziness is uniform.

Count() deserves a footnote of its own: on a source that implements ICollection<T> it short-circuits to the Count property, but on a chained query it walks every element. So the same call is O(1) or O(n) depending on what it is called on.

Closures capture variables, and loops complicate that

Since C# 5, a foreach iteration variable is a fresh variable per iteration, so a lambda created inside the loop body captures that iteration's value. A for loop's counter was not changed and is still one variable shared by every iteration, so a list of deferred queries built inside a for loop all close over the same counter and all see its final value.

var queries = new List<IEnumerable<int>>();
for (int i = 0; i < 3; i++)
{
    // Captures i itself, not its value. All three queries filter on i == 3.
    queries.Add(numbers.Where(n => n == i));
}

The fix is to copy into a local inside the loop body, which is the same discipline the deferred-execution model demands everywhere: if you want a value fixed at the point you wrote the code, you must fix it yourself.

Where to materialise

The rule that keeps this manageable is about boundaries rather than about caution. Materialise with ToList or ToArray when you cross a boundary where the source's mutability or the query's cost stops being yours to reason about: returning from a method whose caller cannot see the source, storing the result for later, enumerating more than once, or handing it to code that will iterate while you mutate. Keep the query lazy inside one method where you can see both ends of it, since that is where laziness pays for itself by not allocating intermediate collections.

The opposite failure is worth naming too. ToList in the middle of a chain allocates a full intermediate collection and defeats the streaming that made the chain cheap, so a defensive ToList after every operator turns a constant-memory pipeline into several copies of the data.

A LINQ-to-objects query is an iterator holding a reference to its source and a closure over live variables, so what you get depends entirely on when you enumerate it and how many times.

Likely follow-ups

  • Any followed by First on the same query. What does that cost, and what would you write instead?
  • Which standard operators must consume the whole sequence before they can yield anything, and why?
  • A method returns IEnumerable built with yield return and the caller enumerates it twice. What happens?
  • What changes about all of this once the query is an IQueryable against a database?

Related questions

Further reading

linqdeferred-executionclosuresiteratorscsharp