Proposed breaking change to how NuGet intepret version constraint

Topics: General
Developer
Apr 30, 2013 at 11:48 PM
Edited Apr 30, 2013 at 11:56 PM
This post is regarding this work item: https://nuget.codeplex.com/workitem/3184

Here're the summary of the problem and our proposed solution.

Today, when you set the version constraint of a dependency package as, for example, (, 2.0), NuGet will not pick the version 2.0, but it will pick any prerelease versions of 2.0, e.g. 2.0-alpha or 2.0-beta. This is because NuGet interprets prerelease versions as smaller than the stable version. While this is technically correct, it's not intuitive and has caught people by surprise.

To tackle this issue, we're considering changing the behavior of the above example such that (, 2.0) will NOT include any prerelease version of 2.0.

For cases where people do want to maintain the current behavior, we're introducing a new syntax, like this: (, 2.0} (curly brace replacing the bracket)

To summarize the new behavior:

(, 2.0) : do NOT include 2.0 stable or any 2.0 pre-release version.
(, 2.0} : include 2.0 pre-release versions but do NOT include 2.0 stable.
(, 2.0] : include 2.0 stable and all 2.0 pre-release versions
Developer
Apr 30, 2013 at 11:52 PM
In addition to that, we will also introduce the matching syntax for lower bound, but with a different interpretation:

[1.0, ) : include 1.0 stable but do NOT include 1.0 pre-release versions.
{1.0, ) : include 1.0 stable AND all 1.0 pre-release versions.
(1.0, ) : do NOT include 1.0 stable or any 1.0 pre-release versions.

Comments and feedbacks are welcomed.
May 1, 2013 at 11:35 AM
Edited May 1, 2013 at 11:38 AM
all in favor!
May 1, 2013 at 5:31 PM
Edited May 1, 2013 at 5:32 PM
This is a great idea! The only ones which I find a bit weird are [1.0, ) and {1.0, ). I'll explain:

If I look at the 2.0 proposals above, it seems what they are saying is that release/pre-release is effectively orthogonal to the existing ( ) and [ ] version constraints. I.e. my version constraint refers to both release/pre-release builds of that version. I think this makes sense.

The new { } symbols effectively allow the constraints to turn at a right angle and split in the release/pre-release dimension. I think this is probably an edge case, but I can imagine some people might find it useful.

So in summary I think the following might be more intuitive

(, 2.0) : do NOT include 2.0 stable or any 2.0 pre-release version.
(, 2.0} : include 2.0 pre-release versions but do NOT include 2.0 stable. <--- special
(, 2.0] : include 2.0 stable and all 2.0 pre-release versions

[1.0, ) : include 1.0 stable AND all 1.0 pre-release versions.
{1.0, ) : include 1.0 stable but do NOT include 1.0 pre-release versions. <--- special
(1.0, ) : do NOT include 1.0 stable or any 1.0 pre-release versions.
Developer
May 1, 2013 at 8:28 PM
Thanks for the feedback. Yes, we did discuss about your proposal for the lower bound. I agree it's nice to have the { and } symbols have the matching symmetric effect. I'll let more people chime in.
Developer
May 1, 2013 at 9:54 PM
@adamralph
Hi Adam,
It got me a while to get the gist of your argument. So I'll paraphrase the argument (I hope I get it right):

'It's nice that round and square braces are ways to write specifications without worrying about prerelease versions existing (pre-release ignorant approach to version specification) - and it's nice because these options will work and do what people wanted 90% of the time with least surprise (like in the original issue).
Curly braces are the advanced feature used by package owners who realized they really, really care about pre-release versions being different from release versions.'

If I paraphrased that right I like the argument!

I think it also aligns quite well with the current state of the semver spec where it states that 'Pre-release versions satisfy but have a lower precedence than the associated normal version'.
Here, we say 'by default' (using the mainstream ( and [ operators), prerelease will be treated 'the same' as release version (hence 'satisfying' the requirement for that version), but to do something different than the default semvery thing to do of treating prerelease like release, then curly braces allow you to do that. Nice!
Tim
May 2, 2013 at 12:20 AM
Edited May 2, 2013 at 12:28 AM
@tilovell yep, you've paraphrased that well. The mainstream ( ) and [ ] operators having no distinction between stable and pre-release - that's the key. They would effectively operate in one dimension on a major.minor.patch axis, with the { } operators operating in a second dimension on a -pre axis. Assuming it was implemented this way, I would use it like this:

(, 2.0) : My package breaks with 2.0. Use an earlier version.
(, 2.0] : My package works with all versions up to and including 2.0. Use any of these versions but not later.

[1.0, ) : My package works with all versions down to and including 1.0. Use any of these versions but not earlier.
(1.0, ) : My package breaks with 1.0. Use a later version.

Stable and pre-release are viewed as the same thing and which one is used is an orthogonal choice made by the consumer of the package.

However, I've been thinking more about the original proposal from @dotnetjunky. The only point where it differs from mine is at the 1.0 stable/pre-release boundary. If we discard my two dimensional view and think of the all the versions as points in a single dimension on a major.minor-patch(-pre) axis, then the original proposal makes perfect sense. Assuming it was implemented that way, I would use it like this:

(, 2.0) : My package breaks with 2.0 pre-release or later (includes stable). Use an earlier version.
(, 2.0] : My package works with all versions up to and including 2.0 stable (includes pre-release). Use any of these versions but not later.

[1.0, ) : My package works with all versions down to and including 1.0 stable (does not include pre-release). Use any of these versions but not earlier.
(1.0, ) : My package breaks with 1.0 stable or earlier (includes pre-release). Use a later version.

A distinction is made between stable and pre-release but which one is used is still a choice made by the consumer of the package.

After some more thought, I can't really see a use for (, 2.0} under either proposal. If I know that my package works up to a given pre-release I don't think there's much value in being able to state that all future pre-releases will be OK but stable will not. I think the best way to express the pessimistic view is to use (, 2.0-known-pre-release], if the syntax would allow it. Similarly, {1.0, ) would not be necessary under either proposal if we could specify [1.0-known-pre-release, ). (Note that even if the UI switch is set to stable only, the version used would simply have to fall within these constraints, even if those constraints are specified in terms of pre-release versions.)

At the moment I'm not sure which of these two approaches best follows the principle of least astonishment.

I do think that being able to specify pre-release versions in the constraints is better than the { } syntax. Accepting that, @dotnetjunky's proposal is perhaps slightler simpler to understand since the constraints are operating on the same single axis as the versions. With my proposal, the constraints suddenly walk down the second axis when they have a pre-release version. But perhaps this is just unnecessary mental imagery on my part ;-).

I think the downside of @dotnetjunky's proposal is that '(' means "not this version or earlier" and ')' means "not any pre-release equivalent of this version or later" (asymmetric), whereas in mine they mean "not this version (stable/pre-release) or earlier" and "not this version (stable/pre-release) or later" respectively (symmetric).
Developer
May 2, 2013 at 4:37 PM
I think we all agree on the upper bound.

@adamralph: you bring up an interesting observation about the two axes. Indeed, when I discussed this topic with @JeffHandley, we looked at it from a single axis perspective.

So with my initial proposal, the axis will look like this:
{  1.0-alpha   [  1.0   (   1.1     ......    )  2.0-alpha   }   2.0   ]   2.1   
Yes, the { } pair is not symmetric, so as [ and ]. The reason we prefer this option is that we retain the behavior of [.

With @adamralph's proposal, it would be a breaking change of the behavior of [, but it introduces symmetry.
[  1.0-alpha   {  1.0   (   1.1     ......    )  2.0-alpha   }   2.0   ]   2.1   
Like you, I don't know which one will introduce less astonishment. We need more data and feedbacks from the community.
Developer
May 2, 2013 at 5:42 PM
We should think about the pedagogical simplicity of what we end up building.

People should be able to learn the brackets and braces in lumps:
Lump 1: what do [] do?
Lump 2: what do () do?
Lump 3: what do {} do?

Lump1 once learned is easily useful to solve your most immediate problem of how to make your package depend on another one, since it allows you to include specific versions you want (hopefully without worrying about whether its prerelease or not).
If all you know is the [] operators you can do [1.1-1.1] or [1.1-1.2] and allow your thing to work with the versions that right now, you know you care about.

Lump2 once learned supplements what you can do with Lump1, allowing you to exlucde specific versions you don't want, and to protect yourself from the breaking changes that are expected in major version bumps. Once you learn this you start doing the ever popular [1.1-2.0) pattern. And by implication you want prerelease versions of 2.0 excluded. (Note that I think we already decided to make a breaking change here - by design)

Lump3 is for the even more advanced case of I need to include or exclude prerelease versions

Basically - Including alpha and beta versions with [] is going to be what more people want to do while they are learning how the feature works, so from that point of view I think '[' is the better option for including prerelease versions.

Tim
Developer
May 2, 2013 at 6:02 PM
I'm going to flip flop on the last part of what I just said. I now think that by default people don't want to include prerelease versions in lower bounds in the common use case. So left bracket [1.0-2.0] should include 2.0-alpha but not 1.0-alpha. The reason being that 1.0-alpha to 1.0 is frequently to be breaking change.
May 2, 2013 at 6:13 PM
Edited May 2, 2013 at 6:13 PM
I keep flip flopping as well. Every time I think I've decided my preference, I think a little more and change my mind again.

I must say your last post with the pedagogic angle almost convinced me once and for all but now you've made me unsure again :-)

I think my main angle was that use of stable/pre-release is something which is decided by the developer updating the package. If they choose to use a pre-release then on their head be it. Simpler to just have the version constraints free of stable/pre-release meaning.

BTW - it's really great to see the NuGet owners so keen to hear the opinions of the community on decisions such as this!
Coordinator
May 3, 2013 at 9:31 PM
The point made about [1.0,2.0] including 2.0-alpha, but not 1.0-alpha is critical. That is a mainline scenario and I think it would really trip people up if [1.0,2.0) included pre-release versions of 1.0. That would be amplified by the fact that NuGet chooses the lowest version that satisfies the dependency and we would therefore choose to give you the pre-release version of 1.0 instead of the stable version.

Overall, I think it's okay to break symmetry here because there is asymmetry by definition in that pre-release versions are always less than stable versions, and therefore depending on when you're looking at "greater than [or equal to]" vs. "less than [or equal to]," there is always going to be asymmetry.

Setting symmetry aside, we can focus on intent. In fact, intent is what the original issue is--we (unintentionally) erred on the side of technical correctness instead of intent. Here's what I think the common intentions would be: (these match @dotnetjunky's initial proposal)

(, 2.0) : do NOT include 2.0 stable or any 2.0 pre-release version.
(, 2.0} : include 2.0 pre-release versions but do NOT include 2.0 stable.
(, 2.0] : include 2.0 stable and all 2.0 pre-release versions

[1.0, ) : include 1.0 stable but do NOT include 1.0 pre-release versions.
{1.0, ) : include 1.0 stable AND all 1.0 pre-release versions.
(1.0, ) : do NOT include 1.0 stable or any 1.0 pre-release versions.

There is however one other glitch in this--what happens if a pre-release version is specified with an operator that excludes pre-release versions? For example:

(, 2.0-beta) : What does this mean?
(, 2.0-beta} : What does this mean?
(, 2.0-beta] : Include 2.0-beta and earlier pre-release versions of 2.0, but no versions of 2.0 greater than 2.0-beta

[1.0-beta, ) : include 1.0-beta and greater but do NOT include earlier 1.0 pre-release versions.
{1.0-beta, ) : What does this mean?
(1.0-beta, ) : What does this mean?
May 3, 2013 at 9:49 PM
JeffHandley wrote:
There is however one other glitch in this--what happens if a pre-release version is specified with an operator that excludes pre-release versions? For example:

(, 2.0-beta) : What does this mean?
(, 2.0-beta} : What does this mean?
(, 2.0-beta] : Include 2.0-beta and earlier pre-release versions of 2.0, but no versions of 2.0 greater than 2.0-beta

[1.0-beta, ) : include 1.0-beta and greater but do NOT include earlier 1.0 pre-release versions.
{1.0-beta, ) : What does this mean?
(1.0-beta, ) : What does this mean?
I agree with Jeff's point on assymetry vs intent.
Regarding the above: looks like denoting a pre-release boundary only makes sense when they are explicit boundaries - square braces [ ].
Could be covered in a package analysis rule during creation, and interpreted as such when parsing the packages.config "allowedVersions" attribute (in the rare event people make this mistake)?
Developer
May 3, 2013 at 11:29 PM
How about:

(, 2.0-beta) : Treat this same as (, 2.0), because the ) never include pre-release.
(, 2.0-beta} : Include any 2.0 pre-release less than 2.0-beta, but does not include 2.0-beta.
(, 2.0-beta] : Include 2.0-beta and earlier pre-release versions of 2.0, but no versions of 2.0 greater than 2.0-beta

[1.0-beta, ) : include 1.0-beta and greater but do NOT include earlier 1.0 pre-release versions.
{1.0-beta, ) : Include any 1.0 pre-release greater than 1.0-beta, but does not include 1.0-beta.
(1.0-beta, ) : Treat this same as (1.0, 0)
May 4, 2013 at 11:26 AM
Edited May 4, 2013 at 3:01 PM
I've been taking a look at how npm does this.

npm allows 'X Version Ranges':
An "x" in a version range specifies that the version number must start with the supplied digits, but any digit may be used in place of the x.
In NuGet, this could look like:

(, 2.0) : do NOT include 2.0 stable or any later versions (2.0 pre-release versions are included).
(, 2.0] : include 2.0 stable and all earlier versions (2.0 pre-release versions are included).
(, 2.x) : do NOT include any versions with a major number of 2 or later (2.0 pre-release versions are NOT included).
(, 2.x] : include any versions with a major number of 2 or earlier (2.0 pre-release versions are included).

[1.0, ) : include 1.0 stable and all later versions (1.0 pre-release versions are NOT included).
(1.0, ) : do NOT include 1.0 stable or any earlier versions (1.0 pre-release versions are NOT included).
[1.x, ) : include any versions with a major number of 1 or later (1.0 pre-release versions are included).
(1.x, ) : do NOT include any versions with a major number of 1 or later (1.0 pre-release versions are NOT included).

Note that the behaviour of ( ) and [ ] are unchanged from the current NuGet release, i.e. technically correct and without any special pre-release treatment.

Moreover, we could even consider deprecating the current ( ) and [ ] syntax and adopting the (IMHO) more intuitive syntax used by both npm and RubyGems based on =, > and >. E.g.:

<2.0 : do NOT include 2.0 stable or any later versions (2.0 pre-release versions are included).
<=2.0 : include 2.0 stable and all earlier versions (2.0 pre-release versions are included).
<2.x : do NOT include any versions with a major number of 2 or later (2.0 pre-release versions are NOT included).
<=2.x : include any versions with a major number of 2 or earlier (2.0 pre-release versions are included).

>=1.0 : include 1.0 stable and all later versions (1.0 pre-release versions are NOT included).
>1.0 : do NOT include 1.0 stable or any earlier versions (1.0 pre-release versions are NOT included).
>=1.x : include any versions with a major number of 1 or later (1.0 pre-release versions are included).
>1.x : do NOT include any versions with a major number of 1 or later (1.0 pre-release versions are NOT included).

This syntax also allows for more complex constraints using || as an OR operator, e.g. if I identify that version 1.1.0 was broken and a fix wasn't added until version 1.3.1, I can do:

<1.1.0 || >= 1.3.1
Developer
May 4, 2013 at 2:35 PM
Interesting ideas but at this time, I'd like us to make as least impact change as possible.
May 4, 2013 at 2:55 PM
Fair enough, the change to a new syntax is quite a radical proposal but I think it's worth considering at some point.

But I do think that addition of X version ranges would have the least impact of all the proposed changes so far. The existing functionality would remain identical so no breaking change. The { } operators would not be needed. It also removes the need to sacrifice technically correct semantics for intuitive semantics for (, 2.0).

Unchanged
(, 2.0) : do NOT include 2.0 stable or any later versions (2.0 pre-release versions are included).
(, 2.0] : include 2.0 stable and all earlier versions (2.0 pre-release versions are included).
[1.0, ) : include 1.0 stable and all later versions (1.0 pre-release versions are NOT included).
(1.0, ) : do NOT include 1.0 stable or any earlier versions (1.0 pre-release versions are NOT included).

New
(, 2.x) : do NOT include any versions with a major number of 2 or later (2.0 pre-release versions are NOT included).
(, 2.x] : include any versions with a major number of 2 or earlier (2.0 pre-release versions are included).
[1.x, ) : include any versions with a major number of 1 or later (1.0 pre-release versions are included).
(1.x, ) : do NOT include any versions with a major number of 1 or later (1.0 pre-release versions are NOT included).
Developer
May 5, 2013 at 2:09 AM
We actually do want to modify the behavior of (, 2.0) to NOT include 2.0 pre-release versions, because we expect that's what people want in a majority of cases.

I don't think the 2.x syntax is easier to understand than { }. A couple of points that I have are:
  • That [1.x, ) includes 1.0 pre-release is not intuitive.
  • Can I specify 2.4.x?
  • Can I specify (, 1.2.3-x) ?
May 6, 2013 at 6:05 AM
That [1.x, ) includes 1.0 pre-release is not intuitive.
For me this is intuitive. 1.0.0-beta01 matches 1.x because it has a major version of 1. However, there is in fact no practical use for this constraint. If I want to specify 1.0 but no pre-release then I would use [1.0, ). If I knew that a given pre-release was OK, I would use [1.0-known-pre-release, ).
Can I specify 2.4.x?
Yes, that means any version with a major of 2 and a minor of 4.
Can I specify (, 1.2.3-x) ?
Now it gets interesting :-). According to SemVer:
A pre-release version MAY be denoted by appending a dash and a series of dot separated identifiers immediately following the patch version. Identifiers MUST be comprised of only ASCII alphanumerics and dash [0-9A-Za-z-].
What this suggests is that I should be able to do something like (, 1.2.3-beta.1.x) which means any version with a major of 1, minor of 2, patch of 3 and first pre-release identifier of beta. However, since x is a valid character in a pre-release identifier, this wouldn't work. In that case we could use * instead, which is not a valid character anywhere in a SemVer version and is therefore a good candidate to be used as a wildcard.

I still think this is a better scheme. It has full logical consistency.

I also think we should still consider changing to <, > and =. I know it's a big change, but sometimes that's a good thing. I don't think that the ( ), [ ] and { } syntax is intuitive. I still have trouble remembering what ( ) and [ ] do- I always have to look it up to double check which one is which and now there's potentially also { } in the mix. The <, > and = operators are elementary. All you have to do is follow SemVer for precedence, * for wildcarding and <, > and = for comparison. With full logical consistency people can then work out everything else for themselves.
May 6, 2013 at 6:12 AM
BTW - I know that the NuGet constraints were inspired by Maven.

Something I find quite amusing is the need for an explanatory mapping table which shows constraints in Maven format and then their 'meaning' expressed using <, > and = (http://maven.apache.org/enforcer/enforcer-rules/versionRanges.html).

The question this begs is, why not just use <, > and = in the first place and do away with the need for this explanation? :-)
May 6, 2013 at 7:58 AM
Although they are easier to understand, <= and > are a pain when written in xml.
May 6, 2013 at 8:34 AM
That's a good point - I hadn't considered that! The joys of XML...

I guess that's why npm uses JSON ;-)
Developer
May 6, 2013 at 3:55 PM
IMO, this syntax isn't more intuitive than the original proposal, and it requires extra effort to parse the new syntax.

I'll let other people chime in.
Developer
May 6, 2013 at 5:50 PM
Reactive thoughts:

{1.0-beta, ) : What does this mean? - It could be disallowed? Validate the nupkg upon create and upload. This sounds like more trouble than we want to go to, if possible.

(1.0-beta, ) : What does this mean? - We already allow this syntax. semver.org (and maybe nuge.core semver class? not sure) imply that there may be some kind of logical ordering on prerelease versions, and people may be relying on that to exclude certain prerelease versions. We should consider preserving that behavior.

New syntax : multiple syntaxes sounds confusing. Clearly changing to <= >= would be a new syntax. Adding a 2.x specifier , is more of an extension to the existing syntax. Which is what we're proposing doing with {}. From this point of view I think it's worth me taking a second look at adamralph's proposal, introducing x:
May 6, 2013 at 5:57 PM
If you want to allow wildcards in the pre-release identifiers then you'll probably need to use * instead of x. It needs to be something which is not permitted in a pre-release identifier.

The < > = syntax was a proposed replacement for ( ) and [ ], which would be deprecated (and removed in 3.0). However, given the nuspec is in XML this won't be workable :-(.
Developer
May 6, 2013 at 6:12 PM
Hm.
How did this bug arise anyway?
We used mathematical ranges () and []. These are designed to be used on numbers, which are a 1-dimensional thing, or to indicate open/closed sets.
Then we created a multi-dimensional versioning system (version_numbers X prerelease_tags), but gave it a projection onto a 1-dimensional precendence relationship.

1.0 pre < 1.0 < 1.1 pre < 1.5 < 2.0 pre < 2.0

And we used [) to specify subsets of this in what we thought was a natural and logical way. But it feels less logical and natural to people because people understand that 2.0 pre is more similar to 2.0 than 1.5, and 1.1 pre is more similar to 1.1 than 1.0.

Now clearly with [) it should be possible to specify any logical range you want - in order from left to right you can do:
[1.0 pre, )
(1.0 pre, 0)
[1.0, )
(1.0, )

And if these worked, they would actually be pretty intuitive.
Only problem is the other half.
May 6, 2013 at 6:16 PM
I agree, the left most range is all covered. It's the right most that's the problem, and all we need there is (, 2.*).
Coordinator
May 6, 2013 at 7:27 PM
I haven't been able to get fully caught up, but I wanted to point out that the wildcard and use of < and > characters does not do anything to help with the immediate problem: People interpret "less than 2.0" to not include pre-release versions of 2.0. I'd like to stay focused on solving that problem as it's a concrete issue that is affecting package consumers and authors alike.
May 6, 2013 at 7:37 PM
I'd argue that this is because under the current scheme, package authors have no way to specify "anything before 2.0 or it's pre-release equivalents". The wildcard would solve this.

Under the current scheme ( ) specify exclusive bounds and [ ] specify inclusive bounds. Versions mean exact versions whether used for a lower bound or an upper bound. I'm not convinced the correct solution to this problem is to break this technical consistency.
Developer
May 6, 2013 at 7:52 PM
tilovell wrote:
Hm.
How did this bug arise anyway?
We used mathematical ranges () and []. These are designed to be used on numbers, which are a 1-dimensional thing, or to indicate open/closed sets.
Then we created a multi-dimensional versioning system (version_numbers X prerelease_tags), but gave it a projection onto a 1-dimensional precendence relationship.

1.0 pre < 1.0 < 1.1 pre < 1.5 < 2.0 pre < 2.0

And we used [) to specify subsets of this in what we thought was a natural and logical way. But it feels less logical and natural to people because people understand that 2.0 pre is more similar to 2.0 than 1.5, and 1.1 pre is more similar to 1.1 than 1.0.

Now clearly with [) it should be possible to specify any logical range you want - in order from left to right you can do:
[1.0 pre, )
(1.0 pre, 0)
[1.0, )
(1.0, )
Continuing where I left off, in order from left to right of end of range again:
(, 1.0 pre)
(, 1.0 pre]
(, 1.0)
(, 1.0]

Problems:
-redundancy: in practice, (1.0 pre,) and [1.0,) are the exact same range - same for (, 1.0) and (, 1.0 pre)
-the original bug - [1.0, 0) does not include prerelease versions wherease (, 2.0) does include prerelease versions
-Clearly introducing {} does not make this existing language any more expressive unless it is a way to 'skip' over elements in a range. We already have a plenty good way of writing ranges

What we've proposed so far is to break the rules of (] working the same as mathematical way of doing things.
This basically works by no longer treating the versions and prerelease versions as a continuous range, instead they are treated as distinct ranges, i.e. separate dimensions.

However, my new observation is that once we start doing this, using 'interval' operators of (] doesn't really make sense in that it is no longer an 'interval'.
Example - say that 1.0 indicates version on the X axis, and 'pre' now indicates a separate dimension on the Y axis:

You can write [(1.0,pre) , (2.0,notpre)) but what does it mean to exclude 2.0,notpre?

When we had things on a straight line, there was only one way to exclude 2.0,notpre, but now there are many:
a) exclude 2.0 and up
b) exclude notpre (and up?)
c) exclude both

So basically I think if we break the straight line metaphor, then the way we do it needs to involve a fundamental rethink of how we specify the group of points in 2-d space that we are allowing. Well it's actually a pretty limited group of points if we treat it as binary off/on in the prerelease dimension, we can lay out our examples as:

1.0-pre 1.1-pre 1.2-pre ... 2.0-pre
1.0 1.1 1.2 ... 2.0

We can start to try to simplify life and work out what our spec language can represent by coming up with 'shapes' we are interested in allowing specifying are (O represents Compatible, X represents not Compatible)

XOOOOX
XOOOOX - allow dependencies on either prerelease or non-prerelease versions

XXXXXXX
XOOOOX - allow dependencies only on release versions [something you can't do with 1-D ranges?]

XOOOOX
XXXXXXX - allow dependencies only on prerelease versions [again something you can't do with 1-D ranges] - would anyone do this?

XXOOOO
XOOOOO - allow dependencies on 1.0 and up, but not 1.0-prerelease

XOOOOO
XXOOOO - allow dependencies on 1.0-prerelease, but not 1.0, but any (pre or not) > 1.0 - would anyone do this?

XOOOOX
XOOOOO - allow depencies on 1.0 and up to 2.0, but not 2.0 prerelease versions - would anyone do this?

XXXXXXO
XOOOOO - allow dependencies on release versions of 1.x, and prerelease and release versions of 2.0 - would anyone do this?

When I think of the efficient ways to express shapes like this, I lean towards having two ranges, one for release versions, and one for non-release versions.

e.g. [1.0-2.0) | (1.0pre-2.0pre] where | represents OR (set intersection)
e.g. {}[1.0pre-2.0pre] where {} represents the empty set

However this feels pretty complicated, and I feel sad that with this idea [1.0-2.0) doesn't let you work with any prerelease versions of e.g. 1.2
Developer
May 6, 2013 at 8:02 PM
Edited May 6, 2013 at 8:03 PM
When I decided to open this thread, I have two goals:
  1. Fix the bug #3184. Note that we do want to change the behavior of the (, 2.0) case to NOT include prerelease, because we think that's what most people expect.
  2. Doing (1) with minimum effort.
The original proposal satisfies both of these requirements. I don't want to introduce major change to the current version spec because I don't think it's worth the effort. We haven't seen a lot of complaint about the behavior () and [], except for the bug #3184.

There's no right or wrong way to address this, because there's no precedent. We just want to pick the solution that has least impact.
May 6, 2013 at 8:29 PM
IMO the expectation specified in the issue is invalid. If I specifiy (, 2.0) then I expect 2.0-a to be used because it is less than 2.0.

If the documentation clearly communicates this and also shows (, 2.x) to be the way to restrict your dependency to be less than any version with a major of 2 whether stable or pre-release then there will never be an expectation of (, 2.0) to NOT include pre-release. The problem (and any incorrect expectation) stems from the original implementation which should have had a way to exclude both stable and pre-release at the upper bound from the start. IMO wildcards are the best way to do this. 2.0 should refer to 2.0 - a specific point on the version precedence scale. It should NOT refer to a range of versions on that scale. Wildcards are the correct mechanism to specify a range of versions.

Anyway, if the NuGet team have already decided that (, 2.0) is going to change to include pre-release then there's no point labouring this point. In that case the only thing worth discussing is how the current behaviour can be preserved where an author requires it and I think the } proposal satisfies that. Note that the introduction of { is surplus to the requirement of the issue but I guess there's no harm in it.

Sorry for labouring the point so far already, but I thought it's worth investigating the issue deeply rather than just scratching the surface. It's been an interesting discussion and it's the most I've thought about versioning for a while! :-)
Developer
May 9, 2013 at 4:57 PM
I agree with you on the expectation of the current upper-bound behavior. The reason being that the pre-release concept was only introduced a few releases after NuGet 1.0. Whereas the [ ] and ( ) spec was there since the beginning of NuGet.

We definitely value your idea about the wildcard. We do think that it's orthogonal to the problem we are trying to fix, meaning that even with the introduction of }, we can still consider adding support for wildcard in the future, if demand arises.

We will go ahead with the { } syntax. Thanks a lot for the valuable feedback.
Coordinator
Jun 10, 2013 at 4:20 PM
Edited Jun 10, 2013 at 4:21 PM
Ugh, sorry I'm late to this. I didn't notice it.
[1.0, ) : include 1.0 stable but do NOT include 1.0 pre-release versions. 
{1.0, ) : include 1.0 stable AND all 1.0 pre-release versions. 
(1.0, ) : do NOT include 1.0 stable or any 1.0 pre-release versions. 
I don't like this part at all. First of all, we chose the range syntax because it corresponds to existing conventions. https://en.wikipedia.org/wiki/Interval_(mathematics)

Second, the choice to include pre-release or not has always been on the consumer.

I do agree with the first part of the proposal: [1.0,2.0) should exclude 2.0.0-beta

We're trying to clarify this behavior in SemVer:
Using mathematical interval notation, https://en.wikipedia.org/wiki/Interval_(mathematics), we'd say the following.

`1.0.0-beta` is in the range `[1.0.0, 2.0.0)` but `2.0.0-beta` is excluded from that range. Yet `1.0.0-beta` still has lower precedence than `1.0.0` (likewise with 2.0.0-beta and 2.0.0).
The idea is that any set that includes a version would also include that version's pre-release packages when you allow pre-release.

I think the worry about receiving 1.0.0-beta when you have [1.0.0,2.0.0) is overblown. Chances are you have 1.0.0 in the feed so you would never practically get 1.0.0-alpha anyways. So why are we worrying about this?

It sounds like you're trying to guard against the worst case scenario. A package specifies a dependency on [1.0.0, 2.0.0) and there is no 1.0.0 in the feed, so they get 1.0.0-beta. Well this would only happen if they specify they want to allow pre-release packages. By default, this would fail without the pre tag.

I think if I as a user specify -Pre and this failed, THAT would violate my expectations. Why didn't you attempt to use 1.0.0-beta to fulfill this dependency? I said to allow pre-release.
Coordinator
Jun 10, 2013 at 4:32 PM
@haacked The problem surfaces because NuGet uses the lowest satisfying version when resolving dependencies. That means a dependency on [1.0.0,2.0.0) would yield 1.0.0-alpha even when there is a beta, rc, and a stable 1.0.0.

By excluding 1.0.0-pre versions from >= 1.0.0, we allow you to depend on the stable version as a minimum. Without that approach, there would be no way to require the stable version as a minimum since, as you said, it is up to the consumer.

If you want to depend on the prerelease as a minimum version however, you can do that with [1.0.0-alpha,2.0.0).
Coordinator
Jun 10, 2013 at 5:30 PM
Ah, I think that is a bug then.

Per the NuGet docs:
If a prerelease package is the latest version, then it is installed. If a stable package is the latest, it is installed.
This was the intent of pre-release. But we were not careful in specifying how that applies to dependencies.

I think there's two ways we can think about this:
  1. You chose to allow pre-release so you want to live life on the edge so let's pick the highest stable or pre-release dependency that matches. This behavior of course differs from our non-prerelease behavior.
  2. We stick as close as possible to the spirit of our current behavior. We choose the lowest STABLE release for the dependency. If no stable release fulfills the dependency, we allow pre-release packages to be considered.
I kind of lean towards number 2 as that addresses the specific issue you brought up with the least amount of change. It seems to me this meets expectations.
Coordinator
Jun 10, 2013 at 5:33 PM
Edited Jun 10, 2013 at 5:35 PM
Just to bolster this case, consider the following scenario:

Package Foo depends on Bar [1.0.0, 2.0.0).

In the feed, we have Bar 1.0.1-alpha, 1.0.1-beta, 1.0.1. Given your original proposal, you would still pick Bar 1.0.1-alpha as that's the lowest satisfying version that fits within the dependency range.

But clearly this defies expectation right? The reason we choose the lowest satisfying version is because it's most likely to be stable with regard to the dependency. It's the version most likely to be close to the version that the author tested against. You should ask @DavidEbbo about this.

So choosing a lower pre-release version than a stable version goes against that expectation.
Jun 10, 2013 at 8:51 PM
I think that the expectation would be to choose the lowest satisfying version on major.minor, and within that subset the highest on patch. So if in the example given 1.0.2 is also in the feed, then that would be the 'expected' result
Jul 25, 2013 at 11:08 AM
Even currently, prerelease is not treated as smaller than stable when queried via API.

e.g. when querying with an upper bound of 2.6 for nuget.core with includePrerelease set to true

http://www.nuget.org/api/v2/GetUpdates%28%29?packageIds=%27nuget.core%27&versions=%270.0.1%27&includePrerelease=true&includeAllVersions=true&$filter=Version%20lt%20%272.6%27&$orderby=Version%20desc&$top=1

This should return 2.6.0-alpha1 as per the version history at https://www.nuget.org/packages/Nuget.Core/
But it returns 2.5.0

whereas setting the upper bound to 2.6.1 does return 2.6.0-alpha1
http://www.nuget.org/api/v2/GetUpdates%28%29?packageIds=%27nuget.core%27&versions=%270.0.1%27&includePrerelease=true&includeAllVersions=true&$filter=Version%20lt%20%272.6.1%27&$orderby=Version%20desc&$top=1