Deterministic Builds in .NET: Locking Down NuGet Versions Without Losing Your Mind
Without explicit version management, dotnet restore pulls the latest compatible version, and your build becomes a moving target. Here's how to lock down NuGet versions for reproducible builds.
11 Mar 2026, 23:46 UTC

The Reproducibility Problem
You've built a .NET application that works perfectly on your machine. You push to CI, and suddenly tests fail with cryptic JSON serialization errors. The culprit? A NuGet package that updated overnight. Without explicit version management, dotnet restore pulls the latest compatible version, and your build becomes a moving target.
The Three Levels of Version Control
NuGet offers three distinct mechanisms, each with different scope and granularity:
1. PackageReference Version Attribute
The most direct approach pins a specific version in your project file:
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
This ensures every restore uses exactly 13.0.1, regardless of what other versions exist in your dependency tree. The trade-off: you must manually update each project when you want to upgrade.
2. Directory.Packages.props (Central Package Management)
Introduced in .NET 6, this file centralizes version definitions across your entire solution:
<Project><PropertyGroup><ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally></PropertyGroup><ItemGroup><PackageVersion Include="Newtonsoft.Json" Version="13.0.1" /></ItemGroup></Project>
Then in your project files, omit the Version attribute:
<PackageReference Include="Newtonsoft.Json" />
This gives you solution-wide version control with per-project override capability when needed.
3. packages.lock.json
For complete determinism, especially with transitive dependencies, generate a lock file:
dotnet restore --locked-mode
This creates a packages.lock.json that locks every resolved version, including dependencies pulled in by your direct packages. Commit this file to source control for truly reproducible builds.
A Practical Example: Securing Your Build Pipeline
Consider a solution with three projects that all depend on Microsoft.Extensions.Logging. Without centralized management, each project might resolve different patch versions, creating inconsistent behavior.
Create Directory.Packages.props at the solution root:
<Project><PropertyGroup><ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally></PropertyGroup><ItemGroup><PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" /><PackageVersion Include="Newtonsoft.Json" Version="13.0.1" /></ItemGroup></Project>
Now each project references packages without versions:
<ItemGroup><PackageReference Include="Microsoft.Extensions.Logging" /><PackageReference Include="Newtonsoft.Json" /></ItemGroup>
To add security patches, update one file and run dotnet restore across the solution. For production releases, generate a lock file to freeze all dependencies:
dotnet restore --locked-mode
# Commit packages.lock.json to source control
Trade-offs and Limitations
Security vs. Stability: Pinning versions prevents breaking changes but delays security patches. Implement automated vulnerability scanning with tools like dotnet list package --vulnerable to identify when updates are needed.
Transitive Conflicts: When two direct dependencies require different versions of the same transitive package, NuGet's resolution algorithm may pick unexpectedly. Use the dotnet restore --verbosity detailed flag to diagnose conflicts, and override with explicit versions when necessary.
Tooling Requirements: Central package management requires .NET 6 SDK or later. Projects targeting .NET Core 3.1 or .NET Framework need alternative approaches like Directory.Build.props with linked version properties.
Actionable Next Steps
1. Audit your current projects: Run dotnet list package to see which packages lack explicit versions.
Directory.Packages.props. For existing projects, add explicit versions to critical packages first.
3. Implement lock files for releases: Generate packages.lock.json before tagging releases and commit it to source control.
4. Schedule regular updates: Set calendar reminders to review package versions quarterly, applying security patches while testing for breaking changes.0 replies
A thoughtful contribution can make all the difference. Be the first to share one.