Skip to content

Commit 519c0ee

Browse files
committed
Create 2026-03-03-directory-build-props-and-directory-build-targets.md
1 parent 0c137bc commit 519c0ee

1 file changed

Lines changed: 193 additions & 0 deletions

File tree

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
---
2+
layout: post
3+
title: "Taming Big .NET Solutions: Centralizing Build Settings and NuGet Versions"
4+
description: "Learn how to simplify large .NET solutions by centralizing build settings, metadata, and NuGet package versions using Directory.Build.props, Directory.Build.targets, and Directory.Packages.props. "
5+
date: 2026-03-03 23:59
6+
author: Robert Muehsig
7+
tags: [NuGet, .NET, MSBuild]
8+
language: en
9+
---
10+
11+
{% include JB/setup %}
12+
13+
Many .NET projects start as a small prototype, but after a few iterations they grow into a sizable Visual Studio solution with dozens — sometimes hundreds — of projects.
14+
At this point, managing common project settings becomes painful:
15+
16+
A NuGet package needs an update → you touch multiple project files.
17+
A new .NET version is released → update every project manually.
18+
Metadata like company name or product name starts to drift across projects.
19+
20+
If this sounds familiar, this guide is for you.
21+
22+
## The Root of the Problem: The .csproj File
23+
24+
```
25+
<Project Sdk="Microsoft.NET.Sdk.Web">
26+
<PropertyGroup>
27+
<LangVersion>13.0</LangVersion>
28+
<TargetFramework>net8.0</TargetFramework>
29+
<Nullable>enable</Nullable>
30+
<ImplicitUsings>enable</ImplicitUsings>
31+
<AssemblyVersion>1.0.0.0</AssemblyVersion>
32+
<FileVersion>1.0.0.0</FileVersion>
33+
<InformationalVersion>1.0.0.0</InformationalVersion>
34+
<Company>Cool Company</Company>
35+
<Product>Cool Product</Product>
36+
<Copyright>...</Copyright>
37+
</PropertyGroup>
38+
39+
<ItemGroup>
40+
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
41+
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
42+
</ItemGroup>
43+
</Project>
44+
```
45+
46+
Common pain points:
47+
48+
- Hard-coded compiler settings (target framework, language version, nullable settings).
49+
- Metadata drift (company, product, copyright).
50+
- NuGet versions spread across all projects.
51+
52+
Luckily, .NET provides clean solutions using:
53+
**Directory.Build.props**, **Directory.Build.targets**, and **Directory.Packages.props**.
54+
55+
## Directory.Build.props
56+
57+
This file is loaded before the build starts and allows you to define default settings for all projects in the solution.
58+
Values can still be overridden inside individual .csproj files.
59+
60+
Example:
61+
62+
```
63+
<Project>
64+
<PropertyGroup>
65+
<LangVersion>latest</LangVersion>
66+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
67+
<Nullable>enable</Nullable>
68+
<AssemblyVersion>1.0.0.0</AssemblyVersion>
69+
<FileVersion>1.0.0.0</FileVersion>
70+
<InformationalVersion>1.0.0.0</InformationalVersion>
71+
<Company>Cool Company</Company>
72+
<Product>Cool Product</Product>
73+
<Copyright>...</Copyright>
74+
</PropertyGroup>
75+
<!-- or if you want to set a global output directory -->
76+
77+
<PropertyGroup>
78+
<OutDir>C:\output\$(MSBuildProjectName)</OutDir>
79+
</PropertyGroup>
80+
81+
</Project>
82+
```
83+
84+
**How MSBuild Locates Directory.Build.props**
85+
86+
MSBuild [searches the directory tree upwards](https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-by-directory) from the project location until it finds a file with that name.
87+
It stops at the first match, unless you explicitly chain them, which can be useful - depending on your directory structure.
88+
89+
## Directory.Build.targets
90+
91+
While .props files provide early defaults, .targets files are loaded later, making them ideal for:
92+
93+
- Extending build steps
94+
- Overriding MSBuild targets
95+
- Inserting Before*/After* hooks
96+
- Running custom build logic
97+
98+
Example:
99+
100+
```
101+
<Target Name="BeforePack">
102+
<Message Text="Preparing NuGet packaging for $(MSBuildProjectName)" />
103+
</Target>
104+
```
105+
106+
**Real-world Example: Muting Warnings:**
107+
108+
When you build older branches of a big solution, NuGet may raise vulnerability warnings (NU1901–NU1904).
109+
If the solution uses TreatWarningsAsErrors=true, the build will fail — even though the old state is intentional.
110+
Solution using Directory.Build.targets:
111+
112+
```
113+
<Project>
114+
<PropertyGroup>
115+
<!-- Mute all vulnerabilities and don't escalate warnings as errors -->
116+
<!-- Uncomment these lines if you need to build an older version (with open vulnerabilities!):
117+
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
118+
<NoWarn>NU1901, NU1902, NU1903, NU1904</NoWarn>
119+
-->
120+
</PropertyGroup>
121+
</Project>
122+
```
123+
124+
## Directory.Packages.props
125+
126+
This file might be the most important - this enabled [Central NuGet Package Management](https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management).
127+
128+
The idea is, that you add all your needed NuGet packages in a `Directory.Packages.props` file like this:
129+
130+
```
131+
<Project>
132+
<PropertyGroup>
133+
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
134+
</PropertyGroup>
135+
136+
<ItemGroup>
137+
<PackageVersion Include="Newtonsoft.Json" Version="13.0.1" />
138+
</ItemGroup>
139+
</Project>
140+
```
141+
142+
Inside your actual `myProject.csproj` you just reference the package, but **without the actual version number**!
143+
144+
```
145+
...
146+
147+
<ItemGroup>
148+
<PackageReference Include="Newtonsoft.Json" />
149+
</ItemGroup>
150+
```
151+
152+
This way you have one central file to update your NuGet package and don't need to scan all existing `.csproj`-files. This way, you update a package version **once**, and every project in your solution automatically picks it up.
153+
154+
**Note:** You can add multiple `Directory.Packages.props` in your directory tree.
155+
156+
**How to deal with exceptions - e.g. one project needs an older/newer package?**
157+
158+
If you have this scenario, you always can define an `VersionOverride` inside your `.csproj` like this:
159+
160+
```
161+
<PackageReference Include="Microsoft.Extensions.Logging" VersionOverride="9.0.6" />
162+
```
163+
164+
## Tooling
165+
166+
The Visual Studio UI is work with the `Directory.Packages.props` as well. If you want to start, just try these `.NET CLI` helper:
167+
168+
```
169+
dotnet new buildprops
170+
dotnet new buildtargets
171+
dotnet new packagesprops
172+
```
173+
174+
There is also a [tool](https://github.com/Webreaper/CentralisedPackageConverter) to convert an existing solution automatically using:
175+
176+
```
177+
dotnet tool install CentralisedPackageConverter --global
178+
central-pkg-converter /SomeAwesomeProject
179+
```
180+
181+
From my experience: In our complex solution it worked `ok-ish`. 80% was migrated, not sure what the problem was. I guess nowadays you can assign an AI to write this file :)
182+
183+
## Summary
184+
185+
With just three small files, even a complex multi-project .NET solution becomes cleaner and easier to maintain:
186+
187+
- **Directory.Build.props:** Defines shared defaults like language version, company name, or build settings.
188+
- **Directory.Build.targets:** Extends and customizes the build pipeline — ideal for automation and global rules.
189+
- **Directory.Packages.props:** Centralizes NuGet version management and prevents version drift across projects.
190+
191+
The migration effort is small, but the payoff in structure and maintainability is huge.
192+
193+
Hope this helps!

0 commit comments

Comments
 (0)