ASP.NET Core Blazor 同步上下文

注意

此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

对于当前版本,请参阅此文的 .NET 8 版本

Blazor 使用同步上下文 (SynchronizationContext) 来强制执行单个逻辑线程。 组件的生命周期方法和 Blazor 引发的事件回调都在此同步上下文上执行。

Blazor 的服务器端同步上下文尝试模拟单线程环境,使其与浏览器(单线程)中的 WebAssembly 模型密切匹配。 此仿真的范围仅限于单个线路,这意味着两个不同的线路可以并行运行。 在一条线路中的任意给定时间点,工作只在一个线程上执行,这会造成单个逻辑线程的印象。 同一线路中不会同时执行两个操作。

避免阻止线程的调用

通常,不要在组件中调用以下方法。 以下方法阻止执行线程,进而阻止应用继续工作,直到基础 Task 完成:

注意

使用此部分中所述的线程阻止方法的 Blazor 文档示例只是使用方法进行演示,而不是用作建议编码指导。 例如,一些组件代码演示通过调用 Thread.Sleep 来模拟长时间运行的进程。

在外部调用组件方法以更新状态

如果组件必须根据外部事件(如计时器或其他通知)进行更新,请使用 InvokeAsync 方法,它将代码执行调度到 Blazor 的同步上下文。 例如,请考虑以下通告程序服务,它可向任何侦听组件通知更新的状态。 可以从应用中的任何位置调用 Update 方法。

TimerService.cs

namespace BlazorSample;

public class TimerService(NotifierService notifier, 
    ILogger<TimerService> logger) : IDisposable
{
    private int elapsedCount;
    private readonly static TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
    private readonly ILogger<TimerService> logger = logger;
    private readonly NotifierService notifier = notifier;
    private PeriodicTimer? timer;

    public async Task Start()
    {
        if (timer is null)
        {
            timer = new(heartbeatTickRate);
            logger.LogInformation("Started");

            using (timer)
            {
                while (await timer.WaitForNextTickAsync())
                {
                    elapsedCount += 1;
                    await notifier.Update("elapsedCount", elapsedCount);
                    logger.LogInformation("ElapsedCount {Count}", elapsedCount);
                }
            }
        }
    }

    public void Dispose()
    {
        timer?.Dispose();

        // The following prevents derived types that introduce a
        // finalizer from needing to re-implement IDisposable.
        GC.SuppressFinalize(this);
    }
}
public class TimerService : IDisposable
{
    private int elapsedCount;
    private readonly static TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
    private readonly ILogger<TimerService> logger;
    private readonly NotifierService notifier;
    private PeriodicTimer? timer;

    public TimerService(NotifierService notifier,
        ILogger<TimerService> logger)
    {
        this.notifier = notifier;
        this.logger = logger;
    }

    public async Task Start()
    {
        if (timer is null)
        {
            timer = new(heartbeatTickRate);
            logger.LogInformation("Started");

            using (timer)
            {
                while (await timer.WaitForNextTickAsync())
                {
                    elapsedCount += 1;
                    await notifier.Update("elapsedCount", elapsedCount);
                    logger.LogInformation($"elapsedCount: {elapsedCount}");
                }
            }
        }
    }

    public void Dispose()
    {
        timer?.Dispose();
    }
}
public class TimerService : IDisposable
{
    private int elapsedCount;
    private readonly static TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
    private readonly ILogger<TimerService> logger;
    private readonly NotifierService notifier;
    private PeriodicTimer? timer;

    public TimerService(NotifierService notifier,
        ILogger<TimerService> logger)
    {
        this.notifier = notifier;
        this.logger = logger;
    }

    public async Task Start()
    {
        if (timer is null)
        {
            timer = new(heartbeatTickRate);
            logger.LogInformation("Started");

            using (timer)
            {
                while (await timer.WaitForNextTickAsync())
                {
                    elapsedCount += 1;
                    await notifier.Update("elapsedCount", elapsedCount);
                    logger.LogInformation($"elapsedCount: {elapsedCount}");
                }
            }
        }
    }

    public void Dispose()
    {
        timer?.Dispose();
    }
}
using System;
using System.Timers;
using Microsoft.Extensions.Logging;

public class TimerService : IDisposable
{
    private int elapsedCount;
    private readonly ILogger<TimerService> logger;
    private readonly NotifierService notifier;
    private Timer timer;

    public TimerService(NotifierService notifier, ILogger<TimerService> logger)
    {
        this.notifier = notifier;
        this.logger = logger;
    }

    public void Start()
    {
        if (timer is null)
        {
            timer = new();
            timer.AutoReset = true;
            timer.Interval = 10000;
            timer.Elapsed += HandleTimer;
            timer.Enabled = true;
            logger.LogInformation("Started");
        }
    }

    private async void HandleTimer(object source, ElapsedEventArgs e)
    {
        elapsedCount += 1;
        await notifier.Update("elapsedCount", elapsedCount);
        logger.LogInformation($"elapsedCount: {elapsedCount}");
    }

    public void Dispose()
    {
        timer?.Dispose();
    }
}
using System;
using System.Timers;
using Microsoft.Extensions.Logging;

public class TimerService : IDisposable
{
    private int elapsedCount;
    private readonly ILogger<TimerService> logger;
    private readonly NotifierService notifier;
    private Timer timer;

    public TimerService(NotifierService notifier, ILogger<TimerService> logger)
    {
        this.notifier = notifier;
        this.logger = logger;
    }

    public void Start()
    {
        if (timer is null)
        {
            timer = new Timer();
            timer.AutoReset = true;
            timer.Interval = 10000;
            timer.Elapsed += HandleTimer;
            timer.Enabled = true;
            logger.LogInformation("Started");
        }
    }

    private async void HandleTimer(object source, ElapsedEventArgs e)
    {
        elapsedCount += 1;
        await notifier.Update("elapsedCount", elapsedCount);
        logger.LogInformation($"elapsedCount: {elapsedCount}");
    }

    public void Dispose()
    {
        timer?.Dispose();
    }
}

NotifierService.cs

namespace BlazorSample;

public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task>? Notify;
}
public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task>? Notify;
}
public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task>? Notify;
}
using System;
using System.Threading.Tasks;

public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task> Notify;
}
using System;
using System.Threading.Tasks;

public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task> Notify;
}

注册服务:

  • 对于客户端开发,请在客户端 Program 文件中将服务注册为单一实例:

    builder.Services.AddSingleton<NotifierService>();
    builder.Services.AddSingleton<TimerService>();
    
  • 对于服务器端开发,请在服务器 Program 文件中将服务注册为限定范围:

    builder.Services.AddScoped<NotifierService>();
    builder.Services.AddScoped<TimerService>();
    

使用 NotifierService 更新组件。

Notifications.razor

@page "/notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer

<PageTitle>Notifications</PageTitle>

<h1>Notifications Example</h1>

<h2>Timer Service</h2>

<button @onclick="StartTimer">Start Timer</button>

<h2>Notifications</h2>

<p>
    Status:
    @if (lastNotification.key is not null)
    {
        <span>@lastNotification.key = @lastNotification.value</span>
    }
    else
    {
        <span>Awaiting notification</span>
    }
</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized()
    {
        Notifier.Notify += OnNotify;
    }

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    private void StartTimer()
    {
        _ = Task.Run(Timer.Start);
    }

    public void Dispose() => Notifier.Notify -= OnNotify;
}

ReceiveNotifications.razor

@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer

<h1>Receive Notifications</h1>

<h2>Timer Service</h2>

<button @onclick="StartTimer">Start Timer</button>

<h2>Notifications</h2>

<p>
    Status:
    @if (lastNotification.key is not null)
    {
        <span>@lastNotification.key = @lastNotification.value</span>
    }
    else
    {
        <span>Awaiting notification</span>
    }
</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized()
    {
        Notifier.Notify += OnNotify;
    }

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    private void StartTimer()
    {
        _ = Task.Run(Timer.Start);
    }

    public void Dispose()
    {
        Notifier.Notify -= OnNotify;
    }
}

ReceiveNotifications.razor

@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer

<h1>Receive Notifications</h1>

<h2>Timer Service</h2>

<button @onclick="StartTimer">Start Timer</button>

<h2>Notifications</h2>

<p>
    Status:
    @if (lastNotification.key is not null)
    {
        <span>@lastNotification.key = @lastNotification.value</span>
    }
    else
    {
        <span>Awaiting notification</span>
    }
</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized()
    {
        Notifier.Notify += OnNotify;
    }

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    private void StartTimer()
    {
        _ = Task.Run(Timer.Start);
    }

    public void Dispose()
    {
        Notifier.Notify -= OnNotify;
    }
}

ReceiveNotifications.razor

@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer

<h1>Receive Notifications</h1>

<h2>Timer Service</h2>

<button @onclick="StartTimer">Start Timer</button>

<h2>Notifications</h2>

<p>
    Status:
    @if (lastNotification.key is not null)
    {
        <span>@lastNotification.key = @lastNotification.value</span>
    }
    else
    {
        <span>Awaiting notification</span>
    }
</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized()
    {
        Notifier.Notify += OnNotify;
    }

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    private void StartTimer()
    {
        Timer.Start();
    }

    public void Dispose()
    {
        Notifier.Notify -= OnNotify;
    }
}

ReceiveNotifications.razor

@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer

<h1>Receive Notifications</h1>

<h2>Timer Service</h2>

<button @onclick="StartTimer">Start Timer</button>

<h2>Notifications</h2>

<p>
    Status:
    @if (lastNotification.key != null)
    {
        <span>@lastNotification.key = @lastNotification.value</span>
    }
    else
    {
        <span>Awaiting notification</span>
    }
</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized()
    {
        Notifier.Notify += OnNotify;
    }

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    private void StartTimer()
    {
        Timer.Start();
    }

    public void Dispose()
    {
        Notifier.Notify -= OnNotify;
    }
}

在上面的示例中:

  • 计时器是使用 _ = Task.Run(Timer.Start) 在 Blazor 的外部启动的。
  • NotifierService 会调用该组件的 OnNotify 方法。 InvokeAsync 用于切换到正确的上下文,并将呈现排入队列。 有关详细信息,请参阅 ASP.NET Core Razor 组件呈现
  • 组件会实现 IDisposableOnNotify 委托在 Dispose 方法中取消订阅,在释放组件时,框架会调用此方法。 有关详细信息,请参阅 ASP.NET Core Razor 组件生命周期
  • NotifierService 在 Blazor 的同步上下文之外调用组件的 OnNotify 方法。 InvokeAsync 用于切换到正确的上下文,并将呈现排入队列。 有关详细信息,请参阅 ASP.NET Core Razor 组件呈现
  • 组件会实现 IDisposableOnNotify 委托在 Dispose 方法中取消订阅,在释放组件时,框架会调用此方法。 有关详细信息,请参阅 ASP.NET Core Razor 组件生命周期

重要

如果组件 Razor 定义从后台线程触发的事件,则可能需要该组件在注册处理程序时捕获和还原执行上下文 (ExecutionContext)。 有关更多信息,请参阅调用 InvokeAsync(StateHasChanged) 会导致页面回退到默认区域性 (dotnet/aspnetcore #28521)

要将捕获的异常从后台 TimerService 调度到组件,以将异常视为正常的生命周期事件异常,请参阅在 Razor 组件生命周期以外处理捕获的异常部分。

在 Razor 组件的生命周期外处理捕获的异常

在 Razor 组件中使用 ComponentBase.DispatchExceptionAsync 来处理在组件的生命周期调用堆栈外部引发的异常。 这允许组件的代码将异常视为生命周期方法异常。 此后,Blazor 的错误处理机制(如错误边界)可以处理异常。

注意

ComponentBase.DispatchExceptionAsync 用于继承自 ComponentBase 的 Razor 组件文件 (.razor)。 创建实现 implement IComponent directly 的组件时,请使用 RenderHandle.DispatchExceptionAsync

若要处理在 Razor 组件生命周期之外捕获的异常,请将异常传递给 DispatchExceptionAsync,然后等待结果:

try
{
    ...
}
catch (Exception ex)
{
    await DispatchExceptionAsync(ex);
}

上述方法的一个常见场景是某个组件启动了一个异步操作但不等待 Task,通常称为“发后即忘”模式,因为方法被触发(启动),而方法的结果被遗忘(丢弃)。 如果操作失败,你可能希望组件将失败视为组件生命周期异常,出于以下任何目标:

  • 例如,将组件置于出错状态,以触发错误边界
  • 如果没有错误边界,则终止线路。
  • 触发针对生命周期异常发生的相同日志记录。

在以下示例中,用户选择“发送报表”按钮,以触发发送报表的后台方法 ReportSender.SendAsync。 在大多数情况下,组件会等待异步调用的 Task,并更新 UI 以指示操作已完成。 在以下示例中,SendReport 方法不会等待 Task,也不会向用户报告结果。 由于组件有意放弃 SendReport 中的 Task,因此任何异步故障都发生在正常的生命周期调用堆栈中,因而不会被 Blazor 看到:

<button @onclick="SendReport">Send report</button>

@code {
    private void SendReport()
    {
        _ = ReportSender.SendAsync();
    }
}

若要处理生命周期方法异常等故障,请使用 DispatchExceptionAsync 将异常显式调度回组件,如以下示例所示:

<button @onclick="SendReport">Send report</button>

@code {
    private void SendReport()
    {
        _ = SendReportAsync();
    }

    private async Task SendReportAsync()
    {
        try
        {
            await ReportSender.SendAsync();
        }
        catch (Exception ex)
        {
            await DispatchExceptionAsync(ex);
        }
    }
}

替代方法利用 Task.Run

private void SendReport()
{
    _ = Task.Run(async () =>
    {
        try
        {
            await ReportSender.SendAsync();
        }
        catch (Exception ex)
        {
            await DispatchExceptionAsync(ex);
        }
    });
}

若要进行有效演示,请实现从外部调用组件方法以更新状态中的计时器通知示例。 在 Blazor 应用中,从计时器通知示例中添加以下文件,并在 Program 文件中注册服务,如以下部分所述:

  • TimerService.cs
  • NotifierService.cs
  • Notifications.razor

该示例使用 Razor 组件生命周期之外的计时器,其中未处理的异常通常不会由 Blazor 的错误处理机制(如错误边界)处理。

首先,更改 TimerService.cs 中的代码,以在组件的生命周期之外创建人工异常。 在 TimerService.cswhile 循环中,当 elapsedCount 达到 2 的值时引发异常:

if (elapsedCount == 2)
{
    throw new Exception("I threw an exception! Somebody help me!");
}

在应用的主布局中放置错误边界。 将 <article>...</article> 标记替换为以下标记。

MainLayout.razor中:

<article class="content px-4">
    <ErrorBoundary>
        <ChildContent>
            @Body
        </ChildContent>
        <ErrorContent>
            <p class="alert alert-danger" role="alert">
                Oh, dear! Oh, my! - George Takei
            </p>
        </ErrorContent>
    </ErrorBoundary>
</article>

在错误边界仅适用于静态 MainLayout 组件的 Blazor Web 应用中,边界仅在静态服务器端呈现(静态 SSR)阶段处于活动状态。 边界不会因为组件层次结构的下一个组件是交互式的而激活。 要广泛地为组件层次结构中的 MainLayout 组件和其余组件启用交互性,请为 App 组件(Components/App.razor)中的 HeadOutletRoutes 组件实例启用交互式呈现。 以下示例采用交互式服务器(InteractiveServer)呈现模式:

<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />

如果此时运行应用,当经过的计数达到 2 的值时,将引发异常。 但是,UI 不会更改。 错误边界不显示错误内容。

为了将计时器服务中的异常调度回 Notifications 组件,对组件进行了以下更改:

Notifications 组件 (Notifications.razor) 的 StartTimer 方法:

private void StartTimer()
{
    _ = Task.Run(async () =>
    {
        try
        {
            await Timer.Start();
        }
        catch (Exception ex)
        {
            await DispatchExceptionAsync(ex);
        }
    });
}

当计时器服务执行并达到 2 的计数时,异常将调度到 Razor 组件,然后反过来触发错误边界,以在 MainLayout 组件中显示 <ErrorBoundary> 的错误内容:

Oh, dear! Oh, my! - George Takei