注:本文隶属于《理解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进行实现的,不过该方法:
{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 还提供了异常过滤器,用于错误处理。
异常过滤器:
通过实现接口IExceptionFilter或IAsyncExceptionFilter来自定义异常过滤器 可以捕获Controller创建时(也就是只捕获构造函数中抛出的异常)、模型绑定、Action Filter和Action中抛出的未处理异常 其他地方抛出的异常不会捕获本节仅介绍异常过滤器,有关过滤器的详细内容,后续文章将会介绍
先来看一下这两个接口:
// 仅具有标记作用,标记其为 mvc 请求管道的过滤器public interface IFilterMetadata { }public interface IExceptionFilter : IFilterMetadata{ // 当抛出异常时,该方法会捕获 void OnException(ExceptionContext context);}public interface IAsyncExceptionFilter : IFilterMetadata{ // 当抛出异常时,该方法会捕获 Task OnExceptionAsync(ExceptionContext context);}OnException和OnExceptionAsync方法都包含一个类型为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时,你会得到如下页面:

错误处理中间件 VS 异常过滤器
现在,我们已经介绍了两种错误处理的方法——错误处理中间件和异常过滤器。现在来比较一下它们的异同,以及我们何时应该选择哪种处理方式。
错误处理中间件:
可以捕获后续中间件的所有未处理异常 拥有RequestDelegate,操作更加灵活 粒度较粗,仅可针对全局进行配置错误处理中间件适合用于处理全局异常。
异常过滤器:
仅可捕获Controller创建时(也就是构造函数中抛出的异常)、模型绑定、Action Filter和Action中抛出的未处理异常,其他地方抛出的异常捕获不到 粒度更小,可以灵活针对Controller或Action配置不同的异常过滤器异常过滤器非常适合用于捕获并处理Action中的异常。
在我们的应用中,可以同时使用错误处理中间件和异常过滤器,只有充分发挥它们各自的优势,才能处理好程序中的错误。








