在工作方面生活在岩石下 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;
}
}
我的问题是,在代码的特定部分获取令牌的最佳方法是什么,或者它是否无关紧要,因为当连接(例如)结束时服务器会终止所有正在运行的任务?
经过 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
,它将为您提供移除电路时的事件。)
您可以创建一个公开
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);
然后尝试导航到另一个页面,您调用的任务将被取消
我自己对 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;
}
}
}