UseExceptionHandler 不会捕获任务异常吗?

时间:2021-07-04 07:58:45

标签: c# asp.net-core .net-core

我创建了一个简单的 webapi .net core 3.1 应用。

我想捕获所有未处理的异常。所以我根据 docs 放置了这段代码:

app.UseExceptionHandler(c => c.Run(async context =>
{
    var exception = context.Features
        .Get<IExceptionHandlerPathFeature>()
        .Error;
    var response = new { error = exception.Message };
    log.LogDebug(exception.Message);
}));

这是我的操作:

[HttpGet]
public IActionResult Get()
{
    throw new Exception("this is a test");
}

当此代码运行时,我确实看到 UseExceptionHandler 正在运行。

但是当我的操作代码是:

[HttpGet]
public IActionResult Get()
{
    Task.Run(async () =>
    {
        await Task.Delay(4000);
        throw new Exception("this is a test");
    });
       
    return Ok();
}

然后 UseExceptionHandler 工作。

然而 - 以下代码确实捕获任务的异常:

 AppDomain.CurrentDomain.FirstChanceException += (sender, eventArgs) =>
 {
     Debug.WriteLine(eventArgs.Exception.ToString());
 };  

问题:

  • 为什么 UseExceptionHandler 无法识别任务异常?
  • 如何捕获所有类型的异常?我应该只依赖AppDomain.CurrentDomain.FirstChanceException吗?

nb ,我确实禁用了 app.UseDeveloperExceptionPage();

2 个答案:

答案 0 :(得分:2)

回答您的问题。

为什么 UseExceptionHandler 无法识别任务异常?

正如评论中已经建议的那样,您不能使用 UseExceptionHandler 来捕获在非等待任务中发起的异常。 UseExceptionHandler 将您的请求包装在 ASP.NET Core 中间件中。一旦操作将 OK 返回给客户端,中间件就无法再捕获从操作内启动的任务中发生的任何异常。

如何捕获所有类型的异常?我应该只依赖 AppDomain.CurrentDomain.FirstChanceException 吗?

如果您愿意,您可以全局捕获异常并以这种方式记录它们。但我不建议你这样做。您需要实现此事件的唯一原因是您正在 Web 请求中启动任务/线程。您无法知道这些任务是否一直在运行(应用程序重新启动、回收等)。如果您希望使用 ASP.NET Core 启动后台任务,您应该使用 Worker Services,这是执行此操作的预期方式:

.ConfigureServices((hostContext, services) =>
{
    services.AddHostedService<MyWorker>();
});
public class MyWorker : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                // Do work
            }
            catch (Exception e)
            {
                // Log it?
            }

            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}

答案 1 :(得分:1)

此特定症状的原因是 Get 正在启动服务器一无所知的即发即弃任务。请求将在任务有机会执行之前完成,因此 UseExceptionHandler 中间件永远不会看到任何异常。这不再是一项即发即忘的任务。

真正的问题是在后台执行长时间运行的任务。执行此操作的内置方法是使用 Background Service。文档展示了如何创建定时和排队的后台服务,作为作业队列。

将带有所需数据的消息从例如控制器发布到后台服务使用(例如通道)同样容易(如果不是更容易的话)。当 BCL 已经有一个异步队列时,不需要创建我们自己的队列。

服务可能如下所示:

public class MyService: BackgroundService 
{

    private readonly ChannelReader<T> _reader;

    public QueuedBspService(MessageQueue<T> queue)
    {
        _reader = queue.Reader;
    }

    protected internal async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            await foreach (var msg in _reader.ReadAllAsync(stoppingToken))
            {                    

                try
                {
                    //Process the message here
                }
                catch (Exception exc)
                {
                    //Handle message-specific errors
                }
            }
        }
        catch (Exception exc)
        {
            //Handle cancellations and other critical errors
        }
    }
}

MessageQueue<T> 包装了 Channel,从而更容易将其注入 BackgroundService 和任何发布者,例如控制器操作:

public class MessageQueue<T> 
{
    private readonly Channel<T> _channel;

    public ChannelReader<T> Reader => _channel;
    public ChannelWriter<T> Writer => _channel;

    public MessageChannel()
    {
        _channel = Channel.CreateBounded<T>(1);
    }
}

我从一次只允许一个操作的服务中调整了这段代码。这是防止控制器发出无法处理的请求的一种快速而肮脏的方法。

在控制端,如果可能,此操作将向队列发送请求,否则返回 Busy 响应:

public class MyController
{
    private readonly ChannelWriter<T> _writer;


    public MyController(MessaggeQueue<T> queue)
    {
        _writer = queue.Writer;
    }

    [HttpPost]
    [ProducesResponseType(StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
    public async Task<ActionResult> Post(....)
    {
        var jobName="SomeJob";
        var id=Guid.NewGuid();
        var jobMsg=CreateMessage(id,...);


        try
        {
            if (_writer.TryWrite(msg))
            {
                return CreatedAtAction("GetItem","Jobs",new {id});
            }
            else
            {
                return Problem(statusCode:(int) HttpStatusCode.ServiceUnavailable,detail:"Jobs in progress",title:"Busy");
            }
        }
        catch (Exception exc)
        {
            _logger.LogError(exc,"Queueing {job} failed",jobName);
            throw;
        }
    }            
}

Post 操作首先检查它是否甚至可以发布工作消息。如果成功,它会返回一个带有 URL 的 201 - Created 响应,该 URL 可以被检查,例如检查作业的状态。可以改用 return Created(),但是一旦您创建了一个长时间运行的作业,您还需要检查其状态。

如果通道已满,核心返回503并解释

相关问题