MemoryCacheのキャッシュは有効期限が過ぎても自動では消してくれない

2022/11/18 デバッグ時のキャッシュの状態を追記

# 初めに
Microsoft.Extensions.Caching.MemoryのMemoryCacheは便利ですが、有効期限が過ぎたキャッシュを自動では消しません。気を付けないとメモリリークの原因になります。

Microsoftの[ドキュメント](https://learn.microsoft.com/ja-jp/aspnet/core/performance/caching/memory)にもさらっと書いてあって見落としがちです。

>有効期限切れはバックグラウンドでは発生しません。
>キャッシュで期限切れのアイテムをアクティブにスキャンするタイマーはありません。
>キャッシュでのあらゆるアクティビティ (Get、Set、Remove) によって、
>バックグラウンドでの期限切れ項目のスキャンをトリガーできます。

そうなのです。Get, Set, Removeを実行した場合、かつ、実行時に指定したキーのキャッシュのみが削除されるのです。

# 試してみる

有効期限が異なる2個のキャッシュを登録し、有効期限切れ後のキャッシュ数を取得してみます。
``` csharp
private static void Test1()
{
    var memoryCache = new MemoryCache(new MemoryCacheOptions());

    // set
    memoryCache.Set("a", "a dummy", new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(1)));
    memoryCache.Set("b", "b dummy", new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(10)));
    Console.WriteLine("set");

    // wait
    Thread.Sleep(15000);
    Console.WriteLine("... wait 15 sec");

    // count
    Console.WriteLine($"count : {memoryCache.Count}");
}
```

→実行後

``` 
set
... wait 15 sec
count : 2
```

→デバッグでキャッシュの状態を確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76153/fa554e45-e1c0-380d-4e2b-380d68e4eab0.png)


有効期限が過ぎてもキャッシュが残っていることが確認できました。

では、

>キャッシュでのあらゆるアクティビティ (Get、Set、Remove) によって、
>バックグラウンドでの期限切れ項目のスキャンをトリガーできます。

についてはどういうことか。
こちらも試してみます。

"a"というキーをGetしてみました。

``` csharp
private static void Test2()
{
    var memoryCache = new MemoryCache(new MemoryCacheOptions());

    // set
    memoryCache.Set("a", "a dummy", new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(1)));
    memoryCache.Set("b", "b dummy", new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(10)));
    Console.WriteLine("set");

    // wait
    Thread.Sleep(15000);
    Console.WriteLine("... wait 15 sec");

    // get
    Console.WriteLine($"get a : {(memoryCache.TryGetValue("a", out string a) ? a : "nothing")}");

    // count
    Console.WriteLine($"count : {memoryCache.Count}");
}
```

→実行後

```
set
... wait 15 sec
get a : nothing
count : 1
```

→デバッグでキャッシュの状態を確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76153/45c315c7-a6d5-08fa-53c1-f37b4d70cdf2.png)


はい。キャッシュの数が1個減りました。
Getしたことで初めて有効期限のチェックが行われ、該当キー(上記の例では"a")のキャッシュが削除されたのです。
一方でGetしなかった"b"のキャッシュは残ったままです。

# 有効期限が過ぎたキャッシュを全て削除するには

Compactメソッドを実行します。
``` memoryCache.Compact(0); ```

``` csharp
private static void Test3()
{
    var memoryCache = new MemoryCache(new MemoryCacheOptions());

    // set
    memoryCache.Set("a", "a dummy", new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(1)));
    memoryCache.Set("b", "b dummy", new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(10)));
    Console.WriteLine("set");

    // wait
    Thread.Sleep(15000);
    Console.WriteLine("... wait 15 sec");

    // count
    Console.WriteLine($"count : {memoryCache.Count}");

    // compact
    memoryCache.Compact(0);
    Console.WriteLine($"compact");

    // count after compact
    Console.WriteLine($"count : {memoryCache.Count}");
}
```

→実行後

```
set
... wait 15 sec
count : 2
compact
count : 0
```

→デバッグでキャッシュの状態を確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76153/930f35ae-42b2-0ebc-4f2b-4ca18e9f909a.png)

Compactメソッド実行後に有効期限が過ぎたキャッシュが全て消えました。

なお、Compactメソッドの引数に圧縮率を指定できますが、有効期限が過ぎたキャッシュのみ削除したい場合は"0"を指定すれば良いと思います。
"0"よりも大きい値を指定すると有効期限内のキャッシュも含めて(Priorityが低い順から)削除されます。

CompactメソッドはIMemoryCacheのインターフェースとして定義されていないため、Dependency Injectionを使用している場合はキャストすれば良いです。

``` csharp
IMemoryCache memoryCache;
:
if (memoryCache is MemoryCache)
{
    ((MemoryCache)memoryCache).Compact(0);
}
```