Découvrez NuGetVersion, le type .NET adapté à la manipulation des versions incluant préversions et métadonnées.

En .NET, lorsqu'on doit manipuler une version, le premier réflexe est généralement d'utiliser System.Version.

C'est logique : le type fait partie de la BCL, il permet de représenter, parser et comparer simplement des numéros de version.

Version version = new Version(2, 4, 1);

Console.WriteLine(version.Major); // 2
Console.WriteLine(version.Minor); // 4
Console.WriteLine(version.Build); // 1

Tout va bien tant que nos versions ressemblent à ceci :

1.0
1.2.3
2.4.1.7

Mais les versions de nos applications, bibliothèques ou outils ressemblent aujourd'hui souvent davantage à ceci :

1.5.0-preview.1
1.5.0-beta.2
1.5.0-rc.1
1.5.0

Et là, System.Version atteint rapidement ses limites.

Les limites de System.Version

Essayons simplement de parser une version contenant un suffixe :

Version version = Version.Parse("1.5.0-preview.1");

Le résultat est une FormatException.

Ce comportement est parfaitement normal. System.Version n'a pas été conçu pour représenter ce type de version.

La documentation .NET définit son format ainsi :

major.minor[.build[.revision]]

Les différents composants sont numériques. System.Version représente historiquement des numéros de version d'assembly, de système d'exploitation ou du CLR.

Le problème n'est donc pas que System.Version soit un mauvais type. Nous essayons simplement de lui faire représenter quelque chose qui ne correspond plus à son modèle.

Et SemVer dans tout ça ?

Les versions logicielles modernes suivent fréquemment la convention Semantic Versioning — généralement abrégée SemVer.

Une version SemVer classique prend la forme :

MAJOR.MINOR.PATCH

mais la spécification permet également d'ajouter une préversion après un - et des métadonnées après un + :

1.4.0
1.5.0-preview.1
1.5.0-rc.2
1.5.0+build.428
1.5.0-preview.2+build.428

L'objectif de cet article n'est pas d'expliquer Semantic Versioning — la spécification officielle le fait déjà très bien — mais cette différence suffit à mettre en évidence notre problème.

System.Version sait parfaitement représenter :

1.5.0.2

mais pas :

1.5.0-preview.2

SemVer définit précisément les versions MAJOR.MINOR.PATCH, les identifiants de préversion et les métadonnées de build, ainsi que leurs règles de précédence.

Il nous faut donc un autre type.

NuGet.Versioning

Ce type existe déjà dans l'écosystème .NET, dans le package :

NuGet.Versioning

On peut l'ajouter à un projet avec :

dotnet add package NuGet.Versioning

Le package fournit notamment le type NuGetVersion.

using NuGet.Versioning;

NuGetVersion version = NuGetVersion.Parse("1.5.0-preview.2");

Cette fois, la version est parfaitement valide.

Console.WriteLine(version.Major);        // 1
Console.WriteLine(version.Minor);        // 5
Console.WriteLine(version.Patch);        // 0
Console.WriteLine(version.IsPrerelease); // true
Console.WriteLine(version.Release);      // preview.2

NuGet.Versioning est l'implémentation du versioning utilisée par NuGet lui-même. Microsoft recommande explicitement son utilisation pour manipuler programmatiquement les versions de packages NuGet.

Mais rien n'oblige à développer un gestionnaire de packages pour que ce type soit utile.

Un exemple concret

Imaginons une application capable de vérifier si une nouvelle version est disponible.

La version actuellement installée est :

3.2.0

Une API nous retourne les versions suivantes :

3.2.1
3.3.0-preview.1
3.3.0-preview.2
3.3.0

On peut commencer par les parser :

NuGetVersion[] versions = new[] {
    NuGetVersion.Parse("3.2.1"),
    NuGetVersion.Parse("3.3.0-preview.1"),
    NuGetVersion.Parse("3.3.0-preview.2"),
    NuGetVersion.Parse("3.3.0")
};

Mais parser les versions n'est qu'une partie du problème.

Il faut également pouvoir les comparer correctement.

Avec NuGetVersion, NuGet fournit également VersionComparer :

NuGetVersion latest = versions.OrderByDescending(version => version, VersionComparer.Default)
                              .First();

Console.WriteLine(latest); // 3.3.0

Le comparer connaît les règles de précédence des versions NuGet et des préversions. Microsoft documente d'ailleurs NuGetVersion.Parse et VersionComparer comme les API à utiliser pour parser et ordonner ces versions.

C'est nettement préférable à une comparaison de chaînes de caractères.

Prenons par exemple :

3.3.0-preview.9
3.3.0-preview.10

Une version n'est pas une simple chaîne à classer alphabétiquement. Les identifiants numériques d'une préversion ont une sémantique particulière définie par SemVer.

preview.10 vient bien après preview.9.

Ne conserver que les versions stables

Un autre besoin courant consiste à ignorer les préversions.

Avec NuGetVersion, l'intention est directement exprimée par le type :

NuGetVersion[] stableVersions = versions.Where(version => !version.IsPrerelease)
                                        .OrderByDescending(version => version, VersionComparer.Default);

Cela permet facilement de proposer, par exemple, deux stratégies dans une application :

Canal stable
    3.2.1
    3.3.0

Canal preview
    3.3.0-preview.1
    3.3.0-preview.2
    3.3.0

Il n'est pas nécessaire de rechercher manuellement -preview, -beta ou -rc dans une chaîne.

Et c'est important : pour NuGet, ces termes ne possèdent pas une signification particulière en eux-mêmes. C'est la présence d'un suffixe de préversion et ses identifiants qui comptent. Un auteur de package peut utiliser le suffixe qu'il souhaite pour identifier une préversion.

Parser une valeur externe

Lorsque la version provient d'une API, d'un fichier de configuration, d'une ligne de commande ou d'une saisie utilisateur, il est généralement préférable de ne pas utiliser Parse.

Comme System.Version, NuGetVersion propose TryParse :

if (NuGetVersion.TryParse(input, out var version)) {
    Console.WriteLine($"Version valide : {version}");
} else {
    Console.WriteLine("Version invalide");
}

On retrouve donc un modèle très familier pour un développeur .NET.

La différence se situe essentiellement dans la richesse du modèle représenté.

Les métadonnées

SemVer permet également d'associer des métadonnées à une version en utilisant +.

Par exemple :

2.1.0+build.1842
2.1.0-preview.2+sha.a83bd91

NuGetVersion sait également les représenter :

NuGetVersion version = NuGetVersion.Parse("2.1.0-preview.2+sha.a83bd91");

Console.WriteLine(version.Release);  // preview.2
Console.WriteLine(version.Metadata); // sha.a83bd91

Ces métadonnées sont particulièrement utiles lorsqu'une CI souhaite associer une version à un numéro de build, un commit Git ou une autre information permettant de retrouver précisément l'artefact produit.

En SemVer, les métadonnées de build n'interviennent pas dans la précédence d'une version.

NuGetVersion n'est pas strictement SemVer

Il reste cependant une subtilité importante.

NuGetVersion implémente le modèle de version utilisé par NuGet, fortement basé sur Semantic Versioning, mais il n'est pas une implémentation strictement identique à la spécification SemVer.

NuGet conserve notamment certaines compatibilités historiques avec l'écosystème .NET.

Par exemple, NuGetVersion accepte un quatrième composant :

1.2.3.4

Il accepte également :

1
1.0
1.0.0
1.0.0.0

et considère ces versions comme équivalentes.

Enfin, les composants textuels des préversions sont comparés sans tenir compte de la casse :

1.0.0-alpha
1.0.0-Alpha

sont considérés comme équivalents par NuGet.

Ces différences sont explicitement documentées par Microsoft. NuGetVersion doit donc être vu comme le modèle de version de NuGet, basé sur SemVer et adapté aux contraintes historiques de l'écosystème .NET.

System.Version ou NuGetVersion ?

Il n'est évidemment pas question de remplacer systématiquement System.Version.

Les deux types représentent simplement des concepts différents.

BesoinSystem.VersionNuGetVersion
1.2✓✓
1.2.3✓✓
1.2.3.4✓✓
1.2.0-preview.1✗✓
1.2.0-beta.2✗✓
1.2.0-rc.1✗✓
1.2.0+build.42✗✓
Détecter une préversion✗✓
Comparaison compatible avec le versioning NuGet✗✓

System.Version reste parfaitement adapté lorsqu'on manipule le modèle numérique historique de .NET.

NuGetVersion devient beaucoup plus naturel lorsqu'on manipule les versions de livraison d'une application ou d'une bibliothèque.

Pas uniquement pour les packages NuGet

Le nom NuGetVersion peut laisser penser que ce type n'a d'intérêt que dans du code manipulant directement des packages NuGet.

Ce serait dommage de s'arrêter à son nom.

De nombreuses applications ont besoin de manipuler des versions :

  • un outil CLI qui vérifie si une nouvelle version est disponible ;
  • un mécanisme d'auto-update ;
  • un pipeline de livraison ;
  • un outil de génération de releases ;
  • une application qui interroge GitHub ou une API pour connaître les versions disponibles ;
  • une règle qui doit accepter ou refuser les préversions ;
  • un outil qui doit simplement comparer correctement deux versions.

Dans tous ces cas, écrire soi-même le parsing et les règles de comparaison des préversions n'apporte pas grand-chose.

Conclusion

System.Version est probablement le type auquel pense spontanément un développeur .NET lorsqu'il doit manipuler une version.

Et tant que la version ressemble à :

1.2.3.4

c'est souvent exactement le bon choix.

Mais dès que le code commence à ressembler à ceci :

var parts = version.Split('-');

Version numericVersion = Version.Parse(parts[0]);

// Maintenant, gérer preview, beta, rc...

il est probablement temps de s'arrêter.

L'écosystème .NET dispose déjà d'une abstraction pour manipuler ce type de versions :

NuGetVersion version = NuGetVersion.Parse(input);

NuGetVersion n'est ni une API spectaculaire ni une révolution.

C'est simplement l'un de ces petits types qu'il est utile de connaître pour éviter de réimplémenter, souvent de manière incomplète, un problème déjà résolu.

Références