【C#/Blazor】実務Webアプリ開発編 (17)C#のソリューション・プロジェクト構成 ~.slnx と .csproj で設計の意図を読む~
実務Webアプリ開発編です。前回までPart III(アーキテクチャ概論)として、クリーンアーキテクチャ・ドメイン駆動設計(DDD)の考え方を学びました。
今回は、クリーンアーキテクチャとDDDの考え方が、C#のソリューション・プロジェクト構成としてどう形になっているかを見ていきます。
概念と設計判断を先に整理したうえで、MentorApp の実際のファイルを読みながら設計の意図を確認していきましょう。
以下のような方に役立つ内容となっています。
- C#のソリューション・プロジェクト・フォルダの関係を整理したい
- 物理フォルダで分けるかプロジェクトで分けるか、判断基準を知りたい
- .sln と .slnx の違いや、新しいソリューション形式について知りたい
- .csproj の ProjectReference の書き方と意味を理解したい
以下のようなMentorAppを題材として進めます。

GitHubにドキュメント・コードの一式があります。
以下の記事を事前に読んでもらえると、より理解が深まるかと思います。
クリーンアーキテクチャ・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 ← プロジェクト複数プロジェクト構成では、各 .csproj の ProjectReference に参照先を明示します。参照していない層の型はコンパイルエラーになり、依存方向が機械的に強制されます。
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 を見ると、src・tests・docs・ソリューション項目 の4つのソリューションフォルダが XML で整理されています。
ソリューション項目 フォルダは、プロジェクトに属さないソリューション全体の管理ファイル(.editorconfig・.gitignore・README.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 は、依存関係を読むための主要部分の抜粋です。
TargetFramework や Nullable などの共通設定、細かなビルド設定は省略し、省略箇所は <!-- ... --> で示します。
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 も追加しています。
OutputType が Exe になっているのは 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.3Visual Studio のメニュー操作(「プロジェクトの追加」「参照の追加」)でも同じ結果になります。
.slnx 形式のソリューションを開いている場合は、内部では .slnx と .csproj を書き換えているだけです。
直接ファイルを手書きすることも技術的には可能ですが、コマンドやGUIを使うほうが記法のミスが起きにくく、実務では一般的です。
dotnetコマンドを使うと、ソリューション・プロジェクト構成手順をスクリプト化し、再利用・チームへの共有がしやすくなります。
以下も参考にしてください。
まとめ
C# のソリューション・プロジェクト構成は、設計の意図をファイルの形として固定する仕組みです。
.slnx でフォルダ構造を読み、.csproj の ProjectReference で依存の向きを確認できます。設計の考え方が、ファイルの記述としてそのまま現れています。
- 3段階の関係:ソリューション(.slnx)がプロジェクト(.csproj)をまとめ、フォルダで論理グループを作る
- 設計判断:フォルダ分割は手軽だが依存を人が守る。プロジェクト分割はコンパイラが強制する
- MentorApp の構成:各層を独立したプロジェクトに分け、.slnx で src / tests / docs の関心を分離している
- ProjectReference:.csproj に参照先を明示することで、依存方向をコンパイラに守らせる
- dotnet CLI:コマンドでソリューション・プロジェクト構成を作成・変更できる
次回は、この構成の上で画面クリックからデータベースまで処理がどう流れるかを、全層をまたいで追いかけていきます。
構成を実際のファイル(slnx、csprojなど)で読むと、設計の意図がコードに落ちているのが見えてきます。
引き続き、クリーンアーキテクチャの処理の流れについて一緒に学んでいきましょう!





