With the release of .NET 8, one killer feature will immediately increase the responsiveness of your APIs and Blazor applications: the ability to stream responses. Yes, you could stream responses before, but it’s never been easier with support for IAsyncEnumerable on API endpoints and the use of StreamRenderingAttribute on your Blazor United applications.

This post will explore why you want to use IAsyncEnumerable and StreamRendering to get the fastest Time to first byte (TTFB) out of your ASP.NET Core web applications. Let’s get started.

What is IAsyncEnumerable?

The interface IAsyncEnumerable<T> is a newish interface designed with the idea that retrieving each element within the iteration is an asynchronous task. This is different than a typical Task<IEnumerable> as the operation to retrieve the enumerable is considered one step. If you’ve used any data-access layer, you’ve likely invoked methods like ToListAsync or ToArrayAsync, invoking a request to your database and materializing the result in a single operation. Thinking about the same operation with IAsyncEnumerable, the process will first execute the query and then materialize each record as you enumerate through the collection. This allows you to start using data as it is received rather than waiting for data to buffer in memory. This leads to more efficient use of resources and faster response times.

Let’s take a look at a simple example.

await foreach (var number in RangeAsync(1, 100))
{
    Console.WriteLine(number);
}

static async IAsyncEnumerable<Number> RangeAsync(int start, int count)
{
    for (int i = 0; i < count; i++)
    {
        await Task.Delay(i);
        yield return new Number(start + i);
    }
}

record Number(int Value);

You’ll notice that IAsyncEnumerable also has C# syntax support with the availability of await foreach. That makes it straightforward to consume in an existing codebase.

What is Stream Rendering?

While recently added to Blazor, stream rendering is not a new concept. Most browser clients support a Transfer-Encoding of chunked. We can get an idea of what this means to web developers from Wikipedia.

Chunked transfer encoding is a streaming data transfer mechanism available in Hypertext Transfer Protocol version 1.1, defined in RFC 9112 §7.1. In chunked transfer encoding, the data stream is divided into a series of non-overlapping “ chunks”. The chunks are sent out and received independently of one another.

Cool! As application developers, we can take advantage of this for both API endpoints and, in the case of Blazor, for streaming HTML markup from our component-based pages.

Let’s look at streaming from a Minimal API endpoint.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapGet("/range", () => 
    Results.Ok(new
    {
        totalCount = 100,
        results = RangeAsync(1, 100)
    }));

app.Run();

static async IAsyncEnumerable<Number> RangeAsync(int start, int count)
{
    for (int i = 0; i < count; i++)
    {
        await Task.Delay(i);
        yield return new Number(start + i);
    }
}

record Number(int Value);

Running this sample in a browser, you’ll see the results get output to the page, even before getting a semantically complete JSON object.

It’s important to understand that a client must understand and support chunking to take advantage of the performance benefits.

Stream Rendering and IAsyncEnumerable for Blazor

While Blazor supports stream rendering for HTML elements, you need to consider how you’ll be invoking an IAsyncEnumerable within a component’s lifecycle. While you may be tempted to do the following in your Blazor components, it will result in an error.

@* ERROR: The 'await foreach' statement can only be used in a method or lambda marked with the 'async' modifier *@
@await foreach (var forecast in forecasts)
{
	<tr>
		<td>@forecast.Date.ToShortDateString()</td>
		<td>@forecast.TemperatureC</td>
		<td>@forecast.TemperatureF</td>
		<td>@forecast.Summary</td>
	</tr>
}

The correct approach to using IAsyncEnumerable within a Blazor component comes down to three essential considerations:

  1. A collection variable must first exist, as you’ll be appending to it, not replacing it altogether.
  2. The IAsyncEnumerable must be awaited within an async component lifecycle method such as OnInitializedAsync.
  3. The method StateHasChanged must be invoked to tell Blazor to flush HTML to the response. This can be after each iteration or on some determined interval.

Let’s see what that looks like in a sample. I’ve tweaked the Weather.razor sample that comes with the Blazor template.

@page "/weather"
@attribute [StreamRendering]

<PageTitle>Weather</PageTitle>

<h1>Weather</h1>

<p>This component demonstrates showing data.</p>

@if (forecasts is { Count : 0 })
{
    <p>
        <em>Loading...</em>
    </p>
}
else
{
    <table class="table">
        <thead>
        <tr>
            <th>Date</th>
            <th>Temp. (C)</th>
            <th>Temp. (F)</th>
            <th>Summary</th>
        </tr>
        </thead>
        <tbody>
        @foreach (var forecast in forecasts)
        {
            <tr>
                <td>@forecast.Date.ToShortDateString()</td>
                <td>@forecast.TemperatureC</td>
                <td>@forecast.TemperatureF</td>
                <td>@forecast.Summary</td>
            </tr>
        }
        </tbody>
    </table>
}

@code {
	//1. the collection we'll be adding to
    private readonly List<WeatherForecast> forecasts = new();

    protected override async Task OnInitializedAsync()
    {
        forecasts.Clear();
        // 2. invoking the IAsyncEnumerable implementation
        await foreach (var forecast in GetForecasts())
        {
            forecasts.Add(forecast);
            // 3. Calling StateHasChanged to flush
            StateHasChanged();
        }
    }

    static async IAsyncEnumerable<WeatherForecast> GetForecasts()
    {
        var startDate = DateOnly.FromDateTime(DateTime.Now);
        var summaries = new[]
        {
            "Freezing", "Bracing", "Chilly",
            "Cool", "Mild", "Warm", "Balmy",
            "Hot", "Sweltering", "Scorching"
        };

        for (var index = 0; index <= 5; index++)
        {
            await Task.Delay(1000);
            yield return new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = summaries[Random.Shared.Next(summaries.Length)]
            };
        }
    }

    private class WeatherForecast
    {
        public DateOnly Date { get; set; }
        public int TemperatureC { get; set; }
        public string? Summary { get; set; }
        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    }

}

There you have it. You are now using an IAsyncEnumerable within stream rendering to get the most performance out of your Blazor applications.

Conclusion

Streaming a response to the user can increase the perceived performance of your applications. Still, it’s important to remember that you also need to improve the performance of your dependencies to get the most out of this approach. While it may make your app more performant to stream HTML to the client as soon as possible, that effort can be undercut by a slow dependency such as a database or web service. Give it a try, and let me know what performance increases you see in your applications.

Thanks for reading and sharing my posts with friends and colleagues. Cheers.