practical-aspnetcore 实战:ASP.NET Core Output Cache 输出缓存中间件入门(默认 1 分钟过期与 64 MB 响应体上限)
practical-aspnetcore 实战:ASP.NET Core Output Cache 输出缓存中间件入门(默认 1 分钟过期与 64 MB 响应体上限)
本文以 practical-aspnetcore 仓库中的 output-cache-1 示例为主线,讲解 ASP.NET Core 输出缓存(Output Cache)中间件的最基本用法:从服务注册、中间件挂载到端点级 .CacheOutput() 标记,并深入解析默认过期时间(1 分钟)与默认最大可缓存响应体大小(64 MB)两个核心默认值。读完本文,你将能独立复现一个最小可运行的输出缓存应用,并理解缓存命中与未命中时服务端行为的差异,为后续学习按查询字符串区分缓存、按标签批量驱逐等进阶用法打下基础。
一、什么是输出缓存(Output Cache)
输出缓存是 ASP.NET Core 内置的一种服务端缓存机制,它在响应生成之后、发送给客户端之前,将完整的 HTTP 响应(状态码、响应头和响应体)缓存起来。当相同请求再次到达时,中间件直接复用缓存中的响应,不再执行端点的处理逻辑,从而显著降低后端计算与数据库访问压力。
practical-aspnetcore 仓库在 output-cache-middleware 目录下用 8 个递进式示例系统展示了这一能力:
| 示例 | 主题 |
|---|---|
| output-cache-1 | 最基础的输出缓存用法(本文重点) |
| output-cache-2 | 按一个或多个查询字符串区分缓存 |
| output-cache-3 | 为缓存项打标签并按标签批量驱逐(IOutputCacheStore.EvictByTagAsync) |
| output-cache-4 | 通过全局 base policy 为所有端点开启缓存 |
| output-cache-5 | 基于 HttpContext 判定:仅缓存带 cache 查询字符串参数的请求 |
| output-cache-6 | 基于 HttpContext 判定:仅缓存 URL 路径以 /cache 开头的请求 |
| output-cache-7 | 命名策略(policy)覆盖全局 base policy |
| output-cache-8 | 自定义策略将过期时间设为 10 分钟 |
此外,仓库 net11/output-cache-policy-provider 还展示了 .NET 11 Preview 新增的 IOutputCachePolicyProvider 动态策略接口,可以按租户等维度动态决定缓存策略。
二、最小可运行示例:output-cache-1 完整源码
output-cache-1 是一个"极简 + 可对比"的演示:它同时暴露一个不缓存的端点和缓存的端点,都返回 DateTime.UtcNow,让读者直观看到缓存生效前后时间戳是否变化。
完整实现见 Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache();
var app = builder.Build();
app.MapGet("/", () =>
{
return Results.Content("""
<html>
<body>
<ul>
<li><a href="/now">DateTime.UtcNow()</a></li>
<li><a href="/cached-now">DateTime.UtcNow() cached</a></li>
</ul>
</body>
</html>
""", "text/html");
});
app.UseOutputCache();
app.MapGet("/now", () => DateTime.UtcNow.ToString());
app.MapGet("/cached-now", () => DateTime.UtcNow.ToString()).CacheOutput();
app.Run();
整个应用只有三个关键步骤,这也是所有输出缓存示例共通的骨架:
- 注册服务:
builder.Services.AddOutputCache()将输出缓存所需的中间件服务(含默认的IOutputCacheStore)注册进依赖注入容器; - 挂载中间件:
app.UseOutputCache()把中间件插入请求处理管道。它必须放在MapGet等端点定义之前,才能拦截并缓存响应; - 标记端点:
.CacheOutput()扩展方法把缓存行为应用到指定端点。
项目文件 output-cache-1.csproj 基于 Microsoft.NET.Sdk.Web,目标框架为 net10.0(与仓库根目录 global.json 中声明的 SDK 10.0.0 一致),使用 ImplicitUsings,因此无需显式 using Microsoft.AspNetCore.OutputCaching 即可使用 CacheOutput()(需要显式引用该命名空间时,可参考 output-cache-3/Program.cs 顶部的写法)。
运行与验证
cd projects/output-cache-middleware/output-cache-1
dotnet run
浏览器打开 http://localhost:5000 后:
- 反复刷新
/now:每次显示的DateTime.UtcNow都在变化——该端点未启用缓存; - 反复刷新
/cached-now:在默认 1 分钟过期时间内,时间戳始终不变——第一次请求生成的响应被缓存并直接复用。
也可以在命令行中对比观察(在 1 分钟窗口内连续执行两次,第二次会得到完全相同的响应体):
curl http://localhost:5000/cached-now
curl http://localhost:5000/cached-now
三、两个关键默认值:1 分钟过期与 64 MB 响应体上限
原文档明确指出本示例使用"零配置"方式,因此生效的是 OutputCacheOptions 的默认值,其中两个最值得关注:
| 默认项 | 值 | 说明 |
|---|---|---|
默认过期时间(DefaultExpirationTimeSpan) |
1 分钟(60 秒) | 缓存项在 1 分钟后过期,下次请求会重新执行端点并生成新响应 |
默认最大可缓存响应体大小(MaximumBodySize) |
64 MB | 响应体超过 64 MB 的响应不会被缓存(流式/超大响应不受影响) |
原文档对这些默认值的出处指向 ASP.NET Core 框架源码中的 OutputCacheOptions 类(src/Middleware/OutputCaching/src/OutputCacheOptions.cs),这两个数值是框架层面的既有实现事实。
理解这两个默认值对生产调优很重要:
- 1 分钟过期意味着"新鲜度"敏感的内容(比如示例中的时间戳、行情、库存等)默认最多滞后 1 分钟;如果需要更长或更短的保鲜期,必须自定义策略(见下文第五节)。
- 64 MB 上限保证了缓存不会因单个超大响应(如大文件下载)而撑爆内存;
AddOutputCache时传入的配置委托可以修改它。
其余默认行为还包括:默认策略要求响应具有可缓存条件(如 Cache-Control: public 语义、非 200 状态码默认不缓存等),这些由框架中间件根据 HTTP 语义自动判定,示例中返回 200 的普通 GET 响应均满足条件。
四、深入源码:为什么 CacheOutput() 只影响标记的端点
output-cache-1 最有教学价值的细节是:同一个应用里,/now 不缓存,/cached-now 缓存。区别仅在于后者末尾多了 .CacheOutput()。
从源码结构可以推断其机制:
.CacheOutput()本质上是给端点附加元数据(endpoint metadata),声明"该端点应用输出缓存策略";UseOutputCache()挂载的中间件在处理请求时,会先检查当前匹配端点是否携带这份元数据。只有携带元数据的端点才进入缓存判定流程;未标记的/now直接穿透中间件正常执行;- 这也是为什么示例中必须先
app.UseOutputCache()、再定义带.CacheOutput()的端点——中间件需要位于管道中端点执行之前的位置。
如果要"为每一个端点都开启缓存",则无需逐个标记,改用全局 base policy 即可。仓库 output-cache-4/Program.cs 给出了写法:
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder => builder.With(c => true));
});
此时 /now、/random 等所有端点都会被缓存,同样遵循默认 1 分钟过期。
五、进阶延伸:从"基础用法"到策略化配置
output-cache-1 是起点,仓库同目录下的后续示例正好构成一条完整的进阶路线,与本文主题一脉相承,可作为实战延伸阅读:
1. 按查询字符串区分缓存(output-cache-2)
同一个端点,?version=1 与 ?version=2 应视为不同缓存项:
app.MapGet("/cached-now", () => DateTime.UtcNow.ToString())
.CacheOutput(p => p.SetVaryByQuery("version", "culture"));
核心代码见 output-cache-2/Program.cs,SetVaryByQuery 可接收多个参数名。
2. 打标签并按标签批量驱逐(output-cache-3)
app.MapGet("/cached-now-tagged", () => DateTime.UtcNow.ToString())
.CacheOutput(p => p.Tag("datetime"));
app.MapGet("/clear", async (IOutputCacheStore cache) =>
{
await cache.EvictByTagAsync("datetime", CancellationToken.None);
return Results.Ok(new { message = "..." });
});
核心代码见 output-cache-3/Program.cs。通过注入 IOutputCacheStore 调用 EvictByTagAsync,可一次清除所有带 datetime 标签的缓存项——这是内容变更后主动失效缓存的推荐方式。
3. 基于 HttpContext 的全局判定(output-cache-5、output-cache-6)
缓存策略可以直接读取当前请求上下文,实现"按条件缓存":
// 仅缓存带 cache 查询字符串参数的请求
options.AddBasePolicy(builder => builder.With(c =>
c.HttpContext.Request.QueryString.Value?.Contains("cache") ?? false));
// 仅缓存路径以 /cached 开头的请求
options.AddBasePolicy(builder => builder.With(c =>
c.HttpContext.Request.Path.StartsWithSegments("/cached")));
分别见 output-cache-5/Program.cs 与 output-cache-6/Program.cs。
4. 命名策略覆盖全局策略(output-cache-7、output-cache-8)
先注册全局 base policy,再注册命名策略;命名策略在端点上显式引用后会覆盖全局默认:
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder => builder.With(c => c.HttpContext.Request.Path.StartsWithSegments("/cached")));
options.AddPolicy("NoCache", o => o.NoCache()); // 不缓存
options.AddPolicy("10Minutes", o => o.Expire(TimeSpan.FromMinutes(10))); // 10 分钟过期
});
app.MapGet("/cached/now-nope", () => DateTime.UtcNow.ToString()).CacheOutput("10Minutes");
分别见 output-cache-7/Program.cs 与 output-cache-8/Program.cs。o.Expire(TimeSpan.FromMinutes(10)) 正是对"默认 1 分钟过期"这一默认值的直接定制示例。
5. .NET 11 动态策略提供器(net11/output-cache-policy-provider)
仓库还收录了较新的 IOutputCachePolicyProvider 接口示例,可从配置、数据库动态加载策略,或按 X-Tenant 请求头区分不同租户的缓存副本,见 projects/net11/output-cache-policy-provider/Program.cs 与 README.md。
六、使用注意事项小结
基于 output-cache-1 及其同目录示例,可以总结出几条直接可用的实践要点:
- 三步齐备:
AddOutputCache()注册 →UseOutputCache()挂载 → 端点.CacheOutput()标记,缺一不可; - 默认过期 1 分钟:对新鲜度有要求的接口要自定义
Expire;对不要求新鲜的接口,1 分钟内的重复请求可以零成本命中缓存; - 默认响应体上限 64 MB:超大响应不会被缓存,这是内存保护机制而非缺陷;
- 主动失效用标签:数据变更时优先考虑
Tag+EvictByTagAsync,而不是等待自然过期; - 全局 vs 端点级:默认策略只作用于标记过的端点;想全量缓存需
AddBasePolicy(builder => builder.With(c => true))。
七、进一步阅读
- 本示例说明文档:output-cache-1/Readme.md
- 整个输出缓存系列导航与说明:output-cache-middleware/readme.md
- 同主题现代实现:net11/output-cache-policy-provider/README.md
- 仓库还提供 clean.sh 与 restore.sh 批量清理/还原各示例项目的辅助脚本,可在复现运行前使用。