Overview
MSBuild docs: Customize your build
The Directory.Build.props
and Directory.Build.targets
file are great for consolidating common properties, when separating projects into different folders (example: src
and test
).
Example
Using GameFinder as an example:
/
├─ Directory.Build.props
├─ Directory.Build.targets
├─ src/
│ ├─ Directory.Build.props
│ ├─ GameFinder.StoreHandlers.Steam/
│ │ ├─ GameFinder.StoreHandlers.Steam.csproj
├─ tests/
│ ├─ Directory.Build.props
│ ├─ Directory.Build.targets
│ ├─ GameFinder.StoreHandlers.Steam.Tests/
│ │ ├─ GameFinder.StoreHandlers.Steam.Tests.csproj
Top-Level
The top-level files can be used to configure properties that apply for every project:
Directory.Build.props
:
<Project>
<PropertyGroup>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.13">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<PropertyGroup>
<!-- https://github.com/meziantou/Meziantou.Analyzer/tree/main/docs/Rules -->
<!-- MA0048: File name must match type name -->
<NoWarn>MA0048</NoWarn>
</PropertyGroup>
</Project>
Directory.Build.targets
:
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<LangVersion>11</LangVersion>
<ImplicitUsings>false</ImplicitUsings>
</PropertyGroup>
</Project>
Every project will target both net6.0
and net7.0
have other properties already set for them. Every project also automatically references Meziantou.Analyzer
and the dependency can me managed from a single file, making updating much easier.
src
folder
src/Directory.Build.props
:
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
<PropertyGroup>
<Authors>erri120</Authors>
<PackageReadmeFile>docs\README.md</PackageReadmeFile>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/erri120/GameFinder</PackageProjectUrl>
<RepositoryUrl>https://github.com/erri120/GameFinder.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<ItemGroup>
<None Include="../../README.md" Pack="true" PackagePath="docs"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2022.3.1" PrivateAssets="all" />
</ItemGroup>
</Project>
Since GameFinder is a library, all projects inside the src
folder are going to be published and require package properties. These can be put into one Directory.Build.props
file. Important is <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
which references the Directory.Build.props
file from the root directory (see docs).
tests
folder
tests/Directory.Build.props
:
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
<PropertyGroup>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
<PackageReference Include="FluentAssertions" Version="6.9.0" />
<PackageReference Include="FluentAssertions.Analyzers" Version="0.17.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="19.1.5" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.2.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>
Every test project has to reference common testing packages like xUnit.net, AutoFixture and FluentAssertions. Having all of those dependencies in the same file makes it very easy to upgrade.
Individual projects
Arriving at the individual .csproj
files, these will contain unique properties for their project:
src/GameFinder.StoreHandlers.Steam/GameFinder.StoreHandlers.Steam.csproj
:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Library for finding games installed with Steam.</Description>
<PackageTags>valve steam games</PackageTags>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DocumentationFile>bin\Debug\GameFinder.StoreHandlers.Steam.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\Release\GameFinder.StoreHandlers.Steam.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ValveKeyValue" Version="0.8.2.162" />
<PackageReference Include="System.IO.Abstractions" Version="19.1.5" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Roslyn.System.IO.Abstractions.Analyzers" Version="12.2.19">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GameFinder.Common\GameFinder.Common.csproj" />
<ProjectReference Include="..\GameFinder.RegistryUtils\GameFinder.RegistryUtils.csproj" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="GameFinder.StoreHandlers.Steam.Tests" />
</ItemGroup>
</Project>
tests/GameFinder.StoreHandlers.Steam.Tests/GameFinder.StoreHandlers.Steam.Tests.csproj
:
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\..\src\GameFinder.StoreHandlers.Steam\GameFinder.StoreHandlers.Steam.csproj" />
<ProjectReference Include="..\TestUtils\TestUtils.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Usings.cs">
<Link>Usings.cs</Link>
</Compile>
</ItemGroup>
</Project>
(Having a common Usings.cs
for tests is also possible with this approach.)
Conclusion
Putting common project properties and dependencies into Directory.Build.props
and Directory.Build.targets
files can drastically remove clutter from project files and make it easier to manage dependencies. The individual project files will then only contain the unique properties, required for the project, and nothing else.