目录
- 一、先搞懂核心:ActionFilterAttribute 是什么?(生活类比 + 流程图)
- 小节:过滤器 = 请求处理的 “安检 + 后勤”
- 二、代码实战:自定义 ActionFilterAttribute 完整示例
- 小节:从 0 到 1 写一个可用的自定义过滤器
- 步骤 1:创建自定义过滤器类(继承 ActionFilterAttribute)
- 步骤 2:注册 / 使用过滤器(3 种方式)
- 方式 1:Action 级别(局部使用)
- 方式 2:控制器级别(整个控制器生效)
- 方式 3:全局注册(所有控制器 / Action 生效)
- 步骤 3:测试效果
- 三、常踩的坑(附解决方案 + 生活类比)
- 小节:避开这些坑,过滤器才好用
- 四、总结
作为ASP.NET开发者,控制器(Controller)是处理前端请求的 “中枢大脑”,而过滤器(Filter)则是给这个大脑加装的 “智能插件”—— 能在请求处理的不同阶段自动执行逻辑,比如日志记录、权限校验、参数校验等。其中,继承ActionFilterAttribute实现自定义过滤器是日常开发中最常用的方式,但新手很容易踩坑。本文就从代码实战、避坑指南、生活类比三个维度,把自定义ActionFilterAttribute讲透,让你少走弯路。
一、先搞懂核心:ActionFilterAttribute 是什么?(生活类比 + 流程图)
小节:过滤器 = 请求处理的 “安检 + 后勤”
先举个生活例子:你去餐厅吃饭,从进门到用餐结束的流程是「进门→选座→点餐→用餐→结账→离开」。对应ASP.NET中 Controller 处理请求的流程是「请求到达→Action 执行前→Action 执行→Action 执行后→结果返回前→响应返回」。
ActionFilterAttribute就像餐厅的 “服务管家”,可以在「Action 执行前」(比如核对预定信息)、「Action 执行后」(比如询问用餐体验)这两个核心节点插入自定义逻辑,不入侵核心的 “做菜(业务逻辑)” 流程,实现功能解耦。
控制器请求 + ActionFilter 执行流程图
ActionFilterAttribute核心重写方法(4 个,常用前 2 个):
1.OnActionExecuting:Action 执行前触发(前置逻辑);
2.OnActionExecuted:Action 执行后触发(后置逻辑);
3.OnResultExecuting:返回 Result 前触发;
4.OnResultExecuted:返回 Result 后触发。
二、代码实战:自定义 ActionFilterAttribute 完整示例
小节:从 0 到 1 写一个可用的自定义过滤器
我们以 “接口访问日志记录” 为例,实现一个自定义日志过滤器,记录每个接口的请求参数、执行时间、响应状态。
步骤 1:创建自定义过滤器类(继承 ActionFilterAttribute)
usingMicrosoft.AspNetCore.Mvc;usingMicrosoft.AspNetCore.Mvc.Filters;usingSystem;usingSystem.Diagnostics;usingSystem.Text;namespaceWebDemo.Filters{/// <summary>/// 自定义接口访问日志过滤器/// </summary>publicclassApiLogFilter:ActionFilterAttribute{// 记录接口执行耗时privateStopwatch_stopwatch;/// <summary>/// Action执行前:初始化计时器+记录请求信息/// </summary>/// <param name="context">Action执行上下文(包含请求、参数等)</param>publicoverridevoidOnActionExecuting(ActionExecutingContextcontext){// 1. 初始化计时器_stopwatch=Stopwatch.StartNew();// 2. 获取请求基础信息varhttpContext=context.HttpContext;varrequest=httpContext.Request;varcontrollerName=context.RouteData.Values["controller"]?.ToString();varactionName=context.RouteData.Values["action"]?.ToString();// 3. 拼接请求参数(GET/POST参数都获取)StringBuilderparamBuilder=newStringBuilder();// 获取GET参数foreach(varqueryinrequest.Query){paramBuilder.Append($"{query.Key}={query.Value}&");}// 获取POST表单参数(JSON参数需单独解析,这里简化演示)foreach(varforminrequest.Form){paramBuilder.Append($"{form.Key}={form.Value}&");}varparamStr=paramBuilder.ToString().TrimEnd('&');// 4. 打印日志(实际项目可写入日志框架如Serilog/NLog)Console.WriteLine($"【请求开始】[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] "+$"控制器:{controllerName}| 方法:{actionName}| "+$"请求地址:{request.Path}| 请求参数:{paramStr}");base.OnActionExecuting(context);}/// <summary>/// Action执行后:记录执行耗时+响应状态/// </summary>/// <param name="context">Action执行完成上下文</param>publicoverridevoidOnActionExecuted(ActionExecutedContextcontext){// 停止计时器_stopwatch.Stop();varelapsedMilliseconds=_stopwatch.ElapsedMilliseconds;// 获取响应状态varstatusCode=context.HttpContext.Response.StatusCode;stringerrorMsg=string.Empty;// 捕获Action执行异常if(context.Exception!=null){errorMsg=context.Exception.Message;// 若需要继续抛出异常,注释下面这行;若想吞掉异常,保留// context.ExceptionHandled = true;}varcontrollerName=context.RouteData.Values["controller"]?.ToString();varactionName=context.RouteData.Values["action"]?.ToString();// 打印执行结果日志Console.WriteLine($"【请求结束】[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] "+$"控制器:{controllerName}| 方法:{actionName}| "+$"执行耗时:{elapsedMilliseconds}ms | 响应状态码:{statusCode}| 异常信息:{errorMsg}");base.OnActionExecuted(context);}}}步骤 2:注册 / 使用过滤器(3 种方式)
过滤器的使用分为 “全局注册”、“控制器级”、“Action 级”,按需选择:
方式 1:Action 级别(局部使用)
usingMicrosoft.AspNetCore.Mvc;usingWebDemo.Filters;namespaceWebDemo.Controllers{[ApiController][Route("api/[controller]")]publicclassUserController:ControllerBase{// 仅该Action生效[ApiLogFilter][HttpGet("GetUser")]publicIActionResultGetUser(intid){// 模拟业务逻辑System.Threading.Thread.Sleep(100);returnOk(new{Id=id,Name="张三",Age=25});}[HttpPost("AddUser")]publicIActionResultAddUser([FromBody]UserDtouser){// 该Action不记录日志returnOk("添加成功");}}publicclassUserDto{publicstringName{get;set;}publicintAge{get;set;}}}方式 2:控制器级别(整个控制器生效)
[ApiController][Route("api/[controller]")][ApiLogFilter]// 控制器下所有Action都生效publicclassOrderController:ControllerBase{[HttpGet("GetOrder")]publicIActionResultGetOrder(intorderId){returnOk(new{OrderId=orderId,Amount=99.9});}}方式 3:全局注册(所有控制器 / Action 生效)
在Program.cs中注册:
varbuilder=WebApplication.CreateBuilder(args);// 添加控制器服务builder.Services.AddControllers(options=>{// 全局注册自定义日志过滤器options.Filters.Add<ApiLogFilter>();});varapp=builder.Build();// 中间件配置...app.MapControllers();app.Run();步骤 3:测试效果
调用/api/User/GetUser?id=1,控制台输出:
【请求开始】[2025-12-13 10:00:00] 控制器:User | 方法:GetUser | 请求地址:/api/User/GetUser | 请求参数:id=1 【请求结束】[2025-12-13 10:00:00] 控制器:User | 方法:GetUser | 执行耗时:102ms | 响应状态码:200 | 异常信息:三、常踩的坑(附解决方案 + 生活类比)
小节:避开这些坑,过滤器才好用
新手使用ActionFilterAttribute最容易踩以下 6 个坑,每个坑都配生活类比和解决方案:
| 坑点 | 生活类比 | 解决方案 |
|---|---|---|
| 1.全局过滤器覆盖局部逻辑,导致预期外执行 | 给所有餐厅菜品加辣(全局),但个别菜品要求不辣(局部),结果全辣 | 1. 局部过滤器可通过Order属性调整执行顺序;2. 敏感逻辑优先用局部过滤器;3. 全局过滤器加开关(如配置文件控制是否生效) |
| 2.未处理ActionExecutedContext.Exception,导致异常吞掉 / 重复处理 | 餐厅服务员发现菜品有问题,既不反馈后厨,也不告诉顾客,导致问题被掩盖 | 1. 异常需明确处理:要么context.ExceptionHandled = true吞掉并返回友好提示;要么不设置,让框架继续抛出;2. 过滤器中捕获异常后,务必记录日志 |
| 3.过滤器中获取 POST JSON 参数为空 | 餐厅服务员想核对顾客的线上点餐信息,但没拿到订单数据(数据还没解析) | 1. OnActionExecuting中获取 JSON 参数需手动解析:var body = await context.HttpContext.Request.BodyReader.ReadAsync();var json = Encoding.UTF8.GetString(body.Buffer);2. 注意读取后重置流位置:context.HttpContext.Request.Body.Position = 0; |
| 4.过滤器中使用异步逻辑但用了同步重写方法 | 服务员同时接多个点餐请求,却用 “逐个处理” 的同步方式,导致卡顿 | 重写异步版本方法:OnActionExecutingAsync/OnActionExecutedAsync,而非同步的OnActionExecuting/OnActionExecuted,示例:public override async Task OnActionExecutingAsync(ActionExecutingContext context, CancellationToken cancellationToken) |
| 5.忽略Order属性,导致多个过滤器执行顺序混乱 | 餐厅先上主食再上凉菜,不符合顾客 “先凉菜后主食” 的要求 | 过滤器通过Order属性指定执行顺序(数值越小越先执行):[ApiLogFilter(Order = 1)]、[AuthFilter(Order = 0)](权限校验优先于日志) |
| 6.过滤器中修改ModelState但未生效 | 服务员发现顾客点的菜品售罄,却没告知收银台,导致订单仍提交 修改ModelState后,需手动设置context.Result终止后续流程:context.ModelState.AddModelError(“Name”, “姓名不能为空”);context.Result = new BadRequestObjectResult(context.ModelState);坑点实战示例(解决 “POST JSON 参数为空” 问题) | 修改ApiLogFilter的OnActionExecuting方法,支持获取 JSON 参数: |
publicoverrideasyncvoidOnActionExecuting(ActionExecutingContextcontext){_stopwatch=Stopwatch.StartNew();varhttpContext=context.HttpContext;varrequest=httpContext.Request;varcontrollerName=context.RouteData.Values["controller"]?.ToString();varactionName=context.RouteData.Values["action"]?.ToString();StringBuilderparamBuilder=newStringBuilder();// 获取GET参数foreach(varqueryinrequest.Query){paramBuilder.Append($"{query.Key}={query.Value}&");}// 新增:获取POST JSON参数if(request.ContentType?.Contains("application/json")==true){// 读取请求体varstream=request.Body;varbuffer=newbyte[Convert.ToInt32(request.ContentLength)];awaitstream.ReadAsync(buffer,0,buffer.Length);varjsonParam=Encoding.UTF8.GetString(buffer);paramBuilder.Append($"JSON参数:{jsonParam}");// 重置流位置,避免后续ModelBinder读取不到参数request.Body.Position=0;}varparamStr=paramBuilder.ToString().TrimEnd('&');Console.WriteLine($"【请求开始】[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] "+$"控制器:{controllerName}| 方法:{actionName}| "+$"请求地址:{request.Path}| 请求参数:{paramStr}");base.OnActionExecuting(context);}四、总结
自定义ActionFilterAttribute是ASP.NET中解耦横切逻辑(日志、权限、参数校验等)的 “利器”,核心是抓住「Action 执行前 / 后」两个核心节点,灵活选择全局 / 控制器 / Action 级使用方式。避开 “参数获取为空、异步逻辑同步写、执行顺序混乱” 等坑,就能让过滤器成为 Controller 的 “得力助手”,而非 “埋雷点”。
留言互动
你在项目中用ActionFilterAttribute实现过哪些功能?(比如权限校验、接口限流、参数脱敏等)有没有遇到过本文没提到的坑?欢迎在评论区分享,一起交流避坑技巧~
专栏说明:本文聚焦ActionFilterAttribute核心实战,后续会更新其他过滤器(如AuthorizationFilterAttribute权限过滤器、ExceptionFilterAttribute异常过滤器)的使用技巧,关注我,持续解锁ASP.NET进阶干货~