目录
-
- [一、先搞懂核心:ActionFilterAttribute 是什么?(生活类比 + 流程图)](#一、先搞懂核心:ActionFilterAttribute 是什么?(生活类比 + 流程图))
-
- [小节:过滤器 = 请求处理的 "安检 + 后勤"](#小节:过滤器 = 请求处理的 “安检 + 后勤”)
- [二、代码实战:自定义 ActionFilterAttribute 完整示例](#二、代码实战:自定义 ActionFilterAttribute 完整示例)
-
- [小节:从 0 到 1 写一个可用的自定义过滤器](#小节:从 0 到 1 写一个可用的自定义过滤器)
- [步骤 1:创建自定义过滤器类(继承 ActionFilterAttribute)](#步骤 1:创建自定义过滤器类(继承 ActionFilterAttribute))
- [步骤 2:注册 / 使用过滤器(3 种方式)](#步骤 2:注册 / 使用过滤器(3 种方式))
-
- [方式 1:Action 级别(局部使用)](#方式 1:Action 级别(局部使用))
- [方式 2:控制器级别(整个控制器生效)](#方式 2:控制器级别(整个控制器生效))
- [方式 3:全局注册(所有控制器 / Action 生效)](#方式 3:全局注册(所有控制器 / Action 生效))
- [步骤 3:测试效果](#步骤 3:测试效果)
- [三、常踩的坑(附解决方案 + 生活类比)](#三、常踩的坑(附解决方案 + 生活类比))
- 四、总结
作为ASP.NET开发者,控制器(Controller)是处理前端请求的 "中枢大脑",而过滤器(Filter)则是给这个大脑加装的 "智能插件"------ 能在请求处理的不同阶段自动执行逻辑,比如日志记录、权限校验、参数校验等。其中,继承ActionFilterAttribute实现自定义过滤器是日常开发中最常用的方式,但新手很容易踩坑。本文就从代码实战、避坑指南、生活类比三个维度,把自定义ActionFilterAttribute讲透,让你少走弯路。

一、先搞懂核心:ActionFilterAttribute 是什么?(生活类比 + 流程图)
小节:过滤器 = 请求处理的 "安检 + 后勤"
先举个生活例子:你去餐厅吃饭,从进门到用餐结束的流程是「进门→选座→点餐→用餐→结账→离开」。对应ASP.NET中 Controller 处理请求的流程是「请求到达→Action 执行前→Action 执行→Action 执行后→结果返回前→响应返回」。
ActionFilterAttribute就像餐厅的 "服务管家",可以在「Action 执行前」(比如核对预定信息)、「Action 执行后」(比如询问用餐体验)这两个核心节点插入自定义逻辑,不入侵核心的 "做菜(业务逻辑)" 流程,实现功能解耦。
控制器请求 + ActionFilter 执行流程图
前端发送请求 Controller接收请求 Action执行前:OnActionExecuting 执行目标Action方法 Action执行后:OnActionExecuted 返回响应给前端
ActionFilterAttribute核心重写方法(4 个,常用前 2 个):
1.OnActionExecuting:Action 执行前触发(前置逻辑);
2.OnActionExecuted:Action 执行后触发(后置逻辑);
3.OnResultExecuting:返回 Result 前触发;
4.OnResultExecuted:返回 Result 后触发。
二、代码实战:自定义 ActionFilterAttribute 完整示例
小节:从 0 到 1 写一个可用的自定义过滤器
我们以 "接口访问日志记录" 为例,实现一个自定义日志过滤器,记录每个接口的请求参数、执行时间、响应状态。
步骤 1:创建自定义过滤器类(继承 ActionFilterAttribute)
csharp
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Diagnostics;
using System.Text;
namespace WebDemo.Filters
{
/// <summary>
/// 自定义接口访问日志过滤器
/// </summary>
public class ApiLogFilter : ActionFilterAttribute
{
// 记录接口执行耗时
private Stopwatch _stopwatch;
/// <summary>
/// Action执行前:初始化计时器+记录请求信息
/// </summary>
/// <param name="context">Action执行上下文(包含请求、参数等)</param>
public override void OnActionExecuting(ActionExecutingContext context)
{
// 1. 初始化计时器
_stopwatch = Stopwatch.StartNew();
// 2. 获取请求基础信息
var httpContext = context.HttpContext;
var request = httpContext.Request;
var controllerName = context.RouteData.Values["controller"]?.ToString();
var actionName = context.RouteData.Values["action"]?.ToString();
// 3. 拼接请求参数(GET/POST参数都获取)
StringBuilder paramBuilder = new StringBuilder();
// 获取GET参数
foreach (var query in request.Query)
{
paramBuilder.Append($"{query.Key}={query.Value}&");
}
// 获取POST表单参数(JSON参数需单独解析,这里简化演示)
foreach (var form in request.Form)
{
paramBuilder.Append($"{form.Key}={form.Value}&");
}
var paramStr = 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>
public override void OnActionExecuted(ActionExecutedContext context)
{
// 停止计时器
_stopwatch.Stop();
var elapsedMilliseconds = _stopwatch.ElapsedMilliseconds;
// 获取响应状态
var statusCode = context.HttpContext.Response.StatusCode;
string errorMsg = string.Empty;
// 捕获Action执行异常
if (context.Exception != null)
{
errorMsg = context.Exception.Message;
// 若需要继续抛出异常,注释下面这行;若想吞掉异常,保留
// context.ExceptionHandled = true;
}
var controllerName = context.RouteData.Values["controller"]?.ToString();
var actionName = 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 级别(局部使用)
csharp
using Microsoft.AspNetCore.Mvc;
using WebDemo.Filters;
namespace WebDemo.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
// 仅该Action生效
[ApiLogFilter]
[HttpGet("GetUser")]
public IActionResult GetUser(int id)
{
// 模拟业务逻辑
System.Threading.Thread.Sleep(100);
return Ok(new { Id = id, Name = "张三", Age = 25 });
}
[HttpPost("AddUser")]
public IActionResult AddUser([FromBody] UserDto user)
{
// 该Action不记录日志
return Ok("添加成功");
}
}
public class UserDto
{
public string Name { get; set; }
public int Age { get; set; }
}
}
方式 2:控制器级别(整个控制器生效)
csharp
[ApiController]
[Route("api/[controller]")]
[ApiLogFilter] // 控制器下所有Action都生效
public class OrderController : ControllerBase
{
[HttpGet("GetOrder")]
public IActionResult GetOrder(int orderId)
{
return Ok(new { OrderId = orderId, Amount = 99.9 });
}
}
方式 3:全局注册(所有控制器 / Action 生效)
在Program.cs中注册:
csharp
var builder = WebApplication.CreateBuilder(args);
// 添加控制器服务
builder.Services.AddControllers(options =>
{
// 全局注册自定义日志过滤器
options.Filters.Add<ApiLogFilter>();
});
var app = builder.Build();
// 中间件配置...
app.MapControllers();
app.Run();
步骤 3:测试效果
调用/api/User/GetUser?id=1,控制台输出:
plaintext
【请求开始】[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 参数: |
csharp
public override async void OnActionExecuting(ActionExecutingContext context)
{
_stopwatch = Stopwatch.StartNew();
var httpContext = context.HttpContext;
var request = httpContext.Request;
var controllerName = context.RouteData.Values["controller"]?.ToString();
var actionName = context.RouteData.Values["action"]?.ToString();
StringBuilder paramBuilder = new StringBuilder();
// 获取GET参数
foreach (var query in request.Query)
{
paramBuilder.Append($"{query.Key}={query.Value}&");
}
// 新增:获取POST JSON参数
if (request.ContentType?.Contains("application/json") == true)
{
// 读取请求体
var stream = request.Body;
var buffer = new byte[Convert.ToInt32(request.ContentLength)];
await stream.ReadAsync(buffer, 0, buffer.Length);
var jsonParam = Encoding.UTF8.GetString(buffer);
paramBuilder.Append($"JSON参数:{jsonParam}");
// 重置流位置,避免后续ModelBinder读取不到参数
request.Body.Position = 0;
}
var paramStr = 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进阶干货~