Blazor 页面中的 CancellationToken?

Kyt*_*yte 11 c# asp.net async-await .net-core blazor

在工作方面经历了两年的埋头苦干后,我现在在新的工作场所遇到了 Blazor,在这两年之前主要从事 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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Kyt*_*yte 11

经过 2 年的 Blazor 经验后,我认为将 an 传递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
}
Run Code Online (Sandbox Code Playgroud)

在重复使用或只是为了遵守 DRY 规则时,您还可以从该类继承ComponentBase,然后将该类用于需要传递以下内容的组件CancellationToken

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

        public void Dispose()
        {
            _cts.Cancel();
            _cts.Dispose();
        }
    }
Run Code Online (Sandbox Code Playgroud)
@page "/"
@inherits CancellableComponent

@* Rest of the Component *@
Run Code Online (Sandbox Code Playgroud)

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

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


Hus*_*ish 5

CancellationTokenSource您可以创建一个公开 a 的基础组件,并CancellationToken项目的所有组件中自动使用该基础组件,而不是手动将 a 添加到所有组件

实现您的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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后添加@inherits ApplicationComponentBase_Imports.razor文件中

在页面中调用:

await Task.Delay(50000, CancellationToken);
Run Code Online (Sandbox Code Playgroud)

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