我使用ASP。NET核心为我的新的REST API项目后使用常规的ASP。NET Web API很多年了。我看不出在ASP中有什么处理异常的好方法。NET核心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。
那么我如何捕捉所有应用程序异常以及任何异常从动作过滤器?
通过添加你自己的“异常处理中间件”,很难重用一些良好的内置异常处理逻辑,比如在错误发生时向客户端发送一个“符合RFC 7807的有效负载”。
我所做的是在Startup.cs类之外扩展内置的异常处理程序,以处理自定义异常或覆盖现有异常的行为。例如,在不改变其他异常默认行为的情况下,将ArgumentException转换为BadRequest:
在Startup.cs上添加:
app.UseExceptionHandler("/error");
并像这样扩展ErrorController.cs:
using System;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;
namespace Api.Controllers
{
[ApiController]
[ApiExplorerSettings(IgnoreApi = true)]
[AllowAnonymous]
public class ErrorController : ControllerBase
{
[Route("/error")]
public IActionResult Error(
[FromServices] IWebHostEnvironment webHostEnvironment)
{
var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
var exceptionType = context.Error.GetType();
if (exceptionType == typeof(ArgumentException)
|| exceptionType == typeof(ArgumentNullException)
|| exceptionType == typeof(ArgumentOutOfRangeException))
{
if (webHostEnvironment.IsDevelopment())
{
return ValidationProblem(
context.Error.StackTrace,
title: context.Error.Message);
}
return ValidationProblem(context.Error.Message);
}
if (exceptionType == typeof(NotFoundException))
{
return NotFound(context.Error.Message);
}
if (webHostEnvironment.IsDevelopment())
{
return Problem(
context.Error.StackTrace,
title: context.Error.Message
);
}
return Problem();
}
}
}
注意:
NotFoundException是一个自定义异常,所有你需要做的是抛出新的NotFoundException(null);或抛出新的ArgumentException(“无效参数。”);
您不应该向客户端提供敏感的错误信息。服务错误是一种安全风险。
首先,感谢Andrei,因为我的解决方案是基于他的例子。
我把我的样本包括在内,因为它是一个更完整的样本,可能会为读者节省一些时间。
Andrei的方法的局限性是不能处理日志记录,捕获潜在有用的请求变量和内容协商(无论客户端请求什么——XML /纯文本等等,它总是返回JSON)。
我的方法是使用一个ObjectResult,它允许我们使用烘焙到MVC中的功能。
这段代码还可以防止缓存响应。
错误响应已被修饰成可以由XML序列化器序列化的方式。
public class ExceptionHandlerMiddleware
{
private readonly RequestDelegate next;
private readonly IActionResultExecutor<ObjectResult> executor;
private readonly ILogger logger;
private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();
public ExceptionHandlerMiddleware(RequestDelegate next, IActionResultExecutor<ObjectResult> executor, ILoggerFactory loggerFactory)
{
this.next = next;
this.executor = executor;
logger = loggerFactory.CreateLogger<ExceptionHandlerMiddleware>();
}
public async Task Invoke(HttpContext context)
{
try
{
await next(context);
}
catch (Exception ex)
{
logger.LogError(ex, $"An unhandled exception has occurred while executing the request. Url: {context.Request.GetDisplayUrl()}. Request Data: " + GetRequestData(context));
if (context.Response.HasStarted)
{
throw;
}
var routeData = context.GetRouteData() ?? new RouteData();
ClearCacheHeaders(context.Response);
var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor);
var result = new ObjectResult(new ErrorResponse("Error processing request. Server error."))
{
StatusCode = (int) HttpStatusCode.InternalServerError,
};
await executor.ExecuteAsync(actionContext, result);
}
}
private static string GetRequestData(HttpContext context)
{
var sb = new StringBuilder();
if (context.Request.HasFormContentType && context.Request.Form.Any())
{
sb.Append("Form variables:");
foreach (var x in context.Request.Form)
{
sb.AppendFormat("Key={0}, Value={1}<br/>", x.Key, x.Value);
}
}
sb.AppendLine("Method: " + context.Request.Method);
return sb.ToString();
}
private static void ClearCacheHeaders(HttpResponse response)
{
response.Headers[HeaderNames.CacheControl] = "no-cache";
response.Headers[HeaderNames.Pragma] = "no-cache";
response.Headers[HeaderNames.Expires] = "-1";
response.Headers.Remove(HeaderNames.ETag);
}
[DataContract(Name= "ErrorResponse")]
public class ErrorResponse
{
[DataMember(Name = "Message")]
public string Message { get; set; }
public ErrorResponse(string message)
{
Message = message;
}
}
}
有一个内置的中间件:
ASP。NET Core 5版本:
app.UseExceptionHandler(a => a.Run(async context =>
{
var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
var exception = exceptionHandlerPathFeature.Error;
await context.Response.WriteAsJsonAsync(new { error = exception.Message });
}));
旧版本(他们没有WriteAsJsonAsync扩展):
app.UseExceptionHandler(a => a.Run(async context =>
{
var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
var exception = exceptionHandlerPathFeature.Error;
var result = JsonConvert.SerializeObject(new { error = exception.Message });
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(result);
}));
它应该做几乎相同的事情,只是要编写的代码少了一点。
重要:记住将它添加在MapControllers \ UseMvc(或。net Core 3中的UseRouting)之前,因为顺序很重要。
如果你想为特定的控制器设置自定义异常处理行为,你可以通过覆盖控制器的OnActionExecuted方法来实现。
记住将ExceptionHandled属性设置为true以禁用默认的异常处理行为。
下面是我正在编写的api的一个示例,我想捕捉特定类型的异常并返回json格式的结果:
private static readonly Type[] API_CATCH_EXCEPTIONS = new Type[]
{
typeof(InvalidOperationException),
typeof(ValidationException)
};
public override void OnActionExecuted(ActionExecutedContext context)
{
base.OnActionExecuted(context);
if (context.Exception != null)
{
var exType = context.Exception.GetType();
if (API_CATCH_EXCEPTIONS.Any(type => exType == type || exType.IsSubclassOf(type)))
{
context.Result = Problem(detail: context.Exception.Message);
context.ExceptionHandled = true;
}
}
}
下面是微软的官方指南,涵盖了所有版本的。net的WebAPI和MVC案例。
对于Web API,它建议重定向到专用控制器端点以返回ProblemDetails。因为这可能会导致OpenAPI规范中不打算直接调用的端点的潜在暴露,我建议一个更简单的解决方案:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseExceptionHandler(a => a.Run(async context =>
{
var error = context.Features.Get<IExceptionHandlerFeature>().Error;
var problem = new ProblemDetails { Title = "Critical Error"};
if (error != null)
{
if (env.IsDevelopment())
{
problem.Title = error.Message;
problem.Detail = error.StackTrace;
}
else
problem.Detail = error.Message;
}
await context.Response.WriteAsJsonAsync(problem);
}));
...
}
在这种情况下,我们利用一个标准的中间件来返回定制的详细信息(用于开发模式的堆栈跟踪),并避免创建“内部”端点。
注:官方指南依赖于。net v3之前的IExceptionHandlerPathFeature,此后(到目前为止v5)依赖于IExceptionHandlerFeature。
p.s.如果你从域层抛出异常,将它们转换为4xx代码,我建议使用khellang的ProblemDetailsMiddleware或返回DomainResult,可以稍后转换为IActionResult或IResult。后一个选项可以帮助您实现相同的结果,而不需要异常的开销。