Directory.Build.props Patterns for .NET Solutions

Every .csproj in your solution probably repeats the same settings: target framework, nullable reference types, implicit usings, maybe a company name. Directory.Build.props lets you define these once and have them apply to every project in the directory tree.

How It Works

MSBuild automatically imports Directory.Build.props from the current directory and every parent directory up to the repository root. It's imported before the project file, so project-level settings can override it.

Create a Directory.Build.props in your repository root:

Directory.Build.props
<Project>
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>
</Project>

Now every project in the solution inherits these settings. Individual projects can still override them if needed.

Enforcing Code Quality

Use Directory.Build.props to enable analysers and enforce coding standards across all projects:

Directory.Build.props
<Project>
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
    <AnalysisLevel>latest-recommended</AnalysisLevel>
  </PropertyGroup>
</Project>

EnforceCodeStyleInBuild ensures .editorconfig rules are enforced during builds, not just in the IDE. AnalysisLevel controls which set of analysers are active.

Separating Source and Test Projects

A common pattern is to have different settings for source projects and test projects. Use nested Directory.Build.props files:

Root Directory.Build.props — shared settings:

Directory.Build.props
<Project>
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

tests/Directory.Build.props — test-specific settings:

tests/Directory.Build.props
<Project>
  <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />

  <PropertyGroup>
    <IsPackable>false</IsPackable>
    <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" />
    <PackageReference Include="xunit" />
    <PackageReference Include="xunit.runner.visualstudio" />
    <PackageReference Include="coverlet.collector" />
  </ItemGroup>
</Project>

The Import line is critical — it tells MSBuild to also import the parent Directory.Build.props. Without it, the nested file completely replaces the parent.

Test projects automatically get the testing packages without listing them individually, and IsPackable is set to false to prevent accidentally publishing test projects as NuGet packages.

Package Metadata for Libraries

If you're publishing NuGet packages, centralise the metadata:

config.xml
<Project>
  <PropertyGroup>
    <Authors>Your Company</Authors>
    <Company>Your Company</Company>
    <PackageLicenseExpression>MIT</PackageLicenseExpression>
    <RepositoryUrl>https://github.com/yourcompany/yourrepo</RepositoryUrl>
    <RepositoryType>git</RepositoryType>
    <Copyright>Copyright © Your Company 2026</Copyright>
  </PropertyGroup>
</Project>

Every packable project in the solution automatically gets consistent metadata.

Conditional Properties

You can set properties conditionally based on configuration or other properties:

config.xml
<Project>
  <PropertyGroup Condition="'$(Configuration)' == 'Release'">
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>

  <PropertyGroup Condition="'$(Configuration)' == 'Debug'">
    <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
  </PropertyGroup>
</Project>

This is useful for allowing warnings during development but failing the build in CI.

Directory.Build.targets

There's a companion file, Directory.Build.targets, which is imported after the project file. Use it for targets and items that depend on project-level settings:

Directory.Build.targets
<Project>
  <Target Name="PrintBuildInfo" AfterTargets="Build">
    <Message Importance="high" Text="Built $(MSBuildProjectName) ($(TargetFramework))" />
  </Target>
</Project>

Common Patterns

A few patterns that work well in practice:

Directory.Build.props
<Project>
  <PropertyGroup>
    <!-- Lock NuGet package versions for reproducible restores -->
    <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>

    <!-- Generate documentation XML for public APIs -->
    <GenerateDocumentationFile>true</GenerateDocumentationFile>

    <!-- Enable deterministic builds -->
    <Deterministic>true</Deterministic>
    <ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
  </PropertyGroup>
</Project>

The ContinuousIntegrationBuild property normalises file paths in PDBs, which is essential for SourceLink and reproducible builds. The condition ensures it's only set in CI environments.

Summary

Directory.Build.props is one of the most underused features in the .NET build system. It eliminates duplication, enforces consistency, and makes solution-wide changes trivial. Start with the basics — target framework, nullable, and warnings as errors — then layer on analysers, package metadata, and conditional properties as your solution grows.