Learning outcome
By the end of this lesson, you should be able to explain when a LINQ to Objects query is only a recipe and when it actually runs, predict the effect of enumerating the same sequence more than once, and choose when to force a snapshot with materialization. Interviewers often want to know whether you understand that a query like Where(...) is usually deferred, while operators such as ToList() and ToArray() immediately enumerate the source. They also want you to notice hidden costs: repeated database access is not the LINQ to Objects topic, but repeated work over an in-memory sequence absolutely is.
Intuition
Think of a LINQ query as a description of a walk through data, not the walk itself. The query object can be created cheaply, passed around, and stored. Deferred operators postpone traversal until a consumer requests results. A foreach enumerates the sequence; scalar operators such as Count() and First() evaluate a result, but can use collection metadata or indexing instead of enumerating every element. This matters because the source sequence may be cheap, expensive, changing, or even one-shot. If you enumerate twice, you often repeat the whole pipeline twice.
A useful interview rule is:
- deferred operators describe work
- terminal operators trigger work
- materialization captures a snapshot at a moment in time
Deep dive
LINQ to Objects is built around IEnumerable<T>. Many query operators, including Where and Select, are deferred and stream items. Others, such as OrderBy and GroupBy, defer execution but buffer the source before yielding results. That means the source is revisited only as needed, and intermediate operators may run for each enumeration. If the source is a generator with side effects, those side effects also repeat.
Consider the difference between these behaviors:
Where,Select,Skip, andTakeusually defer executionOrderBymust inspect the whole sequence before yielding the first resultToList,ToArray, andToDictionaryforce immediate execution and store resultsCount,Any,First, andLastevaluate immediately; how much they enumerate depends on the operator and source, and optimized collection paths can avoid enumeration
A snapshot is important when the source can change between enumerations. If you keep a query around and the underlying list changes, later enumeration sees the new state. If you need stable results, materialize once.
Failure modes
Common interview traps include:
- Assuming query creation executes immediately. It does not for most standard operators.
- Assuming enumerating a query twice is free. It repeats the pipeline.
- Forgetting that
ToList()changes a lazy sequence into a snapshot. - Treating
Count()on a query as a property access. It may enumerate the whole sequence. - Ignoring side effects in selectors or predicates. Those can happen multiple times if the sequence is re-enumerated.
- Confusing deferred execution with streaming:
OrderByandGroupByare deferred but buffer data, whereasToLookup,ToListandToArrayevaluate immediately.
Interview drill
Try answering these in one sentence each:
- What happens when you assign
var q = numbers.Where(...);? - Why might
q.Count()be more expensive than expected? - What changes if you call
ToList()before iterating? - What happens if the source list is modified after query creation but before enumeration?
- How can you avoid repeating expensive computation across multiple passes?
A strong answer mentions deferred execution, repeated enumeration, and snapshotting with materialization.
Revision checklist
Use this checklist before an interview:
- Can I explain deferred vs immediate execution clearly?
- Do I know which operators are lazy and which force full evaluation?
- Can I describe what happens on first and second enumeration?
- Can I explain why materializing a query creates a snapshot?
- Can I warn about side effects inside predicates and selectors?
- Can I recognize when
Count(),Any(), orFirst()still enumerate?
Production code
The example below shows deferred execution, repeated enumeration, and snapshot behavior using a deterministic in-memory sequence. The source is a list created before the query. The loops show the results of repeated enumeration and the effect of adding a source item after materialization.
Code walkthrough
The output ordering is the whole lesson:
Source createdis printed first because the list is built immediately. Defining theWhere/Selectquery does not evaluate its predicates or projections; those run when the result is enumerated.- The first
foreachwalks the source and produces even numbers multiplied by ten. - The second
foreachruns the whole pipeline again, proving repeated enumeration. ToList()materializes the query intosnapshot, so later changes tosourcedo not affect the stored list.- Enumerating
queryafter adding6to the source sees the new element, because the query is not a frozen result.
A useful interview phrasing is: “A LINQ query is a reusable plan, not a cached result, unless I materialize it.” That single sentence captures the main behavior.
Output: Deferred execution, repeated enumeration, and snapshot with LINQ to Objects
Source created
Query created
First enumeration:
item=20
item=40
Second enumeration:
item=20
item=40
Snapshot count=2
After snapshot, mutate source and enumerate snapshot again:
snapshot-item=20
snapshot-item=40
Enumerate query after mutation:
item=20
item=40
item=60
Snapshot scope
Materialization stores the selected entries, not deep copies of referenced objects. A later change to a shared mutable object can still be visible in both collections.
Executable code examples
Deferred execution, repeated enumeration, and snapshot with LINQ to Objects
Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
var source = CreateNumbers();
var query = source.Where(n => n % 2 == 0).Select(n => n * 10);
Console.WriteLine("Query created");
Console.WriteLine("First enumeration:");
foreach (var item in query)
{
Console.WriteLine($"item={item}");
}
Console.WriteLine("Second enumeration:");
foreach (var item in query)
{
Console.WriteLine($"item={item}");
}
var snapshot = query.ToList();
Console.WriteLine($"Snapshot count={snapshot.Count}");
Console.WriteLine("After snapshot, mutate source and enumerate snapshot again:");
source.Add(6);
foreach (var item in snapshot)
{
Console.WriteLine($"snapshot-item={item}");
}
Console.WriteLine("Enumerate query after mutation:");
foreach (var item in query)
{
Console.WriteLine($"item={item}");
}
static List<int> CreateNumbers()
{
var numbers = new List<int> { 1, 2, 3, 4 };
Console.WriteLine("Source created");
return numbers;
}