Having a CancellationToken in scope does not make an await foreach loop cancellable. The loop does not look for a nearby token or use one from the surrounding method automatically.

This code accepts a token but never uses it:

public static async Task PrintUpdatesAsync(
    IAsyncEnumerable<string> updates,
    CancellationToken cancellationToken)
{
    await foreach (string update in updates)
    {
        Console.WriteLine(update);
    }
}

The enumeration starts without cancellationToken. If getting the next item blocks on a database query, network response, or model stream, cancelling the caller may not stop that work.

Attach the token to the enumeration

Use WithCancellation when you receive an IAsyncEnumerable<T> and need to supply the consumer’s token:

await foreach (string update in updates.WithCancellation(cancellationToken))
{
    Console.WriteLine(update);
}

WithCancellation passes the token to GetAsyncEnumerator(CancellationToken). It does not forcibly stop the stream. The enumerator still has to observe the token while producing the next item.

Make custom async iterators observe it

For an async iterator you own, mark the token parameter with [EnumeratorCancellation]:

using System.Runtime.CompilerServices;

public static async IAsyncEnumerable<string> ReadUpdatesAsync(
    [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
    while (true)
    {
        await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
        yield return DateTimeOffset.UtcNow.ToString("O");
    }
}

The attribute tells the compiler that this parameter should receive the token supplied through GetAsyncEnumerator. That includes a token attached with WithCancellation:

await foreach (string update in ReadUpdatesAsync()
    .WithCancellation(cancellationToken))
{
    Console.WriteLine(update);
}

Without [EnumeratorCancellation], a token supplied through WithCancellation does not become this parameter. The compiler reports CS8425 because the token passed to the generated GetAsyncEnumerator would otherwise be unconsumed.

Pass the token directly when the API supports it

Some APIs accept a cancellation token when they create the stream. Pass it there:

await foreach (var update in chatClient.GetStreamingResponseAsync(
    messages,
    cancellationToken: cancellationToken))
{
    Console.Write(update.Text);
}

In that case, adding WithCancellation as well is usually unnecessary. Follow the API’s contract and make sure one token reaches the work that produces each item.

This is easy to miss in code review. The CancellationToken sits a few lines above the loop, so the code looks wired for cancellation.

For every await foreach, identify how cancellation reaches GetAsyncEnumerator or the API that creates the stream. A token elsewhere in the method does nothing for the enumeration until you connect it.