【CSDN 专栏】ASP.NET Controller 过滤器详解:ActionFilter(Action 前后逻辑)从入门到避坑

目录

    • [一、先搞懂:ActionFilter 是什么?核心作用是什么?](#一、先搞懂:ActionFilter 是什么?核心作用是什么?)
      • [1.1 ActionFilter 的核心定位(生活类比)](#1.1 ActionFilter 的核心定位(生活类比))
      • [1.2 ActionFilter 的核心作用](#1.2 ActionFilter 的核心作用)
      • [1.3 ActionFilter 执行流程(流程图)](#1.3 ActionFilter 执行流程(流程图))
    • [二、ActionFilter 核心用法(代码实例)](#二、ActionFilter 核心用法(代码实例))
      • [2.1 基础用法:自定义 ActionFilter 实现参数校验](#2.1 基础用法:自定义 ActionFilter 实现参数校验)
        • [方式 1:Action 级注册(仅当前 Action 生效)](#方式 1:Action 级注册(仅当前 Action 生效))
        • [方式 2:控制器级注册(当前控制器所有 Action 生效)](#方式 2:控制器级注册(当前控制器所有 Action 生效))
        • [方式 3:全局注册(所有控制器的 Action 生效)](#方式 3:全局注册(所有控制器的 Action 生效))
        • [小节:自定义 ActionFilter 的核心是重写OnActionExecuting(前置校验)和OnActionExecuted(后置处理),注册方式可根据生效范围灵活选择 ------Action 级精准控制,全局级批量处理。](#小节:自定义 ActionFilter 的核心是重写OnActionExecuting(前置校验)和OnActionExecuted(后置处理),注册方式可根据生效范围灵活选择 ——Action 级精准控制,全局级批量处理。)
      • [2.2 进阶用法:异步 ActionFilter(IAsyncActionFilter)](#2.2 进阶用法:异步 ActionFilter(IAsyncActionFilter))
        • [小节:异步 ActionFilter 通过OnActionExecutionAsync实现,支持依赖注入和异步 IO 操作,是ASP.NET Core 的推荐写法,避免同步过滤器阻塞请求线程。](#小节:异步 ActionFilter 通过OnActionExecutionAsync实现,支持依赖注入和异步 IO 操作,是ASP.NET Core 的推荐写法,避免同步过滤器阻塞请求线程。)
      • [2.3 实用场景:参数自动格式化(去除字符串空格)](#2.3 实用场景:参数自动格式化(去除字符串空格))
        • [小节:ActionFilter 不仅能校验参数,还能格式化参数,减少业务代码中的重复处理(比如每个 Action 都写param.Trim())。](#小节:ActionFilter 不仅能校验参数,还能格式化参数,减少业务代码中的重复处理(比如每个 Action 都写param.Trim())。)
    • [三、ActionFilter 常踩的坑(避坑指南)](#三、ActionFilter 常踩的坑(避坑指南))
      • [坑 1:异步 Filter 中未正确执行await next()(生活类比:服务员忘记让后厨做餐,直接告诉顾客 "餐好了")](#坑 1:异步 Filter 中未正确执行await next()(生活类比:服务员忘记让后厨做餐,直接告诉顾客 “餐好了”))
        • [小节:异步 ActionFilter 的核心是await next(),漏写会导致业务逻辑 "失联",是新手最高频踩坑点。](#小节:异步 ActionFilter 的核心是await next(),漏写会导致业务逻辑 “失联”,是新手最高频踩坑点。)
      • [坑 2:Action 执行后修改 Result 时忽略类型判断(生活类比:把牛排的包装方式套用到沙拉上,导致包装出错)](#坑 2:Action 执行后修改 Result 时忽略类型判断(生活类比:把牛排的包装方式套用到沙拉上,导致包装出错))
        • [小节:Action 返回的 Result 类型多样,修改前必须做类型判断,避免 "一刀切" 导致异常。](#小节:Action 返回的 Result 类型多样,修改前必须做类型判断,避免 “一刀切” 导致异常。)
      • [坑 3:全局 Filter 与局部 Filter 冲突(生活类比:全局要求所有点餐都加辣,局部要求牛排不辣,结果牛排还是加了辣)](#坑 3:全局 Filter 与局部 Filter 冲突(生活类比:全局要求所有点餐都加辣,局部要求牛排不辣,结果牛排还是加了辣))
        • [小节:全局 Filter 需预留 "跳过开关",避免局部特殊场景被全局规则限制。](#小节:全局 Filter 需预留 “跳过开关”,避免局部特殊场景被全局规则限制。)
      • [坑 4:参数校验时未中断请求(生活类比:服务员发现顾客点的牛排已售罄,但仍让后厨做,导致后厨报错)](#坑 4:参数校验时未中断请求(生活类比:服务员发现顾客点的牛排已售罄,但仍让后厨做,导致后厨报错))
        • [小节:ActionFilter 前置校验的核心是 "拦截无效请求",未设置context.Result的校验等于 "只提醒不拦截",毫无意义。](#小节:ActionFilter 前置校验的核心是 “拦截无效请求”,未设置context.Result的校验等于 “只提醒不拦截”,毫无意义。)
      • [坑 5:依赖注入时 Filter 生命周期错误(生活类比:给每个顾客分配同一个水杯,导致卫生问题)](#坑 5:依赖注入时 Filter 生命周期错误(生活类比:给每个顾客分配同一个水杯,导致卫生问题))
    • 四、总结
    • [五、互动投票 & 读者交流](#五、互动投票 & 读者交流)

在ASP.NET的过滤器体系中,ActionFilter 是最灵活、使用频率最高的过滤器类型 ------ 它就像业务接口的 "贴身管家",能在 Action 执行前做参数校验、日志记录,也能在 Action 执行后处理返回结果、统一响应格式。本文从「生活类比 + 代码实例 + 踩坑指南」三维度,把 ActionFilter(尤其是参数校验场景)讲透,新手也能快速上手并避坑!

一、先搞懂:ActionFilter 是什么?核心作用是什么?

1.1 ActionFilter 的核心定位(生活类比)

把ASP.NET处理请求的过程比作 "餐厅点餐服务":

  • Controller(控制器): 餐厅的点餐区(处理具体需求:点牛排、点沙拉);
  • Action(动作方法): 具体的点餐窗口(比如 "牛排窗口" 对应GetSteak Action);
  • ActionFilter(Action 过滤器): 点餐窗口的 "前置服务员 + 后置服务员":
    • 前置:点餐前核对需求(比如 "是否要七分熟""是否有忌口"→对应参数校验);
    • 后置:餐品做好后包装、加赠品→对应统一响应格式、结果处理。

1.2 ActionFilter 的核心作用

ActionFilter 实现IActionFilter/IAsyncActionFilter接口,核心职责:

  • Action 执行前(OnActionExecuting): 参数校验、权限二次校验、日志记录、请求头验证、参数格式化(如去除字符串空格);
  • Action 执行后(OnActionExecuted): 统一响应格式、异常捕获、返回结果修改、性能监控(记录 Action 执行耗时);
  • 可全局注册(所有 Action 生效)、控制器级注册(当前控制器所有 Action 生效)、Action 级注册(仅当前 Action 生效)。

1.3 ActionFilter 执行流程(流程图)

通过 参数校验通过 参数校验失败 客户端发起请求 AuthorizationFilter 授权校验 ResourceFilter 资源过滤 ActionFilter OnActionExecuting:Action执行前 Action执行 ActionFilter OnActionExecuted:Action执行后 ResultFilter 结果处理 返回响应给客户端 直接返回错误响应

关键结论: ActionFilter 是紧贴 Action 的 "前后置处理器",参数校验是其最核心的前置应用场景,能在业务逻辑执行前拦截无效请求。

二、ActionFilter 核心用法(代码实例)

2.1 基础用法:自定义 ActionFilter 实现参数校验

场景:接口参数统一校验(比如 "订单 ID 不能为空""手机号格式正确")

第一步:创建自定义 ActionFilter(参数校验核心逻辑)

csharp 复制代码
/// <summary>
/// 订单参数校验过滤器(Action执行前校验)
/// </summary>
public class OrderParamValidateFilter : IActionFilter
{
    /// <summary>
    /// Action执行前:参数校验核心逻辑
    /// </summary>
    /// <param name="context">Action执行上下文</param>
    public void OnActionExecuting(ActionExecutingContext context)
    {
        // 1. 获取Action参数(比如订单ID、手机号)
        if (!context.ActionArguments.TryGetValue("orderId", out var orderIdObj))
        {
            // 参数缺失,直接中断请求
            context.Result = new BadRequestObjectResult(new
            {
                code = 400,
                msg = "订单ID参数缺失",
                data = null
            });
            return;
        }

        // 2. 校验订单ID格式(非空+数字)
        var orderId = orderIdObj?.ToString();
        if (string.IsNullOrEmpty(orderId) || !int.TryParse(orderId, out _))
        {
            context.Result = new BadRequestObjectResult(new
            {
                code = 400,
                msg = "订单ID格式错误:必须为非空数字",
                data = null
            });
            return;
        }

        // 3. 校验手机号(如果有该参数)
        if (context.ActionArguments.TryGetValue("phone", out var phoneObj))
        {
            var phone = phoneObj?.ToString();
            if (!string.IsNullOrEmpty(phone) && !System.Text.RegularExpressions.Regex.IsMatch(phone, @"^1[3-9]\d{9}$"))
            {
                context.Result = new BadRequestObjectResult(new
                {
                    code = 400,
                    msg = "手机号格式错误:请输入11位有效手机号",
                    data = null
                });
                return;
            }
        }

        // 校验通过,继续执行Action
        Console.WriteLine($"【前置校验】订单ID {orderId} 校验通过,准备执行Action");
    }

    /// <summary>
    /// Action执行后:统一响应格式
    /// </summary>
    /// <param name="context">Action执行后上下文</param>
    public void OnActionExecuted(ActionExecutedContext context)
    {
        // 1. 捕获Action执行异常
        if (context.Exception != null)
        {
            context.Result = new ObjectResult(new
            {
                code = 500,
                msg = $"业务执行异常:{context.Exception.Message}",
                data = null
            })
            { StatusCode = 500 };
            // 标记异常已处理,避免全局异常过滤器重复处理
            context.ExceptionHandled = true;
            return;
        }

        // 2. 统一响应格式(把Action返回的原始数据包装成固定结构)
        if (context.Result is ObjectResult objectResult)
        {
            var unifiedResult = new
            {
                code = 200,
                msg = "操作成功",
                data = objectResult.Value,
                timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
            };
            context.Result = new OkObjectResult(unifiedResult);
        }
    }
}

第二步:注册并使用 Filter(3 种注册方式)

方式 1:Action 级注册(仅当前 Action 生效)
csharp 复制代码
/// <summary>
/// 订单控制器
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class OrderController : ControllerBase
{
    /// <summary>
    /// 查询订单详情(仅该Action触发参数校验)
    /// </summary>
    /// <param name="orderId">订单ID</param>
    /// <param name="phone">联系手机号(可选)</param>
    [HttpGet("detail")]
    [TypeFilter(typeof(OrderParamValidateFilter))] // Action级注册
    public IActionResult GetOrderDetail(string orderId, string phone = null)
    {
        // 业务逻辑:模拟查询订单
        var order = new
        {
            OrderId = orderId,
            ProductName = "华为Mate60 Pro",
            Price = 6999,
            Phone = phone
        };
        return Ok(order);
    }

    /// <summary>
    /// 取消订单(不触发该过滤器)
    /// </summary>
    [HttpPost("cancel")]
    public IActionResult CancelOrder(string orderId)
    {
        return Ok(new { success = true });
    }
}
方式 2:控制器级注册(当前控制器所有 Action 生效)
csharp 复制代码
[ApiController]
[Route("api/[controller]")]
[TypeFilter(typeof(OrderParamValidateFilter))] // 控制器级注册
public class OrderController : ControllerBase
{
    // 该控制器下所有Action都会触发参数校验和统一响应
}
方式 3:全局注册(所有控制器的 Action 生效)
csharp 复制代码
// Program.cs中注册
var builder = WebApplication.CreateBuilder(args);

// 添加控制器
builder.Services.AddControllers(options =>
{
    // 全局注册ActionFilter
    options.Filters.Add<OrderParamValidateFilter>();
});

var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
小节:自定义 ActionFilter 的核心是重写OnActionExecuting(前置校验)和OnActionExecuted(后置处理),注册方式可根据生效范围灵活选择 ------Action 级精准控制,全局级批量处理。

2.2 进阶用法:异步 ActionFilter(IAsyncActionFilter)

ASP.NET Core 推荐使用异步过滤器(避免阻塞线程),尤其是涉及 IO 操作(如数据库校验参数)时:

csharp 复制代码
/// <summary>
/// 异步参数校验过滤器(适配IO密集型校验)
/// </summary>
public class AsyncOrderParamFilter : IAsyncActionFilter
{
    private readonly ILogger<AsyncOrderParamFilter> _logger;
    // 注入业务服务(比如订单仓储)
    private readonly IOrderRepository _orderRepository;

    public AsyncOrderParamFilter(ILogger<AsyncOrderParamFilter> logger, IOrderRepository orderRepository)
    {
        _logger = logger;
        _orderRepository = orderRepository;
    }

    /// <summary>
    /// 异步执行Action前后逻辑
    /// </summary>
    /// <param name="context">Action执行前上下文</param>
    /// <param name="next">执行下一步(Action)</param>
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        // 1. Action执行前:异步校验订单是否存在(IO操作)
        if (context.ActionArguments.TryGetValue("orderId", out var orderIdObj))
        {
            var orderId = orderIdObj?.ToString();
            if (!string.IsNullOrEmpty(orderId))
            {
                // 异步查询数据库:订单是否存在
                var orderExists = await _orderRepository.IsOrderExistsAsync(orderId);
                if (!orderExists)
                {
                    context.Result = new BadRequestObjectResult(new
                    {
                        code = 400,
                        msg = $"订单ID {orderId} 不存在",
                        data = null
                    });
                    return;
                }
            }
        }

        _logger.LogInformation("【前置异步校验】参数校验通过,执行Action");

        // 2. 执行Action(核心业务逻辑)
        var executedContext = await next();

        // 3. Action执行后:异步记录操作日志
        if (executedContext.Exception == null)
        {
            var log = new
            {
                ActionName = context.ActionDescriptor.DisplayName,
                OrderId = context.ActionArguments["orderId"],
                ExecuteTime = DateTime.Now,
                Status = "成功"
            };
            // 异步写入日志库
            await _orderRepository.RecordOrderLogAsync(log);
            _logger.LogInformation("【后置异步处理】操作日志已记录");
        }
    }
}

// 注册方式(支持依赖注入)
[TypeFilter(typeof(AsyncOrderParamFilter))]
public IActionResult GetOrderDetail(string orderId)
{
    // 业务逻辑
}
小节:异步 ActionFilter 通过OnActionExecutionAsync实现,支持依赖注入和异步 IO 操作,是ASP.NET Core 的推荐写法,避免同步过滤器阻塞请求线程。

2.3 实用场景:参数自动格式化(去除字符串空格)

csharp 复制代码
/// <summary>
/// 参数去空格过滤器(Action执行前格式化参数)
/// </summary>
public class ParamTrimFilter : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        // 遍历所有Action参数,去除字符串空格
        foreach (var key in context.ActionArguments.Keys.ToList())
        {
            var value = context.ActionArguments[key];
            if (value is string strValue && !string.IsNullOrEmpty(strValue))
            {
                // 去除首尾空格
                context.ActionArguments[key] = strValue.Trim();
            }
        }
    }

    public void OnActionExecuted(ActionExecutedContext context) { }
}

// 全局注册后,所有字符串参数自动去空格
builder.Services.AddControllers(options =>
{
    options.Filters.Add<ParamTrimFilter>();
});
小节:ActionFilter 不仅能校验参数,还能格式化参数,减少业务代码中的重复处理(比如每个 Action 都写param.Trim())。

三、ActionFilter 常踩的坑(避坑指南)

坑 1:异步 Filter 中未正确执行await next()(生活类比:服务员忘记让后厨做餐,直接告诉顾客 "餐好了")

问题表现:

自定义异步 ActionFilter 时,漏写await next(),导致 Action 根本不执行,直接进入后置处理逻辑。
错误代码:

csharp 复制代码
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
    // 前置校验
    if (!CheckParam(context))
    {
        context.Result = new BadRequestResult();
        return;
    }
    // 错误:漏写await next(),Action不会执行
    next(); 
    // 后置处理
    LogExecuted(context);
}

解决方案:

  • 异步 Filter 中必须调用await next()才能执行 Action;
  • 若前置校验失败,直接返回context.Result,无需调用next()。
    正确代码:
csharp 复制代码
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
    if (!CheckParam(context))
    {
        context.Result = new BadRequestResult();
        return;
    }
    // 执行Action
    var executedContext = await next();
    // 后置处理
    LogExecuted(executedContext);
}
小节:异步 ActionFilter 的核心是await next(),漏写会导致业务逻辑 "失联",是新手最高频踩坑点。

坑 2:Action 执行后修改 Result 时忽略类型判断(生活类比:把牛排的包装方式套用到沙拉上,导致包装出错)

问题表现:

在OnActionExecuted中直接强转context.Result为ObjectResult,若 Action 返回RedirectResult(重定向)、FileResult(文件),会触发空指针 / 类型转换异常。
错误代码:

csharp 复制代码
public void OnActionExecuted(ActionExecutedContext context)
{
    // 错误:未判断Result类型,重定向/文件返回时会报错
    var objectResult = (ObjectResult)context.Result;
    objectResult.Value = new { code = 200, data = objectResult.Value };
}

解决方案:

  • 先判断context.Result的类型,仅对ObjectResult/OkObjectResult等数据类 Result 做处理;
  • 对重定向、文件、空结果等特殊类型跳过处理。
    正确代码:
csharp 复制代码
public void OnActionExecuted(ActionExecutedContext context)
{
    // 仅处理数据类Result
    if (context.Result is ObjectResult objectResult && objectResult.Value != null)
    {
        objectResult.Value = new { code = 200, msg = "成功", data = objectResult.Value };
    }
    // 对重定向、文件等Result不处理
    else if (context.Result is RedirectResult || context.Result is FileResult)
    {
        return;
    }
}
小节:Action 返回的 Result 类型多样,修改前必须做类型判断,避免 "一刀切" 导致异常。

坑 3:全局 Filter 与局部 Filter 冲突(生活类比:全局要求所有点餐都加辣,局部要求牛排不辣,结果牛排还是加了辣)

问题表现:

全局注册了参数校验 Filter,某个 Action 想跳过校验,但未做排除处理,导致无效校验。
解决方案:

自定义 "跳过过滤器" 特性,在 Filter 中判断是否跳过;

示例:

csharp 复制代码
/// <summary>
/// 跳过参数校验特性
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class SkipParamValidateAttribute : Attribute { }

// 在Filter中判断是否跳过
public void OnActionExecuting(ActionExecutingContext context)
{
    // 检查Action/控制器是否有跳过特性
    var skipAttr = context.ActionDescriptor.EndpointMetadata.FirstOrDefault(m => m is SkipParamValidateAttribute);
    if (skipAttr != null)
    {
        return; // 跳过校验
    }
    // 正常校验逻辑
}

// 在Action上使用
[HttpGet("export")]
[SkipParamValidate] // 跳过全局参数校验
public IActionResult ExportOrder()
{
    // 导出订单文件,无需参数校验
    return File(new byte[0], "application/excel", "订单.xlsx");
}
小节:全局 Filter 需预留 "跳过开关",避免局部特殊场景被全局规则限制。

坑 4:参数校验时未中断请求(生活类比:服务员发现顾客点的牛排已售罄,但仍让后厨做,导致后厨报错)

问题表现:

在OnActionExecuting中仅打印校验错误日志,但未设置context.Result,导致无效请求仍进入 Action 执行,触发业务异常。
错误代码:

csharp 复制代码
public void OnActionExecuting(ActionExecutingContext context)
{
    var orderId = context.ActionArguments["orderId"]?.ToString();
    if (string.IsNullOrEmpty(orderId))
    {
        Console.WriteLine("订单ID为空");
        // 错误:未设置context.Result,请求继续执行
        return;
    }
}

解决方案:

  • 校验失败时必须设置context.Result(如BadRequestObjectResult),中断请求流程;
  • context.Result一旦设置,Action 将不再执行。
小节:ActionFilter 前置校验的核心是 "拦截无效请求",未设置context.Result的校验等于 "只提醒不拦截",毫无意义。

坑 5:依赖注入时 Filter 生命周期错误(生活类比:给每个顾客分配同一个水杯,导致卫生问题)

问题表现:

全局注册 Filter 时直接new OrderParamValidateFilter(),导致 Filter 中的依赖服务(如ILogger)为单例,出现线程安全问题。
错误代码:

csharp 复制代码
// 错误:直接new,无法注入依赖,且生命周期错误
builder.Services.AddControllers(options =>
{
    options.Filters.Add(new OrderParamValidateFilter()); 
});

解决方案:

  • 全局注册时使用Add()或AddServiceFilter(),让容器管理生命周期;
  • 局部注册用[TypeFilter](自动创建实例)或[ServiceFilter](从容器获取实例)。
    正确代码:
csharp 复制代码
// 正确:容器管理生命周期,支持依赖注入
builder.Services.AddControllers(options =>
{
    options.Filters.Add<OrderParamValidateFilter>(); 
});

// 或注册为服务后使用ServiceFilter
builder.Services.AddScoped<OrderParamValidateFilter>();
options.Filters.AddServiceFilter<OrderParamValidateFilter>();
小节:Filter 涉及依赖注入时,必须由容器管理实例,避免手动 new 导致的注入失败和线程安全问题。

四、总结

ActionFilter 是ASP.NET中最灵活的过滤器,核心价值是 "在 Action 前后做通用处理":

1.前置(OnActionExecuting):参数校验、参数格式化、日志记录是核心场景,校验失败需设置context.Result中断请求;

2.后置(OnActionExecuted):统一响应格式、异常捕获、性能监控是核心场景,处理 Result 前需做类型判断;

3.异步 Filter 优先用IAsyncActionFilter,避免阻塞线程,且必须调用await next();

4.全局 Filter 需预留跳过开关,避免与局部场景冲突;

5.Filter 依赖注入需由容器管理,禁止手动 new。

五、互动投票 & 读者交流

读者交流:

如果你在使用 ActionFilter 时遇到过其他坑,或者有更好的参数校验 / 统一响应技巧(比如结合 FluentValidation),欢迎在评论区留言分享!也可以说说你在实际项目中是如何设计通用 Filter 体系的,我们一起交流进步~
专栏说明: 本文聚焦 ActionFilter 的核心用法与避坑指南,后续会陆续更新 ResultFilter、ExceptionFilter 等其他过滤器类型,关注我,带你把ASP.NET过滤器体系彻底搞懂!

相关推荐
悟空码字3 小时前
SpringBoot实现消息推送:让服务器学会“主动搭讪”
java·spring boot·后端
+VX:Fegn08953 小时前
人力资源管理|基于springboot + vue人力资源管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·课程设计
GOTXX4 小时前
性能与可靠双突破:openEuler 服务器场景评测报告
运维·服务器·网络·人工智能·后端·python
秋邱4 小时前
AR 技术团队搭建与规模化接单:从个人到团队的营收跃迁
前端·人工智能·后端·python·html·restful
U盘失踪了4 小时前
Django 登录注册功能实现
后端·python·django
U盘失踪了4 小时前
Django 登录注册功能实现-样式优化
后端·python·django
询问QQ688238864 小时前
JAVA智能配电房管理系统源码带数据字典及完整文档JAVA智能配电房管理系统源码带数据字典及完整文档
asp.net
qinyuan154 小时前
使用husky和fabric规范git提交的注释
前端·后端
uhakadotcom4 小时前
asyncpg 全面教程:常用 API 串联与实战指南
后端·面试·github