リポジトリパターンにキャッシュ機能を付加する

## はじめに

[前々回](https://qiita.com/mxProject/items/ad81cb580f758f2530a9)の記事で紹介したリポジトリパターンライブラリにキャッシュ機能を追加しました。ConcurrentDictionary などをキャッシュとして用いて、リポジトリに読み取りキャッシュ機能を付加します。

## 公開先

https://github.com/mxProject/DataRepository

https://www.nuget.org/packages/mxProject.Data.Repository.Caching/

## サンプル

```c#
// create repositories and add caching functionality.
using var writeRepo = new SampleWriteRepository().WithCache(x => x.ID);
using var readRepo = new SampleReadRepository().WithCache(x => x.ID);

// insert a entity with ID 1.
var entity1 = new SampleEntity() { ID = 1, Code = "001", Name = "entity1" };
writeRepo.Insert(entity1);

// get the entity with ID 1.
// The created instance is cached.
var found1 = readRepo.Get(1);

// get the entity with ID 1 again.
// The cached instance will be returned.
var found2 = readRepo.Get(1);
```

## シーケンス図

### キャッシュされていない場合

キーに対応するエンティティがリポジトリから取得されます。

```mermaid
sequenceDiagram
    participant app as callsite
    participant wrapper as read-repo<br>(wrapper)
    participant cache as cache
    participant repo as read-repo
    app ->> +wrapper : get entity
    wrapper ->> cache : get entity
    cache --x wrapper : not found
    wrapper ->> +repo : get entity
    repo -->> -wrapper : entity
    wrapper -->> cache : store entity
    wrapper -->> -app : entity
```

### キャッシュされている場合

キーに対応するエンティティがキャッシュされている場合、そのエンティティが返されます。リポジトリにはアクセスしません。

```mermaid
sequenceDiagram
    participant app as callsite
    participant wrapper as read-repo<br>(wrapper)
    participant cache as cache
    participant repo as read-repo
    app ->> +wrapper : get entity
    wrapper ->> +cache : get entity
    cache -->> -wrapper : entity
    wrapper -->> -app : entity
```

### エンティティが更新された場合

更新されたエンティティがキャッシュに存在する場合、そのキーとエンティティをキャッシュから削除します。更新されたことの通知には [MessagePipe](https://github.com/Cysharp/MessagePipe) を利用しています。

```mermaid
sequenceDiagram
    participant app as callsite
    participant writeWrap as write-repo<br>(wrapper)
    participant writeRepo as write-repo
    participant pipe as MessagePipe
    participant wrapper as read-repo<br>(wrapper)
    participant cache as cache
    app ->> +writeWrap : update entity
    writeWrap ->> +writeRepo : update entity
    writeRepo -->> -writeWrap : result
    writeWrap -->> pipe : publish entitychanged
    writeWrap -->> -app : result
    pipe -->> wrapper : subscribe<br>entitychanged
    wrapper -->> cache : remove entity
```

## 利用方法

以下のリポジトリインターフェースに対する拡張メソッドとして実装しています。

* IReadDataRepository&lt;TEntity, TKey&gt;
* IReadDataRepositoryWithContext&lt;TEntity, TKey, TContext&gt;
* IReadDataRepositoryWithUniqueKey&lt;TEntity, TPrimaryKey, TUniqueKey&gt;
* IReadDataRepositoryWithUniqueKeyWithContext&lt;TEntity, TPrimaryKey, TUniqueKey, TContext&gt;
* IAsyncReadDataRepository&lt;TEntity, TKey&gt;
* IAsyncReadDataRepositoryWithContext&lt;TEntity, TKey, TContext&gt;
* IAsyncReadDataRepositoryWithUniqueKey&lt;TEntity, TPrimaryKey, TUniqueKey&gt;
* IAsyncReadDataRepositoryWithUniqueKeyWithContext&lt;TEntity, TPrimaryKey, TUniqueKey, TContext&gt;

次のコードは IReadDataRepository&lt;TEntity, TKey&gt; インターフェースに対する拡張メソッドの一覧です。他のインターフェースも同じ考え方です。

* IEntityCache&lt;TKey, TEntity&gt; インターフェースまたは IDictionary&lt;TKey, TEntity&gt; インターフェースを実装している型のインスタンスをキャッシュとして指定できます。リポジトリが dispose されるときにキャッシュを dispose するかどうかを指定します。

* エンティティの型が IHasKey&lt;TKey&gt; インターフェースを実装していない場合、エンティティからキーを取得するメソッドを指定します。

```c#
// A ConcurrentDictionary<TKey, TEntity> is generated internally.
IReadDataRepository<TEntity, TKey> repo = CreateRepository();
repo = repo.WithCache();

// specify a cache that implements the IEntityCache<TKey, TEntity> interface.
IReadDataRepository<TEntity, TKey> repo = CreateRepository();
IEntityCache<TKey, TEntity> cache = CreateCache();
bool disposableCache = true;
repo = repo.WithCache(cache, disposableCache);

// specify a cache that implements the IDictionary<TKey, TEntity> interface.
IReadDataRepository<TEntity, TKey> repo = CreateRepository();
IDictionary<TKey, TEntity> cache = CreateCache();
bool disposableCache = true;
repo = repo.WithCache(cache, disposableCache);

// If TEntity does not implement the IHasKey<TKey> interface, specify a method to get the key.

// A ConcurrentDictionary<TKey, TEntity> is generated internally.
IReadDataRepository<TEntity, TKey> repo = CreateRepository();
Func<TEntity, TKey> keyGetter = x => x.ID;
repo = repo.WithCache(keyGetter);

// specify a cache that implements the IEntityCache<TKey, TEntity> interface.
IReadDataRepository<TEntity, TKey> repo = CreateRepository();
Func<TEntity, TKey> keyGetter = x => x.ID;
IEntityCache<TKey, TEntity> cache = CreateCache();
bool disposableCache = true;
repo = repo.WithCache(keyGetter, cache, disposableCache);

// specify a cache that implements the IDictionary<TKey, TEntity> interface.
IReadDataRepository<TEntity, TKey> repo = CreateRepository();
Func<TEntity, TKey> keyGetter = x => x.ID;
IDictionary<TKey, TEntity> cache = CreateCache();
bool disposableCache = true;
repo = repo.WithCache(keyGetter, cache, disposableCache);
```

更新系リポジトリに対しても拡張メソッドを実装しています。エンティティが更新されたことをキャッシュに通知する機能を付加します。書き込みキャッシュ機能を付加するわけではありません。

* IWriteDataRepository&lt;TEntity&gt;
* IWriteDataRepositoryWithContext&lt;TEntity, TContext&gt;
* IAsyncWriteDataRepository&lt;TEntity&gt;
* IAsyncWriteDataRepositoryWithContext&lt;TEntity, TContext&gt;

次のコードは IWriteDataRepository&lt;TEntity&gt; インターフェースに対する拡張メソッドの一覧です。他のインターフェースも同じ考え方です。

```c#
IWriteDataRepository<TEntity> repo = CreateRepository();
Func<TEntity, TKey> keyGetter = x => x.ID;
repo = repo.WithCache(keyGetter);
```

## おわりに

複数の利用者によって頻繁に更新されるエンティティに対してはこのキャッシュ機能は適していませんが、これで十分なユースケースもあると思います。
インターフェースを用いるとこのような後付け機能が追加しやすくなります。