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

作者:Tom Dykstra

注意

此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本

警告

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

对于当前版本,请参阅此文的 .NET 8 版本

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

输出缓存中间件可用于所有类型的 ASP.NET Core 应用:最小 API、带控制器的 Web API、MVC 和 Razor 页面。 为最小 API 和基于控制器的 API 提供了代码示例。 基于控制器的 API 示例演示如何使用特性配置缓存。 这些特性也可以在 MVC 和 Razor Pages 应用中使用。

代码示例引用 Gravatar 类,该类生成图像并提供“生成于”日期和时间。 该类仅在示例应用中定义和使用。 其用途是方便查看何时使用缓存的输出。 有关详细信息,请参阅如何下载示例示例代码中的预处理器指令

将中间件添加到应用

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

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

例如:

builder.Services.AddOutputCache();
var app = builder.Build();

// Configure the HTTP request pipeline.
app.UseHttpsRedirection();
app.UseOutputCache();
app.UseAuthorization();

调用 AddOutputCacheUseOutputCache 不会启动缓存行为,而是会使缓存可用。 若要做出应用缓存响应,必须按以下部分所示配置缓存。

注意

  • 在使用 CORS 中间件的应用中,UseOutputCache 必须在 UseCors 之后调用。
  • 在 Razor Pages 应用和具有控制器的应用中,UseOutputCache 必须在 UseRouting 之后调用。

配置一个终结点或页面

对于最小 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] 特性时,可以为终结点选择策略。

在最小 API 应用中,以下代码会配置一个过期时间为 20 秒的终结点,一个过期时间为 30 秒的终结点:

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

对于具有控制器的应用,将 [OutputCache] 特性应用于操作方法以选择策略:

[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "Expire20")]
public class Expire20Controller : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

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

然后为终结点选择该命名策略。 以下代码会在最小 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.AddBasePolicy(builder => 
        builder.AddPolicy<MyCustomPolicy2>(), true);
});

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

使用自定义策略作为基本策略时,不要在应该应用基本策略的任何终结点上调用 OutputCache()(没有参数)或使用 [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 策略。 在最小 API 应用中,以下代码会为仅针对 culture 查询字符串的每个唯一值返回唯一响应的终结点选择 VaryByQuery 策略:

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 HTTP 状态代码,而不是完整的响应正文。 此状态代码通知客户端,对请求的响应与客户端之前接收的内容相同。

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

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

}).CacheOutput();

下面介绍如何在基于控制器的 API 中设置 etag 值:

[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。

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

使用标记逐出缓存条目

可以使用标记来标识一组终结点,并逐出该组的所有缓存条目。 例如,以下最小 API 代码会创建一对 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"));

以下代码演示如何将标记分配给基于控制器的 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));
});

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

以下示例为最小 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 用于存储。 默认情况下,它与 MemoryCache 一起使用。 缓存的响应存储在进程内,因此每个服务器会有一个每次重启服务器进程时都会丢失的单独缓存。

Redis 缓存

替代方法是使用 Redis 缓存。 Redis 缓存可通过比单个服务器进程寿命更长的共享缓存来提供服务器节点之间的一致性。 要使用 Redis 输出缓存,请执行以下操作:

  • 安装 Microsoft.AspNetCore.OutputCaching.StackExchangeRedis NuGet 包。

  • 调用 builder.Services.AddStackExchangeRedisOutputCache(而不是 AddStackExchangeRedisCache),并提供指向 Redis 服务器的连接字符串。

    例如:

    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 Redis for Redis)的连接字符串。 例如,适用于 Azure Cache for Redis 的 <instance_name>.redis.cache.windows.net:6380,password=<password>,ssl=True,abortConnect=False
    • options.InstanceName -(可选)指定缓存的逻辑分区。

    配置选项与基于 Redis 的分布式缓存选项相同。

不建议将 IDistributedCache 与输出缓存一起使用。 IDistributedCache 没有标记所需的原子功能。 建议使用对 Redis 的内置支持,或者通过使用基础存储机制上的直接依赖项创建自定义 IOutputCacheStore 实现。

另请参阅

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

输出缓存中间件可用于所有类型的 ASP.NET Core 应用:最小 API、带控制器的 Web API、MVC 和 Razor 页面。 示例应用是一个最小 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 秒。 如果未指定有效时间,则默认为 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] 属性时,可以为终结点选择策略:

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 实施。 或使用 .NET 8 中对 Redis 缓存的内置支持

另请参阅