Architecting Deterministic and Secure NuGet Dependency Resolution
Learn how NuGet's Nearest Win strategy resolves versions, how package source mapping stops dependency confusion, and how lock files enforce deterministic builds.
15 Aug 2025, 07:25 UTC

The Dependency Resolution Problem
In complex .NET projects, dependency conflicts occur when two different packages require different versions of the same transitive dependency. If left unmanaged, this leads to non-deterministic builds where the version of a library changes based on the restore order or the shape of the dependency graph, potentially causing MissingMethodException at runtime.
Takeaway: Using NuGet's Nearest Win strategy, package source mapping for trust boundaries, and lock‑file enforcement gives deterministic, secure builds that are easy to verify.
Requirements for Stable Resolution
- Determinism: Same source produces identical binaries on any machine.
- Security: Internal packages cannot be substituted by malicious public packages with the same name (dependency confusion).
- Predictability: Developers can see exactly which version was selected and why.
The Smallest Suitable Design: Nearest Win
NuGet resolves version conflicts by walking the dependency graph from the project root and picking the version that is geographically closest (fewest edges) to the .csproj. If two versions sit at the same depth, the version declared directly in the project file overrides transitive requirements.
Example: Diamond Dependency with Root Override
Project A (root)
├─ Package B v1.0 → Package D v1.0
└─ Package C v1.0 → Package D v2.0
Both Package D v1.0 and Package D v2.0 are two edges from the root. Without a direct reference, NuGet applies its tie‑breaking rule (often the first encountered or the highest compatible version). Adding an explicit reference to Package D v2.0 in Project A moves that version to depth 1, so v2.0 wins even though Package B needs v1.0.
Trust and Data Boundaries
To stop dependency confusion, map package namespaces to specific feeds via nuget.config.
<packageSourceMapping>
<packageSource key="nuget.org" source="https://api.nuget.org/v3/index.json">
<package pattern="*" />
</packageSource>
<packageSource key="InternalFeed" source="https://pkgs.dev.azure.com/contoso/_packaging/internal">
<package pattern="Contoso.*" />
</packageSource>
</packageSourceMapping>
With the pattern Contoso.* bound to the internal feed, the resolver never queries the public gallery for those names, even if a higher version exists there.
Operational Checks and Verification
1. Enforcing Determinism with Lock Files
Enable lock‑file generation in the project file:
<PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
In a CI pipeline, run the restore with the locked‑mode flag:
dotnet restore --locked-mode
This command must be executed on the build agent with read access to all configured feeds. If the packages.lock.json does not match the current .csproj constraints, the restore fails, preventing silent version drift.
2. Diagnostic Verification
After a successful restore, inspect project.assets.json in the obj folder. This file contains the final resolved graph produced by Nearest Win. Look for an entry like:
"package": "Contoso.Utils/2.3.0",
"dependencies": [...]
Comparing this file across machines confirms that the same versions were selected.
Failure Modes and Design Shifts
| Failure Mode | Root Cause | Mitigation |
|---|---|---|
MissingMethodException | Nearest Win selected a version too old for a transitive dependency. | Add an explicit reference to the newer version in the root .csproj or use a Directory.Packages.props override. |
| Version Conflict Error | Two dependencies require mutually exclusive version ranges that Nearest Win cannot reconcile. | Update the lagging dependency or apply a PackageVersion rule in Directory.Build.props. |
| Restore Timeout/Failure | Unreachable private feed or circular dependency. | Validate feed URLs and inspect project.assets.json for cycles. |
When to change this design: The approach above is the default for SDK‑style projects targeting .NET 5+ and .NET Core. When migrating legacy .NET Framework applications, you must replace app.config binding redirects with NuGet‑managed versioning, because the runtime loader no longer uses redirects; otherwise you may see mismatched assemblies despite a correct NuGet restore.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.