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

📚 Interview question

How does the using statement work in C#, and what are the common bugs with using declarations, deferred execution and exceptions?

using statement versus declaration, lowering to try/finally, disposal order, pattern-based using for ref structs, and bugs with returned objects, deferred LINQ and Dispose exceptions.

Mid-Level CodeReview Intermediate code-review idisposable resource-management using

🎯 Key points

  • using statements and declarations compile to try/finally calling Dispose
  • A using declaration disposes at the end of the enclosing scope; multiple are disposed in reverse order
  • Ref structs can use pattern-based Dispose
  • Do not return or capture objects that the using disposes
  • Materialise lazy queries before the resource is disposed
  • Keep scopes narrow for pooled resources
  • Use await using for IAsyncDisposable types

⏱ 30-second answer

using (var x = ...) { } and the C# 8 using declaration using var x = ...; both compile to try { ... } finally { x?.Dispose(); }. A declaration disposes at the end of the enclosing scope; several declarations are disposed in reverse order. For ref structs, using works with a pattern-based Dispose() method. Common bugs: returning or capturing an object that the using disposes, returning a lazy IEnumerable (LINQ or yield) built on a disposed resource, holding a resource longer than needed because a using declaration lives until the end of a long method, and an exception thrown by Dispose masking the original exception.

🗣 Full answer (senior)

How I would answer this in an interview

"using is just compiler sugar for try/finally that calls Dispose. The classic using (var x = ...) { } statement disposes at the end of its block; the C# 8 using var x = ...; declaration disposes at the end of the enclosing scope, and multiple declarations are disposed in reverse order. It works for IDisposable, for ref structs with a public Dispose() method, and await using handles IAsyncDisposable. The bugs I look for in code review are all lifetime bugs: returning or capturing something the using will dispose, returning a lazy query (LINQ, yield, unbuffered Dapper, IQueryable) built on a resource that is disposed before anyone enumerates it, a using var that keeps a pooled connection open through slow unrelated work, and a Dispose that throws and masks the original exception."


1. What the compiler generates

using var conn = new SqlConnection(cs);
await conn.OpenAsync();
return await conn.QuerySingleAsync<int>("select 1");

becomes, conceptually:

SqlConnection conn = new SqlConnection(cs);
try
{
    await conn.OpenAsync();
    return await conn.QuerySingleAsync<int>("select 1");
}
finally
{
    if (conn != null) ((IDisposable)conn).Dispose();
}
Detail Behaviour
null resource Allowed; nothing is disposed
Value types Dispose called without boxing
The variable Read-only: you can't reassign it inside the scope
ref struct Pattern-based: any accessible void Dispose() works (C# 8+)
await using Lowers to try/finally with await x.DisposeAsync()

2. Statement vs declaration

Form Disposed at Best for
using (var x = ...) { ... } End of the block A short, explicit lifetime inside a longer method
using var x = ...; End of the enclosing scope (method, loop body, block) A resource used until the method ends
Several using var Reverse declaration order Dependent resources (writer over stream)

3. The common bugs

# Bug What goes wrong Fix
1 Returning the resource (using var s = File.OpenRead(p); return s;) Caller gets a closed object Don't using what you return; the caller owns it
2 Returning a lazy sequence (return reader.Lines().Where(...)) Enumerated after disposal → ObjectDisposedException / closed connection Materialise inside the scope, or make the whole method an iterator that owns the resource
3 Returning IQueryable from a disposed DbContext Query executes after the context is disposed Execute (ToListAsync) inside the scope
4 Long-lived using var at the top of a long method Pooled connection/file held during unrelated slow work → pool exhaustion using block with a narrow scope
5 Exception masking Dispose throws in finally and replaces the real exception Dispose must not throw; provide explicit Flush/Commit
6 Plain using on async-capable types Sync disposal may block on I/O or skip async flush; IAsyncDisposable-only types won't compile await using in async code
7 Fire-and-forget inside the scope Background task uses the resource after disposal Await the task before the scope ends, or give it its own resource
8 Captured in a lambda run later (event handler, timer, Task.Run) Closure uses a disposed object Don't capture scoped resources in deferred callbacks

4. Code review checklist

  • Is anything returned from inside a using scope? Is it still valid after disposal?
  • Is a returned IEnumerable/IQueryable/IAsyncEnumerable materialised or owning?
  • Is a using var holding a pooled resource longer than needed?
  • Are tasks started inside the scope awaited before it ends?
  • Does the type implement IAsyncDisposable? Is await using used?
  • Can Dispose throw?

Production example

In our [orders service], a repository method opened using var connection and returned connection.Query<Order>(sql, buffered: false). The unbuffered Dapper query was enumerated by the caller after the method returned, so the connection was already disposed. Most requests happened to work during local testing because of timing and small results, but production logged intermittent InvalidOperationException: The connection is closed. I fixed the common path by materialising with .ToList() inside the scope. For the large export path I changed the API to an async iterator that owns the connection (await using inside an async IAsyncEnumerable<Order> method), so the connection lives exactly as long as the enumeration. I also added the pattern to our code review checklist.

🔍 What the interviewer is checking

The interviewer checks practical understanding of resource lifetimes in C#.

Green flags: lowering to try/finally, statement versus declaration scope, reverse disposal order, deferred execution bug, exception masking, await using, pattern-based using for ref structs.

Red flags: thinks using is only about memory; returns resources from using blocks; unaware of deferred execution issues.

📚 Learn in depth

1. Iterators own their using correctly

// BUG: the reader is disposed when this method returns, before enumeration
static IEnumerable<string> PaidLazy(string path)
{
    using var reader = new StreamReader(path);
    return ReadLines(reader).Where(l => l.EndsWith(",Paid"));
}

// OK: the whole method is an iterator, so the using lives inside the state machine
static IEnumerable<string> PaidStreaming(string path)
{
    using var reader = new StreamReader(path);
    while (reader.ReadLine() is { } line)
        if (line.EndsWith(",Paid")) yield return line;
}

In the iterator version, the compiler moves the using into the generated enumerator. The file opens on the first MoveNext() and is disposed when enumeration completes or when the enumerator is disposed (which foreach and LINQ operators like First() do). Two consequences:

  • If nobody enumerates, the file is never opened.
  • If someone calls GetEnumerator()/MoveNext() manually and never disposes the enumerator, the file leaks until finalisation.

The async version works the same way:

public async IAsyncEnumerable<Order> StreamOrdersAsync([EnumeratorCancellation] CancellationToken ct = default)
{
    await using var conn = new SqlConnection(_cs);
    await conn.OpenAsync(ct);
    await foreach (var o in conn.QueryUnbufferedAsync<Order>("select * from Orders").WithCancellation(ct))
        yield return o;
}

2. Exception masking in detail

using (var tx = new ThrowingOnDispose())
{
    throw new InvalidOperationException("real problem");
}
// What propagates: the exception thrown by Dispose. "real problem" is lost.

In C#, an exception thrown from a finally block replaces the one in flight. That's why Dispose should not throw, and why APIs like transactions expose an explicit Commit()/CompleteAsync() that can fail loudly, while Dispose just rolls back quietly.

3. Disposal order and dependent resources

using var file = File.Create("out.gz");
using var zip = new GZipStream(file, CompressionLevel.Optimal);
using var writer = new StreamWriter(zip);
writer.Write("data");
// Disposed: writer (flush) → zip (write footer) → file (close)

If the order were reversed, the zip footer would be written to a closed file and the archive would be corrupt.

4. await using and ConfigureAwait

ConfigureAwait(false) on an IAsyncDisposable returns a ConfiguredAsyncDisposable wrapper, which await using accepts. Because the wrapper is not the resource, declare the resource first:

var stream = File.OpenWrite(path);
await using (stream.ConfigureAwait(false))
{
    await stream.WriteAsync(data).ConfigureAwait(false);
}

// Declaration form:
var file = File.OpenWrite(path);
await using var _ = file.ConfigureAwait(false);
await file.WriteAsync(data).ConfigureAwait(false);

Writing await using (var s = stream.ConfigureAwait(false)) compiles, but s is the wrapper, not the stream. This matters mainly in library code; ASP.NET Core has no synchronisation context.

5. Pattern-based disposal for ref struct

Before C# 13, ref struct types couldn't implement interfaces, so C# 8 allowed using with any accessible void Dispose() on a ref struct. This is used by stack-based builders that rent buffers:

ref struct PooledBuffer
{
    private byte[]? _array;
    public PooledBuffer(int size) => _array = ArrayPool<byte>.Shared.Rent(size);
    public Span<byte> Span => _array;
    public void Dispose() { if (_array is { } a) { _array = null; ArrayPool<byte>.Shared.Return(a); } }
}

using var buf = new PooledBuffer(4096);

6. Pool-friendly scoping

int count;
using (var conn = await _factory.OpenAsync(ct))
{
    count = await conn.ExecuteScalarAsync<int>("select count(*) from Orders");
} // connection returned to the pool here

await _emailClient.SendReportAsync(count, ct); // slow, no connection held

With using var at the top, the connection would be held during the email call; under load that can exhaust the pool (Max Pool Size) and cause timeouts.

7. Version notes

Version Change
C# 1 using statement
C# 8 using declarations, await using, pattern-based Dispose for ref struct
.NET Core 3.0 IAsyncDisposable, IAsyncEnumerable<T>
C# 13 ref struct can implement interfaces (pattern-based disposal still works)
.NET 10 / C# 14 No changes to using semantics

8. Try it yourself

  1. Run the code example: the lazy version throws ObjectDisposedException; the materialised version works.
  2. Rewrite PaidOrdersLazy as an iterator (yield return) and confirm it works and disposes when the foreach ends.
  3. Write a type whose Dispose throws and put it in a using around code that throws; observe which exception you catch.
  4. Reverse the order of GZipStream/FileStream disposal manually and try to open the resulting .gz.
  5. Set Max Pool Size=5 on a SQL connection string, use using var at the top of a method with Task.Delay(2000) after the query, and run 20 concurrent calls; then narrow the scope and compare.
  6. Call GetEnumerator()/MoveNext() on the iterator version without disposing and check whether the file stays locked.

💻 Code

File.WriteAllLines("orders.txt", ["1,Paid", "2,Pending", "3,Paid"]);

try
{
    foreach (string line in PaidOrdersLazy("orders.txt")) Console.WriteLine(line);
}
catch (ObjectDisposedException ex)
{
    Console.WriteLine($"Bug: {ex.GetType().Name} - the reader was disposed before enumeration");
}

foreach (string line in PaidOrders("orders.txt")) Console.WriteLine($"Fixed: {line}");

// BUG: query is deferred; the reader is disposed when the method returns
static IEnumerable<string> PaidOrdersLazy(string path)
{
    using var reader = new StreamReader(path);
    return ReadLines(reader).Where(l => l.EndsWith(",Paid"));
}

// FIX: materialise inside the scope (or make the whole method an iterator)
static List<string> PaidOrders(string path)
{
    using var reader = new StreamReader(path);
    return ReadLines(reader).Where(l => l.EndsWith(",Paid")).ToList();
}

static IEnumerable<string> ReadLines(StreamReader reader)
{
    while (reader.ReadLine() is { } line) yield return line;
}

// Narrow scope: release the pooled resource before slow work
static async Task<int> CountThenProcessAsync(string path)
{
    int count;
    using (var reader = new StreamReader(path))
    {
        count = 0;
        while (await reader.ReadLineAsync() is not null) count++;
    }                                   // file closed here
    await Task.Delay(100);              // slow work without holding the file open
    return count;
}

⚠ Common mistakes

1. Returning a lazy query built inside using

What happens: return reader.Lines().Where(...), unbuffered Dapper, or IQueryable from a disposed DbContext. Why it is wrong: enumeration happens after disposal → ObjectDisposedException or closed-connection errors, often intermittent. Do instead: materialise inside the scope, or make the method an iterator that owns the resource.

2. Returning the disposed resource itself

What happens: using var stream = File.OpenRead(p); return stream;. Why it is wrong: the caller receives a closed stream. Do instead: don't using what you return; document that the caller owns it.

3. using var at the top of long methods for pooled resources

What happens: the DB connection stays open during HTTP calls, sleeps or CPU work. Why it is wrong: pool exhaustion and timeouts under load. Do instead: narrow using blocks around the actual use.

4. Plain using on async-disposable types in async code

What happens: using var writer = ... for a type with async flush. Why it is wrong: sync disposal may block a thread on I/O or skip async work. Do instead: await using.

5. A Dispose that throws

What happens: Dispose performs a flush that can fail. Why it is wrong: it masks the original exception from the using body. Do instead: keep Dispose non-throwing; add an explicit Flush/Complete call.

6. Fire-and-forget work inside the scope

What happens: _ = ProcessAsync(stream); then the using ends. Why it is wrong: the task uses a disposed resource. Do instead: await before the scope ends, or give the task its own resource.

7. Capturing the resource in a deferred callback

What happens: a timer, event handler or Task.Run lambda captures a using variable. Why it is wrong: the callback runs after disposal. Do instead: don't capture scoped resources in callbacks that outlive the scope.

8. Wrong disposal order with manual Dispose calls

What happens: the stream is disposed before the writer/compressor over it. Why it is wrong: buffered data or footers are lost; files are corrupt. Do instead: let nested using declarations dispose in reverse order.

9. Thinking using frees memory

What happens: using is added around plain managed objects "to free memory". Why it is wrong: Dispose releases resources, not managed memory; the GC still reclaims memory. Do instead: use using for resources; reduce allocations for memory.

10. Manually enumerating an iterator without disposing it

What happens: var e = seq.GetEnumerator(); e.MoveNext(); and then abandon it. Why it is wrong: the iterator's using/finally never runs, leaking the resource. Do instead: use foreach, or wrap manual enumeration in using var e = ....

🏛 Senior insight: trade-offs

using is simple syntax with lifetime consequences that show up in code review.

Code smell Likely incident
Lazy enumerable returned from using Intermittent disposed-object errors
Long-lived using declaration Connection pool exhaustion under load
Background task using a scoped resource Errors after the request ends
Dispose that can throw Lost root-cause exceptions in logs

Seniors review the lifetime, not just the presence, of using.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate mentions deferred execution bugs Why: Checks IEnumerable and deferred execution
    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.

  2. When: Candidate mentions await using Why: Checks async disposal
    How to answer

    Implement IAsyncDisposable when cleanup involves asynchronous I/O, such as flushing a buffered stream, sending a final message or closing a network connection gracefully. DisposeAsync() returns ValueTask. await using var x = ...; compiles to try/finally with await x.DisposeAsync(), and you can write await using (x.ConfigureAwait(false)) in library code. A type that supports both should implement IDisposable and IAsyncDisposable, put async cleanup in protected virtual ValueTask DisposeAsyncCore(), and call GC.SuppressFinalize. In DI, use CreateAsyncScope(): disposing a provider or scope synchronously throws when it holds a service that implements only IAsyncDisposable.

  3. When: Candidate says Dispose should not throw Why: Checks IDisposable implementation
    How to answer

    Implement IDisposable when your type owns something that must be released deterministically: an unmanaged resource (usually via SafeHandle) or another IDisposable. For a sealed class, a simple Dispose() that disposes owned fields is enough. For unsealed classes use the pattern Dispose() calling protected virtual Dispose(bool disposing) and GC.SuppressFinalize(this). Make Dispose idempotent and non-throwing, and throw ObjectDisposedException (ObjectDisposedException.ThrowIf) from members used after disposal. The owner disposes: whoever creates an object disposes it, unless ownership is explicitly transferred. The DI container disposes what it creates, at the end of the scope or the app.

🛠 Real-world problems: how you handle them

Unbuffered query enumerated after its connection was disposed
Situation
A repository method returned an unbuffered Dapper query from inside a `using var connection`.
Signals
Intermittent `InvalidOperationException: Invalid operation. The connection is closed.` in production logs.
Investigation
Code review showed enumeration happened in the caller after the method returned and disposed the connection.
Resolution
Materialised results with `.ToList()` inside the scope; for large results, changed the API to stream with `` `IAsyncEnumerable<T>` `` and `await using` inside an async iterator.
Prevention
Added a code review checklist item and an analyser rule flagging `buffered: false` returned from methods.

How to tell it (STAR)

We had intermittent closed-connection errors. I found a repository returning a lazy query from inside a using scope, so the caller enumerated after disposal. Materialising inside the scope fixed it, and for big exports I switched to an async iterator that owns the connection.

↩ Usually asked after

More in Garbage Collection

Last updated 31 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me