Using NuGet lock files for deterministic .NET builds
Learn how NuGet lock files enable deterministic package restores, see a concrete example with dotnet CLI, and understand the trade‑offs of using locked mode.
04 Nov 2025, 13:54 UTC

The problem: inconsistent package versions across environments
When a team shares a .NET solution, developers often see different package versions restored on their machines versus the CI pipeline. This happens because NuGet, by default, resolves the latest compatible version that satisfies each version range at restore time. A patch release published overnight can cause a test to pass locally but fail in the pipeline, or vice‑versa. The result is flaky builds and wasted debugging time.
Thesis: NuGet lock files give you reproducible restores
NuGet’s packages.lock.json records the exact versions and transitive dependencies that were resolved during a restore. When you restore with the --locked-mode flag, NuGet ignores floating version ranges and reinstalls the exact versions captured in the lock file, making the restore deterministic across machines and CI agents.
How lock files work
During a normal dotnet restore, NuGet creates (or updates) packages.lock.json next to the project file. The file contains a JSON graph where each node is a package ID with its resolved version and a list of its dependencies. If the lock file already exists and you run dotnet restore --locked-mode, NuGet reads this graph and refuses to change any version unless the lock file is explicitly updated.
Lock files are compatible with both packages.config and PackageReference styles, but they are most seamless with SDK‑style projects and the dotnet CLI because the CLI automatically generates and consumes them.
Worked example: adding a lock file to an SDK‑style console app
Create a new SDK‑style console project (run in a terminal with write access to the folder):
dotnet new console -n LockDemo cd LockDemoAdd a package that uses a version range, for example Newtonsoft.Json with a minimum version:
dotnet add package Newtonsoft.Json --version ">=13.0.0"Perform a normal restore to generate the initial lock file:
dotnet restoreAfter this command finishes, you should see a
packages.lock.jsonfile alongsideLockDemo.csproj. Open it and verify that it contains an entry for Newtonsoft.Json with a specific version (e.g., 13.0.3) and its dependencies.Commit the lock file to version control so that all teammates and CI agents use the same baseline.
Now restore in locked mode to confirm determinism:
dotnet restore --locked-modeNuGet will not attempt to fetch a newer compatible version of Newtonsoft.Json even if one becomes available on the feed. The restored packages should match exactly what is recorded in the lock file.
To update the lock file when you intentionally want a newer version, either delete the lock file and run a normal restore, or use the dedicated update command:
dotnet nuget update lockThis re‑evaluates all version ranges and writes a new
packages.lock.jsonwith the latest compatible versions.
Trade‑offs and limitations
Feed availability: If a version recorded in the lock file is later removed from the NuGet feed (e.g., a package is unlisted), a locked‑mode restore will fail with an error like "package X version Y not found". The team must then update the lock file manually.
Security‑patch latency: Because locked mode ignores floating ranges, relying solely on it can delay adoption of critical patches. Teams should establish a regular cadence (e.g., weekly) to run
dotnet nuget update lockand review the changes.Complex dependency graphs: Large solutions with many transitive dependencies can produce lock files that are several hundred lines long. While this is still human‑readable, reviewing diffs in pull requests requires careful attention to avoid unintended version jumps.
Actionable closing
Start by adding a lock file to one of your SDK‑style projects:
- Run
dotnet restoreto generatepackages.lock.json. - Commit the file.
- In your CI pipeline, change the restore step to
dotnet restore --locked-mode. - Schedule a regular job (or a manual reminder) to run
dotnet nuget update lockand evaluate the diff.
With these steps you eliminate version drift as a source of flaky builds while retaining a clear, auditable path to update dependencies when you decide it’s time.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.