Learning outcome
By the end of this lesson, 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() prevents that by enumerating once and storing the output.
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 over the list are cheap compared with rerunning the query.
- 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 lesson 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 assuming a LINQ query already contains results. It does not, unless it has been materialized or otherwise enumerated.
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 forgetting that ToList() freezes the results, not the original source. If the source changes later, the list does not magically track it.
Interview drill
Explain the difference between these two patterns:
- Store an
IEnumerable<T>query and enumerate it twice. - Call
ToList()once and enumerate the list twice.
A strong answer should mention that the first pattern may repeat query evaluation, while the second creates a one-time snapshot.
Follow-up question: why might ToList() make debugging easier when a source collection is mutated after the query is defined?
Revision checklist
- [ ] I can define materialization in one sentence.
- [ ] I can explain that
ToList()forces immediate execution. - [ ] I can describe snapshot semantics clearly.
- [ ] I can explain why repeated enumeration can repeat work.
- [ ] I can distinguish LINQ to Objects from provider-translated queries.
- [ ] I know when
ToList()is helpful and when it is unnecessary.
Production code
Use ToList() when you need a stable, in-memory result and you expect to traverse it more than once. The example below shows a deferred query observing source changes until it is materialized, after which the list stays fixed.
Code walkthrough
The query evenSquaresQuery is not a list yet. It is a deferred pipeline that reads from numbers when enumerated.
The first WriteLine forces enumeration, so the output reflects the original values 2 and 4, transformed into 4 and 16.
ToList() then materializes those results into snapshot. After that, the source list changes: one element is updated and one is added. The deferred query sees those new values the next time it runs.
The snapshot list does not change, because it already holds copied results. That is the core interview takeaway: ToList() gives you a stable snapshot and stops repeated reevaluation of the original query.
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
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));