practical-aspnetcore 实战:ASP.NET Core Output Cache 输出缓存中间件入门(默认 1 分钟过期与 64 MB 响应体上限)

原创2026-10-14 22:03:431,673 阅读
文章标签:示例工程

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();

整个应用只有三个关键步骤,这也是所有输出缓存示例共通的骨架:

  1. 注册服务:builder.Services.AddOutputCache() 将输出缓存所需的中间件服务(含默认的 IOutputCacheStore)注册进依赖注入容器;
  2. 挂载中间件:app.UseOutputCache() 把中间件插入请求处理管道。它必须放在 MapGet 等端点定义之前,才能拦截并缓存响应;
  3. 标记端点:.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 及其同目录示例,可以总结出几条直接可用的实践要点:

  1. 三步齐备:AddOutputCache() 注册 → UseOutputCache() 挂载 → 端点 .CacheOutput() 标记,缺一不可;
  2. 默认过期 1 分钟:对新鲜度有要求的接口要自定义 Expire;对不要求新鲜的接口,1 分钟内的重复请求可以零成本命中缓存;
  3. 默认响应体上限 64 MB:超大响应不会被缓存,这是内存保护机制而非缺陷;
  4. 主动失效用标签:数据变更时优先考虑 Tag + EvictByTagAsync,而不是等待自然过期;
  5. 全局 vs 端点级:默认策略只作用于标记过的端点;想全量缓存需 AddBasePolicy(builder => builder.With(c => true))。

七、进一步阅读

登录后查看全文
practical-aspnetcore