有没有一种方法可以在 Blazor 单页应用程序中全局捕获所有未处理的错误?

41

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

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

我一直在寻找解决方案,但似乎它并不存在。在微软的文档 (https://learn.microsoft.com/en-us/aspnet/core/blazor/handle-errors?view=aspnetcore-3.0) 中列出了错误可能发生的位置,并提供了每个位置的处理方法。我认为一定有更可靠的方法来捕获所有异常。


Blazor有两种主要的版本:服务器端和客户端(WebAssembly)。答案可能不同。你想使用哪一个? - H H
仅客户端。我在考虑使用WebAssembly创建单页应用程序。 我做了一些研究,似乎更多的异常处理示例是在服务器端。 我将编辑我的问题,使其更加精确。 - TroelsGP
相关(仅相关,不重复)https://dev59.com/JLTma4cB1Zd3GeqP9qIw#56872615 - dani herrera
顺便说一下,我正在使用 Blazor 版本 3.0.0-preview8.19405.7。我会更新我的问题并加入这些信息。 - TroelsGP
10个回答

33

.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的示例。

有用的链接


3
如果异常是在服务的构造函数中引发的,那么对我来说这并不起作用。 - clamchoda
4
据我所知,ErrorBoundary似乎有一些未经充分记录的限制。就像@clamchoda提到的那样,异步方法也无法被捕获。更多信息请参见:https://github.com/dotnet/aspnetcore/issues/27716#issuecomment-732115003。 - user160357
3
Context="ex" 是我之前遗漏的部分,因为它允许您访问异常详细信息。微软文档目前缺少这些信息。谢谢! - deadlydog
ErrorBoundaries存在一个错误,如果有多个异常,它们将无法正常工作。这就是为什么建议只在“小”范围内使用它们,而不是用于全局捕获的原因。请参见:https://github.com/dotnet/aspnetcore/issues/39814 - somedotnetguy

13

这适用于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()
                {  
                }
            }
        }
    }
}
将此内容添加到 Program.cs 中。
var unhandledExceptionSender = new UnhandledExceptionSender();
var unhandledExceptionProvider = new UnhandledExceptionProvider(unhandledExceptionSender);
builder.Logging.AddProvider(unhandledExceptionProvider);
builder.Services.AddSingleton<IUnhandledExceptionSender>(unhandledExceptionSender);

这里有一个示例项目,实现了这个解决方案。


3
谢谢,Scotty!对于那些希望自定义UI显示而不是依赖于index.html中的“blazor-error-ui”的人来说,可以通过依赖注入将IJSRuntime传递到UnhandledExceptionSender中,然后再传递到UnhandledExceptionLogger中。这允许使用JavaScript显示自定义错误显示。 - Jax
嗨@Jax,你有这方面的例子吗? - Schoof
抱歉耽搁了。今天才看到你的消息,@schoof。关于我的前一条评论中的依赖注入,我做了一些调整。 我的工作示例类似于这样。 在 program.cs 中,将服务提供程序分配给静态属性,例如: WebAssemblyHost host = builder.Build(); SomeClass.ServiceProvider = host.Services;
在 Log<TState> 方法中的某个地方 IJSRuntime jSRuntime = (IJSRuntime)SomeClass.ServiceProvider.GetService(typeof(IJSRuntime)); jSRuntime.InvokeVoidAsync("SomeJavascriptErrorMethod", someErrorMessage);
- Jax
1
其他备注:- 1)对于从异步方法抛出的异常,Log <TState> 方法似乎不会对异步 void 方法中的异常产生影响,但可在异步 Task 方法中运行。2)尝试使用其他方法在 Blazor 中显示消息(例如组件),但失败了 - 可能是因为电路被断开。 - Jax
@Jax 说得好!更多信息请参见 https://github.com/dotnet/aspnetcore/issues/27716#issuecomment-729051853 - user160357
3
我们现在已经到了2022年,.NET 7也快要发布了,是否有更好的处理方式呢? - Schoof

10

目前没有集中的地方来捕获和处理客户端异常。

这里是 Steve Sanderson 对此的评论

因此,每个组件必须处理其自己的错误。如果您想要,可以创建自己的 ErrorHandlingComponentBase 继承,并在所有的生命周期方法周围放置 try/catch,并针对该组件编写自己的逻辑,以显示“哦,糟糕,抱歉,我挂了”UI,如果出现任何问题。但这不是今天框架的一个功能。

我希望这将来会改变,并且我认为支持应该内置在框架中。


2
这方面有任何进展吗?看起来是一个非常不错的功能。 - Bjorg
7
如何在基类中将 try/catch 放置在子类的生命周期方法周围? - Vencovsky
@Vencovsky 你解决了吗?现在我也需要它 :P - Efe Zaladin
@Vencovsky,你需要创建一个继承自ErrorHandlingComponentBase的组件,它只是ComponentBase的包装类,所有生命周期方法都被重写为try/catch块。因此,当你的自定义组件调用OnInitialed时,实际上会调用带有try/catch块的包装方法。 - user160357
@user160357,你不应该像调用 OnInitialed 方法那样去调用方法,而是要通过 override 来重写它们。所以那样是行不通的。 - symbiont
2023有任何更新吗? - niico

7
对于.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; });
    });

你知道在Log方法中是否有一种访问程序、其组件、会话等的方式吗? - Rye bread
3
这不起作用 - 我仍然有异常,其中包括“System.Exception类型的异常在ServerApp.dll中发生,但未在用户代码中处理”。 - Ulterior

3
在上面的例子中使用CustomErrorBoundary和mudblazor,我制作了一个自定义错误边界组件,它会在一个snackbar弹出窗口中显示错误信息。
如果其他人想要实现这一点,可以参考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;
    }
}

3
这将捕获所有错误。 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>

2

1
在当前的Blazor WebAssembly版本中,所有未处理的异常都被捕获在一个内部类中,并写入到Console.Error中。目前没有其他方式来捕获它们,但Rémi Bourgarel提供了一种解决方案,可以记录它们和/或采取自定义操作。请参见Remi's blog
将它们路由到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);
    }
}

现在在Program.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();

2
Remi的博客上的一条评论指出,由于内部实现已更改,此方法在v3.2+中不再适用:( - CodeThief
这个解决方案在2023年仍然有效吗? - TheLegendaryCopyCoder

0
我发现在 Blazor WebAssembly 中捕获所有错误(包括未实现 try catch 的 Async void)的唯一方法是注册到 AppDomain.CurrentDomain.UnhandledException 事件。
在 MainLayout.razor 中实现。
@code (

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

0

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();
}

index.html 中注释掉 blazor-error-ui div。
    <!-- <div id="blazor-error-ui" dir="rtl">
        an error occurred!
        <a href="" class="reload">refresh</a>
        <a class="dismiss"></a>
    </div> -->

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接