ASP.NET Core のメモリ内キャッシュ

作成者: Rick AndersonJohn LuoSteve Smith

キャッシュを使用すると、コンテンツを生成するために必要な作業量を減らすことによって、アプリのパフォーマンスとスケーラビリティを大幅に向上させることができます。 キャッシュは、頻繁に変更され、しかも、生成に負荷がかかるデータに対して最適に機能します。 キャッシュを使用すると、提供元からよりもはるかに高速に返されるデータのコピーが作成されます。 アプリは、キャッシュされたデータに依存しないように作成およびテストする必要があります。

ASP.NET Core は、いくつかの異なるキャッシュをサポートしています。 最も単純なキャッシュは、IMemoryCache に基づいています。 IMemoryCache は、Web サーバーのメモリに格納されているキャッシュを表します。 サーバー ファーム (複数のサーバー) で実行されているアプリでメモリ内キャッシュを使用するときは、セッションがスティッキーであることを確認する必要があります。 スティッキー セッションでは、クライアントからの要求がすべて同じサーバーに送られます。 たとえば、Azure Web アプリの場合は、アプリケーション要求ルーティング処理 (ARR) を使用して、すべての要求を同じサーバーにルーティングします。

Web ファームのスティッキーでないセッションでは、キャッシュ整合性の問題を回避するために分散キャッシュが必要です。 アプリによっては、分散キャッシュの方がメモリ内キャッシュよりも高いスケールアウトをサポートする場合があります。 分散キャッシュを使用すると、キャッシュ メモリから外部プロセスにオフロードされます。

メモリ内キャッシュには、任意のオブジェクトを格納できます。 分散キャッシュ インターフェイスは byte[] に制限されています。 メモリ内および分散キャッシュには、キャッシュ項目がキーと値のペアとして格納されます。

System.Runtime.Caching と MemoryCache

System.Runtime.Caching/MemoryCache (NuGet パッケージ) は、次のものと共に使用できます。

  • .NET Standard 2.0 以降。
  • .NET Standard 2.0 以降を対象とするすべての .NET 実装。 たとえば、ASP.NET Core 3.1 以降。
  • .NET Framework 4.5 以降。

System.Runtime.Caching/MemoryCache よりも Microsoft.Extensions.Caching.Memory/IMemoryCache (この記事で説明しています) をお勧めします。理由は、ASP.NET Core への統合が強化されているためです。 たとえば、IMemoryCache は ASP.NET Core の依存関係の挿入においてネイティブに機能します。

ASP.NET 4.x から ASP.NET Core にコードを移植するときは、System.Runtime.Caching/MemoryCache を互換性ブリッジとして使用します。

キャッシュのガイドライン

  • コードでは常に、データをフェッチするためのフォールバック オプションを使用し、キャッシュされた値が使用可能であることに依存しないようにする必要があります。
  • キャッシュには、貴重なリソースであるメモリが使用されます。 キャッシュの拡張を制限してください。
    • キャッシュに外部入力を挿入しないでください。 例として、任意のユーザー指定の入力をキャッシュ キーとして使用することはお勧めしません。これは、入力で予期しない量のメモリが消費される可能性があるためです。
    • キャッシュの拡張を制限するには、有効期限を使用します。
    • SetSize、Size、SizeLimit を使用してキャッシュ サイズを制限します。 ASP.NET Core ランタイムでメモリ負荷に基づいてキャッシュ サイズが制限されることはありません。 キャッシュ サイズを制限する決定は開発者が行います。

IMemoryCache を使用する

警告

依存関係の挿入からの "共有" メモリ キャッシュを使用しながら、SetSizeSizeSizeLimit のいずれかを呼び出してキャッシュ サイズを制限すると、アプリにエラーが発生する可能性があります。 キャッシュにサイズ制限が設定されている場合、すべてのエントリの追加時にサイズを指定する必要があります。 これにより問題が発生する可能性があります。開発者は、共有キャッシュを何に使用するかを完全に制御できるとは限らないからです。 SetSizeSize、または SizeLimit を使用してキャッシュを制限する場合は、キャッシュ処理のためのキャッシュ シングルトンを作成します。 詳細と例については、「SetSize、Size、SizeLimit を使用してキャッシュ サイズを制限する」を参照してください。 共有キャッシュは、他のフレームワークまたはライブラリによって共有されます。

メモリ内キャッシュは、依存関係の挿入を使用してアプリから参照される "サービス" です。 コンストラクターで IMemoryCache インスタンスを要求します。

public class IndexModel : PageModel
{
    private readonly IMemoryCache _memoryCache;

    public IndexModel(IMemoryCache memoryCache) =>
        _memoryCache = memoryCache;

    // ...

次のコードでは、TryGetValue を使用して、ある日時がキャッシュ内にあるかどうかを確認します。 日時がキャッシュされていない場合は、新しいエントリが作成され、Set を使用してキャッシュに追加されます:

public void OnGet()
{
    CurrentDateTime = DateTime.Now;

    if (!_memoryCache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
    {
        cacheValue = CurrentDateTime;

        var cacheEntryOptions = new MemoryCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        _memoryCache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
    }

    CacheCurrentDateTime = cacheValue;
}

前のコードでは、キャッシュ エントリは 3 秒のスライド式有効期限で構成されています。 キャッシュ エントリに 3 秒以上アクセスしないと、キャッシュから削除されます。 キャッシュ エントリにアクセスするたびに、さらに 3 秒間キャッシュに残ります。 CacheKeys クラスは、ダウンロード サンプルに含まれています。

現在の時刻とキャッシュされた日時が表示されます。

<ul>
    <li>Current Time: @Model.CurrentDateTime</li>
    <li>Cached Time: @Model.CacheCurrentDateTime</li>
</ul>

次のコードでは、Set 拡張メソッドを使用して、MemoryCacheEntryOptions を作成することなく、相対的な時間のデータをキャッシュしています:

_memoryCache.Set(CacheKeys.Entry, DateTime.Now, TimeSpan.FromDays(1));

前のコードでは、キャッシュ エントリは 1 日の相対有効期限で構成されています。 キャッシュ エントリは、このタイムアウト期間内にアクセスされた場合でも、1 日後にキャッシュから削除されます。

次のコードでは、GetOrCreateGetOrCreateAsync を使用して、データをキャッシュしています。

public void OnGetCacheGetOrCreate()
{
    var cachedValue = _memoryCache.GetOrCreate(
        CacheKeys.Entry,
        cacheEntry =>
        {
            cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return DateTime.Now;
        });

    // ...
}

public async Task OnGetCacheGetOrCreateAsync()
{
    var cachedValue = await _memoryCache.GetOrCreateAsync(
        CacheKeys.Entry,
        cacheEntry =>
        {
            cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return Task.FromResult(DateTime.Now);
        });

    // ...
}

次のコードでは、Get を呼び出して、キャッシュされた日時を取得します。

var cacheEntry = _memoryCache.Get<DateTime?>(CacheKeys.Entry);

次のコードは、絶対的な有効期限を持つキャッシュされた項目を取得または作成します。

var cachedValue = _memoryCache.GetOrCreate(
    CacheKeys.Entry,
    cacheEntry =>
    {
        cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });

スライド式有効期限のみが設定されているキャッシュされたアイテムは、古くなる可能性があります。 キャッシュされたアイテムがスライド式有効期限の期間内に繰り返しアクセスされる場合、アイテムの有効期限は切れません。 スライド式有効期限と絶対的な有効期限を組み合わせることで、アイテムは確実に期限切れになります。 絶対的な有効期限は、アイテムをキャッシュできる期間の上限を設定しますが、スライド式有効期限の間隔内に要求されなかった場合、アイテムはより早く有効期限を迎えることができます。 スライド式有効期限の間隔 "または" 絶対的な有効期限が経過すると、項目はキャッシュから削除されます。

次のコードは、スライド式 "および" 絶対的な有効期限の両方を持つキャッシュされた項目を取得または作成します。

var cachedValue = _memoryCache.GetOrCreate(
    CacheKeys.CallbackEntry,
    cacheEntry =>
    {
        cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
        cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });

上記のコードでは、データが絶対時間より長くキャッシュされないことが保証されています。

GetOrCreateGetOrCreateAsyncGet は、CacheExtensions クラスの拡張メソッドです。 これらのメソッドは、IMemoryCache の機能を拡張します。

MemoryCacheEntryOptions

次のような例です。

  • キャッシュの優先順位を CacheItemPriority.NeverRemove に設定します。
  • エントリがキャッシュから削除された後に呼び出される PostEvictionDelegate を設定します。 コールバックは、キャッシュから項目を削除するコードとは異なるスレッドで実行されます。
public void OnGetCacheRegisterPostEvictionCallback()
{
    var memoryCacheEntryOptions = new MemoryCacheEntryOptions()
        .SetPriority(CacheItemPriority.NeverRemove)
        .RegisterPostEvictionCallback(PostEvictionCallback, _memoryCache);

    _memoryCache.Set(CacheKeys.CallbackEntry, DateTime.Now, memoryCacheEntryOptions);
}

private static void PostEvictionCallback(
    object cacheKey, object cacheValue, EvictionReason evictionReason, object state)
{
    var memoryCache = (IMemoryCache)state;

    memoryCache.Set(
        CacheKeys.CallbackMessage,
        $"Entry {cacheKey} was evicted: {evictionReason}.");
}

SetSize、Size、SizeLimit を使用してキャッシュ サイズを制限する

MemoryCache インスタンスで必要に応じてサイズ制限を指定して適用できます。 キャッシュにはエントリのサイズを測定するしくみがないため、キャッシュ サイズ制限には定義済みの測定単位がありません。 キャッシュ サイズ制限が設定されている場合、すべてのエントリでサイズを指定する必要があります。 ASP.NET Core ランタイムでメモリ負荷に基づいてキャッシュ サイズが制限されることはありません。 キャッシュ サイズを制限する決定は開発者が行います。 指定されたサイズは、開発者が選択した単位で示されます。

次に例を示します。

  • Web アプリで主に文字列をキャッシュしている場合は、各キャッシュ エントリ サイズを文字列の長さにすることができます。
  • アプリでは、すべてのエントリのサイズを 1 と指定することができ、そうするとサイズ制限はエントリの数になります。

SizeLimit が設定されていない場合、キャッシュは無制限に拡張されます。 システム メモリが不足している場合に ASP.NET Core ランタイムでキャッシュがトリミングされることはありません。 アプリは次のように設計する必要があります。

  • キャッシュの拡張を制限します。
  • 使用可能なメモリが制限されているときは、Compact または Remove を呼び出します。

次のコードでは、依存関係の挿入によってアクセス可能な、単位のない固定サイズの MemoryCache を作成しています。

public class MyMemoryCache
{
    public MemoryCache Cache { get; } = new MemoryCache(
        new MemoryCacheOptions
        {
            SizeLimit = 1024
        });
}

SizeLimit には単位がありません。 キャッシュ サイズ制限が設定されている場合、キャッシュされたエントリには、最適と考えられる任意の単位でサイズを指定する必要があります。 キャッシュ インスタンスのすべてのユーザーは、同じ単位システムを使用する必要があります。 キャッシュされたエントリのサイズの合計が SizeLimit で指定された値を超えた場合、エントリはキャッシュされません。 キャッシュ サイズ制限が設定されていない場合、エントリに設定されているキャッシュ サイズは無視されます。

次のコードは、MyMemoryCache依存関係の挿入コンテナーに登録します:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddSingleton<MyMemoryCache>();

MyMemoryCache は、このサイズ制限付きキャッシュを認識し、キャッシュ エントリ サイズを適切に設定する方法を理解しているコンポーネントの独立したメモリ キャッシュとして作成されます。

キャッシュ エントリのサイズは、SetSize 拡張メソッドまたは Size プロパティによって設定できます:

if (!_myMemoryCache.Cache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
{
    var cacheEntryOptions = new MemoryCacheEntryOptions()
        .SetSize(1);

    // cacheEntryOptions.Size = 1;

    _myMemoryCache.Cache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
}

前のコードでは、2 つの強調表示された行で、キャッシュ エントリのサイズを設定した結果と同じ結果が得られます。 SetSize は、呼び出しを new MemoryCacheOptions() にチェーンするときの便宜のために提供されています。

MemoryCache.Compact

MemoryCache.Compact により、指定した割合のキャッシュが次の順序で削除されます。

  • 期限切れのすべての項目。
  • 優先順位別の項目。 最も優先順位の低い項目が最初に削除されます。
  • 最も長く使用されていないオブジェクト。
  • 最も古い絶対的な有効期限を持つ項目。
  • 最も古いスライド式有効期限を持つ項目。

優先順位 NeverRemove の固定されたアイテムは決して削除されません。 次のコードは、キャッシュ項目を削除し、Compact を呼び出してキャッシュされたエントリの 25% を削除します:

_myMemoryCache.Cache.Remove(CacheKeys.Entry);
_myMemoryCache.Cache.Compact(.25);

詳細については、「GitHub にある Compact ソース」を参照してください。

キャッシュの依存関係

次のサンプルは、依存エントリの有効期限が切れた場合に、キャッシュ エントリを期限切れにする方法を示しています。 キャッシュされた項目に CancellationChangeToken が追加されます。 CancellationTokenSourceCancel が呼び出されると、両方のキャッシュ エントリが削除されます:

public void OnGetCacheCreateDependent()
{
    var cancellationTokenSource = new CancellationTokenSource();

    _memoryCache.Set(
        CacheKeys.DependentCancellationTokenSource,
        cancellationTokenSource);

    using var parentCacheEntry = _memoryCache.CreateEntry(CacheKeys.Parent);

    parentCacheEntry.Value = DateTime.Now;

    _memoryCache.Set(
        CacheKeys.Child,
        DateTime.Now,
        new CancellationChangeToken(cancellationTokenSource.Token));
}

public void OnGetCacheRemoveDependent()
{
    var cancellationTokenSource = _memoryCache.Get<CancellationTokenSource>(
        CacheKeys.DependentCancellationTokenSource);

    cancellationTokenSource.Cancel();
}

CancellationTokenSource を使用すると、複数のキャッシュ エントリを 1 つのグループとして削除できます。 上記のコードの using のパターンでは、using スコープ内部で作成されたキャッシュ エントリにトリガーと有効期限の設定が継承されます。

補足メモ

  • 有効期限切れはバックグラウンドでは発生しません。 期限切れアイテムをキャッシュでアクティブにスキャンするタイマーはありません。 キャッシュでのあらゆるアクティビティ (GetSetRemove) によって、バックグラウンドでの期限切れ項目のスキャンをトリガーできます。 CancellationTokenSource (CancelAfter) のタイマーも、エントリを削除し、期限切れ項目のスキャンをトリガーします。 次の例では、登録済みトークンに CancellationTokenSource(TimeSpan) を使用します。 このトークンが起動すると、エントリが直ちに削除され、削除コールバックが起動します:

    if (!_memoryCache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
    {
        cacheValue = DateTime.Now;
    
        var cancellationTokenSource = new CancellationTokenSource(
            TimeSpan.FromSeconds(10));
    
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            .AddExpirationToken(
                new CancellationChangeToken(cancellationTokenSource.Token))
            .RegisterPostEvictionCallback((key, value, reason, state) =>
            {
                ((CancellationTokenSource)state).Dispose();
            }, cancellationTokenSource);
    
        _memoryCache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
    }
    
  • コールバックを使用してキャッシュ項目を再設定する場合:

    • コールバックが完了していないために、複数の要求でキャッシュされたキー値が空であることが判明する場合があります。
    • これにより、キャッシュされた項目が複数のスレッドで再設定される可能性があります。
  • あるキャッシュ エントリを使用して別のキャッシュ エントリを作成すると、親エントリの有効期限トークンと時間ベースの有効期限の設定が子にコピーされます。 親エントリを手動で削除または更新しても、子は有効期限切れになりません。

  • キャッシュからキャッシュ エントリが削除された後に呼び出されるコールバックを取得または設定するには、PostEvictionCallbacks を使用します。

  • ほとんどのアプリでは、IMemoryCache が有効にされます。 たとえば、AddMvcAddControllersWithViewsAddRazorPagesAddMvcCore().AddRazorViewEngine、およびその他多くの Add{Service} メソッドを Program.cs で呼び出すと、IMemoryCache が有効になります。 前の Add{Service} メソッドのいずれかを呼び出さないアプリでは、Program.csAddMemoryCache を呼び出すことが必要な場合があります。

バックグラウンドでのキャッシュ更新

IHostedService などのバックグラウンド サービスを使用してキャッシュを更新します。 バックグラウンド サービスを使用すると、エントリを再計算して準備ができた場合にのみキャッシュに割り当てることができます。

その他のリソース

サンプル コードを表示またはダウンロードします (ダウンロード方法)。

キャッシュの基本

キャッシュを使用すると、コンテンツを生成するために必要な作業量を減らすことによって、アプリのパフォーマンスとスケーラビリティを大幅に向上させることができます。 キャッシュは、頻繁に変更され、しかも、生成に負荷がかかるデータに対して最適に機能します。 キャッシュを使用すると、提供元からよりもはるかに高速に返されるデータのコピーが作成されます。 アプリは、キャッシュされたデータに依存しないように作成およびテストする必要があります。

ASP.NET Core は、いくつかの異なるキャッシュをサポートしています。 最も単純なキャッシュは、IMemoryCache に基づいています。 IMemoryCache は、Web サーバーのメモリに格納されているキャッシュを表します。 サーバー ファーム (複数のサーバー) で実行されているアプリでメモリ内キャッシュを使用するときは、セッションがスティッキーであることを確認する必要があります。 スティッキー セッションでは、クライアントからの後続の要求がすべて同じサーバーに送られます。 たとえば、Azure Web アプリの場合は、アプリケーション要求ルーティング処理 (ARR) を使用して、後続のすべての要求を同じサーバーにルーティングします。

Web ファームのスティッキーでないセッションでは、キャッシュ整合性の問題を回避するために分散キャッシュが必要です。 アプリによっては、分散キャッシュの方がメモリ内キャッシュよりも高いスケールアウトをサポートする場合があります。 分散キャッシュを使用すると、キャッシュ メモリから外部プロセスにオフロードされます。

メモリ内キャッシュには、任意のオブジェクトを格納できます。 分散キャッシュ インターフェイスは byte[] に制限されています。 メモリ内および分散キャッシュには、キャッシュ項目がキーと値のペアとして格納されます。

System.Runtime.Caching と MemoryCache

System.Runtime.Caching/MemoryCache (NuGet パッケージ) は、次のものと共に使用できます。

  • .NET Standard 2.0 以降。
  • .NET Standard 2.0 以降を対象とするすべての .NET 実装。 たとえば、ASP.NET Core 3.1 以降。
  • .NET Framework 4.5 以降。

System.Runtime.Caching/MemoryCache よりも Microsoft.Extensions.Caching.Memory/IMemoryCache (この記事で説明しています) をお勧めします。理由は、ASP.NET Core への統合が強化されているためです。 たとえば、IMemoryCache は ASP.NET Core の依存関係の挿入においてネイティブに機能します。

ASP.NET 4.x から ASP.NET Core にコードを移植するときは、System.Runtime.Caching/MemoryCache を互換性ブリッジとして使用します。

キャッシュのガイドライン

  • コードでは常に、データをフェッチするためのフォールバック オプションを使用し、キャッシュされた値が使用可能であることに依存しないようにする必要があります。
  • キャッシュには、貴重なリソースであるメモリが使用されます。 キャッシュの拡張を制限してください。
    • 外部入力をキャッシュ キーとして使用しないでください。
    • キャッシュの拡張を制限するには、有効期限を使用します。
    • SetSize、Size、SizeLimit を使用してキャッシュ サイズを制限します。 ASP.NET Core ランタイムでメモリ負荷に基づいてキャッシュ サイズが制限されることはありません。 キャッシュ サイズを制限する決定は開発者が行います。

IMemoryCache を使用する

警告

依存関係の挿入からの "共有" メモリ キャッシュを使用しながら、SetSizeSizeSizeLimit のいずれかを呼び出してキャッシュ サイズを制限すると、アプリにエラーが発生する可能性があります。 キャッシュにサイズ制限が設定されている場合、すべてのエントリの追加時にサイズを指定する必要があります。 これにより問題が発生する可能性があります。開発者は、共有キャッシュを何に使用するかを完全に制御できるとは限らないからです。 SetSizeSize、または SizeLimit を使用してキャッシュを制限する場合は、キャッシュ処理のためのキャッシュ シングルトンを作成します。 詳細と例については、「SetSize、Size、SizeLimit を使用してキャッシュ サイズを制限する」を参照してください。 共有キャッシュは、他のフレームワークまたはライブラリによって共有されます。

メモリ内キャッシュは、依存関係の挿入を使用してアプリから参照される "サービス" です。 コンストラクターで IMemoryCache インスタンスを要求します。

public class HomeController : Controller
{
    private IMemoryCache _cache;

    public HomeController(IMemoryCache memoryCache)
    {
        _cache = memoryCache;
    }

次のコードでは、TryGetValue を使用して、ある日時がキャッシュ内にあるかどうかを確認します。 日時がキャッシュされていない場合は、新しいエントリが作成され、Set を使用してキャッシュに追加されます。 CacheKeys クラスは、ダウンロード サンプルに含まれています。

public static class CacheKeys
{
    public static string Entry => "_Entry";
    public static string CallbackEntry => "_Callback";
    public static string CallbackMessage => "_CallbackMessage";
    public static string Parent => "_Parent";
    public static string Child => "_Child";
    public static string DependentMessage => "_DependentMessage";
    public static string DependentCTS => "_DependentCTS";
    public static string Ticks => "_Ticks";
    public static string CancelMsg => "_CancelMsg";
    public static string CancelTokenSource => "_CancelTokenSource";
}
public IActionResult CacheTryGetValueSet()
{
    DateTime cacheEntry;

    // Look for cache key.
    if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now;

        // Set cache options.
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            // Keep in cache for this time, reset time if accessed.
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        // Save data in cache.
        _cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
    }

    return View("Cache", cacheEntry);
}

現在の時刻とキャッシュされた日時が表示されます。

@model DateTime?

<div>
    <h2>Actions</h2>
    <ul>
        <li><a asp-controller="Home" asp-action="CacheTryGetValueSet">TryGetValue and Set</a></li>
        <li><a asp-controller="Home" asp-action="CacheGet">Get</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreate">GetOrCreate</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAsynchronous">CacheGetOrCreateAsynchronous</a></li>
        <li><a asp-controller="Home" asp-action="CacheRemove">Remove</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAbs">CacheGetOrCreateAbs</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAbsSliding">CacheGetOrCreateAbsSliding</a></li>

    </ul>
</div>

<h3>Current Time: @DateTime.Now.TimeOfDay.ToString()</h3>
<h3>Cached Time: @(Model == null ? "No cached entry found" : Model.Value.TimeOfDay.ToString())</h3>

次のコードでは、Set 拡張メソッドを使用して、MemoryCacheEntryOptions オブジェクトを作成することなく、相対的な時間を表すデータをキャッシュしています:

public IActionResult SetCacheRelativeExpiration()
{
    DateTime cacheEntry;

    // Look for cache key.
    if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now;

        // Save data in cache and set the relative expiration time to one day
        _cache.Set(CacheKeys.Entry, cacheEntry, TimeSpan.FromDays(1));
    }

    return View("Cache", cacheEntry);
}

キャッシュされた DateTime 値は、タイムアウト期間内に要求がある間、キャッシュに残ります。

次のコードでは、GetOrCreateGetOrCreateAsync を使用して、データをキャッシュしています。

public IActionResult CacheGetOrCreate()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.SlidingExpiration = TimeSpan.FromSeconds(3);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

public async Task<IActionResult> CacheGetOrCreateAsynchronous()
{
    var cacheEntry = await
        _cache.GetOrCreateAsync(CacheKeys.Entry, entry =>
        {
            entry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return Task.FromResult(DateTime.Now);
        });

    return View("Cache", cacheEntry);
}

次のコードでは、Get を呼び出して、キャッシュされた日時を取得します。

public IActionResult CacheGet()
{
    var cacheEntry = _cache.Get<DateTime?>(CacheKeys.Entry);
    return View("Cache", cacheEntry);
}

次のコードは、絶対的な有効期限を持つキャッシュされた項目を取得または作成します。

public IActionResult CacheGetOrCreateAbs()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

スライド式有効期限のみが設定されているキャッシュされたアイテムは、古くなる可能性があります。 キャッシュされたアイテムがスライド式有効期限の期間内に繰り返しアクセスされる場合、アイテムの有効期限は切れません。 スライド式有効期限と絶対的な有効期限を組み合わせることで、アイテムは確実に期限切れになります。 絶対的な有効期限は、アイテムをキャッシュできる期間の上限を設定しますが、スライド式有効期限の間隔内に要求されなかった場合、アイテムはより早く有効期限を迎えることができます。 スライド式有効期限の間隔 "または" 絶対的な有効期限が経過すると、項目はキャッシュから削除されます。

次のコードは、スライド式 "および" 絶対的な有効期限の両方を持つキャッシュされた項目を取得または作成します。

public IActionResult CacheGetOrCreateAbsSliding()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.SetSlidingExpiration(TimeSpan.FromSeconds(3));
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

上記のコードでは、データが絶対時間より長くキャッシュされないことが保証されています。

GetOrCreateGetOrCreateAsyncGet は、CacheExtensions クラスの拡張メソッドです。 これらのメソッドは、IMemoryCache の機能を拡張します。

MemoryCacheEntryOptions

次のサンプルでは、次のことが行われます。

  • スライド式有効期限を設定します。 このキャッシュされた項目にアクセスする要求により、スライド式有効期限をリセットします。
  • キャッシュの優先順位を CacheItemPriority.NeverRemove に設定します。
  • エントリがキャッシュから削除された後に呼び出される PostEvictionDelegate を設定します。 コールバックは、キャッシュから項目を削除するコードとは異なるスレッドで実行されます。
public IActionResult CreateCallbackEntry()
{
    var cacheEntryOptions = new MemoryCacheEntryOptions()
        // Pin to cache.
        .SetPriority(CacheItemPriority.NeverRemove)
        // Add eviction callback
        .RegisterPostEvictionCallback(callback: EvictionCallback, state: this);

    _cache.Set(CacheKeys.CallbackEntry, DateTime.Now, cacheEntryOptions);

    return RedirectToAction("GetCallbackEntry");
}

public IActionResult GetCallbackEntry()
{
    return View("Callback", new CallbackViewModel
    {
        CachedTime = _cache.Get<DateTime?>(CacheKeys.CallbackEntry),
        Message = _cache.Get<string>(CacheKeys.CallbackMessage)
    });
}

public IActionResult RemoveCallbackEntry()
{
    _cache.Remove(CacheKeys.CallbackEntry);
    return RedirectToAction("GetCallbackEntry");
}

private static void EvictionCallback(object key, object value,
    EvictionReason reason, object state)
{
    var message = $"Entry was evicted. Reason: {reason}.";
    ((HomeController)state)._cache.Set(CacheKeys.CallbackMessage, message);
}

SetSize、Size、SizeLimit を使用してキャッシュ サイズを制限する

MemoryCache インスタンスで必要に応じてサイズ制限を指定して適用できます。 キャッシュにはエントリのサイズを測定するしくみがないため、キャッシュ サイズ制限には定義済みの測定単位がありません。 キャッシュ サイズ制限が設定されている場合、すべてのエントリでサイズを指定する必要があります。 ASP.NET Core ランタイムでメモリ負荷に基づいてキャッシュ サイズが制限されることはありません。 キャッシュ サイズを制限する決定は開発者が行います。 指定されたサイズは、開発者が選択した単位で示されます。

次に例を示します。

  • Web アプリで主に文字列をキャッシュしている場合は、各キャッシュ エントリ サイズを文字列の長さにすることができます。
  • アプリでは、すべてのエントリのサイズを 1 と指定することができ、そうするとサイズ制限はエントリの数になります。

SizeLimit が設定されていない場合、キャッシュは無制限に拡張されます。 システム メモリが不足している場合に ASP.NET Core ランタイムでキャッシュがトリミングされることはありません。 アプリは次のように設計する必要があります。

  • キャッシュの拡張を制限します。
  • 使用可能なメモリが制限されているときは、Compact または Remove を呼び出します。

次のコードでは、依存関係の挿入によってアクセス可能な、単位のない固定サイズの MemoryCache を作成しています。

// using Microsoft.Extensions.Caching.Memory;
public class MyMemoryCache 
{
    public MemoryCache Cache { get; private set; }
    public MyMemoryCache()
    {
        Cache = new MemoryCache(new MemoryCacheOptions
        {
            SizeLimit = 1024
        });
    }
}

SizeLimit に単位はありません。 キャッシュ サイズ制限が設定されている場合、キャッシュされたエントリには、最適と考えられる任意の単位でサイズを指定する必要があります。 キャッシュ インスタンスのすべてのユーザーは、同じ単位システムを使用する必要があります。 キャッシュされたエントリのサイズの合計が SizeLimit で指定された値を超えた場合、エントリはキャッシュされません。 キャッシュ サイズ制限が設定されていない場合、エントリに設定されているキャッシュ サイズは無視されます。

次のコードは、MyMemoryCache依存関係の挿入コンテナーに登録します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddSingleton<MyMemoryCache>();
}

MyMemoryCache は、このサイズ制限付きキャッシュを認識し、キャッシュ エントリ サイズを適切に設定する方法を理解しているコンポーネントの独立したメモリ キャッシュとして作成されます。

次のコードでは MyMemoryCache を使用します。

public class SetSize : PageModel
{
    private MemoryCache _cache;
    public static readonly string MyKey = "_MyKey";

    public SetSize(MyMemoryCache memoryCache)
    {
        _cache = memoryCache.Cache;
    }

    [TempData]
    public string DateTime_Now { get; set; }

    public IActionResult OnGet()
    {
        if (!_cache.TryGetValue(MyKey, out string cacheEntry))
        {
            // Key not in cache, so get data.
            cacheEntry = DateTime.Now.TimeOfDay.ToString();

            var cacheEntryOptions = new MemoryCacheEntryOptions()
                // Set cache entry size by extension method.
                .SetSize(1)
                // Keep in cache for this time, reset time if accessed.
                .SetSlidingExpiration(TimeSpan.FromSeconds(3));

            // Set cache entry size via property.
            // cacheEntryOptions.Size = 1;

            // Save data in cache.
            _cache.Set(MyKey, cacheEntry, cacheEntryOptions);
        }

        DateTime_Now = cacheEntry;

        return RedirectToPage("./Index");
    }
}

キャッシュ エントリのサイズは、Size または SetSize 拡張メソッドによって設定できます。

public IActionResult OnGet()
{
    if (!_cache.TryGetValue(MyKey, out string cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now.TimeOfDay.ToString();

        var cacheEntryOptions = new MemoryCacheEntryOptions()
            // Set cache entry size by extension method.
            .SetSize(1)
            // Keep in cache for this time, reset time if accessed.
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        // Set cache entry size via property.
        // cacheEntryOptions.Size = 1;

        // Save data in cache.
        _cache.Set(MyKey, cacheEntry, cacheEntryOptions);
    }

    DateTime_Now = cacheEntry;

    return RedirectToPage("./Index");
}

MemoryCache.Compact

MemoryCache.Compact により、指定した割合のキャッシュが次の順序で削除されます。

  • 期限切れのすべての項目。
  • 優先順位別の項目。 最も優先順位の低い項目が最初に削除されます。
  • 最も長く使用されていないオブジェクト。
  • 最も古い絶対的な有効期限を持つ項目。
  • 最も古いスライド式有効期限を持つ項目。

優先順位 NeverRemove の固定された項目は削除されません。 次のコードでは、キャッシュ項目を削除し、Compact を呼び出しています。

_cache.Remove(MyKey);

// Remove 33% of cached items.
_cache.Compact(.33);   
cache_size = _cache.Count;

詳細については、「GitHub にある Compact ソース」を参照してください。

キャッシュの依存関係

次のサンプルは、依存エントリの有効期限が切れた場合に、キャッシュ エントリを期限切れにする方法を示しています。 キャッシュされた項目に CancellationChangeToken が追加されます。 CancellationTokenSourceCancel が呼び出されると、両方のキャッシュ エントリが削除されます。

public IActionResult CreateDependentEntries()
{
    var cts = new CancellationTokenSource();
    _cache.Set(CacheKeys.DependentCTS, cts);

    using (var entry = _cache.CreateEntry(CacheKeys.Parent))
    {
        // expire this entry if the dependant entry expires.
        entry.Value = DateTime.Now;
        entry.RegisterPostEvictionCallback(DependentEvictionCallback, this);

        _cache.Set(CacheKeys.Child,
            DateTime.Now,
            new CancellationChangeToken(cts.Token));
    }

    return RedirectToAction("GetDependentEntries");
}

public IActionResult GetDependentEntries()
{
    return View("Dependent", new DependentViewModel
    {
        ParentCachedTime = _cache.Get<DateTime?>(CacheKeys.Parent),
        ChildCachedTime = _cache.Get<DateTime?>(CacheKeys.Child),
        Message = _cache.Get<string>(CacheKeys.DependentMessage)
    });
}

public IActionResult RemoveChildEntry()
{
    _cache.Get<CancellationTokenSource>(CacheKeys.DependentCTS).Cancel();
    return RedirectToAction("GetDependentEntries");
}

private static void DependentEvictionCallback(object key, object value,
    EvictionReason reason, object state)
{
    var message = $"Parent entry was evicted. Reason: {reason}.";
    ((HomeController)state)._cache.Set(CacheKeys.DependentMessage, message);
}

CancellationTokenSource を使用すると、複数のキャッシュ エントリを 1 つのグループとして削除できます。 上記のコードの using のパターンでは、using ブロック内部で作成されたキャッシュ エントリにトリガーと有効期限の設定が継承されます。

補足メモ

  • 有効期限切れはバックグラウンドでは発生しません。 期限切れ項目をキャッシュでアクティブにスキャンするタイマーはありません。 キャッシュでのあらゆるアクティビティ (GetSetRemove) によって、バックグラウンドでの期限切れ項目のスキャンをトリガーできます。 CancellationTokenSource (CancelAfter) のタイマーも、エントリを削除し、期限切れ項目のスキャンをトリガーします。 次の例では、登録済みトークンに CancellationTokenSource(TimeSpan) を使用します。 このトークンが起動すると、エントリが直ちに削除され、削除コールバックが起動します。

    public IActionResult CacheAutoExpiringTryGetValueSet()
    {
        DateTime cacheEntry;
    
        if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
        {
            cacheEntry = DateTime.Now;
    
            var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
    
            var cacheEntryOptions = new MemoryCacheEntryOptions()
                .AddExpirationToken(new CancellationChangeToken(cts.Token));
    
            _cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
        }
    
        return View("Cache", cacheEntry);
    }
    
  • コールバックを使用してキャッシュ項目を再設定する場合:

    • コールバックが完了していないために、複数の要求でキャッシュされたキー値が空であることが判明する場合があります。
    • これにより、キャッシュされた項目が複数のスレッドで再設定される可能性があります。
  • あるキャッシュ エントリを使用して別のキャッシュ エントリを作成すると、親エントリの有効期限トークンと時間ベースの有効期限の設定が子にコピーされます。 親エントリを手動で削除または更新しても、子は有効期限切れになりません。

  • キャッシュからキャッシュ エントリが削除された後に呼び出されるコールバックを取得または設定するには、PostEvictionCallbacks を使用します。 このコード例では、CancellationTokenSource.Dispose() を呼び出して、CancellationTokenSource によって使用されるアンマネージ リソースを解放します。 ただし、CancellationTokenSource はキャッシュ エントリによって引き続き使用されているため、すぐには破棄されません。 CancellationTokenMemoryCacheEntryOptions に渡され、一定時間後に有効期限が切れるキャッシュ エントリを作成します。 そのため、キャッシュ エントリが削除されるか有効期限が切れるまで、Dispose を呼び出さないでください。 このコード例では、RegisterPostEvictionCallback メソッドを呼び出して、キャッシュ エントリが削除されたときに呼び出されるコールバックを登録し、そのコールバック内の CancellationTokenSource を破棄します。

  • ほとんどのアプリでは、IMemoryCache が有効にされます。 たとえば、AddMvcAddControllersWithViewsAddRazorPagesAddMvcCore().AddRazorViewEngine、およびその他多くの Add{Service} メソッドを ConfigureServices で呼び出すと、IMemoryCache が有効になります。 上記の Add{Service} メソッドのいずれかを呼び出さないアプリでは、ConfigureServicesAddMemoryCache を呼び出すことが必要な場合があります。

バックグラウンドでのキャッシュ更新

IHostedService などのバックグラウンド サービスを使用してキャッシュを更新します。 バックグラウンド サービスを使用すると、エントリを再計算して準備ができた場合にのみキャッシュに割り当てることができます。

その他のリソース