Over the past few months people have been asking me a fairly frequent question. Which to be be fair is the same question that I had asked before I found out about it.
So some background, maybe helpful to understanding the need for the Directory.Build.props
file. In previous versions of the .net framework when splitting out your solution into a number of projects, which may be typical when implementing a layered a layered architecture approach. Things would often get messy managing the version of the framework, Root namespace setting or even the version of the C# language was used throughout all the projects. All these settings were managed within the individual project settings and if you wanted to change them you had to remember to go change them everywhere.
This would become increasingly more frustrating the more projects that we added to the solution and also when adding more common properties. Lets say, Code style rules or even code performance inspections etc.
Fortunately when MsBuild 15 was integrated with the .Net Core SDK enabling you to build .NET Core projects on Windows, macOS, and Linux. Quite a number of great MsBuild features were enabled too.
Directory.Build.props
With introduction of MsBuild we can now manage all these property settings and more in a single file that we can place in the Root of our solution directory along our solution file and it will be applied to all projects defined in the solution automatically.
A very simple example of how to use the Directory.Build.props file can be found in my ApiResponse library
In this project I simply use the Directory.Build.props to manage the common the settings required in my projects.
<Project> <PropertyGroup> <Title>API Response</Title> <Authors>Gary Woodfine</Authors> <Description>Standardised API Response</Description> <RepositoryType>git</RepositoryType> <PackageTags>api project, api endpoints, response</PackageTags> <RepositoryUrl>https://github.com/threenine/apiResponse</RepositoryUrl> <PackageProjectUrl>https://threenine.co.uk</PackageProjectUrl> <TargetFramework>net6.0</TargetFramework> <LangVersion>10</LangVersion> <RootNamespace>Threenine</RootNamespace> </PropertyGroup> </Project>
Essentially all I am doing in this file is defining and providing values for some common properties. This a very trivial example but it does serve to illustrate how multiple properties can be shared across multiple projects and managed centrally in one file.
The core aspects being here is that not only can we manage common administrivia of our project as In Description, RepositoryUrl and and type. But also we manage our TargetFramework and C# Lanaguage version. So we only have to set and update these settings in one place and it will update it across the multiple projects in our Directory.
I'll have to be honest, I've never really been a massive user of MsBuild primarily because I don't use Visual Studio or Team Foundation Services and I may have been previously mistaken that the functionality of MsBuild was primarily focused on those use cases.
More advanced Directory.Build.props
The use case defined above is a really trivial example that barely scratches the surface of the power that can be utilised by this file, even though it does address one of the most common use cases it is used in.
An example here, say now I want to configure a Code Style enforcement on all my projects I could simply configure my StyleCop Rules etc in one place and have that enforced and replicated across all projects in my solution as follows.
<Project> <PropertyGroup> <Title>API Response</Title> <Authors>Gary Woodfine</Authors> <Description>Standardised API Response</Description> <RepositoryType>git</RepositoryType> <PackageTags>api project, api endpoints, response</PackageTags> <RepositoryUrl>https://github.com/threenine/apiResponse</RepositoryUrl> <PackageProjectUrl>https://threenine.co.uk</PackageProjectUrl> <TargetFramework>net6.0</TargetFramework> <LangVersion>10</LangVersion> <RootNamespace>Threenine</RootNamespace> <CodeAnalysisRuleset>$(MSBuildThisFileDirectory)MyRules.ruleset</CodeAnalysisRuleset> </PropertyGroup> <ItemGroup> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" /> <AdditionalFiles Include="$(MSBuildThisFileDirectory)stylecop.json" /> </ItemGroup> </Project>
You'll notice we can make use of of some handy MsBuild variables to define exactly where the file is we want to use.
MSBuild Reserved and well-known properties
MSBuild provides a set of predefined properties that store information about the project file and the MSBuild binaries. These properties are evaluated in the same manner as other MSBuild properties.
For exampl as implemented above to use the MSBuildThisFileDirectory property, you type $(MSBuildThisFileDirectory)
. This property provides the directory portion MSBuildThisFileFullPath
, including the final backslash.
Using these properties saves you having to create really complicated filepath strings in your file.
A full list of these properties is available at MSBuild reserved and well-known properties
Per Directory settings
The really cool thing about the Directory.Build.props is that it is not only constrained to Solution wide settings, but you can use it to apply settings to any projects in a specific directory. So for instance, you may use a common set of testing libraries and files that you would like to use across a number of Unit and Integration tests in a directory. The good news is you can have Multiple Directory.Build.props across separate directories.
Which means you can have 1 in your Solution Root directory and then another in your tests
directory, which you would like to share your common test settings.
We can add a new Directory.Build.props
file to our tests directory but we will need to inform MsBuild to continue up a directory to the original Directory.Build.props
to basically link the files together.
We do this by adding the line to link the files together
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
Then we can add references to the common libraries we want to use.
<Project> <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" /> <ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0"/> <PackageReference Include="NBuilder" Version="6.1.0"/> <PackageReference Include="Shouldly" Version="4.0.3"/> <PackageReference Include="xunit" Version="2.4.1"/> <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.1.2"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> </ItemGroup> </Project>
Your final solution layout would look something similar to this.
garyw@woodfine:~/threenine/ApiResponse$ tree -L 3 . ├── ApiResponse.sln ├── ApiResponse.sln.DotSettings.user ├── blog.md ├── build.cake ├── CODEOWNERS ├── Directory.Build.props ├── GitVersion.yml ├── icon.png ├── LICENSE ├── README.md ├── src │ └── ApiResponse │ ├── ApiResponse.csproj │ ├── BaseResponse.cs │ ├── bin │ ├── ErrorKeyNames.cs │ ├── IListResponse.cs │ ├── ISingleResponse.cs │ ├── Link.cs │ ├── ListResponse.cs │ ├── obj │ └── SingleResponse.cs ├── tests │ ├── Directory.Build.props │ └── Unit │ ├── bin │ ├── LinkTests.cs │ ├── ListResponseTests.cs │ ├── obj │ ├── ResponseTests.cs │ ├── SingleResponseTests.cs │ └── Unit.csproj
Reuse package icon through out solution
A very typical and probably popular use of this functionality may be needing or wanting to include your organisations Icon in all your packages or libraries you're developing especially if your package is going to hosted on Nuget.org.
You can simply include an Icon for for organisation in the root of your solution then update your Directory.Build.props
as follows the make use of that icon across all packages.
<Project> <PropertyGroup> <Title>API Response</Title> <Authors>Gary Woodfine</Authors> <Description>Standardised API Response</Description> <RepositoryType>git</RepositoryType> <PackageTags>api project, api endpoints, response</PackageTags> <RepositoryUrl>https://github.com/threenine/apiResponse</RepositoryUrl> <PackageProjectUrl>https://threenine.co.uk</PackageProjectUrl> <TargetFramework>net6.0</TargetFramework> <LangVersion>10</LangVersion> <RootNamespace>Threenine</RootNamespace> <PackageIcon>icon.png</PackageIcon> </PropertyGroup> <ItemGroup> <Content Include="$(MSBuildThisFileDirectory)icon.png" Pack="true" PackagePath="/"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> </ItemGroup> </Project>
Managing package references and using statements
When working on more complex projects which usually consist of multiple project files and usually using the same package references and often files which invariably have very similar Using statements.
A common problem frequently experienced in large projects arises when you need to update library versions in all projects. In the past this would require you to update the individual project files individually with the updated version number, this may often resort in multiple project requiring updates and potentially a large Pull request to be submitted with all the changes documented. Well with Directory.Build.Props
file those days are in the past, all that is required now is an update to 1 file and the changes are reflected throughout the project.
To illustrate this we simply define our Package References as we did so in the example above and state the version number. When we want to update the version, we simply update the version here and build the project and it will update the references across all project files.
<Project> <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" /> <ItemGroup> <PackageReference Include="AutoMapper" Version="11.0.1" /> <PackageReference Include="Moq" Version="4.18.1" /> <PackageReference Include="NBuilder" Version="6.1.0" /> <PackageReference Include="Shouldly" Version="4.0.3" /> <PackageReference Include="Threenine.Data" Version="3.2.13" /> <PackageReference Include="Threenine.ApiResponse" Version="1.0.23" /> <PackageReference Include="Serilog" Version="2.11.0" /> <PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.2.2" /> </ItemGroup> </Project>
We can use a similar approach with the implicit using statements. What this helps with is that if for instance, is when possibly a namespace reference changes in a library, especially if its one of your own libraries which may periodically change during development, you only have to adjust the namespace in one place.
To enable this we simply add an additional Itemgroup as below with all the references we want to implicitly use. This avoids having to have large portions of the top of code files being polluted with using statements
<Project> <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" /> <ItemGroup> <PackageReference Include="AutoMapper" Version="11.0.1" /> <PackageReference Include="Moq" Version="4.18.1" /> <PackageReference Include="NBuilder" Version="6.1.0" /> <PackageReference Include="Shouldly" Version="4.0.3" /> <PackageReference Include="Threenine.Data" Version="3.2.13" /> <PackageReference Include="Threenine.ApiResponse" Version="1.0.23" /> <PackageReference Include="Serilog" Version="2.11.0" /> <PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.2.2" /> </ItemGroup> <ItemGroup> <Using Include="Moq" /> <Using Include="Shouldly" /> <Using Include="Serilog" /> <Using Include="FluentValidation" /> <Using Include="FizzWare.NBuilder" /> </ItemGroup> </Project>
Conclusion
Adding a single file called Directory.Build.props
in the root folder that contains your source code and solution file, is executed when MSBuild runs and Microsoft.Common.props searches your directory structure for the Directory.Build.props file. Once found it imports the file and reads the properties defined within it. Directory.Build.props is a user-defined file that provides customisation to projects under a directory.
Directory.Build.props
is imported very early in the build pipeline and can be overridden elsewhere in the project file or in imported files.
To learn how to handle Package Version numbers in a centrally managed location you may want to check out What is this Directory.Packages.props file all about
If you would like to override properties set in a specific project, use Directory.Build.targets
this file is used and discovered in the same way, but it is loaded much later in the build pipeline. So, it can override properties and targets defined in specific projects. I will most likely write a follow up post to discuss this file in the near future.
Using the Directory.Build.props file enables you to centrally manage your package settings and versions for your solution.
- What is this Directory.Packages.props file all about? - January 25, 2024
- How to add Tailwind CSS to Blazor website - November 20, 2023
- How to deploy a Blazor site to Netlify - November 17, 2023