複数のプロジェクトの設定に同じ既定値を設定する

Visual Studioやdotnet.exeなどでC#で複数の.NETモジュールの開発をしているとき、ソリューションのすべてのプロジェクトで同じ設定を使いたいことがあります。例えば TargetFramework です。

同じソリューション内のプロジェクトは、同じアプリのモジュールを構成する要素であることが多く、フレームワークのバージョン(TargetFramework)には同じ値に設定することが多いと思います。

プロジェクト ファイル(.csproj)ごとにTargetFrameworkを設定していると、フレームワークのバージョンをアップデート(たとえば、.net 6.0から.net 7.0にアップデート)したいときには、すべてのプロジェクト ファイルのTargetFrameworkの値をを変更する必要があります。

このような時、同じソリューション内のすべてのプロジェクトの設定を、ファイルの一ヶ所を変更するのみで一括して設定できると便利でます。

たとえば、以下の内容のファイルを一つ用意し、この内容をすべてのプロジェクト ファイルに反映できることです。

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <TargetFramework>net7.0-windows</TargetFramework>
  </PropertyGroup>
</Project>

このようなことは、Visual Studioのプロジェクトで可能なのでしょうか?

二つの方法があります。

  • 各プロジェクト ファイルで共通設定ファイルを読み込む。
  • Directory.Build.*設定ファイルを使う

各プロジェクト ファイルで共通設定ファイルを直接インポートする

共通設定ファイルをCustom.propsなどのファイル名で用意し、共通の設定をそのファイルに設定します。

そして、各プロジェクトファイルで、<Import Project="../Cumsom.props"/> のように、その共通設定ファイルを直接インポートします。これにより、同じ設定を各プロジェクトファイルに反映することができます。

しかし、各プロジェクトのフォルダー構成が、フラットの場合(すべてのプロジェクトが同じ親フォルダーを持つ場合)は、ImportIncludeのパス指定は、すべてのプロジェクトファイルで同じ"../Cumsom.props"でよいのでシンプルです。

しかし、各プロジェクトのフォルダ構成が、フラットではなく、サブフォルダーやサブサブフォルダーにあったりするような複雑な構成の場合、ImportIncludeのパス指定がプロジェクトごとに異なることになり、複雑になります。

また、新規に作成したプロジェクトに<Import .../>を追加することを忘れたら、共通設定が反映されることはありません。複数人で開発している場合、追加漏れは、発生します。

各プロジェクトファイルで、Importタグで共通設定を直接インポートする方法は直感的でわかりやすくはありますが、このようなデメリットもあります。

Directory.Build.* 設定ファイルを使う

Directory.Build.*設定ファイルの機能は、Visual Studio 2017 (MSBuild 15)で追加されました。

この機能は、プロジェクトフォルダー、もしくは、その親フォルダー(ドライブのルートフォルダーまでの上位の親フォルダーを含む)に、指定されたファイル名で設定ファイルを配置しておくと、各プロジェクトファイルで、そのファイルを自動的にインポートされる機能です。

指定のファイル名とは以下の二つのファイル名です。ファイル名により、インポートされる場所が異なります。

  • Directory.Build.props‥‥‥プロジェクトファイルの先頭でインポートされる
  • Directory.Build.targets‥‥プロジェクトファイルの最後でインポートされる

標準の機能として、Directory.Build.*ファイルが検索されるため、各プロジェクトファイル内にインポートするための記述をする必要はありません。そのため、新しいプロジェクトを追加した場合でも、開発者による追加作業の忘れということが発生しません。

前者のDirectory.Build.propsファイルは、プロジェクト ファイルの先頭でインポートされます。そのため、すべてのプロジェクト共通の既定値を設定するのに適しています。各プロジェクトファイル内では、共通の既定値をそのまま使うこともできますが、上書きすることもできます。

後者のDirectory.Build.targetsファイルは、プロジェクト ファイルの最後でインポートされます。プロジェクトファイルで設定した値を参照して、条件分岐したり、各種設定を共通設定で上書きすることができます。

TargetFrameworkを一括して設定する

ソリューションの全プロジェクトのTargetFrameworkの既定値を設定したいのであれば、冒頭で掲示した設定ファイルを、Directory.Build.propsのファイル名で、全プロジェクトの親となるフォルダーに配置します。すると、各プロジェクトで、そのファイルがインポートされるようになります。そして、各プロジェクトファイル内のTargetFrameworkの設定を削除します。

こうすることで、Directory.Build.propsファイルで、一括してTargetFrameworkの値を設定することができます。

もし、特定のプロジェクトのみ、別のTargetFrameworkの値を使いたい場合は、そのプロジェクトファイル内で、TargetFrameworkの値を上書きすれば可能です。なぜなら、Directory.Build.propsファイルは、各プロジェクトファイルの先頭でインポートされるため、その後に、同じプロパティをプロジェクトファイル内で再設定すれば、その値が有効になるからです。

Directory.Build.*ファイルをインポートされると親フォルダーの検索は中止される

プロジェクトファイルの先頭や末尾でプロジェクトの親フォルダーにあるDirectory.Build.propsファイルやDirectory.Build.targetsファイルが自動的にインポートされます。注意が必要なのは、一度でも、Directory.Build.*ファイルがインポートされると、そこで、親フォルダーの検索は中止されることです。

すべての親フォルダーのDirectory.Build.*ファイルをインポートする

もし、インポートされた後でも、親フォルダーの検索を継続させることも可能です。この場合は、配置するDirectory.Build.{propes/targets}ファイル内に、親フォルダーのDirectory.Build.{propes/targets}ファイルを検索する処理を追加します。

上位のフォルダーのDirectory.Build.{propes/targets}ファイルも有効にしたい場合は以下のようにします。

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <ParentBuildPropsFilePath>$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))</ParentBuildPropsFilePath>
  </PropertyGroup>
  <Import Project="$(ParentBuildPropsFilePath)" Condition="'' != '$(ParentBuildPropsFilePath)'" />
	
  <PropertyGroup>
    <TargetFramework>net7.0-windows</TargetFramework>
  </PropertyGroup>
</Project>

4行目では、ParentBuildPropsFilePathプロパティを設定しています。

GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../')は、親フォルダーにあるDirectory.Build.propsファイルを探し、見つかれば、そのパスを返します。見つからなければ、その親フォルダーを探し、最終的には、ドライブのルートフォルダーまで順番に探します。ドライブのルートフォルダーまで探しても見つからなければ、空文字を返します。

ParentBuildPropsFilePathプロパティには、GetPathOfFileAbove()の結果が設定されます。

6行目では、ParentBuildPropsFilePathプロパティが空文字でなければ、そのファイルをImportします。

このように、Directory.Build.{props/targets}ファイルの先頭で、上記のように親フォルダーの設定ファイルをインポートすることにより、すべての親フォルダーにあるDirectory.Build.{props/targets}ファイルをインポートすることが可能となります。

プロジェクトファイルの種類で限定

Directory.Build.{props/targets}ファイルは、C#のプロジェクトファイルだけでなく、C++のプロジェクトファイルなどでも読み込まれます。

そのため、ソリューションにC#のプロジェクトだけでなく、C++のプロジェクトも含まれる場合は、場合によっては、特定のプロジェクトタイプに限定して、既定値を設定する必要があります。

特定のプロジェクトタイプに限定して、プロパティの既定値を設定する場合は、以下のようにします。

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">    	
  <PropertyGroup Condition="$(MSBuildProjectExtension) == '.csproj'">
    <TargetFramework>net7.0-windows</TargetFramework>
  </PropertyGroup>
  <PropertyGroup Condition="$(MSBuildProjectExtension) == '.vcxproj'">
    <TargetFramework>net7.0</TargetFramework>
  </PropertyGroup>
</Project>

PropertyGroupを定義するときに、プロジェクトファイルの拡張子の条件を追加して、設定を分けています。

上記の例の場合は、C#のプロジェクトのときは、TargetFrameworknet7.0-windowsを設定し、C++のプロジェクトのときは、TargetFrameworknet7.0を設定しています。

Directory.Build.targetsの使用例

Directory.Build.targetsをプロジェクト全体の共通値を設定する例としては以下が考えられます。

複数のプロジェクトで、同じnugetパッケージをパッケージ参照しているとします。この時、ソリューション内のすべてプロジェクトで使用しているパッケージのバージョンを同じに保ちたいとします。

Directory.Build.targetsファイルを使わない場合は、各プロジェクトファイル内に書かれたパッケージのバージョンを同じ値に設定するしかありません。

しかし、Directory.Build.targetsファイルを使うと、共通の設定として、バージョン指定を一ヶ所で設定できます。

パッケージのバージョン指定は、プロジェクトファイル側でパッケージ参照が設定された後、バージョン部分を更新するようにします。そのため、プロジェクトファイルの先頭でインポートされるDirectory.Build.propsでは実現できず、プロジェクトファイルの最後でインポートされるDirectory.Build.targetsを使用する必要があります。

対象のパッケージをMicrosoft.Xaml.Behaviors.Wpfであるととして、具体的な設定例で確認します。

まず、各プロジェクトファイル側では、PackageReferenceVersion属性は指定せずInclude属性のパッケージ名のみを設定します。

実際にはプロジェクトファイル側のPackageReferenceVersion属性を設定したとしても、最終的にはDirectory.Build.targets側でVersionn属性を上書きするので影響はありません。しかし、バージョンを指定しているのに、反映されないという誤解を発生させないためにも、プロジェクトファイル側ではVersion属性を設定しないことを勧めます。

<Project Sdk="Microsoft.NET.Sdk">
...
  <ItemGroup>
    <PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" />
  </ItemGroup>
</Project>

Directory.Build.targets側では、PackageReferenceのパッケージバージョンであるVersion属性を更新します。ここではPackageReferenceInclude属性を指定するのではなく、Update属性でパッケージ名を指定します。Include属性でパッケージ名を指定すると、すべてのプロジェクトで、無条件に指定したパッケージを参照することになります。Update属性でパッケージ名を指定した場合は、プロジェクト側でパッケージを参照している(Include属性が指定されている)ときのみ、パッケージのバージョンを指定することになります。

<?xml version="1.0" encoding="utf-8"?>
<Project ...>
...
  <ItemGroup>
    <PackageReference Update="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
  </ItemGroup>
</Project>

今回の投稿では、Visual StudioやMSBuildで、複数のプロジェクトにまたがる共通設定を実現する方法の説明をしました。

コメントを残す