実務Webアプリ開発編です。前回まででアーキテクチャ概論として以下を学びました。

  • クリーンアーキテクチャ・ドメイン駆動設計(DDD)の考え方
  • C#ソリューション・プロジェクト構成によるアーキテクチャの実現方法

今回はその続きとして、アプリを実際に動かしたとき、処理がどう流れるかをVisual Studioのデバッガで追っていきます。

以下のような方に役立つ内容となっています。

  • クリーンアーキテクチャの4層構成は分かってきたが、実際の処理がどこからどこへ流れるのかまだ頭の中でつながっていない
  • コードを静的に読むだけでは全体像が掴みにくく、どこから読めばよいか迷う
  • DI コンテナが何をしているのか、実務アプリの文脈でざっくり把握したい

以下のようなMentorAppを題材として進めます。

GitHubにドキュメント・コードの一式があります。

プロ太

ソリューション・プロジェクト構成やソースコードは、アプリの「静的」な側面です。

今回はVisual Studioのデバッガも活用し、アプリの「動的」な側面からクリーンアーキテクチャ・DDDの理解を深めましょう!

アーキテクチャ概論の以下の記事を先に見ておくと理解がより深まります。

【C#/Blazor】実務Webアプリ開発編 (15)クリーンアーキテクチャ入門 ~4層構成の役割と依存方向のルール~ 実務Webアプリ開発編です。前回まではPart II(要件定義と設計)で、MentorApp の要件定義と設計を通して、何を作るかを整...
【C#/Blazor】実務Webアプリ開発編 (16)ドメイン駆動設計(DDD)入門:値オブジェクト・エンティティ・集約・ドメインサービス 実務Webアプリ開発編です。前回は Part III(アーキテクチャ概論)の最初の記事として、クリーンアーキテクチャの全体像を整理しま...
【C#/Blazor】実務Webアプリ開発編 (17)C#のソリューション・プロジェクト構成 ~.slnx と .csproj で設計の意図を読む~ 実務Webアプリ開発編です。前回までPart III(アーキテクチャ概論)として、クリーンアーキテクチャ・ドメイン駆動設計(DDD)の...

デバッガで「1ユースケースを追う」アプローチ

前回(17回)では、4つのプロジェクトへの分割や依存方向のルールを見ました。これは静的な構成の話でした。

ただ、コードを静的に読んでいるだけでは、実行時に処理がどこからどこへ渡っていくかは掴みにくいものです。

ファイルが複数の層に分かれているほど、「このメソッドが呼ばれた後、次はどこへ行くのか」が追いにくくなります。

こういうときに役立つのが、1ユースケースをデバッガでステップ実行しながら追うアプローチです。

特定の画面操作を実行し、処理が進むたびにどのクラスのどのメソッドが呼ばれているかを確かめていきます。

変数の値も確認しながら追えるので、コードの動きが具体的に見えてきます。

これはクリーンアーキテクチャに限らず、見慣れないアプリを読むときに役立つ汎用的なアプローチです。

まず1本の処理を最初から最後まで追うことで、アプリ全体の構造が見えてきます。

Visual Studio のデバッガの基本的な操作方法は以下の記事も参考にしてください。

C#入門編 Visual Studio 2022 デバッガの基本① ~ブレークポイントとステップ実行~【初心者でも効率的なデバッグ!】 Visual Studio編です。今回はプログラミングにおける強力な武器となる「デバッガ」について解説します。 Visual S...
C#入門編 Visual Studio 2022 デバッガの基本② ~デバッグで変数を確認する方法~【変数スコープ、ウォッチウィンドウ、条件付きブレークポイント】 Visual Studio編です。今回は前回に引き続き、プログラミングの強力なツールである「デバッガ」について解説します。 前回...
プロ美

デバッガで1ステップずつ進めると、どの層のどのコードが動いているか、その場で確認できるんだね。

プロ太

そうです。1回追うだけで、クリーンアーキテクチャの層構成について、実感を伴って理解できるようになります。

MentorApp で追う:PostMessage の流れ

題材:「トピックにメッセージを投稿する」

今回追うのは、「トピックにメッセージを投稿する」操作(PostMessage)です。

この操作を選んだ理由は3つあります。

  • 全4層(Web / Application / Domain / Infrastructure)を通過する
  • Domain 層に業務ルール(クローズ済みトピックへの投稿禁止)が含まれている
  • ボタンクリックが起点の書き込み系処理で、流れがイメージしやすい

全体の流れのざっくりしたイメージは以下になります。

では、層ごとに見ていきましょう。

プロ太

以下では各層のコードをざっと見ていきます。処理の細部はすべて理解できなくて構いません。

「この層ではこんなことをしている」という雰囲気をつかむことが目標です。

Web 層:ボタンが押されたとき

入口は MentorApp.Web プロジェクトの Topics/Detail.razor です。まず PostMessageAsync の先頭にブレークポイントを設置し、デバッグ実行を開始します。

例えば、「メンター1」でログインし、「Topic」でトピック一覧を表示し、「技術的な質問」トピックを選び、そこで何かメッセージを入力し、「投稿」ボタンを押します。

画面上で処理がここで停止するので、そこからステップ実行で流れを追っていきましょう。

このコンポーネントの冒頭には、@injectTopicService が宣言されています。

@inject TopicService TopicService

フォームの送信時には PostMessageAsync が呼ばれ、その中で Application 層の TopicService.PostMessageAsync() へ処理を渡しています。

<EditForm ... OnSubmit="PostMessageAsync">

...

private async Task PostMessageAsync()
{
    var request = new PostMessageRequest(
        TopicId: topic.Id,
        SenderUserId: currentUser.UserId,
        Content: messageForm.Content!
    );
    await TopicService.PostMessageAsync(request, currentUser);
    ...
}

Web 層がやることはここで終わりです。リクエストを組み立てて Application 層に渡すだけで、業務ロジックはここには書きません。

プロ美

Web 層は「何をするか」は知らなくて、「誰に頼むか」だけ知っているんだね。

Application 層:ユースケースの流れを組み立てる司令塔

次に動くのは MentorApp.Application プロジェクトの TopicServicePostMessageAsync() です。

このメソッドの中身を見ると、5つのステップで処理が組み立てられています。

// コンストラクタ注入で受け取る ─ 型はインターフェース
private readonly IUnitOfWorkFactory unitOfWorkFactory;

public async Task<Message> PostMessageAsync(
    PostMessageRequest request, CurrentUser currentUser, ...)
{
    // 1. UoW を開く(Infrastructure 層に委譲)
    // ↓ uowの型は IUnitOfWork(インターフェース)
    await using var uow = await unitOfWorkFactory.CreateAsync(cancellationToken);

    // 2. 対象 Topic を取得(Infrastructure 層に委譲)
    //    uow.Topics の型は ITopicRepository(インターフェース)
    var topic = await uow.Topics.FindByIdAsync(request.TopicId, cancellationToken)
        ?? throw new KeyNotFoundException(...);

    // 3-1. 認可チェック:Mentorship当事者か(Domain 層・Mentorship.IsParticipant に委譲)
    //    uow.Mentorships の型は IMentorshipRepository(インターフェース)
    var mentorship = await uow.Mentorships.FindByIdAsync(topic.MentorshipId, cancellationToken)
        ?? throw new KeyNotFoundException(...);

    if (!mentorship.IsParticipant(currentUser.UserId))
        throw new UnauthorizedAccessException(...);

    // 3-2. 認可チェック:なりすましでないか(Application 層)
    if (request.SenderUserId != currentUser.UserId)
        throw new UnauthorizedAccessException(...);

    // 4. メッセージを投稿(Domain 層・Topic.PostMessage に委譲)
    var message = topic.PostMessage(request.SenderUserId, request.Content, now);

    // 5. 変更を保存(Infrastructure 層に委譲)
    await uow.SaveChangesAsync(cancellationToken);

    return message;
}

Application 層は「何をどの順で行うか」を組み立てる司令塔です。

モデル操作やビジネスロジックなど業務ルールの中核となる部分はDomain層へ委譲し、永続化などの技術詳細についてはInfrastructure層へ委譲します。

Infrastructure 層の具体実装は知らずに、インターフェース(契約)だけを相手にしています。ステップ1,2,5のInfrastructure層へのアクセスはすべてインターフェース経由です。

次に、ステップ4の topic.PostMessage() を例にDomain層の実装を、ステップ2の FindByIdAsync() を例にInfrastructure層の実装をそれぞれ見ていきます。

補足:依存性逆転の原則と依存性注入(DI)

Domain/Infrastructure層へ進む前に、Application 層のコードで登場したインターフェースと具象クラスの関係について少し補足します。

上述した Application 層のコードでは、Domain 層で定義されたインターフェースだけを参照しており、具象クラスを知りません

フィールド / 変数Application 層が認識している
インターフェース型
(Domain 層で定義)
使われている具象クラス
(Infrastructure 層で定義、
EF Core 依存の実装)
unitOfWorkFactoryIUnitOfWorkFactoryDbUnitOfWorkFactory
uowIUnitOfWorkDbUnitOfWork
uow.TopicsITopicRepositoryTopicRepository

この構造を使うと、たとえば DB の実装を EF Core から Dapper に変えても、Application 層のコードは一切変える必要がありません。

インターフェースという「接続口」さえ守れば、裏側の実装は自由に差し替えられます。

「呼び出しは Application から Infrastructure へ届くのに、コードの参照は Infrastructure から Domain(インターフェース定義側)へ向いている」わけです。

これを依存性逆転の原則と呼びます。以下のイメージです。

Infrastructure の技術詳細が Application に影響しないのは、この構造があるからです。

なお、このインターフェースと具象クラスの対応付けは、Web 層の起動設定で DI(依存性注入)コンテナを使って登録されています。

実行時には DI コンテナがこの対応表を参照して具象クラスを注入するため、Application 層は具象クラスを知らないまま正しい実装を受け取れます。

プロ美

インターフェースと実装の対応関係を、プログラムの起動時に DI コンテナが解決してくれているんだね。

プロ太

その通りです。DI の詳細な仕組みや、なぜこれがテストに役立つかはこのシリーズ内で解説していきます。

ここではWeb層で「配線役として起動時に動く」というイメージを持っておいてください。

DI コンテナについては以下の記事も参考にしてください。

C#入門編(24)依存性注入(DI)を徹底解説 ~DIの本質からDIコンテナの使い方まで~ C#入門編です。今回はC#における依存性注入(DI: Dependency Injection)について解説します。 「依存性注...

Domain 層:業務ルールを守る

Application 層がステップ4で呼び出す topic.PostMessage() が、MentorApp.Domain プロジェクトの Topic.cs の中に定義されています。

public Message PostMessage(Guid senderUserId, string? content, DateTimeOffset sentAt)
{
    if (Status == TopicStatus.Closed)
        throw new InvalidOperationException("クローズされたトピックにはメッセージを投稿できません。");

    var message = new Message(Id, senderUserId, content, sentAt);
    _messages.Add(message);
    return message;
}

「クローズ済みトピックには投稿できない」という業務ルールのチェックと、許可された場合のメッセージのトピックへの追加が、DBや画面に関する技術詳細なしに書かれています。

これは、Domain 層には純粋な業務ルールだけを書くというルールの結果です。

プロ美

DBの保存も画面の表示も気にせず、業務ルールのチェックと、許可された場合のメッセージ追加だけを書いているんだね。

プロ太

その通りです。Domain 層が純粋な業務ルールだけで書けているのは、余計な責務を持ち込まないからです。

Infrastructure 層:実際のDB操作を担う

Application 層のステップ 2 で uow.Topics.FindByIdAsync() が呼ばれると、実際に動いているのは TopicRepositoryFindByIdAsync() です。
MentorApp.Infrastructure プロジェクトで定義されたクラスです。)

public async Task<Topic?> FindByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
    return await dbContext.Topics
        .Include(t => t.Messages.OrderBy(m => m.SentAt))
        .FirstOrDefaultAsync(t => t.Id == id, cancellationToken);
}

EF Core の IncludeFirstOrDefaultAsync といった DB アクセスの詳細は、ここに閉じています。

ステップ 5 で uow.SaveChangesAsync() が呼ばれると、DbUnitOfWorkSaveChangesAsync() が呼び出されます。
(これもMentorApp.Infrastructure プロジェクトで定義されたクラスです。)

public async Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
    await dbContext.SaveChangesAsync(cancellationToken);
}

EF Core の dbContext.SaveChangesAsync() を呼び出して変更を保存します。

Application 層はどちらの実装も知らずに、インターフェース経由で呼び出しています。DB アクセスの詳細はすべて Infrastructure 層に閉じています。

プロ美

取得も保存も、EF Core のコードが全部 Infrastructure 層にまとまっているんだね。

プロ太

そうです。Application 層は DB の種類も操作の詳細も知りません。

Infrastructure 層の実装が変わっても、Application 層のコードはそのままでいられます。

まとめ

今回は「トピックにメッセージを投稿する」操作を通して、クリーンアーキテクチャの全4層を縦断する処理の流れを追いました。

前回(17回)で見た静的な構成と合わせることで、コードを読むときに「このクラスはどの層の責務で、どこから呼ばれるのか」が分かりながら読めるようになります。

ポイントを整理します。

  • アプローチ:1ユースケースをデバッガで追うと、各層の役割が実感を伴って分かる
  • Web 層:ユーザー操作を受け取り、Application 層にリクエストを渡す
  • Application 層:認可・Domain 呼び出し・データ保存の流れを組み立てる司令塔。インターフェース経由でInfrastructure層の実装を使う
  • Domain 層:純粋な業務ルールだけを持つ。DB も画面も知らない
  • Infrastructure 層:インターフェースの実装を持ち、EF Core による DB アクセス等を担う
  • 依存性逆転の原則と DI:「参照の向き」を「呼び出しの向き」と逆にすることで技術詳細を分離し、DI コンテナが起動時に具体実装を配線する

次回からは Part IV に入り、まずは値オブジェクト(Value Object)の実装を見ていきます。

プロ太

引き続き、DDDに基づいたドメイン層の具体的な実装方法を一緒に学んでいきましょう!

ABOUT ME
プロ太
ソフトウェア開発を楽しく、効率的に行う方法を追求しています。 開発者の視点から技術的課題に向き合い、「純粋な技術的興味に基づく探求」と「実践的な課題解決」という二つの柱を両輪として活動しています。