处理 ASP.NET Core Blazor 应用中的错误
注意
此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本。
警告
此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本。
本文介绍 Blazor 如何管理未经处理的异常以及如何开发用于检测和处理错误的应用。
开发过程中的错误详细信息
当 Blazor 应用在开发过程中运行不正常时,从该应用接收详细的错误信息有助于故障排除和修复问题。 出现错误时,Blazor 应用会在屏幕底部显示一个浅黄色条框:
- 在开发过程中,这个条框会将你定向到浏览器控制台,你可在其中查看异常。
- 在生产过程中,这个条框会通知用户发生了错误,并建议刷新浏览器。
此错误处理体验的 UI 属于 Blazor 项目模板。 并非所有版本的 Blazor 项目模板都使用 data-nosnippet
属性向浏览器发出信号,而不缓存错误 UI 的内容,但所有版本的 Blazor 文档都应用该属性。
在 Blazor Web App 中,在 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 App。
本部分适用于 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
环境变量和值 true
将 DetailedErrors 配置键设置为 true
。
警告
始终避免向 Internet 上的客户端公开错误信息,这会带来安全风险。
Razor 组件服务器端呈现的详细错误
本部分适用于 Blazor Web App。
使用 RazorComponentsServiceOptions.DetailedErrors 选项可控制生成有关 Razor 组件服务器端呈现错误的详细信息。 默认值为 false
。
以下示例启用详细错误:
builder.Services.AddRazorComponents(options =>
options.DetailedErrors = builder.Environment.IsDevelopment());
警告
仅在 Development
环境中启用详细错误。 详细错误可能包含有关恶意用户可在攻击中使用的应用的敏感信息。
前面的示例通过根据 IsDevelopment 返回的值设置 DetailedErrors 的值来提供一定程度的安全性。 当应用位于 Development
环境中时,DetailedErrors 会设置为 true
。 这种方法并非万无一失,因为可以在 Development
环境中的公共服务器上托管生产应用。
在开发人员代码中管理未经处理的异常
若要在出现错误后继续运行应用,该应用必须具备错误处理逻辑。 本文后面的部分将介绍未经处理的异常出现的潜在原因。
在生产环境中,不要在 UI 中呈现框架异常消息或堆栈跟踪信息。 呈现异常消息或堆栈跟踪信息可能导致:
- 向最终用户公开敏感信息。
- 帮助恶意用户发现应用中可能会危及应用、服务器或网络安全的弱点。
线路的未经处理的异常
本部分适用于通过线路运行的服务器端应用。
启用了服务器交互的 Razor 组件在服务器上是有状态的。 当用户与服务器上的组件交互时,他们会保持与服务器的连接,称为线路。 线路包含活动组件实例,以及状态的许多其他方面,例如:
- 最新呈现的组件输出。
- 可由客户端事件触发的事件处理委托的当前集合。
如果用户在多个浏览器标签页中打开应用,则用户就会创建多条独立线路。
Blazor 将大部分未经处理的异常视为发生该异常的线路的严重异常。 如果线路由于未经处理的异常而终止,则用户只能重新加载页面来创建新线路,从而继续与应用进行交互。 终止的线路以外的其他线路(即其他用户或其他浏览器标签页的线路)不会受到影响。 此场景类似于出现故障的桌面应用。 出现故障的应用必须重新启动,但其他应用不受影响。
当发生未处理异常时,框架会出于以下原因终止线路:
- 未经处理的异常通常会将线路置于未定义状态。
- 发生未经处理的异常后,应用可能无法正常运行。
- 如果线路继续保持未定义状态,应用中可能会出现安全漏洞。
全局异常处理
有关全局处理异常的方式,请参阅以下部分:
- 错误边界:适用于所有 Blazor 应用。
- 替代性全局异常处理:适用于采用全局交互式呈现模式的 Blazor Server、Blazor WebAssembly 和 Blazor Web App(8.0 或更高版本)。
错误边界
错误边界提供了一种用于处理异常的便捷方法。 ErrorBoundary 组件:
- 在未发生错误时呈现其子内容。
- 在错误边界内的任何组件引发未经处理的异常时呈现错误 UI。
若要定义错误边界,请使用 ErrorBoundary 组件包装一个或多个其他组件。 错误边界管理它包装的组件引发的未经处理的异常。
<ErrorBoundary>
...
</ErrorBoundary>
要以全局方式实现错误边界,请在应用主布局的正文内容周围添加边界。
在 MainLayout.razor
中:
<article class="content px-4">
<ErrorBoundary>
@Body
</ErrorBoundary>
</article>
在错误边界仅应用于静态 MainLayout
组件的 Blazor Web App 中,该边界仅在静态服务器端渲染(静态 SSR)期间处于活动状态。 边界不会因为组件层次结构的下一个组件是交互式的而激活。
交互式呈现模式不能应用于 MainLayout
组件,因为组件的 Body
参数是一个 RenderFragment 委托,该委托是任意代码,无法序列化。 若要为 MainLayout
组件和组件层次结构中更下方的 rest 组件广泛启用交互性,应用必须采用全局交互式呈现模式,方法是将交互式呈现模式应用于应用的根组件(通常是 App
组件)中的 HeadOutlet
和 Routes
组件实例。 以下示例全局采用交互式服务器 (InteractiveServer
) 呈现模式。
在 Components/App.razor
中:
<HeadOutlet @rendermode="InteractiveServer" />
...
<Routes @rendermode="InteractiveServer" />
如果不想启用全局交互,请将错误边界放置在组件层次结构中的更下方。 需要记住的重要概念是,无论在何处放置错误边界:
- 如果放置错误边界的组件不是交互式的,则错误边界只能在静态 SSR 期间在服务器上激活。 例如,当组件生命周期方法中引发错误时,边界可以激活,但对于组件内的用户交互触发的事件(例如按钮单击处理程序引发的错误),边界则不能激活。
- 如果放置错误边界的组件是交互式的,则错误边界可以为它包装的交互式组件而激活。
注意
上述注意事项与独立 Blazor WebAssembly 应用无关,因为 Blazor WebAssembly 应用的客户端呈现 (CSR) 是完全交互式的。
请考虑以下示例,其中一个嵌入式计数器组件引发的异常是由 Home
组件(采用交互式呈现模式)中的错误边界捕获的。
EmbeddedCounter.razor
:
<h1>Embedded Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
if (currentCount > 5)
{
throw new InvalidOperationException("Current count is too big!");
}
}
}
Home.razor
:
@page "/"
@rendermode InteractiveServer
<PageTitle>Home</PageTitle>
<h1>Home</h1>
<ErrorBoundary>
<EmbeddedCounter />
</ErrorBoundary>
请考虑以下示例,其中一个嵌入式计数器组件引发的异常是由 Home
组件中的错误边界捕获的。
EmbeddedCounter.razor
:
<h1>Embedded Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
if (currentCount > 5)
{
throw new InvalidOperationException("Current count is too big!");
}
}
}
Home.razor
:
@page "/"
<PageTitle>Home</PageTitle>
<h1>Home</h1>
<ErrorBoundary>
<EmbeddedCounter />
</ErrorBoundary>
如果 currentCount
超过 5,则会引发未处理的异常:
- 错误记录正常 (
System.InvalidOperationException: Current count is too big!
)。 - 异常由错误边界处理。
- 默认错误 UI 由错误边界呈现。
ErrorBoundary 组件使用 blazor-error-boundary
CSS 类来呈现一个空的 <div>
元素作为其错误内容。 默认 UI 的颜色、文本和图标是在 wwwroot
文件夹内应用的样式表中定义的,因此你可以自由自定义错误 UI。
若要更改默认错误内容,请执行以下操作:
- 将错误边界的组件包装在 ChildContent 属性中。
- 将 ErrorContent 属性设置为错误内容。
以下示例包装 EmbeddedCounter
组件并提供自定义错误内容:
<ErrorBoundary>
<ChildContent>
<EmbeddedCounter />
</ChildContent>
<ErrorContent>
<p class="errorUI">😈 A rotten gremlin got us. Sorry!</p>
</ErrorContent>
</ErrorBoundary>
对于前面的示例,应用的样式表可能包括一个用于设置内容样式的 errorUI
CSS 类。 错误内容是从没有块级元素的 ErrorContent 属性呈现的。 块级元素(如分区 (<div>
) 或段落 (<p>
) 元素)可以包装错误内容标记,但这不是必需的。
可以选择使用 ErrorContent 的上下文 (@context
) 获取错误数据:
<ErrorContent>
@context.HelpLink
</ErrorContent>
ErrorContent 还可以为上下文命名。 在以下示例中,将上下文命名为 exception
:
<ErrorContent Context="exception">
@exception.HelpLink
</ErrorContent>
警告
始终避免向 Internet 上的客户端公开错误信息,这会带来安全风险。
如果错误边界是在应用的布局中定义的,则无论用户在错误发生后导航到哪个页面,都会看到该错误 UI。 建议在大多数场景下缩小错误边界的范围。 如果设置了较广泛的错误边界,则可以通过调用错误边界的 Recover 方法,在后续页面导航事件中将其重置为非错误状态。
在 MainLayout.razor
中:
- 为 ErrorBoundary 添加字段,以使用
@ref
属性指令捕获对其的引用。 - 在
OnParameterSet
生命周期方法中,在用户导航到其他组件时,可以在错误边界通过 Recover 触发恢复,以清除错误。
...
<ErrorBoundary @ref="errorBoundary">
@Body
</ErrorBoundary>
...
@code {
private ErrorBoundary? errorBoundary;
protected override void OnParametersSet()
{
errorBoundary?.Recover();
}
}
为了避免无限循环,其中恢复只会重新呈现再次引发错误的组件,请勿从呈现逻辑调用 Recover。 仅在以下情况下呼叫 Recover:
- 用户执行 UI 手势,例如选择按钮以指示其想要重试过程,或者当用户导航到新组件时。
- 执行的附加逻辑也会清除异常。 重新呈现组件时,错误不会再次出现。
以下示例允许用户使用按钮从异常中恢复:
<ErrorBoundary @ref="errorBoundary">
<ChildContent>
<EmbeddedCounter />
</ChildContent>
<ErrorContent>
<div class="alert alert-danger" role="alert">
<p class="fs-3 fw-bold">😈 A rotten gremlin got us. Sorry!</p>
<p>@context.HelpLink</p>
<button class="btn btn-info" @onclick="_ => errorBoundary?.Recover()">
Clear
</button>
</div>
</ErrorContent>
</ErrorBoundary>
@code {
private ErrorBoundary? errorBoundary;
}
还可以通过重写 OnErrorAsync 来创建 ErrorBoundary 子类以进行自定义处理。 以下示例仅记录错误,但可以实现所需的任何错误处理代码。 如果代码等待异步任务,则可以删除返回 CompletedTask 的行。
CustomErrorBoundary.razor
:
@inherits ErrorBoundary
@inject ILogger<CustomErrorBoundary> Logger
@if (CurrentException is null)
{
@ChildContent
}
else if (ErrorContent is not null)
{
@ErrorContent(CurrentException)
}
@code {
protected override Task OnErrorAsync(Exception ex)
{
Logger.LogError(ex, "😈 A rotten gremlin got us. Sorry!");
return Task.CompletedTask;
}
}
前面的示例也可以作为类实现。
CustomErrorBoundary.cs
:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
namespace BlazorSample;
public class CustomErrorBoundary : ErrorBoundary
{
[Inject]
ILogger<CustomErrorBoundary> Logger { get; set; } = default!;
protected override Task OnErrorAsync(Exception ex)
{
Logger.LogError(ex, "😈 A rotten gremlin got us. Sorry!");
return Task.CompletedTask;
}
}
组件中使用的上述任一实现:
<CustomErrorBoundary>
...
</CustomErrorBoundary>
备用全局异常处理
本部分中所述的方法适用于采用全局交互式呈现模式(InteractiveServer
、InteractiveWebAssembly
或 InteractiveAuto
)的 Blazor Server、Blazor WebAssembly 和 Blazor Web App。 此方法不适用于采用按页/组件呈现模式或静态服务器端呈现(静态 SSR)的 Blazor Web App,因为此方法依赖于 CascadingValue
/CascadingParameter
,后者不能跨呈现模式边界或与采用静态 SSR 的组件配合使用。
可将自定义错误组件作为 CascadingValue
传递给子组件,来代替使用错误边界 (ErrorBoundary)。 与使用注入式服务或自定义记录器实现相比,使用组件的一个优点是,在发生错误时,级联组件可以呈现内容并应用 CSS 样式。
下面的 ProcessError
组件示例仅记录错误,但组件的方法可以按照应用要求的任何方式处理错误,包括通过使用多种错误处理方法。
ProcessError.razor
:
@inject ILogger<ProcessError> Logger
<CascadingValue Value="this">
@ChildContent
</CascadingValue>
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
public void LogError(Exception ex)
{
Logger.LogError("ProcessError.LogError: {Type} Message: {Message}",
ex.GetType(), ex.Message);
// Call StateHasChanged if LogError directly participates in
// rendering. If LogError only logs or records the error,
// there's no need to call StateHasChanged.
//StateHasChanged();
}
}
注意
有关 RenderFragment 的详细信息,请参阅 ASP.NET Core Razor 组件。
在 Blazor Web App 中使用此方法时,请打开 Routes
组件并使用 ProcessError
组件包装 Router 组件 (<Router>...</Router>
)。 这允许 ProcessError
组件向下级联到应用中将 ProcessError
组件作为 CascadingParameter
接收的任何组件。
在 Routes.razor
中:
<ProcessError>
<Router ...>
...
</Router>
</ProcessError>
在 Blazor Server 或 Blazor WebAssembly 应用中使用此方法时,请打开 App
组件,并使用 ProcessError
组件包装 Router 组件 (<Router>...</Router>
)。 这允许 ProcessError
组件向下级联到应用中将 ProcessError
组件作为 CascadingParameter
接收的任何组件。
在 App.razor
中:
<ProcessError>
<Router ...>
...
</Router>
</ProcessError>
要在组件中处理错误:
将
ProcessError
组件指定为@code
块中的CascadingParameter
。 在基于 Blazor 项目模板的应用的示例Counter
组件中,添加以下ProcessError
属性:[CascadingParameter] public ProcessError? ProcessError { get; set; }
使用适当的异常类型在任何
catch
块中调用错误处理方法。 该示例ProcessError
组件只提供了一个LogError
方法,但错误处理组件可以提供任意数量的错误处理方法来解决整个应用中的其他错误处理要求。 以下Counter
组件@code
块示例包括ProcessError
级联参数,并在计数大于 5 时捕获异常进行日志记录:@code { private int currentCount = 0; [CascadingParameter] public ProcessError? ProcessError { get; set; } private void IncrementCount() { try { currentCount++; if (currentCount > 5) { throw new InvalidOperationException("Current count is over five!"); } } catch (Exception ex) { ProcessError?.LogError(ex); } } }
记录的错误:
fail: {COMPONENT NAMESPACE}.ProcessError[0]
ProcessError.LogError: System.InvalidOperationException Message: Current count is over five!
如果 LogError
方法直接参与呈现,例如,显示自定义错误消息栏或更改所呈现元素的 CSS 样式,请在 LogError
方法末尾调用 StateHasChanged
来重新呈现 UI。
由于本部分中的方法使用 try-catch
语句处理错误,因此在发生错误时,客户端和服务器之间应用的 SignalR 连接不会中断,并且线路保持活跃状态。 其他未处理异常对于线路来说仍然是严重异常。 有关详细信息,请参阅有关线路如何对未经处理的异常作出反应的部分。
应用可以使用错误处理组件作为级联值来集中处理错误。
以下 ProcessError
组件将自身作为 CascadingValue
传递给子组件。 下面的示例仅记录错误,但组件的方法可以按照应用要求的任何方式处理错误,包括通过使用多种错误处理方法。 与使用注入式服务或自定义记录器实现相比,使用组件的一个优点是,在发生错误时,级联组件可以呈现内容并应用 CSS 样式。
ProcessError.razor
:
@using Microsoft.Extensions.Logging
@inject ILogger<ProcessError> Logger
<CascadingValue Value="this">
@ChildContent
</CascadingValue>
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
public void LogError(Exception ex)
{
Logger.LogError("ProcessError.LogError: {Type} Message: {Message}",
ex.GetType(), ex.Message);
}
}
注意
有关 RenderFragment 的详细信息,请参阅 ASP.NET Core Razor 组件。
在 App
组件中,用 ProcessError
组件将 Router 组件包装起来。 这允许 ProcessError
组件向下级联到应用中将 ProcessError
组件作为 CascadingParameter
接收的任何组件。
App.razor
:
<ProcessError>
<Router ...>
...
</Router>
</ProcessError>
要在组件中处理错误:
将
ProcessError
组件指定为@code
块中的CascadingParameter
:[CascadingParameter] public ProcessError ProcessError { get; set; }
使用适当的异常类型在任何
catch
块中调用错误处理方法。 该示例ProcessError
组件只提供了一个LogError
方法,但错误处理组件可以提供任意数量的错误处理方法来解决整个应用中的其他错误处理要求。try { ... } catch (Exception ex) { ProcessError.LogError(ex); }
使用前面的示例 ProcessError
组件和 LogError
方法,浏览器的开发人员工具控制台会指示捕获并记录的错误:
fail: {COMPONENT NAMESPACE}.Shared.ProcessError[0]
ProcessError.LogError: System.NullReferenceException Message: Object reference not set to an instance of an object.
如果 LogError
方法直接参与呈现,例如,显示自定义错误消息栏或更改所呈现元素的 CSS 样式,请在 LogError
方法末尾调用 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 SDK 和 JS 互操作直接将错误从客户端应用记录到 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 SDK 和 JS 互操作直接将错误从客户端应用记录到 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?}"
@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
,但 @someObject
为 null
时,就会发生这种情况。 对于在线路上运行的 Blazor 应用,呈现逻辑引发的未经处理的异常对于应用的线路来说是严重异常。
为防止呈现逻辑中出现 NullReferenceException,请在访问其成员之前检查是否存在 null
对象。 在以下示例中,如果 person.Address
为 null
,则不访问 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 端代码会引发异常或返回完成状态为
rejected
的Promise
。 开发人员代码必须捕获异常。 如果使用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 代码,开发人员必须保证代码的正确性。 例如,开发人员必须确保:
- 对 OpenElement 和 CloseElement 的调用已正确均衡。
- 仅将特性添加到正确的位置。
以及安全漏洞。不正确的手动呈现树生成器逻辑会导致任意未定义的行为,包括崩溃、应用或服务器停止响应以及安全漏洞。
请知悉:手动呈现树生成器逻辑的复杂程度和危险程度与手动编写程序集代码或 Microsoft 中间语言 (MSIL) 指令是一样的。
其他资源
†适用于客户端 Blazor 应用用于日志记录的后端 ASP.NET Core Web API 应用。