在使用普通的ASP.NET Web API多年后,我开始为我的新REST API项目使用ASP.NET Core。我没有看到在ASP.NET Core Web API中处理异常的好方法。我试图实现异常处理过滤器/属性。
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
};
}
}
而这里是我的启动过滤器注册。
services.AddMvc(options =>
{
options.Filters.Add(new AuthorizationFilter());
options.Filters.Add(new ErrorHandlingFilter());
});
我遇到的问题是,当异常发生在我的AuthorizationFilter'时,它没有被
ErrorHandlingFilter'所处理。我希望它能在那里被捕获,就像在旧的ASP.NET Web API中那样。
那么,我怎样才能捕捉到所有应用程序的异常以及动作过滤器的任何异常呢?
在对不同的异常处理方法进行多次实验后,我最终使用了中间件。对于我的ASP.NET Core Web API应用程序来说,它的效果是最好的。它可以处理应用程序的异常,也可以处理来自动作过滤器的异常,我可以完全控制异常处理和HTTP响应。下面是我的异常处理中间件。
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 ex)
{
var code = HttpStatusCode.InternalServerError; // 500 if unexpected
if (ex is MyNotFoundException) code = HttpStatusCode.NotFound;
else if (ex is MyUnauthorizedException) code = HttpStatusCode.Unauthorized;
else if (ex is MyException) code = HttpStatusCode.BadRequest;
var result = JsonConvert.SerializeObject(new { error = ex.Message });
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)code;
return context.Response.WriteAsync(result);
}
}
在MVC之前**在Startup
类中注册它。
app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseMvc();
你可以向它添加堆栈跟踪、异常类型名称、错误代码或任何你想要的东西。非常灵活。下面是一个异常响应的例子。
{ "error": "Authentication token is not valid." }
考虑将IOptions<MvcJsonOptions>
注入到Invoke
方法中,然后在序列化响应对象时使用它,以利用ASP.NET MVC'中JsonConvert.SerializeObject(errorObj, opts.Value.SerializerSettings)
的序列化设置,在所有端点上实现更好的序列化一致性。
还有一个不显眼的API,叫做UseExceptionHandler
,对于简单的场景,它的作用是"ok" 。
app.UseExceptionHandler(a => a.Run(async context =>
{
var feature = context.Features.Get<IExceptionHandlerPathFeature>();
var exception = feature.Error;
var result = JsonConvert.SerializeObject(new { error = exception.Message });
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(result);
}));
这不是一个很明显但很容易设置异常处理的方法。但是我还是更喜欢中间件的方法,因为我可以通过注入必要的依赖关系来获得更多的控制权。
你最好的选择是使用中间件来实现你所寻找的日志记录。 你想把你的异常记录放在一个中间件中,然后在另一个中间件中处理显示给用户的错误页面。 这允许逻辑的分离,并遵循微软对两个中间件组件的设计。 这里有一个很好的链接到微软的文档。ASP.Net Core中的错误处理。
对于你的具体例子,你可能想使用StatusCodePage中间件中的一个扩展,或者像这个一样推出你自己的。
你可以在这里找到一个用于记录异常的例子。ExceptionHandlerMiddleware.cs。
public void Configure(IApplicationBuilder app)
{
// app.UseErrorPage(ErrorPageOptions.ShowAll);
// app.UseStatusCodePages();
// app.UseStatusCodePages(context => context.HttpContext.Response.SendAsync("Handler, status code: " + context.HttpContext.Response.StatusCode, "text/plain"));
// app.UseStatusCodePages("text/plain", "Response, status code: {0}");
// app.UseStatusCodePagesWithRedirects("~/errors/{0}");
// app.UseStatusCodePagesWithRedirects("/base/errors/{0}");
// app.UseStatusCodePages(builder => builder.UseWelcomePage());
app.UseStatusCodePagesWithReExecute("/Errors/{0}"); // I use this version
// Exception handling logging below
app.UseExceptionHandler();
}
如果你不喜欢这个具体的实现,那么你也可以使用ELM Middleware,这里有一些例子。Elm Exception Middleware。
public void Configure(IApplicationBuilder app)
{
app.UseStatusCodePagesWithReExecute("/Errors/{0}");
// Exception handling logging below
app.UseElmCapture();
app.UseElmPage();
}
如果这不能满足你的需求,你可以通过查看ExceptionHandlerMiddleware和ElmMiddleware的实现,掌握建立自己的概念,从而推出自己的中间件组件。
重要的是,要把异常处理中间件添加到StatusCodePages中间件下面,但要在所有其他中间件组件上面。 这样,你的Exception中间件将捕获异常,记录它,然后允许请求继续进行到StatusCodePage中间件,它将向用户显示友好的错误页面。
首先,配置ASP.NET Core 2 Startup
,使其重新执行到错误页面,以处理来自Web服务器的任何错误和任何未处理的异常。
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment()) {
// Debug config here...
} else {
app.UseStatusCodePagesWithReExecute("/Error");
app.UseExceptionHandler("/Error");
}
// More config...
}
接下来,定义一个异常类型,让你用HTTP状态代码抛出错误。
public class HttpException : Exception
{
public HttpException(HttpStatusCode statusCode) { StatusCode = statusCode; }
public HttpStatusCode StatusCode { get; private set; }
}
最后,在错误页面的控制器中,根据错误的原因和响应是否会被终端用户直接看到,定制响应。这段代码假设所有的API URL都以/api/
开头。
[AllowAnonymous]
public IActionResult Error()
{
// Gets the status code from the exception or web server.
var statusCode = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error is HttpException httpEx ?
httpEx.StatusCode : (HttpStatusCode)Response.StatusCode;
// For API errors, responds with just the status code (no page).
if (HttpContext.Features.Get<IHttpRequestFeature>().RawTarget.StartsWith("/api/", StringComparison.Ordinal))
return StatusCode((int)statusCode);
// Creates a view model for a user-friendly error page.
string text = null;
switch (statusCode) {
case HttpStatusCode.NotFound: text = "Page not found."; break;
// Add more as desired.
}
return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, ErrorText = text });
}
ASP.NET Core会记录错误细节,供你调试,所以你只需向(可能不信任的)请求者提供一个状态代码即可。如果你想显示更多的信息,你可以增强HttpException
来提供它。对于API错误,你可以将JSON编码的错误信息放在消息正文中,方法是将return StatusCode...
替换为return Json...
。