CodeGym /Courses /C# SELF /Deferred Execution

Deferred Execution

C# SELF
Level 34, Lesson 0
Available

1. Introduction

Imagine this: you wrote a long, beautiful LINQ query and you think — "Voilà, everything will be processed now!" But then you notice nothing happens until you count the elements or turn the result into an array. That's not a bug, it's a feature.

In .NET, LINQ uses deferred execution: the query doesn't run until you actually start pulling data out. It's like a lazy waiter — he won't run to the kitchen until he hears: "Bring the food!", even if the order is already placed.

This behavior, known as lazy evaluation (Lazy Evaluation), makes LINQ especially efficient when working with large, potentially infinite, or resource-heavy data sources.

Deferred execution means that a LINQ query doesn't execute right after you define it. It only starts working when you actually start looping through or looking at the data in the collection.

Example Illustration


var numbers = new List<int> { 1, 2, 3, 4, 5 };

// LINQ query
var query = numbers.Where(n =>{
    Console.WriteLine($"Checking {n}");
    return n % 2 == 0;
});
Console.WriteLine("Query defined, but numbers haven't been checked!");

// Only now does the iteration start!
foreach (var n in query)
{
    Console.WriteLine($"Found even: {n}");
}

What will happen?
When you run this code, you'll see that before the foreach loop, nothing is printed to the console — not even the conditions in your Where are executed. As soon as you start iterating (for example, with foreach), the query starts running.

This is deferred execution — until you ask, nobody does anything!

2. Why do we need deferred execution?

Deferred execution makes your code not just elegant, but super efficient. Why do work ahead of time if maybe you won't even need the result? This is especially important when you're dealing with big collections or data streams that are still coming in — imagine a source that could be infinite. Loading all that into memory at once just doesn't make sense.

Plus, deferred execution lets you freely combine and build up queries. You can chain together complex LINQ operations without worrying that they'll start running right away. Everything happens only when you actually start getting the data — and not a second earlier.

Analogy

Deferred execution is like a shopping list on your phone: you can add and edit items as much as you want, but you only go to the store when you're actually ready to buy something (and only then do you work with the list).

3. How does it work under the hood?

LINQ queries that return IEnumerable<T> are usually implemented with iterators (yield return) or special lazy constructs. Every time you start looping through the collection (like calling foreach or ToList()), the query runs again.

Important Note

If you change the original collection between defining the query and running it, the new or changed data will also show up in the result.

Example:


var numbers = new List<int> { 1, 2, 3 };
var query = numbers.Where(n => n > 1);
numbers.Add(4); // added a new number
foreach (var n in query)
{
    Console.WriteLine(n); // will print 2, 3, 4
}

Diagram: When does a LINQ query run?

Deferred execution diagram: the query "waits" until it's actually used

4. Examples where LINQ's "laziness" matters

Lazy filtering


var bigNumbers = Enumerable.Range(1, 1_000_000_000)
    .Where(n => n % 123_456 == 0);

foreach (var n in bigNumbers.Take(5))
{
    Console.WriteLine(n);
}

What's going on here?
The query creates a potentially billion-item collection, but actually filters and returns only 5 numbers! Everything else just isn't calculated and doesn't take up memory.

Nested queries and transactions

Let's say we have a list of orders and products, and we want to get the first 5 orders that contain a certain product.


var orders = GetBigOrderList(); // imagine there are thousands of orders here

var filtered = orders
    .Where(order => order.Products.Any(p => p.Name == "Coffee"))
    .Take(5);

foreach(var o in filtered)
{
    Console.WriteLine(o.Id);
}

LINQ doesn't look at the rest of the orders once it finds 5 matches!

5. Surprises

Scenarios where deferred execution can lead to surprises:

Changing the data source after defining the query

As shown earlier, if you change the original collection between defining and actually running the query, the query will see the new data.

Iterating the same query multiple times

A LINQ query runs again every time you enumerate it.


var query = numbers.Where(n => {
    Console.WriteLine($"Checking {n}");
    return n % 2 == 0;
});

foreach(var n in query) {} // first iteration
foreach(var n in query) {} // second iteration — everything is recalculated again

If you want a repeatable result — materialize it (do .ToList() or .ToArray()).

6. When are LINQ queries NOT deferred?

Not all LINQ operations are deferred. Some methods do immediate ("eager") execution (Immediate Execution). For example:

  • .ToList()
  • .ToArray()
  • .Count()
  • .Average()
  • .Sum()
  • .First(), .Last(), .Single()

They all force LINQ to run the query right away, because they return not IEnumerable, but a ready result.

Example:


var query = numbers.Where(n => n > 2);
var result = query.ToList(); // The query runs right here!

7. The concept of "lazy evaluation" (Lazy Evaluation)

Deferred execution is one specific example of Lazy Evaluation in .NET.

Lazy evaluation is when a result isn't calculated until it's actually needed. Besides LINQ, C# has other mechanisms for lazy evaluation too.

The Lazy<T> class

C# provides a special type Lazy<T> to create values "on demand".

Simple example:


// Create a lazy number that will only be calculated when we ask for it
var lazyValue = new Lazy<int>(() =>
{
    Console.WriteLine("Calculating value!");
    return 42;
});

Console.WriteLine("Lazy object created, but value not calculated yet");
Console.WriteLine($"Value: {lazyValue.Value}"); // calculation happens here

Why bother?
For example, you store a parameter that's rarely needed, and calculating it is slow or resource-intensive.

8. Table: LINQ methods and execution mode

Method Deferred execution Immediate execution
Where
✅ ❌
Select
✅ ❌
Take
✅ ❌
Skip
✅ ❌
Join
✅ ❌
ToList
❌ ✅
ToArray
❌ ✅
Count
❌ ✅
First
❌ ✅

9. Common mistakes and implementation quirks

"Extra work" from repeated iterations

Since deferred execution triggers new calculations every time you iterate, sometimes you can get extra recalculations.


var expensiveQuery = bigList.Where(x => SomeHeavyCalculation(x));

var result1 = expensiveQuery.ToList(); // calculated — once
var result2 = expensiveQuery.ToList(); // calculated — twice (same result, but wasting time again)

Fix it by materializing the data once:
var cached = expensiveQuery.ToList();

"Catch the error if you can"

If a function in your query throws an exception, it'll only happen when you iterate the collection, not when you define the query.

Modifying the collection while iterating

Changing the collection while you're iterating over it can throw an InvalidOperationException.

2
Task
C# SELF, level 34, lesson 0
Locked
Deferred Execution and Multiple Queries
Deferred Execution and Multiple Queries
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION