やりたいこと
複数バージョンを開発・サポートする場合に、以下のような状況に遭遇します。
- どのバージョンにも実施したいユニットテスト群がある
 - どのバージョンでもソース群が同じか、ほとんど同じプロジェクトがある
 - どのバージョンも同時に編集したい(せざるを得ない)
 
ところが、ソースファイルはほとんど同じでありながら、ターゲット環境・ランタイム(.NET Framework / Standard / Core)や参照パッケージ/バージョンが異なることがよくあります。
このへんをいい感じに管理する1つの方法です。
解決方法
- プロジェクト(*.csproj)はSDKスタイルを使う
 - バージョンでプロジェクトを分けて同じフォルダに格納する
 - それぞれ異なる出力フォルダを指定する
 
<Project>
  <PropertyGroup>
    <BaseIntermediateOutputPath>obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
  </PropertyGroup>
  <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
  <PropertyGroup>
    <OutputPath>bin\$(Configuration)\$(MSBuildProjectName)</OutputPath>
  </PropertyGroup>
  <Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
やっていること
複数プロジェクトに分ける
SDKスタイルのプロジェクトファイルはフォルダ内のソースをビルド対象とみなすので、1つのフォルダに複数のプロジェクトファイルを作ればソースを共有したことになります。
└ ProjectA
   ├ ProjectA.V1.csproj         # V1保守用のプロジェクト
   ├ ProjectA.V2.csproj         # V2開発用のプロジェクト
   ├ source1.cs                 # ソースは同じ
   ├ source2.cs                 
   :
以前からVisual Studioのプロジェクトはソースファイルを複数プロジェクトで共有できます。プロジェクトにソースファイルを「リンクとして追加」するんですが、ファイル増減や名前変更時は各プロジェクトの設定を手動で追従させる必要があって手間でした。
SDKスタイルのプロジェクトなら、どれか1つのプロジェクトでファイル追加・削除すれば、他のプロジェクトにも自動的に反映されるので、リファクタリング機能を使ってファイル名変更や移動もどんどんやれます。
出力フォルダを区別する
実は、1つのフォルダに複数プロジェクトを作成するだけではダメです。そのままでは生成ファイルを格納するフォルダが衝突してしまいます。
複数プロジェクトでフォルダが同じだと、最後にビルドしたプロジェクトの生成ファイルで上書きしてしまうので、参照パッケージが別プロジェクトで参照しているバージョンにすり替わってしまったり、Visual Studioを開いているとフォルダ内のファイル変更を検出して他プロジェクトの参照設定が消えてしまったりします(あらためてnuget参照を復元すると復活したり)。
ですので、出力フォルダをプロジェクトごとに分けてあげる必要があります。
BaseIntermediateOutputPath - 中間ファイル出力フォルダ
通常は先頭のProject要素の属性でSDKを指定します。
<Project Sdk="Microsoft.NET.Sdk">
しかし、中間ファイルの出力パスはSDKの設定より前に指定しておく必要がある(SDKの設定はこの属性を使用するため)ので以下のようにします。最初にSDKは読み込まず、出力パスを設定してからSDKを読み込んでいます。
<Project>
  <PropertyGroup>
    <BaseIntermediateOutputPath>obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
  </PropertyGroup>
  <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
OutputPath - ビルド結果の出力フォルダ
ビルド結果の出力フォルダも分けます。
フォルダ名は何でもよいですが、プロジェクト名でサブフォルダを分けるようにするとどのプロジェクトも同じ設定を使いまわせて便利です。
  <PropertyGroup>
    <OutputPath>bin\$(Configuration)\$(MSBuildProjectName)</OutputPath>
  </PropertyGroup>
Q&A
プロジェクトファイルを別フォルダに置けばフォルダ指定変えなくていいんじゃないの?
そうです。その場合は同じファイル名でもいいです。
同時に保守・開発するバージョン数や、それらの差分規模、同時にビルドしたいかどうか、などやりたいこと次第ですね。
- 1つのフォルダに複数プロジェクトをつっこむ(本記事の例)
- バージョンの増減でフォルダ構成を変えなくていい。特定バージョンだけの削除やリポジトリ移動してもフォルダ構成が変わらない。
 - プロジェクトファイルにフォルダ指定が必要だし、プロジェクトファイル名やプロジェクト名も重複してはいけない。
 - バージョン間の違いが限定的で、#if~#endif で切り分ければよい程度の違いにとどまる場合、つまり機能的には枯れて安定していて、ターゲット変更や些細な拡張をするくらい、という想定の場合におすすめ。
 
 - プロジェクトごとにフォルダを作る
- フォルダ指定を変えなくていいし、プロジェクトファイル名も同じでいい。
 - プロジェクトファイルにソースファイルの場所(フォルダ)を指定する必要がある。
 - あるバージョンのみのソースファイルがあるとか、ファイル単位の差分があるような場合におすすめ。1つのフォルダにまとめるとかえって混沌としてしまうので。
 
 
1つのプロジェクトにまとめられないの?
csprojファイル(正しくはMSBuildの定義ファイル)には条件式評価や分岐が記述できるので、1つのプロジェクトファイルにターゲットの異なる複数のビルド設定を定義したり、パラメータに応じて参照ファイルやバージョンを切り替えることも可能です。
可能ですが、プロジェクト全体を共有するようなケースにはおすすめしません。
- 特定バージョン向けの設定を変更したいだけなのに、別バージョンの設定を変更しなければならない場合がある
- 新規追加したファイルが過去バージョンには含まれないようにしたり、条件分岐を増やすために既存の定義を変更するとか
 
 - 過去バージョンの保守が完了して破棄したり、特定バージョンだけ別リポジトリへ引っ越したいなんて場合でも、プロジェクトが分かれていれば簡単だし間違えない
- ちゃんと確認しろって話ですが、それでも間違いは起きるもので。。。
 
 
既存のプロジェクトファイルは変更せず、新バージョン追加時は追加分だけの別プロジェクトを作るようにするとよいです。