目录
-
- [一、先搞懂: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))
- [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过滤器体系彻底搞懂!