次の方法で共有


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

著者: Tom Dykstra

Note

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

警告

このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、「.NET および .NET Core サポート ポリシー」を参照してください。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

重要

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

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

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

出力キャッシュ ミドルウェアは、すべての種類の ASP.NET Core アプリ (Minimal API、コントローラーを使用した Web API、MVC、Razor Pages) で使用できます。 最小限の 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 を呼び出してもキャッシュ動作は開始されません。これはキャッシュを使用可能にします。 アプリが応答をキャッシュできるようにするには、次のセクションで示すようにキャッシュを構成する必要があります。

Note

  • CORS ミドルウェアを使用するアプリでは、UseOutputCacheUseCors の後に呼び出す必要があります。
  • Razor Pages アプリとコントローラーを使用したアプリでは、UseOutputCacheUseRouting の後に呼び出す必要があります。

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

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

次の強調表示されたコードは 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] 属性を使用するときに、エンドポイントのポリシーを選択できます。

最小限の API アプリでは、次のコードは 20 秒の有効期限を持つ 1 つのエンドポイントと 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 - キャッシュ キーに追加する 1 つ以上のクエリ文字列名を指定します。

  • SetVaryByHeader - キャッシュ キーに追加する 1 つ以上の 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 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);
    }
}

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

次の例では、最小限の 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 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 キャッシュの組み込みサポートを使用します。

関連項目