A query can describe work that has not happened yet
The Where call creates a sequence describing which list elements should be selected. It does not immediately copy the matching integers into a separate collection. This distinction is called deferred execution: the filtering work happens when a consumer enumerates the query. In the sample, the source list still exists and can change between query construction and consumption. To predict the output, follow the program’s timeline rather than imagining the variable named query already contains a fixed answer.
View full size ↗Read the diagram
Begin with source = [1, 2, 3] and query = source.Where(n => n % 2 == 0). Creating this query does not enumerate the list.
Add 4 to source before calling query.ToList(). Enumeration now produces [2, 4]; snapshot holds that materialized list.
Next add 6 to source. A fresh enumeration of query produces [2, 4, 6], while snapshot still contains [2, 4]. The source changes occur between enumerations, not during a list enumeration.
The numeric illustration is independent of the runnable sample. For reference-type elements, ToList copies references into a new list; it does not deep-clone the objects.
Illustration created with ImageGen for SharpRune.
Locate the actual enumeration boundary
The list initially contains one and four, and the predicate accepts values greater than two. After constructing query, the program appends six to that same list. String.Join then enumerates query to produce the printed text, so the filter sees one, four, and six. It rejects one and yields four and six, producing 4,6. The placement of Add before Join is the crucial ordering fact; the output is not determined solely by the list contents on the Where line.
var query = values.Where(x => x > 2);Repeated enumeration repeats the observation
Keeping a deferred sequence allows a later consumer to ask for its elements again. That later enumeration can observe later source contents and rerun the predicate. This is useful for composing pipelines without intermediate arrays, but it is not a promise of a stable snapshot. For this small List source, the source object and the captured predicate remain the query’s inputs. A different enumerable can have additional behavior, so avoid assuming every sequence is cheap, reusable, or independent of external state.
Mutation timing and mutation during enumeration differ
Appending six before enumeration begins is the deliberate behavior demonstrated here. Changing a List while its enumerator is actively running is a different situation and can invalidate that enumerator. Do not generalize the example into a promise that arbitrary concurrent or mid-loop edits are safe. Another trap is adding side effects to the predicate and then accidentally enumerating twice: a log message, counter increment, or other effect can happen twice even when the source values did not change.
Practice: move the observation in time
Print query once before appending six and once afterward. Predict 4 for the first print and 4,6 for the second. Then append a value that fails the predicate and verify why the source count changes while the visible result does not. These edits isolate the difference between source mutation and selected-result mutation. Keep the predicate pure so any change in output can be explained by source values and the time at which the sequence is consumed.
- Draw a timeline containing Where construction, each Add call, and each enumeration through String.Join.
- Use a fresh list for each experiment so repeated runs of a modified example do not mix several intended cases.
Practice: choose a different contract explicitly
As an independent experiment, call ToArray immediately after Where and keep that array as the query result. Append six to the source afterward and compare the array with a newly enumerated deferred query. Explain why the array retains its selected membership while the deferred query sees the new matching value. This is not evidence that one style is universally superior; the two forms express different requirements about when work happens and which version of mutable input the caller wants.
- State whether the consumer needs current source contents or the contents captured at an earlier boundary.
- If you instrument the predicate, count evaluations separately from the number of values printed.
Make timing part of the query explanation
The most important question is where enumeration occurs. A query variable can be a reusable description of work rather than a stored answer, and a terminal consumer determines when that work is observed. In a user interface, an import, or a report, document whether results should follow later source edits or remain tied to an explicit snapshot. The lesson uses in-memory managed collections available in the browser runtime; it does not imply that a remote query provider or an arbitrary external resource has identical execution behavior.
- Baseline output is
4,6because the append happens before enumeration. - Where filters during enumeration; it does not materialize an array by itself.
- A snapshot boundary, repeated enumeration, and mutation during active enumeration are three separate concerns.
A working example
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
var values = new List<int> { 1, 4 };
var query = values.Where(x => x > 2);
values.Add(6);
Console.WriteLine(string.Join(",", query));
Expected output
4,6
Creating a Where query does not freeze its source.