Materialization with ToList: Snapshotting LINQ Results
See when ToList executes a deferred query. • Understand shallow snapshots and shared reference elements. • Compare repeated enumeration with materialization costs.
Learning outcome
By the end of this note, you should be able to explain why ToList() is often used at the end of a LINQ pipeline, what “materialization” means, and how it changes both behavior and performance. In interviews, the key idea is that a LINQ query is often just a recipe until it is enumerated. Calling ToList() executes that recipe immediately and stores the results in memory.
Intuition
LINQ to Objects is usually deferred: building the query does not read the data right away. Instead, the query is evaluated when you iterate it. ToList() changes that behavior by forcing the enumeration now and copying the results into a list.
That copy acts like a snapshot. Adding, removing or replacing entries in the source collection does not change the materialized list. This is a shallow copy: for reference-type elements, both collections can still refer to the same mutable objects. Mutating one of those objects remains visible through either reference. This is useful when you need stable data, want to iterate multiple times without re-running the query, or want to detach from a mutable source.
Deep dive
A deferred LINQ query can be re-evaluated every time you enumerate it. That means the query may observe new source values, produce new results, or repeat expensive work. ToList() stores the output; reuse the returned list to avoid rerunning the original pipeline.
This matters for three common reasons:
- Snapshot semantics: the list captures the selected entries at the moment
ToList()runs; it does not deep-clone reference-type objects. - Repeated access: later loops reuse stored results; whether this is worthwhile depends on query cost and memory needs.
- Side effects and cost: if the query includes expensive projections or reads from a mutable source, materialization avoids repeating that work.
Use this distinction carefully in interviews: this note is about LINQ to Objects. A database provider may translate a query differently, and ToList() there often means “send the query to the database now and pull the rows into memory.” The snapshot idea still applies, but the execution source is different.
Failure modes
A common mistake is treating enumeration as caching. Iterating a deferred query once does not make later iterations reuse its earlier results.
Another mistake is overusing ToList() too early. Materializing too soon can increase memory usage and remove opportunities for filtering or streaming.
A third mistake is treating the returned list as immutable or as a deep clone. Its entries do not track collection changes in the source; the list itself remains editable, and reference-type elements can still share mutable objects.
Interview drill — worked answers
- How do two query iterations differ from two list iterations? In this program, iterating
evenSquaresQuerybefore and after the edits gives4, 16and64, 16, 36. Iteratingsnapshottwice after those edits gives4, 16both times. Keep track of which variable the caller uses. - Why does the snapshot help debugging? It gives this example a clear boundary:
snapshotwas captured beforenumbers.Add(6)andnumbers[1] = 8. Those later edits explain why the query and list disagree. - What if materialization moves below both edits? The saved integers would be
64, 16, 36. The capture point is part of the program's behavior. - Would a list of mutable customer objects behave identically? The collection membership is copied, but the objects may be shared. Changing a shared customer's name is different from adding another customer to the source list.
Revision checklist
Production code
Use ToList() when you need a stable, in-memory result and you expect to traverse it more than once. The example below compares the same deferred query with its materialized integer results after source changes.
Executable code examples
ToList creates a snapshot and stops repeated reevaluation
Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
var numbers = new List<int> { 1, 2, 3, 4 };
IEnumerable<int> evenSquaresQuery = numbers
.Where(n => n % 2 == 0)
.Select(n => n * n);
Console.WriteLine("Before changing source:");
Console.WriteLine(string.Join(", ", evenSquaresQuery));
var snapshot = evenSquaresQuery.ToList();
numbers.Add(6);
numbers[1] = 8;
Console.WriteLine("After changing source, deferred query:");
Console.WriteLine(string.Join(", ", evenSquaresQuery));
Console.WriteLine("Materialized snapshot:");
Console.WriteLine(string.Join(", ", snapshot));Code walkthrough
The query evenSquaresQuery is not a list yet. It is a deferred pipeline that reads from numbers when enumerated.
The first WriteLine prints a label. The following string.Join enumerates the query: 2 and 4 pass the filter and become 4 and 16.
ToList() enumerates the pipeline again and stores 4, 16 in snapshot. After numbers.Add(6) and numbers[1] = 8, the source is 1, 8, 3, 4, 6. Its even values now square to 64, 16, 36.
The snapshot list does not change, because it already holds copied results. The query variable still points to the deferred pipeline; only snapshot holds the saved integer results.
Output: ToList creates a snapshot and stops repeated reevaluation
Before changing source:
4, 16
After changing source, deferred query:
64, 16, 36
Materialized snapshot:
4, 16
Primary references
TechInterviewNotes Editorial Team · Updated 25 Oct 2026 ·