Package Versions and Ranges

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.

Every PackageReference Version attribute is a range, even when it looks like a single version, and NuGet’s version strings follow Semantic Versioning with a few differences. This page tries each notation against a feed that holds Ledgerline.Money 1.0.0, 1.5.0, 2.0.0 and 2.1.0-beta.1, using NuGet 7.9.0 in .NET SDK 10.0.402. Choosing versions for packages you publish is on Versioning and Prerelease Packages.

Version basics

A NuGet version is Major.Minor.Patch[.Revision][-Prerelease][+Metadata]:

  • Prerelease labels (-beta.1, -rc.2) sort before the release with the same numbers: 2.1.0-beta.1 < 2.1.0. Labels are compared part by part, numeric parts numerically (beta.2 < beta.10) and text parts alphabetically and case-insensitively.

  • Metadata after + (+e33226ff) is ignored when comparing and is not part of the identity on nuget.org.

  • A package using SemVer 2.0.0 features — dotted prerelease labels or metadata — is only visible to NuGet 4.3+ clients.

Where NuGet’s NuGetVersion differs from SemVer 2.0.0: it allows a fourth part (1.0.0.1), it requires only the major part (1, 1.0, 1.0.0 and 1.0.0.0 are equal), and it compares prerelease labels without case.

Normalization

NuGet normalizes versions when it stores and compares them: missing parts become 0, leading zeros are removed, a zero fourth part is dropped, and metadata is removed. 1.5, 1.5.0, 01.5.00 and 1.5.0.0 are the same version — the reference Version="01.5.00" restored Ledgerline.Money/1.5.0. Feeds and the global-packages folder use the normalized string in paths (ledgerline.money/1.5.0/).

Range notation

Notation Meaning Restored here

1.5

x >= 1.5 (minimum, inclusive)

1.5.0

[1.5]

x == 1.5 (exact)

1.5.0

(1.0,)

x > 1.0 (exclusive minimum)

1.5.0, with NU1604

[1.0,1.5]

1.0 <= x <= 1.5

1.0.0

[1.0,2.0)

1.0 <= x < 2.0

1.0.0

[1.0,2.0]

1.0 <= x <= 2.0 (both bounds inclusive)

1.0.0

(1.0,2.0)

1.0 < x < 2.0 (both bounds exclusive)

1.5.0, with NU1604

(,2.0], (,2.0)

at most 2.0 / below 2.0 (no lower bound)

1.0.0 (the lowest available), with NU1604

(1.0)

invalid

restore failed

Two rules explain the results: NuGet picks the lowest version that satisfies the range (Dependency Resolution), and a plain version is a minimum, not "this version". [1.0,1.5] therefore restored 1.0.0, not 1.5.0, and so did [1.0,2.0]: an inclusive upper bound admits 2.0.0 but the lowest match still wins. (1.0,2.0) excludes both ends, which leaves only 1.5.0 on this feed.

The exclusive lower bound produced a warning, for (1.0,) and again for (1.0,2.0), where NuGet prints the range with both bounds:

~/ledgerline/samples/Deps/Deps.csproj : warning NU1604: Project dependency Ledgerline.Money (> 1.0.0) does not contain an inclusive lower bound. Include a lower bound in the dependency version to ensure consistent restore results.
~/ranges/Deps/Deps.csproj : warning NU1604: Project dependency Ledgerline.Money (> 1.0.0 && < 2.0.0) does not contain an inclusive lower bound. Include a lower bound in the dependency version to ensure consistent restore results.

(The second line comes from a scratch project, ~/ranges/Deps, whose nuget.config lists only a folder feed with the same four Ledgerline.Money versions; the (1.0,2.0), [1.0,2.0] and prerelease examples below were run there.)

The invalid (1.0) gave no NuGet diagnostic at all — only MSBuild’s generic message that the restore task failed silently:

~/.dotnet/sdk/10.0.402/NuGet.targets(198,5): error MSB4181: The "RestoreTask" task returned false but did not log an error. [~/ledgerline/samples/Deps/Deps.csproj]

A minimum that no feed has is NU1102, which lists what each source offers:

~/ledgerline/samples/Deps/Deps.csproj : error NU1102: Unable to find package Ledgerline.Money with version (>= 3.0.0)
~/ledgerline/samples/Deps/Deps.csproj : error NU1102:   - Found 4 version(s) in ledgerline-local [ Nearest version: 2.1.0-beta.1 ]
~/ledgerline/samples/Deps/Deps.csproj : error NU1102:   - Versions from ledgerline-bagetter were not considered
~/ledgerline/samples/Deps/Deps.csproj : error NU1102:   - Versions from nuget.org were not considered

("not considered" because the repository’s package source mapping sends Ledgerline.* only to ledgerline-local; Package Sources and Source Mapping.) A minimum whose exact version is missing but a higher one exists resolves with NU1603:

~/ledgerline/samples/Deps/Deps.csproj : warning NU1603: Deps depends on Ledgerline.Money (>= 2.1.0-beta.0) but Ledgerline.Money 2.1.0-beta.0 was not found. Ledgerline.Money 2.1.0-beta.1 was resolved instead.

Prereleases in a range

A range admits prereleases only when something in the graph asks for one — in practice, when a bound of the range is itself a prerelease. The feed’s only version above 2.0.0 is 2.1.0-beta.1:

<PackageReference Include="Ledgerline.Money" Version="[2.0.1,3.0)" />
  Determining projects to restore...
~/ranges/Deps/Deps.csproj : error NU1103: Unable to find a stable package Ledgerline.Money with version (>= 2.0.1 && < 3.0.0)
~/ranges/Deps/Deps.csproj : error NU1103:   - Found 4 version(s) in ledgerline-local [ Nearest version: 2.1.0-beta.1 ]
  Failed to restore ~/ranges/Deps/Deps.csproj (in 176 ms).

2.1.0-beta.1 lies inside [2.0.1,3.0) but is not stable, so NuGet reports NU1103 rather than NU1102. Giving the lower bound a prerelease label opts in:

<PackageReference Include="Ledgerline.Money" Version="[2.1.0-beta.1,3.0)" />
<!-- or, to accept any prerelease at or above 2.0.1: -->
<PackageReference Include="Ledgerline.Money" Version="[2.0.1-0,3.0)" />
  Determining projects to restore...
  Restored ~/ranges/Deps/Deps.csproj (in 275 ms).
  Determining projects to restore...
~/ranges/Deps/Deps.csproj : warning NU1603: Deps depends on Ledgerline.Money (>= 2.0.1-0 && < 3.0.0) but Ledgerline.Money 2.0.1-0 was not found. Ledgerline.Money 2.1.0-beta.1 was resolved instead.
  Restored ~/ranges/Deps/Deps.csproj (in 196 ms).

Both restored Ledgerline.Money/2.1.0-beta.1 (each was restored separately with dotnet restore --force). -0 is the lowest possible prerelease label, so 2.0.1-0 means "2.0.1 or any prerelease of it or later"; because no package 2.0.1-0 exists, the restore adds NU1603. packages.config projects never mix stable and prerelease versions this way: a stable range there excludes prereleases from the whole graph.

How the tools write versions

The tools that add a reference write different things into the project:

Tool What it writes Ranges

dotnet package add (no --version)

The latest stable version as a plain Version — a minimum (Version="2.0.0" here); with --prerelease, the latest version including prereleases (Version="2.1.0-beta.1").

 — 

dotnet package add --version <v>

The string exactly as given, not normalized: --version 1.5 wrote Version="1.5".

Accepted: --version "[1.0,2.0)" wrote Version="[1.0,2.0)", and --version "1.*" a floating version. A version that cannot be resolved (--version 3.0.0, NU1102) is not written.

Visual Studio Package Manager UI (not run here)

One version picked from the Version list (prereleases appear only with Include prerelease); the documentation does not show the attribute it writes.

No range is entered in the UI. In packages.config projects the UI disables every version outside a package’s allowedVersions.

nuget.exe (not run here)

nuget install <id> -Version <v> downloads a package to a folder; nuget update rewrites the version attribute in packages.config. That attribute is "the exact version of the package to install" — "Ranges are not allowed."

Only in allowedVersions (for example allowedVersions="[1,2)"), added by hand in a text editor; it limits update and the UI, not install or restore. nuget.exe does not edit PackageReference items.

dotnet package add Ledgerline.Money
dotnet package add Ledgerline.Money --version "[1.0,2.0)"
info : Adding PackageReference for package 'Ledgerline.Money' into project '~/ranges/Deps/Deps.csproj'.
info : PackageReference for package 'Ledgerline.Money' version '2.0.0' added to file '~/ranges/Deps/Deps.csproj'.
info : Adding PackageReference for package 'Ledgerline.Money' into project '~/ranges/Deps/Deps.csproj'.
info : PackageReference for package 'Ledgerline.Money' version '[1.0,2.0)' added to file '~/ranges/Deps/Deps.csproj'.

(The reference was removed between the two commands; the lines shown are the ones that report what was written.)

Floating versions

A * makes the range float to the highest matching version (the opposite of the normal lowest-applicable rule):

Version Floats to Restored here

1.*

highest 1.x release

1.5.0

*

highest release

2.0.0

*-*

highest version, including prereleases

2.1.0-beta.1

2.1.0-*

highest 2.1.0 prerelease (or 2.1.0)

2.1.0-beta.1

Floating versions make a restore depend on when it runs: the same commit restores differently after a new package is published. They suit tools and experiments, not shipped builds; with lock files they are recorded at the resolved version (Lock Files and Locked Mode), and with Central Package Management they need CentralPackageFloatingVersionsEnabled.

Best practice

  • Applications and libraries alike: a plain minimum (Version="1.5.0") and an update process. Exact versions and upper bounds in a library make it impossible for consumers to combine it with other packages that need a newer dependency (NU1107/NU1608 on Dependency Resolution).

  • Upper bounds only for a known incompatibility, documented next to the reference.

  • Lock files, not exact versions, for reproducible application restores.

Common mistakes

  • Reading Version="1.5.0" as "exactly 1.5.0" — it is a minimum.

  • Expecting a stable range to pick up a prerelease — [2.0.1,3.0) fails with NU1103; put a prerelease label on a bound.

  • (1.0) and other invalid ranges — restore fails with no NuGet message.

  • Upper bounds in library dependencies — consumers cannot resolve.

  • Floating versions in release builds — the restore changes over time.