Deferred execution

2 min read
On trails:CSLINQstep 2/4

Deferred execution is one of the key characteristics of LINQLINQ: query syntax and method syntaxOne SQL-like way to query many kinds of data, written in two equivalent forms.Opens in a new tab. Queries are not executed where they are defined. Execution is deferred until the query variable is actually enumerated, for example in a foreach loop. It also happens when you call a conversion operator like ToList(), ToArray(), First() or Count().

Where it matters

Deferred execution lets you build complex queries dynamically by chaining operations. It also means the query works on the most current data at the time it runs.

It can be a pitfall if it isn’t well understood. Enumerating a LINQ query repeatedly makes it run again each time, and that gets expensive when the query involves significant computation or database access. To avoid it, when you need the results more than once, materialize the query into a collection (for example by calling .ToList()) right after defining it.

Which operators are which

Deferred (lazy) Immediate (eager)
Where ToList(), ToArray()
Select Count()
OrderBy First(), FirstOrDefault()
Take, Skip Sum(), Max(), Min(), Average()
GroupBy Any(), All()
If it returns IEnumerable<T>, it’s deferred. If it returns a concrete value or collection type (an int, a List<T>, a single item), it has already run.

The proof

Nothing runs at the Where line. The lambda only executes when the foreach starts pulling items, and by then it looks at the current state of numbers, which now includes 10. ToList() forces immediate execution: the query runs right then and locks in the results, so a later change to numbers doesn’t affect it. See ToList and what it returnsToList and what it returnsToList runs the query and returns a List, which offers more than IEnumerable.Opens in a new tab.

C#
List<int> numbers = new List<int> { 1, 2, 3 }; var query = numbers.Where(n => n > 1); // NOT executed yet, just stored numbers.Add(10); // the list changes before enumeration foreach (var n in query)    Console.WriteLine(n); // 2, 3, 10 // Now with ToList(), which runs right now and takes a snapshot:var snapshot = numbers.Where(n => n > 1).ToList(); numbers.Add(20); foreach (var n in snapshot)    Console.WriteLine(n); // 2, 3, 10 (no 20)

Running it twice

Enumerating the same deferred query twice runs it twice. If the source is expensive, like a database call, that’s wasteful. Call .ToList() once and reuse the result.

C#
var query = numbers.Where(n => n > 1); Console.WriteLine(query.Count()); // runs the filterConsole.WriteLine(query.Count()); // runs the filter AGAIN

Chains stay lazy

In a chain like .Where(...).Select(...), nothing has run until the end. Each item then flows through Where and then Select one at a time as the foreach pulls it. It is not “filter everything, then transform everything” in separate passes.


Sources