ASP.NET Core 中的输出缓存中间件

作者:Tom Dykstra

本文介绍如何在 ASP.NET Core应用中配置输出缓存中间件。 有关输出缓存的简介,请参阅 输出缓存

输出缓存中间件可用于所有类型的 ASP.NET Core应用:最小 API、带控制器的 Web API、MVC 和 Razor Pages。 示例应用是一个最小 API,但它演示的每个缓存功能在其他应用类型中也受支持。

将中间件添加到应用

通过调用 AddOutputCache将输出缓存中间件添加到服务集合。

通过调用 UseOutputCache将中间件添加到请求处理管道。

注意

  • 在使用 CORS 中间件的应用中, UseOutputCache 必须在 之后 UseCors调用 。
  • 在 Razor 具有控制器的 Pages 应用和应用中, UseOutputCache 必须在 之后 UseRouting调用 。
  • 调用 AddOutputCacheUseOutputCache 不会启动缓存行为,它使缓存可用。 必须按以下部分所示配置缓存响应数据。

配置一个终结点或页面

对于最小 API 应用,请通过调用 CacheOutput或应用 [OutputCache] 属性来配置终结点以执行缓存,如以下示例所示:

app.MapGet("/cached", Gravatar.WriteGravatar).CacheOutput();
app.MapGet("/attribute", [OutputCache] (context) => 
    Gravatar.WriteGravatar(context));

对于具有控制器的应用,将 [OutputCache] 属性应用于操作方法。 对于 Razor Pages 应用,将 属性应用于 Razor 页面类。

配置多个终结点或页面

调用 AddOutputCache 以指定应用于多个终结点的缓存配置时创建策略。 可以为特定终结点选择策略,而基本策略为终结点集合提供默认缓存配置。

以下突出显示的代码为应用的所有终结点配置缓存,过期时间为 10 秒。 如果未指定过期时间,则默认为一分钟。

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] 可以为终结点选择策略:

app.MapGet("/20", Gravatar.WriteGravatar).CacheOutput("Expire20");
app.MapGet("/30", [OutputCache(PolicyName = "Expire30")] (context) => 
    Gravatar.WriteGravatar(context));

对于具有控制器的应用,将 [OutputCache] 属性应用于操作方法。 对于 Razor Pages 应用,将 属性应用于 Razor 页面类。

默认输出缓存策略

默认情况下,输出缓存遵循以下规则:

  • 仅缓存 HTTP 200 响应。
  • 仅缓存 HTTP GET 或 HEAD 请求。
  • 不会缓存设置 cookie的响应。
  • 不会缓存对经过身份验证的请求的响应。

以下代码将所有默认缓存规则应用于应用的所有终结点:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder.Cache());
});

替代默认策略

以下代码演示如何重写默认规则。 以下自定义策略代码中突出显示的行为 HTTP POST 方法和 HTTP 301 响应启用缓存:

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

然后选择终结点的命名策略:

app.MapPost("/cachedpost", Gravatar.WriteGravatar)
    .CacheOutput("CachePost");

替代默认策略替代

或者,使用依赖项注入 (DI) 初始化实例,对自定义策略类进行以下更改:

  • 公共构造函数,而不是专用构造函数。
  • Instance消除自定义策略类中的 属性。

例如: 。

public sealed class MyCustomPolicy2 : IOutputCachePolicy
{

    public MyCustomPolicy2()
    {
    }

类的其余部分与前面所示的相同。 添加自定义策略,如以下示例所示:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.AddPolicy<MyCustomPolicy2>(), true);
});

前面的代码使用 DI 创建自定义策略类的实例。 将解析构造函数中的任何公共参数。

将自定义策略用作基本策略时,不要在应应用基本策略的任何终结点上调用 OutputCache() 没有参数) (。 调用 OutputCache() 会将默认策略添加到终结点。

指定缓存键

默认情况下,URL 的每个部分都作为缓存项(即方案、主机、端口、路径和查询字符串)的键包含在内。 但是,你可能希望显式控制缓存密钥。 例如,假设有一个终结点,该终结点仅针对查询字符串的每个唯一值返回唯一响应 culture 。 URL 的其他部分(如其他查询字符串)的变化不应导致不同的缓存条目。 可以在策略中指定此类规则,如以下突出显示的代码所示:

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

然后,可以为终结点选择 VaryByQuery 策略:

app.MapGet("/query", Gravatar.WriteGravatar).CacheOutput("Query");

下面是用于控制缓存键的一些选项:

  • SetVaryByQuery - 指定要添加到缓存键的一个或多个查询字符串名称。

  • SetVaryByHeader - 指定要添加到缓存密钥的一个或多个 HTTP 标头。

  • VaryByValue- 指定要添加到缓存键的值。 以下示例使用一个值,该值指示当前服务器时间(以秒为单位)是奇数还是偶数。 仅当秒数从奇数到偶数甚至奇数时,才会生成新的响应。

    app.MapGet("/varybyvalue", Gravatar.WriteGravatar)
        .CacheOutput(c => c.VaryByValue((context) => 
            new KeyValuePair<string, string>(
                "time", (DateTime.Now.Second % 2)
                    .ToString(CultureInfo.InvariantCulture))));
    

使用 OutputCacheOptions.UseCaseSensitivePaths 指定键的路径部分区分大小写。 默认值不区分大小写。

有关更多选项,请参阅 OutputCachePolicyBuilder 类。

缓存重新验证

缓存重新验证意味着服务器可以返回 304 Not Modified HTTP 状态代码,而不是完整的响应正文。 此状态代码通知客户端,对请求的响应与客户端之前收到的响应保持不变。

以下代码演示了如何使用 Etag 标头来启用缓存重新验证。 如果客户端发送的 If-None-Match 标头具有早期响应的 etag 值,并且缓存条目为全新,则服务器将返回 304 Not Modified 而不是完整响应:

app.MapGet("/etag", async (context) =>
{
    var etag = $"\"{Guid.NewGuid():n}\"";
    context.Response.Headers.ETag = etag;
    await Gravatar.WriteGravatar(context);

}).CacheOutput();

执行缓存重新验证的另一种方法是检查缓存条目创建日期与客户端请求的日期相比。 提供请求标头 If-Modified-Since 时,如果缓存的条目较旧且未过期,输出缓存将返回 304。

缓存重新验证会自动响应从客户端发送的这些标头。 除了启用输出缓存之外,无需在服务器上进行特殊配置即可启用此行为。

使用标记逐出缓存条目

可以使用标记来标识一组终结点,并逐出该组的所有缓存条目。 例如,以下代码创建一对 URL 以“blog”开头的终结点,并将其标记为“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")); ;

为同一对终结点分配标记的另一种方法是定义一个适用于以 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));
});

另一种替代方法是调用 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));
});

以下示例为终结点选择无锁定策略:

app.MapGet("/nolock", Gravatar.WriteGravatar)
    .CacheOutput("NoLock");

限制

的以下属性 OutputCacheOptions 允许配置适用于所有终结点的限制:

  • SizeLimit - 缓存存储的最大大小。 达到此限制时,在逐出较旧的条目之前,不会缓存任何新响应。 默认值为 100 MB。
  • MaximumBodySize - 如果响应正文超过此限制,则不会缓存它。 默认值为 64 MB。
  • DefaultExpirationTimeSpan - 策略未指定时适用的过期时间持续时间。 默认值为 60 秒。

缓存存储

IOutputCacheStore 用于存储。 默认情况下,它与 MemoryCache一起使用。 不建议 IDistributedCache 与输出缓存一起使用。 IDistributedCache 没有标记所需的原子功能。 建议使用对基础存储机制(如 Redis)的直接依赖项创建自定义 IOutputCacheStore 实现。

另请参阅