Returning a large result set meant materialising the whole thing into a list, and the async equivalent of a generator did not exist until C# 8.
public async IAsyncEnumerable<Order> StreamAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
await using var reader = await _command.ExecuteReaderAsync(ct);
while (await reader.ReadAsync(ct))
{
yield return Map(reader);
}
}
// await foreach (var order in repo.StreamAsync(ct)) { ... }
This is the same idea as a PHP generator with the addition that each step can await, which is what a database reader needs. The EnumeratorCancellation attribute is required for the cancellation token to reach the enumerator and its absence is a silent no-op — the token is accepted and ignored. await foreach is the consumer syntax and it disposes the enumerator correctly, which a manual loop frequently does not.