【ASP.NET Core 进阶】Controller 过滤器之 ExceptionFilter:全局异常捕获的 “终极方案”(附避坑指南)

目录

    • [一、ExceptionFilter 是什么?先搞懂核心定位](#一、ExceptionFilter 是什么?先搞懂核心定位)
      • [小节:ExceptionFilter 的 "角色定位"------ 程序运行的 "急诊医生"](#小节:ExceptionFilter 的 “角色定位”—— 程序运行的 “急诊医生”)
    • [二、ExceptionFilter 实战:全局异常统一处理](#二、ExceptionFilter 实战:全局异常统一处理)
      • [小节:从 0 到 1 实现全局异常捕获,告别零散 try-catch](#小节:从 0 到 1 实现全局异常捕获,告别零散 try-catch)
      • [步骤 1:定义统一错误响应模型](#步骤 1:定义统一错误响应模型)
      • [步骤 2:自定义 ExceptionFilter](#步骤 2:自定义 ExceptionFilter)
      • [步骤 3:注册 ExceptionFilter(3 种方式)](#步骤 3:注册 ExceptionFilter(3 种方式))
        • [方式 1:全局注册(所有 Controller 生效,推荐)](#方式 1:全局注册(所有 Controller 生效,推荐))
        • [方式 2:控制器级注册(仅当前 Controller 生效)](#方式 2:控制器级注册(仅当前 Controller 生效))
        • [方式 3:Action 级注册(仅当前 Action 生效)](#方式 3:Action 级注册(仅当前 Action 生效))
      • [步骤 4:创建自定义错误页面(可选)](#步骤 4:创建自定义错误页面(可选))
    • [三、ExceptionFilter 常踩的 10 个坑(附避坑方案)](#三、ExceptionFilter 常踩的 10 个坑(附避坑方案))
      • [小节:踩坑不可怕,关键是知道 "为什么错、怎么改"](#小节:踩坑不可怕,关键是知道 “为什么错、怎么改”)
    • [四、ExceptionFilter 的适用场景(列表)](#四、ExceptionFilter 的适用场景(列表))
      • [小节:知道 "什么时候用",才是真正掌握](#小节:知道 “什么时候用”,才是真正掌握)
    • [五、ExceptionFilter vs UseExceptionHandler:该怎么选?](#五、ExceptionFilter vs UseExceptionHandler:该怎么选?)
    • 六、互动环节:你在异常处理中遇到过哪些问题?

作为ASP.NET Core 开发者,你一定遇到过这样的场景:Action 里的代码抛出未捕获的异常,页面直接返回 500 错误,给用户的体验极差;如果每个 Action 都写 try-catch,又会造成代码冗余、维护成本高。而 ExceptionFilter(异常过滤器)正是解决这个问题的 "利器"------ 它能全局捕获 Controller 层的所有未处理异常,统一封装错误响应、记录日志、返回友好提示,是 Controller 层过滤器中 "兜底保障" 的核心角色。

本文会用生活化例子、完整代码、流程图拆解 ExceptionFilter 的核心用法,盘点 90% 开发者踩过的坑,让你彻底掌握全局异常处理的正确姿势!

一、ExceptionFilter 是什么?先搞懂核心定位

小节:ExceptionFilter 的 "角色定位"------ 程序运行的 "急诊医生"

我们先给 ExceptionFilter 做个生活化类比:

你去医院看病(客户端发起请求),医生诊疗(Action 执行)过程中突发意外(代码抛出异常),急诊医生(ExceptionFilter)会第一时间介入:记录病情(日志)、给出统一的救治方案(返回友好错误)、避免病情扩散(防止程序崩溃)------ExceptionFilter 就是这个 "急诊医生",专门处理 Action 执行过程中未被捕获的异常,且只针对 Controller 层生效。

核心定义

ExceptionFilter 是ASP.NET Core 过滤器的核心类型之一,作用于Action 执行过程中抛出未处理异常时,分为两个核心方法(同步 / 异步):

  • OnException(同步):捕获异常后同步处理逻辑(记录日志、封装响应);
  • OnExceptionAsync(异步):异步场景下的异常处理(推荐,适配异步 Action)。
    ExceptionFilter 的执行位置(流程图)

无异常 有异常 客户端发起请求 AuthorizationFilter 授权 ResourceFilter 资源缓存 ActionFilter-OnActionExecuting Action执行前 Action执行 可能抛出异常 ResultFilter 结果处理 ExceptionFilter-OnException 异常捕获处理 封装错误响应返回客户端 View/Json渲染返回客户端

二、ExceptionFilter 实战:全局异常统一处理

小节:从 0 到 1 实现全局异常捕获,告别零散 try-catch

日常开发中,我们需要实现 3 个核心需求:

1.捕获所有 Controller 层未处理异常;

2.记录异常日志(含请求信息、异常堆栈);

3.给前端返回统一格式的错误响应(区分开发 / 生产环境)。
完整代码实现

步骤 1:定义统一错误响应模型

先封装全局错误返回格式,保证前端能统一解析:

csharp 复制代码
/// <summary>
/// 全局统一错误响应模型
/// </summary>
public class ErrorResponse
{
    /// <summary>
    /// 错误码
    /// </summary>
    public int Code { get; set; }

    /// <summary>
    /// 错误信息
    /// </summary>
    public string Message { get; set; }

    /// <summary>
    /// 异常详情(仅开发环境返回)
    /// </summary>
    public string Detail { get; set; }

    /// <summary>
    /// 请求ID(用于排查问题)
    /// </summary>
    public string RequestId { get; set; }

    /// <summary>
    /// 请求路径
    /// </summary>
    public string Path { get; set; }
}

步骤 2:自定义 ExceptionFilter

实现IExceptionFilter(同步)或IAsyncExceptionFilter(异步,推荐):

csharp 复制代码
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Diagnostics;

/// <summary>
/// 全局异常过滤器
/// </summary>
public class GlobalExceptionFilter : IAsyncExceptionFilter
{
    private readonly ILogger<GlobalExceptionFilter> _logger;
    private readonly IHostEnvironment _env;

    /// <summary>
    /// 构造函数注入依赖
    /// </summary>
    /// <param name="logger">日志器</param>
    /// <param name="env">环境变量</param>
    public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger, IHostEnvironment env)
    {
        _logger = logger;
        _env = env;
    }

    /// <summary>
    /// 异步异常处理核心方法
    /// </summary>
    /// <param name="context">异常上下文</param>
    /// <returns></returns>
    public async Task OnExceptionAsync(ExceptionContext context)
    {
        // 1. 提取基础信息
        var exception = context.Exception; // 捕获的异常对象
        var httpContext = context.HttpContext;
        var requestId = Activity.Current?.Id ?? httpContext.TraceIdentifier; // 请求ID
        var requestPath = httpContext.Request.Path; // 请求路径

        // 2. 记录详细异常日志(关键:方便排查问题)
        _logger.LogError(
            exception, 
            "全局异常捕获 | RequestId:{RequestId} | Path:{Path} | 异常信息:{Message}",
            requestId,
            requestPath,
            exception.Message
        );

        // 3. 封装错误响应(区分开发/生产环境)
        var errorResponse = new ErrorResponse
        {
            Code = StatusCodes.Status500InternalServerError,
            Message = _env.IsDevelopment() ? exception.Message : "服务器内部错误,请稍后重试",
            Detail = _env.IsDevelopment() ? exception.StackTrace : null, // 生产环境隐藏堆栈
            RequestId = requestId,
            Path = requestPath
        };

        // 4. 设置异常已处理(避免系统默认的500页面)
        context.ExceptionHandled = true;

        // 5. 返回统一错误响应(支持View/Json两种场景)
        var result = new ObjectResult(errorResponse)
        {
            StatusCode = StatusCodes.Status500InternalServerError
        };

        // 如果是AJAX/API请求,返回Json;否则返回错误页面(可选)
        if (httpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest" || 
            requestPath.StartsWith("/api"))
        {
            context.Result = result;
        }
        else
        {
            // 返回自定义错误页面(需提前创建Views/Shared/Error.cshtml)
            context.Result = new ViewResult
            {
                ViewName = "~/Views/Shared/Error.cshtml",
                ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
                {
                    ["ErrorModel"] = errorResponse
                }
            };
        }

        await Task.CompletedTask;
    }
}

步骤 3:注册 ExceptionFilter(3 种方式)

ExceptionFilter 的注册分 "全局、控制器、Action" 三级,全局注册是最常用的方式:

方式 1:全局注册(所有 Controller 生效,推荐)

在Program.cs中添加:

csharp 复制代码
var builder = WebApplication.CreateBuilder(args);

// 添加MVC并注册全局异常过滤器
builder.Services.AddControllersWithViews(options =>
{
    options.Filters.Add<GlobalExceptionFilter>();
});

// 注册日志、环境变量等依赖(已自动注册,无需额外操作)

var app = builder.Build();

// 省略中间件配置(注意:全局异常过滤器不处理中间件层异常)
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
方式 2:控制器级注册(仅当前 Controller 生效)
csharp 复制代码
[TypeFilter(typeof(GlobalExceptionFilter))]
public class HomeController : Controller
{
    public IActionResult Index()
    {
        // 模拟抛出异常
        throw new DivideByZeroException("除数不能为0");
        return View();
    }
}
方式 3:Action 级注册(仅当前 Action 生效)
csharp 复制代码
public class HomeController : Controller
{
    [TypeFilter(typeof(GlobalExceptionFilter))]
    public IActionResult Index()
    {
        throw new NullReferenceException("对象引用未设置为实例");
        return View();
    }
}

步骤 4:创建自定义错误页面(可选)

在Views/Shared目录下创建Error.cshtml,用于非 API 请求的错误展示:

html 复制代码
@model ErrorResponse
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>出错了</title>
    <style>
        .error-container { text-align: center; margin-top: 100px; }
        .error-code { font-size: 80px; color: #dc3545; }
        .error-msg { font-size: 20px; margin: 20px 0; }
        .error-detail { color: #6c757d; font-size: 14px; }
    </style>
</head>
<body>
    <div class="error-container">
        <div class="error-code">@Model.Code</div>
        <div class="error-msg">@Model.Message</div>
        @if (!string.IsNullOrEmpty(Model.Detail))
        {
            <div class="error-detail">@Model.Detail</div>
        }
        <div class="error-detail">Request ID: @Model.RequestId</div>
    </div>
</body>
</html>

测试效果
1.开发环境: 访问抛出异常的 Action,会返回包含异常堆栈的详细错误;
2.生产环境: 仅返回 "服务器内部错误,请稍后重试",隐藏敏感信息;
3.API 请求: 返回 JSON 格式的错误响应;
4.普通页面请求: 返回自定义错误页面。

三、ExceptionFilter 常踩的 10 个坑(附避坑方案)

小节:踩坑不可怕,关键是知道 "为什么错、怎么改"

ExceptionFilter 看似简单,但新手容易踩以下坑,我们用表格清晰拆解:

坑位 问题现象 根本原因 避坑方案
1 过滤器捕获不到异常 异常被 Action 内部的 try-catch 捕获并吞掉 1. Action 中捕获异常后需重新抛出(throw);2. 仅用 ExceptionFilter 处理未捕获异常
2 过滤器中注入的 ILogger 为 null 直接用[GlobalExceptionFilter]标注,未通过 DI 容器创建 必须用[TypeFilter(typeof(GlobalExceptionFilter))]或全局注册,禁止直接标注过滤器类
3 生产环境返回了异常堆栈 未区分环境变量,直接返回了 exception.StackTrace 用IHostEnvironment.IsDevelopment()判断环境,生产环境隐藏 StackTrace
4 捕获到异常但页面仍显示默认 500 错误 未设置context.ExceptionHandled = true 处理完异常后必须将 ExceptionHandled 设为 true,告诉系统 "异常已处理"
5 中间件层的异常捕获不到 ExceptionFilter 仅处理 Controller 层异常,不处理中间件异常 中间件层异常需用app.UseExceptionHandler兜底,或自定义中间件捕获
6 异步 Action 的异常捕获不到 实现了同步 IExceptionFilter,未实现 IAsyncExceptionFilter 异步 Action 必须实现IAsyncExceptionFilter(OnExceptionAsync)
7 过滤器中修改 Response 无效 已调用context.Result设置返回结果,再修改 Response 会冲突 统一通过context.Result设置返回结果,不要直接操作 Response
8 全局过滤器和控制器级过滤器同时生效 多级过滤器重复注册,导致日志重复记录 1. 统一注册级别;2. 在过滤器中添加 "已处理" 标记,避免重复处理
9 授权过滤器(AuthorizationFilter)的异常捕获不到 ExceptionFilter 执行在 AuthorizationFilter 之后,授权异常已提前终止 授权异常需在app.UseAuthorization()后添加异常处理中间件,或自定义授权过滤器
10 过滤器中记录的请求参数为空 读取 Request.Body 时流已被消耗,无法重复读取 提前启用请求体重新读取:builder.Services.Configure<Microsoft.AspNetCore.Http.Features.FormOptions>(x => x.BufferBody = true);

生活化类比理解坑位 4:

急诊医生(ExceptionFilter)处理完病人(异常)后,未告诉护士(系统)"已处理",护士仍会按默认流程呼叫其他医生(系统默认 500 错误)------ 所以必须设置ExceptionHandled = true,明确 "这个异常我接手了"。

四、ExceptionFilter 的适用场景(列表)

小节:知道 "什么时候用",才是真正掌握

ExceptionFilter 的核心价值是 "统一处理 Controller 层未捕获异常",以下场景优先用它:

1.全局记录 Controller 层异常日志(含请求上下文);

2.给前端返回统一格式的错误响应(JSON / 页面);

3.区分环境返回不同的错误信息(开发环境详、生产环境简);

4.针对特定异常做自定义处理(如数据库异常返回 "数据操作失败");

5.异常后做兜底操作(如释放资源、回滚事务)。

五、ExceptionFilter vs UseExceptionHandler:该怎么选?

小节:分清边界,组合使用效果最佳

很多开发者会混淆 ExceptionFilter 和app.UseExceptionHandler,这里做清晰对比:

特性 ExceptionFilter UseExceptionHandler(中间件)
作用范围 仅 Controller 层 全链路(中间件 + Controller)
灵活性 可针对 Controller/Action 粒度处理 全局兜底,粒度较粗
依赖注入 支持完整 DI 注入 需手动获取服务,灵活性稍差
适用场景 Controller 层异常的精细化处理 全局异常兜底,处理中间件层异常

最佳实践:

1.用 ExceptionFilter 处理 Controller 层异常(精细化、带业务上下文);

2.用app.UseExceptionHandler做全局兜底,处理中间件层 / 未被捕获的异常;

3.两者结合,既保证精细化处理,又避免异常遗漏。

六、互动环节:你在异常处理中遇到过哪些问题?

留言互动

如果本文解决了你的问题,欢迎留言交流:

  • 你踩过哪个 ExceptionFilter 的坑?是怎么解决的?
  • 你觉得 ExceptionFilter 和中间件异常处理哪个更好用?
  • 还有哪些异常处理的场景想了解?
    总结
    ExceptionFilter 是ASP.NET Core 中 Controller 层异常处理的 "核心工具",它就像程序的 "急诊医生",能统一捕获未处理异常、记录日志、返回友好响应,告别零散的 try-catch。
    掌握它的关键:一是分清执行阶段(仅处理 Controller 层异常),二是避开 "依赖注入 null、未设置 ExceptionHandled、环境判断错误" 等坑,三是结合UseExceptionHandler做全局兜底。
    希望本文能帮你彻底吃透 ExceptionFilter,让你的项目异常处理更优雅、更可控!
    (如果觉得本文有用,欢迎点赞 + 收藏 + 关注,后续会更新其他过滤器(ResourceFilter/AuthorizationFilter)的实战教程~)
相关推荐
梦未8 小时前
Spring控制反转与依赖注入
java·后端·spring
无限大68 小时前
验证码对抗史
后端
用户2190326527359 小时前
Java后端必须的Docker 部署 Redis 集群完整指南
linux·后端
VX:Fegn08959 小时前
计算机毕业设计|基于springboot + vue音乐管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·课程设计
bcbnb9 小时前
苹果手机iOS应用管理全指南与隐藏功能详解
后端
用户47949283569159 小时前
面试官:DNS 解析过程你能说清吗?DNS 解析全流程深度剖析
前端·后端·面试
幌才_loong9 小时前
.NET8 实时通信秘籍:WebSocket 全双工通信 + 分布式推送,代码实操全解析
后端·.net
开心猴爷9 小时前
iOS应用发布:App Store上架完整步骤与销售范围管理
后端
JSON_L9 小时前
Fastadmin API接口实现多语言提示语
后端·php·fastadmin