Async .NET · Lesson 25

Cancellation through async enumeration

Pass a consumer token into an async iterator and stop between elements.

How it works

An async iterator can produce values over time while allowing the consumer to await each next element. Cancellation needs an explicit path from the consumer to that iterator. WithCancellation supplies the consumer token, and EnumeratorCancellation identifies the parameter that receives the enumerator's cancellation request in this pattern.

The iterator checks the token at the start of each loop iteration. The consumer prints one and two, then requests cancellation. Before producing three, the next iteration observes the request and throws OperationCanceledException, which the surrounding handler reports as canceled. No assumption about timer duration is needed.

Cancellation is cooperative and occurs at the checks or token-aware awaited operations the implementation provides. If a long synchronous calculation runs between checks, the consumer cannot assume it stops immediately. Place checks at meaningful boundaries without claiming a token can forcibly interrupt arbitrary code.

Await foreach also owns the enumerator lifetime and arranges asynchronous disposal when enumeration ends. That is important when the iterator holds resources. Stopping early with break and canceling through an exception are different control paths, but both need the iterator's cleanup obligations to remain correct.

Practice by canceling before enumeration begins and verifying that no values are printed. Then replace cancellation after two with a simple break and compare the caller-visible outcome. Add a finally block inside the iterator that prints a cleanup marker, checking that both early-exit paths release the iterator's owned state.

Finally, distinguish an async stream from a preloaded collection wrapped in an async signature. A stream can reduce waiting and memory when values genuinely arrive incrementally, but it also extends the lifetime of the producing operation. Document cancellation, ordering, failure behavior, and whether partial results remain useful when enumeration does not finish normally.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

using var source = new CancellationTokenSource();
try {
    await foreach (int value in Numbers().WithCancellation(source.Token)) {
        Console.WriteLine(value);
        if (value == 2) source.Cancel();
    }
}
catch (OperationCanceledException) { Console.WriteLine("canceled"); }
static async IAsyncEnumerable<int> Numbers([EnumeratorCancellation] CancellationToken token = default) {
    for (int value = 1; value <= 4; value++) {
        token.ThrowIfCancellationRequested();
        await Task.Yield();
        yield return value;
    }
}

Expected output

1
2
canceled
Try this
EnumeratorCancellation connects the consumer token to the iterator parameter.