CancellationToken никогда не отменяет мою долгую функцию загрузки данных

У меня есть компонент Blazor, который должен отображать данные длительной операции. По этой причине я показываю счетчик, но поскольку это занимает много времени, я хочу иметь возможность отменить эту загрузку, когда, например, пользователь уходит (например, пользователь нажимает кнопку входа в систему во время загрузки данных).

Я реализовал шаблон Dispose с объектом CancellationTokenSource в моем компоненте, я сделал свою функцию асинхронной с параметром Token as, но кажется, что IsCanceled токена никогда не устанавливается в значение true внутри моей функции загрузки данных, и при этом не возникает OperationCanceledException. Если я провожу тест с фиктивной функцией, которая просто ожидает с Task.Delay в течение 20 секунд, и я передаю токен, он будет правильно отменен. Что я делаю неправильно?

Конечным результатом является то, что пока данные загружаются и отображается счетчик, если пользователь нажимает кнопку, чтобы уйти, он ожидает завершения загрузки данных.

Представление, в котором я отображаю свои данные; LoadingBox показывает счетчик, пока список не создан.

<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">
                                            @[email protected]
                                        </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>

Код программной части компонента. Здесь я вызываю длительную функцию (GetRecentAsync), которую хочу отменить, когда пользователь уходит или выполняет какую-либо другую операцию:

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
            }
        }

Спасибо!

ИЗМЕНИТЬ 20.07.2021

Спасибо @Henk Holterman. GetRecentAsync получает все последние коммерческие предложения, скомпилированные с помощью простой формы и имеющие некоторые данные в качестве обычного варианта использования. Каждое из этих коммерческих предложений относится к 4 пользователям (которые управляют предложением, вышестоящим, утверждающим и т. Д.), И я заполняю циклом foreach каждого из этих пользователей для каждого коммерческого предложения, которое я хочу отобразить.

Я знаю, что должен с самого начала создать всю сущность (коммерческое предложение) из SQL-запроса, но мне это нужно для порядка и разделения задач.

Итак, _populateOfferUsersAsync (CommercialOffer) запрашивает 4 пользователей предложения, создает эти 4 объекта и назначает их предложению:

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);
        }

Под капотом я использую Dapper для запросов к БД:

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();
        }

Из того, что я видел, нет простого и эффективного способа передать CancellationToken, чтобы остановить запросы Dapper, это может быть я или Dapper, которые не справляются с этим.


person exrezzo    schedule 16.07.2021    source источник
comment
Я не очень знаком с Blazor, но заметил в в этой статье @implements IDisposable и метод Dispose добавляются непосредственно в код компонента, а не в класс модели. Это то, на что стоит обратить внимание?   -  person StriplingWarrior    schedule 16.07.2021
comment
Хорошо, я пропустил ракурс Даппера. Это ваш самый важный тег здесь.   -  person Henk Holterman    schedule 20.07.2021
comment
Ответ см. stackoverflow.com/q/25540793/60761   -  person Henk Holterman    schedule 20.07.2021
comment
Давайте предположим, что Dapper будет работать с токеном (на самом деле не очень хорошо), но тот факт, что я не могу остановить цикл внутри GetRecentAsync, не имеет смысла. В этом случае я читаю данные из db, поэтому нет необходимости работать над отменой транзакции sql (это была бы другая история, если бы я писал в db, в этом случае я предполагаю, что мне придется откатить транзакцию ). Лучшее, чего я добился к настоящему моменту, - это использование await Task.Run(() => CommercialOfferService.GetRecentAsync(cancellationToken: cts.Token), cts.Token);, теперь он бросает   -  person exrezzo    schedule 20.07.2021


Ответы (1)


Что я делаю неправильно?

Важно переслать ваш токен отмены всем асинхронным методам ввода-вывода:

// var list = await _unitOfWork.CommercialOfferRepository.GetAllWithOptionsAsync();
var list = await _unitOfWork.CommercialOfferRepository.GetAllWithOptionsAsync(cancellationToken);

И затем, конечно, соответствующим образом измените GetAllWithOptionsAsync(). Все асинхронные методы в структуре сущностей имеют перегрузку, которая принимает CancellationToken.

... чтобы уйти, он ждет завершения загрузки данных.

Это происходит, когда GetAllWithOptionsAsync () занимает большую часть времени. Следующий цикл foreach должен прерваться при отмене, но это может быть незаметно.
Тем не менее, _populateOfferUsersAsync(commercialOffer) также должен принимать CancellationToken в качестве параметра.

Как видно из вашего собственного FakeLoad (), Blazor и CancellationTokenSource не сломаны.

person Henk Holterman    schedule 16.07.2021
comment
К сожалению, даже если я передам токен последующим функциям, таким как _populateOfferUsersAsync (CommercialOffer), я не могу получить OperationCanceledException, ни isCancellationRequested == true нигде, похоже, что токен внутри этих функций (из GetAllWithOptionsAsync on) он никогда не отменяется, как будто это другой объект или я не знаю, что - person exrezzo; 19.07.2021
comment
Можете ли вы опубликовать план для GetAllWithOptionsAsync и _populateOfferUsersAsync? Насколько они асинхронны? - person Henk Holterman; 19.07.2021
comment
Я отредактировал свой вопрос, добавив больше информации - person exrezzo; 20.07.2021