DEV Community

Nick
Nick

Posted on AI-assisted

The LINQ Query That Never Runs (Until You Force It)

The LINQ Query That Never Runs (Until You Force It)

You write a LINQ query. It compiles. It looks correct. But something is off — it executes at the weirdest times, or worse, executes multiple times when you expected once.

Welcome to deferred execution — LINQ's most misunderstood feature.

The Illusion of Immediate

Most developers assume this runs immediately:

var filtered = products.Where(p => p.Price > 100);
Enter fullscreen mode Exit fullscreen mode

It doesn't. Not a single item is touched. The filtered variable holds a recipe, not a meal. The actual filtering happens only when you iterate — foreach, ToList(), Count(), First().

Here's where it gets interesting: LINQ remembers the source, not the snapshot.

var numbers = new List<int> { 1, 2, 3 };
var doubled = numbers.Select(n => n * 2);

numbers.Add(4);  // Modify the source

foreach (var n in doubled)
    Console.WriteLine(n);  // Prints 2, 4, 6, 8 — includes the new element!
Enter fullscreen mode Exit fullscreen mode

The query re-evaluates against the current state of numbers. Every. Single. Time.

The Multiple Execution Trap

This pattern destroys performance in real codebases:

var expensive = GetProducts()
    .Where(p => p.Category == "Electronics")
    .Select(p => new { p.Name, p.Price });

// First enumeration — database hit
var count = expensive.Count();

// Second enumeration — ANOTHER database hit
var first = expensive.First();

// Third enumeration — you get the idea
foreach (var item in expensive) { }
Enter fullscreen mode Exit fullscreen mode

Three database calls. Same query. Because expensive is a recipe, not results.

Force Immediate Execution

The fix is simple — materialize when you need stable data:

var products = GetProducts()
    .Where(p => p.Category == "Electronics")
    .ToList();  // Execute NOW, store results

var count = products.Count;     // Just counts the list
var first = products.First();   // Just reads from memory
Enter fullscreen mode Exit fullscreen mode

ToList(), ToArray(), ToDictionary() — these force execution and store results.

Fun Fact

The term "deferred execution" comes from functional programming's "lazy evaluation" — a concept dating back to 1976 with SASL language. LINQ brought this academic concept into mainstream C# development in 2007, and developers have been confused by it ever since.

When Deferred Execution Shines

It's not a bug — it's a feature. Consider:

IEnumerable<Product> BuildQuery(bool includeInactive)
{
    var query = products.Where(p => p.Price > 0);

    if (!includeInactive)
        query = query.Where(p => p.IsActive);

    return query.OrderBy(p => p.Name);
}
Enter fullscreen mode Exit fullscreen mode

You can compose queries conditionally before execution. The database sees one optimized query, not three separate ones.

The Rule

  • Building a query? Let it stay deferred — compose freely.
  • Passing results around? Materialize with ToList().
  • Multiple enumerations? Materialize first.
  • Single enumeration? Deferred is fine.

Next time, we'll look at how IQueryable takes deferred execution even further — translating C# into SQL you never wrote. Hope to see you!

Top comments (0)