Skip to content
LINQ Deep Dive: Deferred Execution
step 1/7

Reading — step 1 of 7

Learn

~3 min readGenerics, LINQ Internals, Pattern Matching

C# in Depth treats LINQ as one of the language's signature features. The basics (Where, Select, OrderBy) you know. This lesson covers what makes LINQ tick: deferred execution, iterator state machines, and expression trees.

Two LINQs: LINQ to Objects vs IQueryable

var nums = new[] { 1, 2, 3, 4, 5 };

// LINQ to Objects — runs in-memory
var evens = nums.Where(n => n % 2 == 0);

// LINQ to SQL / EF / etc — translates to SQL
var dbEvens = dbContext.Numbers.Where(n => n.Value % 2 == 0);

First returns IEnumerable<int>. Second returns IQueryable<Number> — the lambda is converted to an expression tree that the provider translates to SQL.

Deferred execution

LINQ operators are LAZY — they don't execute until enumerated:

var results = nums.Where(n => {
    Console.WriteLine($"checking {n}");
    return n > 2;
});

Console.WriteLine("before iteration");
foreach (var r in results) Console.WriteLine($"got {r}");

// Output:
// before iteration
// checking 1
// checking 2
// checking 3
// got 3
// checking 4
// got 4
// checking 5
// got 5

Where doesn't run when called. It runs when the foreach pulls values.

Implications:

  • Side effects in LINQ chains run during iteration, not call time
  • Iterating a query TWICE re-runs it twice (often a bug source)
  • Storing the query is cheap; iterating it is the cost

Materializing the result:

  • .ToList() — eager, returns List
  • .ToArray() — eager, returns array
  • .ToDictionary(keySelector) — eager, builds Dictionary
  • .First() / .Single() / .Count() — eager (terminal operators)

yield return — the iterator pattern

LINQ's lazy operators are implemented with yield:

public static IEnumerable<T> MyWhere<T>(IEnumerable<T> source, Func<T, bool> pred)
{
    foreach (var item in source)
        if (pred(item))
            yield return item;
}

The compiler turns this into a state-machine class implementing IEnumerator<T>. Each yield return is a suspension point.

Roll your own:

static IEnumerable<int> Fibonacci()
{
    int a = 0, b = 1;
    while (true)
    {
        yield return a;
        (a, b) = (b, a + b);
    }
}

fibonacci.Take(10).ToList();    // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Infinite sequences are fine — laziness means you only compute what you consume.

Expression trees

When you write a lambda for IQueryable, the compiler does NOT compile it to IL. It builds an Expression<Func<T, bool>> tree:

Expression<Func<int, bool>> expr = n => n > 5;
// At runtime: BinaryExpression(GreaterThan, ParameterExpression, ConstantExpression)

Providers (Entity Framework, LINQ to SQL, Cosmos DB SDK) walk this tree and emit a query in their native language.

Compiling at runtime:

Func<int, bool> compiled = expr.Compile();
compiled(7);    // true

Common LINQ patterns

// Group counting
var wordCounts = words.GroupBy(w => w).Select(g => new { Word = g.Key, Count = g.Count() });

// Join
var joined = users.Join(orders, u => u.Id, o => o.UserId, (u, o) => new { u.Name, o.Total });

// Aggregate (reduce)
var product = nums.Aggregate(1, (acc, n) => acc * n);

// SelectMany (flatMap)
var allTags = posts.SelectMany(p => p.Tags);

// Pagination
var page = items.Skip(offset).Take(pageSize);

// Distinct by key (manual since LINQ doesn't have DistinctBy until .NET 6)
var uniqueByName = items.GroupBy(i => i.Name).Select(g => g.First());

Common mistakes

  • Iterating a query multiple times — runs the work twice. ToList once, iterate the list.
  • Side effects in operators — execute lazily, surprises during debug.
  • Missing ConfigureAwait equivalent for IQueryable — use .AsAsyncEnumerable() or provider-specific async.
  • Trying to use unsupported expressions in IQueryable — provider can only translate what it knows. Custom methods may need to be fetched server-side via .AsEnumerable() then operated on in memory.
  • Forgetting null-handling in FirstOrDefault — for non-nullable structs, default is 0/false/etc.; for reference types it's null.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…

LINQ Deep Dive: Deferred Execution — C# Advanced