Lambda expressions and extension methods behind LINQ

Every query operator is an extension method taking a delegate, and knowing the difference between a delegate and an expression tree explains IQueryable.

Delegates and lambdas

// A lambda with an expression body compiles to a method plus a delegate
Func<int, bool> isEven = n => n % 2 == 0;

// A statement body is the same delegate with more lines
Func<int, bool> isBig = n =>
{
    var doubled = n * 2;
    return doubled > 100;
};

// Func<T, TResult> returns a value; Action<T> returns nothing
Action<string> log = message => Console.WriteLine(message);

// Predicate<T> is the old name for Func<T, bool>
Predicate<int> legacy = n => n > 0;

// The compiler infers the parameter types from the target type
var lengths = new[] { "a", "bb", "ccc" }.Select(s => s.Length);
// A closure captures the variable, not its value
var counter = 0;
Func<int> next = () => ++counter;      // counter lives on a compiler-generated class

Console.WriteLine(next());              // 1
Console.WriteLine(next());              // 2

// The classic loop-capture bug
var actions = new List<Action>();
for (var i = 0; i < 3; i++)
{
    actions.Add(() => Console.WriteLine(i));   // captures the variable i
}
foreach (var a in actions) a();                // prints 3, 3, 3

// Fixed by copying into a fresh variable per iteration
actions.Clear();
for (var i = 0; i < 3; i++)
{
    var captured = i;
    actions.Add(() => Console.WriteLine(captured));   // prints 0, 1, 2
}
  • foreach has had per-iteration capture semantics since C# 5, so the bug above only affects for loops and while loops.
  • A captured variable is hoisted into a generated class, which means a closure allocates. A lambda that captures nothing can be cached by the compiler as a static delegate.
  • Capturing this is easy to do accidentally inside a method, and it keeps the whole object alive for as long as the delegate lives.

Extension methods and iterators

// An extension method is a static method with a this parameter in a static class
public static class EnumerableExtensions
{
    public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source)
        where T : class
    {
        ArgumentNullException.ThrowIfNull(source);
        foreach (var item in source)
            if (item is not null) yield return item;
    }

    // Deferred: nothing runs until the result is enumerated
    public static IEnumerable<T> TakeEvery<T>(this IEnumerable<T> source, int n)
    {
        ArgumentNullException.ThrowIfNull(source);
        if (n <= 0) throw new ArgumentOutOfRangeException(nameof(n));

        var index = 0;
        foreach (var item in source)
            if (index++ % n == 0) yield return item;
    }

    // Eager: materialise deliberately when the source may change underneath you
    public static IReadOnlyList<T> ToStableList<T>(this IEnumerable<T> source) =>
        Array.AsReadOnly(source.ToArray());
}

// Compose operators; the query is a pipeline description, not a set of steps
var names = people
    .WhereNotNull()
    .Where(p => p.IsActive)
    .TakeEvery(2)
    .Select(p => p.Name)
    .ToStableList();
  • An iterator method does not execute until someone enumerates it. That is why a validation exception thrown inside one appears at the foreach, not at the call.
  • Argument validation in an iterator method runs late for the same reason. Extract the body into a non-iterator method that validates first, then returns the iterator.
  • yield return compiles into a state machine class, so an iterator allocates once per enumeration, which matters in a tight loop.
  • IEnumerable<T> can be enumerated more than once with different results; the type gives you no guarantee of stability.
💡
Deferred execution is the single most important behaviour to internalise. A query that looks like a value is actually a description of work, and it will re-run every time you enumerate it — including inside a logging statement or a debugger watch.

FAQ

Why does my iterator method not throw where I expected?
Because none of the body runs until the first MoveNext. Validate arguments in a wrapper method that is not itself an iterator, then return the iterator from it.
Should I use a lambda or a local method?
A local method when it does not capture anything, because it compiles to a static method with no delegate allocation. A lambda when you need to pass it as a delegate or read the call site inline.

Query syntax and method syntax Deferred execution and querying a database

Last refreshed 2026-09-18.