Why don't we have .lock files in .NET

If you ever worked with Node, Go (precisely a checksum), or Rust, you’ve seen lock files like Cargo.lock, yarn.lock. However, it’s not that common in .NET, and everything just works. So, to understand why .NET doesn’t need them (mostly), we first need to understand what problem lock files solve.
Setup
Your project has a manifest file - Cargo.toml, package.json, .csproj - where you declare your dependencies. In most ecosystems, you specify a version range, not an exact version:
serde = "1.0" # means >= 1.0.0, <2.0.0
But why? Why not just specify the exact version in manifest file?
There are a few good reasons for it:
semver is a contract: The whole point of semantic versioning is that
1.0.xpatches and1.x.0minor updates are backward compatible. When you writeserde = “1.0”, you’re saying: “I depend on public API of serde 1.x, any compatible version also works.”diamond dependency problem: Say your project depends on
fooandbar, and both of them depend onserde. Iffoopins1.0.97, andbarpins1.0.98, you’re stuck - two incompatible versions of the same package. So giving ranges allow dependency manager to select a version that satisfies both.you won’t get security patches: If
serdeships a fix in1.0.98, every package (crate) that pinned1.0.97would need to publish a new release just to bump the number. Multiply that across the whole dependency tree, and you’ve got a cascading update nightmare.
So what’s the issue?
Ranges are great, but they introduce instability. Without a lock file, cargo build today might resolve to serde 1.0.97. Tomorrow, after a new release, it resolves to 1.0.98. Same code, same manifest, different dependencies. Now you are in - “it works on my machine, but breaks in CI” world.
A lock file fixes this by recording the exact resolved version of every dependency in your tree. Once it’s generated, subsequent builds use those exact versions. It only changes when you explicitly run something like npm update.
So the division is clean: the manifest is your human decision about API compatibility, while the lock file is a machine-generated snapshot.
Now back to .NET
NuGet actually does support lock files - you can enable packages.lock.json by adding:
<PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
But almost nobody does. And the reason is that NuGet's resolution strategy makes them mostly unnecessary.
Here's the key difference. Cargo and npm resolve to the newest compatible version. If versions 1.0.0 through 1.0.4 exist on the registry and you depend on >= 1.0.0, you get 1.0.4. Tomorrow, someone publishes 1.0.5, and suddenly your build resolves differently. That's why you need a lock file — to freeze the resolution.
NuGet does the opposite. It resolves to the lowest applicable version. Same scenario, you get 1.0.0. When 1.0.5 shows up on the feed, nothing changes. 1.0.0 still satisfies the constraint, so NuGet keeps using it. Your build is already stable without a lock file.
NuGet's resolution is inherently sticky, so the ecosystem never felt the pain that made lock files essential elsewhere.
The tradeoff
There's a catch, though. If 1.0.0 has a security vulnerability and the fix ships in 1.0.4, NuGet will happily keep resolving to the vulnerable version forever. You'll never get the fix unless you explicitly update.
Cargo and npm have the opposite problem — you get fixes automatically, but your builds drift without a lock file.
Neither approach is strictly better. Cargo chose "safe by default, need lock files for stability." NuGet chose "stable by default, need vigilance for security."
If you're on .NET, it's worth knowing about dotnet list package --vulnerable. It checks your dependencies against known vulnerability databases.
You also have different options to work around updates. For example, you could enable packages.lock.json and run dotnet restore --locked-mode in CI. Another option could be to run an automated dependency update tool on some schedule (e.g., GitHub Dependabot, Renovate).



