Centralizing Nuget Package References

A common complaint of our Pull Requests (PRs / Code Reviews) on our C# projects were the number of files changed. Most of the time these files are changes to the .csproj or manifest/data files. It would be nice if the dependencies were in one central manifest like requirements.txt for Python or like packages.json for NodeJs projects or go.mod for Golang.
Looks like C# supports Central Package Management, but requires a one time change to our .csproj files and a Directory.Packages.props. I took some time to write a converter in PowerShell:
Looks like Jeroen Vannevel (Vannevelj) already wrote one with directory-packages-props-converter. Give him a ⭐. It’s written in Rust so it should be faster, but for our smaller repos, my script will suffice. Also save’s downloading a binary or separate compilation.
For centralizing properties, use the Directory.Build.props.
Learnings
Note that if you’re building form a Linux system, make sure that the proper casing is used for Directory.Build.props and Directory.Packages.props, otherwise your properties won’t get detected since the file is not loaded.
This can be debugged with:
dotnet msbuild .\YourMainProject.csproj -pp:output.xml
This command will preprocesses your project file and all its imported .props and .targets files into a single, combined XML file. This process does not perform a full build. Instead, it expands all variables, conditions, and imported properties to show you exactly how MSBuild will interpret your project before any compilation begins.
It can be also helpful to list all transitive packages with:
dotnet list package --include-transitive
This came up since we had transitive dependencies that didn’t get resolved which is why CentralPackageTransitivePinningEnabled is set to true:
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
Update
The script has been updated to support projects that use multiple frameworks (multitarget builds). Projects that support multiple target frameworks may have conditional ItemGroups like:
<ItemGroup>
<PackageReference Include="Serilog" Version="4.3.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" />
</ItemGroup>
This seems to be supported in Directory.Packages.props.
Update 11-07-2025
Now sets PrivateAssets and IncludedAssets attributes to to PackageVersion entries in the Directory.Packages.props.
<PackageVersion Include="coverlet.msbuild" Version="6.0.4" PrivateAssets="All" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" />
There is a flag for $PreserveAssets which is set to false for those who want preserve the nested elements for PrivateAssets and IncludedAssets in each .csproj:
<PackageReference Include="coverlet.msbuild">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
The PrivateAssets specifies which assets from a NuGet packages are private to the project and should not be propagated to projects that reference that project.
The IncludedAssets specifies which assets from a NuGet package should be included in that project.
These attributes/elements are set the third-party packages so we want to preserve them to avoid leaking build/test/analyzer dependencies to our consuming projects.
References
https://devblogs.microsoft.com/dotnet/introducing-central-package-management/
https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management
https://github.com/Vannevelj/directory-packages-props-converter
https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-by-directory?view=vs-2022
https://learn.microsoft.com/en-us/nuget/consume-packages/package-source-mapping
https://learn.microsoft.com/en-us/nuget/create-packages/multiple-target-frameworks-project-file




