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 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:
The query will be executed on first time when have the iteration:
foreach(var num in pairs)
{
Console.WriteLine(num);
}Output:
2
4What’s happening now?
The foreach loop triggers the query execution. Now LINQ:
Let’s modify the list
numbers.Add(6)foreach(var num in pairs)
{
Console.WriteLine(num);
}Output:
2
4
6What’s happening here?
This is the key insight into lazy evaluation:
numbers listnumbers, which now contains {1, 2, 3, 4, 5, 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:
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:
List<int> containing { 2, 4 }The query was already executed, now we just iterate over the materialized data:
foreach (var num in pairs)
{
Console.WriteLine(num);
}Output:
2
4What’s happening now?
The foreach loop does not trigger query execution. Instead:
List<int> that already exists in memoryLet’s modify the list
numbers.Add(6)foreach(var num in pairs)
{
Console.WriteLine(num);
}Output:
2
4What’s happening here?
This is the key difference from lazy evaluation:
numbers list.ToList() was called{ 2, 4 }numbers have no effect on pairs2, 4, the 6 does NOT appearAs 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.
Understanding the execution model behind your LINQ queries is not an optimization detail; it’s critical for building efficient, maintainable, and correct code.