Blazor 页面中的 CancellationToken?

问题描述 投票:0回答:3

在工作方面生活在岩石下 2 年后,我现在在新工作场所遇到 Blazor,并且在 2 年前主要完成 ASP.NET Framework MVC 后,我还有很多工作要做。

在 Blazor 服务器端尝试自己,我尝试应用我过去的知识,其中包括用于异步操作的取消令牌,但我找不到有关它们与 Blazor 结合使用的太多信息。

它们仍然是最佳实践还是在某个时候已经过时了? 我确实发现了这个之前提出的问题,建议在

OnInitializedAsync()
方法上创建一个令牌源,并在
Dispose()
上取消它,老实说我觉得有点粗糙。 (我需要为每个页面实现这个,你知道......干)

我还在 Microsoft Docs 上找到了这篇关于高级场景的文章,它解释了如何实现电路处理程序,老实说,这有点超出了我的能力范围,而且很可能超出了我的小型家庭项目的范围。

相比之下,在 asp.net Framework MVC 中我会构建一个像这样的控制器:

namespace SampleWebsite.Controllers
{
    public class SampleController : ApiController
    {
        private readonly MyEntities _entities = new MyEntities();

        public async Task<IHttpActionResult> MyAsyncApi(CancellationToken cancellationToken)
        {
            var result = _entities.MyModel.FirstOrDefault(e => e.Id == 1, cancellationToken: cancellationToken);
            return OK(result);
        }
    }
}

CancellationToken 将由 ASP.NET Framework / Core 注入,并直接链接到当前上下文连接管道。 因此,如果用户关闭连接,令牌就会失效。

我本以为对于 asp.net core 和 blazor 来说,依赖注入是其中很大一部分,这里也是这种情况,但我在这里找不到任何关于此的文档。

那么,此时是否仍应使用取消令牌,或者微软是否在后台为异步任务做了一些魔法?如果是,最好的实施是什么?

编辑: 这是我的设置来澄清:

Blazor 组件:

@page "/Index"
@inject IIndexService Service

@* Some fancy UI stuff *@

@code {
    private IEnumerable<FancyUiValue> _uiValues;

    protected override async Task OnInitializedAsync()
    {
        _uiValues = await Service.FetchCostlyValues();
    }
}

以及执行繁重任务的注入服务类:

public interface IIndexService
{
    Task<IEnumerable<FancyUiValue>> FetchCostlyValues();
}

public class IndexService : IIndexService
{
    public async Task<IEnumerable<FancyUiValue>> FetchCostlyValues()
    {
        var uiValues = await heavyTask.ToListAsync(); // <-- Best way to get a cancellationtoken here?
        return uiValues;
    }
}

我的问题是,在代码的特定部分获取令牌的最佳方法是什么,或者它是否无关紧要,因为当连接(例如)结束时服务器会终止所有正在运行的任务?

c# asp.net .net-core async-await blazor
3个回答
14
投票

经过 2 年的 Blazor 经验,我认为将

CancellationToken
传递给生命周期较长的对象(例如单例或作用域服务)中的任务的唯一可靠方法是
IDisposeable
CancellationTokenSource
的组合

@page "/"
@implements IDisposable

*@ Razor Stuff *@

@code
{
    private CancellationTokenSource _cts = new();

    protected override async Task OnInitializedAsync()
    {
        await BusinessLogicSingleton.DoExpensiveTask(_cts.Token);
    }

    #region IDisposable

    public void Dispose()
    {
        _cts.Cancel();
        _cts.Dispose();
    }

    #endregion
}

在重复使用或只是为了遵守 DRY 规则时,您还可以从

ComponentBase
类继承,然后将该类用于需要传递
CancellationToken
的组件:

public class CancellableComponent : ComponentBase, IDisposable
    {
        internal CancellationTokenSource _cts = new();

        public void Dispose()
        {
            _cts.Cancel();
            _cts.Dispose();
        }
    }
@page "/"
@inherits CancellableComponent

@* Rest of the Component *@

我还发现,虽然您可以注入

IHttpContextAccessor
并使用
HttpContext.RequestAborted
令牌,该令牌与将在 ASP.Net MVC 方法调用中生成和注入的令牌相同,但从当前
.Net6
版本开始,它将 即使与客户端的连接被切断并且所提供的 HttpContext
 被处置,也永远不会
触发。

这可能是 Github 上开发团队的情况,因为我确实看到了用例,其中允许用户退出组件,而任务继续进行,直到用户完全离开网站。
(对于这种情况,我建议的解决方法是编写您自己的

CircuitHandler
,它将为您提供移除电路时的事件。)


7
投票

您可以创建一个公开

CancellationTokenSource
的基础组件,并在
 项目的所有组件
中自动使用此基础组件,而不是手动向所有组件添加 CancellationToken

实现您的ApplicationComponentBase

public abstract class ApplicationComponentBase: ComponentBase, IDisposable
{
    private CancellationTokenSource? cancellationTokenSource;

    protected CancellationToken CancellationToken => (cancellationTokenSource ??= new()).Token;

    public virtual void Dispose()
    {
        if (cancellationTokenSource != null)
        {
            cancellationTokenSource.Cancel();
            cancellationTokenSource.Dispose();
            cancellationTokenSource = null;
        }
    }
}

然后将

@inherits ApplicationComponentBase
添加到
_Imports.razor
文件

在页面中调用:

await Task.Delay(50000, CancellationToken);

然后尝试导航到另一个页面,您调用的任务将被取消


0
投票

我自己对 Blazor 还很陌生,但这是我首先想到的问题之一,我认为我有更好的解决方案。

IServiceProvider
自动知道如何提供
IHostApplicationLifetime
。这会暴露一个代表服务器正常关闭的
CancellationToken
。将应用程序生命周期注入到您的组件库中,并使用
CreateLinkedTokenSource
创建一个 CTS,您可以在处置组件时取消该 CTS:

    public abstract class AsyncComponentBase : ComponentBase, IDisposable
    {
        [Inject]
        private IHostApplicationLifetime? Lifetime { get; set; }
        private CancellationTokenSource? _cts;
        protected CancellationToken Cancel { get; private set; }

        protected override void OnInitialized()
        {
            if(Lifetime != null)
            {
                _cts = CancellationTokenSource.CreateLinkedTokenSource(Lifetime.ApplicationStopping);
                Cancel = _cts.Token;
            }            
        }

        public void Dispose()
        {
            GC.SuppressFinalize(this);
            if(_cts != null)
            {
                _cts.Cancel();
                _cts.Dispose();
                _cts = null;
            }
        }
    }
© www.soinside.com 2019 - 2024. All rights reserved.