実務Webアプリ開発編です。前回までPart III(アーキテクチャ概論)として、クリーンアーキテクチャ・ドメイン駆動設計(DDD)の考え方を学びました。

今回は、クリーンアーキテクチャとDDDの考え方が、C#のソリューション・プロジェクト構成としてどう形になっているかを見ていきます。

概念と設計判断を先に整理したうえで、MentorApp の実際のファイルを読みながら設計の意図を確認していきましょう。

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

  • C#のソリューション・プロジェクト・フォルダの関係を整理したい
  • 物理フォルダで分けるかプロジェクトで分けるか、判断基準を知りたい
  • .sln と .slnx の違いや、新しいソリューション形式について知りたい
  • .csproj の ProjectReference の書き方と意味を理解したい

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

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

以下の記事を事前に読んでもらえると、より理解が深まるかと思います。

【C#/Blazor】実務Webアプリ開発編 (11) 要件定義の書き方 ~ロール・機能・非機能・対象外をどう整理するか 実務Webアプリ開発編です。 前回の「ソフトウェア開発の全体像」では、要件定義からデプロイまでの流れを俯瞰しました。 https...
【C#/Blazor】実務Webアプリ開発編 (16)ドメイン駆動設計(DDD)入門:値オブジェクト・エンティティ・集約・ドメインサービス 実務Webアプリ開発編です。前回は Part III(アーキテクチャ概論)の最初の記事として、クリーンアーキテクチャの全体像を整理しま...
プロ太

クリーンアーキテクチャ・DDDといった考え方に基づき、具体的にどのようにC#のソリューション・プロジェクト構成を整理するか学びましょう!

ソリューション・プロジェクト・フォルダの3段階

C# では、コードをまとめる単位として「ソリューション」「プロジェクト」「フォルダ」の3つが登場します。役割がそれぞれ異なるため、先に整理しておきます。

この節では「フォルダ」という言葉を2つの意味で使います。混乱を避けるため先に定義しておきます。

  • ソリューションフォルダ:Solution Explorer 上の論理グループです。物理フォルダとは独立した概念です。
  • 物理フォルダ:ファイルシステム上の実フォルダです。

ソリューション

ソリューションは、複数のプロジェクトをまとめる管理単位です。どのプロジェクトを含めるかは .slnx ファイルで定義されます。

プロ太

まずは「アプリ全体を束ねる入れ物」くらいに捉えておけば大丈夫です。

プロジェクト

プロジェクトは、ビルドの単位です。.csproj ファイルで定義され、1つのプロジェクトが1つの出力物(DLL や実行ファイル)に対応します。

ソリューションフォルダ

ソリューションフォルダは、Solution Explorer 上の表示を整理するための論理グループです。プロジェクトや、プロジェクト外のファイルを見やすくまとめるために使います。

物理フォルダと名前を合わせることも多いですが、必ず一致させる必要はありません。

具体例

3段階の関係をシンプルな例で示すと、以下のようになります。

物理フォルダ構成(ファイルシステム上の実際の姿)

MyApp/                          ← ルートディレクトリ
  MyApp.slnx                    ← ソリューション
  .editorconfig
  src/
    MyApp.Domain/
      MyApp.Domain.csproj       ← プロジェクト
    MyApp.Application/
      MyApp.Application.csproj  ← プロジェクト
  tests/
    MyApp.Tests/
      MyApp.Tests.csproj        ← プロジェクト

ソリューションエクスプローラの表示(ソリューションフォルダによる論理構成)

MyApp(ソリューション)
  src/(ソリューションフォルダ)
    MyApp.Domain(プロジェクト)
    MyApp.Application(プロジェクト)
  tests/(ソリューションフォルダ)
    MyApp.Tests(プロジェクト)
  設定ファイル/(ソリューションフォルダ・物理フォルダなし)
    .editorconfig(ファイル参照)

この表示は、slnx形式ファイルMyApp.slnx)の中では例えば次のように表現されます。

<Solution>
  <Folder Name="/src/">
    <Project Path="src/MyApp.Domain/MyApp.Domain.csproj" />
    <Project Path="src/MyApp.Application/MyApp.Application.csproj" />
  </Folder>
  <Folder Name="/tests/">
    <Project Path="tests/MyApp.Tests/MyApp.Tests.csproj" />
  </Folder>
  <Folder Name="/設定ファイル/">
    <File Path=".editorconfig" />
  </Folder>
</Solution>

<Folder> は Solution Explorer 上のソリューションフォルダ、<Project> はそこに表示するプロジェクト、<File> はプロジェクト外のファイル参照を表しています。

この例では、src/tests/ は物理フォルダ名とソリューションフォルダ名が一致しています。

一方、設定ファイル/ は物理フォルダとしては存在せず、Solution Explorer 上で .editorconfig を見やすく置くための論理グループです。

<File> で登録したファイルは Solution Explorer から開けるようになりますが、ビルドへの影響はありません。

プロジェクト配下のソースコードは .csproj が管理するため、通常は .slnx に1つずつ書く必要はありません。

従来の .sln は独自構文のソリューション形式です。

.slnx は XML ベースの新しい形式で、同じようにソリューションを表しながら、人にも読みやすい構造になっています。

プロ太

新しく作る場合は .slnx 形式を選ぶのがおすすめです。.NET 10 以降の dotnet new sln では既定で .slnx が作成されます。

ただ、SDK のバージョンやツールによっては .sln が作成されることもあります。

物理フォルダ分割 vs プロジェクト分割

クリーンアーキテクチャで層を分けるとき、C# では「(1)1プロジェクト内で物理フォルダを分ける方法」と「(2)プロジェクト自体を分ける方法」の2つのアプローチがあります。

最大の違いは、依存の向きをコンパイラに守らせられるかどうかです。

構成依存の制御作り始め向いているケース
(1)1プロジェクト+物理フォルダ分割人が注意するしかない手軽小規模・短命な試作
(2)複数プロジェクト分割コンパイラが強制するやや重い長期・チーム開発

それぞれの構成を図で比較すると以下のようになります。

1プロジェクト+物理フォルダ分割

MyApp/
  MyApp.sln
  MyApp/
    MyApp.csproj          ← プロジェクト(1つ)
    Domain/               ← 物理フォルダ
    Application/          ← 物理フォルダ
    Infrastructure/       ← 物理フォルダ
    Web/                  ← 物理フォルダ

複数プロジェクト分割

MyApp/
  MyApp.slnx
  MyApp.Domain/
    MyApp.Domain.csproj         ← プロジェクト
  MyApp.Application/
    MyApp.Application.csproj    ← プロジェクト
  MyApp.Infrastructure/
    MyApp.Infrastructure.csproj ← プロジェクト
  MyApp.Web/
    MyApp.Web.csproj            ← プロジェクト

複数プロジェクト構成では、各 .csprojProjectReference に参照先を明示します。参照していない層の型はコンパイルエラーになり、依存方向が機械的に強制されます。

1プロジェクト構成は、試作や小規模アプリでは十分です。ただし、依存ルールを守れるかどうかは開発者の注意力だけに頼ることになります。

複数プロジェクトに分けると、参照していない層の型はそもそもコンパイルできなくなります。依存方向の違反が「うっかりミス」から「コンパイルエラー」に変わります。

「常に複数プロジェクトが正解」ではなく、アプリの規模・寿命・チーム人数によって判断することが大切です。

プロ美

「気をつけて守る」から「そもそも書けないようにする」に変わるんだね。

プロ太

設計ルールを「人が注意して守る」から「機械が自動的にはじく」に変えるのが実務では大きな効果を持ちます。

MentorApp の具体的な構成をみる

MentorAppの具体的な構成を確認し、ソリューション・プロジェクト・フォルダという3段階構成について理解を深めましょう。

全体構成

MentorApp の3段階の関係を示すと、以下のようになります。(参考:フォルダ構成

MentorApp/                        ← ルートディレクトリ(物理フォルダ)
  MentorApp.slnx                  ← ソリューション
  src/   (物理フォルダ兼ソリューションフォルダ)
    MentorApp.Domain/             ← プロジェクト
    MentorApp.Application/        ← プロジェクト
    MentorApp.Infrastructure/    ← プロジェクト
    MentorApp.Web/                ← プロジェクト
  tests/ (物理フォルダ兼ソリューションフォルダ)
    MentorApp.Tests/              ← プロジェクト
  docs/  (物理フォルダ兼ソリューションフォルダ)
    spec.md 等                    ← 設計ドキュメント
  ソリューション項目/ (ソリューションフォルダのみ・物理フォルダなし)
    .editorconfig                 ← 実体はルート直下
    README.md                 ← 実体はルート直下

この全体像を念頭に置くと、後で .slnx.csproj を読んだときに、どの単位に何が書かれているかが把握しやすくなります。

MentorApp は各層を独立したプロジェクトとして分割する構成を採用しています。クリーンアーキテクチャの依存方向をコンパイラに強制させるためです。

ソリューション(.slnx)

MentorApp の MentorApp.slnx を見ると、srctestsdocsソリューション項目 の4つのソリューションフォルダが XML で整理されています。

ソリューション項目 フォルダは、プロジェクトに属さないソリューション全体の管理ファイル(.editorconfig.gitignoreREADME.md など)を置く場所です。

Visual Studio のソリューションエクスプローラーからこれらのファイルを直接開けるようにするための仕組みです。

<Solution>
  <Folder Name="/src/">
    <Project Path="src/MentorApp.Domain/MentorApp.Domain.csproj" />
    <Project Path="src/MentorApp.Application/MentorApp.Application.csproj" />
    <Project Path="src/MentorApp.Infrastructure/MentorApp.Infrastructure.csproj" />
    <Project Path="src/MentorApp.Web/MentorApp.Web.csproj" />
  </Folder>
  <Folder Name="/tests/">
    <Project Path="tests/MentorApp.Tests/MentorApp.Tests.csproj" />
  </Folder>
  <Folder Name="/docs/">
    <File Path="docs/spec.md" />
    <!-- ... -->
  </Folder>
  <Folder Name="/ソリューション項目/">
    <File Path=".editorconfig" />
    <File Path="README.md" />
    <!-- ... -->
  </Folder>
</Solution>

src/ にはアプリ本体の4プロジェクト、tests/ にはテストプロジェクト、docs/ には設計ドキュメント、ソリューション項目 には README.md などが置かれています。

Visual Studioのソリューションエクスプローラでは以下のように表示されます。

この分離により、「アプリを変更するのか」「テストを書くのか」「ドキュメントを更新するのか」という作業の目的が、フォルダの切り替えで明確になります。

なお、「src・tests・docs」のように目的別の物理フォルダを設けるかどうかはチームや慣習によります。

C# の従来の慣習ではソリューション直下にプロジェクトをフラットに並べることも多くありました。

近年は JavaScriptなど他言語の流儀が取り入れられ、目的別に階層化するスタイルが広まっています。MentorApp ではこの階層化スタイルを採用しています。

プロジェクト(.csproj)

全体構成で見たとおり、MentorApp は各層を独立したプロジェクトに分けています。各プロジェクトが何を参照するかは、.csproj 内の ProjectReference に明示されています。

参照のあるプロジェクトの型だけが使え、参照のない層の型はコンパイルエラーになります。これが依存方向をコンパイラに守らせる仕組みです。

以下の .csproj は、依存関係を読むための主要部分の抜粋です。

TargetFrameworkNullable などの共通設定、細かなビルド設定は省略し、省略箇所は <!-- ... --> で示します。

Domain:参照なし(設計の中心として独立)

MentorApp.Domain.csproj には ProjectReference が一切ありません。Domain は他のどのプロジェクトにも依存しない、設計の中心として完全に独立しています。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <!-- ... -->
  </PropertyGroup>
  <!-- ProjectReference なし → どの層にも依存しない -->
</Project>

Application:Domain のみ参照

MentorApp.Application.csproj は Domain のみを参照します。

DI とログのために Microsoft.Extensions 系パッケージを追加していますが、EF Core や Blazor には依存していません。ユースケース層としての独立性が保たれています。

<Project Sdk="Microsoft.NET.Sdk">
  <!-- ... -->
  <ItemGroup>
    <ProjectReference Include="..\MentorApp.Domain\MentorApp.Domain.csproj" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
  </ItemGroup>
</Project>

Infrastructure:Domain + Application を参照、技術パッケージを追加

MentorApp.Infrastructure.csproj は Domain と Application を参照します。さらに EF Core と OIDC 認証の NuGet パッケージが加わります。

技術実装に必要なパッケージはこの層に集まっており、内側の Domain・Application はこれらを知らない状態を保っています。

ASP.NET Core 関連の型を使うため、Microsoft.AspNetCore.App への FrameworkReference もここに置いています。

<Project Sdk="Microsoft.NET.Sdk">
  <!-- ... -->
  <ItemGroup>
    <ProjectReference Include="..\MentorApp.Domain\MentorApp.Domain.csproj" />
    <ProjectReference Include="..\MentorApp.Application\MentorApp.Application.csproj" />
  </ItemGroup>
  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.3" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.3" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.3" />
    <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.3" />
  </ItemGroup>
</Project>

Web:全3プロジェクトを参照、Blazor Server の入口

MentorApp.Web.csproj は Domain・Application・Infrastructure の3プロジェクトをすべて参照します。

SDK が Microsoft.NET.Sdk.Web になっており、Blazor Server として動作するための設定が含まれています。

ログ出力のための Serilog.AspNetCore も追加し、アプリ全体を組み合わせる入口になっています。

<Project Sdk="Microsoft.NET.Sdk.Web">
  <!-- ... -->
  <ItemGroup>
    <ProjectReference Include="..\MentorApp.Domain\MentorApp.Domain.csproj" />
    <ProjectReference Include="..\MentorApp.Application\MentorApp.Application.csproj" />
    <ProjectReference Include="..\MentorApp.Infrastructure\MentorApp.Infrastructure.csproj" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
  </ItemGroup>
</Project>

Tests:全4プロジェクトを参照、統合・E2E テストを担う

MentorApp.Tests.csproj はアプリ本体の4プロジェクトをすべて参照します。E2E テストや統合テストで全層を横断して動作確認を行うためです。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <!-- ... -->
    <OutputType>Exe</OutputType>
    <IsPackable>false</IsPackable>
    <TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="xunit.v3" Version="3.2.2" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.3" />
    <PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="10.3.0" />
    <PackageReference Include="Microsoft.Playwright" Version="1.58.0" />
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\..\src\MentorApp.Domain\MentorApp.Domain.csproj" />
    <ProjectReference Include="..\..\src\MentorApp.Application\MentorApp.Application.csproj" />
    <ProjectReference Include="..\..\src\MentorApp.Infrastructure\MentorApp.Infrastructure.csproj" />
    <ProjectReference Include="..\..\src\MentorApp.Web\MentorApp.Web.csproj" />
  </ItemGroup>
</Project>

テストライブラリとして xUnit v3、Microsoft.AspNetCore.Mvc.Testing(WebApplicationFactory)、Microsoft.Playwright を使用しています。

時刻を扱うテストのために Microsoft.Extensions.TimeProvider.Testing も追加しています。

OutputTypeExe になっているのは xUnit v3 の新しい実行モデルに対応するためです。

v3 ではテストプロジェクトを実行ファイルとして出力し、直接起動します。dotnet test での実行方法は変わりません。

プロ美

層間の参照関係が .csproj から読み取れるんだね。

プロ太

そのとおりです。.csproj を読むだけで、依存の構造が設計の意図どおりに守られているかを確認できます。

同じ構成をどう作るか(dotnet CLI)

ここまで MentorApp の既存ファイルを読んできましたが、同じ構成を一から作る場合は dotnet コマンドを使います。

主な手順は5つです。ソリューションの作成、プロジェクトの作成、ソリューションへの追加、プロジェクト間の参照追加、NuGet パッケージの追加です。以下は例です。

# ① ソリューションを slnx 形式で作成
dotnet new sln -n MentorApp --format slnx

# ② プロジェクトを作成(Domain の例)
dotnet new classlib -n MentorApp.Domain -o src/MentorApp.Domain

# ③ ソリューションにプロジェクトを追加
dotnet sln add src/MentorApp.Domain

# ④ プロジェクト間の参照を追加(Application → Domain の例)
dotnet add src/MentorApp.Application reference src/MentorApp.Domain

# ⑤ NuGet パッケージを追加(Infrastructure に EF Core を追加する例)
dotnet add src/MentorApp.Infrastructure package Microsoft.EntityFrameworkCore.SqlServer --version 10.0.3

Visual Studio のメニュー操作(「プロジェクトの追加」「参照の追加」)でも同じ結果になります。

.slnx 形式のソリューションを開いている場合は、内部では .slnx.csproj を書き換えているだけです。

直接ファイルを手書きすることも技術的には可能ですが、コマンドやGUIを使うほうが記法のミスが起きにくく、実務では一般的です。

プロ太

dotnetコマンドを使うと、ソリューション・プロジェクト構成手順をスクリプト化し、再利用・チームへの共有がしやすくなります。

以下も参考にしてください。

【C#入門編】GUI操作 vs CLI操作 どちらが効率的?~Visual Studioとコマンド活用、AI時代の開発スタイル~ C#開発を始めるとき、多くの方がVisual StudioなどのGUI操作(クリックや画面操作)に頼りがちです。 しかし、近年は...

まとめ

C# のソリューション・プロジェクト構成は、設計の意図をファイルの形として固定する仕組みです。

.slnx でフォルダ構造を読み、.csprojProjectReference で依存の向きを確認できます。設計の考え方が、ファイルの記述としてそのまま現れています。

  • 3段階の関係:ソリューション(.slnx)がプロジェクト(.csproj)をまとめ、フォルダで論理グループを作る
  • 設計判断:フォルダ分割は手軽だが依存を人が守る。プロジェクト分割はコンパイラが強制する
  • MentorApp の構成:各層を独立したプロジェクトに分け、.slnx で src / tests / docs の関心を分離している
  • ProjectReference:.csproj に参照先を明示することで、依存方向をコンパイラに守らせる
  • dotnet CLI:コマンドでソリューション・プロジェクト構成を作成・変更できる

次回は、この構成の上で画面クリックからデータベースまで処理がどう流れるかを、全層をまたいで追いかけていきます。

プロ太

構成を実際のファイル(slnx、csprojなど)で読むと、設計の意図がコードに落ちているのが見えてきます。

引き続き、クリーンアーキテクチャの処理の流れについて一緒に学んでいきましょう!

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