每个 ASP.NET Core Web API 项目都需要什么 - 第 3 部分 - 异常处理中间件
在上一篇文章中,我介绍了 API 版本控制以及如何将 Swagger 添加到支持 API 版本控制的示例项目中。在本文中,我将展示如何添加自定义中间件来全局处理异常,并在发生错误时创建自定义响应。
谁能写出完全没有bug的代码?反正我写不出来。虽然每个系统都可能出现未处理的异常,但捕获错误、记录并修复它们,以及向客户端提供正确的响应至关重要。异常处理中间件可以帮助我们在同一个地方捕获异常,避免在应用程序中编写重复的异常处理代码。
步骤 1 - 实现异常处理中间件
首先,在Infrastructure文件夹中添加一个新文件夹并命名,Middlewares然后在其中添加一个新文件ApiExceptionHandlingMiddleware.cs。添加以下代码:
public class ApiExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ApiExceptionHandlingMiddleware> _logger;
public ApiExceptionHandlingMiddleware(RequestDelegate next, ILogger<ApiExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception ex)
{
_logger.LogError(ex, $"An unhandled exception has occurred, {ex.Message}");
var problemDetails = new ProblemDetails
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1",
Title = "Internal Server Error",
Status = (int)HttpStatusCode.InternalServerError,
Instance = context.Request.Path,
Detail = "Internal server error occured!"
};
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
var result = JsonSerializer.Serialize(problemDetails);
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(result);
}
}
除了将响应状态码设置为 500 之外,响应正文中context.Response.StatusCode = (int)HttpStatusCode.InternalServerError还包含一条格式为 exist 的消息。ProblemDetails
在 ASP.NET Core 2.2 之前,HTTP 400 的默认响应类型BadRequest(ModelState)是:
{
"": [
"A non-empty request body is required."
]
}
根据互联网工程任务组 (IETF) RFC-7231 文档,ASP.NET Core 团队实现了ProblemDetails,这是一种机器可读格式,用于指定 Web API 响应中的错误,并且符合RFC 7807规范。
步骤 2 - 注册中间件
- 创建用于注册中间件的扩展方法:
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseApiExceptionHandling(this IApplicationBuilder app)
=> app.UseMiddleware<ApiExceptionHandlingMiddleware>();
}
- 打开
Startup.cs类,并在Configure方法中添加中间件:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
...
}
app.UseApiExceptionHandling();
如您所知,添加中间件组件的顺序很重要。如果您是UseDeveloperExceptionPage在开发环境中使用,那么请ApiExceptionHandling在完成上述步骤后再添加中间件。
步骤 3 - 将业务错误转化为领域异常
关于何时抛出异常有很多争论,但是,究竟何时应该抛出异常呢?
- 首要原因是完成整个过程并给出结果是不可能的(快速失败):
private async Task AddProductToBasketAsync(Guid productId)
{
var product = await _repository.GetProductByIdAsync(productId);
if(product == null)
throw new DomainException($"Product with id {productId} could not be found.");
// Or simply return null?
// Or return an error code or warping response into another object that has `Succeeded` property like `IdentityResult`?
// Or return a tuple (false, "Product with id {productId} could not be found.")?
-
抛出异常的缺点之一是它会带来性能开销。如果您正在编写高性能应用程序,抛出异常可能会降低性能。
重申一遍:例外情况必须是真正意义上的例外。
-
使用错误代码或包装对象有哪些缺点?
- 抛出异常更安全,因为调用代码可能忘记检查结果是否为错误代码或空值,而继续执行。必须从下往上检查方法调用的结果。
- 如果将结果封装到另一个对象中,例如`IdentityResult`,则需要支付额外的堆内存分配费用。每次调用都需要初始化一个额外的对象,即使操作成功也是如此。如果使用不同的输入调用 API 100 次,可能会抛出多少次异常?因此,在需要初始化额外对象(以及进行堆内存分配)的情况下,抛出异常的概率并不相同。
步骤 4 - 添加 DomainException 类
- 在项目根目录创建一个新文件夹并命名,
Domain然后添加另一个文件夹Exception DomainException.cs向文件夹添加新文件Exception:
public class DomainException : Exception
{
public DomainException(string message)
: base(message)
{
}
}
DomainException在异常处理中间件中捕获并转换为错误请求结果:
private async Task HandleExceptionAsync(HttpContext context, Exception ex)
{
string result;
**if (ex is DomainException)
{
var problemDetails = new ValidationProblemDetails(new Dictionary<string, string[]> { { "Error", new[] { ex.Message } } })
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
Title = "One or more validation errors occurred.",
Status = (int)HttpStatusCode.BadRequest,
Instance = context.Request.Path,
};
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
result = JsonSerializer.Serialize(problemDetails);
}**
else
{
_logger.LogError(ex, $"An unhandled exception has occurred, {ex.Message}");
var problemDetails = new ProblemDetails
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1",
Title = "Internal Server Error.",
Status = (int)HttpStatusCode.InternalServerError,
Instance = context.Request.Path,
Detail = "Internal Server Error!"
};
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
result = JsonSerializer.Serialize(problemDetails);
}
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(result);
}
我把它翻译DomainException成了ValidationProblemDetails类似未处理异常的形式。DomainException稍后我会用到。让我们测试一下域异常的实际应用:
[HttpGet("throw-domain-exception")]
public IActionResult ThrowDomainError()
{
throw new DomainException("Product could not be found");
}
您可以在Github上找到本教程的源代码。
文章来源:https://dev.to/moesmp/what-every-asp-net-core-web-api-project-needs-part-3-exception-handling-middleware-3nif
