捕获 BackgroundService 中的异常

前言

上次,我们实现了《使用“装饰者模式”捕获 BackgroundService 中的异常》。

结果发现,微软已经发现了这个问题,并在 .NET 6 中解决了。(囧).

捕获 BackgroundService 中的异常

让我们验证一下:

using IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddHostedService<DemoBackgroundService>();
    })
    .Build();

await host.RunAsync();

public class DemoBackgroundService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        Console.WriteLine("DemoBackgroundService.ExecuteAsync");
        await Task.Delay(5000);
        throw new Exception("DemoBackgroundService throw Exception");
    }
}

确实会抛出异常并终止程序:

捕获 BackgroundService 中的异常

那现在让我们学习下,微软官方是如何实现的,对我们以后处理类似问题可以起到借鉴作用。

实现代码

通过查看提交历史[1]。我们发现如下修改:

BackgroundService

BackgroundService[2] 并没有较大修改,只是将 _executingTask 改名为 _executeTask,并暴露出去:

/// <summary>
/// Gets the Task that executes the background operation.
/// </summary>
/// <remarks>
/// Will return <see langword="null"/> if the background operation hasn't started.
/// </remarks>
public virtual Task ExecuteTask => _executeTask;

Host

关键改动是在 Host[3]

原来是仅仅启动了 IHostedService:

foreach (IHostedService hostedService in _hostedServices)
{
    // Fire IHostedService.Start
    await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false);
}

现在在启动了 IHostedService 后,还会对 BackgroundService.ExecuteTask 进行try-catch:

foreach (IHostedService hostedService in _hostedServices)
{
    // Fire IHostedService.Start
    await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false);

    if (hostedService is BackgroundService backgroundService)
    {
        _ = TryExecuteBackgroundServiceAsync(backgroundService);
    }
}

private async Task TryExecuteBackgroundServiceAsync(BackgroundService backgroundService)
{
    Task backgroundTask = backgroundService.ExecuteTask;

    ...

    try
    {
        await backgroundTask.ConfigureAwait(false);
    }
    catch (Exception ex)
    {
        ...

        if (_options.BackgroundServiceExceptionBehavior == BackgroundServiceExceptionBehavior.StopHost)
        {
            _logger.BackgroundServiceStoppingHost(ex);
            _applicationLifetime.StopApplication();
        }
    }
}

关键之处在于,执行TryExecuteBackgroundServiceAsync时并没有加上await,也就不会阻塞后续代码的执行;但在方法内部使用了await backgroundTask等待 ExecuteTask 执行完成。所以当 ExecuteTask 出错时,能够截获错误并执行终止当前应用程序操作。