When working on large enterprise scale projects .NET Solutions can often become unwieldy and difficult to maintain. This is particularly true of .csproj files which end up repeating configuration across all projects. How can one file save you hours of maintenance by keeping project configuration DRY?
A Directory.Build.props file is an MSBuild file used in .NET projects to define common properties and configurations that apply to multiple projects within a directory tree. This file helps centralize the configuration and reduce redundancy by allowing you to specify settings that will be inherited by all projects under the directory where the file is located.
This can be used for:
Project1.csproj:
<Project Sdk="Microsoft.NET.Sdk.Web"><PropertyGroup><TargetFramework>net10.0</TargetFramework><Nullable>enable</Nullable><ImplicitUsings>enable</ImplicitUsings><!-- Configure code analysis. --><AnalysisLevel>latest</AnalysisLevel><AnalysisMode>Recommended</AnalysisMode><TreatWarningsAsErrors Condition="'$(Configuration)' == 'Release'">true</TreatWarningsAsErrors><CodeAnalysisTreatWarningsAsErrors Condition="'$(Configuration)' == 'Release'">true</CodeAnalysisTreatWarningsAsErrors><EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild></PropertyGroup></Project>
Project2.csproj:
<Project Sdk="Microsoft.NET.Sdk.Web"><PropertyGroup><TargetFramework>net10.0</TargetFramework><Nullable>enable</Nullable><ImplicitUsings>enable</ImplicitUsings><!-- Configure code analysis. --><AnalysisLevel>latest</AnalysisLevel><AnalysisMode>Recommended</AnalysisMode><TreatWarningsAsErrors Condition="'$(Configuration)' == 'Release'">true</TreatWarningsAsErrors><CodeAnalysisTreatWarningsAsErrors Condition="'$(Configuration)' == 'Release'">true</CodeAnalysisTreatWarningsAsErrors><EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild></PropertyGroup></Project>
❌ Figure: Bad example - Redundant configuration
**✅ Project1.csproj:
<Project Sdk="Microsoft.NET.Sdk.Web"></Project>
Project2.csproj:
<Project Sdk="Microsoft.NET.Sdk.Web"></Project>
Directory.Build.props:
<Project><PropertyGroup><TargetFramework>net10.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable><!-- Configure code analysis. --><AnalysisLevel>latest</AnalysisLevel><AnalysisMode>Recommended</AnalysisMode><TreatWarningsAsErrors Condition="'$(Configuration)' == 'Release'">true</TreatWarningsAsErrors><CodeAnalysisTreatWarningsAsErrors Condition="'$(Configuration)' == 'Release'">true</CodeAnalysisTreatWarningsAsErrors><EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild></PropertyGroup></Project>```****✅ Good example - Centralized configuration**## What about Directory.Build.targets?MSBuild auto-imports a second file, `Directory.Build.targets`, and the difference between the 2 is **when** they are imported:* `Directory.Build.props` is imported **early** - before the project's own content. Perfect for **defaults** that individual projects may still override* `Directory.Build.targets` is imported **late** - after the project's content and the SDK targets. Perfect for **overrides** and shared build logic that must react to (or win over) whatever a project setsRule of thumb: **defaults go in .props, overrides and custom targets go in .targets**.```xml<Project><!-- Reacts to a property each project sets in its own body,which is only reliable in .targets (imported after the project) --><PropertyGroup Condition="'$(IsPackable)' == 'true'"><PackageOutputPath>$(MSBuildThisFileDirectory)artifacts\packages</PackageOutputPath></PropertyGroup><!-- Custom build step shared by every project --><Target Name="PrintBuildInfo" AfterTargets="Build"><Message Importance="high" Text="Built $(MSBuildProjectName) ($(TargetFramework))" /></Target></Project>
✅ Good example - Directory.Build.targets reacting to per-project properties and defining a shared target
Note: MSBuild only auto-imports the nearest Directory.Build.props/.targets above each project. If you need multiple levels (e.g. one at the repo root plus one for the tests folder), the inner file must import the outer one explicitly:
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
Tip: If a property doesn't have the value you expect and you can't work out which file set it, capture a binlog and search for the property - the Structured Log Viewer shows every assignment and where it came from.
Directory.Build.props centralizes build configuration. To centralize NuGet package versions the same way, use a Directory.Packages.props file - see Do you use Central Package Management?.