ASP.NET Core MVC で軽量 DDD

1人でフロントエンドとバックエンドの両方を開発していたら、DDD と軽量 DDD の区別が付かないような状況になってきた。個人的には軽量 DDD 上等。ただ、アーキテクチャに関して言えば、クリーンアーキテクチャはやり過ぎ感あるので、シンプルなレイヤーアーキテクチャを好んで採用している。

実践 DDD で紹介されているようなレイヤーアーキテクチャだと、アプリケーション層にあたるのがアプリケーションサービスで、プレゼンテーション層の Controller から呼び出すことになる。このアプリケーションサービス、責任多くなりがち。仮に1メソッドが1ユースケースだとしても、アプリケーションサービスにユースケースを詰め込む形になって、自分の中でしっくりこなかった。

そこで試行錯誤してたどり着いたのが、ユースケースをクラスとして切り出すやり方。1 ユースケース 1 クラス 1 メソッド 。これも、単一責任の原則と言えなくもない、かも。

public class RegisterProductUseCase
{
    public async ValueTask<RegisterProductResult> ExecuteAsync(
        RegisterProductCommand command)
    {
        // いろいろやるけど紙面の都合で省略
 
        return new RegisterProductResult.Success
        {
            Product = product, // いろいろやって登録できた製品をセット
        };
    }
}

public abstract class RegisterProductResult
{
    private RegisterProductResult() { }

    public sealed class Success : RegisterProductResult
    {
        public Product Product { get; init; }
    }

    public sealed class Error : RegisterProductResult
    {
        // 紙面の都合で省略
    }
}

ASP.NET Core MVC なら、DI コンテナに登録したユースケースを、FromServices で引数として受け取ることができる。コンストラクタインジェクションだと、コンストラクタの引数が増えがちだったけど、必要としているメソッドの引数で受け取ればスッキリ。

[Route("/products")]
[ApiController]
[Authorize]
public class ProductsController : ControllerBase
{
    [HttpPost]
    public async Task<ActionResult<Product>> RegisterProduct(
        [FromBody] ProductInputModel model,
        [FromServices] RegisterProductUseCase useCase)
    {
        var result = await useCase.ExecuteAsync(new RegisterProductCommand
        {
            // init でいろいろ詰める
        });

        return result switch
        {
            RegisterProductResult.Success success => success.Product;
            _ => BadRequest(ModelState); // 紙面の都合で手抜き
        };
    }
}

ASP.NET Core MVC を使って、ちゃんとした Web API を実装するとき、最近はこのやり方に落ち着いた。戻り値用の型を定義するのだけはいつも面倒で、TypeScript の Union 型が C# にも欲しいと、しょっちゅう思う。