目录
-
- [一、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)的实战教程~)