1

I would like to consume an IAsyncEnumerable via a HttpClient from a Blazor-Page. The stream of the results works properly but in case of cancelling the request, the Task does not end on the server side.

Server-Side Code (Sample Code->results in an endless-loop):

async IAsyncEnumerable<WeatherForecast> Post([EnumeratorCancellation] CancellationToken cancellationToken, ILogger<Program> logger)
{
    logger?.LogInformation($"Called Server");
    var i = 0;
    while(!cancellationToken.IsCancellationRequested)
    {
        logger?.LogInformation($"Send item {i}");
        cancellationToken.ThrowIfCancellationRequested();
        yield return new WeatherForecast
        (
               DateTime.Now.AddDays(i),
               Random.Shared.Next(-20, 55),
              WeatherForecastController.Summaries[Random.Shared.Next(WeatherForecastController.Summaries.Length)]
         );
        await Task.Delay(1000, cancellationToken);
        i++;
    }

}

Client-Side (Cancellation via Button):

this._cts = new();
 using var client = factory.CreateClient("Default");
    using var req = new HttpRequestMessage(HttpMethod.Post, uri);
    req.SetBrowserResponseStreamingEnabled(true);
var response = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, this._cts.Token);
if (response.IsSuccessStatusCode)
{
    using var responseStream = await response.Content.ReadAsStreamAsync(this._cts.Token);
    var weatherForcasts = JsonSerializer.DeserializeAsyncEnumerable<WeatherForecast>(
         responseStream,
         new JsonSerializerOptions
             {
                 PropertyNameCaseInsensitive = true,
                 DefaultBufferSize = 128
             }
     , this._cts.Token);


    await foreach (var weatherForecast in weatherForcasts)
    {
        if (this._cts.Token.IsCancellationRequested)
        {
            break;
        }
        forecasts.Add(weatherForecast);
        await this.InvokeAsync(StateHasChanged);
        await Task.Delay(1);
    }
}

UPDATE: When i try to iterate through a loop without "yield return" inside only awaiting a Delay, the cancellation works properly. After canelling the client request, the cancelationtoken of the server-call will be canceled as well.

  • You can't cancel a server side task remotely from the client. – Liam Oct 18 '22 at 08:19
  • It seems that you should consider switching to SignalR instead of using ordinary HTTP requests. – Guru Stron Oct 18 '22 at 11:07
  • @GuruStron just want to display results while loading. Or have the possibility to cancel the call. In production there will be a long time sql-query operation with sub queries to gather metadata. I dont want to have a persitent connection like signalR – user20270550 Oct 18 '22 at 11:27
  • @liam are you sure? then why does asp.net pass a cancellation token to the controller action? – Darragh May 31 '23 at 17:26
  • The controller action is not client side @Darragh – Liam May 31 '23 at 18:15
  • @Liam if the request is long running, as it is in this case, then you can cancel it . https://andrewlock.net/using-cancellationtokens-in-asp-net-core-mvc-controllers/ – Darragh Jun 01 '23 at 20:07
  • That is also server side, not client side. The browser does not know what a cancellation token is... – Liam Jun 02 '23 at 07:57

1 Answers1

-1

3 Things

1# Bad usage of infinite loop server side

You do have an infinite loop, server side an HttpRequest really isn't intended to be essentially a continuous connection. Like one comment your treating this as a persistent connection, so SignalR may very well be a better choice. As is the only way to stop this code IS through cancelation, and frankly looks like an abusive technique forcing a scenario onto HttpClient.

HttpRequests should at the end of the day should have a normal path of start and end clearly defined, Request/Response behavior, even with IAsyncEnumerable the idea is the client can start the Request and the server can start Responding with the first available items while not making you wait for them all.

#2 Faster Cancelation

If you want your cancelation to be more immediate, this may have to do with how your canceling the loop.

await foreach (var weatherForecast in weatherForcasts)

the IAsyncEnumerable itself has a cancelation process

await foreach (var weatherForecast in weatherForcasts.WithCancellation(this._cts.Token))

Without this WithCancellation added you are technically forced to wait till the next item is fully received before you are able exit the loop.

#3 CancelationToken behavior

While not deeply documented, most MSFT methods only honor listening to a CancelationToken at the most immediate entry to a method. You could decompile or pop over to GitHub to the relevant code to see what its doing but its usually faster to assume like I've stated already. So like in #2 you have to use AND handle cancelation in every place necessary

Michael Rieger
  • 472
  • 9
  • 18