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:
- A collection variable must first exist, as you’ll be appending to it, not replacing it altogether.
- The
IAsyncEnumerable
must be awaited within an async component lifecycle method such asOnInitializedAsync
. - 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.