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

Is there a way to globally catch all unhandled errors in a Blazor single page application?

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

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

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

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

这里引用史蒂夫·桑德森 (Steve Sanderson) 的话:

So overall, each component must deal with handling its own errors. If you want, you could make your own ErrorHandlingComponentBase to inherit from, and put a try/catch around all the lifecycle methods, and have your own logic for displaying an "oh dear sorry I died" UI on that component if anything went wrong. But it's not a feature of the framework today.

我希望这会在未来有所改变,我相信支持应该回到框架中。

在当前的 Blazor webassembly 版本中,所有未处理的异常都在内部 class 中捕获并写入 Console.Error。目前没有办法以不同的方式捕获它们,但 Rémi Bourgarel 展示了一个能够记录它们的解决方案 and/or 采取自定义操作。参见 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();

这适用于 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);

Here 是一个实施此解决方案的示例项目。

对于 .NET 5 Blazor 服务器端,这个 post Create Your Own Logging Provider to Log to Text Files in .NET Core 对我有用。对于我的情况,我已经对此进行了调整以捕获未处理的异常以写入 Azure 存储 table.

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

在 .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).

HereCustomErrorBoundary的样本。

有用的链接

要访问异常,您可以使用内置 ErrorBoundary component and access RenderFragment using Context attribute

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

这将捕获 ALL 个错误。

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>