Blazor with CancellationToken support

18/05/2022
C#Blazor

What happens when a user navigates away and still has a pending request to our server? Or what happens if we have a request which might take forever and wastes our resources?

CancellationToken

Taken the definition directly out of the Microsoft page:

A CancellationToken enables cooperative cancellation between threads, thread pool work items, or Task objects. You create a cancellation token by instantiating a CancellationTokenSource object, which manages cancellation tokens retrieved from its CancellationTokenSource.Token property. You then pass the cancellation token to any number of threads, tasks, or operations that should receive notice of cancellation.

The important part here is that we can cancel a Task object, which is perfect. For that we just re-use the sample Blazor App.

Usage example

A very simple example how to use a CancellationToken can be seen here:

var tokenSource = new CancellationTokenSource();
var t = DoWorkAsync(tokenSource.Token);
await Task.Delay(300);
tokenSource.Cancel();
await Task.Delay(300);
Console.Write("Done");

static async Task DoWorkAsync(CancellationToken token)
{
    while (true)
    {
        if(token.IsCancellationRequested) return;
		
         await Task.Delay(100);
         Console.WriteLine("Still working...");
    }
}

The output would be:

Still working...
Still working...
Still working...
Done

So we have a convenient way to cancel a Task from the outside world. But wait there is more: We also have a wait to define a timeout where the token automatically get cancelled like this:var tokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));. If we exchange that in our original code we get something like this now as output:

Still working...
Done

Blazor Cancellation when navigating away

The good thing is that the majority of the async API is supporting CancellationTokens. That also includes library like Entity Framework Core. That is super convenient because we can save precious resources when a user is going away from our page but there are still open requests. And of course we can utilize this in Blazor as well.

Let's simulate a service which supports cancellation. For that we just use the existing WeatherForecastService and make it cancelable with a small delay:

public async Task<WeatherForecast[]> GetForecastAsync(DateTime startDate, CancellationToken token)
    {
        await Task.Delay(2500);
        if (token.IsCancellationRequested)
        {
            Console.WriteLine("Cancelled");
            return Array.Empty<WeatherForecast>();
        }

        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = startDate.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        }).ToArray();
    }

Now the important part:

@page "/fetchdata"

<PageTitle>Weather forecast</PageTitle>

@using BlazorCancellation.Data
@inject WeatherForecastService ForecastService
@inject NavigationManager NavigationManager

@* left out intentionally *@

}

@code {
    private readonly CancellationTokenSource tokenSource = new(TimeSpan.FromSeconds(3));
    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        NavigationManager.LocationChanged += (_, _) => tokenSource.Cancel();
        forecasts = await ForecastService.GetForecastAsync(DateTime.Now, tokenSource.Token);
    }
}

Let's go through:

  • We inject the NavigationManager because we need the hook into the event chain when the user navigates away
  • private readonly CancellationTokenSource tokenSource = new(TimeSpan.FromSeconds(3)); will create a CancellationTokenSource which automatically gets cancelled after 3 seconds. That means if the request takes longer than 3 seconds, we cancel the Task.
  • NavigationManager.LocationChanged += (_, _) => tokenSource.Cancel() - we want to cancel the task everytime when the user navigates away from our page.
  • forecasts = await ForecastService.GetForecastAsync(DateTime.Now, tokenSource.Token); as seen earlier we have to pass the Token from the CancellationTokenSource.

That's all! Super easy and convenient. If you now open the Fetch Data page and immediately navigate away you will see Cancelled in your console. You can also change the timeout to something small like: ``private readonly CancellationTokenSource tokenSource = new(TimeSpan.FromSeconds(1));` and even without navigating away you will see the message, plus after 2.5 seconds you see an empty table because in case of cancellation we return an empty array.

Conclusion

CancellationTokens are a nice and convenient way to save some precious resources and help you scaling. You can also use them for an UI/UX point of view to know that some of the API's timed out. I'd encourage you to use CancellationTokens more often.

Resources

3
An error has occurred. This application may no longer respond until reloaded. Reload x