Codectionary / Developer documentation / C#

LINQ Deferred Execution

Most LINQ operators (Where, Select, OrderBy, etc.) use deferred execution - they don't actually run when you write the query, only when you enumerate the result, such as with a foreach loop or a call to ToList(). This means a deferred query re-runs every time you enumerate it, picking up any changes to the underlying data source since it was defined, which can be surprising if not understood.

Read the syntax

Most LINQ sequence operators build a query rather than executing it immediately. Enumeration, including foreach or ToList, evaluates the query.

A deferred query can observe later changes to its source. ToList or ToArray creates a snapshot at that moment; repeat enumeration of an unsnapshotted query can repeat work.

Syntax

var query = collection.Where(predicate);  // not executed yet

Examples

Queries Are Not Executed Immediately

Defining a query does not run it - only enumerating it does.

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

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

Console.WriteLine("Query defined, but not run yet");
var result = query.ToList();  // THIS is when "Checking..." actually prints
  1. Defining a query does not run it - only enumerating it does.
  2. Most LINQ sequence operators build a query rather than executing it immediately. Enumeration, including foreach or ToList, evaluates the query.
  3. Creating query prints nothing from its predicate. ToList later enumerates it, prints Checking for each source value and retains those greater than 1.

A Deferred Query Re-Reads the Source

Because execution is deferred, changes to the source between enumerations are picked up.

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

numbers.Add(4);
numbers.Add(6);

Console.WriteLine(string.Join(", ", evens));  // 2, 4, 6 - includes the new items!
  1. Because execution is deferred, changes to the source between enumerations are picked up.
  2. Most LINQ sequence operators build a query rather than executing it immediately. Enumeration, including foreach or ToList, evaluates the query.
  3. The Where query is still deferred when 4 and 6 are added. Join enumerates afterward, so it sees 2, 4 and 6.

Forcing Immediate Execution

Calling ToList(), ToArray(), or Count() forces the query to run right away and captures a snapshot.

List<int> numbers = new List<int> { 1, 2, 3 };
List<int> evensSnapshot = numbers.Where(n => n % 2 == 0).ToList();  // runs NOW

numbers.Add(4);

Console.WriteLine(string.Join(", ", evensSnapshot));  // 2 - unaffected by the later Add
  1. Calling ToList(), ToArray(), or Count() forces the query to run right away and captures a snapshot.
  2. Most LINQ sequence operators build a query rather than executing it immediately. Enumeration, including foreach or ToList, evaluates the query.
  3. ToList evaluates the query before 4 is added, capturing only 2. Printing the snapshot later still shows 2.

Best practices

  • Call .ToList() or .ToArray() when you need a stable snapshot of results that will not change if the underlying source is modified later
  • Be cautious about enumerating the same deferred query multiple times when the source (or the query itself) is expensive to compute - each enumeration re-runs the whole pipeline
  • Remember methods like Count(), Sum(), First(), and ToList() all trigger immediate execution of everything before them in the chain
  • Watch for 'multiple enumeration' bugs where a deferred query is enumerated more than once unintentionally (e.g. once in an if check, again in a loop) - materialize it first with .ToList() to avoid redundant work

Runtime & versions

.NET runtime. Feature availability depends on the language and runtime version used by your project; consult the official reference below.

C# language reference

Specifications & further reading