Blazor Bits: Automatic Request Cancellation From Components
Cancellation tokens may be a familar concept to dotnet API developers. If you’re in that camp you may also find yourself trying to implement that logic around your Blazor web app.
Here’s the scenario, you’re writing a component and it needs data. Not a problem, you’ve injected your data fetching service with a carefully crafted interface which includes cancellation support. Clever. But you’re calling your method and omitting the cancellation. Log lines are flying everywhere, requests responding to an absent listener. Bytes going straight into the bin. Those bytes emit carbon you know.
Inherit from this CancellableComponent component and when your users navigate away the components disposal logic will request the cancellation of any attached requests.
namespace BlazorBits;
using Microsoft.AspNetCore.Components;
/// <summary>
/// Base class for components which require a cancellation token which
/// requests cancellation when the component is detached.
/// </summary>
public abstract class CancellableComponent : ComponentBase, IDisposable
{
private CancellationTokenSource? _cancellationTokenSource;
/// <summary>
/// Cancellation token which becomes cancelled when the component detaches
/// </summary>
protected CancellationToken ComponentCancellationToken => (_cancellationTokenSource ??= new()).Token;
/// <inheritdoc />
public virtual void Dispose()
{
if (_cancellationTokenSource == null)
{
return;
}
_cancellationTokenSource.Cancel();
_cancellationTokenSource.Dispose();
_cancellationTokenSource = null;
GC.SuppressFinalize(this);
}
}
Example
Using the Blazor 8 template as an example we can refactor the weather page to use a service called the WeatherService:
namespace BlazorBits.Weather;
public class WeatherService
{
public async Task<WeatherForecast[]> GetForecast(DateOnly startDate, int daysToForecast,
CancellationToken ct = default)
{
// Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500, ct);
var summaries = new[]
{ "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
var forecasts = Enumerable.Range(1, daysToForecast)
.Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
})
.ToArray();
return forecasts;
}
}
When we inject the service into the component we can attach the cancellation token to our request:
@page "/weatherWithCancellation"
@inject WeatherService WeatherService
@using BlazorBits.Weather
@inherits CancellableComponent
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if (forecasts == null)
{
<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 {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
var startDate = DateOnly.FromDateTime(DateTime.Now);
const int daysOfWeatherToRetrieve = 5;
forecasts = await WeatherService.GetForecast(
startDate,
daysOfWeatherToRetrieve,
ComponentCancellationToken);
}
}