NuGet Package Lock Files for Deterministic .NET Restores
Commit the exact NuGet restore graph with packages.lock.json to prevent transitive dependency drift across developers and CI for .NET projects.
07 Mar 2026, 20:14 UTC

Dependency drift breaks builds quietly
A .NET project declares direct dependencies with a version range in the .csproj, for example PackageReference Version="[1.2,2.0)". NuGet resolves transitive dependencies from those ranges. Because ranges can match different concrete versions over time, two developers or a CI agent can restore different sub-dependency graphs even with the same commit. The result is a build that works on one machine and fails on another.
The engineering decision is to commit the resolved graph. NuGet Package Lock Files do that by recording the exact versions and hashes of every package in the restore graph to packages.lock.json.
What a lock file records
When EnableLockFile is enabled, a restore writes packages.lock.json to the project output location. The file lists each package, the exact version selected for each target framework, and a SHA512 hash of the package payload.
The hash provides integrity checking against corruption or registry-side changes between restores. It does not assess security of the locked version itself. A lock file ensures reproducibility, not safety.
The mechanism is designed to support multi-targeting. A single lock file can contain entries for multiple target frameworks and runtimes, so a project targeting net6.0 and net8.0 can keep one file for the whole project.
Enabling lock files for a solution
Add the property to a Directory.Build.props so all projects inherit it without editing each .csproj.
<Project>
<PropertyGroup>
<EnableLockFile>true</EnableLockFile>
</PropertyGroup>
</Project>
Run a restore from the repository root where the solution lives. The command requires read access to the configured NuGet feeds.
dotnet restore
Expected check: packages.lock.json appears alongside the project file. Commit the file to version control. Subsequent restores read the file and use the recorded versions, reducing variation between environments.
Risk: the lock file changes whenever the dependency graph changes. Review diffs before committing to avoid accidental updates.
Updating the graph intentionally
Change a PackageReference version in the .csproj, then run restore again. The restore rewrites packages.lock.json to reflect the new graph. Update only after an explicit dependency change and review the diff.
Lock files do not replace vulnerability scanning. A locked version can still contain known vulnerabilities. Pair lock files with regular scans of the dependency set.
Verifying determinism
Clone the repository on a second machine with a clean NuGet cache. Run dotnet restore and compare the resulting packages.lock.json with the committed version using git diff. An empty diff indicates the same resolution was reproduced.
Limitation: lock files record what was resolved, not why. If a package author republishes the same version with different content, the hash check will surface a mismatch, but the lock file will not prevent you from adopting a vulnerable but unchanged version.
Start by enabling EnableLockFile for one project, commit the generated packages.lock.json, and require the lock file to be present in CI. This removes a class of "works on my machine" issues caused by transitive version drift.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.