ASP.NET Core OutputCache必须显式注册服务并启用中间件:在Program.cs中调用AddOutputCache()和UseOutputCache(),且UseOutputCache必须位于UseRouting之后、MapEndpoints之前,否则[OutputCache]等全部静默失效。

ASP.NET Core 的 OutputCache 不是加个特性就自动生效的——它必须在 Program.cs 中显式注册服务 + 启用中间件,否则所有 [OutputCache] 或 .CacheOutput() 都静默失效,连日志都不会报错。
为什么加了 [OutputCache] 却没缓存?
根本原因是底层服务没注册。ASP.NET Core 7+ 的输出缓存是独立中间件,和旧版 [ResponseCache] 完全不兼容,也不共享逻辑。
-
builder.Services.AddOutputCache()必须调用,否则IOutputCacheStore无法解析,任何缓存操作都会跳过 -
app.UseOutputCache()必须放在app.UseRouting()之后、app.MapControllers()或app.MapGet()之前,顺序错会导致中间件不介入请求流 - 即使你写了
[OutputCache(Duration = 60)],若上面两步缺失,请求照常执行,响应头里也不会出现X-Output-Cache: Hit - 验证是否真命中:别只看
Cache-Control响应头,要查日志里有没有Cache hit for key 'xxx'
Minimal API 中怎么配缓存策略?
Minimal API 不支持特性,得用 .CacheOutput() 或 .WithMetadata(),但后者容易写错类型。
- 最简写法:
app.MapGet("/time", () => DateTime.UtcNow).CacheOutput()—— 使用默认策略(20 分钟,按路径+查询参数+Accept 头生成键) - 指定命名策略:
app.MapGet("/api/items", GetItems).CacheOutput("paged"),前提是已在AddOutputCache()里注册过:options.AddPolicy("paged", p => p.Expire(TimeSpan.FromMinutes(5)).VaryByQueryKeys("page", "size")) - 避免用
.WithMetadata(new OutputCacheAttribute { Duration = 60 }):它不会触发策略注册逻辑,且OutputCacheAttribute在 Minimal API 中实际被忽略,推荐直接用.CacheOutput(p => p.Duration(60)) - 注意:带查询参数的请求(如
/api/items?id=1和/api/items?id=2)默认视为不同缓存项,无需额外配置
VaryByQueryKeys 写错会怎样?
这个参数决定“哪些查询参数参与缓存键计算”,写错会导致缓存爆炸或完全不区分。
- 写成
VaryByQueryKeys = new[] { "*" }或VaryByAll = true:每个唯一查询字符串都生成新缓存项,相当于没缓存(尤其分页接口带时间戳时极易踩坑) - 漏掉关键参数:比如分页接口只写
"page"却漏了"size",/items?page=1&size=10和/items?page=1&size=20会共用一个缓存,返回错误数据 - 想排除某个参数(如跟踪用的
utm_source):不能靠“不写它”,而要用VaryByQueryKeys = new[] { "page", "sort" }显式列出需要区分的,其余自动忽略 - 参数名大小写敏感:
VaryByQueryKeys = new[] { "Page" }对?page=1无效,必须小写匹配
缓存清除为什么有时不生效?
标签化清除(EvictByTagAsync)依赖缓存项是否真绑定了对应标签,且中间件链必须完整。
- 清除前没打标签:比如用
.CacheOutput()而不是.CacheOutput(p => p.Tag("user")),后续调EvictByTagAsync("user")就什么都不会发生 - 标签名拼写不一致:清除时用
"User",打标时用"user",不匹配 - 清除端点本身没启用缓存中间件:如果清除接口是
app.MapGet("/clear", ...),但它在UseOutputCache()之前注册,那它的请求根本不会经过缓存管道,IOutputCacheStore注入失败 - 分布式部署下,内存缓存(默认)只清本机,需换
IDistributedCache实现(如 Redis)并确保所有实例共享同一缓存后端
最易被忽略的一点:缓存键生成逻辑是硬编码在中间件里的,你无法通过配置关闭对 Accept-Encoding 或 Accept-Language 的自动 Vary;如果前端发了乱七八糟的 Accept 头,可能意外导致缓存碎片化——这时候得用自定义策略配合 NoCache() 主动拦截。


















