用户的 dashboard 要求邮箱已验证,但项目里有二十个控制器,每个 action 开头都写着同样的几行:查当前用户、问服务「邮箱验证了吗」、没验证就返回 403。有人已经复制粘贴了十遍,还有十遍正在路上。
这就是 IAsyncActionFilter 的现场。Muhammad Waseem 在推文里说它被低估了,但极其强大——它的价值不止是省掉重复代码,而是把这一类「检查需要等待」的横切逻辑,稳当地留在请求管道里。
过滤器能做什么
在 ASP.NET Core 中,过滤器让你能在请求处理管道的特定阶段之前或之后运行自定义逻辑,而不必碰控制器。微软官方文档总结了过滤器运行的完整顺序:授权过滤器最先跑,然后是资源过滤器(模型绑定之前),接着是紧接着操作方法前后运行的 action 过滤器。
值得用过滤器的原因有两个:
- 复用:同一段逻辑一次实现,应用到多个控制器或多个操作。
- 可扩展:把业务规则完全移出操作方法,action 里只剩它真正要做的业务。
同步还是异步
如果你的检查只是轻量的同步计算,用 IActionFilter——它有两个方法,OnActionExecuting 在方法执行前调用,OnActionExecuted 在执行后调用。
当检查本身需要 await 某些东西——一次数据库查询、一个服务调用——就该选 IAsyncActionFilter。它只有一个 OnActionExecutionAsync 方法,包裹着一个 next() 委托,而不是同步版本的两个分离方法:
- 调用
await next():继续执行管道,让操作方法跑起来。 - 跳过
next()并设置context.Result:完全短路请求,操作方法根本不会执行。
完整示例:邮箱验证过滤器
下面这个过滤器来自原推文,业务含义是「进入 dashboard 前,当前用户的邮箱必须已验证」。注意它用了 C# 12 的主构造函数(需要 .NET 8 或更高版本的 SDK)来接收 IUser 和 IUserService 两个依赖:
// EmailVerifiedFilter.cs
internal sealed class EmailVerifiedFilter(
IUser currentUser,
IUserService userService) : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
Guid? userId = currentUser.Id;
if (userId != null && !await userService.IsEmailVerifiedAsync(userId))
{
context.Result = new ForbidResult();
return;
}
await next();
}
}
三步把它用起来:
1. 注册到依赖注入。 在 Program.cs 里按作用域注册。这一步不能省——[ServiceFilter] 是从 DI 容器里取实例的,没注册就会在运行时抛异常:
builder.Services.AddScoped<EmailVerifiedFilter>();
2. 在目标操作上应用。 用 [ServiceFilter] 特性指出过滤器类型:
// UsersController.cs
[HttpGet("dashboard")]
[ServiceFilter(typeof(EmailVerifiedFilter))]
public IActionResult GetUserDashboard() { ... }
3. 验证行为。 用一个邮箱已验证的账号和一个未验证的账号同时请求 /users/dashboard:已验证返回 200 与正常内容;未验证得到 403 Forbidden(ForbidResult),且操作方法的代码从头到尾没有执行。
什么时候该用这个模式
原推文给了四个典型的适用场景:
- 邮箱或订阅验证(上面的例子)。
- 在操作运行前强制执行业务规则。
- 有条件的阻塞,而不必在每个 action 里写早期返回。
- 否则会被复制粘贴进十个控制器的横切逻辑。
一个重要的边界
⚠️ 对于不涉及数据库调用的纯角色或策略检查,IAsyncAuthorizationFilter 通常是更好的选择。官方文档确认授权过滤器是整个管道中最先运行的那种——它在模型绑定之前执行,因此被拒绝的请求永远不会为它不需要的绑定工作付出代价。同为检查,放在正确的管道层级,省下的是每次被拒请求的模型绑定成本。
常见问题
[ServiceFilter] 运行时抛异常说类型未注册? 最常见的踩坑点:忘记在 Program.cs 注册过滤器(AddScoped<EmailVerifiedFilter>())。ServiceFilter 从 DI 取实例,而直接用作 attribute 的过滤器类不能从 DI 注入构造函数依赖——这正是为什么示例需要 ServiceFilter 而不是直接写 [EmailVerifiedFilter]。
主构造函数编译不过? 主构造函数是 C# 12 特性,需要 .NET 8 或更高版本的 SDK。如果项目还停在旧版本,改写成普通构造函数 + 只读字段即可。
什么时候用同步的 IActionFilter? 检查是纯内存计算、没有任何 await 时。有 await 却用同步版本,会变成阻塞线程的异步代码——这是更隐蔽的错误。
ForbidResult 返回什么状态码? 403。如果你希望返回其他响应(比如带说明的 401 或自定义 JSON),设置 context.Result 为对应的结果类型即可。
ServiceFilter 的 IsReusable 是什么? 它是给运行时的一个提示:这个过滤器实例可能被跨请求复用。官方文档明确——IsReusable = true 只能用于无状态、且依赖来源是单例的过滤器;如果过滤器依赖 scoped 或 transient 服务,不要设置它。对上面的 EmailVerifiedFilter(依赖按请求解析的 IUser 与 IUserService),保持默认值即可。
过滤器和中间件怎么选? 过滤器是 MVC 框架的一部分,能访问 action 上下文、能操作结果、支持短路;中间件更底层。如果逻辑需要依赖 action 的元数据、参数或返回值,用过滤器;如果只在应用层面做请求处理,中间件可能更合适。另外 action 过滤器不支持 Razor Pages,需要时换用 Razor Page 过滤器。
找一个「第十遍」开始
想试试的话,找找项目里哪个检查已经重复出现多次——它应该在每个 action 开头做同样的等待式检查。把它抽成一个 IAsyncActionFilter,注册到 DI,用 [ServiceFilter] 挂到一个 action 上,先用未通过检查的账号验证短路,再验证正常路径不受影响。
如果检查根本不需要等数据库,就别用这个模式,把同样的心思花在授权过滤器上。
Aide Hub 会继续分享 AI 助手、开发工具与软件工程实践中的具体做法。