Lock Files and Locked Mode

This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its dotnet CLI) and NuGet 7.9, as published in the MSBuild, .NET and NuGet documentation on learn.microsoft.com, which is the reference these pages are written and verified against. Material about .NET 11 / NuGet 8.0 is still in preview and is always flagged as such — never presented as baseline.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The C# Reference bibliography lists the reference material consulted while preparing these pages.

A restore is a function of the project’s references and of what the feeds offer at that moment: a newly published version, a deleted one or a re-uploaded package can change the graph without any commit. A lock file records the whole resolved graph — every direct and transitive package, its version and a hash of its content — and locked mode makes restore fail instead of silently differing. Ledgerline enables both. This page shows them working and failing with NuGet 7.9.0 (.NET SDK 10.0.402), on the scratch project and synthetic packages of Dependency Resolution.

Enabling the lock file

<PropertyGroup>
  <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>

Ledgerline sets it in Directory.Build.props, so every project writes a packages.lock.json next to its project file. Once a lock file exists, restore uses it even if the property is removed. For a project referencing Ledgerline.Rates 1.0.0:

{
  "version": 1,
  "dependencies": {
    "net10.0": {
      "Ledgerline.Rates": {
        "type": "Direct",
        "requested": "[1.0.0, )",
        "resolved": "1.0.0",
        "contentHash": "BWREbaJ1wXNuacUeiRinzc7hFx3DfPIPPtfV0QQGjnibJJEy/KOnGzviwYcW9CkgLwkL5yow86pIkMMD9H2Beg==",
        "dependencies": {
          "Ledgerline.Money": "1.2.0"
        }
      },
      "Ledgerline.Money": {
        "type": "Transitive",
        "resolved": "1.5.0",
        "contentHash": "ikqFv5yDeM68b02nB3Ou0dYlkIRu7EjVvuyM5d8glrcPRVDYHvG7wAqqbO3sMMaMYnvX2vCdcEF6+9KYKxy0fg=="
      }
    }
  }
}
  • One section per target framework (and per RID with RuntimeIdentifiers).

  • requested is the range from the project, resolved the version chosen, dependencies the package’s own declared minimums (Ledgerline.Money 1.2.0, resolved to 1.5.0 as on the dependency-resolution page).

  • contentHash is the SHA-512 recorded in the package’s .nupkg.metadata (How Package Installation Works).

  • The format is versioned: this project wrote "version": 1; Ledgerline's projects, which use Central Package Management, write "version": 2.

Locked mode

Without locked mode, a restore that sees a changed dependency simply re-resolves and rewrites the lock file. In CI that is never what you want. dotnet restore --locked-mode (or RestoreLockedMode=true) restores exactly the recorded graph or fails.

A changed reference — NU1004. The project switched from Ledgerline.Rates to Ledgerline.Fx without updating the lock file:

~/ledgerline/samples/Deps/Deps.csproj : error NU1004: A new package reference was found Ledgerline.Fx for the project target framework net10.0.The packages lock file is inconsistent with the project dependencies so restore can't be run in locked mode. Disable the RestoreLockedMode MSBuild property or pass an explicit --force-evaluate option to run restore to update the lock file.

Changed package content — NU1403. Ledgerline.Money 1.5.0 was replaced on the feed with a different package of the same ID and version (possible on any feed that allows overwriting — nuget.org does not), and removed from the global-packages folder:

~/ledgerline/samples/Deps/Deps.csproj : error NU1403: Package content hash validation failed for Ledgerline.Money.1.5.0. The package is different than the last restore.

This is the lock file’s supply-chain value: same ID, same version, different bytes — caught (Supply-Chain Security).

Updating the lock file

dotnet restore --force-evaluate (RestoreForceEvaluate) re-resolves and rewrites the lock file. Run against the tampered feed above, it succeeded silently and replaced the recorded hash of Ledgerline.Money 1.5.0 with the hash of the tampered package:

before: "contentHash": "ikqFv5yDeM68b02nB3Ou0dYlkIRu7EjVvuyM5d8glrcPRVDYHvG7wAqqbO3sMMaMYnvX2vCdcEF6+9KYKxy0fg=="
after:  "contentHash": "LdP9SOr5OSn4BbyjwGXHzSss1YrRL64cUfXKylfja2drIQz5JrsjVVuoX0TCtAtz/HMcULkU0WDOG0S3V2VXOg=="

A lock-file diff that changes a hash without changing the version is therefore something to investigate, not to merge. Review lock-file changes like code.

Passing --locked-mode and --force-evaluate together is contradictory; force-evaluate wins. SDK 10.0.402 said nothing; with -p:SdkAnalysisLevel=11.0.100 — the default in the .NET 11 SDK — it warns:

~/.dotnet/sdk/10.0.402/NuGet.targets(198,5): warning NU1512: RestoreForceEvaluate overrides RestoreLockedMode; the packages lock file will be re-evaluated and regenerated, and locked mode is ignored.

A missing lock file is not an error

NuGetLockFilePath moves the file (-p:NuGetLockFilePath=lock/custom.lock.json). A locked-mode restore pointed at a path where no lock file existed did not fail — it created lock/custom.lock.json. Locked mode compares against a lock file; it does not require one. A CI job should also check that the expected packages.lock.json files are committed (for example, git diff --exit-code after the restore).

Lock files with other features

  • Central Package Management — the lock file records the central versions; transitive pinning adds CentralTransitive entries (format version 2).

  • Pruning — packages pruned for net10.0 are absent from the lock file; enabling pruning on an existing project shrinks the next regenerated lock file, and the locked-mode check is pruning-aware (PackageReference).

  • SDK version — implicit package references and pruning data come from the SDK, so a different SDK can make a locked restore fail; the documentation suggests global.json with rollForward: disable for repositories that lock (Installing and Selecting SDKs).

  • Floating versions — locked at the version they floated to until --force-evaluate.

What to commit

Commit lock files for applications, services, tools and test projects — anything at the top of a dependency chain. For a library, a committed lock file only describes the library’s own test restore; consumers resolve their own graph. Ledgerline commits all of them and runs dotnet restore --locked-mode first in CI (CI Builds and Caching); the lock files also serve as the cache key for the NuGet folder.

Common mistakes

  • Enabling the lock file but not locked mode in CI — the file is rewritten instead of enforced.

  • Merging a lock file whose hashes changed without a version change — --force-evaluate accepts tampered content.

  • Relying on locked mode to require a lock file — it creates a missing one.

  • Upgrading the SDK and being surprised by NU1004 after pruning changes — regenerate deliberately.