共用方式為


ASP.NET Core 中的輸出快取中介軟體

作者:Tom Dykstra

注意

這不是這篇文章的最新版本。 如需目前的版本,請參閱 本文的 .NET 9 版本。

警告

不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支援原則。 如需目前版本,請參閱本文的 .NET 8 版本

重要

這些發行前產品的相關資訊在產品正式發行前可能會有大幅修改。 Microsoft 對此處提供的資訊,不做任何明確或隱含的瑕疵擔保。

如需目前的版本,請參閱 本文的 .NET 9 版本。

本文說明如何在 ASP.NET Core 應用程式中設定輸出快取中介軟體。 如需輸出快取的簡介,請參閱輸出快取

輸出快取中介軟體可用於所有類型的 ASP.NET 核心應用程式:最小 API、具有控制器、MVC 和 Razor Pages 的 Web API。 提供了最小 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 中介軟體的應用程式中,必須在 UseCors 之後呼叫 UseOutputCache
  • 在具有控制器的 Razor Pages 應用程式和應用程式中,必須在 UseRouting 之後呼叫 UseOutputCache

設定一個端點或頁面

針對最小 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 秒。 如果未指定到期時間,則預設值為一分鐘。

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.AddPolicy("CachePost", 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 應用程式中,以下程式碼為端點選取 VaryByQuery 原則,其僅為 culture 査詢字串的每個唯一值傳回唯一回應:

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 標頭來啟用快取重新驗證。 如果用戶端傳送具有先前回應之 etag 值的 If-None-Match 標頭,且快取輸入是全新的,則伺服器會傳回 304 未修改,而不是完整回應。 以下是如何在最小 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));
});

此基底原則可讓您使用「全部標記」標籤來收回快取中的所有專案。

停用資源鎖定

根據預設,會啟用資源鎖定,以降低快取踩踏和驚群效應的風險。 如需詳細資訊,請參閱輸出快取

若要停用資源鎖定,請在建立原則時呼叫 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 Cache 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 核心應用程式:最小 API、具有控制器、MVC 和 Razor Pages 的 Web API。 範例應用程式是基本 API,但它所說明的每個快取功能在其他應用程式類型中也受到支援。

將中介軟體新增至應用程式

呼叫 AddOutputCache,將輸出快取中介軟體新增至服務集合。

呼叫 UseOutputCache,將中介軟體新增至要求處理管線。

注意

  • 在使用 CORS 中介軟體的應用程式中,必須在 UseCors 之後呼叫 UseOutputCache
  • 在具有控制器的 Razor Pages 應用程式和應用程式中,必須在 UseRouting 之後呼叫 UseOutputCache
  • 呼叫 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.AddPolicy("CachePost", 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 標頭來啟用快取重新驗證。 如果用戶端傳送具有先前回應之 etag 值的 If-None-Match 標頭,且快取專案是全新的,則伺服器會傳回 304 未修改,而不是完整回應:

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

此基底原則可讓您使用「全部標記」標籤來收回快取中的所有專案。

停用資源鎖定

根據預設,會啟用資源鎖定,以降低快取踩踏和驚群效應的風險。 如需詳細資訊,請參閱輸出快取

若要停用資源鎖定,請在建立原則時呼叫 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 沒有標記所需的不可部分完成功能。 建議您使用直接相依性來建立自訂 IOutputCacheStore 實作,例如 Redis。 或使用 .NET 8 中 Redis 快取的內建支援

另請參閱