理解ASP.NET Core 错误处理机制(Handle Errors)

2022-04-15 20:40:21

注:本文隶属于《理解ASP.NET Core》系列文章,请查看置顶博客或点击此处查看全文目录

使用中间件进行错误处理

开发人员异常页

开发人员异常页用于显示未处理的请求异常的详细信息。当我们通过ASP.NET Core模板创建一个项目时,Startup.Configure方法中会自动生成以下代码:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env){    if (env.IsDevelopment())    {        // 添加开发人员异常页中间件        app.UseDeveloperExceptionPage();    }}

需要注意的是,与“异常处理”有关的中间件,一定要尽早添加,这样,它可以最大限度的捕获后续中间件抛出的未处理异常。

可以看到,当程序运行在开发环境中时,才会启用开发人员异常页,这很好理解,因为在生产环境中,我们不能将异常的详细信息暴露给用户,否则,这将会导致一系列安全问题。

现在我们在下方添加如下代码抛出一个异常:

app.Use((context, next) =>{    throw new NotImplementedException();});

当开发人员异常页中间件捕获了该未处理异常时,会展示类似如下的相关信息:

tatusCode 作占位符 var location = string.Format(CultureInfo.InvariantCulture, locationFormat, context.HttpContext.Response.StatusCode); // 重定向(302)到设定的资源 context.HttpContext.Response.Redirect(location); return Task.CompletedTask; }); }}

如果你不想更改原始请求的Url,而且保留原始状态码,那么你应该使用接下来要介绍的UseStatusCodePagesWithReExecute

UseStatusCodePagesWithReExecute

同样的,该扩展方法,内部也是通过调用UseStatusCodePages并传入lambda进行实现的,不过该方法:

接收1个路径字符串和和1个查询字符串。同样的,会有一个占位符{0},用于填充Http状态码 Url保持不变,并向客户端返回原始Http状态码 执行备用管道,用于生成响应正文
// 注意,这里要分开写app.UseStatusCodePagesWithReExecute("/Home/StatusCodeError", "?code={0}");

具体例子就不再列举了,用上面的就行了。现在来看看源码:

public static IApplicationBuilder UseStatusCodePagesWithReExecute(    this IApplicationBuilder app,    string pathFormat,    string queryFormat = null){    return app.UseStatusCodePages(async context =>    {        // 请注意,此时Http响应还未启动            // 格式化资源路径,context.HttpContext.Response.StatusCode 作占位符        var newPath = new PathString(            string.Format(CultureInfo.InvariantCulture, pathFormat, context.HttpContext.Response.StatusCode));        // 格式化查询字符串,context.HttpContext.Response.StatusCode 作占位符        var formatedQueryString = queryFormat == null ? null :            string.Format(CultureInfo.InvariantCulture, queryFormat, context.HttpContext.Response.StatusCode);        var newQueryString = queryFormat == null ? QueryString.Empty : new QueryString(formatedQueryString);        var originalPath = context.HttpContext.Request.Path;        var originalQueryString = context.HttpContext.Request.QueryString;        // 将原始请求信息保存下来,以便后续进行还原        context.HttpContext.Features.Set<IStatusCodeReExecuteFeature>(new StatusCodeReExecuteFeature()        {            OriginalPathBase = context.HttpContext.Request.PathBase.Value,            OriginalPath = originalPath.Value,            OriginalQueryString = originalQueryString.HasValue ? originalQueryString.Value : null,        });        context.HttpContext.SetEndpoint(endpoint: null);        var routeValuesFeature = context.HttpContext.Features.Get<IRouteValuesFeature>();        routeValuesFeature?.RouteValues?.Clear();        // 构造新请求        context.HttpContext.Request.Path = newPath;        context.HttpContext.Request.QueryString = newQueryString;        try        {            // 执行备用管道,生成响应正文            await context.Next(context.HttpContext);        }        finally        {            // 还原原始请求信息            context.HttpContext.Request.QueryString = originalQueryString;            context.HttpContext.Request.Path = originalPath;            context.HttpContext.Features.Set<IStatusCodeReExecuteFeature>(null);        }    });}

在MVC中,你可以通过给控制器或其中的Action方法添加[SkipStatusCodePages]特性,可以略过StatusCodePagesMiddleware

使用过滤器进行错误处理

除了错误处理中间件外,ASP.NET Core 还提供了异常过滤器,用于错误处理。

异常过滤器:

通过实现接口IExceptionFilterIAsyncExceptionFilter来自定义异常过滤器 可以捕获Controller创建时(也就是只捕获构造函数中抛出的异常)、模型绑定、Action Filter和Action中抛出的未处理异常 其他地方抛出的异常不会捕获

本节仅介绍异常过滤器,有关过滤器的详细内容,后续文章将会介绍

先来看一下这两个接口:

// 仅具有标记作用,标记其为 mvc 请求管道的过滤器public interface IFilterMetadata { }public interface IExceptionFilter : IFilterMetadata{    // 当抛出异常时,该方法会捕获    void OnException(ExceptionContext context);}public interface IAsyncExceptionFilter : IFilterMetadata{    // 当抛出异常时,该方法会捕获    Task OnExceptionAsync(ExceptionContext context);}

OnExceptionOnExceptionAsync方法都包含一个类型为ExceptionContext参数,很显然,它就是与异常有关的上下文,我们的异常处理逻辑离不开它。那接着来看一下它的结构吧:

public class ExceptionContext : FilterContext{    // 捕获到的未处理异常    public virtual Exception Exception { get; set; }    public virtual ExceptionDispatchInfo? ExceptionDispatchInfo { get; set; }    // 指示异常是否已被处理    // true:表示异常已被处理,异常不会再向上抛出    // false:表示异常未被处理,异常仍会继续向上抛出    public virtual bool ExceptionHandled { get; set; }    // 设置响应的 IActionResult    // 如果设置了结果,也表示异常已被处理,异常不会再向上抛出    public virtual IActionResult? Result { get; set; }}

除此之外,ExceptionContext还继承了FilterContext,而FilterContext又继承了ActionContext(这也从侧面说明,过滤器是为Action服务的),也就是说我们也能够获取到一些过滤器和Action相关的信息,看看都有什么吧:

public class ActionContext{    // Action相关的信息    public ActionDescriptor ActionDescriptor { get; set; }    // HTTP上下文    public HttpContext HttpContext { get; set; }    // 模型绑定和验证    public ModelStateDictionary ModelState { get; }    // 路由数据    public RouteData RouteData { get; set; }}public abstract class FilterContext : ActionContext{    public virtual IList<IFilterMetadata> Filters { get; }    public bool IsEffectivePolicy<TMetadata>(TMetadata policy) where TMetadata : IFilterMetadata {}    public TMetadata FindEffectivePolicy<TMetadata>() where TMetadata : IFilterMetadata {}}

更多参数细节,我会在专门讲过滤器的文章中详细介绍。

下面,我们就来实现一个自定义的异常处理器:

public class MyExceptionFilterAttribute : ExceptionFilterAttribute{    private readonly IModelMetadataProvider _modelMetadataProvider;    public MyExceptionFilterAttribute(IModelMetadataProvider modelMetadataProvider)    {        _modelMetadataProvider = modelMetadataProvider;    }    public override void OnException(ExceptionContext context)    {        if (!context.ExceptionHandled)        {            // 此处仅为简单演示            var exception = context.Exception;            var result = new ViewResult()            {                ViewName = "Error",                ViewData = new ViewDataDictionary(_modelMetadataProvider, context.ModelState)                {                    // 记得给 ErrorViewModel 加上 Message 属性                    Model = new ErrorViewModel                    {                        Message = exception.ToString()                    }                }            };            context.Result = result;            // 标记异常已处理            context.ExceptionHandled = true;        }    }}

接着,找到/Views/Shared/Error.cshtml,展示一下错误消息:

@model ErrorViewModel@{    ViewData["Title"] = "Error";}<p>@Model.Message</p>

最后,将服务MyExceptionFilterAttribute注册到DI容器:

public void ConfigureServices(IServiceCollection services){    services.AddScoped<MyExceptionFilterAttribute>();    services.AddControllersWithViews();}

现在,我们将该异常处理器加在/Home/Index上,并抛个异常:

public class HomeController : Controller{    [ServiceFilter(typeof(MyExceptionFilterAttribute))]    public IActionResult Index()    {        throw new Exception("Home Index Error");        return View();    }}

当请求/Home/Index时,你会得到如下页面:

理解ASP.NETCore错误处理机制(HandleErrors)

错误处理中间件 VS 异常过滤器

现在,我们已经介绍了两种错误处理的方法——错误处理中间件和异常过滤器。现在来比较一下它们的异同,以及我们何时应该选择哪种处理方式。

错误处理中间件:

可以捕获后续中间件的所有未处理异常 拥有RequestDelegate,操作更加灵活 粒度较粗,仅可针对全局进行配置

错误处理中间件适合用于处理全局异常。

异常过滤器:

仅可捕获Controller创建时(也就是构造函数中抛出的异常)、模型绑定、Action Filter和Action中抛出的未处理异常,其他地方抛出的异常捕获不到 粒度更小,可以灵活针对Controller或Action配置不同的异常过滤器

异常过滤器非常适合用于捕获并处理Action中的异常。

在我们的应用中,可以同时使用错误处理中间件和异常过滤器,只有充分发挥它们各自的优势,才能处理好程序中的错误。