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

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


当前回答

使用中间件或者IExceptionHandlerPathFeature就可以了。 在商店里还有另一种方法

创建一个exceptionfilter并注册它

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

其他回答

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

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

下面是微软的官方指南,涵盖了所有版本的。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。后一个选项可以帮助您实现相同的结果,而不需要异常的开销。

首先,配置ASP。NET Core 2启动以重新执行到错误页面,以处理来自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编码的错误信息放在消息体中,方法是替换返回StatusCode…返回Json....

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

通过添加你自己的“异常处理中间件”,很难重用一些良好的内置异常处理逻辑,比如在错误发生时向客户端发送一个“符合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(“无效参数。”); 您不应该向客户端提供敏感的错误信息。服务错误是一种安全风险。