📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

A colleague's LINQ code behaves strangely: a filtered list seems to change after it was built, a database query throws ObjectDisposedException, logging shows the same query running three times, and an exception appears far from the line that caused it. Explain deferred execution and how you would debug and fix each symptom.

Deferred execution in practice: when queries run, captured variables, multiple enumeration, disposed contexts, late exceptions, and the tools and rules that prevent these bugs.

Mid-Level Debugging Intermediate csharp debugging deferred-execution ef-core linq

🎯 Key points

  • Deferred execution: a query is a recipe that runs each time it is enumerated, not when it is declared
  • Queries see the current state of the source and of captured variables at enumeration time
  • Enumerating an IQueryable after its DbContext is disposed throws ObjectDisposedException
  • Every enumeration repeats the work: multiple Count/foreach/Any calls mean multiple database round trips or recomputation (CA1851)
  • Exceptions inside lambdas surface at enumeration time, often far from the query definition
  • Fix by materialising once (ToList/ToArray) at a clear boundary, or by keeping the query alive only within the context's scope
  • The debugger's Results View enumerates the query, which can itself cause side effects

⏱ 30-second answer

Deferred execution means a LINQ query is a recipe that runs each time you enumerate it, not when you write it. That explains all four symptoms. The "changing" list is a query that re-reads its source and captured variables. The ObjectDisposedException comes from enumerating an EF query after the DbContext was disposed. Three log lines are three enumerations, and the late exception is a lambda failing during enumeration. I fix them by materialising once with ToList at a clear boundary inside the context's scope, and by turning on CA1851 for multiple enumeration.

🗣 Full answer (senior)

How I would answer this in an interview

All four symptoms come from deferred execution: a LINQ query is a recipe, not a result. It runs when it's enumerated, every time it's enumerated, against whatever state exists at that moment. I'd explain the concept and then debug each symptom.

1. Deferred execution in one paragraph

Where, Select, OrderBy etc. return an IEnumerable<T>/IQueryable<T> that describes the computation. Nothing happens until something enumerates it: foreach, ToList, Count, First, Any, serialisation, data binding, the debugger's "Results View". Each enumeration re-runs the whole pipeline from the source.

2. Symptom 1: "The filtered list changes after it was built"

var active = customers.Where(c => c.IsActive);   // not a list - a query
customers.Add(new Customer { IsActive = true });
Console.WriteLine(active.Count());              // includes the new customer

The variable holds a query over customers; enumerating later sees later changes. Same with captured variables:

var minTotal = 100m;
var big = orders.Where(o => o.Total > minTotal);
minTotal = 1000m;                                // query now uses 1000

Fix: materialise when you need a snapshot — var active = customers.Where(c => c.IsActive).ToList(); — and avoid mutating captured variables after building queries.

3. Symptom 2: ObjectDisposedException from a database query

public IEnumerable<Order> GetOrders(Guid customerId)
{
    using var db = _factory.CreateDbContext();
    return db.Orders.Where(o => o.CustomerId == customerId);   // returned unexecuted
}   // db disposed here

foreach (var o in GetOrders(id)) ...   // executes now -> ObjectDisposedException

The query executes after the DbContext was disposed. Same pattern with yield return inside a using, or returning IQueryable from a scoped repository and enumerating after the request scope ended.

Fix: materialise inside the scope: return await db.Orders.Where(...).ToListAsync(ct); and return IReadOnlyList<Order> (or DTOs).

4. Symptom 3: "The same query runs three times"

var pending = db.Orders.Where(o => o.Status == OrderStatus.Pending);
if (pending.Any())                       // query 1: SELECT CASE WHEN EXISTS...
{
    _logger.LogInformation("{Count} pending", pending.Count());   // query 2: SELECT COUNT(*)
    foreach (var o in pending) Process(o);                         // query 3: SELECT ...
}

Each terminal operation re-executes the query. With LINQ to Objects, it re-runs filters and projections (and any side effects); with EF Core, it hits the database each time — and results may differ between executions.

Fix: materialise once: var pending = await db.Orders.Where(...).ToListAsync(ct); if (pending.Count > 0) .... Use analyser CA1851 (possible multiple enumeration) and EF Core logging to detect it.

5. Symptom 4: "The exception appears far from the line that caused it"

var parsed = lines.Select(l => int.Parse(l.Split(',')[2]));   // line 10: no error yet
...
var total = parsed.Sum();                                      // line 80: FormatException here

The lambda runs during enumeration, so the stack trace points at Sum() (with the lambda frame inside) rather than where the query was defined. Similarly, iterator methods with argument validation inside the yield body throw only when enumerated.

Fix: materialise at the boundary where the data is produced (ToList() right after parsing), validate eagerly (separate argument checks from iterator bodies using local functions — as the BCL does), and handle bad data explicitly (TryParse).

6. How I debug these

  • Look at the declared type of the variable: IEnumerable/IQueryable means "query", List/array means "data".
  • Put a breakpoint or log inside the lambda to see when and how often it runs.
  • Be careful with the debugger's Results View — expanding it enumerates the query (and executes database queries).
  • Enable EF Core SQL logging to count executions.
  • Turn on CA1851 and review method signatures that return IEnumerable<T> from scoped resources.

7. A production example

A report service returned IEnumerable<InvoiceDto> built from an EF query inside a using block. It worked in unit tests (in-memory list) but threw ObjectDisposedException in production. After fixing that by moving ToList() inside, logs showed the report's main query executing three times (Any, Count, foreach) — tripling database load at month-end. Materialising once fixed both and cut report time by ~60%.

🔍 What the interviewer is checking

Checks practical mastery of deferred execution through realistic symptoms.

Green flags: maps each symptom to its cause; materialisation boundaries; DbContext lifetime; CA1851; side-effect-free lambdas; mentions Results View.

Red flags: "just add ToList everywhere" without reasoning; does not know queries re-run; blames EF Core for disposal bugs.

📚 Learn in depth

1. What a deferred query object holds

  • LINQ to Objects: an iterator object referencing the source sequence and the delegates (and their closures). Enumeration calls GetEnumerator() afresh each time, creating new iterator state and re-reading the source.
  • EF Core: an EntityQueryable<T> holding an expression tree and the provider (which references the DbContext). Enumeration triggers translation (cached) and a database round trip.

2. Which operations enumerate

Enumerates (executes) Doesn't enumerate
foreach, ToList, ToArray, ToDictionary, ToHashSet, ToLookup Where, Select, OrderBy, Skip, Take, Concat, Distinct, GroupBy*
Count, Any, All, Contains, First, Single, Last, ElementAt, Min, Max, Sum, Average, Aggregate AsEnumerable, AsQueryable, Cast, OfType
string.Join(", ", query), serialisation, data binding, List<T>(query) constructor Assigning to a variable

* GroupBy/OrderBy are deferred, but buffer the whole source on first enumeration.

3. Multiple enumeration hazards

  • Performance: repeated work or queries.
  • Correctness: results can differ between enumerations if the source changes or the query is non-deterministic (OrderBy(_ => Guid.NewGuid()), DateTime.Now in a lambda, database changes).
  • One-shot sources: some sequences can only be enumerated once (IAsyncEnumerable from a network stream, File.ReadLines re-opens the file each time, a Channel reader) — second enumeration fails or yields nothing.

4. Eager validation pattern

public static IEnumerable<T> TakeEvery<T>(this IEnumerable<T> source, int step)
{
    ArgumentNullException.ThrowIfNull(source);                 // runs at call time
    ArgumentOutOfRangeException.ThrowIfNegativeOrZero(step);
    return Impl(source, step);

    static IEnumerable<T> Impl(IEnumerable<T> s, int st) { int i = 0; foreach (var x in s) if (i++ % st == 0) yield return x; }
}

This is how the BCL's LINQ operators validate arguments immediately while executing lazily.

5. Detecting at scale

  • Analyser CA1851 flags possible multiple enumerations of IEnumerable<T> within a method.
  • Code review rule: methods that open resources (DbContext, file, HTTP) must not return unexecuted queries.
  • Integration tests with SQL logging/command counting (e.g. counting DbCommandInterceptor executions) catch query multiplication.

Try it yourself

  1. Build a query over a list, add an item, and enumerate.
  2. Return an EF query from a method with using var db and enumerate outside.
  3. Log inside a Select lambda and call Any, Count and foreach on the same query.
  4. Put int.Parse inside a Select, define the query, and enumerate it later; inspect the stack trace.

💻 Code

namespace Linq.Deferred;

public sealed record Customer(int Id, string Name, decimal Balance);

public static class Program
{
    public static void Main()
    {
        List<Customer> customers = [new(1, "Ava", 50m), new(2, "Noah", 500m)];
        var minimum = 100m;
        var evaluations = 0;

        IEnumerable<Customer> rich = customers.Where(c =>
        {
            evaluations++;               // side effect for demonstration only
            return c.Balance > minimum;
        });

        // Symptom 1: the "list" changes
        customers.Add(new Customer(3, "Mia", 900m));
        minimum = 40m;
        Console.WriteLine(string.Join(",", rich.Select(c => c.Name)));   // Ava, Noah, Mia

        // Symptom 3: multiple enumeration
        if (rich.Any())
        {
            Console.WriteLine(rich.Count());
            var list = rich.ToList();
        }
        Console.WriteLine($"Predicate ran {evaluations} times");

        // Fix: materialise once at a clear boundary
        evaluations = 0;
        IReadOnlyList<Customer> snapshot = customers.Where(c => c.Balance > minimum).ToList();
        if (snapshot.Count > 0) Console.WriteLine(snapshot.Count);

        // Symptom 4: exception appears at enumeration time
        IEnumerable<int> lengths = customers.Select(c => c.Name.Length / (c.Id - 2)); // divide by zero for Id 2
        try
        {
            foreach (var l in lengths) Console.WriteLine(l);
        }
        catch (DivideByZeroException)
        {
            Console.WriteLine("Thrown during enumeration, not at the Select line");
        }
    }
}

// Symptom 2 (EF Core), shown as a pattern:
// public IQueryable<Order> GetOrders() { using var db = _factory.CreateDbContext(); return db.Orders.Where(...); }  // BUG
// public async Task<IReadOnlyList<Order>> GetOrdersAsync(CancellationToken ct)
// {
//     await using var db = await _factory.CreateDbContextAsync(ct);
//     return await db.Orders.Where(...).AsNoTracking().ToListAsync(ct);                             // FIX
// }

⚠ Common mistakes

1. Returning unexecuted queries from scoped resources

What happens: methods return IEnumerable/IQueryable built over a disposed DbContext or closed file. Why it is wrong: ObjectDisposedException or reading closed resources. Do instead: materialise inside the scope and return concrete read-only results.

2. Enumerating the same query multiple times

What happens: Any(), Count(), then foreach on one query. Why it is wrong: repeated work or database queries, possibly inconsistent results. Do instead: materialise once.

3. Expecting snapshots

What happens: treating a query variable as a fixed list. Why it is wrong: later source changes and captured variable changes affect results. Do instead: ToList() when you need a snapshot.

4. Side effects inside lambdas

What happens: logging, counting or mutating state in Select/Where. Why it is wrong: runs per enumeration, possibly multiple times or partially. Do instead: keep lambdas pure.

5. Expanding Results View in the debugger on database queries

What happens: inspecting a query in the debugger. Why it is wrong: executes the query (and may change state with side-effecting lambdas). Do instead: inspect materialised variables or SQL via ToQueryString().

6. Lazy validation in iterator methods

What happens: argument checks inside a yield method body. Why it is wrong: errors surface far from the bad call. Do instead: validate eagerly and delegate to a local iterator function.

7. Over-materialising everything

What happens: ToList() after every operator "to be safe". Why it is wrong: wasted memory and CPU; for EF, breaks SQL translation by moving filters into memory. Do instead: materialise once at the boundary, after composing filters.

8. Using non-deterministic expressions in deferred queries

What happens: DateTime.Now or random values inside lambdas. Why it is wrong: each enumeration produces different results. Do instead: capture values into locals before building the query, then materialise.

🏛 Senior insight: trade-offs

Deferred execution is a feature: it lets you compose queries and lets EF Core push work into SQL. The senior skill is choosing a clear materialisation boundary, usually at the edge of the service or repository method, so that database work happens where you can see, time and trace it. I enable CA1851, keep IQueryable inside the data layer, and teach the team the four symptoms because they explain most "LINQ is weird" bug reports.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate says to call ToList at a boundary. Why: The fix is materialisation; tests choosing the right immediate operator.
    How to answer

    Any operator that returns a value or a concrete collection runs the query immediately: Count, Sum, First, Single, Any, Contains, and the conversions ToList, ToArray, ToDictionary, ToHashSet and ToLookup. I use Any rather than Count() > 0 because it stops at the first match, and Single only when more than one result really is a bug. For conversions I choose by what the consumer needs: a list or array to iterate, a dictionary or hash set for lookups, a lookup for one-to-many. With EF Core I always use the async versions with a cancellation token.

  2. When: Candidate mentions multiple enumeration or CA1851. Why: Checks what IEnumerable promises and what it hides.
    How to answer

    IEnumerable<T> is just a promise that you can get an enumerator. IEnumerator<T> gives MoveNext, Current and Dispose, and that is all: no count, no indexing, and possibly lazy, infinite or single-use. foreach is pattern-based and compiles to a while (MoveNext()) loop with Dispose in a finally. List<T> uses a struct enumerator, which gets boxed if you iterate through the interface. IAsyncEnumerable<T> is the async version, used with await foreach. I accept IEnumerable<T> when I only iterate once, and return IReadOnlyList<T> unless I mean to stream.

  3. When: Candidate explains the ObjectDisposedException. Why: Deferred EF queries depend on IQueryable and context lifetime.
    How to answer

    IQueryable<T> is an IEnumerable<T> that also carries an expression tree and a provider. Queryable.Where takes an Expression<Func<T,bool>> and just wraps the expression, and when you enumerate, the provider, for example EF Core, translates the whole tree, typically into SQL. IEnumerable operators take delegates and run in memory. Which one you get depends on the static type of the variable, so storing a DbSet in an IEnumerable variable quietly moves every later Where into memory. AsEnumerable() does that on purpose. I keep IQueryable inside the data layer and return materialised results.

🛠 Real-world problems: how you handle them

Intermittent ObjectDisposedException during JSON serialisation
Situation
A reporting endpoint in ASP.NET Core returned an IEnumerable<ReportRow> built with Select over an EF Core query created from an IDbContextFactory inside a using block.
Signals
Intermittent 500 errors with ObjectDisposedException: 'Cannot access a disposed context instance', stack trace inside System.Text.Json, not in the handler.
Investigation
Set a breakpoint inside the Select lambda and saw it hit during serialisation, after the handler returned and the context was disposed; confirmed with SQL logging that the query ran at serialisation time.
Resolution
Materialised with await ToListAsync inside the using block and returned IReadOnlyList<ReportRow>.
Prevention
Enabled CA1851, added an architecture test that service methods do not return IQueryable or IEnumerable from EF queries, and documented materialisation boundaries.

How to tell it (STAR)

**S:** A reporting endpoint failed intermittently with disposed-context errors. **T:** Find the cause. **A:** I showed with a breakpoint that the query only ran during JSON serialisation, after the context was gone, because of deferred execution. I materialised inside the scope and added an architecture test. **R:** The errors stopped and query timings became visible in our traces.

↩ Usually asked after

More in LINQ

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me