This issue is made possible thanks to JetBrains, who help keep this newsletter free for everyone. A huge shout-out to them for their support of our community. Let's thank them by entering the link below. Struggling with slow builds, tricky bugs, or hard-to-understand performance issues? dotUltimate fixes all of that. It’s the all-in-one toolbox for serious .NET developers. 👉 Upgrade your .NET workflow.
Every .NET developer eventually hits the same wall:
You tweak one setting…and then spend the next 15 minutes hunting it down across the solution.
In .NET 9 (and any modern SDK-style project), there’s a better way: Directory.Build.props.
With a single file, you can:
In this article, we’ll walk through:
Gotchas and best practices
Directory.Build.props is an MSBuild file that MSBuild automatically imports before your project file. Any properties and items you define there will be applied to all projects in that directory and its subdirectories.
The import process works like this:
That means:
This works the same in .NET 9 as in previous modern SDK versions—the big difference is that .NET 9 projects typically lean even more on analyzers, nullable, and modern build features, which makes centralization even more valuable.
Imagine you’re working on a real solution that looks like this:
src/ Api/ Api.csproj Worker/ Worker.csproj Web/ Web.csprojtests/ Api.Tests/ Api.Tests.csproj Worker.Tests/ Worker.Tests.csproj Shared.Testing/ Shared.Testing.csprojDirectory.Build.propstests/Directory.Build.props
Typical problems:
<TargetFramework>net9.0</TargetFramework><Nullable>enable</Nullable><ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors><AnalysisLevel>latest</AnalysisLevel>
At the solution root, create a file named Directory.Build.props:
<Project> <!-- Shared configuration for all .NET 9 projects in the repo --> <PropertyGroup> <!-- Targeting --> <TargetFramework>net9.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <LangVersion>latest</LangVersion> <!-- Code quality --> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <AnalysisLevel>latest</AnalysisLevel> <!-- Assembly metadata --> <Company>TheCodeMan</Company> <Authors>Stefan Djokic</Authors> <RepositoryUrl>https://github.com/thecodeman/your-repo</RepositoryUrl> <RepositoryType>git</RepositoryType> <!-- Output layout --> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath> <BaseOutputPath>artifacts\bin\</BaseOutputPath> <BaseIntermediateOutputPath>artifacts\obj\</BaseIntermediateOutputPath> </PropertyGroup> <!-- Packages shared by most projects --> <ItemGroup> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" /> </ItemGroup> </Project>
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <AssemblyName>MyCompany.Api</AssemblyName> </PropertyGroup></Project>
Cleaner, easier to review, and harder to misconfigure. Note: For package management itself (centralizing versions), you’ll often pair this with Directory.Packages.props in modern .NET. Directory.Build.props is primarily for build configuration, not for central package versioning - though it can hold PackageReferences if you need defaults.
Now let’s treat tests differently: they often need extra packages and slightly relaxed rules. Create tests/Directory.Build.props:
<Project> <!-- This file is imported AFTER the root Directory.Build.props for test projects --> <PropertyGroup> <!-- Optional: tests may not treat all warnings as errors --> <TreatWarningsAsErrors>false</TreatWarningsAsErrors> <!-- Mark these assemblies as test assemblies --> <IsTestProject>true</IsTestProject> </PropertyGroup> <ItemGroup> <!-- Shared test libraries --> <PackageReference Include="xunit" Version="2.9.0" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" /> <PackageReference Include="FluentAssertions" Version="6.12.0" /> <PackageReference Include="coverlet.collector" Version="6.0.0"> <PrivateAssets>all</PrivateAssets> </PackageReference> </ItemGroup> </Project>
Now:
Another powerful use case is central assembly versioning. Instead of repeating version info in each project, you can define it once and push CI metadata through MSBuild properties. Extend the root Directory.Build.props:
<Project> <PropertyGroup> <!-- Base semantic version for the whole solution --> <VersionPrefix>1.4.0</VersionPrefix> <!-- Optional manually-set suffix for prereleases --> <VersionSuffix>beta</VersionSuffix> <!-- e.g. "", "beta", "rc1" --> <!-- Assembly versions --> <AssemblyVersion>1.4.0.0</AssemblyVersion> <!-- FileVersion may include build number from CI --> <FileVersion>1.4.0.$(BuildNumber)</FileVersion> <!-- InformationalVersion is what you see in "Product version" and NuGet --> <InformationalVersion>$(VersionPrefix)-$(VersionSuffix)+build.$(BuildNumber)</InformationalVersion> </PropertyGroup></Project>
In CI (GitHub Actions / Azure DevOps / GitLab), you pass BuildNumber:
dotnet build MySolution.sln /p:BuildNumber=123
Result:
You’re already using .editorconfig (I know you are 😄). But analyzers and build-level enforcement still live in MSBuild. Directory.Build.props is a perfect place to wire that up:
<Project> <PropertyGroup> <!-- Enforce analyzers globally --> <AnalysisLevel>latest</AnalysisLevel> <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> <!-- Treat all analyzer diagnostics as errors (unless overridden) --> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> <ItemGroup> <!-- Example analyzer packages --> <PackageReference Include="Roslynator.Analyzers" Version="4.12.0" PrivateAssets="all" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="all" /> </ItemGroup> </Project>
Now:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <!-- opt-out from global warnings-as-errors for this project --> <TreatWarningsAsErrors>false</TreatWarningsAsErrors> </PropertyGroup></Project>
You’re not limited to just one props file. MSBuild lets you create a hierarchy of Directory.Build.props files, and it will pick the first one it finds while walking up directories from the project location. Common patterns:
csharp
Directory.Build.props // global defaults
src/Directory.Build.props // overrides for production code
tests/Directory.Build.props // overrides for test code
tools/Directory.Build.props // overrides for tiny internal utilities
A src/Directory.Build.props might look like:<Project> <PropertyGroup> <!-- Only for production code --> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <GenerateDocumentationFile>true</GenerateDocumentationFile> <NoWarn>CS1591</NoWarn> <!-- but don't require XML docs everywhere --> </PropertyGroup></Project>
And if you really need a project not to inherit any root-level props, you can create a dummy local Directory.Build.props in that project folder:
<Project> <!-- intentionally empty to stop inheritance from parent props --></Project>
This works because MSBuild stops at the first Directory.Build.props it finds when walking upwards from the project’s directory.
Another file you’ll see mentioned in the docs is Directory.Build.targets. The short version:
Directory.Build.props is one of those features that quietly solves a lot of pain:
Once you adopt it, adding a new project becomes almost trivial - no more copying settings from some “template” project or forgetting to turn on nullable or analyzers.
If you’re already battling with a big solution today:
Want to enforce clean code automatically? My Pragmatic .NET Code Rules course shows you how to set up analyzers, CI quality gates, and architecture tests - a production-ready system that keeps your codebase clean without manual reviews. Or grab the free Starter Kit to try it out.
Stop arguing about code style. In this course you get a production-proven setup with analyzers, CI quality gates, and architecture tests — the exact system I use in real projects. Join here.
Not sure yet? Grab the free Starter Kit — a drop-in setup with the essentials from Module 01.
Design Patterns that Deliver — Solve real problems with 5 battle-tested patterns (Builder, Decorator, Strategy, Adapter, Mediator) using practical, real-world examples. Trusted by 650+ developers.
Just getting started? Design Patterns Simplified covers 10 essential patterns in a beginner-friendly, 30-page guide for just $9.95.
Every Monday morning, I share 1 actionable tip on C#, .NET & Architecture that you can use right away. Join here.
Join 20,000+ subscribers who mass-improve their .NET skills with actionable tips on C#, Software Architecture & Best Practices.
Subscribe to the TheCodeMan.net and be among the 20,000+ subscribers gaining practical tips and resources to enhance your .NET expertise.