I have a Blazor component that has to display data from a long running operation. For this reason I display a spinner, but since it takes long time I want to be able to cancel this loading when for example the user navigates away (e.g. the user clicks login while data is loading).
I implemented the Dispose pattern with the CancellationTokenSource object in my component, I made my function async with a Token as parameter but it seems that the token's "IsCanceled" is never set to true inside my load data function, neither it throws OperationCanceledException. If I make a test with a dummy function that simply awaits with Task.Delay for 20 sec and I pass the token it is correctly canceled. What am I doing wrong?
The end result is that while data is loading and the spinner is showing, if the user clicks on a button in order to navigate away it waits for the data loading completion.
The view where I display my data; "LoadingBox" shows a spinner while the list is not created.
<Card>
    <CardHeader><h3>Ultime offerte</h3></CardHeader>
    <CardBody>
        <div class="overflow-auto" style="max-height: 550px;">
            <div class="@(offersAreLoading ?"text-danger":"text-info")">LOADING: @offersAreLoading</div>
            <LoadingBox IsLoading="lastOffers == null">
                @if (lastOffers != null)
                {
                    @if (lastOffers.Count == 0)
                    {
                        <em>Non sono presenti offerte.</em>
                    }
                    <div class="list-group list-group-flush">
                        @foreach (var off in lastOffers)
                        {
                            <div class="list-group-item list-group-item-action flex-column align-items-start">
                                <div class="d-flex w-100 justify-content-between">
                                    <h5 class="mb-1">
                                        <a href="@(NavigationManager.BaseUri)offerta/@off.Oarti">
                                            @off.CodiceOfferta4Humans-@off.Versione
                                        </a>
                                    </h5>
                                    <small>@((int) ((DateTime.Now - off.Created).TotalDays)) giorni fa</small>
                                </div>
                                <p class="mb-1"><em>@(off.OggettoOfferta.Length > 50 ? off.OggettoOfferta.Substring(0, 50) + "..." : off.OggettoOfferta)</em></p>
                                <small>@off?.Redattore?.Username - @off.Created</small>
                            </div>
                        }
                    </div>
                }
            </LoadingBox>
        </div>
    </CardBody>
</Card>
The component code-behind. Here I call the long-running function (GetRecentAsync) that I want to cancel when the user navigates away or if the user does some other operation:
public partial class Test : IDisposable
    {
        private CancellationTokenSource cts = new();
        private IList<CommercialOffer> lastOffers;
        private bool offersAreLoading;
        [Inject] public CommercialOfferService CommercialOfferService { get; set; }
        async Task LoadLastOffers()
        {
            offersAreLoading = true;
            await InvokeAsync(StateHasChanged);
            var lo = await CommercialOfferService.GetRecentAsync(cancellationToken: cts.Token);
            lastOffers = lo;
            offersAreLoading = false;
            await InvokeAsync(StateHasChanged);
        }
        async Task fakeLoad()
        {
            offersAreLoading = true;
            await InvokeAsync(StateHasChanged);
            await Task.Delay(TimeSpan.FromSeconds(20), cts.Token);
            offersAreLoading = false;
            await InvokeAsync(StateHasChanged);
        }
        protected override async Task OnAfterRenderAsync(bool firstRender)
        {
            if (firstRender)
            {
                await LoadLastOffers();
            }
            await base.OnAfterRenderAsync(firstRender);
        }
        public void Dispose()
        {
            cts.Cancel();
            cts.Dispose();
        }
    }
public async Task<List<CommercialOffer>> GetRecentAsync(CancellationToken cancellationToken)
        {
            try
            {
                cancellationToken.ThrowIfCancellationRequested();
                var result = await _cache.GetOrCreateAsync<List<CommercialOffer>>("recentOffers", async entry =>
                {
                    entry.AbsoluteExpiration = DateTimeOffset.Now.Add(new TimeSpan(0, 0, 0, 30));
                    var list = await _unitOfWork.CommercialOfferRepository.GetAllWithOptionsAsync();
                    foreach (var commercialOffer in list)
                    {
                        // sta operazione è pesante, per questo ho dovuto cachare
                        // BOTH ISCANCELLATIONREQUESTED AND THROWIFCANCELLATINREQUESTED DOES NOT WORK, ISCANCELLATIONREQUESTED IS ALWAYS FALSE.
                        cancellationToken.ThrowIfCancellationRequested();
                        if (cancellationToken.IsCancellationRequested) return new List<CommercialOffer>();
                       
                        await _populateOfferUsersAsync(commercialOffer);
                    }
                    return list.Take(15).OrderByDescending(o => o.Oarti).ToList();
                });
                return result;
            }
            catch (OperationCanceledException)
            {
                // HERE I SET A BREAKPOINT IN ORDER TO SEE IF IT RUNS, BUT IT DOESN'T WORK
            }
        }
Thanks!
EDIT 20/07/2021
Thanks @Henk Holterman. GetRecentAsync gets all recent commercial offers, compiled by a simple form and having some data as a usual use case. Each of these commercial offers refer to 4 users (who manages the offer, the superior, the approver, etc.) and I populate with a foreach loop each of these users, for each commercial offer I want to display.
I know I should create from the start the entire entity (commercial offer) from the SQL query, but I need this for a matter of order and separation of concerns.
So, _populateOfferUsersAsync(commercialOffer) queries for 4 users of an offer, creates these 4 entities and assign them to the offer:
private async Task _populateOfferUsersAsync(CommercialOffer commercialOffer)
        {
            commercialOffer.Responsabile = await _unitOfWork.UserRepository.GetByIdAsync(commercialOffer.IdResponsabile);
            commercialOffer.Redattore = await _unitOfWork.UserRepository.GetByIdAsync(commercialOffer.IdRedattore);
            commercialOffer.Approvatore = await _unitOfWork.UserRepository.GetByIdAsync(commercialOffer.IdApprovatore);
            commercialOffer.Revisore = await _unitOfWork.UserRepository.GetByIdAsync(commercialOffer.IdRevisore);
        }
Under the hood I use Dapper for DB queries:
public async Task<User> GetByIdAsync(long id)
        {
            var queryBuilder = _dbTransaction.Connection.QueryBuilder($@"SELECT * FROM GEUTENTI /**where**/");
            queryBuilder.Where($"CUSER = {id}");
            queryBuilder.Where($"BSTOR = 'A'");
            queryBuilder.Where($"BDELE = 'S'");
            var users = await queryBuilder.QueryAsync<User>(_dbTransaction);
            return users.FirstOrDefault();
        }
From what I've seen there's no simple and effective way to pass a CancellationToken to stop Dapper queries, it might be me or Dapper being poor for that stuff
 
    