汎用的なリポジトリパターンをライブラリにしました

## はじめに

汎用的かつ柔軟なリポジトリパターンを提供するインターフェース群をライブラリにしました。次の要件を実現することを目的としています。

* TransactionScope による暗黙のトランザクションと明示的なトランザクションの両方をサポートする
* ADO\.NET に依存しない
* エンティティおよびキーの型に対する制約を最小限に留める

## 公開先

https://github.com/mxProject/DataRepository

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

## 基本形のリポジトリ

### IReadDataRepository&lt;TEntity, TKey&gt; インターフェース

指定されたキーに対応するエンティティを取得するリポジトリ

```c#
bool UseTransactionScope { get; }
TEntity? Get(TKey key);
IEnumerable<TEntity> GetRange(IEnumerable<TKey> keys);
IEnumerable<TEntity> GetAll();
IEnumerable<TKey> GetAllKeys();
```

```c#
internal class SampleEntity
{
    // key
    internal int ID { get; set; }
    internal string? Name { get; set; }
}

internal class SampleReadRepository : IReadDataRepository<SampleEntity, int>
{
    // omission
}

// create a repository.
var repo = new SampleReadRepository();

// get the entity corresponding to the specified id.
var entity = repo.Get(1);

// get the entities corresponding to the specified ids.
var entities = repo.GetRange(new[] { 1, 2, 3 });
```

### IReadDataRepositoryWithUniqueKey&lt;TEntity, TPrimaryKey, TUniqueKey&gt; インターフェース

指定された主キーまたは一意キーに対応するエンティティを取得するリポジトリ

```c#
bool UseTransactionScope { get; }
TEntity? GetByPrimaryKey(TPrimaryKey primaryKey);
TEntity? GetByUniqueKey(TUniqueKey uniqueKey);
IEnumerable<TEntity> GetRangeByPrimaryKey(IEnumerable<TPrimaryKey> primaryKeys);
IEnumerable<TEntity> GetRangeByUniqueKey(IEnumerable<TUniqueKey> uniqueKeys);
IEnumerable<TEntity> GetAll();
IEnumerable<TPrimaryKey> GetAllPrimaryKeys();
IEnumerable<TUniqueKey> GetAllUniqueKeys();
```

```c#
internal class SampleEntity
{
    // primary key
    internal int ID { get; set; }
    // unique key
    internal string Code { get; set; }
    internal string? Name { get; set; }
}

internal class SampleReadRepository : IReadDataRepositoryWithUniqueKey<SampleEntity, int, string>
{
    // omission
}

// create a repository.
var repo = new SampleReadRepository();

// get the entity corresponding to the specified id.
var entity = repo.GetByPrimaryKey(1);

// get the entities corresponding to the specified codes.
var entities = repo.GetRangeByUniqueKey(new[] { "001", "002", "003" });
```

### IWriteDataRepository&lt;TEntity&gt; インターフェース

指定されたエンティティを更新するリポジトリ

```c#
bool UseTransactionScope { get; }
int Insert(TEntity entity);
int InsertRange(IEnumerable<TEntity> entities);
int Update(TEntity entity);
int UpdateRange(IEnumerable<TEntity> entities);
int Delete(TEntity entity);
int DeleteRange(IEnumerable<TEntity> entities);
```

```c#
internal class SampleEntity
{
    internal int ID { get; set; }
    internal string? Name { get; set; }
}

internal class SampleReadRepository : IWriteDataRepository<SampleEntity>
{
    public bool UseTransactionScope => true;
    
    // omission
}

using (TransactionScope scope = new())
{
    // create a repository.
    var repo = new SampleReadRepository();

    // insert the specified entity.
    repo.Insert(new SampleEntity() { ID = 1, Name = "item1" });

    // insert the specified entities.
    repo.InsertRange(new[] {
        new SampleEntity() { ID = 2, Name = "item2" },
        new SampleEntity() { ID = 3, Name = "item3" }
    });

    scope.Complete();
}
```

TransactionScope を利用しない場合、トランザクションオブジェクトを引き渡すことになります。後述するコンテキストを用いるのがその方法の一つです。

### IDataQuery&lt;TEntity, TCondition&gt; インターフェース

指定された条件に合致するエンティティを取得するリポジトリ

```c#
IEnumerable<TEntity> Query(TCondition condition, int skipCount = 0, int? maximumCount = null);
int GetCount(TCondition condition);
```

```c#
internal class SampleEntity
{
    internal int ID { get; set; }
    internal string Code { get; set; }
    internal string? Name { get; set; }
}

internal class SampleEntityCondition
{
    internal string? MinimumCode { get; set; }
    internal string? MaximumCode { get; set; }
    internal string? NamePattern { get; set; }
}

internal class SampleEntityQuery : IDataQuery<SampleEntity, SampleEntityCondition>
{
    // omission
}

// creates a repository.
var repo = new SampleEntityQuery();

// creates a condition.
var condition = new SampleEntityCondition()
{
    MinimumCode = "001",
    MaximumCode = "100"
};

// find entities that match the specified condition.
var entities = repo.Query(condition);

// get the first 10 entities.
var top10 = repo.Query(condition, maximumCount: 10);

// skip the first 10 entities and get the next 10 entities.
var next10 = repo.Query(condition, skipCount: 10, maximumCount: 10);
```

## 任意の型のコンテキストを使用するリポジトリ

基本形のリポジトリとの違いは、各メソッドの引数にコンテキストが追加されている点です。データベースコネクションやトランザクションなど、エンティティの取得と更新に必要なオブジェクトを引き渡す設計に適しています。実装イメージは基本形のリポジトリとほぼ同じですので割愛します。

### IReadDataRepositoryWithContext&lt;TEntity, TKey, TContext&gt; インターフェース

指定されたキーに対応するエンティティを取得するリポジトリ

```c#
bool UseTransactionScope { get; }
TEntity? GetByPrimaryKey(TPrimaryKey primaryKey, TContext context);
TEntity? GetByUniqueKey(TUniqueKey uniqueKey, TContext context);
IEnumerable<TEntity> GetRangeByPrimaryKey(IEnumerable<TPrimaryKey> primaryKeys, TContext context);
IEnumerable<TEntity> GetRangeByUniqueKey(IEnumerable<TUniqueKey> uniqueKeys, TContext context);
IEnumerable<TEntity> GetAll(TContext context);
IEnumerable<TPrimaryKey> GetAllPrimaryKeys(TContext context);
IEnumerable<TUniqueKey> GetAllUniqueKeys(TContext context);
```

### IReadDataRepositoryWithUniqueKeyWithContext&lt;TEntity, TPrimaryKey, TUniqueKey, TContext&gt; インターフェース

指定された主キーまたは一意キーに対応するエンティティを取得するリポジトリ

```c#
bool UseTransactionScope { get; }
TEntity? GetByPrimaryKey(TPrimaryKey primaryKey, TContext context);
TEntity? GetByUniqueKey(TUniqueKey uniqueKey, TContext context);
IEnumerable<TEntity> GetRangeByPrimaryKey(IEnumerable<TPrimaryKey> primaryKeys, TContext context);
IEnumerable<TEntity> GetRangeByUniqueKey(IEnumerable<TUniqueKey> uniqueKeys, TContext context);
IEnumerable<TEntity> GetAll(TContext context);
IEnumerable<TPrimaryKey> GetAllPrimaryKeys(TContext context);
IEnumerable<TUniqueKey> GetAllUniqueKeys(TContext context);
```

### IWriteDataRepositoryWithContext&lt;TEntity, TContext&gt; インターフェース

指定されたエンティティを更新するリポジトリ

```c#
bool UseTransactionScope { get; }
int Insert(TEntity entity, TContext context);
int InsertRange(IEnumerable<TEntity> entities, TContext context);
int Update(TEntity entity, TContext context);
int UpdateRange(IEnumerable<TEntity> entities, TContext context);
int Delete(TEntity entity, TContext context);
int DeleteRange(IEnumerable<TEntity> entities, TContext context);
```

### IDataQueryWithContext&lt;TEntity, TCondition, TContext&gt; インターフェース

指定された条件に合致するエンティティを取得するリポジトリ  

```c#
bool UseTransactionScope { get; }
IEnumerable<TEntity> Query(TCondition condition, TContext context, int skipCount = 0, int? maximumCount = null);
int GetCount(TCondition condition, TContext context);
```

## 非同期リポジトリ

基本形のリポジトリとの違いは、各メソッドが非同期メソッドである点です。実装イメージは基本形のリポジトリとほぼ同じですので割愛します。

### IAsyncReadDataRepository&lt;TEntity, TKey&gt; インターフェース

指定されたキーに対応するエンティティを取得するリポジトリ

```c#
bool UseTransactionScope { get; }
ValueTask<TEntity?> GetAsync(TKey key);
IAsyncEnumerable<TEntity> GetRangeAsync(IEnumerable<TKey> keys, CancellationToken cancellationToken = default);
IAsyncEnumerable<TEntity> GetRangeAsync(IAsyncEnumerable<TKey> keys, CancellationToken cancellationToken = default);
IAsyncEnumerable<TEntity> GetAllAsync(CancellationToken cancellationToken = default);
IAsyncEnumerable<TKey> GetAllKeysAsync(CancellationToken cancellationToken = default);
```

### IAsyncReadDataRepositoryWithUniqueKey&lt;TEntity, TPrimaryKey, TUniqueKey&gt; インターフェース

指定された主キーまたは一意キーに対応するエンティティを取得するリポジトリ

```c#
bool UseTransactionScope { get; }
ValueTask<TEntity?> GetByPrimaryKeyAsync(TPrimaryKey primaryKey);
ValueTask<TEntity?> GetByUniqueKeyAsync(TUniqueKey uniqueKey);
IAsyncEnumerable<TEntity> GetRangeByPrimaryKeyAsync(IEnumerable<TPrimaryKey> primaryKeys, CancellationToken cancellationToken = default);
IAsyncEnumerable<TEntity> GetRangeByPrimaryKeyAsync(IAsyncEnumerable<TPrimaryKey> primaryKeys, CancellationToken cancellationToken = default);
IAsyncEnumerable<TEntity> GetRangeByUniqueKeyAsync(IEnumerable<TUniqueKey> uniqueKeys, CancellationToken cancellationToken = default);
IAsyncEnumerable<TEntity> GetRangeByUniqueKeyAsync(IAsyncEnumerable<TUniqueKey> uniqueKeys, CancellationToken cancellationToken = default);
IAsyncEnumerable<TEntity> GetAllAsync(CancellationToken cancellationToken = default);
IAsyncEnumerable<TPrimaryKey> GetAllPrimaryKeysAsync(CancellationToken cancellationToken = default);
IAsyncEnumerable<TUniqueKey> GetAllUniqueKeysAsync(CancellationToken cancellationToken = default);
```

### IAsyncWriteDataRepository&lt;TEntity&gt; インターフェース

指定されたエンティティを更新するリポジトリ

```c#
bool UseTransactionScope { get; }
ValueTask<int> InsertAsync(TEntity entity);
ValueTask<int> InsertRangeAsync(IEnumerable<TEntity> entities, CancellationToken cancellationToken = default);
ValueTask<int> InsertRangeAsync(IAsyncEnumerable<TEntity> entities, CancellationToken cancellationToken = default);
ValueTask<int> UpdateAsync(TEntity entity);
ValueTask<int> UpdateRangeAsync(IEnumerable<TEntity> entities, CancellationToken cancellationToken = default);
ValueTask<int> UpdateRangeAsync(IAsyncEnumerable<TEntity> entities, CancellationToken cancellationToken = default);
ValueTask<int> DeleteAsync(TEntity entity);
ValueTask<int> DeleteRangeAsync(IEnumerable<TEntity> entities, CancellationToken cancellationToken = default);
ValueTask<int> DeleteRangeAsync(IAsyncEnumerable<TEntity> entities, CancellationToken cancellationToken = default);
```

### IAsyncDataQuery&lt;TEntity, TCondition&gt; インターフェース

指定された条件に合致するエンティティを取得するリポジトリ

```c#
bool UseTransactionScope { get; }
IAsyncEnumerable<TEntity> QueryAsync(TCondition condition, int skipCount = 0, int? maximumCount = null, CancellationToken cancellationToken = default);
ValueTask<int> GetCountAsync(TCondition condition);
```

## 任意の型のコンテキストを使用する非同期リポジトリ

基本形のリポジトリとの違いは、各メソッドが非同期メソッドであり、引数にコンテキストが追加されている点です。データベースコネクションやトランザクションなど、エンティティの取得と更新に必要なオブジェクトを引き渡す設計に適しています。実装イメージは基本形のリポジトリとほぼ同じですので割愛します。

### IAsyncReadDataRepositoryWithContext&lt;TEntity, TKey, TContext&gt; インターフェース

指定されたキーに対応するエンティティを取得するリポジトリ

```c#
bool UseTransactionScope { get; }
ValueTask<TEntity?> GetAsync(TKey key, TContext context);
IAsyncEnumerable<TEntity> GetRangeAsync(IEnumerable<TKey> keys, TContext context, CancellationToken cancellationToken = default);
IAsyncEnumerable<TEntity> GetRangeAsync(IAsyncEnumerable<TKey> keys, TContext context, CancellationToken cancellationToken = default);
IAsyncEnumerable<TEntity> GetAllAsync(TContext context, CancellationToken cancellationToken = default);
IAsyncEnumerable<TKey> GetAllKeysAsync(TContext context, CancellationToken cancellationToken = default);
```

### IAsyncReadDataRepositoryWithUniqueKeyWithContext&lt;TEntity, TPrimaryKey, TUniqueKey, TContext&gt; インターフェース

指定された主キーまたは一意キーに対応するエンティティを取得するリポジトリ

```c#
bool UseTransactionScope { get; }
ValueTask<TEntity?> GetByPrimaryKeyAsync(TPrimaryKey primaryKey, TContext context);
ValueTask<TEntity?> GetByUniqueKeyAsync(TUniqueKey uniqueKey, TContext context);
IAsyncEnumerable<TEntity> GetRangeByPrimaryKeyAsync(IEnumerable<TPrimaryKey> primaryKeys, TContext context, CancellationToken cancellationToken = default);
IAsyncEnumerable<TEntity> GetRangeByPrimaryKeyAsync(IAsyncEnumerable<TPrimaryKey> primaryKeys, TContext context, CancellationToken cancellationToken = default);
IAsyncEnumerable<TEntity> GetRangeByUniqueKeyAsync(IEnumerable<TUniqueKey> uniqueKeys, TContext context, CancellationToken cancellationToken = default);
IAsyncEnumerable<TEntity> GetRangeByUniqueKeyAsync(IAsyncEnumerable<TUniqueKey> uniqueKeys, TContext context, CancellationToken cancellationToken = default);
IAsyncEnumerable<TEntity> GetAllAsync(TContext context, CancellationToken cancellationToken = default);
IAsyncEnumerable<TPrimaryKey> GetAllPrimaryKeysAsync(TContext context, CancellationToken cancellationToken = default);
IAsyncEnumerable<TUniqueKey> GetAllUniqueKeysAsync(TContext context, CancellationToken cancellationToken = default);
```

### IAsyncWriteDataRepositoryWithContext&lt;TEntity, TContext&gt; インターフェース

指定されたエンティティを更新するリポジトリ

```c#
bool UseTransactionScope { get; }
ValueTask<int> InsertAsync(TEntity entity, TContext context);
ValueTask<int> InsertRangeAsync(IEnumerable<TEntity> entities, TContext context, CancellationToken cancellationToken = default);
ValueTask<int> InsertRangeAsync(IAsyncEnumerable<TEntity> entities, TContext context, CancellationToken cancellationToken = default);
ValueTask<int> UpdateAsync(TEntity entity, TContext context);
ValueTask<int> UpdateRangeAsync(IEnumerable<TEntity> entities, TContext context, CancellationToken cancellationToken = default);
ValueTask<int> UpdateRangeAsync(IAsyncEnumerable<TEntity> entities, TContext context, CancellationToken cancellationToken = default);
ValueTask<int> DeleteAsync(TEntity entity, TContext context);
ValueTask<int> DeleteRangeAsync(IEnumerable<TEntity> entities, TContext context, CancellationToken cancellationToken = default);
ValueTask<int> DeleteRangeAsync(IAsyncEnumerable<TEntity> entities, TContext context, CancellationToken cancellationToken = default);
```

### IAsyncDataQueryWithContext&lt;TEntity, TCondition, TContext&gt; インターフェース

指定された条件に合致するエンティティを取得するリポジトリ

```c#
bool UseTransactionScope { get; }
IAsyncEnumerable<TEntity> QueryAsync(TCondition condition, TContext context, int skipCount = 0, int? maximumCount = null, CancellationToken cancellationToken = default);
ValueTask<int> GetCountAsync(TCondition condition, TContext context);
```

## おわりに

これらのインターフェースを実装したデータベース用リポジトリとファイルシステム用リポジトリを開発しています。後日紹介したいと考えています。

---

2025/03/28 投稿しました。

https://qiita.com/mxProject/items/2de1ca32db89995179fa

2025/04/03 投稿しました。

https://qiita.com/mxProject/items/8474484096193030177a

2025/04/10 投稿しました。

https://qiita.com/mxProject/items/6319b41a5ecae5db3826