← Open in the full interactive course (progress tracking, search & more)

Task<T> represents one value, not yet ready. IAsyncEnumerable<T> represents many values, arriving one at a time, over time.

Everything in this module so far has answered one question: how do I asynchronously get a single result? A customer record, a price, a combined array of pricing/inventory/reviews — always, in the end, one Task producing one value. But what happens when the thing you're producing isn't one value at all, but a whole sequence of them, and each one becomes available only after some asynchronous wait — a page of search results fetched from an API, then another page, then another; or a giant log file being read one line at a time?

You already know IEnumerable<T> from earlier in this course — the contract that lets you foreach over any sequence, one item at a time, via MoveNext(). In this lesson, you'll see exactly why that contract breaks down the moment producing the next item requires an asynchronous wait, and meet its async counterpart: IAsyncEnumerable<T>.

What Is It?

The Simple Explanation

An async stream is a sequence of values that arrive one at a time, where getting the next value might require waiting — for a network response, for more data to be read off disk, for another page of results to come back. IAsyncEnumerable<T> is C#'s type for exactly that: "a sequence of T, where asking for the next item is itself an asynchronous operation."

The Technical Definition

Recall IEnumerable<T>'s contract from earlier in this course:

public interface IEnumerable<T> { IEnumerator<T> GetEnumerator(); } public interface IEnumerator<T> { T Current { get; } bool MoveNext(); // SYNCHRONOUS — returns immediately, true or false }

IAsyncEnumerable<T> mirrors this shape almost exactly, with one deliberate, crucial change:

public interface IAsyncEnumerable<T> { IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default); } public interface IAsyncEnumerator<T> : IAsyncDisposable { T Current { get; } ValueTask<bool> MoveNextAsync(); // ASYNCHRONOUS — may need to wait before it knows }

The entire idea is captured in that one line: MoveNext() became MoveNextAsync(). Advancing to the next item is no longer guaranteed to be instant — it's an operation you await, exactly like any other asynchronous call in this module.

One change, same shape

IAsyncEnumerable<T> isn't a new idea bolted onto the language — it's the exact same "walk through items one at a time" contract you already know from IEnumerable<T>, with MoveNext() becoming awaitable. Everything you already understand about enumeration carries over directly.

(You'll also notice it returns ValueTask<bool> rather than Task<bool> — a performance-oriented Task variant whose full details belong to a later, more advanced lesson. For now, treat it exactly like an awaitable Task<bool>.)

Why Does It Exist?

The Problem — Neither Existing Tool Fits

Say you're building a method that streams pages of search results from an external API, one page at a time. You have two existing tools, and both are the wrong shape for this job:

IEnumerable<T> — synchronous only

Task<List<T>> — all-or-nothing

The Need

What's needed is a sequence type where producing each individual item can involve an asynchronous wait, but the caller can still process items as they arrive — starting work on the first item without needing to wait for the last one, and without ever blocking a thread while waiting for the next one to show up.

The Solution — IAsyncEnumerable<T> and await foreach

IAsyncEnumerable<T> fills exactly this gap. On the consuming side, C# gives you a matching keyword: await foreach, which loops over an async stream exactly like an ordinary foreach, except each step to the next item is awaited — freeing the thread while it waits, exactly like every other await in this module, then resuming with the next item once it's actually ready.

await foreach (SearchResult result in SearchProductsStreamAsync(query)) { Console.WriteLine(result.Name); // process each item as soon as it arrives }

The full mechanics of writing the producing side of this — with async IAsyncEnumerable<T> and yield return — are the next lesson's focus. This lesson's job is the concept and the shape of the problem it solves.

Big Picture

Shape of the problemThe right tool
One value, computed synchronouslyJust return T
One value, available only after waitingTask<T> — awaited once
Many values, all available synchronously, up frontIEnumerable<T>foreach
Many values, each one available only after waitingIAsyncEnumerable<T>await foreach

Notice the clean progression: going from "one value" to "many values" is the same jump as going from T to IEnumerable<T>. Going from "synchronous" to "asynchronous" is the same jump as going from a plain value to a Task. IAsyncEnumerable<T> is simply what you get when you make both jumps at once.

How It Works

WHAT await foreach REALLY DOES
You write this:
await foreach (var item in stream)
    Process(item);
▼ the compiler translates it into (conceptually) ▼
await using var e = stream.GetAsyncEnumerator();
while (await e.MoveNextAsync())
{
    var item = e.Current;
    Process(item);
}

Each await e.MoveNextAsync() is a genuine suspension point, exactly like any await you've used throughout this module — the thread is freed while the next item is being produced (waiting on a network call, more disk I/O, whatever the producer needs), and execution resumes with that item once it's ready. await using ensures the enumerator's async cleanup (DisposeAsync() — releasing a connection, closing a file) also happens properly and asynchronously when the loop ends, exactly the way a plain using works for synchronous disposal.

Simple Example

A before/after comparison that makes the difference concrete — fetching three pages of results, "all at once" versus "as an async stream":

BEFORE — Task<List<T>>

AFTER — IAsyncEnumerable<T>

// Consuming an async stream — the producing side is next lesson's topic public async Task PrintFirstMatchingResultAsync(string query) { await foreach (SearchResult result in SearchProductsStreamAsync(query)) { if (result.IsInStock) { Console.WriteLine($"Found: {result.Name}"); break; // stops early — later pages are never even fetched } } }

Walking through it:

Real-World Example

A background worker that processes rows streamed out of a large database export, without ever loading the whole export into memory:

public async Task ProcessOrderExportAsync(IAsyncEnumerable<OrderRecord> exportStream, CancellationToken ct) { int processedCount = 0; await foreach (OrderRecord order in exportStream.WithCancellation(ct)) { await _shippingQueue.EnqueueAsync(order, ct); processedCount++; if (processedCount % 1000 == 0) _logger.LogInformation("Processed {Count} orders so far...", processedCount); } _logger.LogInformation("Export processing complete: {Count} total orders.", processedCount); }

Whether the export holds a hundred orders or ten million, this method's memory footprint stays roughly constant — only the current OrderRecord needs to be in memory at any moment, exactly the same benefit IEnumerable<T> gives you for large synchronous sequences, now available even when producing each record genuinely requires waiting (e.g., reading the next chunk off disk or a paginated source). .WithCancellation(ct) threads a CancellationToken into the stream itself — the deep mechanics of this are covered fully in the next lesson.

Analogy

A Buffet vs. Courses Brought Out One at a Time

Task<List<T>> is like a buffet: the kitchen won't let you in until every single dish is fully prepared and laid out. You get everything at once, but you wait for the slowest dish before you can eat anything — even the dish that was ready twenty minutes ago.

IAsyncEnumerable<T> is like a proper multi-course meal: the kitchen brings out each course the moment it's ready, and you start eating the appetizer while the main course is still being prepared. If you decide you're full after the second course, the kitchen never bothers finishing the third — nothing wasted. That's exactly the "process items as they arrive, and stop early if you want to" benefit async streams give you over waiting for one giant, fully-loaded collection.

Under the Hood

At a high level, IAsyncEnumerable<T> combines two compiler transformations you've already met separately in this course: the iterator machinery behind yield return (which lets a method pause and resume, remembering where it left off, to produce one item at a time) and the async state machine behind async/await (which lets a method pause and resume around an asynchronous wait, freeing the thread in the meantime). A method written as async IAsyncEnumerable<T> gets both: it can pause to yield return a value and pause to await something, in any combination, and the compiler generates a single state machine capable of both kinds of suspension. The full mechanics of writing one of these methods yourself are exactly what the next lesson covers.

Common Confusion

1. "IAsyncEnumerable<T> means everything is loaded in the background while I do other stuff" — not automatically

An async stream doesn't pre-fetch every item ahead of time on its own. Each item is typically produced exactly when MoveNextAsync() is called for it — not before. This is precisely what allows the "stop early and skip the rest" benefit from the earlier example: nothing has been fetched yet for items you never asked for.

2. This isn't the same thing as Task.WhenAll or Task.WhenAny

Those two lessons were about coordinating several separate Tasks that were all started up front. Async streams are about a single ongoing sequence of values, produced incrementally over time — a fundamentally different shape of problem, even though both live under the "asynchronous programming" umbrella.

Common Mistakes

Mistake 1 — Reaching for Task<List<T>> out of habit when the sequence could be huge or unbounded

Buffering an entire large or open-ended sequence (a huge export, a live feed) into one List<T> before returning it — high memory use, and the caller waits for everything before seeing anything.

If the caller can meaningfully process items one at a time, and producing each one may involve waiting, IAsyncEnumerable<T> is usually the better fit.

Mistake 2 — Using a plain foreach on an IAsyncEnumerable<T>

This won't even compile — a plain foreach expects synchronous IEnumerable<T>/MoveNext(), and IAsyncEnumerable<T> only offers MoveNextAsync().

Use await foreach specifically — and make sure the enclosing method is itself async, since await foreach awaits internally.

When Should I Use It?

Paginated API results
Stream pages as they're fetched instead of loading them all first.
Large files, read incrementally
Process a huge file line by line without holding the whole thing in memory.
Not for a small, fixed, already-available list
If everything's ready synchronously and it's small, a plain List<T> is simpler.
Consumers that might stop early
Async streams avoid wasted work when a caller only needs the first few items.

Mental Model

IEnumerable<T> = "many values, each one ready instantly."
Task<T> = "one value, not ready yet — await it."
IAsyncEnumerable<T> = "many values, and getting each next one might mean waiting."

Remember:
· Same contract as IEnumerable<T>, with MoveNext() becoming an awaitable MoveNextAsync().
· Consume it with await foreach — never a plain foreach.
· Items are produced incrementally, on demand — stopping early skips the rest of the work.

Key Takeaway


Check Your Understanding

You've seen why a new sequence type was needed for asynchronously-produced items. Let's check the core distinction is clear.

1. What is the fundamental limitation of IEnumerable<T> that IAsyncEnumerable<T> was created to solve?

Show answer

Correct: B

Why B is correct: IEnumerator<T>.MoveNext() is a synchronous method — it must return true or false immediately. There's no way to await something inside it without blocking the thread, which is exactly the problem asynchronous programming exists to avoid.

Why A is incorrect: IEnumerable<T> can represent sequences of any size, including unbounded ones — size isn't the limitation here.

Why C is incorrect: IEnumerable<T> is precisely what powers plain foreach — the limitation is about asynchronous production of items, not about foreach support itself.

Why D is incorrect: IEnumerable<T> works with any type implementing it, not just arrays — that's the whole point of the interface.

Reinforcement: The synchronous nature of MoveNext() is exactly the gap IAsyncEnumerable<T> closes with its awaitable MoveNextAsync().

2. A method needs to stream a million rows from a database export, processing each one as it arrives, without holding all of them in memory at once. Which return type best fits this need?

Show answer

Correct: B

Why B is correct: IAsyncEnumerable<OrderRecord> lets each row be produced (potentially with an asynchronous wait) and consumed one at a time via await foreach, keeping memory use roughly constant regardless of how many rows there are.

Why A is incorrect: This would force loading all one million rows into memory before the caller can process even the first one — exactly the problem being avoided.

Why C is incorrect: A plain array has the same "everything must exist up front, all in memory" problem as the List<T> option, with no asynchrony at all.

Why D is incorrect: Task<OrderRecord> represents exactly one value, not a sequence of a million of them.

Reinforcement: IAsyncEnumerable<T> is specifically the tool for large or open-ended sequences produced incrementally.

3. What is the key difference between the plain foreach keyword and await foreach?

Show answer

Correct: B

Why B is correct: await foreach is specifically for IAsyncEnumerable<T>, where advancing to the next item is an asynchronous operation. It awaits MoveNextAsync() at each step, suspending the method and freeing the thread while the next item is being produced — exactly like any other await in this module.

Why A is incorrect: There's no inherent multithreading involved — the benefit is not blocking the thread while waiting, not parallel execution.

Why C is incorrect: await foreach works with any IAsyncEnumerable<T>, which arrays don't even implement.

Why D is incorrect: They target genuinely different interfaces (IEnumerable<T> vs IAsyncEnumerable<T>) with a real behavioral difference — synchronous vs. awaitable advancement.

Reinforcement: await foreach is the consuming-side counterpart to IAsyncEnumerable<T>, exactly as foreach is to IEnumerable<T>.

4. A caller uses await foreach over an async stream of search results and calls break after finding the first in-stock item, on page 2 of what could have been 10 pages. What happens to pages 3 through 10?

Show answer

Correct: B

Why B is correct: Async streams typically produce each item (or page) only when it's actually requested via MoveNextAsync — not ahead of time. Breaking out of the loop early means later items were never asked for, so that work is simply never done.

Why A is incorrect: Nothing is pre-fetched automatically in the background — that would defeat the memory and efficiency benefits of streaming in the first place.

Why C is incorrect: There's no such automatic caching behavior — a stream that's stopped early simply stops producing further items.

Why D is incorrect: Breaking out of an await foreach loop early is completely normal and doesn't throw any exception — the enumerator is disposed cleanly (via await using under the hood).

Reinforcement: On-demand production is one of the biggest practical advantages of async streams over pre-loading everything into a single collection.

You now understand what problem async streams solve and how they're consumed. Next up: writing the producing side yourself — async IAsyncEnumerable<T> and yield return, with real paginated-API and file-reading examples.


dotnetmadeeasy.com — Learn C# and .NET, the right way.