Deferred Execution and Repeated Enumeration in LINQ to Objects
Learn when a LINQ to Objects query actually runs, why enumerating the same query twice repeats work, and how that differs from database query translation and execution.
What you will learn
Use the two examples to answer: when does the predicate run, what repeats on a second pass, and what changes after materialization? These examples use LINQ to Objects over an array or list. Run each example as a separate console program.
Where builds a deferred query: the predicate runs as a consumer asks for elements. Keeping the query variable does not store the matching values. The source collection and the query are different things.
A library slip and a saved list
Imagine a library slip saying “copy the even-numbered shelf labels.” Writing the slip makes no list. A visit does the checking; keeping its completed list lets another reader reuse that visit’s result. The slip represents the query, a visit represents enumeration, and the completed list represents materialization.
Limit: this story describes the examples below. It does not promise that every enumerable can be replayed, or that keeping object references freezes the objects themselves.
Example 1 Count the work instead of printing every check
Before reading the output, predict the three counter values. The counter is a teaching probe, not a recommended way to structure production filtering.
using System;
using System.Linq;
var source = new[] { 1, 2, 3, 4 };
int predicateCalls = 0;
var query = source.Where(n =>
{
predicateCalls++; // Teaching probe only.
return n % 2 == 0;
});
Console.WriteLine($"Defined: {predicateCalls}");
foreach (var item in query)
{
Console.WriteLine($"First value: {item}");
}
Console.WriteLine($"After first pass: {predicateCalls}");
foreach (var item in query)
{
Console.WriteLine($"Second value: {item}");
}
Console.WriteLine($"After second pass: {predicateCalls}");Expected output
Defined: 0 First value: 2 First value: 4 After first pass: 4 Second value: 2 Second value: 4 After second pass: 8
The three checkpoints are 0 → 4 → 8. The first loop tests all four input values and prints the two even values. The second loop performs the same four tests. The counter is cumulative, so 8 means four checks in each of two passes, not eight checks in the second pass alone.
Nothing in this program changes the array or the condition. That explains why the values repeat even though the work repeats. Equal output does not prove that the earlier results were saved.
Example 2 Keep one result before the source changes
ToList() evaluates the query when called and returns a list containing that result. Reuse the returned list when you want to keep those selected entries. It does not change the original query into a cache.
using System;
using System.Collections.Generic;
using System.Linq;
var source = new List<int> { 1, 2, 3, 4 };
var query = source.Where(n => n % 2 == 0);
var snapshot = query.ToList();
source.Add(6); // Change the source after materialization.
Console.WriteLine($"Query now: {string.Join(", ", query)}");
Console.WriteLine($"Saved list: {string.Join(", ", snapshot)}");Expected output
Query now: 2, 4, 6 Saved list: 2, 4
The list was made before 6 was added. The later query sees 6; the saved list still contains 2 and 4. Enumerating snapshot again reads its stored entries. Calling query.ToList() again would be another materialization, not reuse of snapshot.
Snapshot scope: the list stores entries, not deep copies of referenced objects. Its membership is separate from the source collection, but shared mutable objects can still change. The returned list is also mutable.
When ToList is worth it
- Use it deliberately: you need several passes over one selected result, or you need to retain that result before its source changes.
- Account for the cost: materialization does the work now and allocates storage. A single pass over a cheap query often needs no extra list.
- First consider one pass: if you only need to process each match, consume the query once rather than counting it and then looping again.
If the question is only “does any match exist?”, Any(predicate) expresses that question and stops as soon as it can decide. Creating a list just to test whether it is empty does unnecessary storage work.
Only materialize a sequence you can afford to consume completely. An infinite sequence cannot become a completed finite list; a very large result may exceed your memory budget.
Pitfalls to catch in a review
- Hidden extra passes: look for separate consumers of one deferred query. Extra passes can repeat expensive work or side effects.
- Side effects in a predicate: the counter above is diagnostic. Avoid relying on repeated filtering to perform essential writes or other actions.
A count is not always a traversal: Count() can use an available collection count. Do not claim every count scans all elements. A filtered sequence still needs to determine which elements match.
Deferred does not always mean streaming: an ordinary enumeration of OrderBy needs the input sorted before it yields the first result. Delaying work and producing results incrementally are separate properties.
Scope the timing claim: in Example 1, defining query does not run its predicate. That is narrower than claiming an entire statement containing a query can do no work.
Keep database queries distinct
An IQueryable<T> provider decides how its expression tree executes; it may translate the query for another data source. The interface alone does not promise SQL or remote execution.
EF Core database queries are a concrete example: the provider translates a query representation to the database’s language. Repeating a database query can mean another database execution, so use the provider’s behavior when evaluating cost.
Practice before checking the answers
- In Example 1, remove the second loop. What is the final counter value?
- Restart Example 1, but break the first loop immediately after it prints its first item. At the break, which item has been printed, and how many predicate calls have happened?
- In Example 2, move
source.Add(6)beforequery.ToList(). What will the saved list contain? - Starting from Example 1 after both loops, call
var saved = query.ToList();, then loop oversavedtwice. What will the counter be after materialization and after those two loops?
Answers and reasoning
- 4. There is one complete pass over the four-item input.
- 2 is printed; the counter is 2. The filter tests 1 and rejects it, then tests 2 and yields it. The break prevents this loop from asking for another match.
- 2, 4, 6. The changed order makes 6 part of the source before the list is built.
- 12, then still 12. Building the list adds four tests to the existing eight. Both later loops read the saved entries and leave the counter alone.
Interview answer in your own words: identify the source, the query, and each consumer. Explain the observed passes, then justify whether you want a fresh result each time or one retained result. Mention the storage trade-off if you choose materialization.
Practice
TechInterviewNotes Editorial Team · Updated 7 Oct 2026 ·