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"/>
のように、その共通設定ファイルを直接インポートします。これにより、同じ設定を各プロジェクトファイルに反映することができます。
しかし、各プロジェクトのフォルダー構成が、フラットの場合(すべてのプロジェクトが同じ親フォルダーを持つ場合)は、Import
のInclude
のパス指定は、すべてのプロジェクトファイルで同じ"../Cumsom.props"
でよいのでシンプルです。
しかし、各プロジェクトのフォルダ構成が、フラットではなく、サブフォルダーやサブサブフォルダーにあったりするような複雑な構成の場合、Import
のInclude
のパス指定がプロジェクトごとに異なることになり、複雑になります。
また、新規に作成したプロジェクトに<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#のプロジェクトのときは、TargetFramework
にnet7.0-windows
を設定し、C++のプロジェクトのときは、TargetFramework
にnet7.0
を設定しています。
Directory.Build.targetsの使用例
Directory.Build.targets
をプロジェクト全体の共通値を設定する例としては以下が考えられます。
複数のプロジェクトで、同じnugetパッケージをパッケージ参照しているとします。この時、ソリューション内のすべてプロジェクトで使用しているパッケージのバージョンを同じに保ちたいとします。
Directory.Build.targets
ファイルを使わない場合は、各プロジェクトファイル内に書かれたパッケージのバージョンを同じ値に設定するしかありません。
しかし、Directory.Build.targets
ファイルを使うと、共通の設定として、バージョン指定を一ヶ所で設定できます。
パッケージのバージョン指定は、プロジェクトファイル側でパッケージ参照が設定された後、バージョン部分を更新するようにします。そのため、プロジェクトファイルの先頭でインポートされるDirectory.Build.props
では実現できず、プロジェクトファイルの最後でインポートされるDirectory.Build.targets
を使用する必要があります。
対象のパッケージをMicrosoft.Xaml.Behaviors.Wpf
であるととして、具体的な設定例で確認します。
まず、各プロジェクトファイル側では、PackageReference
でVersion
属性は指定せずInclude
属性のパッケージ名のみを設定します。
実際にはプロジェクトファイル側のPackageReference
でVersion
属性を設定したとしても、最終的にはDirectory.Build.targets
側でVersionn
属性を上書きするので影響はありません。しかし、バージョンを指定しているのに、反映されないという誤解を発生させないためにも、プロジェクトファイル側ではVersion
属性を設定しないことを勧めます。
<Project Sdk="Microsoft.NET.Sdk">
...
<ItemGroup>
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" />
</ItemGroup>
</Project>
Directory.Build.targets
側では、PackageReference
のパッケージバージョンであるVersion
属性を更新します。ここではPackageReference
でInclude
属性を指定するのではなく、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で、複数のプロジェクトにまたがる共通設定を実現する方法の説明をしました。