ASP.NET Core での出力キャッシュ ミドルウェア

著者: Tom Dykstra

Note

これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

重要

この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。

現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

この記事では、ASP.NET Core アプリで出力キャッシュ ミドルウェアを構成する方法について説明します。 出力キャッシュの概要については、「出力キャッシュ」を参照してください。

出力キャッシュ ミドルウェアは、すべての種類の ASP.NET Core アプリ (Minimal API、コントローラーを使用した Web API、MVC、Razor Pages) で使用できます。 サンプル アプリは Minimal API ですが、示されているすべてのキャッシュ機能は、他のアプリの種類でもサポートされています。

ミドルウェアをアプリに追加する

AddOutputCache を呼び出すことで、出力キャッシュ ミドルウェアをサービス コレクションに追加します。

要求処理パイプラインにミドルウェアを追加するには、UseOutputCache を呼び出します。

Note

  • CORS ミドルウェアを使用するアプリでは、UseOutputCacheUseCors の後に呼び出す必要があります。
  • Razor Pages アプリとコントローラーを使用したアプリでは、UseOutputCacheUseRouting の後に呼び出す必要があります。
  • AddOutputCacheUseOutputCache を呼び出してもキャッシュ動作は開始されません。これはキャッシュを使用可能にします。 応答データのキャッシュは、次のセクションに示すように構成する必要があります。

1 つのエンドポイントまたはページを構成する

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

次の強調表示されたコードは 2 つのポリシーを作成し、それぞれが異なる有効期限を指定します。 選択されたエンドポイントは 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 - キャッシュ キーに追加する 1 つ以上のクエリ文字列名を指定します。

  • SetVaryByHeader - キャッシュ キーに追加する 1 つ以上の 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 Not Modified を返します。

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

}).CacheOutput();

キャッシュの再検証を行うもう 1 つの方法は、キャッシュ エントリの作成日をクライアントが要求した日付と比較してチェックすることです。 要求ヘッダー 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));
});

もう 1 つの代替方法は次のように MapGroup を呼び出すことです。

var blog = app.MapGroup("blog")
    .CacheOutput(builder => builder.Tag("tag-blog"));
blog.MapGet("/", Gravatar.WriteGravatar);
blog.MapGet("/post/{id}", Gravatar.WriteGravatar);

前述のタグ割り当ての例では、両方のエンドポイントが tag-blog タグによって識別されます。 その後、次のようにそのタグを参照する 1 つのステートメントを使用して、これらのエンドポイントのキャッシュ エントリを削除できます。

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" タグを使用してキャッシュ内のすべてを削除できます。

リソース ロックを無効にする

既定では、キャッシュの Stampede と Thundering Herd のリスクを軽減するためにリソース ロックは有効になっています。 詳細については、「出力キャッシュ」を参照してください。

リソース ロックを無効にするには、次の例に示すように、ポリシーの作成時に 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 と共に使用されます。 キャッシュされた応答はインプロセスに保存されるため、各サーバーには、サーバー プロセスが再起動されるたびに失われる個別のキャッシュがあります。

Redis Cache

別の方法として、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 Core アプリ (Minimal API、コントローラーを使用した Web API、MVC、Razor Pages) で使用できます。 サンプル アプリは Minimal API ですが、示されているすべてのキャッシュ機能は、他のアプリの種類でもサポートされています。

ミドルウェアをアプリに追加する

AddOutputCache を呼び出すことで、出力キャッシュ ミドルウェアをサービス コレクションに追加します。

要求処理パイプラインにミドルウェアを追加するには、UseOutputCache を呼び出します。

Note

  • CORS ミドルウェアを使用するアプリでは、UseOutputCacheUseCors の後に呼び出す必要があります。
  • Razor Pages アプリとコントローラーを使用したアプリでは、UseOutputCacheUseRouting の後に呼び出す必要があります。
  • AddOutputCacheUseOutputCache を呼び出してもキャッシュ動作は開始されません。これはキャッシュを使用可能にします。 応答データのキャッシュは、次のセクションに示すように構成する必要があります。

1 つのエンドポイントまたはページを構成する

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

次の強調表示されたコードは 2 つのポリシーを作成し、それぞれが異なる有効期限を指定します。 選択されたエンドポイントは 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 - キャッシュ キーに追加する 1 つ以上のクエリ文字列名を指定します。

  • SetVaryByHeader - キャッシュ キーに追加する 1 つ以上の 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 Not Modified を返します。

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

}).CacheOutput();

キャッシュの再検証を行うもう 1 つの方法は、キャッシュ エントリの作成日をクライアントが要求した日付と比較してチェックすることです。 要求ヘッダー 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));
});

もう 1 つの代替方法は次のように MapGroup を呼び出すことです。

var blog = app.MapGroup("blog")
    .CacheOutput(builder => builder.Tag("tag-blog"));
blog.MapGet("/", Gravatar.WriteGravatar);
blog.MapGet("/post/{id}", Gravatar.WriteGravatar);

前述のタグ割り当ての例では、両方のエンドポイントが tag-blog タグによって識別されます。 その後、次のようにそのタグを参照する 1 つのステートメントを使用して、これらのエンドポイントのキャッシュ エントリを削除できます。

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" タグを使用してキャッシュ内のすべてを削除できます。

リソース ロックを無効にする

既定では、キャッシュの Stampede と Thundering Herd のリスクを軽減するためにリソース ロックは有効になっています。 詳細については、「出力キャッシュ」を参照してください。

リソース ロックを無効にするには、次の例に示すように、ポリシーの作成時に 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 キャッシュの組み込みサポートを使用します。

関連項目