处理 ASP.NET Core Blazor 应用中的错误

注意

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

重要

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

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

本文介绍 Blazor 如何管理未经处理的异常以及如何开发用于检测和处理错误的应用。

开发过程中的错误详细信息

当 Blazor 应用在开发过程中运行不正常时,从该应用接收详细的错误信息有助于故障排除和修复问题。 出现错误时,Blazor 应用会在屏幕底部显示一个浅黄色条框:

  • 在开发过程中,这个条框会将你定向到浏览器控制台,你可在其中查看异常。
  • 在生产过程中,这个条框会通知用户发生了错误,并建议刷新浏览器。

此错误处理体验的 UI 属于 Blazor 项目模板。 并非所有版本的 Blazor 项目模板都使用 data-nosnippet 属性向浏览器发出信号,而不缓存错误 UI 的内容,但所有版本的 Blazor 文档都应用该属性。

在 Blazor Web 应用中,在 MainLayout 组件中自定义体验。 由于 Razor 组件中不支持环境标记帮助程序(例如,<environment include="Production">...</environment>),因此以下示例注入 IHostEnvironment 以配置不同环境的错误消息。

MainLayout.razor 的顶部:

@inject IHostEnvironment HostEnvironment

创建或修改 Blazor 错误 UI 标记:

<div id="blazor-error-ui" data-nosnippet>
    @if (HostEnvironment.IsProduction())
    {
        <span>An error has occurred.</span>
    }
    else
    {
        <span>An unhandled exception occurred.</span>
    }
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

在 Blazor Server 应用中,在 Pages/_Host.cshtml 文件中自定义体验。 以下示例使用环境标记帮助程序为不同的环境配置错误消息。

在 Blazor Server 应用中,在 Pages/_Layout.cshtml 文件中自定义体验。 以下示例使用环境标记帮助程序为不同的环境配置错误消息。

在 Blazor Server 应用中,在 Pages/_Host.cshtml 文件中自定义体验。 以下示例使用环境标记帮助程序为不同的环境配置错误消息。

创建或修改 Blazor 错误 UI 标记:

<div id="blazor-error-ui" data-nosnippet>
    <environment include="Staging,Production">
        An error has occurred.
    </environment>
    <environment include="Development">
        An unhandled exception occurred.
    </environment>
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

在 Blazor WebAssembly 应用的 wwwroot/index.html 文件中自定义体验:

<div id="blazor-error-ui" data-nosnippet>
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

blazor-error-ui 元素通常是隐藏的,因为应用的自动生成样式表中存在 blazor-error-ui CSS 类的 display: none 样式。 当发生错误时,框架将 display: block 应用于该元素。

由于 wwwroot/css 文件夹中站点样式表中存在 blazor-error-ui CSS 类的 display: none 样式,blazor-error-ui 元素通常被隐藏。 当发生错误时,框架将 display: block 应用于该元素。

详细线路错误

本部分适用于通过线路运行的 Blazor Web 应用。

本部分适用于 Blazor Server 应用。

客户端错误不包括调用堆栈,也不提供有关错误原因的详细信息,但服务器日志的确包含此类信息。 出于开发目的,可通过启用详细错误向客户端提供敏感线路错误信息。

CircuitOptions.DetailedErrors 设置为 true。 有关详细信息和示例,请参阅 ASP.NET Core BlazorSignalR 指南

另一种设置 CircuitOptions.DetailedErrors 的方法是在应用 Development 环境设置文件 (appsettings.Development.json) 中将 DetailedErrors 配制键设置为 true。 此外,SignalR 服务器端日志记录 (Microsoft.AspNetCore.SignalR) 可设置为调试跟踪,以用于详细 SignalR 日志记录。

appsettings.Development.json

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.AspNetCore.SignalR": "Debug"
    }
  }
}

Development/Staging 环境服务器或本地系统上,还可以使用 ASPNETCORE_DETAILEDERRORS 环境变量和值 trueDetailedErrors 配置键设置为 true

警告

始终避免向 Internet 上的客户端公开错误信息,这会带来安全风险。

Razor 组件服务器端呈现的详细错误

本部分适用于 BlazorWeb 应用。

使用 RazorComponentsServiceOptions.DetailedErrors 选项可控制生成有关 Razor 组件服务器端呈现错误的详细信息。 默认值为 false

以下示例启用详细错误:

builder.Services.AddRazorComponents(options => 
    options.DetailedErrors = builder.Environment.IsDevelopment());

警告

仅在 Development 环境中启用详细错误。 详细错误可能包含有关恶意用户可在攻击中使用的应用的敏感信息。

前面的示例通过根据 IsDevelopment 返回的值设置 DetailedErrors 的值来提供一定程度的安全性。 当应用位于 Development 环境中时,DetailedErrors 会设置为 true。 这种方法并非万无一失,因为可以在 Development 环境中的公共服务器上托管生产应用。

在开发人员代码中管理未经处理的异常

若要在出现错误后继续运行应用,该应用必须具备错误处理逻辑。 本文后面的部分将介绍未经处理的异常出现的潜在原因。

在生产环境中,不要在 UI 中呈现框架异常消息或堆栈跟踪信息。 呈现异常消息或堆栈跟踪信息可能导致:

  • 向最终用户公开敏感信息。
  • 帮助恶意用户发现应用中可能会危及应用、服务器或网络安全的弱点。

线路的未经处理的异常

本部分适用于通过线路运行的服务器端应用。

启用了服务器交互的 Razor 组件在服务器上是有状态的。 当用户与服务器上的组件交互时,他们会保持与服务器的连接,称为线路。 线路包含活动组件实例,以及状态的许多其他方面,例如:

  • 最新呈现的组件输出。
  • 可由客户端事件触发的事件处理委托的当前集合。

如果用户在多个浏览器标签页中打开应用,则用户就会创建多条独立线路。

Blazor 将大部分未经处理的异常视为发生该异常的线路的严重异常。 如果线路由于未经处理的异常而终止,则用户只能重新加载页面来创建新线路,从而继续与应用进行交互。 终止的线路以外的其他线路(即其他用户或其他浏览器标签页的线路)不会受到影响。 此场景类似于出现故障的桌面应用。 出现故障的应用必须重新启动,但其他应用不受影响。

当发生未处理异常时,框架会出于以下原因终止线路:

  • 未经处理的异常通常会将线路置于未定义状态。
  • 发生未经处理的异常后,应用可能无法正常运行。
  • 如果线路继续保持未定义状态,应用中可能会出现安全漏洞。

全局异常处理

有关全局异常处理,请参阅以下部分:

错误边界

错误边界提供了一种用于处理异常的便捷方法ErrorBoundary 组件:

  • 在未发生错误时呈现其子内容。
  • 在引发未处理的异常时呈现错误 UI。

要定义错误边界,请使用 ErrorBoundary 组件来包装现有内容。 应用继续正常运行,但错误边界会处理未处理的异常。

<ErrorBoundary>
    ...
</ErrorBoundary>

要以全局方式实现错误边界,请在应用主布局的正文内容周围添加边界。

MainLayout.razor中:

<article class="content px-4">
    <ErrorBoundary>
        @Body
    </ErrorBoundary>
</article>

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

<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />

如果不希望从 Routes 组件跨整个应用启用服务器交互性,请将错误边界置于组件层次结构中更低的位置。 例如,将错误边界放在启用交互性的单个组件中标记的附近,而不放在应用的主布局中。 需要记住的重要概念是,无论在何处放置错误边界:

  • 如果错误边界不是交互式的,则只能在静态呈现期间在服务器上激活。 例如,当组件生命周期方法中引发错误时,边界可以激活。
  • 如果错误边界是交互式的,它能够激活它包装的交互式服务器呈现的组件。

请考虑使用以下示例,其中 Counter 组件在计数增加超过 5 时引发异常。

Counter.razor中:

private void IncrementCount()
{
    currentCount++;

    if (currentCount > 5)
    {
        throw new InvalidOperationException("Current count is too big!");
    }
}

如果 currentCount 超过 5,则会引发未处理的异常:

  • 错误记录正常 (System.InvalidOperationException: Current count is too big!)。
  • 异常由错误边界处理。
  • 错误 UI 由错误边界呈现,并显示以下默认错误消息:An error has occurred.

默认情况下,ErrorBoundary 组件会为其错误内容呈现具有 blazor-error-boundary CSS 类的空 <div> 元素。 默认 UI 的颜色、文本和图标是使用 wwwroot 文件夹中应用样式表中的 CSS 定义的,因此可以自定义错误 UI。

通过设置 ErrorContent 属性来更改默认错误内容:

<ErrorBoundary>
    <ChildContent>
        @Body
    </ChildContent>
    <ErrorContent>
        <p class="errorUI">😈 A rotten gremlin got us. Sorry!</p>
    </ErrorContent>
</ErrorBoundary>

由于错误边界是在前面的示例中在布局中定义的,因此无论用户在错误发生后导航到哪个页面,都会看到错误 UI。 建议在大多数场景下缩小错误边界的范围。 如果设置了较广泛的错误边界,则可以通过调用错误边界的 Recover 方法,在后续页面导航事件中将其重置为非错误状态。

MainLayout.razor中:

...

<ErrorBoundary @ref="errorBoundary">
    @Body
</ErrorBoundary>

...

@code {
    private ErrorBoundary? errorBoundary;

    protected override void OnParametersSet()
    {
        errorBoundary?.Recover();
    }
}

为了避免无限循环,其中恢复只会重新呈现再次引发错误的组件,请勿从呈现逻辑调用 Recover。 仅在以下情况下呼叫 Recover

  • 用户执行 UI 手势,例如选择按钮以指示其想要重试过程,或者当用户导航到新组件时。
  • 额外的逻辑也会清除异常。 重新呈现组件时,错误不会再次出现。

备用全局异常处理

可将自定义错误组件作为 CascadingValue 传递给子组件,来代替使用错误边界 (ErrorBoundary)。 与使用注入式服务或自定义记录器实现相比,使用组件的一个优点是,在发生错误时,级联组件可以呈现内容并应用 CSS 样式。

下面的 Error 组件示例仅记录错误,但组件的方法可以按照应用要求的任何方式处理错误,包括通过使用多种错误处理方法。

Error.razor

@inject ILogger<Error> Logger

<CascadingValue Value="this">
    @ChildContent
</CascadingValue>

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    public void ProcessError(Exception ex)
    {
        Logger.LogError("Error:ProcessError - Type: {Type} Message: {Message}", 
            ex.GetType(), ex.Message);

        // Call StateHasChanged if ProcessError directly participates in 
        // rendering. If ProcessError only logs or records the error,
        // there's no need to call StateHasChanged.
        //StateHasChanged();
    }
}

注意

有关 RenderFragment 的详细信息,请参阅 ASP.NET Core Razor 组件

Routes 组件中,使用 Error 组件将 Router<Router>...</Router>)组件包装起来。 这允许 Error 组件向下级联到应用中将 Error 组件作为 CascadingParameter 接收的任何组件。

Routes.razor中:

<Error>
    <Router ...>
        ...
    </Router>
</Error>

App 组件中,使用 Error 组件将 Router<Router>...</Router>)组件包装起来。 这允许 Error 组件向下级联到应用中将 Error 组件作为 CascadingParameter 接收的任何组件。

App.razor中:

<Error>
    <Router ...>
        ...
    </Router>
</Error>

要在组件中处理错误:

  • Error 组件指定为 @code 块中的 CascadingParameter。 在基于 Blazor 项目模板的应用的示例 Counter 组件中,添加以下 Error 属性:

    [CascadingParameter]
    public Error? Error { get; set; }
    
  • 使用适当的异常类型在任何 catch 块中调用错误处理方法。 该示例 Error 组件只提供了一个 ProcessError 方法,但错误处理组件可以提供任意数量的错误处理方法来解决整个应用中的其他错误处理要求。 在下面的 Counter 组件示例中,当计数大于 5 时会引发并捕获异常:

    @code {
        private int currentCount = 0;
    
        [CascadingParameter]
        public Error? Error { get; set; }
    
        private void IncrementCount()
        {
            try
            {
                currentCount++;
    
                if (currentCount > 5)
                {
                    throw new InvalidOperationException("Current count is over five!");
                }
            }
            catch (Exception ex)
            {
                Error?.ProcessError(ex);
            }
        }
    }
    

使用前面的 Error 组件和先前对 Counter 组件做出的更改,浏览器的开发人员工具控制台会指示捕获并记录的错误:

fail: {COMPONENT NAMESPACE}.Error[0]
Error:ProcessError - Type: System.InvalidOperationException Message: Current count is over five!

如果 ProcessError 方法直接参与呈现,例如,显示自定义错误消息栏或更改所呈现元素的 CSS 样式,请在 ProcessErrors 方法末尾调用 StateHasChanged 来重新呈现 UI。

由于本部分中的方法使用 try-catch 语句处理错误,因此在发生错误时,客户端和服务器之间应用的 SignalR 连接不会中断,并且线路保持活跃状态。 其他未处理异常对于线路来说仍然是严重异常。 有关详细信息,请参阅有关线路如何对未经处理的异常作出反应的部分。

应用可以使用错误处理组件作为级联值来集中处理错误。

以下 Error 组件将自身作为 CascadingValue 传递给子组件。 下面的示例仅记录错误,但组件的方法可以按照应用要求的任何方式处理错误,包括通过使用多种错误处理方法。 与使用注入式服务或自定义记录器实现相比,使用组件的一个优点是,在发生错误时,级联组件可以呈现内容并应用 CSS 样式。

Error.razor

@using Microsoft.Extensions.Logging
@inject ILogger<Error> Logger

<CascadingValue Value="this">
    @ChildContent
</CascadingValue>

@code {
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    public void ProcessError(Exception ex)
    {
        Logger.LogError("Error:ProcessError - Type: {Type} Message: {Message}", 
            ex.GetType(), ex.Message);
    }
}

注意

有关 RenderFragment 的详细信息,请参阅 ASP.NET Core Razor 组件

App 组件中,用 Error 组件将 Router 组件包装起来。 这允许 Error 组件向下级联到应用中将 Error 组件作为 CascadingParameter 接收的任何组件。

App.razor

<Error>
    <Router ...>
        ...
    </Router>
</Error>

要在组件中处理错误:

  • Error 组件指定为 @code 块中的 CascadingParameter

    [CascadingParameter]
    public Error Error { get; set; }
    
  • 使用适当的异常类型在任何 catch 块中调用错误处理方法。 该示例 Error 组件只提供了一个 ProcessError 方法,但错误处理组件可以提供任意数量的错误处理方法来解决整个应用中的其他错误处理要求。

    try
    {
        ...
    }
    catch (Exception ex)
    {
        Error.ProcessError(ex);
    }
    

使用前面的示例 Error 组件和 ProcessError 方法,浏览器的开发人员工具控制台会指示捕获并记录的错误:

fail: BlazorSample.Shared.Error[0] Error:ProcessError - Type: System.NullReferenceException Message: Object reference not set to an instance of an object.

如果 ProcessError 方法直接参与呈现,例如,显示自定义错误消息栏或更改所呈现元素的 CSS 样式,请在 ProcessErrors 方法末尾调用 StateHasChanged 来重新呈现 UI。

由于本部分中的方法使用 try-catch 语句处理错误,因此在发生错误时,客户端和服务器之间的 Blazor 应用的 SignalR 连接不会中断,并且线路保持活跃状态。 任何未处理异常对于线路而言都是致命错误。 有关详细信息,请参阅有关线路如何对未经处理的异常作出反应的部分。

使用永久性提供程序记录错误信息

在发生未经处理的异常时,将异常记录到在服务容器中配置的 ILogger 实例。 默认情况下,Blazor 应用使用控制台日志记录提供程序记录到控制台输出中。 请考虑使用管理日志大小和日志轮换的提供程序登录到服务器上的某个位置(或客户端应用的后端 Web API)。 或者,应用也可以使用应用程序性能管理 (APM) 服务,如 Azure Application Insights (Azure Monitor)

说明

用于支持客户端应用的本机 Application Insights 功能和对 Google Analytics 的本机 Blazor 框架支持可能会在这些技术的未来版本中推出。 有关更多信息,请参阅在 Blazor WASM 客户端支持 App Insights (microsoft/ApplicationInsights-dotnet #2143)Web 分析和诊断(包含社区实现链接)(dotnet/aspnetcore #5461)。 同时,客户端应用可以结合使用 Application Insights JavaScript SDKJS 互操作直接将错误从客户端应用记录到 Application Insights。

在线路上运行的 Blazor 应用中开发时,应用通常会将异常的完整详细信息发送到浏览器的控制台来帮助进行调试。 在生产环境中,详细错误不会发送到客户端,但异常的完整详细信息会记录在服务器上。

必须确定要记录的事件以及已记录的事件的严重性级别。 恶意用户也许能刻意触发错误。 例如,若显示产品详细信息的组件的 URL 中提供了未知的 ProductId,则请勿记录错误中的事件。 并非所有的错误都应被视为需要记录的事件。

有关详细信息,请参阅以下文章:

‡适用于 Blazor 服务器端应用和其他服务器端 ASP.NET Core 应用,这些应用是 Blazor 的 Web API 后端应用。 客户端应用可捕获客户端上的错误信息,并将其发送到 Web API,该 API 将错误信息记录到持久日志记录提供程序。

在发生未经处理的异常时,将异常记录到在服务容器中配置的 ILogger 实例。 默认情况下,Blazor 应用使用控制台日志记录提供程序记录到控制台输出中。 考虑将日志记录到服务器上保存时间更长久的位置,方法是将错误信息发送到后端 Web API,并且该 API 使用具有日志大小管理和日志轮替功能的日志记录提供程序。 或者,后端 Web API 应用可使用应用程序性能管理 (APM) 服务(如 Azure Application Insights (Azure Monitor)†)来记录从客户端接收的错误信息。

必须确定要记录的事件以及已记录的事件的严重性级别。 恶意用户也许能刻意触发错误。 例如,若显示产品详细信息的组件的 URL 中提供了未知的 ProductId,则请勿记录错误中的事件。 并非所有的错误都应被视为需要记录的事件。

有关详细信息,请参阅以下文章:

†用于支持客户端应用的本机 Application Insights 功能和对 Google Analytics 的本机 Blazor 框架支持可能会在这些技术的未来版本中推出。 有关更多信息,请参阅在 Blazor WASM 客户端支持 App Insights (microsoft/ApplicationInsights-dotnet #2143)Web 分析和诊断(包含社区实现链接)(dotnet/aspnetcore #5461)。 同时,客户端应用可以结合使用 Application Insights JavaScript SDKJS 互操作直接将错误从客户端应用记录到 Application Insights。

‡适用于服务器端 ASP.NET Core 应用,这些应用是 Blazor 应用的 Web API 后端应用。 客户端应用捕获错误信息,并发送到 Web API,该 API 将错误信息记录到持久日志记录提供程序。

可能发生错误的位置

框架和应用代码可能会在以下任何位置触发未处理异常,相关的具体内容将在本文后续部分介绍:

组件实例化

当 Blazor 创建某组件的实例时:

  • 会调用该组件的构造函数。
  • 会调用通过 @inject 指令或 [Inject] 特性提供给组件构造函数的 DI 服务的构造函数。

已执行构造函数中或任何 [Inject] 属性的 setter 中发生错误,可能会引起未处理异常,并阻止框架对组件进行实例化。 如果应用在线路上运行,则线路出现故障。 如果构造函数逻辑可能引发异常,应用应使用 try-catch 语句捕获异常,并进行错误处理和日志记录。

生命周期方法

在组件的生命周期内,Blazor 会调用 生命周期方法 。 如果任何生命周期方法以同步或异步方式引发异常,则该异常对于 线路而言是严重异常。 若要使组件处理生命周期方法中的错误,请添加错误处理逻辑。

在下面的示例中,OnParametersSetAsync 会调用方法来获取产品:

  • ProductRepository.GetProductByIdAsync 方法中引发的异常由 try-catch 语句处理。
  • 在执行 catch 块时:
    • loadFailed 设置为 true,用于向用户显示一条错误消息。
    • 错误会被记录。
@page "/product-details/{ProductId:int?}"
@inject ILogger<ProductDetails> Logger
@inject IProductRepository Product

<PageTitle>Product Details</PageTitle>

<h1>Product Details Example</h1>

@if (details != null)
{
    <h2>@details.ProductName</h2>
    <p>
        @details.Description
        <a href="@details.Url">Company Link</a>
    </p>
    
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail? details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await Product.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string? ProductName { get; set; }
        public string? Description { get; set; }
        public string? Url { get; set; }
    }

    /*
    * Register the service in Program.cs:
    * using static BlazorSample.Components.Pages.ProductDetails;
    * builder.Services.AddScoped<IProductRepository, ProductRepository>();
    */

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }

    public class ProductRepository : IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id)
        {
            return Task.FromResult(
                new ProductDetail()
                {
                    ProductName = "Flowbee ",
                    Description = "The Revolutionary Haircutting System You've Come to Love!",
                    Url = "https://flowbee.com/"
                });
        }
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail? details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string? ProductName { get; set; }
        public string? Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail? details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string? ProductName { get; set; }
        public string? Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string ProductName { get; set; }
        public string Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string ProductName { get; set; }
        public string Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}

呈现逻辑

Razor 组件文件 (.razor) 中的声明性标记被编译到名为 BuildRenderTree 的 C# 方法中。 当组件呈现时,BuildRenderTree 会执行并构建一个数据结构,该结构描述所呈现组件的元素、文本和子组件。

呈现逻辑可能会引发异常。 例如评估了 @someObject.PropertyName,但 @someObjectnull 时,就会发生这种情况。 对于在线路上运行的 Blazor 应用,呈现逻辑引发的未经处理的异常对于应用的线路来说是严重异常。

为防止呈现逻辑中出现 NullReferenceException,请在访问其成员之前检查是否存在 null 对象。 在以下示例中,如果 person.Addressnull,则不访问 person.Address 属性:

@if (person.Address != null)
{
    <div>@person.Address.Line1</div>
    <div>@person.Address.Line2</div>
    <div>@person.Address.City</div>
    <div>@person.Address.Country</div>
}

上述代码假定 person 不是 null。 通常,代码的结构保证了呈现组件时存在对象。 在这些情况下,不需要检查呈现逻辑中是否存在 null。 在前面的示例中,由于在实例化组件时创建了 person,因此可保证存在 person,如以下示例所示:

@code {
    private Person person = new();

    ...
}

事件处理程序

使用以下内容创建事件处理程序时,客户端代码将触发 C# 代码调用:

  • @onclick
  • @onchange
  • 其他 @on... 特性
  • @bind

在这些情况下,事件处理程序代码可能会引发未经处理的异常。

如果应用调用可能因外部原因而失败的代码,请使用 try-catch 语句捕获异常,并进行错误处理和日志记录。

如果事件处理程序引发未经处理的异常(例如数据库查询失败),其未捕获且由开发人员代码处理:

  • 框架会记录异常。
  • 在线路上运行的 Blazor 应用中,异常对于应用的线路来说是严重异常。

组件处置

例如,可从 UI 中删除组件,因为用户已导航到其他页面。 当从 UI 中删除实现 System.IDisposable 的组件时,框架将调用该组件的 Dispose 方法。

如果组件的 Dispose 方法在线路上运行的 Blazor 应用中引发未经处理的异常,则该异常对于应用的线路来说是严重异常。

如果处置逻辑可能引发异常,应用应使用 try-catch 语句捕获异常,并进行错误处理和日志记录。

有关组件处置的详细信息,请参阅 ASP.NET Core Razor 组件生命周期

JavaScript 互操作

IJSRuntime 由 Blazor 框架注册。 IJSRuntime.InvokeAsync 允许 .NET 代码在用户浏览器中对 JavaScript (JS) 运行时进行异步调用。

以下条件适用于带有 InvokeAsync 的错误处理:

  • 如果无法对 InvokeAsync 进行同步调用,则会发生 .NET 异常。 例如,对 InvokeAsync 的调用可能会失败,因为不能序列化提供的自变量。 开发人员代码必须捕获异常。 如果事件处理程序或组件生命周期方法中的应用代码在线路上运行的 Blazor 应用中没有处理异常,则该异常对于应用的线路来说是严重异常。
  • 如果无法对 InvokeAsync 进行异步调用,则 .NET Task 会失败。 例如,对 InvokeAsync 的调用可能会失败,这是因为 JS 端代码会引发异常或返回完成状态为 rejectedPromise。 开发人员代码必须捕获异常。 如果使用 await 运算符,请考虑使用 try-catch 语句包装方法调用,并进行错误处理和日志记录。 否则,在线路上运行的 Blazor 应用中,失败的代码会导致未经处理的异常,这对于应用的线路来说是严重异常。
  • 默认情况下,对 InvokeAsync 的调用必须在特定时间段内完成,否则调用会超时。默认超时时间为一分钟。 超时会保护代码免受网络连接丢失的影响,或者保护永远不会发回完成消息的 JS 代码。 如果调用超时,则生成的 System.Threading.Tasks 将失败,并出现 OperationCanceledException。 捕获异常,并进行异常处理和日志记录。

同样,JS 代码可以对 [JSInvokable] 特性指示的 .NET 方法发起调用。 如果这些 .NET 方法引发未经处理的异常:

  • 在线路上运行的 Blazor 应用中,异常会被视为对应用线路严重异常。
  • JS 端 Promise 将被拒绝。

可选择在方法调用的 .NET 端或 JS 端使用错误处理代码。

有关详细信息,请参阅以下文章:

预呈现

默认情况下,Razor 组件是预先呈现的,因此它们呈现的 HTML 标记将作为用户初始 HTTP 请求的一部分返回。

在线路上运行的 Blazor 应用中,预呈现的工作方式如下:

  • 为属于同一页面的所有预呈现组件创建新的线路。
  • 生成初始 HTML。
  • 将线路视为 disconnected,直到用户浏览器与同一服务器重新建立起 SignalR 连接。 建立该连接后,将恢复线路的交互性,并更新组件的 HTML 标记。

对于预提交的客户端组件,预提交的工作方式如下:

  • 针对属于同一页的所有预呈现组件,在服务器上生成初始 HTML。
  • 在浏览器加载应用的已编译代码和 .NET 运行时后(如果尚未加载),使组件在客户端上交互。

如果组件在预呈现期间引发未经处理的异常,例如在生命周期方法或呈现逻辑中:

  • 在线路上运行的 Blazor 应用中,异常对于线路来说是严重异常。 对于预呈现的客户端组件,异常会阻止呈现组件。
  • 此异常将从 ComponentTagHelper 中的调用堆栈引发。

在正常情况下,如果预呈现失败,则继续生成和呈现组件都将没有作用,因为无法呈现工作组件。

若要容许在预呈现期间可能发生的错误,必须将错误处理逻辑置于可能引发异常的组件中。 请使用 try-catch 语句,并进行错误处理和日志记录。 请勿将 ComponentTagHelper 包装在 try-catch 语句中,而是将错误处理逻辑放在由 ComponentTagHelper 呈现的组件中。

高级方案

递归呈现

组件能以递归方式嵌套。 这适用于表示递归数据结构。 例如,TreeNode 组件可以为节点的每个子级呈现更多 TreeNode 组件。

以递归方式呈现时,请避免采用会导致无限递归的编码模式:

  • 请勿以递归方式呈现包含循环的数据结构。 例如,请勿呈现其子级包含其自身的树节点。
  • 请勿创建包含循环的布局链。 例如,请勿创建布局为其本身的布局。
  • 请勿允许最终用户通过恶意数据输入或 JavaScript 互操作调用违反递归固定协定(规则)。

呈现过程中的无限循环:

  • 会导致呈现过程永久地继续下去。
  • 相当于创建不终止的循环。

在这些情况下,Blazor 会失败,通常会尝试:

  • 在操作系统允许范围内无限期地消耗 CPU 时间。
  • 消耗不限量的内存。 消耗不限量的内存相当于不终止的循环在每次迭代时向集合添加条目的情况。

若要避免无限递归模式,请确保递归呈现代码包含合适的停止条件。

自定义呈现器树逻辑

大多数 Razor 组件都实现为 Razor 组件文件 (.razor),并由框架编译以生成在 RenderTreeBuilder 上运行的逻辑,从而呈现其输出。 但是,开发人员可使用程序 C# 代码手动实现 RenderTreeBuilder 逻辑。 有关详细信息,请参阅 ASP.NET Core Blazor 高级方案(呈现器树构造)

警告

手动呈现树生成器逻辑被视为一种高级且不安全的方案,不建议开发人员在常规组件开发工作中采用。

如果编写 RenderTreeBuilder 代码,开发人员必须保证代码的正确性。 例如,开发人员必须确保:

如果手动呈现树生成器逻辑不正确,可能会出现任意未定义的行为(包括崩溃、应用或服务器挂起)以及安全漏洞。

请知悉:手动呈现树生成器逻辑的复杂程度和危险程度与手动编写程序集代码或 Microsoft 中间语言 (MSIL) 指令是一样的。

其他资源

†适用于客户端 Blazor 应用用于日志记录的后端 ASP.NET Core Web API 应用。