← Open in the full interactive course (progress tracking, search & more)

Writing a LINQ query and running it are two different moments — this lesson is about the gap between them.

Every earlier lesson in this module has quietly relied on one idea, without ever naming it directly: that var query = products.Where(p => p.Stock > 0); doesn't actually filter anything the moment it runs. It's time to name that idea properly, because it explains some of the most surprising bugs in real LINQ code — bugs where a query somehow returns different results the second time you run it, with no code in between that looks like it should have changed anything.

This lesson covers deferred execution in full: what it means, why most LINQ operators work this way, two real gotchas it causes (a query silently re-evaluated every time it's enumerated, and a captured variable that changes before enumeration happens), and how it connects back to the yield return iterators from Foundations.

What Is It?

The Simple Explanation

Deferred execution means that writing a LINQ query doesn't run it. Calling .Where(...), .Select(...), .OrderBy(...), or .GroupBy(...) just builds a small object that describes the work to be done — none of your predicate, selector, or key-selector logic actually executes yet. The real work only happens the moment something enumerates the result: a foreach loop, a call to ToList(), or an aggregation like Count().

The Technical Definition

A LINQ query variable of type IEnumerable<T> doesn't hold a result — it holds a description of a computation, built from chained iterator objects. Each operator in a chain wraps the one before it. Nothing runs until something calls GetEnumerator() on the outermost one and starts pulling items through MoveNext() — which is exactly what a foreach loop does under the compiler's hood. Until that first pull happens, the query is inert: no predicate has run, no exception it might throw has been thrown, no side effect it might cause has happened.

Why Does It Exist?

Imagine LINQ worked the opposite way — every operator ran immediately, the instant you called it:

// If Where() ran eagerly (it does NOT — this is hypothetical) var expensive = products.Where(p => p.Price > 1000); // would run the ENTIRE filter right now // You then decide you also want in-stock only var expensiveInStock = expensive.Where(p => p.Stock > 0); // would run the WHOLE thing again

Eager evaluation would mean every intermediate step in a chain does its own full pass over the data — wasted work whenever you build a query up gradually, or whenever the final consumer only needs the first few results and never asks for the rest. Deferred execution lets you compose a query piece by piece, exactly like assembling a plan, and pay the actual computation cost only once, at the one moment you actually need results:

// Deferred — building this up costs nothing until enumerated var query = products .Where(p => p.Price > 1000) .Where(p => p.Stock > 0) .OrderBy(p => p.Name); // The ENTIRE chain runs in one single pass, only now: foreach (var p in query) Console.WriteLine(p.Name);

It's also the exact mechanism that makes IQueryable<T> (from earlier in this module) possible at all — a query can't be translated into SQL before it's fully built, and it can't be fully built if pieces of it already ran the moment they were written.

Big Picture

DECLARING A QUERY vs RUNNING IT
var query = products.Where(p => p.Stock > 0);

Nothing has run. No item has been touched.
"query" only holds a description: "when someone asks, filter products by this."

foreach (var p in query) { ... } — or query.ToList(), query.Count(), etc.

Only NOW does the predicate actually run, once per item.

How It Works

DEFERRED EXECUTION, STEP BY STEP
1. CALLING A LINQ OPERATOR BUILDS AN ITERATOR OBJECT
2. CHAINING WRAPS ITERATORS INSIDE ITERATORS
3. ENUMERATION TRIGGERS THE FIRST PULL
4. EACH ITEM FLOWS THROUGH THE WHOLE CHAIN, ONE AT A TIME

Simple Example

Two classic gotchas fall directly out of deferred execution — both surprise developers who don't yet have this mental model.

Gotcha 1 — A Query Is Re-Evaluated Every Time It's Enumerated

public record Product(int Id, string Name, int Stock); List<Product> products = [ new(1, "Wireless Mouse", 120), new(2, "Standing Desk", 15), new(3, "Desk Lamp", 0), ]; var inStock = products.Where(p => p.Stock > 0); // still nothing has run Console.WriteLine(inStock.Count()); // 2 — runs the filter, right now products.Add(new Product(4, "Headphones", 8)); // mutate the ORIGINAL list Console.WriteLine(inStock.Count()); // 3 — runs the filter AGAIN, sees the new item!

Code → Meaning → Result: inStock was never a snapshot of "products in stock at the moment I wrote this line" — it's a standing instruction, "filter products whenever asked." Every single enumeration re-runs the predicate against whatever products currently contains, which is why the second Count() call sees the newly added headphones even though inStock itself was never reassigned.

Gotcha 2 — A Captured Variable Can Change Before Enumeration

decimal threshold = 50m; var affordable = products .Select(p => p) // (imagine a real product list with prices here) .Where(p => GetPrice(p) < threshold); // captures "threshold" itself, not its value threshold = 500m; // changed BEFORE the query is ever enumerated foreach (var p in affordable) Console.WriteLine(p); // uses 500m, NOT the 50m that was in scope when the query was written!

The lambda p => GetPrice(p) < threshold doesn't capture the value 50m — it captures the variable threshold itself, exactly as closures work throughout C# (a rule you saw with delegates and lambdas back in Intermediate Part III). Because the predicate only actually runs during enumeration, and enumeration happens after threshold was reassigned, the query silently uses the new value — a genuine, real-world source of subtle bugs, especially in loops that build several queries with a changing loop variable.

Real-World Example

A background service builds a query for "today's orders" once when it starts, then reuses that query variable throughout a long-running process — a subtle bug that's genuinely happened in production systems:

public class DailyReportService { private readonly List<Order> _orders; public IEnumerable<Order> GetTodaysOrders() { DateOnly today = DateOnly.FromDateTime(DateTime.Now); // Looks like a snapshot of "today" — it is NOT. // "today" is captured by reference; the predicate re-evaluates // DateOnly.FromDateTime(DateTime.Now) — no wait, it captures the LOCAL "today" variable, // which is fine here since it's not reassigned — but the underlying QUERY still // re-filters _orders every single time it's enumerated. return _orders.Where(o => o.OrderDate == today); } } // Elsewhere, called repeatedly through the day: var report = service.GetTodaysOrders(); int countAtNoon = report.Count(); // filters _orders right now, at noon _orders.Add(new Order(999, DateToday, 42.00m)); // a new order comes in int countAtFivePm = report.Count(); // filters AGAIN — includes the new order too // FIX — snapshot the result once, deliberately, if a stable point-in-time view is what's needed: var stableReport = service.GetTodaysOrders().ToList(); // covered fully in the next lesson

Whether re-evaluating on every call is a bug or a feature depends entirely on intent: if "always reflect the current state of _orders" is exactly what's wanted, deferred execution is doing its job perfectly. If a stable snapshot at one point in time was actually needed, this is the bug — and the fix (forcing immediate execution) is exactly what the next lesson covers.

Analogy

A Recipe, Not a Finished Meal

Writing a LINQ query is like writing down a recipe: "take the vegetables, filter out anything wilted, chop them, sort by size." Writing the recipe down doesn't put food on the table — nothing gets chopped or sorted until someone actually starts cooking. If the pantry's contents change between when the recipe was written and when someone finally cooks it, the cook uses whatever's in the pantry at cooking time, not whatever was there when the recipe was jotted down. That's deferred execution: the query is the recipe; enumeration is the cooking.

Under the Hood

yield return AND THE COMPILER-GENERATED STATE MACHINE
1. RECALL yield return FROM FOUNDATIONS (LESSON 038)
2. Where, Select, GroupBy — ALL BUILT THE SAME WAY
3. foreach IS SYNTACTIC SUGAR FOR CALLING MoveNext() IN A LOOP

Common Confusion

1. "Deferred execution" and "lazy evaluation" — used almost interchangeably here

In the context of LINQ, these two terms describe the same practical behavior and are frequently used interchangeably: the work doesn't happen until it's needed. Some sources draw a finer distinction elsewhere in computer science, but for everyday LINQ code, treating them as synonyms is accurate and standard.

2. Not every operator streams item-by-item, even though every operator is still deferred at the call site

As the Sorting and Grouping lessons both noted, OrderBy and GroupBy are still deferred — calling them does nothing immediately. But once enumeration actually starts, they must pull the entire source through before producing even their first result, because sorting and bucketing both require seeing everything first. Contrast that with Where and Select, which can produce their first result after examining just one source item. Deferred vs. immediate is about when work starts; streaming vs. buffering is a separate question about how much of the source must be consumed before the first result appears.

Common Mistakes

Mistake 1 — Treating a query variable as if it already holds a result

Assuming var query = products.Where(...); means "the filtered list is sitting in query right now." It means "when something enumerates query, run this filter against whatever products contains at that moment." If a genuine snapshot is needed, force it — the very next lesson shows how.

Mistake 2 — Enumerating the same expensive query multiple times without realizing it re-runs each time

Calling .Count() and then separately foreach-ing over the exact same deferred query variable, unaware that this triggers the whole predicate chain to run twice — costly if the predicate does real work (a database call inside a delegate, heavy computation, and so on). Recognize when a query is about to be enumerated more than once, and decide deliberately whether that's intended or whether the result should be materialized once instead.

When Should I Use It?

Deferred execution is the right default when

Force immediate execution instead when

Mental Model

Writing a query = "describing what should happen, later"
Enumerating a query = "actually making it happen, right now"

Remember:
· A LINQ query variable holds instructions, not results.
· Every enumeration re-runs the whole chain against the source's current state.
· A lambda captures the variable, not the value it held when the lambda was written.
· This is the same yield return state-machine mechanism from Foundations, powering nearly every LINQ operator.

Key Takeaway


Check Your Understanding

You've learned what deferred execution really means, and the two classic gotchas it causes. Let's check your understanding.

1. var query = products.Where(p => p.Stock > 0); is written, followed by no other code for several lines. At that point, has the filtering actually happened?

Show answer

Correct: B

Why B is correct: This is the core idea of the entire lesson — calling Where builds an iterator object describing the filter; the predicate itself doesn't run against any item until enumeration begins.

Why A is incorrect: This is exactly the misconception deferred execution corrects.

Why C is incorrect: The size of products has no bearing on whether execution is deferred — deferral is a property of how Where is built, not of the data.

Why D is incorrect: Absolutely nothing runs, not even a single item's worth, until something actually starts enumerating the result.

Reinforcement: A LINQ query variable is instructions, not a result — remember this before assuming any work has happened.

2. A deferred query built from products.Where(...) is enumerated once, then products is mutated (an item is added), then the same query variable is enumerated a second time. What happens?

Show answer

Correct: C

Why C is correct: As shown in Gotcha 1, a deferred query is a standing instruction re-executed on every enumeration, always against the source's current state — the mutation is fully visible the next time it's enumerated.

Why A is incorrect: Nothing about a deferred query caches or freezes the first result — there is no such snapshotting unless you explicitly force it.

Why B is incorrect: Adding an item to a List<T> between two separate, complete enumerations doesn't throw — that specific exception only applies to modifying a collection during an in-progress enumeration of it.

Why D is incorrect: The query variable remains perfectly valid and reusable — that's precisely why it can be enumerated a second time at all.

Reinforcement: A deferred query always reflects its source's state at the moment of enumeration, not at the moment it was declared.

3. decimal threshold = 50m; var q = products.Where(p => p.Price < threshold); threshold = 500m; — when q is finally enumerated with foreach, which threshold value does the predicate actually use?

Show answer

Correct: B

Why B is correct: As shown in Gotcha 2, a lambda captures the variable, not a copy of its value at the time of writing. Since the predicate doesn't actually run until foreach enumerates q — after threshold was reassigned — it sees 500m.

Why A is incorrect: This is exactly the intuitive-but-wrong assumption the lesson warns about — the value at declaration time is irrelevant to what the predicate actually uses.

Why C is incorrect: This is entirely valid C# — capturing and later reassigning a local variable used in a lambda compiles and runs without error, even though the behavior can be surprising.

Why D is incorrect: There's no alternation — every single item in the enumeration uses whatever threshold holds at that point (500m throughout, since it isn't reassigned again mid-enumeration).

Reinforcement: Combined with deferred execution, closure-captured variables can change out from under a query before it ever runs — a genuinely common real-world bug.

4. How does deferred execution in LINQ relate to the yield return iterators covered in Foundations?

Show answer

Correct: B

Why B is correct: As shown in Under the Hood (and previously in the Filtering and Projection lessons' own "Under the Hood" sections), Where and Select are built with yield return — the compiler-generated state machine is precisely the mechanism that makes "nothing runs until MoveNext() is called" true.

Why A is incorrect: They're directly connected — deferred execution is a direct consequence of how yield return-based iterators work.

Why C is incorrect: Multiple operators across this module (Where, Select, SelectMany) have all been shown using yield return — it's not limited to any one operator.

Why D is incorrect: yield return remains a core, actively used C# feature and is exactly what underlies deferred execution — nothing has replaced it.

Reinforcement: Deferred execution isn't a separate LINQ-specific trick — it's a natural consequence of the same iterator mechanism you already learned in Foundations.

You now understand why LINQ queries behave the way they do. Next up: the operators that deliberately break out of deferred execution and run right now.


dotnetmadeeasy.com — Learn C# and .NET, the right way.