cswork / libraryOpen practice workspace

Articles

C# Async Stream Interview Questions: Cancellation, Disposal, and Deferred Work

Practice C# async stream interview questions with deferred execution, cooperative cancellation, awaited disposal, and explicit enumeration ownership tests.

Calling an async iterator method can return an enumerable without starting its network reads. The first await foreach advancement starts the deferred body. Breaking the loop then awaits disposal, but requesting cancellation does not stop a producer that never observes the token. These are separate lifecycle events, not one generic promise that an async stream manages everything automatically.

For C# async stream interview questions, trace construction, enumerator creation, advancement, cancellation, and disposal. An async iterator defers its body until enumeration advances. Cancellation must reach an operation that cooperates, while early loop exit has an asynchronous cleanup protocol. Neither feature proves that a pending remote operation was interrupted or rolled back.

These original exercises use Microsoft documentation, not reported employer prompts.

What work is deferred by an async iterator?

IAsyncEnumerable<T> represents asynchronous enumeration. In an async iterator method, yield return produces elements and await can suspend work between them. Microsoft's async-stream tutorial describes creating and consuming this pattern.

An original event source should distinguish the factory call from iterator-body execution. Calling ReadEvents may create an enumerable object without entering the body's first logging statement. Moving to the first element is the operation that begins the deferred body.

Do not generalize that every method returning IAsyncEnumerable must be entirely lazy. An ordinary wrapper can perform validation or side effects before returning another enumerable. State whether the method itself is an async iterator or merely returns an async-enumerable value.

That distinction determines where callers observe errors. A wrapper can reject an argument at the ordinary call site. A check inside an iterator body can fail during advancement instead. Both designs can be valid, but callers need the exception timing contract to place logging and cleanup correctly.

How do GetAsyncEnumerator and MoveNextAsync cooperate?

The enumerable supplies an enumerator through GetAsyncEnumerator. The enumerator exposes Current, MoveNextAsync, and asynchronous disposal. The GetAsyncEnumerator API includes the cancellation-token parameter used during enumerator creation.

MoveNextAsync indicates whether another element is available. A successful true result makes the current element available for consumption; false indicates completion. The consumer should not treat Current as an independent request for the producer to fetch another item.

In the original event-ledger exercise, record factory, acquire, advance, yield, body, and dispose events separately. A method that returns an enumerable and a consumer that advances its enumerator are different owners. This separation makes “nothing happened until I iterated” an expected observation rather than a mystery.

Avoid overlapping MoveNextAsync calls on one enumerator unless its specific contract explicitly supports that use. Ordinary consumption advances sequentially. Starting concurrent reads changes the protocol problem; an async return type alone does not make one mutable enumerator a safe parallel work queue.

Write a producer that observes cancellation

This original fixture uses a finite sequence and a cancellable delay rather than real network timing:

async IAsyncEnumerable<int> ReadEvents(
    [EnumeratorCancellation] CancellationToken token = default)
{
    events.Add("open");
    try
    {
        for (int i = 0; i < 3; i++)
        {
            token.ThrowIfCancellationRequested();
            await Task.Delay(10, token);
            events.Add($"yield:{i}");
            yield return i;
        }
    }
    finally
    {
        events.Add("close");
    }
}

The surrounding fixture supplies events and the usual namespaces. The delay is an illustrative scheduling point, not a performance target or a reliable race-control mechanism. A stronger automated cancellation test uses a controllable gate to decide exactly when advancement proceeds.

The token is checked explicitly and passed into the awaited operation. If the producer instead awaits a noncooperative operation, cancellation can remain requested while that operation continues. A caller must distinguish a requested token from observed cancellation and completed cleanup.

The finally block belongs to the entered iterator scope. Do not acquire an external handle elsewhere and assume this particular finally owns it before the body begins. Resource acquisition and release should have an explicit common owner.

Why does EnumeratorCancellation matter?

The EnumeratorCancellation attribute tells the compiler which async-iterator parameter should receive the token associated with enumeration. It connects the enumerator-level token with the producer's parameter under the generated iterator machinery.

Passing a token as an ordinary method argument and supplying a token through enumeration are distinct entry points. The attribute helps compiler-generated code account for them. Without the appropriate connection, a consumer can supply an enumeration token while the body continues using its original default parameter.

When both entry points supply different nondefault tokens, generated code can combine their cancellation conditions through a linked source. Do not promise that the producer always sees the exact same token identity the caller supplied. The useful contract is which cancellation requests can become observable.

Keep custom implementations separate from compiler-generated iterators. A hand-written IAsyncEnumerable owns its GetAsyncEnumerator implementation and must honor or explicitly document its token behavior. An attribute on an unrelated method does not repair arbitrary custom enumeration code.

What does WithCancellation do, and what does it not do?

The WithCancellation extension configures the token passed when enumeration obtains the enumerator. It does not inject cancellation checks into every producer statement or invent support in an underlying network client.

For the original fixture, consume ReadEvents().WithCancellation(token). Then compare it with a deliberately noncooperative producer that ignores the enumeration token. The consumer syntax can look correct in both cases while only one producer observes cancellation.

A useful test records requested, observed, and closed as separate events. Requesting cancellation is not the same event as throwing OperationCanceledException, and neither is automatically proof that a remote server abandoned its work. Establish integration behavior separately when the exercise includes an actual service.

Also decide whether cancellation is an expected user action or an error reported to operations. A canceled search request and an unexpected transport failure may need different reporting. Avoid classifying every canceled task as a generic “stream broken” event that triggers retries indefinitely.

What happens when await foreach breaks?

await foreach arranges disposal of its acquired enumerator when the loop exits according to its supported pattern. Breaking early does not request the rest of the elements simply to reach natural exhaustion. It leaves the enumeration scope and awaits its disposal operation.

For a compiler-generated async iterator already inside the try block, early disposal can execute its finally. If finally awaits cleanup, that cleanup contributes to the time required for the loop's exit to finish. The IAsyncDisposable API defines the asynchronous cleanup contract.

Test a consumer that reads one value and breaks. The ledger should show the entered resource scope, one accepted value, and cleanup, rather than all remaining yields. State whether acquisition occurred before predicting a close event.

A consumer-body exception also leaves the loop and invokes its cleanup path. If disposal itself fails, the observed exception may reflect cleanup behavior rather than only the original body error. A production adapter should preserve enough diagnostic context to explain both instead of assuming one exception always remains dominant.

How does manual enumeration change ownership?

Manual enumeration exposes the cleanup obligation directly. This original sketch places the enumerator in an asynchronous disposal scope:

await using var enumerator = source.GetAsyncEnumerator(token);
while (await enumerator.MoveNextAsync())
{
    if (ShouldStop(enumerator.Current)) break;
    Consume(enumerator.Current);
}

The containing method must support await. The await using scope owns disposal when control leaves that scope, including early exits. Simply calling MoveNextAsync once and abandoning the enumerator does not reproduce that ownership pattern.

Choose the stop boundary carefully. If the maximum count is checked only after requesting another element, the consumer may trigger one extra remote read even while returning the correct prefix. Assert advancement count as well as returned values in a bounded-consumer test.

Do not assume disposing an unadvanced compiler-generated iterator will run a body-level finally that was never entered. If an adapter acquires a resource eagerly during construction or GetAsyncEnumerator, its own cleanup contract must cover that acquisition. The body fixture and adapter ownership are separate mechanisms.

Can repeated enumeration repeat side effects?

An enumerable abstraction does not universally promise caching or replay. A compiler-generated async iterator can execute its body again for another enumeration, while a custom source may be one-shot or share an underlying cursor. The application must state which behavior it supports.

For an original paginated reader, enumerating twice may issue the requests twice. Saving the enumerable in a field does not necessarily save previously fetched results. If the application needs materialized replay, it must own storage and decide freshness, capacity, and failure policy.

Now add a consumer that starts a second traversal while the first is still suspended. Are resources independent, shared, or rejected by design? The interface name cannot answer that question. Record the source's ownership arrangement and test it directly.

Avoid marketing an async stream as automatically memory-free. The producer can prefetch, retain buffers, or collect a page before yielding. The consumer can also accumulate every result. Streaming syntax changes how values are delivered; memory behavior depends on the implementation and workload.

Build a deterministic lifecycle test

The original test matrix should cover no advancement, full exhaustion, one-value break, consumer-body exception, producer failure, cancellation before advancement, cancellation while awaiting, and disposal failure. Each case targets a different boundary and should have its own expected ledger.

Use a manually controlled completion gate instead of hoping a short sleep creates the desired interleaving. Pause the producer at a known await, request cancellation, release or cancel the gate according to its contract, and assert the exact observed events. A timeout protects the test runner; it is not the algorithm's correctness proof.

Compare cooperative and noncooperative producers honestly. For the latter, a cancellation request may remain pending until its underlying operation settles. Your test should make that limitation visible rather than hang indefinitely or claim that WithCancellation forcibly interrupts arbitrary work.

The C# snippets here are documentation-reviewed; no local .NET compiler is available and no executed async-stream result is claimed. Run the complete fixture in the intended SDK and language version, then retain outputs for each lifecycle case. No database driver, cloud request, or production cleanup behavior follows from the illustrative source alone.

Sources and Further Reading

Research note: Official documentation checked September 30, 2026. Original lifecycle expectations require the stated compiler-generated or custom-enumerator contract; cancellation is cooperative and production cleanup needs separate integration evidence.