有没有办法全局捕获 Blazor 单页应用程序中的所有未处理错误?



我希望能够在一个地方捕获所有未经处理的异常,以构建 Blazor 单页应用程序。 就像在WPF应用程序中使用"Current.DispatcherUnhandledException"一样。

这个问题专门涉及客户端(webassembly(异常处理。 我正在使用 Blazor 版本 3.0.0-preview8.19405.7

我一直在寻找解决方案,但似乎不存在。在 Microsoft 的文档 (https://learn.microsoft.com/en-us/aspnet/core/blazor/handle-errors?view=aspnetcore-3.0( 中,有一个可能发生错误的位置列表,以及如何处理每个错误。 它相信必须有一种更防弹的方式来捕捉所有人。

在.NET 6中,有一个名为ErrorBoundary的组件。

简单的例子:

<ErrorBoundary>
@Body
</ErrorBoundary>

高级示例:

<ErrorBoundary>
<ChildContent>
@Body
</ChildContent>
<ErrorContent Context="ex">
@{ OnError(@ex); } @*calls custom handler*@
<p>@ex.Message</p> @*prints exeption on page*@
</ErrorContent>
</ErrorBoundary>

对于全局异常处理,我认为这是一个选项: 创建CustomErrorBoundary(继承ErrorBoundary(并覆盖OnErrorAsync(Exception exception)

这是CustomErrorBoundary的样本。

有用的链接

  • 官方文档
  • .NET 6 预览版 4 博客文章中的一些信息。
  • 在dotnet repo中测试错误边界(很棒的示例(。
  • 在 dotnet 存储库上的 PR。
  • ErrorBoundary的简单用法(优酷(

这适用于 v3.2+

using Microsoft.Extensions.Logging;
using System;
namespace UnhandledExceptions.Client
{
public interface IUnhandledExceptionSender
{
event EventHandler<Exception> UnhandledExceptionThrown;
}
public class UnhandledExceptionSender : ILogger, IUnhandledExceptionSender
{
public event EventHandler<Exception> UnhandledExceptionThrown;
public IDisposable BeginScope<TState>(TState state)
{
return null;
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
Exception exception, Func<TState, Exception, string> formatter)
{
if (exception != null)
{
UnhandledExceptionThrown?.Invoke(this, exception);
}
}
}
public class UnhandledExceptionProvider : ILoggerProvider
{
UnhandledExceptionSender _unhandledExceptionSender;

public UnhandledExceptionProvider(UnhandledExceptionSender unhandledExceptionSender)
{
_unhandledExceptionSender = unhandledExceptionSender;
}
public ILogger CreateLogger(string categoryName)
{
return new UnhandledExceptionLogger(categoryName, _unhandledExceptionSender);
}
public void Dispose()
{            
}
public class UnhandledExceptionLogger : ILogger
{
private readonly string _categoryName;
private readonly UnhandledExceptionSender _unhandeledExceptionSender;
public UnhandledExceptionLogger(string categoryName, UnhandledExceptionSender unhandledExceptionSender)
{
_unhandeledExceptionSender = unhandledExceptionSender;
_categoryName = categoryName;
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
// Unhandled exceptions will call this method
// Blazor already logs unhandled exceptions to the browser console
// but, one could pass the exception to the server to log, this is easily done with serilog
Serilog.Log.Fatal(exception, exception.Message);                             
}
public IDisposable BeginScope<TState>(TState state)
{
return new NoopDisposable();
}
private class NoopDisposable : IDisposable
{
public void Dispose()
{  
}
}
}
}
}

将此添加到程序.cs

var unhandledExceptionSender = new UnhandledExceptionSender();
var unhandledExceptionProvider = new UnhandledExceptionProvider(unhandledExceptionSender);
builder.Logging.AddProvider(unhandledExceptionProvider);
builder.Services.AddSingleton<IUnhandledExceptionSender>(unhandledExceptionSender);

下面是实现此解决方案的示例项目。

目前没有中心位置来捕获和处理客户端异常。

以下是史蒂夫·桑德森(Steve Sanderson(对此的引用:

因此,总的来说,每个组件都必须处理自己的错误。如果 你想要,你可以制作你自己的错误处理组件库 继承自所有生命周期方法,并尝试/捕获所有生命周期方法, 并拥有自己的逻辑来显示"哦,亲爱的对不起,我死了"UI 如果出现任何问题,该组件。但这不是 今天的框架。

我希望这种情况将来会改变,我相信应该支持框架。

对于 .NET 5 Blazor服务器端,这篇文章创建自己的日志记录提供程序以记录到 .NET Core 中的文本文件对我有用。就我而言,我已经对其进行了调整,以捕获未经处理的异常以写入 Azure 存储表。

public class ExceptionLoggerOptions
{
public virtual bool Enabled { get; set; }
}
[ProviderAlias("ExceptionLogger")]
public class ExceptionLoggerProvider : ILoggerProvider
{
public readonly ExceptionLoggerOptions Options;
public ExceptionLoggerProvider(IOptions<ExceptionLoggerOptions> _options)
{
Options = _options.Value;
}
public ILogger CreateLogger(string categoryName)
{
return new ExceptionLogger(this);
}
public void Dispose()
{
}
}
public class ExceptionLogger : ILogger
{
protected readonly ExceptionLoggerProvider _exceptionLoggerProvider;
public ExceptionLogger([NotNull] ExceptionLoggerProvider exceptionLoggerProvider)
{
_exceptionLoggerProvider = exceptionLoggerProvider;
}
public IDisposable BeginScope<TState>(TState state)
{
return null;
}
public bool IsEnabled(LogLevel logLevel)
{
return logLevel == LogLevel.Error;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (false == _exceptionLoggerProvider.Options.Enabled) return;
if (null == exception) return;
if (false == IsEnabled(logLevel)) return;
var record = $"{exception.Message}"; // string.Format("{0} {1} {2}",  logLevel.ToString(), formatter(state, exception), exception?.StackTrace);
// Record exception into Azure Table
}
}
public static class ExceptionLoggerExtensions
{
public static ILoggingBuilder AddExceptionLogger(this ILoggingBuilder builder, Action<ExceptionLoggerOptions> configure)
{
builder.Services.AddSingleton<ILoggerProvider, ExceptionLoggerProvider>();
builder.Services.Configure(configure);
return builder;
}
}
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStaticWebAssets().UseStartup<Startup>();
}).ConfigureLogging((hostBuilderContext, logging) =>
{
logging.AddExceptionLogger(options => { options.Enabled = true; });
});

要访问异常,您可以使用内置的 ErrorBoundary 组件并使用 Context 属性访问 RenderFragment

<ErrorBoundary> 
<ChildContent>
@Body   
</ChildContent>
<ErrorContent Context="ex">
<h1 style="color: red;">Oops... error occured: @ex.Message </h1>
</ErrorContent>
</ErrorBoundary>

这将捕获所有错误。

App.razor

<ErrorBoundary>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</ErrorBoundary>

如果要自定义消息:

<ErrorBoundary>
<ChildContent>
... App
</ChildContent>
<ErrorContent Context="errorException">
<div class="blazor-error-boundary">
Boom!
</div>
</ErrorContent>
</ErrorBoundary>

在上面的例子中使用 CustomErrorBoundary 和 mudblazor。我制作了一个自定义错误边界组件,该组件在小吃栏弹出窗口中显示错误。

万一其他人想这样做。

CustomErrorBoundary.razor

@inherits ErrorBoundary
@inject ISnackbar Snackbar
@if (CurrentException is null)
{
@ChildContent
}
else if (ErrorContent is not null)
{
@ErrorContent(CurrentException)
}
else
{
@ChildContent
@foreach (var exception in receivedExceptions)
{
Snackbar.Add(@exception.Message, Severity.Error);
}
Recover();
}
@code {
List<Exception> receivedExceptions = new();
protected override Task OnErrorAsync(Exception exception)
{
receivedExceptions.Add(exception);
return base.OnErrorAsync(exception);
}
public new void Recover()
{
receivedExceptions.Clear();
base.Recover();
}
}

MainLayout.razor

@inherits LayoutComponentBase
@inject ISnackbar Snackbar
<MudThemeProvider IsDarkMode="true"/>
<MudDialogProvider />
<MudSnackbarProvider />
<MudLayout>
<MudAppBar>
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@((e) => DrawerToggle())" />
</MudAppBar>
<MudDrawer @bind-Open="@_drawerOpen">
<NavMenu/>
</MudDrawer>
<MudMainContent>
<CustomErrorBoundary>
@Body
</CustomErrorBoundary>
</MudMainContent>
</MudLayout>
@code {
bool _drawerOpen = true;
private void DrawerToggle()
{
_drawerOpen = !_drawerOpen;
}
}

在当前的 Blazor Web Assembly 版本中,所有未经处理的异常都捕获在内部类中并写入Console.Error。目前没有办法以不同的方式捕获它们,但Rémi Bourgarel展示了一种能够记录它们和/或采取自定义操作的解决方案。请参阅雷米的博客。

简单的记录器将它们路由到 ILogger:

public class UnhandledExceptionLogger : TextWriter
{
private readonly TextWriter _consoleErrorLogger;
private readonly ILogger _logger;
public override Encoding Encoding => Encoding.UTF8;
public UnhandledExceptionLogger(ILogger logger)
{
_logger = logger;
_consoleErrorLogger = Console.Error;
Console.SetError(this);
}
public override void WriteLine(string value)
{
_logger.LogCritical(value);
// Must also route thru original logger to trigger error window.
_consoleErrorLogger.WriteLine(value);
}
}

现在在程序中.cs添加builder.Services.AddLogging...并添加:

builder.Services.AddSingleton<UnhandledExceptionLogger>();
...
// Change end of Main() from await builder.Build().RunAsync(); to:
var host = builder.Build();
// Make sure UnhandledExceptionLogger is created at startup:
host.Services.GetService<UnhandledExceptionLogger>();
await host.RunAsync();

我发现捕获 Blazor 网络程序集的所有错误(包括没有实现 try catch 的异步无效(的唯一方法是 以注册到 AppDomain.CurrentDomain.UnhandledException 事件

in MainLayout.razor

@code (
protected override async void OnInitialized()
{
base.OnInitialized();
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
//Call your class that handles error
};
}
}

MainLayout.razor

<ErrorBoundary @ref="errorBoundary">
<ChildContent>
@Body
</ChildContent>
<ErrorContent Context="ex">
@Body
@{
if (ex.Message.Contains("status code"))
Console.WriteLine(ex.Message);
else Snackbar.Add(@ex.Message, Severity.Error);
}
</ErrorContent>
</ErrorBoundary>

MainLayout.razor.cs

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

注释 blazingor-error-uidiv inindex.html

<!-- <div id="blazor-error-ui" dir="rtl">
an error occurred!
<a href="" class="reload">refresh</a>
<a class="dismiss">🗙</a>
</div> -->

最新更新