One of the issues I keep running into with the "default open" world: prereleases. As more and more projects move to semantic versioning, it'd be nice to have a built-in type to handle these. Today, this fails:
```C#
Version.Parse("1.0.0-beta1");
Because [`System.Version`](https://msdn.microsoft.com/en-us/library/system.version_properties.aspx) has no concept of pre-release suffixes. I'm not arguing it should be added (that's maybe a little to breaking).
Instead how about a new type? `System.SemanticVersion` The *main* property most people would be interested in is the prerelease label, but other tenants of semantic versioning should be included. Comparisons being a big one. A lot of devs get this wrong on corner cases. I was looking for how to do best structure this and noticed: NuGet is already doing it. Here's their `SemanticVersion` class: ([part 1](https://github.com/NuGet/NuGet.Client/blob/4cccb13833ad29d6a0bcff055460d964f1b49cfe/src/NuGet.Core/NuGet.Versioning/SemanticVersion.cs), [part2](https://github.com/NuGet/NuGet.Client/blob/4cccb13833ad29d6a0bcff055460d964f1b49cfe/src/NuGet.Core/NuGet.Versioning/SemanticVersionBase.cs)).
The API likely needs a little refinement for general use in the BCL, but is there a desire for this from others? I have use cases from parsing package versions to determining which version of Elasticsearch a client is connected to. There's a broad spectrum of semantic versioning use cases now.
Suggestions from others (collating here):
- It should be comparable to `Version` (additional operator overloads)
## API
```c#
namespace System
{
public class SemanticVersion : IFormattable, IComparable, IComparable<SemanticVersion>, IEquatable<SemanticVersion>, ICloneable
{
public SemanticVersion(int major, int minor, int patch);
public SemanticVersion(int major, int minor, int patch, string prereleaseLabel);
public SemanticVersion(int major, int minor, int patch, string prereleaseLabel, string metadata);
public SemanticVersion(int major, int minor, int patch, IEnumerable<string> prereleaseLabel, IEnumerable<string> metadata);
public SemanticVersion(string version);
protected SemanticVersion(Version version, string prereleaseLabel = null, string metadata = null);
protected SemanticVersion(Version version, IEnumerable<string> prereleaseLabels, IEnumerable<string> metadata);
public int Major { get; }
public int Minor { get; }
public int Patch { get; }
public virtual IEnumerable<string> PrereleaseLabels { get; }
public virtual bool IsPrerelease { get; }
public virtual IEnumerable<string> Metadata { get; }
public virtual bool HasMetadata { get; }
public virtual string ToString(string format, IFormatProvider formatProvider);
public override string ToString();
public override int GetHashCode();
public virtual SemanticVersion Clone();
object ICloneable.Clone() => Clone();
public virtual bool Equals(SemanticVersion other);
public virtual bool Equals(Version other);
public virtual int CompareTo(object obj);
public virtual int CompareTo(SemanticVersion other);
public virtual int CompareTo(Version other);
public static SemanticVersion Parse(string versionString);
public static bool TryParse(string versionString, out SemanticVersion version);
public static bool operator ==(SemanticVersion left, SemanticVersion right);
public static bool operator !=(SemanticVersion left, SemanticVersion right);
public static bool operator <(SemanticVersion left, SemanticVersion right);
public static bool operator <=(SemanticVersion left, SemanticVersion right);
public static bool operator >(SemanticVersion left, SemanticVersion right);
public static bool operator >=(SemanticVersion left, SemanticVersion right);
public static bool operator ==(Version left, SemanticVersion right);
public static bool operator !=(Version left, SemanticVersion right);
public static bool operator <(Version left, SemanticVersion right);
public static bool operator <=(Version left, SemanticVersion right);
public static bool operator >(Version left, SemanticVersion right);
public static bool operator >=(Version left, SemanticVersion right);
public static explicit operator SemanticVersion(Version version);
public static bool operator ==(SemanticVersion left, Version right);
public static bool operator !=(SemanticVersion left, Version right);
public static bool operator <(SemanticVersion left, Version right);
public static bool operator <=(SemanticVersion left, Version right);
public static bool operator >(SemanticVersion left, Version right);
public static bool operator >=(SemanticVersion left, Version right);
}
}
As an addition, I'd want to see an associated comparer that could properly sort SemanticVersion
and Version
values together using semantic versioning rules.
Sounds like a good idea. Also quite a few votes (15) in just 5 hours ...
Side question out of curiosity: Are all those 15 folks watching the whole repo or was the issue promoted somewhere (Twitter, MVP summit)?
We need formal API proposal ...
I'm talking with the NuGet team tomorrow, I'll try and follow-up on this after. They obviously have some good insights here that are excellent use cases and considerations. I'll try to pitch a formal API (or provide more info) as soon as time allows afterwards.
Since a major aspects of the semantics of semantic versioning is about compatibility, it could be worth to also provide basic functionality to check whether an actual semantic version is expected to be compatible to a known version (simply put: same major version, same or larger minor, patch)
Sounds good, let us know how that goes - assigning to you for now as you're working on it. Let me know if that changes.
The issue was indeed shared via Twitter by @NickCraver
@Structed that explains it, thanks!
I talked with @yishaigalatzer and @rrelyea at the summit a little about this. I'm not sure they're confident NuGet can use a SemanticVersion in the BCL and I'm not sure either - but let's find out!
Starting with NuGet's current implementation (linked in the issue), here's a first pass at what System.SemanticVersion
could look like. The changes are including Version
comparison operators and conversions, renaming the comparison enum (this will be something we'll have to find common ground on for NuGet to use those overloads, or add methods). I left updated comments on members I thought may be a little confusing.
My hope is that NuGet's SemanticVersion could simply inherit from this at some point. I'm not sure if that's an achievable goal.
``` C#
namespace Sysem
{
public class SemanticVersion : IFormattable, IComparable, IComparable
{
public SemanticVersion(SemanticVersion version); // Copy
public SemanticVersion(int major, int minor, int patch);
public SemanticVersion(int major, int minor, int patch, string releaseLabel);
public SemanticVersion(int major, int minor, int patch, string releaseLabel, string metadata);
public SemanticVersion(int major, int minor, int patch, IEnumerable
protected SemanticVersion(int major, int minor, int patch, int revision, string releaseLabel, string metadata);
protected SemanticVersion(int major, int minor, int patch, int revision, IEnumerable<string> releaseLabels, string metadata);
protected SemanticVersion(Version version, string releaseLabel = null, string metadata = null);
protected SemanticVersion(Version version, IEnumerable<string> releaseLabels, string metadata);
public int Major { get; }
public int Minor { get; }
public int Patch { get; }
public IEnumerable<string> ReleaseLabels { get; }
public virtual bool IsPrerelease { get; }
public virtual string Metadata { get; }
public virtual bool HasMetadata { get; }
/// <summary>
/// Gives a normalized representation of the version, excluding metadata.
/// </summary>
public virtual string ToNormalizedString();
/// <summary>
/// Gives a full representation of the version including metadata.
/// </summary>
public virtual string ToFullString();
public virtual string ToString(string format, IFormatProvider formatProvider);
public virtual bool Equals(SemanticVersion other);
public virtual bool Equals(SemanticVersion other, SemanticVersionComparison versionComparison);
public virtual bool Equals(Version other);
public virtual int CompareTo(object obj);
public virtual int CompareTo(SemanticVersion other);
public virtual int CompareTo(SemanticVersion other, SemanticVersionComparison versionComparison);
public virtual int CompareTo(Version other);
public static bool operator ==(SemanticVersion version1, SemanticVersion version2);
public static bool operator !=(SemanticVersion version1, SemanticVersion version2);
public static bool operator <(SemanticVersion version1, SemanticVersion version2);
public static bool operator <=(SemanticVersion version1, SemanticVersion version2);
public static bool operator >(SemanticVersion version1, SemanticVersion version2);
public static bool operator >=(SemanticVersion version1, SemanticVersion version2);
public static explicit operator SemanticVersion(Version version);
public static bool operator ==(SemanticVersion version1, Version version2);
public static bool operator !=(SemanticVersion version1, Version version2);
public static bool operator <(SemanticVersion version1, Version version2);
public static bool operator <=(SemanticVersion version1, Version version2);
public static bool operator >(SemanticVersion version1, Version version2);
public static bool operator >=(SemanticVersion version1, Version version2);
}
public enum SemanticVersionComparison
{
/// <summary>
/// Semantic version 2.0.1-rc comparison.
/// </summary>
Default = 0,
/// <summary>
/// Compares only the version numbers.
/// </summary>
Version = 1,
/// <summary>
/// Include Version number and Release labels in the compare.
/// </summary>
VersionRelease = 2,
/// <summary>
/// Include all metadata during the compare.
/// </summary>
VersionReleaseMetadata = 3
}
}
```
Some questions:
Version
be included? I'd think this does more harm than good.Version
fail if prerelease labels are present? If so, how?Thoughts?
So anything that doesn't match the original Version is now a release-label?
Can't we establish some commonalities around that? alpha, beta, pre, etc. By providing a text enum we start to solidify common business practices and get people on the same stage. You can still have populated release labels but how often are those strings _plural_?
If you have Version 1.0.9-1pre1
what does that parse into? Are we assuming that 1pre1 is by itself a single release label?
I think that if we're gonna handle sem-ver, let's go ahead and do ranges. Those could be nullable LowerMajor
, LowerMinor
, LowerPatch
as additional fields, and then we could incorporate those into the operator methods. This is because we would assume that your two options are "upper - lower" when you parse things, and that you won't have an array of versions. That would need to be handled with an array of Version or SemVer.
SemanticVersion
? Why not make it sealed
like Version
and be fully immutable like Version
?Clone()
method that returns SemanticVersion
instead of having a copy constructor. SemanticVersion
could also implement ICloneable
explicitly (privately) with it's ICloneable.Clone()
implementation returning the result of calling the strongly-typed Clone()
. This would be more consistent with Version
which has a Clone()
(albeit, not strongly typed) instead of a copy constructor.GetHashCode()
(since it overrides Equals()
and implements IEquatable<SemanticVersion>
)?ToString()
?left
and right
instead of version1
and version2
according to the framework design guidelines.Parse
/TryParse
?I agree that this is a BCL must-have. I am a huge proponent of minimalism as long as people's needs aren't blocked.
@karelz
Side question out of curiosity: Are all those 15 folks watching the whole repo or was the issue promoted somewhere (Twitter, MVP summit)?
I came from Twitter but now that issues that I care about like this have been brought to my attention, I will be watching the repo from now on.
@jcolebrand SemanticVersion is not constrained to those things, so this has to be more flexible per the rules at http://semver.org/ to be generally useful.
I think that if we're gonna handle sem-ver, let's go ahead and do ranges.
I don't disagree...but what's that look like, in API form? Pitch out some ideas?
@justinvp Responses:
What are the scenarios for subclassing SemanticVersion? Why not make it sealed like Version and be fully immutable like Version?
NuGet does additional comparisons in NuGetVersion
, you can find it here. Whether it's best for them to subclass this or not is of course up for debate. Maybe sealing it is better, I'm not sure.
Consider a strongly-typed
Clone()
method that returnsSemanticVersion
instead of having a copy constructor.
Good idea, that's a better approach. Updated.
Override
GetHashCode()
(since it overridesEquals()
and implementsIEquatable<SemanticVersion>
)?
Override ToString()?
Yes, absolutely. I just left these off for conciseness and left them as assumptions - I'll add them back to be explicit this is happening.
The operator parameter names should be
left
andright
instead ofversion1
andversion2
according to the framework design guidelines.
IMO, neither of those are really clear, but if that's consistent elsewhere in the framework of course we should stay consistent. I'll update.
What about
Parse
/TryParse
?
Well now I just look like an idiot for leaving that off. Adding.
Here's a new revision (should we track this in one place at the top, or does that make comments confusing as it's edited? I can see it both ways):
``` C#
namespace Sysem
{
public class SemanticVersion : IFormattable, IComparable, IComparable
{
public SemanticVersion(int major, int minor, int patch);
public SemanticVersion(int major, int minor, int patch, string releaseLabel);
public SemanticVersion(int major, int minor, int patch, string releaseLabel, string metadata);
public SemanticVersion(int major, int minor, int patch, IEnumerable
protected SemanticVersion(int major, int minor, int patch, int revision, string releaseLabel, string metadata);
protected SemanticVersion(int major, int minor, int patch, int revision, IEnumerable<string> releaseLabels, string metadata);
protected SemanticVersion(Version version, string releaseLabel = null, string metadata = null);
protected SemanticVersion(Version version, IEnumerable<string> releaseLabels, string metadata);
public int Major { get; }
public int Minor { get; }
public int Patch { get; }
public IEnumerable<string> ReleaseLabels { get; }
public virtual bool IsPrerelease { get; }
public virtual string Metadata { get; }
public virtual bool HasMetadata { get; }
/// <summary>
/// Gives a normalized representation of the version, excluding metadata.
/// </summary>
public virtual string ToNormalizedString();
/// <summary>
/// Gives a full representation of the version including metadata.
/// </summary>
public virtual string ToFullString();
public virtual string ToString(string format, IFormatProvider formatProvider);
public override string ToString();
public override int GetHashCode();
public virtual SemanticVersion Clone();
object ICloneable.Clone() => Clone();
public virtual bool Equals(SemanticVersion other);
public virtual bool Equals(SemanticVersion other, SemanticVersionComparison versionComparison);
public virtual bool Equals(Version other);
public virtual int CompareTo(object obj);
public virtual int CompareTo(SemanticVersion other);
public virtual int CompareTo(SemanticVersion other, SemanticVersionComparison versionComparison);
public virtual int CompareTo(Version other);
public static SemanticVersion Parse(string versionString);
public static bool TryParse(string versionString, out SemanticVersion version);
public static bool operator ==(SemanticVersion left, SemanticVersion right);
public static bool operator !=(SemanticVersion left, SemanticVersion right);
public static bool operator <(SemanticVersion left, SemanticVersion right);
public static bool operator <=(SemanticVersion left, SemanticVersion right);
public static bool operator >(SemanticVersion left, SemanticVersion right);
public static bool operator >=(SemanticVersion left, SemanticVersion right);
public static explicit operator SemanticVersion(Version version);
public static bool operator ==(SemanticVersion left, Version right);
public static bool operator !=(SemanticVersion left, Version right);
public static bool operator <(SemanticVersion left, Version right);
public static bool operator <=(SemanticVersion left, Version right);
public static bool operator >(SemanticVersion left, Version right);
public static bool operator >=(SemanticVersion left, Version right);
}
public enum SemanticVersionComparison
{
/// <summary>
/// Semantic version 2.0.1-rc comparison.
/// </summary>
Default = 0,
/// <summary>
/// Compares only the version numbers.
/// </summary>
Version = 1,
/// <summary>
/// Include Version number and Release labels in the compare.
/// </summary>
VersionRelease = 2,
/// <summary>
/// Include all metadata during the compare.
/// </summary>
VersionReleaseMetadata = 3
}
}
```
@NickCraver The public SemanticVersion Clone()
method would need to be virtual
based on the current pattern.
Oh yes, updating it that way.
I bet we end up sealed here, but I can't be the one to do it, @davidfowl won't talk to me again.
I think the ReleaseLabels
needs a way to be set from a derived class as well. Not sure if it should just be an IList<string>
or be virtual?
Please don't ever seal, that's just an unnecessary constraint.
Version ranges do not belong in dotnet core, they are a nuget feature that we plan to keep expanding. Either all of these go to a package from nuget we can keep revving without being tied to dotnet releases as we add new features or stick with just version.
Metadata should not be part of the comparison nor should imho the enum be there. If you want to compare the core version just get the version component out of it. Keep it simple.
Semver only supports 3 levels of version, but nuget always allowed 4 to match backward compatibility
A semver1 vs semver2 class or distinction is required.
One issue with the equality operators is this scenario:
Version version = new Version(1, 0, 0);
SemanticVersion semver = new SemanticVersion(1, 0, 0);
var equal2 = semver == version; //Compiles
var equal1 = version == semver; //Does not compile
I'm not a big fan of having equality operators having left and right be in a specific order, by type.
This can be solved in a few ways.
Version
.SemanticVersion
with left
and right
swapped, e.g.:csharp
public static bool operator ==(SemanticVersion left, Version right);
public static bool operator ==(Version left, SemanticVersion right);
public static bool operator !=(SemanticVersion left, Version right);
public static bool operator !=(Version left, SemanticVersion right);
//etc...
Personally I like the idea of Version
also having matching operators. I suppose there is probably precedent somewhere in the framework that should be followed for how this might be solved.
This also opens up the discussion that currently a SemanticVersion
knows how to compare itself to a Version
, but a Version
doesn't know how to compare itself to a SemanticVersion
.
I _think_ it would make sense for that to all be a single proposal, otherwise it's difficult to see how all of the pieces fit together.
@yishaigalatzer Good thoughts, much appreciated.
Version ranges do not belong in dotnet core, they are a nuget feature that we plan to keep expanding. Either all of these go to a package from nuget we can keep revving without being tied to dotnet releases as we add new features or stick with just version.
I agree on ranges (and didn't include them). If they are _only_ NuGet specific then yes, we should leave them out and hopefully leave this extensible where NuGet can use the base SemanticVersion
, if that can be reasonably done. I'm not sure I follow on the second half. Many more people than NuGet have a need for SemanticVersion
(hence this proposal), it should be more widely available that that, and IMO, a BCL include as Version
is. I'm not 100% against it being in another library, e.g. System.SemanticVersioning
, but can you provide some examples of how NuGet has evolved this?
A few followup questions:
Nuget.Versioning
? If so, have the bits in this base version revved any, or just the inheritor NuGet would maintain? I'm trying to ascertain the split of where the changes are happening.Metadata should not be part of the comparison nor should imho the enum be there. If you want to compare the core version just get the version component out of it. Keep it simple.
After reading this, I agree. Thoughts from others on nuking the enum and overloads?
A semver1 vs semver2 class or distinction is required.
Perhaps a return of the version in enum form (which can be added to in a non-breaking way) would be the best option here? e.g.:
C#
public SemanticVersionSpecification VersionSpecification { get; };
...
public enum SemanticVersionSpecification {
Version1,
Version2
}
...these are terrible names, but you get the idea. We could determine the minimum (or maximum) level it's compatible with similar to how NuGet does this today.
@vcsjones I looked at the first example I could think of being added later: BigInterger
, and it implements both directions, though it's in another library so I think it's viewable to do either way given the inclusion situation.
@NickCraver
I looked at the first example I could think of being added later: BigInterger, and it implements both directions
That seems the best way to go. The more I think on it, the more I dislike the idea of Version
knowing anything about SemanticVersion
. I would then suggest that the following be added to the proposal:
public static bool operator ==(Version left, SemanticVersion right);
public static bool operator !=(Version left, SemanticVersion right);
public static bool operator <(Version left, SemanticVersion right);
public static bool operator <=(Version left, SemanticVersion right);
public static bool operator >(Version left, SemanticVersion right);
public static bool operator >=(Version left, SemanticVersion right);
@NickCraver
string
field but rather an IEnumerable<string>
just like ReleaseLabels
. The semver "spec" has specific requirements about how these have to be constructed so it would seem like a good place to put the logic and not require users to look up and implement it themselves.Clone()
is needed.sealed
)sealed
topic: If i build an API with it, i would expect the behaviour to be the one defined by the BCL and not from a different user's subclass - having an overwritten CompareTo()
on an instance passed to an API method, this can potentially blow up. Or a subclass that returns a different metadata string on every call because someone really tries to break my code :trollface: .class SemanticVersion3 : SemanticVersion
. But that would break any expectation about the behaviour just as much as a custom subclasses would.Addition:
I would also very much like to see an AssemblySemanticVersionAttribute
that makes use of the proposed SemanticVersion
. Would be a piece of 🍰 to generate using the new msbuild system and be of great use for diagnostic / logging purposes.
I would also very much like to see an
AssemblySemanticVersionAttribute
that makes use of the proposedSemanticVersion
. Would be a piece of 🍰 to generate using the new msbuild system and be of great use for diagnostic / logging purposes.
It would be super nice to be able to do this in one attribute instead of two:
c#
[assembly: AssemblyVersion("2.0.10.0")]
[assembly: AssemblyInformationalVersion("2.0.10")] // Or sometimes "2.0.10-rc.1"
This implementation of SemVer might be worth a look: https://github.com/AndyPook/SemVer There are some unit tests based on the spec.
:question: Why is the operator to convert Version
to SemanticVersion
an explicit operator instead of implicit? I'm generally a fan of avoiding implicit operators, but this seems like a case where the conversion would always succeed without loss of information or broken behavior.
:question: Is there ever a case where semVer {operator} (SemanticVersion)ver
is not the same as semVer {operator} ver
(where ver
is a Version
)?
Another question:
SemVer does not have 4 components in its version number, it has major
, minor
, patch
. However, this API proposal allows creating an instance from a Version
, either by cast or through the constructor. There are also constructor overloads that accept revision
. However, it isn't exposed as a property like the other components.
What is the purpose of this 4th component? Are we just ignoring it? If so, then @sharwell's suggestion of an implicit cast is a no-go since the 4th component is ignored. If we aren't ignoring it, are we OK deviating from the SemVer spec?
I'm not sure about the comparisons. Given 0.9, 0.9.1 and 1.0-beta, surely one always considers 1.0-beta the highest, but vary one whether or not one filters it out of consideration?
Nuget respects all 4 due to historical reasons and hence it can't be dropped (at least in the nuget type)
From: Jon Hanna notifications@github.com
Sent: Dec 2, 2016 11:32 AM
To: dotnet/corefx
Cc: Yishai Galatzer; Mention
Subject: Re: [dotnet/corefx] Proposal: Adding System.SemanticVersion (#13526)
I'm not sure about the comparisons. Given 0.9, 0.9.1 and 1.0-beta, surely one always considers 1.0-beta the highest, but vary one whether or not one filters it out of consideration?
-
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHubhttps://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Fdotnet%2Fcorefx%2Fissues%2F13526%23issuecomment-264541657&data=02%7C01%7Cyigalatz%40microsoft.com%7C35425a522d964b53c7ab08d41ae9e551%7C72f988bf86f141af91ab2d7cd011db47%7C1%7C0%7C636163039256770159&sdata=TpOf31p%2FFKiBgbDDucfbgrwSfJ7PnjPffBSPPdJ%2BGXQ%3D&reserved=0, or mute the threadhttps://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Fnotifications%2Funsubscribe-auth%2FABLmt9tG6qPjiygl6FsuOAz5yKC0zeLzks5rEHIygaJpZM4Kt7_i&data=02%7C01%7Cyigalatz%40microsoft.com%7C35425a522d964b53c7ab08d41ae9e551%7C72f988bf86f141af91ab2d7cd011db47%7C1%7C0%7C636163039256770159&sdata=Ok6ojCfSCQfi%2FaBVvyvL58dOpnFa2mMWsmbz2zDazfk%3D&reserved=0.
@NickCraver I have updated your initial comment to contain the proposed API. Please take a look and let me know if it looks right.
@yishaigalatzer can you please review the API proposal as well?
@yishaigalatzer is OOF, he suggested to ask @rrelyea and @emgarten from NuGet team for the API review.
@karelz @AlexGhiondea I feel pretty strongly that these should also be part of the proposal: https://github.com/dotnet/corefx/issues/13526#issuecomment-260192413. Otherwise, the equality operators are not symmetric.
This is the same design as BigInteger
as @NickCraver pointed out, and solves the problem where version == symanticVersion
would not compile but symanticVersion == version
would.
@vcsjones agreed. I updated the comment at the top with those APIs.
@rrelyea, @emgarten did you get chance to review the API? We have BCL API review tomorrow morning and would like to make the call ideally.
IEnumerable<string>
makes sense as @dasMulli pointed out, it should be the same as ReleaseLabels, semver has similar rules for them. NuGet just didn't have a reason to read them individually.Release
property that joins the release labels together might be helpful, but this could also be done with the format provider. Same for Metadata if it is moved to IEnumerable.2.0.1-rc comparison
mentioned in the comments refers to comparing release labels case-insensitively, which was never approved. For NuGet treating 1.0.0-BETA and 1.0.0-beta as completely different versions does not work well for URLs, and it causes problems when trying to extract them to disk on a case-insensitive file system. I think it is worth discussing what the behavior should be for System.SemanticVersion, and if it would be confusing to differ from NuGet.Metadata
and ReleaseLabels
should be the same in terms of virtual or non-virtual.NuGet could build on this class and expose the 4th digit for legacy. Much of the logic is in version comparer and formatter, if those could also be overridden in places to allow keeping the difficult release label comparing the same, but allowing extra comparing on the revision it would be perfect. 🎉
@karelz @emgarten Can we clarify if this proposal is going to have 4 components to cover NuGet's needs, since they do require 4 version components, or that this is supposed to be a true reflection of SemVer?
I'm holding my breath and hoping you'll do the best thing for the wider BCL consumer base, even if that isn't ideal for NuGet.
@NickCraver do you want to incorporate NuGet team feedback from @emgarten above?
Can we clarify if this proposal is going to have 4 components to cover NuGet's needs, since they do require 4 version components, or that this is supposed to be a true reflection of SemVer?
I would expect System.SemanticVersion to be 3 parts and follow the SemVer rules. NuGet can continue supporting the legacy 4 part versions separately.
Here's a revised API based on feedback that mostly removes the NuGet specific pieces. I want to sanity check here before updating the top (since GitHub doesn't show history):
```c#
namespace System
{
public class SemanticVersion : IFormattable, IComparable, IComparable
{
public SemanticVersion(int major, int minor, int patch);
public SemanticVersion(int major, int minor, int patch, string releaseLabel);
public SemanticVersion(int major, int minor, int patch, string releaseLabel, string metadata);
public SemanticVersion(int major, int minor, int patch, IEnumerable
public SemanticVersion(string version);
protected SemanticVersion(Version version, string releaseLabel = null, string metadata = null);
protected SemanticVersion(Version version, IEnumerable<string> releaseLabels, IEnumerable<string> metadata);
public int Major { get; }
public int Minor { get; }
public int Patch { get; }
public virtual IEnumerable<string> ReleaseLabels { get; }
public virtual bool IsPrerelease { get; }
public virtual IEnumerable<string> Metadata { get; }
public virtual bool HasMetadata { get; }
public virtual string ToString(string format, IFormatProvider formatProvider);
public override string ToString();
public override int GetHashCode();
public virtual SemanticVersion Clone();
object ICloneable.Clone() => Clone();
public virtual bool Equals(SemanticVersion other);
public virtual bool Equals(Version other);
public virtual int CompareTo(object obj);
public virtual int CompareTo(SemanticVersion other);
public virtual int CompareTo(Version other);
public static SemanticVersion Parse(string versionString);
public static bool TryParse(string versionString, out SemanticVersion version);
public static bool operator ==(SemanticVersion left, SemanticVersion right);
public static bool operator !=(SemanticVersion left, SemanticVersion right);
public static bool operator <(SemanticVersion left, SemanticVersion right);
public static bool operator <=(SemanticVersion left, SemanticVersion right);
public static bool operator >(SemanticVersion left, SemanticVersion right);
public static bool operator >=(SemanticVersion left, SemanticVersion right);
public static bool operator ==(Version left, SemanticVersion right);
public static bool operator !=(Version left, SemanticVersion right);
public static bool operator <(Version left, SemanticVersion right);
public static bool operator <=(Version left, SemanticVersion right);
public static bool operator >(Version left, SemanticVersion right);
public static bool operator >=(Version left, SemanticVersion right);
public static explicit operator SemanticVersion(Version version);
public static bool operator ==(SemanticVersion left, Version right);
public static bool operator !=(SemanticVersion left, Version right);
public static bool operator <(SemanticVersion left, Version right);
public static bool operator <=(SemanticVersion left, Version right);
public static bool operator >(SemanticVersion left, Version right);
public static bool operator >=(SemanticVersion left, Version right);
}
}
```
Open questions:
string version
constructor, for parity with Version
.System.Version
with a Revision > 0 do?@NickCraver looks good!
I would not add the string representations for Metadata/Release -- do you have a use case in mind where that would be useful?
What do you mean by
What does System.Version with a Revision > 0 do?
I am curious about the protected method that has optional parameters on it -- is that needed in the implementation?
I ended up writing my own SemanticVersion
, so would love to see one in the BCL
.
A question on HasMetadata
; does it need to be virtual
, any reason is can't simply be:
public bool HasMetadata => Metadata != null;
(?)
@NickCraver
Version
I think the most important question is what to do with a System.Version
where Revision > 0. If at all possible, I would want the answer to this question to have the following characteristic:
For all SemanticVersion s and Version v, and operator op in (==
, !=
, <
, <=
, >
, >=
):
:bulb: If the characteristic holds, I believe we can make the following changes:
public virtual bool Equals(SemanticVersion other);
-public virtual bool Equals(Version other);
public virtual int CompareTo(object obj);
public virtual int CompareTo(SemanticVersion other);
-public virtual int CompareTo(Version other);
public static SemanticVersion Parse(string versionString);
public static bool TryParse(string versionString, out SemanticVersion version);
public static bool operator ==(SemanticVersion left, SemanticVersion right);
public static bool operator !=(SemanticVersion left, SemanticVersion right);
public static bool operator <(SemanticVersion left, SemanticVersion right);
public static bool operator <=(SemanticVersion left, SemanticVersion right);
public static bool operator >(SemanticVersion left, SemanticVersion right);
public static bool operator >=(SemanticVersion left, SemanticVersion right);
-public static bool operator ==(Version left, SemanticVersion right);
-public static bool operator !=(Version left, SemanticVersion right);
-public static bool operator <(Version left, SemanticVersion right);
-public static bool operator <=(Version left, SemanticVersion right);
-public static bool operator >(Version left, SemanticVersion right);
-public static bool operator >=(Version left, SemanticVersion right);
-public static explicit operator SemanticVersion(Version version);
+public static implicit operator SemanticVersion(Version version);
-public static bool operator ==(SemanticVersion left, Version right);
-public static bool operator !=(SemanticVersion left, Version right);
-public static bool operator <(SemanticVersion left, Version right);
-public static bool operator <=(SemanticVersion left, Version right);
-public static bool operator >(SemanticVersion left, Version right);
-public static bool operator >=(SemanticVersion left, Version right);
:bulb: At this point, I would recommend making the type sealed
. In particular, I don't find the use case of attempting to layer NuGet's pseudo-semver handling on top of the proposed SemanticVersion
class to be a compelling reason to make the new SemanticVersion
class inheritable.
:question: Why are we implementing ICloneable
here?
:bulb: Since SemanticVersion
is immutable, I would suggest removing the ICloneable
interface and both Clone
methods.
-public class SemanticVersion : IFormattable, IComparable, IComparable<SemanticVersion>, IEquatable<SemanticVersion>, ICloneable
+public class SemanticVersion : IFormattable, IComparable, IComparable<SemanticVersion>, IEquatable<SemanticVersion>
{
...
- public virtual SemanticVersion Clone();
- object ICloneable.Clone() => Clone();
:question: Is there any reason not to provide access to a list of information instead of just an enumerable?
-public virtual IEnumerable<string> ReleaseLabels { get; }
+public virtual IReadOnlyList<string> ReleaseLabels { get; }
public virtual bool IsPrerelease { get; }
-public virtual IEnumerable<string> Metadata { get; }
+public virtual IReadOnlyList<string> Metadata { get; }
public virtual bool HasMetadata { get; }
@sharwell I'm still not sure how revision should behave, but any exceptional behavior is made far less clear with the implicit operator, IMO.
To address points specifically:
Revision
): it's not the comparison that failed, but a cast I didn't make. That's unintuitive for any use case. It also means we can't add any intellisense to those specific overloads telling the user why it'd blow up for the Revision
cases (if that ends up being the behavior).SemanticVersion
on every comparison. That's a pretty bad inefficiency.System.Version
here (and I assume: so is Nuget). I can't think of daily uses - perhaps someone can comment on System.Version's ICloneable history?@AlexGhiondea
What do you mean by "What does System.Version with a Revision > 0 do?"
The issue there is say we have a System.Version
with a Revision of 6, what do we compare that to? It's basically a loss of precision issue since that 4th field is not a concept in semantic versioning.
I am curious about the protected method that has optional parameters on it -- is that needed in the implementation?
Depends if we seal, if it's sealed, that certainly changes.
@Worthaboutapig: Same story, depends on sealed
or not. And the backer. I assume we'll likely store as a minimal ReadOnlyList<T>
type on the backend, and assume null
would be the case for no items passed to the constructor, yes. There's no reason to allocate if we have no members on the passed enumerable.
@NickCraver
explicit
and still remove all the other items, forcing the user to cast if they plan to use Version
.@sharwell While I agree that conversion may meet your conditions, it doesn't fit the scenario. You're effectively tossing away the revision precision (the root issue), or we decide to compare metadata as numbers for all comparisons...which also doesn't make much sense in a global approach.
As for allocations - we're shying away from that across the framework. I agree an explicit cast and API reduction is probably the happy medium there. Or, a constructor that takes Version
simply being public...since that'd have to be the underlying implementation anyway.
@sharwell if we just add Build and Revision you can get the wrong answer.
Consider the case where you have 2 versions: 1.0.0.4
and 1.0.3.0
If you convert them to SemanticVersion
you are going to get 1.0.4
and 1.0.3
which is not what you want.
If we want to normalize Build and Revision we can do something like Build*100 + Revision.
To take the example from above, using this transformation you will get 1.0.4
and 1.0.300
which will maintain the relationship between the two versions.
The downside of this is that they are now looking rather ugly... and you might end up with an overflow when doing the transformation.
Stepping back, I think this type is going to be useful as a whole so I would like to move forward with a formal API review and discuss these few remaining open issues during the meeting. The behavior of the Revision != 0 conversion is an implementation detail and should not block progress on the API shape.
Looking at the API shape, the only think I am not sure of is the use of optional parameters.
@NickCraver - other than the behavior for Revision != 0 question is there anything else we need to figure out before we submit this for API review ?
@AlexGhiondea I think it's ready for review and most productive in a call. The Revision
detail may affect API shape though (the necessary comparison operators to independently handle it may or may not be required). Let's see what a live discussion yields :)
@NickCraver great! I have marked it as such!
Would you mind updating the top post with the most recent shape of the API to make it easy to find during the review?
@AlexGhiondea absolutely, done!
Since only pre-release versions have labels, would it make sense to rename ReleaseLabels
to PrereleaseLabels
?
Since there is constructor that takes a string releaseLabel
parameter, it may mislead users unfamiliar with the semver spec to assume this parameter to be any form of metadata (e.g. "RTM"
) and then be surprised that IsPrerelease
is true
. This would be different if the parameter (and the property) was string prereleaseLabel
.
Since only pre-release versions have labels, would it make sense to rename ReleaseLabels to PrereleaseLabels?
Yes. Please.
PrereleaseLabels
makes a lot of sense to me, it's clearer and they can't be used any other way.
Serious question: how is this going to be used in practice? I fully understand the lack of ability to store pre-release info is a problem with System.Version, no questions there. The problem I see a bit is that an assembly currently has two version info elements: a System.Version instance and a FileVersionInfo element of its file (on windows at least). These often differ: the System.Version part is kept the same to please strong naming and the FileVersion is updated to have a way to differ the real version (something the .NET full framework has used for years, if I might add).
Adding SemanticVersion adds a third version to an assembly. If we see an assembly as a nuget package, it has 4: the nuget package can be versioned separately.
Aren't we creating more confusion by trying to get rid of confusion? I mean: if an assembly has a SemanticVersion property, why look at it if there's a Version property too, and vice versa? Doesn't an assembly have just one version? Or more importantly: isn't SemanticVersion a specific 'Version' (and therefore should be a subtype) ? Or is SemanticVersion the same as FileVersionInfo, an informative element that you can use if you want to to inform yourself about the real version, but isn't really used in code / machinery?
@FransBouma I don't understand tying all reasoning to .NET assemblies here, SemanticVersion
is used in far more places than that. What initiated this was me parsing Elasticsearch versions, as an example. Tens of thousands of software projects, package management systems, etc. use this.
Even in the .NET case, this reasoning is not correct. Semantic versioning is applied to a package, not an assembly. It's already in use this way today, right now. It's simply tucked away in the NuGet code and unavailable for the many thousands of other use cases we have. This proposal aims to resolve all that, and open it up to those additional use cases, via availability in the BCL.
As for the difference: Version
is sealed
, and semantic versioning has no 4th (build) number. They are not compatible. The comparison semantics would become quite complicated as well.
Ah ok, then I misunderstood, as System.Version's usage is on assemblies (and elsewhere) and I wondered where this is going to be stored, as it was unclear to me it would be used to deal with nuget packages, not assemblies. Thanks for clearing that up!
@FransBouma I use it for database and file format versioning, as well as AssemblyInformationalVersion.
Note: I updated ReleaseLabel
to PrereleaseLabel
(and corresponding arguments) per discussion above - top post has the updated API.
Shouldn't this be a struct? It's naturally a value type in terms of equality, it's immutable, and it has all of the typical interfaces/operators of a value type. And who needs virtual members on it anyway? For example, does anybody really need to derive from SemanticVersion
to override the Boolean IsPrerelease
property? :-) Instead, how about a static factory method and a builder pattern with params arrays to make it declarative?
SemanticVersion alpha150 = SemanticVersion.Prerelease(1, 5, "alpha", "label2", "label3")
.WithMetadata("meta1", "meta2", "meta3");
Shouldn't this be a struct? It's naturally a value type in terms of equality, it's immutable
I couldn't agree more.
I think the type should be "reasonably immutable" meaning it should be either a struct
or a sealed class
with read-only properties only.
I don't think it should be valid to subclass SemanticVersion
as it would break any assumption one would have about it's equality and the behaviour of the getters / methods. (So no one can "sneak in" unexpected behaviour)
For cases when you'd need a custom version (e.g. company-specific versioning that has e.g. ?signedOff=true
) it is perfectly reasonable to copy the code and modify it, creating a new MyCompanyVersion
type and implementing conversions wherever possible.
I hate sealing things just because someone _might_ do something naughty. Why bother with interfaces and polymorphism at all? If you want to be certain that you are really working with an unaltered SemanticVersion
then do some type checking.
@dasMulli I don't think having to copy/paste/change a type is a good path to go down. I would like us to come up with a type that is extensible in meaningful ways and enables as many people as possible to use it.
@AlexGhiondea Why would you have to copy/paste/change this type? It should be a type that does one thing and does it well (single responsibility principle); namely, it implements the "semver" spec. If you need it to do anything else, then why not just use object composition instead? That way you'll avoid confusing people, rather than changing the meaning of members on a standard type.
public class MyFunFlexibleType
{
public SemanticVersion SemVer { get; set; }
public virtual bool IsPrereleaseForRealzOrSomethingElseMaybe { get; set; }
}
Yes copy&modify would be pretty extreme.
@somewhatabstract i share your concern that classes should not be sealed by default. But here we need to distinguish between "logic" classes and types for fixed data representation.
One could even argue that the proposed type can be split into a type storing the fields and types doing the semver interpretation.. (but I would then argue that the "semver spec" defines both and a possible "semver 3.0.0" could require changes to both aspects).
I would instead look for compelling reasons to subclass that do not violate the semver spec. If we truly care about polymorphism, then: is a type that for example implements IsPrerelease
differently still behaving as a "semantic version"? I'd argue that this is not the case and either create an extension method (IsSafeToDeployToProduction()
), another type through composition as @RxDave showed or make an IPrereleaseVersioningStrategy
a thing for my logic (like implementing the linux kernel versioning strategy where all odd minor versions were considered development builds).
I'd say it is like DateTime
and DateTimeOffset
. DateTimeOffset
is not a DateTime
from a type system perspective but offers appropriate conversions (.DateTime
, .UtcDateTime
). Also, no one would should consider subclassing DateTime
to make .Year
return a value using a different calendar and use a Calendar
instance instead where this special year value is necessary (not discussing that having .Year
and friends on DateTime
was probably a bad idea anyway).
We talked about two major aspects of this proposal offline while I was on campus this week. They were:
The first needs community help. A decent addition to the BCL includes usage in the BCL, or a large set of known usages outside the BCL (e.g. being a handy class for many consuming the BCL).
Chiming in here would help frame the worth greatly. Add them up and we can maintain a distinct list at the top. Use cases inside the BCL (or not) also help the decision of where it lives.
As an example, one use case I'd like is [assembly:SemanticVersion("1.0.0-alpha1")]
, so that we can determine which DLLs are pre-release. Production apps don't have packages in most scenarios, they have a directory of DLLs. Currently you cannot lineup the DLL version inside a pre-release package with the package. So you either have a string version in the informational attribute, or something that doesn't match (usually the same version without the pre-release suffixes, which is not correct). A standard place would be an improvement here, IMO.
The second question of where does it live is a little more complicated. It doesn't have a set of sibling classes that'd denote a namespace (e.g. System.Versioning
). So do we put it in the BCL as System.Version
? Do we put it in a NuGet package by itself? If so, who maintains it? Me or Microsoft. If Microsoft, where does the repo live so that it can be updated? Is this a good strategy to put all one-offs in their own package? What about a CodeFX.Contrib
repo? Would it be one package or many? There's a lot on the discussion table here and we agreed there's no clear win scenario. No one wants to see CoreFX be a dumping ground of one-off classes, but where do useful things go?
If we don't pass the bar for number 1, then 2 doesn't matter. So let's see where the first stands with feedback here.
cc @terrajobst @karelz in case they want to expand on any of the above
Thanks @NickCraver for writing it up!
For context: We have been discussing the general topic internally since start of December (and there is no obvious consensus). I was trying to find best way to bring the discussion to the open - this particular issue/API looks like a good candidate for that.
Regarding 2 - it is interesting general discussion regardless how 1 ends. At minimum I expect it will keep coming back in future API proposals.
cc: @danmosemsft @stephentoub @KrzysztofCwalina @weshaggard
I know that we would use this extensively in PowerShell scripts for builds and deployments, both locally in our dev environments and on CI servers. It would also be incredibly helpful to us in our production deployments. We work in healthcare and being able to tag versions with metadata would be very helpful in tracking what has been deployed to which customers and in turn, using that for audit logs of usage.
That said, the most value for this is in the management and interaction with contemporary package management systems, which use semantic versioning. This probably narrows the audience down to mostly those in the development arena, which may make the bar too low for inclusion in the BCL since production usage seems less likely, though I would hope that is not the case.
I know we would use this a lot (it would certainly tidy up some of our hacks that fill in the gap where it should be). I do worry about how these things get packaged if not in the BCL since one package per feature would make package restore a horrible experience in the long term but bundling too many things together also feels wrong (shipping tons of code that doesn't get used). That's an age old problem though and I don't know what the right answer is.
Besides covering vast consumer requirements, do we need to consider SemVer's own Spec versions?
would this library be compatible with both SemVer 1 and SemVer 2 specs: http://semver.org/ (diff: https://github.com/mojombo/semver/compare/v1.0.0...v2.0.0#files_bucket)?
Shortly, from this comment https://github.com/mojombo/semver/issues/231#issuecomment-278617758, there are at least two breaking changes between spec v1 and v2:
- leading zeros are not accepted (this one matters for library implementation viewpoint)
- minor version numbers MUST be incremented if API is marked as deprecated (this is more of the usage behavior), so doesn't effect the library implementation
One way could be to add an overloaded ctor:
```c#
...
...
public SemanticVersion(string version) : this(version, SemVerSpecVersion.Version1)
public SemanticVersion(string version, SemVerSpecVersion version)
// this should throw if there is leading zero, e.g.
// SemanticVersion("1.02.3", SemVerSpecVersion.Version2)
```
how would this library keep up with the future (v3, v4) SemVer specs?
how would this library keep up with the future (v3, v4) SemVer specs?
Since any new major version of SemVer will be a breaking change by definition, it will likely be a breaking change to pass a SemVer3 version to a piece of code expecting a SemVer2 version.
As much as I hate numbers on type names, future variant names like SemanticVersion3
, SemanticVersion4
could make sense since here. As long as there are conversion methods/constructors/operators(/interfaces?), it is probably more manageable from a type safety perspective than adding features to SemanticVersion
and having a weird version property on a version type.
For all the pkg management eco-systems that have adopted semver (NuGet, npm, PowerShell, Conan, etc) it is a major bummer that .NET Core still does not provide support for semantic versions.
You mean all of those ecosystems have first class support in their BCL for parsing semantic versions? Is that true? Can you point me at the examples?
@davidfowl no they don’t. And even npm is not “in node” anyway.
NuGet:
https://github.com/NuGet/NuGet2/blob/2.13/src/Core/SemanticVersion.cs
npm - well this one claims to be "the semantic versioner for npm"
https://github.com/npm/node-semver
PowerShell:
https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.semanticversion?view=pscore-6.2.0
Conan - OK Conan's a bit weird - I'd say they have quasi-semver support. The version numbers we use for our Conan pkgs are semver and they support specifying version ranges in conanfile.txt/py that appear to obey semver rules.
https://github.com/conan-io/conan/blob/b38f6c5a6de87f9bf63f481a1d7dfbcf1abd87fd/conans/model/version.py
Ah, sorry in their "BCL equivalent". OK fair point but I thought frameworks were about providing common functionality so that it doesn't get re-invented over and over (like in NuGet and PowerShell)?
The last previous 5 comments seem to be the most helpful and explain why this story has been going on for many years without progress.
We could close this discussion and do nothing - we (.Net ecosystem) have been living this way for the last 4 years and nothing terrible has happened, which means it will not happen.
But a reasonable question arises - _can we still do something useful for .Net ecosystem?_
Since .Net developers use Semantic Version standard and communicate with other ecosystems which use Semantic Version then the reasonable answer is yes.
But that would take us 4 years back to the beginning of this discussion - and this begs the next question -
The reasonable answer is: _System.SemanticVersion
class is not what we need._
This answer seems counterintuitive, but it explains a lot. Let's see what all ecosystems have in common.
- Whole ecosystem (all projects, services and so on) depends on the rules.
- Yes, changing something in .Net Core versioning could break all third-party projects and services.
Let's look PowerShell ecosystem. If we move PowerShellGet to Semantic Version we should:
So moving to Semantic Version causes an avalanche of changes - this is a catastrophe. We need a lot of effort not to ruin everything and find workarounds and tradeoffs for backward compatibility.
The solution that does not make _breaking changes_ will take root.
Then it becomes obvious that the only solution that will make progress is to enhancement the System.Version class itself.
Most helpful comment
As an addition, I'd want to see an associated comparer that could properly sort
SemanticVersion
andVersion
values together using semantic versioning rules.