Search code examples
c#blazor-server-sidecancellationcancellationtokensource

How to do proper cancellation within a blazor server app?


I have a method bound to a click event from a table row. The method which gets called is:

private async Task BestelldispositionsItemClicked(Bestelldisposition pos)
{
    _tokenSourceClicked.Cancel();
    _tokenSourceClicked = new CancellationTokenSource();
    _belege.Clear();
    using FbController2 fbController = new FbController2();
    _isLoading = true;
    _selectedBestelldisposition = pos;

    await foreach (var beleg in pos.GetBelegeMitPositionAsync(fbController, _tokenSourceClicked.Token))
    {
        if (_tokenSourceClicked.IsCancellationRequested)
        {
            break;
        }

        _belege.Add(beleg);
        StateHasChanged();
    }


    _isLoading = false;
}

As you can see I am canceling the previous task with _tokenSourceClicked.Cancel();. _belege is a List<Beleg> which gets cleared afterwards. The token is being passed down to antoher function GetBelegeMitPositionAsync which is defined as:

public async IAsyncEnumerable<Auftrag> GetBelegeMitPositionAsync(FbController2 fbController, [EnumeratorCancellation] CancellationToken cancellationToken)
{
    if (!cancellationToken.IsCancellationRequested)
    {
        fbController.AddParameter("@BPOS_A_ARTIKELNR", Artikelnummer);
        var data = await fbController.SelectDataAsync(@"SELECT BELE_N_NR FROM BELEGPOS BP 
INNER JOIN BELEGE B ON (B.BELE_N_NR = BP.BPOS_N_NR AND B.BELE_A_TYP = 'AU')
WHERE BPOS_A_TYP = 'AU' AND BPOS_A_ARTIKELNR = @BPOS_A_ARTIKELNR
AND BELE_L_ERLEDIGT = 'N'");


        foreach (DataRow row in data.Rows)
        {
            if (cancellationToken.IsCancellationRequested)
            {
                break;
            }
            int belegnummer = row.Field<int>("BELE_N_NR");

            if (belegnummer > 0)
            {
                var auftrag = await Auftrag.GetAuftragAsync(belegnummer, fbController);

                if (auftrag is not null)
                {
                    if (!cancellationToken.IsCancellationRequested)
                    {
                        yield return auftrag;
                    }
                }
            }
        }
    }
}

As you can see I don't add/return anything when a canellation has being requested. However when I click my table row in quick succession, I'll see the same items up to 3 times within my list. How is this even possible if I cancel all old running methods before I call my method again? Does anyone know what I should do different here?


Solution

  • How is this even possible if I cancel all old running methods before I call my method again? Does anyone know what I should do different here?

    GetBelegeMitPositionAsync is checking the cancellation token for the CTS that was canceled; but the await foreach loop in BestelldispositionsItemClicked is checking the CTS member variable directly, which is changed after the old one is cancelled.

    I strongly recommend following the standard cancellation pattern for .NET. This pattern means that cancelled code should not return; it should throw OperationCanceledException. This is most easily done by replacing all IsCancellationRequested checks with calls to ThrowIfCancellationRequested. Also, all cancelable code should use (a copy of) the CancellationToken rather than checking the CancellationTokenSource directly.

    E.g.:

    private async Task BestelldispositionsItemClicked(Bestelldisposition pos)
    {
      _tokenSourceClicked.Cancel();
      _tokenSourceClicked = new CancellationTokenSource();
      var token = _tokenSourceClicked.Token;
      _belege.Clear();
      using FbController2 fbController = new FbController2();
      _isLoading = true;
      _selectedBestelldisposition = pos;
    
      try
      {
        await foreach (var beleg in pos.GetBelegeMitPositionAsync(fbController, token))
        {
          _belege.Add(beleg);
          StateHasChanged();
        }
      }
      catch (OperationCanceledException) { }
      finally
      {
        _isLoading = false;
      }
    }