问题描述

在传统的ASP.NET Web Api 应用程序开发中,我们处理全局异常的方法通常是实现一个​​ExceptionFilterAttribute​​的子类,如下:

public class ErrorHandlingFilter : ExceptionFilterAttribute
{
public override void OnException(ExceptionContext context)
{
HandleExceptionAsync(context);
context.ExceptionHandled = true;
}

private static void HandleExceptionAsync(ExceptionContext context)
{
var exception = context.Exception;

if (exception is MyNotFoundException)
SetExceptionResult(context, exception, HttpStatusCode.NotFound);
else if (exception is MyUnauthorizedException)
SetExceptionResult(context, exception, HttpStatusCode.Unauthorized);
else if (exception is MyException)
SetExceptionResult(context, exception, HttpStatusCode.BadRequest);
else
SetExceptionResult(context, exception, HttpStatusCode.InternalServerError);
}

private static void SetExceptionResult(
ExceptionContext context,
Exception exception,
HttpStatusCode code)
{
context.Result = new JsonResult(new ApiResponse(exception))
{
StatusCode = (int)code
};
}
}



然后,在​​Startup​​​过滤器中注册这个​​ErrorHandlingFilter​​自定义的错误处理属性类,如:

config.Filter.Add(new ErrorHandlingFilter());



这样,在ASP.NET Web Api 应用程序就可以捕获全局的错误并进行处理。

但在ASP.NET Core Web Api的应用程序开发开,以上的解决方案就不起作用了。ASP.NET Core Web Api的全局错误捕获与处理方式与ASP.NET Web Api应用程序的处理方式并非一样。

所以,我们得使用适合ASP.NET Core Web Api的全局错误捕获与处理的解决方案。那么,有哪些方案可以捕获和处理ASP.NET Core Web Api应用程序的全局错误呢?

方案一

一个比较好并且推荐的做法是使用中间件(middleware)来处理。在ASP.NET Core Web Api应用程序开发中,中间件是最好的解决方案。它不仅可以处理应用程序的异常,同时也可以捕获过滤器的异常并且也能创建自定义的响应返回结果(比如json的响应返回结果),以下是一个基于ASP.NET Core Web Api的异常处理的中间件:

public class ErrorHandlingMiddleware
{
private readonly RequestDelegate next;

public ErrorHandlingMiddleware(RequestDelegate next)
{
this.next = next;
}

public async Task Invoke(HttpContext context /* other dependencies */)
{
try
{
await next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}

private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
var code = HttpStatusCode.InternalServerError; // 500 if unexpected

if (exception is MyNotFoundException) code = HttpStatusCode.NotFound;
else if (exception is MyUnauthorizedException) code = HttpStatusCode.Unauthorized;
else if (exception is MyException) code = HttpStatusCode.BadRequest;

var result = JsonConvert.SerializeObject(new { error = exception.Message });
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)code;
return context.Response.WriteAsync(result);
}
}



使用方法,​​Startup.cs​​​类文件中,在调用MVC之前注册错误处理中间件​​ErrorHandlingMiddleware​​,如:

app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseMvc();



如果中间件捕获到应用程序的异常,则会返回类似如下的响应结果:

{ "error": "Authentication token is not valid." }



在中间件​​ErrorHandlingMiddleware​​​的方法​​HandleExceptionAsync​​中,可以对当前上下文的错误进行任意处理(比如,跟踪详细的错误信息,记录错误日志等等)。

方案二

在ASP.NET Core 2或者以上的.NET Core 版本中,我们可以使用​​UseExceptionHandler​​扩展方法来捕获并处理ASP.NET Core的全局错误。

首页,在ASP.NET Core 2的​​Startup.cs​​​文件中注册错误处理扩展方法并指定一个错误处理页面(这里是​​/Error​​),如下:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment()) {
// 开发模式...
} else {
app.UseStatusCodePagesWithReExecute("/Error");
app.UseExceptionHandler("/Error");
}
// 其他...
}



接着,定义一个可以指定响应状态码(HttpStatusCode)的异常类,如下:

public class HttpException : Exception
{
public HttpException(HttpStatusCode statusCode) { StatusCode = statusCode; }
public HttpStatusCode StatusCode { get; private set; }
}



最后,在控制器中创建上文提到的​​/Error​​错误处理页面,ASP.NET Core 2捕获到的错误将转到这个错误处理页面。在这个页面,我们就可以处理这些错误/异常,如下:

[AllowAnonymous]
public IActionResult Error()
{
// 捕获状态码
var statusCode = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error is HttpException httpEx ?
httpEx.StatusCode : (HttpStatusCode)Response.StatusCode;

// 如果是ASP.NET Core Web Api 应用程序,直接返回状态码(不跳转到错误页面,这里假设所有API接口的路径都是以/api/开始的)
if (HttpContext.Features.Get<IHttpRequestFeature>().RawTarget.StartsWith("/api/", StringComparison.Ordinal))
return StatusCode((int)statusCode);

// 创建一个友好的错误处理页面视图模型.
string text = null;
switch (statusCode) {
case HttpStatusCode.NotFound: text = "Page not found."; break;
// 其他详细信息
}
return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, ErrorText = text });
}




注: 如果需要让 API 接口返回json对象,则可以使用 ​​return Json(...)​​​来替换​​return StatusCode(...)​


方案三

在ASP.NET Core 2.1+版本中,我们可以使用中间件​​Community.AspNetCore.ExceptionHandling.Mvc[1]​​来处理,如下:

public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();

services.AddExceptionHandlingPolicies(options =>
{
options.For<InitializationException>().Rethrow();

options.For<SomeTransientException>().Retry(ro => ro.MaxRetryCount = 2).NextPolicy();

options.For<SomeBadRequestException>()
.Response(e => 400)
.Headers((h, e) => h["X-MyCustomHeader"] = e.Message)
.WithBody((req,sw, exception) =>
{
byte[] array = Encoding.UTF8.GetBytes(exception.ToString());
return sw.WriteAsync(array, 0, array.Length);
})
.NextPolicy();

// 确保所有的异常均被捕获
options.For<Exception>()
.Log(lo =>
{
lo.EventIdFactory = (c, e) => new EventId(123, "UnhandlerException");
lo.Category = (context, exception) => "MyCategory";
})
.Response(null, ResponseAlreadyStartedBehaviour.GoToNextHandler)
.ClearCacheHeaders()
.WithObjectResult((r, e) => new { msg = e.Message, path = r.Path })
.Handled();
});
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseExceptionHandlingPolicies();
app.UseMvc();
}