ASP.NET MVC 自定义全局异常处理:构建健壮应用的基石
在 ASP.NET MVC 应用开发中,未处理的异常是用户体验的“杀手”和系统稳定性的“隐患”。默认的“黄屏死机”(Yellow Screen of Death, YSOD) 不仅不友好,还可能暴露敏感信息。全局异常处理机制 正是为了解决此问题而生,它允许你集中捕获、记录异常,并以统一、可控的方式向用户返回友好的错误页面或结构化的错误信息(如 API 返回 JSON)。自定义全局异常处理是构建专业、可靠、用户友好的 Web 应用程序的关键环节。本文将深入探讨其原理、多种实现方式、最佳实践,并提供实用代码示例。
目录#
- 为什么需要全局异常处理?
- ASP.NET MVC 异常处理机制概览
HandleErrorAttribute的局限性
- 自定义全局异常处理方法
- 方法 1:使用
Application_Error(Global.asax)- 原理
- 实现步骤 (带代码)
- 优缺点
- 方法 2:创建自定义错误过滤器 (
IExceptionFilter)- 原理 (
OnException/OnExceptionAsync) - 实现自定义 Filter (带代码)
- 全局注册 Filter
filterContext.ExceptionHandled和filterContext.Result
- 原理 (
- 方法 3(ASP.NET Core):使用中间件 (Middleware)
UseExceptionHandler中间件简介- 配置自定义错误处理管道
- 示例:处理不同环境(Development/Production)
- 方法 1:使用
- 关键实现细节与最佳实践
- 记录日志(Logging)是必备项
- 推荐库 (NLog, Serilog, log4net)
- 记录关键信息 (Exception, Request Data, User Context)
- 区分错误类型处理
- 业务异常 (返回 400 Bad Request)
- 未授权异常 (返回 401 Unauthorized / 403 Forbidden)
- 未找到资源 (返回 404 Not Found)
- 其他未预期异常 (返回 500 Internal Server Error)
- 友好的用户界面
- 自定义错误视图 (
Error.cshtml) - API 友好错误响应 (JSON Format)
- 自定义错误视图 (
- HTTP 状态码的精准控制
- 设置
Response.StatusCode
- 设置
- 防止敏感信息泄露
- 生产环境与开发环境的差异处理
- 避免暴露堆栈跟踪、服务器信息
- 处理
ThreadAbortException(MVC5 中需注意)
- 记录日志(Logging)是必备项
- 完整示例:集成自定义过滤器
- 场景:Web 应用(混合视图 + Web API)
- 代码:
CustomExceptionHandlerFilter.cs - 代码:
ErrorController.cs - 代码:注册 Filter (
FilterConfig.cs或Startup.cs) - 代码:自定义错误视图
- 代码:API 错误响应格式
- 常见问题与陷阱(Troubleshooting)
- 全局过滤器没生效?
Application_Error和自定义过滤器哪个先执行?- 返回 JSON 时设置
Content-Type - 处理静态文件错误 (IIS 配置补充)
- Core 中间件顺序的重要性
- 总结
- 参考文献
1. 为什么需要全局异常处理?#
- 提升用户体验: 避免丑陋的默认错误页面,提供清晰、友好的错误提示。
- 增强安全性: 防止敏感的应用程序信息(如堆栈跟踪、服务器版本、内部路径)暴露给最终用户和潜在攻击者。
- 统一处理逻辑: 避免在每个 Action 中重复编写
try-catch块,保持代码 DRY (Don't Repeat Yourself)。 - 记录错误信息: 集中记录未处理异常的详细信息(包括上下文数据),便于后续诊断和修复问题。
- 灵活的响应格式: 根据请求类型(浏览器请求 / AJAX 请求 / API 请求)动态返回 HTML 错误页面或 JSON 结构化的错误信息。
- 维护性: 错误处理逻辑集中在统一位置,修改和维护更便捷。
2. ASP.NET MVC 异常处理机制概览#
ASP.NET MVC 框架在处理请求时有一个清晰的管道(Controller/Action 执行管道)。异常可以在管道中的多个点被抛出:
- 模型绑定(Model Binding)
- Action 方法执行
- Action 过滤器(Filter)执行 (
IActionFilter,IAuthorizationFilter,IExceptionFilter) - 结果执行 (
ViewResult,JsonResult, etc.)
框架内置了 HandleErrorAttribute 过滤器,它提供了一种处理管道中发生的异常并渲染 Error.cshtml 视图的基本机制。但它的局限显著:
- 无法记录日志: 只是渲染一个视图,不提供日志记录功能。
- HTTP 状态码固定为 500: 无法根据异常类型设置不同的状态码(如 400, 404)。
- 无法处理所有错误: 在 Action 执行 前(如授权过滤器中)或 后(如结果执行中)发生的异常可能被遗漏。
- 响应格式单一: 通常只处理视图渲染,难以灵活适配 API 的 JSON 错误响应。
因此,自定义全局异常处理是生产环境应用的必然选择。
3. 自定义全局异常处理方法#
方法 1:使用 Application_Error (Global.asax)#
原理: 这是 ASP.NET(包括 MVC 5)应用程序生命周期事件 HttpApplication.Error 的处理入口点,位于 MVC 处理管道的最外层。任何未在 MVC 内部管道(包括过滤器)中被处理的异常最终都会冒泡到这里。
实现步骤:
-
打开/创建
Global.asax文件。 -
重写
Application_Error方法。// Global.asax.cs (MVC 5) public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { // ... 其他初始化代码 (AreaRegistration, RouteConfig, BundleConfig) ... } protected void Application_Error(object sender, EventArgs e) { // 1. 获取最后的异常(可能需要检查 InnerException 获取根因) Exception exception = Server.GetLastError(); // 2. 【关键】记录日志 (使用你选择的日志库) Logger.Error(exception, "全局异常处理器捕获未处理异常"); // 3. 清除之前的错误响应 HttpContext httpContext = HttpContext.Current; Server.ClearError(); // 4. 根据需求生成响应 // 4a. 处理 AJAX/API 请求 (假设通过 Request.Headers 判断) if (httpContext.Request.IsAjaxRequest() || httpContext.Request.ContentType?.Contains("application/json") == true || httpContext.Request.Path.StartsWith("/api/", StringComparison.OrdinalIgnoreCase)) { httpContext.Response.ContentType = "application/json"; httpContext.Response.StatusCode = (int)MapExceptionToStatusCode(exception); // 自定义映射方法 httpContext.Response.Write(JsonConvert.SerializeObject(new { Error = "请求处理失败", Message = exception.Message, // 生产环境可能需要更模糊的消息 ExceptionType = exception.GetType().Name, // 仅在开发环境添加详情 StackTrace = HttpContext.Current.IsDebuggingEnabled ? exception.StackTrace : null })); httpContext.Response.End(); } // 4b. 处理普通视图请求 else { // 创建路由数据指向你的自定义 ErrorController var routeData = new RouteData(); routeData.Values["controller"] = "Error"; routeData.Values["action"] = "Index"; routeData.Values["exception"] = exception; // 使用自定义错误 Controller 处理 IController errorController = new Controllers.ErrorController(); var requestContext = new RequestContext( new HttpContextWrapper(httpContext), routeData); errorController.Execute(requestContext); } } private HttpStatusCode MapExceptionToStatusCode(Exception ex) { // 根据你的业务异常类型进行映射 if (ex is ArgumentException || ex is InvalidOperationException) return HttpStatusCode.BadRequest; // 400 if (ex is UnauthorizedAccessException) return HttpStatusCode.Unauthorized; // 401 if (ex is HttpException httpEx) // 注意 HttpException 包含状态码 return (HttpStatusCode)httpEx.GetHttpCode(); if (ex is KeyNotFoundException || ex is FileNotFoundException) return HttpStatusCode.NotFound; // 404 return HttpStatusCode.InternalServerError; // 500 } }
优点:
- 能捕获 MVC 管道最外层发生的所有未处理异常(包括未在过滤器中处理的)。
- 代码位置集中。
缺点:
- 代码位于
Global.asax,可能不太符合 MVC 的 “Filter” 思维。 - 处理逻辑相对复杂(要处理请求类型判断、路由模拟等)。
- 在 ASP.NET Core 中不再适用。
方法 2:创建自定义错误过滤器 (IExceptionFilter)#
原理: IExceptionFilter 接口定义了 OnException (同步) 或 OnExceptionAsync (异步) 方法。当 Controller Action 执行期间(包括其调用的过滤器、Action 方法本身或结果执行过程)发生未处理异常时,框架会调用已注册的 IExceptionFilter。通过创建全局注册的自定义过滤器,可以实现集中处理。
实现步骤:
-
创建自定义异常过滤器类:
// App_Start/CustomExceptionHandlerFilter.cs (MVC 5) 或 Filters/CustomExceptionHandlerFilter.cs (推荐) public class CustomExceptionHandlerFilter : IExceptionFilter // 也可以同时实现 IAsyncExceptionFilter { private readonly ILogger _logger; // 通常通过 DI 注入日志服务 public CustomExceptionHandlerFilter(ILogger logger) // 依赖注入构造器 { _logger = logger; } public void OnException(ExceptionContext filterContext) { // 0. 如果异常已被其他过滤器标记为已处理,则退出 if (filterContext.ExceptionHandled) { return; } // 1. 获取异常 var exception = filterContext.Exception; // 2. 【最佳实践】记录日志 _logger.LogError(exception, $"全局异常过滤器捕获异常: {exception.Message}"); // 3. 设置 HTTP 状态码 var statusCode = MapExceptionToStatusCode(exception); filterContext.HttpContext.Response.StatusCode = (int)statusCode; filterContext.HttpContext.Response.TrySkipIisCustomErrors = true; // 防止 IIS 覆盖你的错误处理 (IIS集成模式) // 4. 根据请求类型选择处理方式 if (IsApiRequest(filterContext.HttpContext.Request)) { // 4a. API 请求:返回 JSON filterContext.Result = new JsonResult(new ApiErrorResponse { ErrorCode = statusCode.ToString(), Message = GetUserFriendlyMessage(exception, filterContext.HttpContext), // 可选:在开发环境添加 Details Details = filterContext.HttpContext.IsDebuggingEnabled ? exception.ToString() : null }); // 确保返回类型设置为 JSON filterContext.HttpContext.Response.ContentType = "application/json"; } else { // 4b. 普通视图请求:渲染错误视图 filterContext.Result = new ViewResult { ViewName = "Error", // 指向你的错误视图 (如 Views/Shared/Error.cshtml) ViewData = new ViewDataDictionary<HandleErrorInfo>(new HandleErrorInfo( exception, filterContext.RouteData?.Values["controller"]?.ToString(), filterContext.RouteData?.Values["action"]?.ToString())) }; } // 5. 【关键】标记异常已处理,阻止框架传递异常给其他处理器(如Application_Error) filterContext.ExceptionHandled = true; } private bool IsApiRequest(HttpRequestBase request) { return request.Path.StartsWith("/api/", StringComparison.OrdinalIgnoreCase) || request.ContentType?.Contains("application/json") == true || (request.Headers["X-Requested-With"] == "XMLHttpRequest"); } private HttpStatusCode MapExceptionToStatusCode(Exception ex) { // 同 Application_Error 中的实现逻辑 // ... } private string GetUserFriendlyMessage(Exception ex, HttpContextBase context) { // 生产环境下通常返回通用错误消息,或者基于异常类型返回预设的友好消息 if (context.IsDebuggingEnabled) return ex.Message; if (ex is CustomBusinessException custEx) return custEx.UserMessage; // 业务异常自带友好提示 return "抱歉,处理您的请求时发生了一个错误。请稍后再试或联系管理员。"; } } // API 错误响应模型 (放在 Models 目录) public class ApiErrorResponse { public string ErrorCode { get; set; } public string Message { get; set; } public string Details { get; set; } // 仅在开发环境暴露 } -
全局注册过滤器:
-
MVC 5 (
FilterConfig.cs):public class FilterConfig { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute()); // 可考虑移除或保留(但注意顺序) // 【重要】注册你的自定义异常过滤器 filters.Add(new CustomExceptionHandlerFilter(LoggingService.Instance)); // 单例示例,最好使用依赖注入框架(DI) // 其他全局过滤器 (如 AuthorizeAttribute) } }确保在
Global.asax的Application_Start中调用:protected void Application_Start() { // ... FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); // ... } -
ASP.NET Core (
Startup.cs-ConfigureServices):public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(options => { // 添加全局过滤器 options.Filters.Add(typeof(CustomExceptionHandlerFilter)); // 注意:Core 的 DI 会解析其构造函数依赖项 }); // 注册你的日志服务实现 (如 NLog, Serilog) services.AddSingleton<ILogger, MyLoggerImplementation>(); }
-
优点:
- 更加符合 MVC 架构模式,逻辑更清晰。
- 可以直接访问 MVC 上下文信息 (
ExceptionContext),如RouteData,HttpContext,Controller等。 - 在 ASP.NET Core MVC 中依然是主流方式之一,与 DI 集成方便。
- 可以在
GlobalFilterCollection中控制过滤器顺序(异常过滤器通常应注册为全局且靠后)。
缺点:
- 需要创建和管理过滤器类。
- 无法捕获 所有 框架级别异常(某些发生在 MVC 处理管道之前或之后的错误,如静态文件处理错误)。
重要属性/方法:
ExceptionContext.ExceptionHandled: 设置为true表示此过滤器已处理异常,通知框架不需要再调用后续的IExceptionFilter或冒泡到Application_Error。ExceptionContext.Result: 设置当异常处理后应执行的ActionResult(如ViewResult,JsonResult,RedirectResult)。
方法 3(ASP.NET Core):使用中间件 (Middleware)#
原理: ASP.NET Core 的请求处理管道是中间件链。UseExceptionHandler 是一个强大的内置中间件,专门用于捕获管道中发生的所有未处理异常,允许你配置一个错误处理终端节点。这是 ASP.NET Core 中推荐的首选方法,因为它能捕获发生在整个请求管道中的所有异常(包括 MVC 中间件内部的、静态文件中间件的、或者身份验证中间件的)。
实现步骤:
- 在
Startup.cs的Configure方法中配置中间件:public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // 【重要】错误处理中间件应尽可能早地放在管道中, // 这样它才能捕获后续中间件中发生的异常。 app.UseExceptionHandler(errorApp => { errorApp.Run(async context => { // 1. 获取最后的异常 var exceptionHandlerFeature = context.Features.Get<IExceptionHandlerFeature>(); var exception = exceptionHandlerFeature?.Error; // 2. 【关键】记录日志 (使用注入的 ILogger 或通过 context 获取) var logger = context.RequestServices.GetRequiredService<ILogger<Program>>(); logger.LogError(exception, "异常处理中间件捕获全局异常"); // 3. 清空响应流 (确保没有其他写入) context.Response.Clear(); context.Response.StatusCode = StatusCodes.Status500InternalServerError; // 默认状态码 // 4. 映射状态码(可选,根据异常类型) if (exception is MyCustomNotFoundException) { context.Response.StatusCode = StatusCodes.Status404NotFound; } else if (exception is UnauthorizedAccessException) { context.Response.StatusCode = StatusCodes.Status401Unauthorized; } // ... 其他映射规则 // 5. 设置响应 Content-Type context.Response.ContentType = "text/html"; // 默认错误页 // 6. 根据请求类型生成响应内容 if (context.Request.Path.StartsWithSegments("/api")) { // API 请求:返回 JSON context.Response.ContentType = "application/json"; var apiError = new ApiErrorResponse { ErrorCode = context.Response.StatusCode.ToString(), Message = env.IsDevelopment() ? exception?.Message : "An error occurred.", Details = env.IsDevelopment() ? exception?.ToString() : null }; var json = JsonSerializer.Serialize(apiError); await context.Response.WriteAsync(json); } else { // 普通页面请求:渲染错误视图 // 方法 6a: 重定向到 MVC ErrorController (更灵活,易于维护复杂视图) context.Response.Redirect($"/Error/Index?statusCode={context.Response.StatusCode}"); // 或 方法 6b: 直接在中间件中写入简单 HTML //await context.Response.WriteAsync("<html><body><h1>Sorry, something went wrong!</h1></body></html>"); } }); }); // 其他中间件: HTTPS重定向、静态文件、路由、端点、授权等... app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } - (可选但推荐)创建
ErrorController: 如果选择重定向到 Controller(如上面示例中的"/Error/Index"),则创建该 Controller 和对应的视图来处理错误页面的渲染逻辑。这种方式通常更灵活,更适合复杂的错误页面。// Controllers/ErrorController.cs [AllowAnonymous] // 确保错误页面在授权失败时也能访问 public class ErrorController : Controller { [Route("Error/{statusCode?}")] public IActionResult Index(int? statusCode = null) { // 获取原始异常路径 (可选) var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>(); var exception = exceptionHandlerPathFeature?.Error; // 设置状态码(使用传递进来的或默认500) if (statusCode.HasValue) { HttpContext.Response.StatusCode = statusCode.Value; } else { HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; } // 创建视图模型,包含错误信息 var errorViewModel = new ErrorViewModel { StatusCode = HttpContext.Response.StatusCode.ToString(), RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, ErrorMessage = exception?.Message ?? $"发生错误 (状态码: {HttpContext.Response.StatusCode})", // 仅在开发环境显示详细错误 ShowErrorDetail = HttpContext.RequestServices.GetRequiredService<IHostEnvironment>().IsDevelopment(), StackTrace = exception?.StackTrace }; return View(errorViewModel); } } - 创建
ErrorViewModel(Models/ErrorViewModel.cs):public class ErrorViewModel { public string StatusCode { get; set; } public string RequestId { get; set; } public string ErrorMessage { get; set; } public bool ShowErrorDetail { get; set; } public string? StackTrace { get; set; } // Nullable if using C# 8+ } - 创建
Views/Error/Index.cshtml:使用ErrorViewModel渲染错误信息。
优点 (Middleware):
- 能捕获整个请求管道中发生的任何异常,范围最广。
- 完全控制错误响应生成过程。
- 与 ASP.NET Core 的管道式架构无缝集成。
- 易于区分开发和生产环境行为(通过
IHostEnvironment)。 - 是 ASP.NET Core 的标准推荐方式。
缺点:
- 特定于 ASP.NET Core。
- 配置逻辑位于
Startup.Configure,可能相对复杂,尤其如果使用重定向到控制器的方式。
4. 关键实现细节与最佳实践#
-
记录日志(Logging)是必备项:
- 选择强大日志库: NLog, Serilog, log4net 都是优秀选择。利用它们的结构化日志、多种输出目标(文件、数据库、Elasticsearch)、滚动策略。
- 记录关键信息:
- 异常对象本身 (
Exception.ToString()通常已包含Message,StackTrace,InnerException)。 - 请求路径 (
Request.Path) 和 HTTP 方法 (Request.Method)。 - 请求查询字符串 (
Request.QueryString) 和表单数据(敏感信息注意脱敏!)。 - 用户标识(如
User.Identity.Name或UserId,如果可用)。 - 请求唯一标识符(如
TraceIdentifier)。
- 异常对象本身 (
- 级别: 未处理异常通常使用
Error或Critical级别。
-
区分错误类型处理:
- 业务逻辑/校验失败: (例如
ValidationException,BusinessRuleViolationException):通常意味着用户输入有误或业务规则不满足。返回 HTTP 400 Bad Request,并在响应体 (JSON 或错误消息) 中提供具体错误细节,指导用户修正。 - 未授权/无权限:
UnauthorizedAccessException-> 401 Unauthorized (表示需要认证) 或 403 Forbidden (表示已认证但无权限)。通常由授权框架处理,但全局处理器可作为最后防线。避免暴露过多权限信息。 - 资源未找到:
FileNotFoundException,KeyNotFoundException或自定义NotFoundException-> 404 Not Found。提供友好的“找不到”提示。 - 其他意外/系统错误: 所有其他未预料的异常(数据库连接失败、IO错误、空引用等) -> 500 Internal Server Error。返回通用的、安全的错误信息给用户(如“系统内部错误,请稍后再试”),详细信息记录在日志中供管理员排查。
- 业务逻辑/校验失败: (例如
-
友好的用户界面:
- 视图渲染: 为
Error.cshtml视图设计专业的布局,包含:- 简洁的错误标题 (如“抱歉,出错了”)
- 概括性的错误消息 (避免技术术语)。
- 请求唯一标识符 (
RequestId),方便用户报告问题。 - (可选,仅限于开发/测试环境)错误详细信息和堆栈跟踪。确保生产环境绝对不显示!
- 引导用户返回首页或联系支持的链接。
- API 响应: 遵循一致的 JSON 错误格式,例如:
标准如 RFC 7807 (Problem Details) 更佳。{ "error": { "code": "404", "message": "Requested user with ID '123' was not found." // "details": "Additional details for debugging (optional, Dev only)" // "requestId": "0HLR5VQJ3AAQG" } }
- 视图渲染: 为
-
HTTP 状态码的精准控制:
- 务必设置
context.Response.StatusCode(Middleware) 或filterContext.HttpContext.Response.StatusCode(Filter)。 - 正确使用状态码有助于客户端(浏览器、前端框架、API 调用方)理解错误性质。
- 使用 .NET 提供的
HttpStatusCode枚举或StatusCodes常量 (Core) 代替数字。
- 务必设置
-
防止敏感信息泄露 (Security):
- 生产环境:
- 绝不 将异常的
StackTrace、InnerException细节、服务器文件路径、数据库连接字符串、内部类名方法名等直接展示给终端用户。 - 在错误视图中使用通用消息(如“处理您的请求时发生错误”)。
- 在 API 响应中返回模糊的
Message。 - 禁用
customErrors mode="Off"(web.config) 或暴露详细错误的框架设置。
- 绝不 将异常的
- 开发环境: 为了方便调试,可以(也应)显示详细错误信息。使用
#if DEBUG,HttpContext.IsDebuggingEnabled(MVC5) 或IHostEnvironment.IsDevelopment()(Core) 进行环境判断。 - 配置错误页: 在
web.config(IIS 托管)或服务器配置中设置友好的默认错误页(作为全局异常处理失效时的最后一道防线),确保这些页面本身也不包含敏感信息。
- 生产环境:
-
处理
ThreadAbortException(主要 MVC5):- 在某些情况下(如重定向
Response.Redirect(url, true)的第二个参数为true,或显式调用Response.End()),框架会抛出ThreadAbortException。这不是真正的“错误”。在你的全局处理逻辑中,应在尝试记录或处理之前检查异常类型:if (exception is System.Threading.ThreadAbortException) { // 通常忽略或简单记录为信息级别 _logger.LogInformation("捕获 ThreadAbortException,通常由重定向或 Response.End 引起。"); filterContext.ExceptionHandled = true; // 如果使用过滤器 return; }
- 在某些情况下(如重定向
5. 完整示例:集成自定义过滤器 (ASP.NET Core MVC with Middleware + Controller)#
(此处给出 ASP.NET Core 集成 UseExceptionHandler + ErrorController 的方案,因其为首选)
项目结构:
MyWebApp/
├── Controllers/
│ ├── ErrorController.cs
│ └── ... (其他Controller)
├── Models/
│ └── ViewModels/
│ └── ErrorViewModel.cs
├── Views/
│ ├── Shared/
│ │ └── Error.cshtml
│ └── Error/
│ └── Index.cshtml
├── Program.cs (或 Startup.cs 取决于 .NET 6+ 模板)
└── ...
-
Program.cs(或Startup.Configure):var builder = WebApplication.CreateBuilder(args); // 添加服务和日志记录配置... builder.Services.AddControllersWithViews(); builder.Services.AddSingleton<ILogger, MyLoggerImpl>(); // 替换为你的日志实现注册 var app = builder.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error/Index"); // 【关键】异常处理中间件指向 ErrorController app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); app.Run(); -
ErrorController.cs:[AllowAnonymous] public class ErrorController : Controller { private readonly IHostEnvironment _env; public ErrorController(IHostEnvironment env) { _env = env; } [Route("Error/Index")] // 匹配中间件配置的路径 [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult Index() { // 尝试获取原始异常特征 var exceptionFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>(); var exception = exceptionFeature?.Error; // 创建视图模型 var viewModel = new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, ErrorMessage = exception?.Message ?? "发生了一个意外错误。", StackTrace = _env.IsDevelopment() ? exception?.StackTrace : null, ShowDetails = _env.IsDevelopment() }; // 尝试确定 StatusCode (可能由前一个中间件设置) var statusCodeReExecuteFeature = HttpContext.Features.Get<IStatusCodeReExecuteFeature>(); if (statusCodeReExecuteFeature != null) { int statusCode = HttpContext.Response.StatusCode; viewModel.StatusCode = statusCode.ToString(); } else { viewModel.StatusCode = "500"; } return View(viewModel); } } -
ErrorViewModel.cs:public class ErrorViewModel { public string StatusCode { get; set; } = "500"; public string? RequestId { get; set; } public bool ShowDetails { get; set; } public string? ErrorMessage { get; set; } public string? StackTrace { get; set; } } -
Views/Error/Index.cshtml:@model ErrorViewModel @{ ViewData["Title"] = "错误"; } <h1 class="text-danger">错误 @Model.StatusCode</h1> <h2>处理您的请求时出错。</h2> <p>请求 ID: <code>@Model.RequestId</code></p> @if (Model.ShowDetails) { <div class="alert alert-warning"> <h5>错误信息:</h5> <p>@Model.ErrorMessage</p> </div> <h5>堆栈跟踪:</h5> <pre>@Model.StackTrace</pre> } else { <p>很抱歉给您带来不便。技术团队已收到此问题通知。如需进一步帮助,请联系支持并提供请求 ID。</p> } -
日志实现 (
MyLoggerImpl):根据你选择的日志库配置实现,在Program.cs的服务容器中注册为ILogger。
6. 常见问题与陷阱#
- 全局过滤器没生效?
- 检查是否 全局注册 (
GlobalFilters.Filters.Addin MVC5 或options.Filters.Addin Core)。 - 检查注册顺序(确保没有前置过滤器
catch并handled了异常)。 - 检查 命名空间和类名 是否正确。
- MVC5: 确保在
Global.asax中Application_Start调用了FilterConfig.RegisterGlobalFilters。 - Core: 检查
Startup.ConfigureServices中是否添加了ControllersWithViews并配置了过滤器。 - 在
OnException中设置filterContext.ExceptionHandled = true防止异常继续传播。
- 检查是否 全局注册 (
Application_Error和自定义过滤器哪个先执行?IExceptionFilter.OnException在异常发生时在 MVC 管道内部第一时间执行。- 如果
IExceptionFilter设置了ExceptionHandled = true,异常不会传播到Application_Error。 - 如果
IExceptionFilter没有处理异常(ExceptionHandled = false),或者异常发生在 MVC 管道之外(如 View 渲染前就已经发生错误),那么异常最终会冒泡到Application_Error。
- 返回 JSON 时设置
Content-Type:- 在 Filter 中设置
filterContext.HttpContext.Response.ContentType = "application/json";。 - 在 Middleware 中设置
context.Response.ContentType = "application/json";。 - 忘记设置会导致客户端无法正确解析响应,可能引发二次错误。
- 在 Filter 中设置
- 处理静态文件错误:
IExceptionFilter和Application_Error通常处理不了静态文件请求本身的错误(因为它们在进入 MVC 管道前就失败了)。- 解决方案: 在
web.config(<system.webServer><httpErrors>) 或 Web 服务器(IIS、Nginx、Apache)配置中为静态文件错误设置友好的默认错误页面。 - ASP.NET Core:
UseExceptionHandler能捕获发生在静态文件中间件 (UseStaticFiles()) 之前的异常,但发生在静态文件中间件处理(读取文件时)的错误,需要由中间件本身处理或服务器配置处理。
- Core 中间件顺序的重要性:
UseExceptionHandler必须放置在尽可能早在管道中(通常紧跟在环境检测中间件之后),这样才能捕获后续所有中间件中发生的异常。- 错误发生在
UseExceptionHandler之前中间件中的话,它将无法被捕获(这时需要服务器级错误处理)。
7. 总结#
自定义全局异常处理是构建稳定、安全和用户友好的 ASP.NET (Core) MVC 应用程序不可或缺的部分。本文详细介绍了三种主要实现方式:
Application_Error:适用于遗留 MVC 5应用捕获所有“漏网”异常,代码略显繁杂。- 自定义
IExceptionFilter:标准的 MVC 方式,逻辑清晰,能良好集成 DI,适合 MVC 管道内部异常,推荐与 MVC 紧密集成的方案使用。 - ASP.NET Core
UseExceptionHandlerMiddleware + ErrorController:Core 首选方案,捕获范围最广(整个请求管道),控制灵活,完全符合 Core 的架构哲学,强烈推荐在 ASP.NET Core 项目中使用此方式。
无论采用哪种方式,遵循记录日志、精确映射状态码、提供友好用户界面、严防信息泄露、区分错误类型处理等最佳实践是成功的关键。通过精心设计的全局异常处理机制,你将显著提升应用的健壮性、可维护性和用户满意度。
8. 参考文献#
- Microsoft Docs: Handle errors in ASP.NET Core
- Microsoft Docs: Filters in ASP.NET Core
- Microsoft Docs: Application_Error (Legacy ASP.NET)
- Microsoft Docs: HandleErrorAttribute Class (Legacy)
- RFC 7807: Problem Details for HTTP APIs
- Serilog: .NET Logging Library
- NLog: Advanced .NET Logging