【CSDN 专栏】C# ASP.NET控制器过滤器:自定义 ActionFilterAttribute 实战(避坑 + 图解)

目录

    • [一、先搞懂核心: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进阶干货~

相关推荐
William_cl5 小时前
【CSDN 专栏】C# ASP.NET Razor 视图引擎实战:.cshtml 从入门到避坑(图解 + 案例)
开发语言·c#·asp.net
isyoungboy5 小时前
c++使用win新api替代DirectShow驱动uvc摄像头,可改c#驱动
开发语言·c++·c#
柯南二号5 小时前
【后端】【Java】一文详解Spring Boot RESTful 接口统一返回与异常处理实践
java·spring boot·状态模式·restful
技术支持者python,php6 小时前
USB摄像头采集数据
人工智能·c#
c#上位机17 小时前
halcon刚性变换(平移+旋转)——vector_to_rigid
图像处理·人工智能·计算机视觉·c#·halcon
Miss_SQ17 小时前
Webgl打包后删除StreamingAssets文件夹下多余资源
unity·c#·webgl
小猪快跑爱摄影17 小时前
【AutoCad 2025】【C#】零基础教程(二)——遍历 Entity 插件 =》 AutoCAD 核心对象层级结构
开发语言·c#·autocad
烛阴18 小时前
C# Dictionary 入门:用键值对告别低效遍历
前端·c#
Monkey_Xuan20 小时前
C#中的引用传递和值传递
unity·c#