😽

C# 複数EXE+共通DLLをどう管理する?―「1リポジトリ+1ソリューション」が効く条件と、大規模化したときの現実解

に公開

0. 結論(先に答え)

小〜中規模(チーム・プロジェクト数がまだ小さい)

  • 1リポジトリ(モノレポ)にまとめ、1つのソリューション(.sln)に複数EXE+共通DLL(クラスライブラリ)を入れるのが、最も手戻りが少ない構成になりやすいです。
    • 共通DLLへステップインできる(プロジェクト参照で開発できる)
    • 参照箇所の洗い出しが IDE の機能で一発(Find All References / ソリューション全体検索)
    • 「スタートアップ プロジェクト変更だけで Git が荒れる」問題は、通常は起きません(後述)

大規模(プロジェクト数が増え、ソリューションが重い・分業が進む)

  • モノレポは維持しつつ、次の「分割のしかた」でスケールさせるのが、現実的で安全です。
    1. **全体用の All.sln(または .slnx)**は1つ保つ(統合参照調査・統合ビルドの基準)
    2. 開発者の普段使いは **Solution Filter(.slnf)**で「必要な部分だけ読み込む」
    3. ビルド設定は Directory.Build.props / Directory.Build.targets で中央集権化
    4. どうしても複数リポジトリに分けるなら、“統合用のスーパーリポジトリ(サブモジュール)+統合ソリューション” を用意して、
      git clone した時点でフォルダ構造を強制する(後述)

1. 何を死守したいのか(要件の整理)

ここで守りたいのは、次の2つです。

  1. デバッガーで共通DLLのソースへステップインできること
    → これは「成果物DLL参照」ではなく プロジェクト参照(ProjectReference) が基本になります。

  2. 共通DLLのクラス/メソッドの参照箇所を、システム全体として半自動で一発抽出できること
    → Visual Studio の Find All References / ソリューション全体検索 / 参照の検索を、
    **“システム全体を含む単一の視界”**で実行できる状態が重要です。


2. 「スタートアップ変更で .sln が汚れて Git が荒れる」問題は、通常は起きない

2-1. スタートアップ等の“ユーザー設定”は .suo(Solution User Options)に保存される

Visual Studio の **Startup Project(起動プロジェクト)**のような「ソリューションのプロパティ」は、通常 **.suo(Solution User Options)**側に保存されます。

Microsoft Learn(Solutions overview)には、.sln と .suo の役割が次のように説明されています(抜粋)。

公式(抜粋)

  • 「The solution file contains text-based information … can be placed under source code control.」
  • 「The solution user option file contains binary information … cannot be placed under source code control … user-specific information.」

根拠URL(公式):
https://learn.microsoft.com/en-us/visualstudio/extensibility/internals/solutions-overview?view=visualstudio

つまり、起動プロジェクトを切り替えるだけで .sln が更新され、チーム全員に影響する——という心配は、通常は当たりません。
(※ .sln が更新されるのは、プロジェクト追加/削除や、ソリューション構成の追加など「共有すべき変更」を行った場合です。)

2-2. 「.suo / .vs をコミットしない」前提は、定番の .gitignore に組み込まれている

Visual Studio 系の定番 .gitignore として広く使われる GitHub の VisualStudio.gitignore では、.vs/ や *.suo が ignore 対象です(= “ユーザー固有”なので共有しない前提)。

テンプレート(抜粋)

  • .vs/
  • *.suo

根拠URL(テンプレート):
https://raw.githubusercontent.com/github/gitignore/main/VisualStudio.gitignore

2-3. Visual Studio の「Git リポジトリ作成」では .gitignore を自動生成できる

Microsoft Learn(Azure Repos の Git 解説)には、Visual Studio で Git リポジトリを作成すると、Visual Studio 用の .gitignore を自動生成することが明記されています(抜粋)。

公式(抜粋)

  • 「Visual Studio automatically creates a .gitignore file for the Visual Studio development environment when you create a Git repo.」

根拠URL(公式):
https://learn.microsoft.com/en-us/azure/devops/repos/git/ignore-files?view=azure-devops

結論:“起動プロジェクトを変えたらコミットが要る” ではなく、
「.suo/.vs をコミットしない」前提であれば、起動プロジェクトの変更だけで Git が荒れるのは避けられます。

3. 小〜中規模での推奨構成(最短で効く)

3-1. 構成の形(モノレポ+1ソリューション)

共通DLL(Class Library)も、各EXE(Console/WinForms/WPF/Service 等)も、同一リポジトリの同一ソリューションに含める形です。

3-2. これが効く理由(反論材料として使える“事実”)


4. 大規模になったらどうする?(理想:モノレポを維持しつつ“重さ”だけ逃がす)

大規模化で現実的に起きるのは、次の問題です。

  • All.sln が巨大化して IDE の読み込みが重い
  • 担当外のプロジェクトまで読み込む必要がない
  • それでも「全体参照調査」「統合ビルド」は捨てたくない

このときの定石が、**Solution Filter(.slnf)**です。

4-1. Solution Filter(.slnf)で “読み込みだけ”分割する

4-1-1. Solution Filter(.slnf)の基礎(初心者向け)

**Solution Filter(拡張子 .slnf)は、「1つの .sln のうち、どのプロジェクトを“読み込むか”を保存するファイル」**です。

Microsoft Learn(Filtered solutions)では、**ソリューションフィルタで“必要なプロジェクトだけをロードして開く”**こと、
さらに .slnf を作成して共有/コミットできることが説明されています(抜粋)。

公式(抜粋)

  • 「open a solution with only selective projects loaded」
  • 「create a solution filter file … share … commit it to source control」

根拠URL(公式):
https://learn.microsoft.com/en-us/visualstudio/ide/filtered-solutions?view=visualstudio

また MSBuild の仕様として、.slnf は JSON 形式で、solution に “元の .sln” を指定する構造です(= .slnf は .sln の代替ではなく、.sln が必須)。

根拠URL(公式):
https://learn.microsoft.com/en-us/visualstudio/msbuild/solution-filters?view=visualstudio

4-1-2. 重要:全体参照調査(Find All References)を “YES” にする鍵は「全体を開く入口」を残すこと

Solution Filter は “軽く開く”ための仕組みです。
担当サブセットだけをロードした状態では、当然「未ロードのプロジェクト」まで含めた参照調査はできません。
そこで、全体参照調査を死守するために、次の 2種類の入口をリポジトリに固定します。

  • 入口A:全体を読む入口(全体参照調査のため)

    • All.sln を開く(全プロジェクトをロード)
    • もしくは Filters/AllLoaded.slnf(全プロジェクトを列挙した .slnf)を開く
      → この入口を開けば、共通DLLのクラス/メソッドの参照箇所をシステム全体で一発で洗い出せます。
  • 入口B:普段を軽くする入口(作業効率のため)

    • Filters/TeamA.slnf / Filters/TeamB.slnf のように、担当サブセットだけを読む
    • 必要な時だけ「依存関係をロード」等で追加ロードする(Filtered solutions の手順に “Load project dependencies” が案内されています)

これで、先の質問「.slnf で全体参照調査は死守できるの?」に YES と言えます。
“全体を読む入口(All.sln / AllLoaded.slnf)” を同じリポジトリに固定しているからです。

4-2. さらに merge conflict を減らす(.slnx)

Microsoft は、新しいソリューション形式 .slnx を導入しており、
「読みやすさ」「差分の追いやすさ」「バージョン管理でのマージ衝突低減」などを目的にしています。

※既存の .sln 運用をすぐ捨てる必要はありません。
ただし、「巨大ソリューションを複数人で触る」ほど 差分衝突のコストが効いてくるため、検討価値があります。

4-3. ビルド設定・参照の統一は Directory.Build.props で“構造的に強制”する

大規模になるほど「プロジェクトごとに設定がバラバラ」が事故になります。
MSBuild は Directory.Build.props / Directory.Build.targets による フォルダ単位の共通設定を公式にサポートしています。

これは リポジトリ直下に置けば、git clone の時点で構造的に強制できます。


5. 「1リポジトリ = 1ソリューション = 1プロジェクト」を押し付けられた時の現実解

ここで問題になるのは、次の2点です。

  • 統合的な参照箇所調査(IDE一発)が難しくなる
  • 共通DLLへのステップインが難しくなる(成果物DLL参照に寄りやすい)

5-1. それでも“構造で強制”し、IDE一発調査もステップインも守る:スーパーリポジトリ+サブモジュール

5-1-1. 「スーパーリポジトリ(superproject)」「サブモジュール」の基礎(初心者向け)

スーパーリポジトリ(superproject)とは

**サブモジュールを含む“親リポジトリ”**のことです。
この親リポジトリの中に、別リポジトリを 指定パスに固定配置できます(例:modules/CommonRepo)。
その結果、git clone した時点でディレクトリ構造が決まり、相対パス参照が壊れにくくなります。

サブモジュール(submodule)とは

Git が公式に提供する仕組みで、**「別リポジトリを、親リポジトリの特定ディレクトリに“埋め込む”」**ための機能です。
親リポジトリは、各サブモジュールについて どのコミット(どの版)を使うかを記録します。

clone するときに必要なコマンド(ここが重要)

Git Submodule を採用したリポジトリは、通常の git clone だけでは submodule の中身が揃いません。
submodule を初期化して、内容を取得する操作が必須です。

  1. 最初の clone で一発で揃える(推奨)
git clone --recurse-submodules <superproject-url>

(古い書き方として --recursive もあります)

git clone --recursive <superproject-url>
  1. すでに clone 済みなら、あとから揃える
git submodule update --init --recursive
  1. pull した後、submodule の更新も揃える(運用として固定する)
git pull
git submodule update --init --recursive

根拠URL(公式):

https://git-scm.com/docs/git-clone
https://git-scm.com/docs/git-submodule
https://git-scm.com/book/en/v2/Git-Tools-Submodules

公式(抜粋・要点)

  • git clone の --recurse-submodules は、clone 時に submodule を初期化/更新するオプション。
  • git submodule update --init --recursive は、未初期化 submodule を初期化して取得し、入れ子も含めて更新する。

注意(大規模で“やるなら”ここまで決める)

  • サブモジュール運用は、**更新手順(どのタイミングで superproject 側の参照コミットを進めるか)が必要になります。
    ただしそれでも、
    「統合調査用 All.sln を保持し、構造は clone で強制」**という目的には強力です。

複数リポジトリに分ける事情がある場合でも、
「統合調査用の入口」を 別リポジトリとして用意し、各リポジトリを サブモジュールで固定配置します。

Git はサブモジュールを「別リポジトリを親リポジトリのサブディレクトリに埋め込む」仕組みとして定義しています。

この方法だと、git clone した時点でディレクトリ構造が決まり、相対パスが壊れにくい(=ルールではなく構造で強制)というメリットがあります。

重要

  • 個別リポジトリを残しつつ、統合の“見える化”と参照調査を取り戻せます
  • 「共通DLLにステップイン」も ProjectReference で守れます
  • “統合用ソリューション”を触るのはスーパーリポ側なので、個別リポの運用ポリシーとも切り分けやすいです

もし「共通DLLは NuGet 化して配布する」などの事情で、成果物参照が避けられない場合は、
Visual Studio の External Sources や Source Link により、外部依存のソースをデバッグで追いやすくする方向があります。

ただし、この方法は「IDEの参照箇所調査を“システム全体で一発”」という要求を満たしにくいので、
統合ソリューション(All.sln)を別途維持するのが安全です。


6. まとめ(反論材料の形に落とす)

  • “共通DLLにステップイン”と “参照箇所をIDE一発で洗い出し”を同時に満たすなら、基本は
    同一リポジトリ+同一ソリューションに全プロジェクトを含め、ProjectReference でつなぐのが最短。
  • スタートアップ設定で Git が荒れるは、通常は **.suo(ユーザー設定)**の領域なので、
    .gitignore で除外していれば共有されません。
  • 大規模化したら、モノレポを崩す前に
    **Solution Filter(.slnf)+ Directory.Build.props +(必要なら .slnx)**でスケールさせるのが現実的。
  • どうしても複数リポジトリなら、
    スーパーリポ+サブモジュール+統合ソリューションで「clone時点で構造を強制」しつつ、
    IDE一発調査+ステップインを守る。

参考リンク(一次情報中心)

Discussion