首页 > 解决方案 > CancellationToken 永远不会取消我长时间运行的加载数据功能

问题描述

我有一个 Blazor 组件,它必须显示来自长时间运行的操作的数据。出于这个原因,我显示了一个微调器,但是由于它需要很长时间,因此我希望能够在例如用户导航离开时取消此加载(例如,用户在加载数据时单击登录)。

我在组件中使用 CancellationTokenSource 对象实现了 Dispose 模式,我使用 Token 作为参数使我的函数异步,但似乎在我的加载数据函数中,令牌的“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">
                                            @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>

组件代码隐藏。在这里,我调用了长时间运行的函数 ( 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 不适合这些东西

标签: c#asynchronousblazordappercancellationtokensource

解决方案


我究竟做错了什么?

将您的取消令牌转发给所有异步 I/O 方法很重要:

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

然后当然进行相应的修改GetAllWithOptionsAsync()。实体框架中的所有异步方法都有一个接受CancellationToken.

...导航离开它等待数据加载完成。

当 GetAllWithOptionsAsync() 占用大部分时间时,这就是数字。下一个 foreach 循环应该在取消时中断,但这可能并不明显。
尽管如此,_populateOfferUsersAsync(commercialOffer)还应该将 CancellationToken 作为参数。

从您自己的 FakeLoad() 中可以看出,Blazor 和 CancellationTokenSource 没有损坏。


推荐阅读