ASP.NET MVC 自定义全局异常处理:构建健壮应用的基石

在 ASP.NET MVC 应用开发中,未处理的异常是用户体验的“杀手”和系统稳定性的“隐患”。默认的“黄屏死机”(Yellow Screen of Death, YSOD) 不仅不友好,还可能暴露敏感信息。全局异常处理机制 正是为了解决此问题而生,它允许你集中捕获、记录异常,并以统一、可控的方式向用户返回友好的错误页面或结构化的错误信息(如 API 返回 JSON)。自定义全局异常处理是构建专业、可靠、用户友好的 Web 应用程序的关键环节。本文将深入探讨其原理、多种实现方式、最佳实践,并提供实用代码示例。

目录#

  1. 为什么需要全局异常处理?
  2. ASP.NET MVC 异常处理机制概览
    • HandleErrorAttribute 的局限性
  3. 自定义全局异常处理方法
    • 方法 1:使用 Application_Error (Global.asax)
      • 原理
      • 实现步骤 (带代码)
      • 优缺点
    • 方法 2:创建自定义错误过滤器 (IExceptionFilter)
      • 原理 (OnException / OnExceptionAsync)
      • 实现自定义 Filter (带代码)
      • 全局注册 Filter
      • filterContext.ExceptionHandledfilterContext.Result
    • 方法 3(ASP.NET Core):使用中间件 (Middleware)
      • UseExceptionHandler 中间件简介
      • 配置自定义错误处理管道
      • 示例:处理不同环境(Development/Production)
  4. 关键实现细节与最佳实践
    • 记录日志(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 中需注意)
  5. 完整示例:集成自定义过滤器
    • 场景:Web 应用(混合视图 + Web API)
    • 代码:CustomExceptionHandlerFilter.cs
    • 代码:ErrorController.cs
    • 代码:注册 Filter (FilterConfig.csStartup.cs)
    • 代码:自定义错误视图
    • 代码:API 错误响应格式
  6. 常见问题与陷阱(Troubleshooting)
    • 全局过滤器没生效?
    • Application_Error 和自定义过滤器哪个先执行?
    • 返回 JSON 时设置 Content-Type
    • 处理静态文件错误 (IIS 配置补充)
    • Core 中间件顺序的重要性
  7. 总结
  8. 参考文献

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 内部管道(包括过滤器)中被处理的异常最终都会冒泡到这里。

实现步骤:

  1. 打开/创建 Global.asax 文件。

  2. 重写 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。通过创建全局注册的自定义过滤器,可以实现集中处理。

实现步骤:

  1. 创建自定义异常过滤器类

    // 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; } // 仅在开发环境暴露
    }
  2. 全局注册过滤器

    • 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.asaxApplication_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 中间件内部的、静态文件中间件的、或者身份验证中间件的)。

实现步骤:

  1. Startup.csConfigure 方法中配置中间件:
    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(); });
    }
  2. (可选但推荐)创建 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);
        }
    }
  3. 创建 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+
    }
  4. 创建 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.NameUserId,如果可用)。
      • 请求唯一标识符(如 TraceIdentifier)。
    • 级别: 未处理异常通常使用 ErrorCritical 级别。
  • 区分错误类型处理:

    • 业务逻辑/校验失败: (例如 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 错误格式,例如:
      {
          "error": {
              "code": "404",
              "message": "Requested user with ID '123' was not found."
              // "details": "Additional details for debugging (optional, Dev only)"
              // "requestId": "0HLR5VQJ3AAQG"
          }
      }
      标准如 RFC 7807 (Problem Details) 更佳。
  • HTTP 状态码的精准控制:

    • 务必设置 context.Response.StatusCode (Middleware) 或 filterContext.HttpContext.Response.StatusCode (Filter)。
    • 正确使用状态码有助于客户端(浏览器、前端框架、API 调用方)理解错误性质。
    • 使用 .NET 提供的 HttpStatusCode 枚举或 StatusCodes 常量 (Core) 代替数字。
  • 防止敏感信息泄露 (Security):

    • 生产环境:
      • 绝不 将异常的 StackTraceInnerException 细节、服务器文件路径、数据库连接字符串、内部类名方法名等直接展示给终端用户。
      • 在错误视图中使用通用消息(如“处理您的请求时发生错误”)。
      • 在 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+ 模板)
└── ...
  1. 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();
  2. 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);
        }
    }
  3. 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; }
    }
  4. 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>
    }
  5. 日志实现 (MyLoggerImpl):根据你选择的日志库配置实现,在 Program.cs 的服务容器中注册为 ILogger

6. 常见问题与陷阱#

  • 全局过滤器没生效?
    • 检查是否 全局注册 (GlobalFilters.Filters.Add in MVC5 或 options.Filters.Add in Core)。
    • 检查注册顺序(确保没有前置过滤器 catchhandled 了异常)。
    • 检查 命名空间和类名 是否正确。
    • MVC5: 确保在 Global.asaxApplication_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";
    • 忘记设置会导致客户端无法正确解析响应,可能引发二次错误。
  • 处理静态文件错误:
    • IExceptionFilterApplication_Error 通常处理不了静态文件请求本身的错误(因为它们在进入 MVC 管道前就失败了)。
    • 解决方案:web.config (<system.webServer><httpErrors>) 或 Web 服务器(IIS、Nginx、Apache)配置中为静态文件错误设置友好的默认错误页面。
    • ASP.NET Core: UseExceptionHandler 能捕获发生在静态文件中间件 (UseStaticFiles()) 之前的异常,但发生在静态文件中间件处理(读取文件时)的错误,需要由中间件本身处理或服务器配置处理。
  • Core 中间件顺序的重要性:
    • UseExceptionHandler 必须放置在尽可能早在管道中(通常紧跟在环境检测中间件之后),这样才能捕获后续所有中间件中发生的异常。
    • 错误发生在 UseExceptionHandler 之前中间件中的话,它将无法被捕获(这时需要服务器级错误处理)。

7. 总结#

自定义全局异常处理是构建稳定、安全和用户友好的 ASP.NET (Core) MVC 应用程序不可或缺的部分。本文详细介绍了三种主要实现方式:

  1. Application_Error:适用于遗留 MVC 5应用捕获所有“漏网”异常,代码略显繁杂。
  2. 自定义 IExceptionFilter:标准的 MVC 方式,逻辑清晰,能良好集成 DI,适合 MVC 管道内部异常,推荐与 MVC 紧密集成的方案使用
  3. ASP.NET Core UseExceptionHandler Middleware + ErrorControllerCore 首选方案,捕获范围最广(整个请求管道),控制灵活,完全符合 Core 的架构哲学,强烈推荐在 ASP.NET Core 项目中使用此方式

无论采用哪种方式,遵循记录日志、精确映射状态码、提供友好用户界面、严防信息泄露、区分错误类型处理等最佳实践是成功的关键。通过精心设计的全局异常处理机制,你将显著提升应用的健壮性、可维护性和用户满意度。

8. 参考文献#

  1. Microsoft Docs: Handle errors in ASP.NET Core
  2. Microsoft Docs: Filters in ASP.NET Core
  3. Microsoft Docs: Application_Error (Legacy ASP.NET)
  4. Microsoft Docs: HandleErrorAttribute Class (Legacy)
  5. RFC 7807: Problem Details for HTTP APIs
  6. Serilog: .NET Logging Library
  7. NLog: Advanced .NET Logging