Task<List<T>> gom toàn bộ kết quả vào bộ nhớ rồi mới trả một lần. IAsyncEnumerable<T> là async stream: mỗi phần tử được trả ra ngay khi sẵn sàng, consumer xử lý dần bằng await foreach.
public async IAsyncEnumerable<Order> StreamAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
await foreach (var row in _db.Orders.AsAsyncEnumerable().WithCancellation(ct))
{
yield return row;
}
}
await foreach (var order in StreamAsync().WithCancellation(token))
{
await ExportAsync(order);
}Dùng khi tập dữ liệu lớn hoặc không biết trước độ dài: export báo cáo, đọc file log, đọc từng dòng từ DB — bộ nhớ giữ ở mức hằng số thay vì tăng theo số dòng. Trong ASP.NET Core, action trả IAsyncEnumerable<T> được serialize theo kiểu streaming nên client nhận byte đầu tiên sớm hơn.
[EnumeratorCancellation] giải quyết một vấn đề cụ thể: consumer huỷ bằng .WithCancellation(token), mà token đó được đưa vào GetAsyncEnumerator(CancellationToken) chứ không phải vào tham số method. Đánh dấu attribute này lên tham số CancellationToken là bảo compiler nối token của consumer vào tham số đó. Thiếu nó, ct trong thân method luôn là default và việc huỷ không có tác dụng — bug im lặng, code vẫn biên dịch bình thường.
Hạn chế cần biết: không dùng được cho endpoint cần trả HTTP status lỗi giữa chừng, vì header đã gửi đi rồi.