All Articles

LINQ - Lazy Evaluation vs Eager Execution

C# Linq

LINQ (Language-Integrated Query) is a powerful feature in C# that enables developers to query collections such as arrays and lists using a declarative, SQL-like syntax. It leverages lambda expressions and the extension methods provided by System.Linq namespace.

To effectively use LINQ, it’s important to understand two core execution models: Lazy Evaluation and Eager Execution.

Lazy Evaluation

Lazy Evaluation means that a query is not executed immediately. Instead, it builds a query pipeline that’s only evaluated when the data is explicitly requested.

For example, methods like .Where(), .Select(), or .Take() returns an IEnumerable<T> or IQueryable<T> representing the query definition, not its result. The actual execution only occurs when a materialization method is invoked, such as .ToList(), .ToArray(), or when iterating with foreach.

This code does not execute the query:

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var pairs = numbers.Where(n => n % 2 == 0);

What’s happening here?

When you write numbers.Where(n => n % 2 == 0), you are not filtering the list yet. You are creating a query definition, think of it as a recipe that says: “when someone asks for values, filter the even numbers from numbers“.

At this point:

  • No filtering has occurred
  • No iteration through the list has happned
  • The variable pairs holds an instruction, not actual data

The query will be executed on first time when have the iteration:

foreach(var num in pairs) 
{
  Console.WriteLine(num);
}

Output:

2
4

What’s happening now?

The foreach loop triggers the query execution. Now LINQ:

  1. Iterates through the original numbers list
  2. Applies the filter n % 2 == 0 to each element
  3. Returns only the values that match (2 and 4)

Let’s modify the list

numbers.Add(6)
foreach(var num in pairs) 
{
  Console.WriteLine(num);
}

Output:

2
4
6

What’s happening here?

This is the key insight into lazy evaluation:

  1. We added 6 to the original numbers list
  2. When we iterate through pairs again, the query re-executes.
  3. The query looks at the current state of numbers, which now contains {1, 2, 3, 4, 5, 6}
  4. It applies the same filter n % 2 == 0 again
  5. Now it finds three even numbers: 2, 4 and 6

As you can see, the query will be executed, and the new data showed. This demonstrates that pairs is not a snapshot of data from when it was created, it is a living query that re-evaluates against the source every time you iterate.

Changes to the original numbers list are reflected in subsequent iterations and each foreach loop causes a complete re-execution of the filtering logic.

Key Benefits:

  • Performance: Only process elements when needed, reducing unnecessary computation.
  • Composability: Enables building complex, reusable query pipelines with minimal overhead.

Eager Execution

Eager execution, on the other hand, executes the query immediately and stores the results in memory. This happens when you call methods like ToList(), .Count(), .First(), etc.

Once executed, the result is fixed, even if the underlying data source changes afterward.

This code executes the query immediately:

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var pairs = numbers.Where(n => n % 2 == 0).ToList();

What’s happening here? When you add .ToList() at the end of the query, you’re forcing immediate execution. The filtering happens right now, not later.

At this point:

  • The filtering has already occurred
  • The iteration through the list has already happened
  • The variable pairs holds actual data in memory, a List<int> containing { 2, 4 }
  • The data is materialized and stored independently from the source

The query was already executed, now we just iterate over the materialized data:

foreach (var num in pairs)
{
    Console.WriteLine(num);
}

Output:

2
4

What’s happening now?

The foreach loop does not trigger query execution. Instead:

  1. It simply iterates through the List<int> that already exists in memory
  2. No filtering occurs here, the work was done at line 2
  3. It reads the pre-calculated values (2 and 4)
  4. This is just a simple loop over a regular list

Let’s modify the list

numbers.Add(6)
foreach(var num in pairs) 
{
  Console.WriteLine(num);
}

Output:

2
4

What’s happening here?

This is the key difference from lazy evaluation:

  1. We added 6 to the original numbers list
  2. When we iterate through pairs again, NO query re-execution happens
  3. The variable pairs is a snapshot taken when .ToList() was called
  4. It contains a separate copy of the filtered data: { 2, 4 }
  5. Changes to numbers have no effect on pairs
  6. The output remains 2, 4, the 6 does NOT appear

As you can see, the query was executed only once, and changes to the source data are NOT reflected. Pairs is a fixed snapshot of data from when .ToList*() was called, is independent from the source numbers list. Changes the original numbers list are not reflected in pairs, not matter how many times you iterate, it always reads the same in memory data.

Key benefits:

  • Predictability: Captures the data at the moment of execution.
  • Efficiency in Reuse: Prevents repeated execution during multiple iterations.

When to use each approach

Use Lazy Evaluation when:

  • Working with streaming or large datasets.
  • You don’t need all the data immediately.
  • You want to chain multiple LINQ operations efficiently.
  • You aim to minimize memory usage.

Use Eager Execution when:

  • You need to iterate over the results multiple times.
  • The data source may become unavailable (e.g., after a DB connection closes).
  • You want to ensure the query is executed at a specific point in time.
  • You’re working with Entity Framework or external databases where deferred execution can cause side effects.

Understanding the execution model behind your LINQ queries is not an optimization detail; it’s critical for building efficient, maintainable, and correct code.

Published Nov 4, 2025

Software Engineer, AI Engineer and ML Engineer