我创建了一个简单的 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();
答案 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
并解释