Repository Pattern & Unit of Work の実装:ASP.NET Core のコードを「スパゲッティ状態」にしないために

Development tutorial - IT technology blog
Development tutorial - IT technology blog

なぜ「古い書き方」を卒業すべきなのか?

ASP.NET Core を使い始めたばかりの頃、私はデータベース処理のロジックをすべて Controller に直接詰め込んでいました。当時は _context.Users.Add(user); await _context.SaveChangesAsync(); と書くだけで済むので、とても速く感じました。しかし、プロジェクトが拡大し、数十ものテーブルを扱うようになると、本当の悪夢が始まりました。

50 個の Controller があり、SQL Server から MongoDB に変更する必要があると想像してみてください。クエリロジックを修正するために一つひとつのファイルを探し回るのは、一週間はかかるでしょう。さらに、Controller が Entity Framework と密結合(tight coupling)しているため、Unit Test を書くことはほぼ不可能です。Repository Pattern と Unit of Work は、まさにこの混乱を解決するために生まれました。

Repository をスマートなデータの「倉庫」と考えてください。データベースに直接要求する代わりに、Repository に問い合わせるだけです。一方、Unit of Work はトランザクション管理の役割を担います。例えば 5 つの連続したデータ保存操作を行う場合、すべてが成功するか、あるいは何も変更されないかのどちらかであることを保証します。これはデータの整合性(ACID)を維持するために非常に重要です。

環境構築とサンプルプロジェクト

まず、ASP.NET Core Web API プロジェクトを準備する必要があります。プロジェクトの作成と Entity Framework Core (EF Core) の設定には慣れていることを前提とします。NuGet を通じて必要なパッケージを素早くインストールしましょう。

dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Tools

次のようなシンプルな Product エンティティを扱います。

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

詳細設定:プロフェッショナルな骨組みの構築

1. Generic Repository の設定

各テーブル(Product、Category など)ごとに個別の Repository を書く代わりに、IGenericRepository を使用することをお勧めします。このアプローチにより、基本的な CRUD 操作におけるボイラープレートコード(定型コード)を 60〜70% 削減できます。

public interface IGenericRepository<T> where T : class
{
    Task<T> GetByIdAsync(int id);
    Task<IEnumerable<T>> GetAllAsync();
    Task AddAsync(T entity);
    void Update(T entity);
    void Delete(T entity);
}

以下は、参考にできる実際の実装(Implementation)です。

public class GenericRepository<T> : IGenericRepository<T> where T : class
{
    protected readonly MyDbContext _context;
    public GenericRepository(MyDbContext context)
    {
        _context = context;
    }

    public async Task<T> GetByIdAsync(int id) => await _context.Set<T>().FindAsync(id);
    public async Task<IEnumerable<T>> GetAllAsync() => await _context.Set<T>().ToListAsync();
    public async Task AddAsync(T entity) => await _context.Set<T>().AddAsync(entity);
    public void Update(T entity) => _context.Set<T>().Update(entity);
    public void Delete(T entity) => _context.Set<T>().Remove(entity);
}

2. Unit of Work の実装

これは Repository を調整する「指揮者」です。例えば、注文処理を行う場合、Product Repository で在庫を減らし、Order Repository で請求書を作成する必要があります。Unit of Work を使うことで、これら両方を単一のトランザクション内で保存できます。

public interface IUnitOfWork : IDisposable
{
    IGenericRepository<Product> Products { get; }
    Task<int> CompleteAsync();
}

public class UnitOfWork : IUnitOfWork
{
    private readonly MyDbContext _context;
    public IGenericRepository<Product> Products { get; private set; }

    public UnitOfWork(MyDbContext context)
    {
        _context = context;
        Products = new GenericRepository<Product>(_context);
    }

    public async Task<int> CompleteAsync() => await _context.SaveChangesAsync();
    public void Dispose() => _context.Dispose();
}

3. Dependency Injection (DI) の登録

Program.cs ファイルを開き、次の 2 行を追加します。このステップにより、必要に応じてシステムが自動的に Service を Controller に「注入」できるようになります。

builder.Services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>));
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();

ワークフローのテストと最適化

これで、Controller は非常にスッキリします。DbContext がどのように動作するかを気にする必要はなくなり、ビジネスロジックにのみ集中できるようになります。

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IUnitOfWork _unitOfWork;
    public ProductsController(IUnitOfWork unitOfWork) => _unitOfWork = unitOfWork;

    [HttpGet]
    public async Task<IActionResult> GetAll() 
    {
        var products = await _unitOfWork.Products.GetAllAsync();
        return Ok(products);
    }
}

API 開発において、返される JSON のデバッグは頻繁に行う作業です重い拡張機能をインストールする代わりに、私はよく toolcraft.app(例えば JSON Formatter ツールなど)を使用します。ブラウザ上で素早くデータを整形し、ネストされた構造を効率的に確認できます。

なぜこの方法が Unit Test の「救世主」なのか?

ProductsController のテストを書く際、実際のデータベースをセットアップしたり、In-Memory DB を使用したりする必要はありません。Moq ライブラリを使用して IUnitOfWork インターフェースを Mock 化するだけです。わずか数行のコードで GetAllAsync() から返されるデータをシミュレートできます。これにより、テストの実行速度が何倍も速くなります。

var mockUow = new Mock<IUnitOfWork>();
mockUow.Setup(u => u.Products.GetAllAsync()).ReturnsAsync(new List<Product>());
var controller = new ProductsController(mockUow.Object);

実務におけるいくつかの注意点

すべてのプロジェクトに Repository Pattern を機械的に適用しないでください。1〜2 個のテーブルしかない小規模なアプリの場合、インターフェースを追加することはコードを複雑にするだけです。しかし、エンタープライズ(Enterprise)プロジェクトや、本格的な Unit Test を書く必要がある場合には、これが最良の選択肢となります。

IncludeThenInclude を使用する複雑なクエリに遭遇した場合は、Generic Repository に無理に詰め込まないでください。Generic を継承した個別の Repository(例:ProductRepository)を作成して、特有のロジックを処理するようにしましょう。

この記事が、プロフェッショナルで管理しやすい ASP.NET Core のコード設計に自信を持つ助けになれば幸いです。

Share: