🔥 Pragmatic .NET Code Rules Course is on Presale - 40% off!BUY NOW

Mastering Directory.Build.props in .NET

December 08 2025

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.

Introduction

Every .NET developer eventually hits the same wall:

• 20+ projects in a solution • 20+ copies of the same TargetFramework, Nullable, and LangVersion • 20+ places to flip TreatWarningsAsErrors • 20+ csproj files full of repeated PackageReference and build settings

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:

• Define global build rules for all projects in a folder tree • Keep each .csproj small and focused • Standardize code style, analyzers, and warnings • Control versioning and metadata for all assemblies from one place

In this article, we’ll walk through:

  1. What Directory.Build.props actually is, and how MSBuild discovers it
  2. A realistic multi-project solution scenario
  3. A step-by-step implementation for .NET 9
  4. Advanced patterns: scoped props, versioning, analyzers, and opt-out tricks
  5. Gotchas and best practices

    What is Directory.Build.props?

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:

  1. When MSBuild loads a project (e.g. Api.csproj), it first imports Microsoft.Common.props.
  2. Microsoft.Common.props then walks up the directory tree from the project’s folder, looking for the first Directory.Build.props.
  3. When it finds one, it imports it.
  4. Anything defined in Directory.Build.props is now available in the project file.

That means:

• Put Directory.Build.props at the solution root → every project below will inherit it. • Put another Directory.Build.props in tests/ → test projects can have extra settings on top of the root ones. • If you put a dummy Directory.Build.props next to an individual project, MSBuild will stop there and won’t keep searching upwards - effectively shielding that project from the upper-level props.

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.

A Real-World Scenario: Cleaning Up a .NET 9 Microservices Solution

Imagine you’re working on a real solution that looks like this:

C#
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: • Every project repeats:

C#
<TargetFramework>net9.0</TargetFramework><Nullable>enable</Nullable><ImplicitUsings>enable</ImplicitUsings>

• Every project copies:

C#
<TreatWarningsAsErrors>true</TreatWarningsAsErrors><AnalysisLevel>latest</AnalysisLevel>

• All projects share the same company metadata & repository URL. • Test projects repeatedly reference xunit, FluentAssertions, and coverlet.collector. If you change TargetFramework from net8.0 to net9.0 or decide to tighten analyzers, you have to change every .csproj manually. Let’s fix that with Directory.Build.props.

Step 1: Create a Solution-Level Directory.Build.props

At the solution root, create a file named Directory.Build.props:

C#
<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>

What this does

Every project now targets net9.0, uses nullable reference types, implicit usings, and the latest language features. • Code quality is enforced everywhere via TreatWarningsAsErrors and AnalysisLevel. • All binaries end up under artifacts\bin\ and artifacts\obj\ instead of being spread across bin/Debug folders. • Common PackageReferences are no longer duplicated in each project. Your individual .csproj files can now be tiny:

C#
<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.

Step 2: Add a Test-Specific Directory.Build.props

Now let’s treat tests differently: they often need extra packages and slightly relaxed rules. Create tests/Directory.Build.props:

C#
<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: • Any project under tests/ automatically gets test packages. • You can slightly relax test projects (e.g., no warnings-as-errors) without affecting production code. • New test projects become trivial to create - almost no build configuration is needed inside the .csproj

Step 3: Centralized Versioning with Directory.Build.props

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:

C#
<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:

C#
dotnet build MySolution.sln /p:BuildNumber=123

Result: • All projects share consistent versioning. • Changing VersionPrefix once upgrades the whole solution. • You can differentiate between internal Release builds and Deploy builds by controlling the properties passed from CI.

Step 4: Enforcing Code Style and Analyzers Centrally

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:

C#
<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: • Every project uses the same analyzer level and code-style enforcement. • You don’t have to remember to add analyzer packages in each .csproj. • Build breaks if someone violates rules, regardless of their IDE settings - perfect for CI and team consistency. If a particular project truly needs to relax something, you can still override locally:

C#
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup>  <!-- opt-out from global warnings-as-errors for this project --> <TreatWarningsAsErrors>false</TreatWarningsAsErrors>  </PropertyGroup></Project>

Step 5: Layering and Scoping: Multiple Directory.Build.props Files

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: • Solution root: core settings for all projects • src/Directory.Build.props: production-only settings and packages • tests/Directory.Build.props: test-only packages and relaxed rules • tools/Directory.Build.props: for small CLI tools that don’t need analyzers or warnings-as-errors Example structure:

C#
Directory.Build.props // global defaultssrc/Directory.Build.props // overrides for production codetests/Directory.Build.props // overrides for test codetools/Directory.Build.props // overrides for tiny internal utilities

A src/Directory.Build.props might look like:

C#
<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:

C#
<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.

Directory.Build.props vs Directory.Build.targets

Another file you’ll see mentioned in the docs is Directory.Build.targets. The short version: • Directory.Build.props - for properties and items, imported early in the build. Great for configuration and metadata. • Directory.Build.targets - for targets and custom build actions, imported late in the build. Great for custom build steps (e.g., running a tool after build, generating artifacts, etc.). For this article, we’re focusing on props, but it’s good to keep Directory.Build.targets in mind when you want to centralize “do this after build” logic.

Wrapping Up

Directory.Build.props is one of those features that quietly solves a lot of pain: • It keeps your .csproj files short, readable, and focused • It lets you enforce consistent rules across your .NET 9 solution • It centralizes versioning and metadata • It gives you a clean way to differentiate between src/, tests/, and other areas

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:

  1. Add a Directory.Build.props at the root.
  2. Move TargetFramework, Nullable, ImplicitUsings, and analyzer settings into it.
  3. Add a tests/Directory.Build.props for common test packages.
  4. Clean up your .csproj files until they’re boring again. Your future self (and your teammates) will thank you. That's all from me for 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.

About the Author

Stefan Djokic is a Microsoft MVP and senior .NET engineer with extensive experience designing enterprise-grade systems and teaching architectural best practices.

There are 3 ways I can help you:

1. Pragmatic .NET Code Rules Course

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.

2. Design Patterns Ebooks

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.

3. Join 20,000+ subscribers

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#, Architecture & Best Practices.

Subscribe to
TheCodeMan.net

Subscribe to the TheCodeMan.net and be among the 20,000+ subscribers gaining practical tips and resources to enhance your .NET expertise.