Controlling NuGet Version Range Resolution in .NET Projects
NuGet's highest-compatible range resolution is convenient but not automatically reproducible. Learn how to pin transitive dependencies and verify what your build actually restores.
10 Jun 2026, 21:31 UTC

The same range, two different restores
You add <PackageReference Include='SomeLibrary' Version='[1.0.0, 2.0.0)' /> expecting the range to keep you below a breaking major version. On your laptop the build restores 1.2.3. On CI it restores 1.2.9, which changes a method signature your code relies on. Both restores succeed, but they do not restore the same dependency graph.
NuGet resolves semantic version ranges by selecting the highest compatible version available on your configured feeds. That default is useful for adopting patches, but it is not a reproducibility strategy. To make restores predictable, combine explicit ranges, consistent feed configuration, and verification of the restored asset file.
What the range syntax means
NuGet range syntax uses brackets for inclusive bounds and parentheses for exclusive bounds. A version like 1.0.0 by itself is an exact pin; 1.0.* is a floating version that matches the highest patch for that major.minor pair.
| Pattern | Meaning |
|---|---|
[1.0.0, 2.0.0) | 1.0.0 and above, below 2.0.0 |
(1.0.0, 2.0.0] | Above 1.0.0, up to and including 2.0.0 |
[1.0.0] | Exactly 1.0.0 |
1.0.* | Highest patch for 1.0.x |
Prerelease packages are excluded by default unless the range explicitly references a prerelease tag. Behavior around prerelease inclusion differs between NuGet 4.x and NuGet 5+/6.x, so confirm which NuGet version your SDK and CI images use before relying on range resolution across environments.
Transitive dependencies collapse to one version
If your application depends on two libraries that both depend on a shared package, NuGet deduplicates that shared dependency to a single version across the whole dependency closure. It applies the highest compatible rule to every constraint simultaneously. That means one library's upper bound can force another library's dependency up or down, and all consumers in the graph receive the same assembly version.
This is usually desirable because it prevents side-by-side copies of the same library, but it also means a range on a direct dependency can indirectly change the version of a transitive package you never declared directly.
Worked example: inspect a resolved range
Assumptions: .NET SDK 6.x or later, NuGet 6.x behavior, network access to the default NuGet feed or your configured feeds.
- Create a console app in a terminal:
- Edit
RangeDemo.csprojto use a version range. Adjust the target framework to match your installed SDK: - Run the restore from the project folder. No administrator permissions are required; packages download to the global packages folder (default
~/.nuget/packagesor%USERPROFILE%\.nuget\packages): - Open
obj/project.assets.jsonand locate the entry forNewtonsoft.Json. Check theversionfield. The expected result is a version inside[13.0.0, 14.0.0), typically the highest stable version your feeds advertise within that interval. - Compare feed visibility with
nuget.exe list Newtonsoft.Jsonfor stable packages, thennuget.exe list Newtonsoft.Json -prereleaseto see whether prerelease builds appear. Different feeds or authentication can change which versions are visible, which changes the resolved version without touching the project file.
dotnet new console -o RangeDemo
cd RangeDemo
<Project Sdk='Microsoft.NET.Sdk'>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include='Newtonsoft.Json' Version='[13.0.0, 14.0.0)' />
</ItemGroup>
</Project>
dotnet restore
Risk: adding a new feed, changing feed priority, or authenticating to a private feed can cause a clean restore to select a different version than a previous restore, even when the csproj is unchanged.
Make restores reproducible
Use exact versions for shared transitive dependencies where stability matters:
<PackageReference Include='Newtonsoft.Json' Version='13.0.3' />
Use narrow ranges when you want patch-level flexibility but not minor or major movement:
<PackageReference Include='Newtonsoft.Json' Version='[13.0.0, 13.0.4)' />
A NuGet.Config placed next to your solution can control default handling for floating or unpinned references. Example:
<configuration>
<config>
<add key='dependencyVersion' value='Patch' />
</config>
</configuration>
The dependencyVersion setting accepts values such as Min, Patch, Minor, Major, and Exact. Verify its effect by running dotnet restore after adding the file and re-inspecting obj/project.assets.json.
Limitations
- Ranges simplify security patching but can silently move a clean restore to a newer patch version on another machine.
obj/project.assets.jsonis a restore artifact, not a lock file you commit by default in SDK-style projects. Reproducibility comes from pinned references, identical feed configuration, and consistent NuGet versions.- Private feeds and feed ordering affect resolution. Authenticate CI to the same feeds developers use, or resolution may diverge.
Closing check
Audit your csproj files for wide ranges such as [1.0.0, 2.0.0) on shared dependencies. Replace them with exact pins or narrow ranges where stability matters, add a NuGet.Config that fixes feed priority, and make inspecting obj/project.assets.json part of your restore review. A range is a policy, not a guarantee.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.