ASP.NET Core 输出缓存策略:为接口设置 10 分钟过期时间的实战示例(practical-aspnetcore 的 output-cache-8)
ASP.NET Core 输出缓存策略:为接口设置 10 分钟过期时间的实战示例(practical-aspnetcore 的 output-cache-8)
本文基于 practical-aspnetcore 仓库中的 output-cache-8 示例 展开。该示例演示了如何在 ASP.NET Core 中注册一个指定过期时间为 10 分钟的输出缓存策略(Output Cache Policy),并在单个端点(endpoint)上显式应用它。读完本文,你将掌握 AddOutputCache、AddPolicy、Expire、CacheOutput 等核心 API 的用法,理解“命名策略覆盖全局基础策略”的机制,并能在自己的 Minimal API 项目中复现同样的缓存行为。
输出缓存与缓存策略:先理解两个核心概念
ASP.NET Core 自 .NET 7 起提供了输出缓存中间件(Output Caching Middleware),它允许将 HTTP 响应整体缓存起来,后续相同请求直接复用缓存响应,避免重复执行处理器逻辑、显著降低服务器开销。output-cache-8 所在的 output-cache-middleware 系列 共有 8 个示例,循序渐进地覆盖了该中间件的主要用法:
| 示例 | 主题 |
|---|---|
| output-cache-1 | 使用最基本的 AddOutputCache() 选项 |
| output-cache-2 | 按一个或多个查询字符串参数区分缓存 |
| output-cache-3 | 为缓存项打标签,并通过 IOutputCacheStore.EvictByTagAsync 按标签清除 |
| output-cache-4 | 为所有端点启用输出缓存 |
| output-cache-5 | 全局策略:缓存带 "cache" 查询字符串参数的请求 |
| output-cache-6 | 全局策略:缓存 URL 段以 "/cache" 开头的请求 |
| output-cache-7 | 定义并应用命名策略,具体策略覆盖全局基础策略 |
| output-cache-8 | 定义过期时间为 10 分钟的缓存策略 |
本示例聚焦“策略(Policy)”与“过期时间(Expiration)”的组合用法。在输出缓存体系中,策略分为两类:
- 基础策略(Base Policy):通过
AddBasePolicy注册,定义一个匹配条件(谓词),命中条件的请求默认都会走该策略; - 命名策略(Named Policy):通过
AddPolicy("名称", ...)注册,仅在端点显式调用CacheOutput("名称")时才生效,且优先于基础策略。
output-cache-8 同时演示了这两者的搭配:既有一个面向 /cached 路径的基础策略,又有一个名为 "10Minutes"、过期时间为 10 分钟的命名策略。
示例概览:三个端点对照演示
示例首页通过一个 HTML 页面列出三个链接,用 DateTime.UtcNow() 的输出时间直观展示缓存效果:
/now:不缓存,每次刷新都返回新的 UTC 时间;/cached/now:命中/cached前缀的基础策略,按默认 60 秒过期缓存;/cached/now-nope:路径同样以/cached开头,但端点显式应用了"10Minutes"命名策略,因此按 10 分钟过期缓存。
(“默认 60 秒”这一说法直接来自示例首页的文案 DateTime.UtcNow() cached for default 60 seconds,是仓库内可验证的事实。)
核心代码逐段解析
output-cache-8 的完整源码 Program.cs 如下:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder => builder.With(c => c.HttpContext.Request.Path.StartsWithSegments("/cached")));
options.AddPolicy("10Minutes", o => o.Expire(TimeSpan.FromMinutes(10)));
});
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 for default 60 seconds</a></li>
<li><a href="/cached/now-nope">DateTime.UtcNow() cached for 10 minutes</a></li>
</ul>
</body>
</html>
""", "text/html");
});
app.UseOutputCache();
app.MapGet("/now", () => DateTime.UtcNow.ToString());
app.MapGet("/cached/now", () => DateTime.UtcNow.ToString());
app.MapGet("/cached/now-nope", () => DateTime.UtcNow.ToString()).CacheOutput("10Minutes");
app.Run();
第一步:注册输出缓存服务并定义策略
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder => builder.With(c => c.HttpContext.Request.Path.StartsWithSegments("/cached")));
options.AddPolicy("10Minutes", o => o.Expire(TimeSpan.FromMinutes(10)));
});
这里发生了三件事:
AddOutputCache():向依赖注入容器注册输出缓存服务(缓存存储、策略注册表、中间件所需的依赖)。项目文件 output-cache-8.csproj 表明示例基于net10.0目标框架,ImplicitUsings已启用,因此无需显式using命名空间即可使用这些 API。AddBasePolicy:注册基础策略,谓词c.HttpContext.Request.Path.StartsWithSegments("/cached")表示“凡是请求路径以/cached段开头的端点,都默认纳入输出缓存”。这是该系列中 output-cache-6 的延续(output-cache-6 源码 使用完全相同的谓词),此处基础策略未显式指定过期时间,因此采用框架默认的 60 秒。AddPolicy("10Minutes", o => o.Expire(TimeSpan.FromMinutes(10))):注册一个名为"10Minutes"的命名策略,通过Expire(TimeSpan.FromMinutes(10))把该策略的缓存过期时间设置为 10 分钟。Expire接受一个TimeSpan,是控制缓存存活时长的最直接手段。
第二步:启用中间件并挂载端点
app.UseOutputCache();
app.MapGet("/now", () => DateTime.UtcNow.ToString());
app.MapGet("/cached/now", () => DateTime.UtcNow.ToString());
app.MapGet("/cached/now-nope", () => DateTime.UtcNow.ToString()).CacheOutput("10Minutes");
关键点在于:
app.UseOutputCache()必须调用,中间件才会实际介入请求管线,在端点执行前查询缓存、在端点执行后写入缓存;- 三个端点中,
/cached/now与/cached/now-nope都满足基础策略的/cached前缀条件,本来都会按默认 60 秒缓存; - 唯独
/cached/now-nope通过.CacheOutput("10Minutes")显式指定了命名策略。由于命名策略优先于基础策略,这个端点最终按 10 分钟过期,而/cached/now仍按基础策略的默认 60 秒过期——首页文案中的“cached for 10 minutes”与“cached for default 60 seconds”正是对这一行为的实证。
第三步:项目配置
output-cache-8.csproj 内容极简:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>true</ImplicitUsings>
</PropertyGroup>
</Project>
值得注意的是,整个示例不需要引入任何 NuGet 包——输出缓存是 ASP.NET Core 框架自带能力,这是它相比 Response Caching 等方案的便利之处。
运行与验证
在示例目录下执行:
dotnet run
随后访问 http://localhost:5000/(具体端口以启动日志为准)打开演示首页,按顺序观察:
- 点击
/now,多次刷新,时间每次都变化——未缓存; - 点击
/cached/now,多次刷新,时间在 60 秒内保持不变——基础策略默认 60 秒缓存生效; - 点击
/cached/now-nope,多次刷新,时间在 10 分钟内保持不变——"10Minutes"命名策略覆盖了基础策略。
通过这个三端点对照,可以直观地验证“策略是端点级显式行为”这一核心机制:基础策略负责兜底,命名策略负责精准控制。
为什么命名策略能覆盖基础策略:对照 output-cache-7
output-cache-8 与 output-cache-7 示例 的代码结构几乎完全一致,后者在其 Readme 中明确写道:"Specific policy override the global base policy."(具体策略覆盖全局基础策略)。
对比两份 Program.cs 可以看到同一模式的两种变体:
- output-cache-7 注册
AddPolicy("NoCache", o => o.NoCache()),并在端点CacheOutput("NoCache"),使该端点完全不缓存; - output-cache-8 注册
AddPolicy("10Minutes", o => o.Expire(TimeSpan.FromMinutes(10))),并在端点CacheOutput("10Minutes"),使该端点缓存 10 分钟。
两者用的是同一条规则:当端点显式指定命名策略时,命名策略的配置(缓存与否、过期时长)会覆盖全局基础策略对该端点的约束。output-cache-8 正是“控制过期时间”这一维度的演示。从源码结构看,CacheOutput(policyName) 是 OutputCachePolicyBuilder 应用策略的入口,它会以该命名策略的定义为准构建端点的缓存行为,因此同一路径前缀下的端点可以拥有截然不同的过期时间。
同系列扩展:从 60 秒到 10 分钟再到更多玩法
本示例的 10 分钟过期策略并非孤立存在,它在整个系列中处于“策略体系”的收尾位置。若想进一步掌握输出缓存,可以顺着下列源码继续深入:
- output-cache-1:不配置任何策略,端点直接
.CacheOutput(),使用默认 60 秒过期——理解最小可用形态; - output-cache-2:
.CacheOutput(p => p.SetVaryByQuery("version", "culture")),按查询字符串参数拆分缓存条目; - output-cache-3:
.CacheOutput(p => p.Tag("datetime"))打标签,再通过IOutputCacheStore.EvictByTagAsync主动清除整组缓存; - output-cache-4:
AddBasePolicy(builder => builder.With(c => true)),让所有端点默认都缓存; - output-cache-5:谓词判断查询字符串是否包含 "cache" 字样,实现“按条件缓存”;
- output-cache-6:谓词判断路径是否以
/cached开头,正是本示例基础策略的出处; - output-cache-7:
NoCache()策略覆盖基础策略,与本示例互为镜像。
通过这组对照可以看出,OutputCachePolicyBuilder 提供的 Expire、NoCache、SetVaryByQuery、Tag 等 API 组合起来,足以覆盖绝大多数 Web API 的缓存需求:默认兜底、按需差异化、可控过期、可主动失效。
小结
output-cache-8 用不到 30 行代码讲清了 ASP.NET Core 输出缓存中“策略 + 过期时间”的完整链路:用 AddBasePolicy 建立默认缓存范围,用 AddPolicy("10Minutes", o => o.Expire(TimeSpan.FromMinutes(10))) 定义带 10 分钟过期时间的命名策略,再用端点上的 CacheOutput("10Minutes") 让特定接口获得更长的缓存寿命。将这一模式推广开来,你只需调整 TimeSpan 参数,就能为不同接口定制从秒级到小时级的差异化缓存策略。