ASP。NET核心Web API异常处理

我使用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。

那么我如何捕捉所有应用程序异常以及任何异常从动作过滤器?

414519 次浏览

最好的办法是使用中间件来实现您正在寻找的日志记录。您希望将异常日志记录放在一个中间件中,然后在另一个中间件中处理显示给用户的错误页面。这允许逻辑分离,并遵循微软在2个中间件组件上的设计。这是微软文档的一个很好的链接:ASP中的错误处理。网络核心

对于你的特定示例,你可能想要使用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();
}

如果你不喜欢这个特定的实现,那么你也可以使用榆树中间件,这里有一些例子

public void Configure(IApplicationBuilder app)
{
app.UseStatusCodePagesWithReExecute("/Errors/{0}");
// Exception handling logging below
app.UseElmCapture();
app.UseElmPage();
}

如果这不能满足您的需求,您总是可以通过查看它们的ExceptionHandlerMiddleware和ElmMiddleware的实现来生成自己的中间件组件,以掌握构建自己的中间件的概念。

重要的是要在StatusCodePages中间件下面添加异常处理中间件,但要在所有其他中间件组件之上添加异常处理中间件。这样,您的Exception中间件将捕获异常,记录它,然后允许请求继续到StatusCodePage中间件,后者将向用户显示友好的错误页面。

快速和简单的异常处理

简单地在ASP之前添加这个中间件。NET路由到中间件注册。

app.UseExceptionHandler(c => c.Run(async context =>
{
var exception = context.Features
.Get<IExceptionHandlerPathFeature>()
.Error;
var response = new { error = exception.Message };
await context.Response.WriteAsJsonAsync(response);
}));
app.UseMvc(); // or .UseRouting() or .UseEndpoints()

完成了!


为日志记录和其他目的启用依赖注入

步骤1。在启动时,注册异常处理路由:

// It should be one of your very first registrations
app.UseExceptionHandler("/error"); // Add this
app.UseEndpoints(endpoints => endpoints.MapControllers());

步骤2。创建控制器,该控制器将处理所有异常并产生错误响应:

[AllowAnonymous]
[ApiExplorerSettings(IgnoreApi = true)]
public class ErrorsController : ControllerBase
{
[Route("error")]
public MyErrorResponse Error()
{
var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
var exception = context.Error; // Your exception
var code = 500; // Internal Server Error by default


if      (exception is MyNotFoundException) code = 404; // Not Found
else if (exception is MyUnauthException)   code = 401; // Unauthorized
else if (exception is MyException)         code = 400; // Bad Request


Response.StatusCode = code; // You can use HttpStatusCode enum instead


return new MyErrorResponse(exception); // Your error model
}
}

一些重要的注意事项和观察:

  • 你可以将注入依赖项放入控制器的构造函数中。
  • [ApiExplorerSettings(IgnoreApi = true)]是必需的。否则,它可能会破坏你的虚张声势
  • 同样,app.UseExceptionHandler("/error");必须是你的Startup Configure(...)方法中最重要的注册之一。把它放在方法的顶部可能是安全的。
  • app.UseExceptionHandler("/error")中的路径和控制器[Route("error")]中的路径应该相同,以允许控制器处理从异常处理程序中间件重定向的异常。

下面是微软官方文档的链接


响应模型思想。

实现您自己的响应模型和异常。 这个例子只是一个很好的起点。每个服务都需要以自己的方式处理异常。使用所描述的方法,您可以完全灵活地控制处理异常并从服务返回正确的响应

一个错误响应模型的例子(只是给你一些想法):

public class MyErrorResponse
{
public string Type { get; set; }
public string Message { get; set; }
public string StackTrace { get; set; }


public MyErrorResponse(Exception ex)
{
Type = ex.GetType().Name;
Message = ex.Message;
StackTrace = ex.ToString();
}
}

对于更简单的服务,你可能想要实现http状态码异常,看起来像这样:

public class HttpStatusException : Exception
{
public HttpStatusCode Status { get; private set; }


public HttpStatusException(HttpStatusCode status, string msg) : base(msg)
{
Status = status;
}
}

这可以从任何地方抛出:

throw new HttpStatusCodeException(HttpStatusCode.NotFound, "User not found");

然后你的处理代码可以简化成这样:

if (exception is HttpStatusException httpException)
{
code = (int) httpException.Status;
}

HttpContext.Features.Get<IExceptionHandlerFeature>()窟?

ASP。NET Core开发人员接受了中间件的概念,在中间件中,不同方面的功能(如Auth、MVC、Swagger等)被分开,并在请求处理管道中按顺序执行。每个中间件都可以访问请求上下文,并在需要时写入响应。如果像处理MVC异常一样处理来自非MVC中间件的错误很重要,那么将异常处理从MVC中取出是有意义的,我发现这在现实世界的应用程序中非常常见。因此,由于内置的异常处理中间件不是MVC的一部分,MVC本身对它一无所知,反之亦然,异常处理中间件并不真正知道异常来自哪里,当然,除了它知道它发生在请求执行管道的某个地方。但两者可能都需要“联系”起来;彼此之间。因此,当异常在任何地方都没有被捕获时,异常处理中间件就会捕获它,并为路由重新运行管道,并在其中注册。这就是你如何“通过”的方法。异常处理返回到MVC,使用一致的内容协商或其他中间件(如果你愿意)。异常本身是从公共中间件上下文中提取的。看起来很有趣,但完成了工作:)。

首先,配置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...

要配置每种异常类型的异常处理行为,您可以使用NuGet包中的中间件:

代码示例:

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();


// Ensure that all exception types are handled by adding handler for generic exception at the end.
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();
}

首先,感谢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;
}
}
}

这个被广泛接受的答案对我帮助很大,但是我想在中间件中传递HttpStatusCode,以便在运行时管理错误状态代码。

根据这个链接,我有一些想法做同样的事情。所以我把安德烈的答案和这个合并了。所以我的最终代码如下:

< br > 1。基类

public class ErrorDetails
{
public int StatusCode { get; set; }
public string Message { get; set; }


public override string ToString()
{
return JsonConvert.SerializeObject(this);
}
}

< br > 2。自定义异常类类型

public class HttpStatusCodeException : Exception
{
public HttpStatusCode StatusCode { get; set; }
public string ContentType { get; set; } = @"text/plain";


public HttpStatusCodeException(HttpStatusCode statusCode)
{
this.StatusCode = statusCode;
}


public HttpStatusCodeException(HttpStatusCode statusCode, string message)
: base(message)
{
this.StatusCode = statusCode;
}


public HttpStatusCodeException(HttpStatusCode statusCode, Exception inner)
: this(statusCode, inner.ToString()) { }


public HttpStatusCodeException(HttpStatusCode statusCode, JObject errorObject)
: this(statusCode, errorObject.ToString())
{
this.ContentType = @"application/json";
}


}

< br > 3。自定义异常中间件

public class CustomExceptionMiddleware
{
private readonly RequestDelegate next;


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


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


private Task HandleExceptionAsync(HttpContext context, HttpStatusCodeException exception)
{
string result = null;
context.Response.ContentType = "application/json";
if (exception is HttpStatusCodeException)
{
result = new ErrorDetails()
{
Message = exception.Message,
StatusCode = (int)exception.StatusCode
}.ToString();
context.Response.StatusCode = (int)exception.StatusCode;
}
else
{
result = new ErrorDetails()
{
Message = "Runtime Error",
StatusCode = (int)HttpStatusCode.BadRequest
}.ToString();
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
}
return context.Response.WriteAsync(result);
}


private Task HandleExceptionAsync(HttpContext context, Exception exception)
{
string result = new ErrorDetails()
{
Message = exception.Message,
StatusCode = (int)HttpStatusCode.InternalServerError
}.ToString();
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
return context.Response.WriteAsync(result);
}
}

< br > 4。扩展方法

public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app)
{
app.UseMiddleware<CustomExceptionMiddleware>();
}

5. 在startup.cs中配置Method

app.ConfigureCustomExceptionMiddleware();
app.UseMvc();

现在我在帐户控制器中的登录方法:

try
{
IRepository<UserMaster> obj
= new Repository<UserMaster>(_objHeaderCapture, Constants.Tables.UserMaster);
var result = obj.Get()
.AsQueryable()
.Where(sb => sb.EmailId.ToLower() == objData.UserName.ToLower()
&& sb.Password == objData.Password.ToEncrypt()
&& sb.Status == (int)StatusType.Active)
.FirstOrDefault();
if (result != null)//User Found
return result;
else // Not Found
throw new HttpStatusCodeException(HttpStatusCode.NotFound,
"Please check username or password");
}
catch (Exception ex)
{
throw ex;
}

上面你可以看到,如果我没有找到用户,然后引发HttpStatusCodeException,其中我已经传递了HttpStatusCode。中间件中的NotFound状态和自定义消息

catch (HttpStatusCodeException ex)

block将被调用,它将把控制传递给

private Task HandleExceptionAsync(HttpContext context) HttpStatusCodeException异常)方法

但如果我得到运行时错误之前?为此,我使用了try catch块抛出异常,并将在catch (exception exceptionObj)块中捕获,并将控制传递给

任务HandleExceptionAsync(HttpContext上下文,异常异常)

方法。为了保持一致性,我使用了一个ErrorDetails类。

有一个内置的中间件:

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)之前,因为顺序很重要。

使用中间件或IExceptionHandlerPathFeature是好的。 在eshop

中有另一种方法

创建一个exceptionfilter并注册它

public class HttpGlobalExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{...}
}
services.AddMvc(options =>
{
options.Filters.Add(typeof(HttpGlobalExceptionFilter));
})

通过添加你自己的“异常处理中间件”,很难重用一些好的异常处理程序的内置逻辑,比如向客户端发送“符合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();
}
}
}

注意:

  1. NotFoundException是一个自定义异常,你所需要做的就是throw new NotFoundException(null);throw new ArgumentException("Invalid argument.");
  2. 您应该不向客户端提供敏感错误信息。服务错误是一个安全风险

在任何特定方法上处理异常的简单方法是:

using Microsoft.AspNetCore.Http;
...


public ActionResult MyAPIMethod()
{
try
{
var myObject = ... something;


return Json(myObject);
}
catch (Exception ex)
{
Log.Error($"Error: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}

如果你想为特定的控制器设置自定义异常处理行为,你可以通过覆盖控制器的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

附注:如果你从Domain层抛出异常以将它们转换为4xx代码,我建议要么使用khellang的ProblemDetailsMiddleware,要么返回DomainResult,之后可以转换为IActionResultIResult。后一个选项可以帮助您实现相同的结果,而不需要异常的开销。