本文介绍如何在 ASP.NET Core 应用中配置输出缓存中间件。关于输出缓存的概念,见输出缓存概述。
输出缓存中间件可用于 Minimal API、带控制器的 Web API、MVC 和 Razor Pages。下面提供 Minimal API 与控制器 API 的示例。控制器示例用特性配置缓存,这些特性也能用于 MVC 和 Razor Pages。
示例引用一个 Gravatar 类,它生成图像并显示生成日期与时间,方便观察是否使用了缓存响应。该类只定义和用于示例应用,并非框架自带 API。更多信息见下载示例和预处理器指令。
将中间件添加到应用
调用 AddOutputCache,把输出缓存服务加入服务集合:
builder.Services.AddOutputCache();
调用 UseOutputCache,把中间件加入请求处理管道:
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseHttpsRedirection();
app.UseAuthorization();
app.UseOutputCache();
这两个调用使缓存能力可用,但不会自动开始缓存响应。还需要按下文配置缓存策略或端点。
- 使用 CORS 中间件时,
UseOutputCache必须在UseCors之后调用。 - Razor Pages 与控制器应用中,
UseOutputCache必须在UseRouting之后调用。 - 使用身份验证或授权时,
UseOutputCache必须在UseAuthentication和UseAuthorization之后调用。否则可能向未授权用户提供缓存内容。此顺序按官方英文和文档仓库核对,修正了中文页面的倒译。
为一个端点或页面配置缓存
Minimal API 可以调用 CacheOutput,也可以使用 [OutputCache] 特性:
app.MapGet("/cached", Gravatar.WriteGravatar).CacheOutput();
app.MapGet("/attribute", [OutputCache] (context) =>
Gravatar.WriteGravatar(context));
控制器应用可以在操作方法上应用 [OutputCache]。下面示例把特性放在控制器类上:
[ApiController]
[Route("/[controller]")]
[OutputCache]
public class CachedController : ControllerBase
{
public async Task GetAsync()
{
await Gravatar.WriteGravatar(HttpContext);
}
}
Razor Pages 应用把特性应用到 Razor 页面类。
为多个端点或页面配置缓存
调用 AddOutputCache 时创建策略,为多个端点指定缓存配置。可以给具体端点选择命名策略,也可以用基本策略为一组端点提供默认配置。
下面的基本策略把所有端点的过期时间设为 10 秒。未指定过期时间时,默认值为 1 分钟:
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder =>
builder.Expire(TimeSpan.FromSeconds(10)));
options.AddPolicy("Expire20", builder =>
builder.Expire(TimeSpan.FromSeconds(20)));
options.AddPolicy("Expire30", builder =>
builder.Expire(TimeSpan.FromSeconds(30)));
});
同一段配置还创建了两个命名策略,分别使用 20 秒和 30 秒的过期时间:
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder =>
builder.Expire(TimeSpan.FromSeconds(10)));
options.AddPolicy("Expire20", builder =>
builder.Expire(TimeSpan.FromSeconds(20)));
options.AddPolicy("Expire30", builder =>
builder.Expire(TimeSpan.FromSeconds(30)));
});
调用 CacheOutput 或使用 [OutputCache] 时,可以选择端点策略。以下 Minimal API 端点分别选择 20 秒与 30 秒:
app.MapGet("/20", Gravatar.WriteGravatar).CacheOutput("Expire20");
app.MapGet("/30", [OutputCache(PolicyName = "Expire30")] (context) =>
Gravatar.WriteGravatar(context));
控制器应用使用特性的 PolicyName 选择策略:
[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "Expire20")]
public class Expire20Controller : ControllerBase
{
public async Task GetAsync()
{
await Gravatar.WriteGravatar(HttpContext);
}
}
Razor Pages 应用把特性应用到页面类。
使用默认输出缓存策略
默认策略遵循以下规则:
- 只缓存 HTTP 200 响应。
- 只缓存 HTTP GET 或 HEAD 请求。
- 不缓存设置 Cookie 的响应。
- 不缓存已经过身份验证的请求所产生的响应。
下面把全部默认缓存规则应用到所有端点:
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder => builder.Cache());
});
覆盖默认策略
下面的自定义策略允许缓存 HTTP POST 请求以及 HTTP 301 响应。它仍检查认证信息与 Cookie,并启用查询字符串变化键和资源锁:
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.Primitives;
namespace OCMinimal;
public sealed class MyCustomPolicy : IOutputCachePolicy
{
public static readonly MyCustomPolicy Instance = new();
private MyCustomPolicy()
{
}
ValueTask IOutputCachePolicy.CacheRequestAsync(
OutputCacheContext context,
CancellationToken cancellationToken)
{
var attemptOutputCaching = AttemptOutputCaching(context);
context.EnableOutputCaching = true;
context.AllowCacheLookup = attemptOutputCaching;
context.AllowCacheStorage = attemptOutputCaching;
context.AllowLocking = true;
// Vary by any query by default
context.CacheVaryByRules.QueryKeys = "*";
return ValueTask.CompletedTask;
}
ValueTask IOutputCachePolicy.ServeFromCacheAsync
(OutputCacheContext context, CancellationToken cancellationToken)
{
return ValueTask.CompletedTask;
}
ValueTask IOutputCachePolicy.ServeResponseAsync
(OutputCacheContext context, CancellationToken cancellationToken)
{
var response = context.HttpContext.Response;
// Verify existence of cookie headers
if (!StringValues.IsNullOrEmpty(response.Headers.SetCookie))
{
context.AllowCacheStorage = false;
return ValueTask.CompletedTask;
}
// Check response code
if (response.StatusCode != StatusCodes.Status200OK &&
response.StatusCode != StatusCodes.Status301MovedPermanently)
{
context.AllowCacheStorage = false;
return ValueTask.CompletedTask;
}
return ValueTask.CompletedTask;
}
private static bool AttemptOutputCaching(OutputCacheContext context)
{
// Check if the current request fulfills the requirements
// to be cached
var request = context.HttpContext.Request;
// Verify the method
if (!HttpMethods.IsGet(request.Method) &&
!HttpMethods.IsHead(request.Method) &&
!HttpMethods.IsPost(request.Method))
{
return false;
}
// Verify existence of authorization headers
if (!StringValues.IsNullOrEmpty(request.Headers.Authorization) ||
request.HttpContext.User?.Identity?.IsAuthenticated == true)
{
return false;
}
return true;
}
}
使用该自定义策略时,先创建命名策略:
builder.Services.AddOutputCache(options =>
{
options.AddPolicy("CachePost", MyCustomPolicy.Instance);
});
然后为端点选择该策略。Minimal API 示例:
app.MapPost("/cachedpost", Gravatar.WriteGravatar)
.CacheOutput("CachePost");
控制器示例:
[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "CachePost")]
public class PostController : ControllerBase
{
public async Task GetAsync()
{
await Gravatar.WriteGravatar(HttpContext);
}
}
使用另一种默认策略覆盖方式
也可以通过依赖注入(DI)初始化策略实例。此时把自定义策略类的构造函数从私有改为公开,并删除 Instance 属性。下面仅展示变化后的类开头,类的其余部分与前面的实现相同:
public sealed class MyCustomPolicy2 : IOutputCachePolicy
{
public MyCustomPolicy2()
{
}
按下面方式加入自定义策略:
builder.Services.AddOutputCache(options =>
{
options.AddPolicy("CachePost", builder =>
builder.AddPolicy<MyCustomPolicy2>(), true);
});
DI 会创建该策略类的实例,构造函数中的公开参数会被解析。
将自定义策略作为基本策略时,不要在该基本策略应覆盖的端点上再调用不带参数的 CacheOutput(),也不要再应用 [OutputCache];它们会给端点加入默认策略。原文这一段写作 OutputCache(),这里按官方端点扩展 API和上下文示例澄清方法名称,代码保持原样。
指定缓存键
默认情况下,URL 的每个部分都包含在缓存键中,包括 scheme、host、port、path 与 query string。但有时需要显式控制缓存键。例如某端点只按 culture 查询参数返回不同响应,那么其他查询参数的变化不应创建不同缓存项。
可以在策略中指定这样的规则:
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder => builder
.With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
.Tag("tag-blog"));
options.AddBasePolicy(builder => builder.Tag("tag-all"));
options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
options.AddPolicy("NoCache", builder => builder.NoCache());
options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});
再为端点选择查询参数变化策略。Minimal API 示例:
app.MapGet("/query", Gravatar.WriteGravatar).CacheOutput("Query");
控制器示例:
[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "Query")]
public class QueryController : ControllerBase
{
public async Task GetAsync()
{
await Gravatar.WriteGravatar(HttpContext);
}
}
控制缓存键的一些选项:
SetVaryByQuery:指定加入缓存键的一个或多个查询参数名。SetVaryByHeader:指定加入缓存键的一个或多个 HTTP 请求头。VaryByValue:提供一个加入缓存键的值。下面按当前服务器时间秒数的奇偶性生成变化键;只有秒数从奇变偶或从偶变奇时,才生成新的响应:
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder => builder
.With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
.Tag("tag-blog"));
options.AddBasePolicy(builder => builder.Tag("tag-all"));
options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
options.AddPolicy("NoCache", builder => builder.NoCache());
options.AddPolicy("NoLock", builder => builder.SetLocking(false));
options.AddPolicy("VaryByValue", builder =>
builder.VaryByValue((context) =>
new KeyValuePair<string, string>(
"time", (DateTime.Now.Second % 2)
.ToString(CultureInfo.InvariantCulture))));
});
可以用 OutputCacheOptions.UseCaseSensitivePaths 指定缓存键的路径部分区分大小写;默认不区分。更多选项见 OutputCachePolicyBuilder。
启用缓存重新验证
重新验证允许服务器返回 304 Not Modified,代替完整响应体。这个状态告诉客户端,响应内容与其此前收到的版本相同。
下面用 ETag 响应头启用重新验证。客户端发送 If-None-Match,值与先前响应的 ETag 相同,并且缓存项仍有效时,服务器会返回 304 Not Modified。Minimal API 示例:
app.MapGet("/etag", async (context) =>
{
var etag = $"\"{Guid.NewGuid():n}\"";
context.Response.Headers.ETag = etag;
await Gravatar.WriteGravatar(context);
}).CacheOutput();
控制器 API 示例:
[ApiController]
[Route("/[controller]")]
[OutputCache]
public class EtagController : ControllerBase
{
public async Task GetAsync()
{
var etag = $"\"{Guid.NewGuid():n}\"";
HttpContext.Response.Headers.ETag = etag;
await Gravatar.WriteGravatar(HttpContext);
}
}
另一种方式是比较缓存项创建时间与客户端请求的日期。请求包含 If-Modified-Since 时,如果缓存项比该日期更旧且没有过期,输出缓存会返回 304。
启用输出缓存后,中间件会自动响应这些客户端请求头;除启用缓存外,无需另外配置服务器来开启这项重新验证行为。
使用标签驱逐缓存项
标签可以标识一组端点,并一次驱逐整组缓存项。下面创建两个 URL 以 blog 开头的 Minimal API 端点,并赋予 tag-blog 标签:
app.MapGet("/blog", Gravatar.WriteGravatar)
.CacheOutput(builder => builder.Tag("tag-blog"));
app.MapGet("/blog/post/{id}", Gravatar.WriteGravatar)
.CacheOutput(builder => builder.Tag("tag-blog"));
控制器 API 的标签配置:
[ApiController]
[Route("/[controller]")]
[OutputCache(Tags = new[] { "tag-blog", "tag-all" })]
public class TagEndpointController : ControllerBase
{
public async Task GetAsync()
{
await Gravatar.WriteGravatar(HttpContext);
}
}
也可以通过适用于 blog 路由端点的基本策略设置标签:
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder => builder
.With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
.Tag("tag-blog"));
options.AddBasePolicy(builder => builder.Tag("tag-all"));
options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
options.AddPolicy("NoCache", builder => builder.NoCache());
options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});
Minimal API 还可以通过 MapGroup 设置:
var blog = app.MapGroup("blog")
.CacheOutput(builder => builder.Tag("tag-blog"));
blog.MapGet("/", Gravatar.WriteGravatar);
blog.MapGet("/post/{id}", Gravatar.WriteGravatar);
上面的标签示例让两个端点都带有 tag-blog。下面通过引用该标签,一次驱逐这些端点的缓存项:
app.MapPost("/purge/{tag}", async (IOutputCacheStore cache, string tag) =>
{
await cache.EvictByTagAsync(tag, default);
});
向 https://localhost:<port>/purge/tag-blog 发送 HTTP POST 请求,会驱逐对应缓存。
如果需要驱逐所有端点的全部缓存项,可以给所有端点加上基本标签策略:
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder => builder
.With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
.Tag("tag-blog"));
options.AddBasePolicy(builder => builder.Tag("tag-all"));
options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
options.AddPolicy("NoCache", builder => builder.NoCache());
options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});
这样就能用 tag-all 驱逐全部缓存项。
关闭资源锁
默认启用资源锁,以降低缓存击穿与大量并发请求同时重新生成同一内容的风险。更多背景见输出缓存概述。
创建策略时调用 SetLocking(false) 可以关闭资源锁:
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder => builder
.With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
.Tag("tag-blog"));
options.AddBasePolicy(builder => builder.Tag("tag-all"));
options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
options.AddPolicy("NoCache", builder => builder.NoCache());
options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});
Minimal API 为端点选择不加锁策略:
app.MapGet("/nolock", Gravatar.WriteGravatar)
.CacheOutput("NoLock");
控制器 API 使用特性选择:
[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "NoLock")]
public class NoLockController : ControllerBase
{
public async Task GetAsync()
{
await Gravatar.WriteGravatar(HttpContext);
}
}
配置容量与期限限制
OutputCacheOptions 提供以下作用于所有端点的设置:
SizeLimit:缓存存储的最大容量。达到上限后,旧条目被驱逐前不再缓存新响应。默认 100 MB。MaximumBodySize:允许缓存的最大响应体大小。响应体超过限制时不缓存。默认 64 MB。DefaultExpirationTimeSpan:策略未指定过期时间时使用的默认缓存时长。默认 60 秒。
选择缓存存储
存储通过 IOutputCacheStore 接口提供。默认采用内存缓存,响应缓存在进程内,因此各服务器拥有独立缓存;服务器进程重启后,该进程的缓存会丢失。
替代方案:Redis
Redis 提供多个服务器节点之间的共享缓存,使各节点的缓存状态一致,并且不随单个应用进程结束而丢失。使用 Redis 输出缓存:
- 安装
Microsoft.AspNetCore.OutputCaching.StackExchangeRedisNuGet 包。 - 调用
builder.Services.AddStackExchangeRedisOutputCache,提供指向 Redis 服务器的连接字符串;这里使用的是输出缓存注册方法,不是AddStackExchangeRedisCache。
builder.Services.AddStackExchangeRedisOutputCache(options =>
{
options.Configuration =
builder.Configuration.GetConnectionString("MyRedisConStr");
options.InstanceName = "SampleInstance";
});
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder =>
builder.Expire(TimeSpan.FromSeconds(10)));
});
options.Configuration是本地或托管 Redis 服务器的连接字符串。例如官方 Azure 示例为<instance_name>.redis.cache.windows.net:6380,password=,pw,ssl=True,abortConnect=False;它是占位示例,不是真实凭据。- 可选的
options.InstanceName为缓存指定逻辑分区。
这些配置选项与 Redis 分布式缓存选项相同。
不推荐:直接使用 IDistributedCache
不推荐把 IDistributedCache 用作输出缓存存储,因为它不提供标签功能所需的原子操作。推荐使用内置 Redis 支持,或直接依赖底层存储机制,实现自定义 IOutputCacheStore。











暂无评论内容