
这篇文章就针对这些痛点展开:先帮你理清ResourceFilter的核心原理——它是如何在“资源准备”与“资源回收”两个阶段发挥作用的,和AuthorizationFilter、ActionFilter的本质区别是什么;再给你 step-by-step 的实战代码,教你从零实现一个自定义ResourceFilter(比如处理文件上传时的临时文件管理);最后把新手常踩的“坑”一一扒出来:比如忘记实现IAsyncResourceFilter导致异步请求出错、OnResourceExecuted中没处理异常导致资源泄漏、混淆同步与异步方法的执行逻辑……
不管你是刚入门想搞懂“为什么要用ResourceFilter”,还是项目里遇到资源管理问题想优化,这篇文章都能帮你“从懂到会”,真正把ResourceFilter用对、用顺。
做ASP.NET Core开发时,你有没有遇到过这种糟心事儿?接口明明没做什么复杂逻辑,却慢得像蜗牛;数据库连接池动不动就“爆满”,报错说“无法获取新的连接”;或者缓存的数据老是和数据库不一致,用户投诉“我改了头像怎么还没显示”?我前两年帮一个做在线教育的朋友排查系统性能问题,就碰到过这些坑——他们的课程详情接口每次都要重新查数据库、重新读缓存,而且用完的数据库连接没及时关,导致连接池经常满。后来我给他们加了个ResourceFilter,把资源的“预加载”和“回收”统一处理,结果接口响应时间从2秒降到了500毫秒,数据库连接池的使用量直接砍了一半。
其实ResourceFilter就是ASP.NET Core里专门管“资源管理”的“工具人”,但很多人要么没搞懂它的原理,要么写代码时踩坑,今天我把自己踩过的坑、用过的技巧全给你抖出来。
ResourceFilter到底是什么?先把原理讲明白
要搞懂ResourceFilter,得先弄清楚它在过滤器管道里的位置——ASP.NET Core的请求处理管道像一条流水线,每个过滤器负责一个环节:
ResourceFilter的特殊之处在于它是“资源的守门员”:比ActionFilter早一步执行,能在Action没开始前就把资源准备好(比如提前从缓存读用户信息);等Action执行完,再把资源“收走”(比如关闭数据库连接、清理临时文件)。就像你做顿大餐,ResourceFilter负责“提前淘好米、洗好菜”,做完饭再“把厨房收拾干净”,而ActionFilter更像“炒菜时加调料”——前者管的是“前置准备”和“后置清理”,后者管的是“过程中的调整”。
我之前帮朋友调优项目时,发现他们的UserController
里,每个Action(比如GetUserById
、UpdateUser
)都要重新开一次数据库连接,用完还没关。我给他们加了个DatabaseConnectionResourceFilter
,把“开连接”放到OnResourceExecuting
(资源准备阶段),“关连接”放到OnResourceExecuted
(资源回收阶段),结果数据库连接的复用率直接提高了30%——因为多个Action可以共享同一个连接,不用每次都新建。
再举个更直观的例子:如果你的接口需要从Redis读缓存,ResourceFilter可以在OnResourceExecuting
里先查缓存,如果有数据,直接返回缓存结果,跳过后面的Action(相当于“提前上菜”);如果没缓存,再执行Action,然后在OnResourceExecuted
里把Action的结果存到缓存里——这样下次请求就能直接用缓存,不用再走数据库。
微软官方文档(点击查看)里明确说过:“ResourceFilter适合处理跨多个Action的资源管理,比如缓存、数据库连接、文件流这些需要复用或清理的资源”。 只要你需要“提前准备资源”或者“统一回收资源”,找ResourceFilter准没错。
手把手写一个能直接用的ResourceFilter:从同步到异步
光懂原理没用,得动手写代码才会真的会。我给你举两个最常用的场景:数据库连接管理和缓存资源处理,一步一步教你实现。
同步的ResourceFilter实现IResourceFilter
接口,重写两个方法:
OnResourceExecuting
:资源准备(开数据库连接); OnResourceExecuted
:资源回收(关数据库连接)。 比如我要写一个DatabaseConnectionResourceFilter
,代码长这样:
public class DatabaseConnectionResourceFilter IResourceFilter
{
private readonly string _connectionString;
// 通过构造函数注入连接字符串(从配置文件读)
public DatabaseConnectionResourceFilter(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("DefaultConnection");
}
public void OnResourceExecuting(ResourceExecutingContext context)
{
//
预加载资源:打开数据库连接
var connection = new SqlConnection(_connectionString);
try
{
connection.Open();
// 把连接存到HttpContext.Items里,后面Action能直接用
context.HttpContext.Items["DbConnection"] = connection;
Console.WriteLine("数据库连接已打开");
}
catch (Exception ex)
{
// 准备资源失败,直接返回错误结果
context.Result = new StatusCodeResult(500);
Console.WriteLine($"打开连接失败:{ex.Message}");
}
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
//
回收资源:关闭数据库连接
if (context.HttpContext.Items.TryGetValue("DbConnection", out var connectionObj) && connectionObj is SqlConnection connection)
{
if (connection.State == System.Data.ConnectionState.Open)
{
connection.Close();
connection.Dispose();
Console.WriteLine("数据库连接已关闭");
}
}
}
}
写完之后,要注册Filter——在Program.cs
里加一行:
builder.Services.AddScoped();
然后在Controller上用[ServiceFilter]
标记,比如:
[ApiController]
[Route("api/[controller]")]
[ServiceFilter(typeof(DatabaseConnectionResourceFilter))]
public class UserController ControllerBase
{
[HttpGet("{id}")]
public IActionResult GetUserById(int id)
{
// 直接从HttpContext.Items拿连接,不用再开了
var connection = HttpContext.Items["DbConnection"] as SqlConnection;
var user = new UserRepository(connection).GetUserById(id);
return Ok(user);
}
}
这样一来,UserController
里的所有Action都能共享同一个数据库连接,不用每次都新建——我朋友的项目用了这个Filter后,数据库连接池的使用率从80%降到了30%,再也没出现过“连接池满”的错误。
同步Filter适合简单场景,但高并发下一定要用异步——因为同步方法会阻塞线程,而异步方法能“让出线程”给其他请求,提升系统吞吐量。比如电商项目的“商品详情接口”,高并发时每秒有几千次请求,用异步ResourceFilter处理缓存,能把响应时间从2秒降到500毫秒。
异步ResourceFilter要实现IAsyncResourceFilter
接口,重写OnResourceExecutionAsync
方法——这个方法里能处理“预加载缓存”“执行后续管道”“缓存结果”三个步骤。比如我写一个ProductCacheAsyncResourceFilter
,用来缓存商品详情:
public class ProductCacheAsyncResourceFilter IAsyncResourceFilter
{
private readonly IRedisCache _redisCache; // 假设你用了Redis做缓存
private readonly IConfiguration _configuration;
public ProductCacheAsyncResourceFilter(IRedisCache redisCache, IConfiguration configuration)
{
_redisCache = redisCache;
_configuration = configuration;
}
public async Task OnResourceExecutionAsync(
ResourceExecutingContext context, // 资源准备阶段的上下文
ResourceExecutionDelegate next // 执行后续管道(比如Action)的委托
)
{
//
预加载缓存:先查Redis有没有商品数据
var productId = context.RouteData.Values["id"]?.ToString();
if (string.IsNullOrEmpty(productId))
{
// 没有商品ID,直接执行后续管道
await next();
return;
}
var cacheKey = $"Product:{productId}";
var cachedProduct = await _redisCache.GetAsync(cacheKey);
if (cachedProduct != null)
{
// 有缓存,直接返回,跳过Action
context.Result = new OkObjectResult(cachedProduct);
Console.WriteLine($"从缓存读取商品:{productId}");
return;
}
//
没有缓存,执行后续管道(Action)
var executedContext = await next(); // 这里会执行Action(比如GetProductById)
//
缓存Action的结果:把商品数据存到Redis
if (executedContext.Result is OkObjectResult okResult && okResult.Value is Product product)
{
var cacheExpireTime = TimeSpan.FromMinutes(_configuration.GetValue("Cache:ProductExpireMinutes"));
await _redisCache.SetAsync(cacheKey, product, cacheExpireTime);
Console.WriteLine($"缓存商品:{productId},过期时间:{cacheExpireTime}");
}
}
}
注册和使用的方式和同步Filter一样:先在Program.cs
里加builder.Services.AddScoped();
,然后在ProductController
上用[ServiceFilter(typeof(ProductCacheAsyncResourceFilter))]
。
我之前做电商项目时,用这个Filter处理“商品详情”接口,结果接口的QPS(每秒请求数)从500涨到了2000——因为大部分请求直接读缓存,不用走数据库。
写ResourceFilter时,这3个坑千万别踩
我写了5年ASP.NET Core,踩过的ResourceFilter的坑能堆成山,今天把最常踩的3个坑给你列出来,帮你少走弯路。
很多人刚学ResourceFilter时,会犯一个低级错误:在同步的IResourceFilter
里调用异步方法(比如await _redisCache.GetAsync()
)。比如这样:
// 错误示例:同步Filter里调用异步方法
public class BadSyncResourceFilter IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
// 这里会报错:同步方法里不能用await
var cachedData = await _redisCache.GetAsync("key"); // 错误!
}
// ... 其他方法
}
为什么错?因为同步方法会阻塞线程——比如await _redisCache.GetAsync()
需要等Redis返回结果,但同步方法会“占着线程不放”,高并发时线程池被占满,接口直接超时。
正确做法:如果要调用异步方法,一定要用IAsyncResourceFilter
(异步过滤器),像我前面写的ProductCacheAsyncResourceFilter
那样。
OnResourceExecuted
里处理异常 另一个常见的坑是:没处理context.Exception
,导致异常时资源没释放。比如:
// 错误示例:没处理异常,导致资源泄漏
public class BadResourceFilter IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var connection = new SqlConnection("connStr");
connection.Open();
context.HttpContext.Items["DbConnection"] = connection;
}
public void OnResourceExecuted(ResourceExecutingContext context)
{
// 没判断context.Exception,异常时不会执行关闭连接的逻辑
var connection = context.HttpContext.Items["DbConnection"] as SqlConnection;
connection.Close(); // 如果Action抛异常,这里不会执行!
}
}
比如GetUserById
Action里抛了NullReferenceException
,OnResourceExecuted
还是会执行,但如果没判断context.Exception
,connection.Close()
就不会被调用——结果数据库连接没关,连接池满了,系统直接崩。
正确做法:不管有没有异常,都要释放资源——要么用try...finally
,要么直接释放:
// 正确示例:处理异常,确保资源释放
public void OnResourceExecuted(ResourceExecutedContext context)
{
if (context.HttpContext.Items.TryGetValue("DbConnection", out var connectionObj) && connectionObj is SqlConnection connection)
{
// 不管有没有异常,都要关连接
if (connection.State == System.Data.ConnectionState.Open)
{
connection.Close();
connection.Dispose();
}
}
}
我之前做订单系统时,就踩过这个坑:CreateOrder
Action里抛了“库存不足”的异常,结果OnResourceExecuted
里没关数据库连接,导致连接池满了,整整修了3小时才找到问题——从那以后,我写ResourceFilter时,必加try...finally
或者直接释放资源。
ResourceFilter的执行顺序遵循“先注册,先执行;先执行,后回收”的规则——比如你注册了两个Filter:FilterA
和FilterB
,顺序是A→B
,那么执行流程是:
FilterA.OnResourceExecuting
(A准备资源); FilterB.OnResourceExecuting
(B准备资源); FilterB.OnResourceExecuted
(B回收资源); FilterA.OnResourceExecuted
(A回收资源)。 如果搞反了顺序,会出大问题——比如FilterB
依赖FilterA
准备的资源(比如FilterA
开数据库连接,FilterB
要用这个连接),但你先注册了FilterB
,结果FilterB
执行时,FilterA
还没准备好资源,直接报错。
我之前遇到过一个项目,他们把CacheFilter
(缓存)注册在DatabaseConnectionFilter
(数据库连接)前面,结果CacheFilter
要读数据库里的商品数据,却发现DatabaseConnectionFilter
还没开连接——接口直接返回“数据库连接未打开”的错误。后来把DatabaseConnectionFilter
的注册顺序调到CacheFilter
前面,问题就解决了。
最后再给你补个过滤器执行顺序对比表,帮你记清楚:
过滤器类型 | 执行时机 | 核心作用 | 是否异步支持 |
---|---|---|---|
AuthorizationFilter | 管道最早期 | 验证身份/权限 | 是(IAsyncAuthorizationFilter) |
ResourceFilter | Authorization之后,Action之前 | 预加载/回收资源 | 是(IAsyncResourceFilter) |
ActionFilter | Action执行前后 | 参数验证/结果格式化 | 是(IAsyncActionFilter) |
现在你应该明白,ResourceFilter不是什么“高大上”的东西——它就是帮你管“资源的准备和回收”的工具,只要把原理搞懂,代码写对,避过那些坑,就能用它解决很多实际问题。
如果你按我讲的方法试了,或者遇到了问题,欢迎留言告诉我——
ResourceFilter和ActionFilter有什么区别呀?
最核心的区别是执行顺序和职责不一样。ResourceFilter比ActionFilter早一步执行——它在AuthorizationFilter(授权)之后、Action开始之前就干活,主要负责“资源的准备和回收”,比如提前开数据库连接、读缓存;而ActionFilter是在Action执行前后生效,比如参数验证、结果格式化这些和Action直接相关的逻辑。打个比方,ResourceFilter像“提前把菜洗好切好”,ActionFilter像“炒菜时加调料”,分工不一样。
ResourceFilter能跳过后续管道——比如如果缓存里有数据,ResourceFilter可以直接返回结果,不用再走Action,这是ActionFilter做不到的。
同步和异步的ResourceFilter该怎么选?
如果你的逻辑是简单的同步操作(比如开个数据库连接、读本地文件),用同步的IResourceFilter没问题;但如果要调用异步方法(比如 await 读Redis、异步查数据库),或者高并发场景,一定要用异步的IAsyncResourceFilter。
因为同步Filter里用await会报错,而且同步方法会阻塞线程——高并发时线程池被占满,接口就会超时。我之前踩过坑,在同步Filter里调用await 读Redis,结果高并发时接口直接504,换成异步Filter才解决。
Action抛异常了,ResourceFilter里的资源没释放怎么办?
这是新手常踩的坑!解决办法很简单:不管Action有没有抛异常,都要在OnResourceExecuted(或异步的OnResourceExecutionAsync)里释放资源。比如你可以判断context.Exception——如果有异常,还是要把数据库连接关掉、临时文件删掉;或者更保险的是,直接在OnResourceExecuted里取资源然后释放,不用管有没有异常。
我之前帮朋友调项目时,就碰到过Action抛异常导致数据库连接没关的情况,后来在OnResourceExecuted里加了判断,不管context.Exception有没有值,都关闭连接,结果连接池再也没满过。
为什么不在Action里自己处理资源,反而用ResourceFilter?
当然可以,但会很麻烦!比如你有10个Action都要开数据库连接、读完关掉,自己在每个Action里写一遍,不仅重复代码多,还容易漏——比如某个Action忘了关连接,就会导致连接池满。用ResourceFilter可以把这些逻辑“统一管起来”,不管多少个Action,只要加个Filter,就能自动处理资源的准备和回收,省得重复写代码。
还有,ResourceFilter能在Action开始前就准备资源,比如提前读缓存,如果缓存有数据直接返回,不用再执行Action,这比在Action里自己读缓存更高效——因为Action都不用跑了,直接省了后面的逻辑。