Materialization captures selected membership now
A snapshot is useful when later processing should use the elements selected at a particular boundary. ToArray enumerates the query immediately and stores the results in a new array. In this sample the elements are integers, so both the array storage and those copied values are straightforward to reason about. Materialization specifies when selection happens; it does not automatically guarantee that every result is immutable or that every object reachable from a selected element has also been copied.
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.
Compare the captured array with a fresh query
The source starts with two and five. Filtering for values greater than three yields only five, and ToArray stores that result before the next line runs. The source then receives eight. Printing snapshot therefore produces 5, while building and consuming the later filter sees both five and eight and prints 5,8. The two output lines answer different historical questions: which elements matched at snapshot creation, and which elements match in the source after the append.
var snapshot = source.Where(x => x > 3).ToArray();The array is a separate, still-mutable collection
The new array has its own sequence storage and a fixed Length established during materialization. Adding an item to the source List does not resize or repopulate that array. The array itself can still be edited through its indexer, however, so calling it a snapshot is a statement about captured membership, not an immutable type guarantee. If another caller receives the array reference, that caller can mutate the array unless the API establishes a different access or ownership boundary.
Materialization is not a recursive object clone
When elements are references to mutable records or classes, ToArray copies the selected references. It does not inspect and clone each object’s properties. A later edit to a selected object can remain visible through both the source and snapshot array. That does not contradict this integer example; it exposes a different element type’s ownership rules. Decide whether a report requires stable membership, stable object contents, or both, and implement the additional copying or immutable representation needed for that actual contract.
Practice: edit each collection independently
Change snapshot[0] to ninety-nine after materialization and print both snapshot and the fresh filtered source. Predict ninety-nine for the array while the source still contains five and eight as matching values. Then restore the array and append a source value below the threshold. Explain why the source changes without either the snapshot’s Length or the later selected values necessarily changing. This exercise separates storage independence from predicate selection and from the total source collection size.
- Record source contents and snapshot contents in separate columns after every mutation.
- Do not use an array modification as evidence that ToArray failed to create a separate array.
Practice: choose stable contents as well as membership
For a separate extension, replace the integers with objects containing a mutable score. Materialize the matching objects, change one selected object’s score through the source reference, and inspect the array. Then project each object into a new small value-like summary before materializing it. Compare the two snapshot contracts. The concrete task is to decide which fields a later report must preserve rather than to invoke a vague deep-copy operation whose treatment of every nested object is unspecified.
- Test a source append separately from a selected object’s property edit; they affect different boundaries.
- Write down whether the result is intended for one use, repeated inspection, or sharing with another component.
Pay for a snapshot because the consumer needs one
Immediate materialization consumes the source and allocates result storage. That cost can be appropriate for a stable boundary, repeated reads, or avoiding repeated expensive enumeration. It can be unnecessary for a single streaming pass, and it cannot finish for a truly unbounded sequence. The example is intentionally small and local, so correctness is about timing and ownership rather than a performance benchmark. Choose materialization based on the required semantics, then test its behavior under the source changes that the surrounding workflow actually permits.
- Baseline prints
5, then5,8. - ToArray establishes an immediate enumeration boundary and separate array storage.
- Stable membership does not imply immutable elements or a recursively copied object graph.
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 source = new List<int> { 2, 5 };
var snapshot = source.Where(x => x > 3).ToArray();
source.Add(8);
Console.WriteLine(string.Join(",", snapshot));
Console.WriteLine(string.Join(",", source.Where(x => x > 3)));
Expected output
5 5,8
ToArray fixes membership at the time it enumerates.