Un guide définitif sur les changements de

Je souhaiterais rassembler autant d’informations que possible sur la gestion des versions d’API dans .NET / CLR, et sur la manière dont les modifications de l’API interrompent ou non les applications client. Tout d’abord, définissons quelques termes:

Modification de l’API – modification de la définition visible d’un type, y compris de ses membres publics. Cela inclut la modification des noms de type et de membre, la modification du type de base d’un type, l’ajout / la suppression d’interfaces d’un type, l’ajout / la suppression de membres (y compris les surcharges), la modification de la visibilité des membres, pour les parameters de méthode, en ajoutant / supprimant des atsortingbuts sur les types et les membres, et en ajoutant / supprimant des parameters de type génériques sur les types et les membres (ai-je manqué quelque chose?). Cela n’inclut aucun changement dans les organismes membres, ni aucun changement dans les membres privés (c.-à-d. Que nous ne prenons pas en compte la reflection).

Interruption au niveau binary – modification de l’API entraînant la compilation des assemblys client avec une version antérieure de l’API, qui risque de ne pas être chargée avec la nouvelle version. Exemple: modification de la signature de la méthode, même si elle permet d’être appelée de la même manière qu’auparavant (par exemple: void pour renvoyer les surcharges de type / paramètre par défaut).

Rupture au niveau de la source – modification de l’API entraînant la compilation d’un code existant sur une ancienne version de l’API, potentiellement incompatible avec la nouvelle version. Les assemblys clients déjà compilés fonctionnent toutefois comme avant. Exemple: ajout d’une nouvelle surcharge pouvant entraîner une ambiguïté dans les appels de méthode sans ambiguïté antérieurs.

Changement de la sémantique silencieuse au niveau de la source – une modification de l’API entraînant la création d’un code existant pour comstackr sur une ancienne version de l’API modifie discrètement sa sémantique, par exemple en appelant une autre méthode. Le code devrait cependant continuer à se comstackr sans avertissements / erreurs, et les assemblys précédemment compilés devraient fonctionner comme avant. Exemple: implémenter une nouvelle interface sur une classe existante, ce qui entraîne la sélection d’une surcharge supplémentaire lors de la résolution de la surcharge.

Le but ultime est de cataloguer autant de changements de API de sémantique que possible et de décrire l’effet exact de la rupture, ainsi que les langages concernés et non affectés. Pour développer ce dernier point: alors que certains changements affectent toutes les langues de manière universelle (par exemple, l’ajout d’un nouveau membre à une interface brisera les implémentations de cette interface dans n’importe quelle langue), certains requièrent une sémantique linguistique très spécifique Cela implique généralement la surcharge de la méthode et, en général, tout ce qui concerne les conversions de types implicites. Il ne semble pas y avoir de moyen de définir le «plus petit dénominateur commun» ici même pour les langages conformes au CLS (c’est-à-dire ceux qui se conforment au moins aux règles du consommateur CLS définies dans les spécifications CLI). quelqu’un me corrige comme étant faux ici – donc cela devra passer par la langue. Les plus intéressants sont naturellement ceux qui sont fournis avec .NET: C #, VB et F #; mais d’autres, tels que IronPython, IronRuby, Delphi Prism, etc. sont également pertinents. Plus il y a d’angle, plus cela sera intéressant – des choses comme la suppression de membres sont assez évidentes, mais des interactions subtiles entre par exemple la surcharge de méthode, les parameters optionnels / par défaut, l’inférence de type lambda et les opérateurs de conversion peuvent surprendre a l’heure.

Quelques exemples pour démarrer ceci:

Ajout de nouvelles surcharges de méthode

Genre: pause au niveau de la source

Langues concernées: C #, VB, F #

API avant changement:

public class Foo { public void Bar(IEnumerable x); } 

API après modification:

 public class Foo { public void Bar(IEnumerable x); public void Bar(ICloneable x); } 

Exemple de code client fonctionnant avant le changement et brisé après celui-ci:

 new Foo().Bar(new int[0]); 

Ajout de nouvelles surcharges d’opérateur de conversion implicite

Kind: coupure au niveau de la source.

Langues concernées: C #, VB

Langues non affectées: F #

API avant changement:

 public class Foo { public static implicit operator int (); } 

API après modification:

 public class Foo { public static implicit operator int (); public static implicit operator float (); } 

Exemple de code client fonctionnant avant le changement et brisé après celui-ci:

 void Bar(int x); void Bar(float x); Bar(new Foo()); 

Remarques: F # n’est pas rompu, car il ne prend en charge aucun niveau de langue pour les opérateurs surchargés, ni explicites ni implicites – les deux doivent être appelés directement en tant que méthodes op_Explicit et op_Implicit .

Ajout de nouvelles méthodes d’instance

Kind: modification de la sémantique silencieuse au niveau de la source.

Langues concernées: C #, VB

Langues non affectées: F #

API avant changement:

 public class Foo { } 

API après modification:

 public class Foo { public void Bar(); } 

Exemple de code client qui subit un changement sémantique discret:

 public static class FooExtensions { public void Bar(this Foo foo); } new Foo().Bar(); 

Remarques: F # n’est pas rompu, car il ne prend pas en charge la langue pour ExtensionMethodAtsortingbute et requirejs que les méthodes d’extension CLS soient appelées en tant que méthodes statiques.

Changer une signature de méthode

Genre: Pause au niveau binary

Langues concernées: C # (VB et F # le plus probable, mais non testé)

API avant le changement

 public static class Foo { public static void bar(int i); } 

API après changement

 public static class Foo { public static bool bar(int i); } 

Exemple de code client fonctionnant avant le changement

 Foo.bar(13); 

Ajout d’un paramètre avec une valeur par défaut.

Type de pause: pause au niveau binary

Même si le code source appelant n’a pas besoin de changer, il doit toujours être recompilé (comme lors de l’ajout d’un paramètre normal).

En effet, C # comstack les valeurs par défaut des parameters directement dans l’assembly appelant. Cela signifie que si vous ne recomstackz pas, vous obtiendrez une exception MissingMethodException car l’ancien assembly tente d’appeler une méthode avec moins d’arguments.

API avant changement

 public void Foo(int a) { } 

API après modification

 public void Foo(int a, ssortingng b = null) { } 

Exemple de code client rompu après

 Foo(5); 

Le code client doit être recompilé en Foo(5, null) au niveau du bytecode. L’assembly appelé ne contiendra que Foo(int, ssortingng) , pas Foo(int) . C’est parce que les valeurs de parameters par défaut sont purement une fonctionnalité de langage, le runtime .Net ne sait rien à leur sujet. (Ceci explique également pourquoi les valeurs par défaut doivent être des constantes de compilation en C #).

Celui-ci était très peu évident quand je l’ai découvert, surtout à la lumière de la différence avec la même situation pour les interfaces. Ce n’est pas une pause du tout, mais c’est assez surprenant que j’ai décidé de l’inclure:

Refactoring des membres de classe dans une classe de base

Kind: pas une pause!

Langues concernées: aucune (c.-à-d. Aucune n’est brisée)

API avant changement:

 class Foo { public virtual void Bar() {} public virtual void Baz() {} } 

API après modification:

 class FooBase { public virtual void Bar() {} } class Foo : FooBase { public virtual void Baz() {} } 

Exemple de code qui continue à fonctionner tout au long du changement (même si je m’attendais à ce qu’il se brise):

 // C++/CLI ref class Derived : Foo { public virtual void Baz() {{ // Explicit override public virtual void BarOverride() = Foo::Bar {} }; 

Remarques:

C ++ / CLI est le seul langage .NET qui possède une construction analogue à l’implémentation d’interface explicite pour les membres de classe de base virtuelle – “remplacement explicite”. Je m’attendais à ce que cela se traduise par le même type de casse que lors du déplacement des membres d’interface vers une interface de base (car la génération IL pour une substitution explicite est la même que pour une implémentation explicite). À ma grande surprise, ce n’est pas le cas – même si l’IL généré indique toujours que BarOverride remplace Foo::Bar plutôt que FooBase::Bar , le chargeur d’assemblage est suffisamment intelligent pour se substituer correctement à une autre sans se plaindre – apparemment, le fait que Foo est une classe, c’est ce qui fait la différence. Allez comprendre…

Celui-ci est un cas particulier peut-être moins évident de “l’ajout / suppression de membres d’interface”, et j’ai pensé qu’il méritait sa propre entrée à la lumière d’un autre cas que je vais publier prochainement. Alors:

Refactoring des membres de l’interface dans une interface de base

Kind: pauses à la fois aux niveaux source et binary

Langages affectés: C #, VB, C ++ / CLI, F # (pour les sauts de source, le premier binary affecte naturellement n’importe quel langage)

API avant changement:

 interface IFoo { void Bar(); void Baz(); } 

API après modification:

 interface IFooBase { void Bar(); } interface IFoo : IFooBase { void Baz(); } 

Exemple de code client rompu par un changement au niveau source:

 class Foo : IFoo { void IFoo.Bar() { ... } void IFoo.Baz() { ... } } 

Exemple de code client rompu par un changement au niveau binary;

 (new Foo()).Bar(); 

Remarques:

Pour la rupture au niveau source, le problème est que C #, VB et C ++ / CLI requièrent tous un nom d’interface exact dans la déclaration de l’implémentation des membres de l’interface; ainsi, si le membre est déplacé vers une interface de base, le code ne sera plus compilé.

La rupture binary est due au fait que les méthodes d’interface sont entièrement qualifiées dans les IL générées pour les implémentations explicites, et le nom de l’interface doit également être exact.

L’implémentation implicite là où elle est disponible (c.-à-d. C # et C ++ / CLI, mais pas VB) fonctionnera correctement à la fois au niveau source et au niveau binary. Les appels de méthode ne se cassent pas non plus.

Réorganisation des valeurs énumérées

Type de rupture: Changement de sémantique silencieuse au niveau de la source / au niveau binary

Langues concernées: toutes

La réorganisation des valeurs énumérées conservera la compatibilité au niveau source car les littéraux ont le même nom, mais leurs index ordinaux seront mis à jour, ce qui peut provoquer certains types de sauts au niveau de la source.

Pire encore, les pauses au niveau binary peuvent être introduites si le code client n’est pas recompilé avec la nouvelle version de l’API. Les valeurs d’énumération sont des constantes à la compilation et, à ce titre, elles sont utilisées dans l’IL de l’assembly client. Ce cas peut être particulièrement difficile à détecter à certains moments.

API avant changement

 public enum Foo { Bar, Baz } 

API après modification

 public enum Foo { Baz, Bar } 

Exemple de code client qui fonctionne mais qui est cassé par la suite:

 Foo.Bar < Foo.Baz 

Celui-ci est vraiment très rare dans la pratique, mais néanmoins surprenant quand cela arrive.

Ajout de nouveaux membres non surchargés

Kind: changement de niveau source ou sémantique discrète

Langues concernées: C #, VB

Langues non affectées: F #, C ++ / CLI

API avant changement:

 public class Foo { } 

API après modification:

 public class Foo { public void Frob() {} } 

Exemple de code client rompu par le changement:

 class Bar { public void Frob() {} } class Program { static void Qux(Action a) { } static void Qux(Action a) { } static void Main() { Qux(x => x.Frob()); } } 

Remarques:

Le problème est dû à l’inférence de type lambda dans C # et VB en présence d’une résolution de surcharge. Une forme limitée de typage de canard est employée ici pour casser des liens où plusieurs types correspondent, en vérifiant si le corps du lambda a un sens pour un type donné – si un seul type donne un corps compilable, celui-ci est choisi.

Le danger ici est que le code client peut avoir un groupe de méthodes surchargé où certaines méthodes prennent des arguments de ses propres types, et d’autres prennent des arguments de types exposés par votre bibliothèque. Si l’un de ses codes s’appuie alors sur un algorithme d’inférence de type pour déterminer la méthode correcte basée uniquement sur la présence ou l’absence de membres, l’ajout d’un nouveau membre à l’un de vos types portant le même nom off, ce qui entraîne une ambiguïté lors de la résolution de la surcharge.

Notez que les types Foo et Bar dans cet exemple ne sont liés d’aucune manière, pas par inheritance ou autrement. Leur utilisation dans un seul groupe de méthodes suffit pour déclencher cela, et si cela se produit dans le code client, vous ne pouvez pas le contrôler.

L’exemple de code ci-dessus montre une situation plus simple où il s’agit d’une rupture au niveau source (c’est-à-dire des résultats d’erreur du compilateur). Cependant, cela peut aussi être une modification sémantique silencieuse, si la surcharge choisie par inférence avait d’autres arguments qui la classeraient par la suite (par exemple des arguments optionnels avec des valeurs par défaut, ou une incompatibilité de type entre un argument déclaré et un argument implicite). conversion). Dans un tel scénario, la résolution de la surcharge n’échouera plus, mais une surcharge différente sera discrètement sélectionnée par le compilateur. En pratique, cependant, il est très difficile de se heurter à ce problème sans construire soigneusement les signatures de méthode pour les provoquer délibérément.

Convertir une implémentation d’interface implicite en une implémentation explicite.

Genre de pause: source et binary

Langues concernées: Tous

Ceci est juste une variante de la modification de l’accessibilité d’une méthode – c’est juste un peu plus subtil car il est facile de négliger le fait que tous les access aux méthodes d’une interface ne sont pas nécessairement une référence au type de l’interface.

API avant modification:

 public class Foo : IEnumerable { public IEnumerator GetEnumerator(); } 

API après modification:

 public class Foo : IEnumerable { IEnumerator IEnumerable.GetEnumerator(); } 

Exemple de code client qui fonctionne avant le changement et qui est rompu par la suite:

 new Foo().GetEnumerator(); // fails because GetEnumerator() is no longer public 

Convertir une implémentation d’interface explicite en implicite.

Genre de pause: Source

Langues concernées: Tous

Le refactoring d’une implémentation d’interface explicite dans une implicite est plus subtile dans la manière dont il peut casser une API. En apparence, il semblerait que cela devrait être relativement sûr, cependant, combiné avec l’inheritance, cela peut causer des problèmes.

API avant modification:

 public class Foo : IEnumerable { IEnumerator IEnumerable.GetEnumerator() { yield return "Foo"; } } 

API après modification:

 public class Foo : IEnumerable { public IEnumerator GetEnumerator() { yield return "Foo"; } } 

Exemple de code client qui fonctionne avant le changement et qui est rompu par la suite:

 class Bar : Foo, IEnumerable { IEnumerator IEnumerable.GetEnumerator() // silently hides base instance { yield return "Bar"; } } foreach( var x in new Bar() ) Console.WriteLine(x); // originally output "Bar", now outputs "Foo" 

Changer un champ en propriété

Type de pause: API

Langues concernées: Visual Basic et C # *

Info: Lorsque vous modifiez un champ ou une variable normale dans une propriété en Visual Basic, tout code externe faisant référence à ce membre doit être recompilé.

API avant modification:

 Public Class Foo Public Shared Bar As Ssortingng = "" End Class 

API après modification:

 Public Class Foo Private Shared _Bar As Ssortingng = "" Public Shared Property Bar As Ssortingng Get Return _Bar End Get Set(value As Ssortingng) _Bar = value End Set End Property End Class 

Exemple de code client qui fonctionne mais qui est cassé par la suite:

 Foo.Bar = "foobar" 

Ajout d’espace de noms

Modification de la rupture au niveau de la source / sémantique silencieuse au niveau de la source

En raison de la manière dont la résolution d’espace de noms fonctionne dans vb.Net, l’ajout d’un espace de noms à une bibliothèque peut empêcher le code Visual Basic compilé avec une version précédente de l’API de comstackr avec une nouvelle version.

Exemple de code client:

 Imports System Imports Api.SomeNamespace Public Class Foo Public Sub Bar() Dim dr As Data.DataRow End Sub End Class 

Si une nouvelle version de l’API ajoute l’espace de noms Api.SomeNamespace.Data , le code ci-dessus ne sera pas compilé.

Cela devient plus compliqué avec les importations d’espace de noms au niveau du projet. Si Imports System est omis du code ci-dessus, mais que l’espace de noms System est importé au niveau du projet, le code peut générer une erreur.

Cependant, si l’ DataRow inclut une classe DataRow dans son Api.SomeNamespace.Data noms Api.SomeNamespace.Data , le code sera compilé, mais dr sera une instance de System.Data.DataRow lorsqu’il sera compilé avec l’ancienne version de l’API et Api.SomeNamespace.Data.DataRow lorsqu’il est compilé avec la nouvelle version de l’API.

Argument Renommer

Pause au niveau de la source

Changer les noms des arguments est un changement radical dans vb.net à partir de la version 7 (?) (.Net version 1?) Et c # .net à partir de la version 4 (.Net version 4).

API avant changement:

 namespace SomeNamespace { public class Foo { public static void Bar(ssortingng x) { ... } } } 

API après modification:

 namespace SomeNamespace { public class Foo { public static void Bar(ssortingng y) { ... } } } 

Exemple de code client:

 Api.SomeNamespace.Foo.Bar(x:"hi"); //C# Api.SomeNamespace.Foo.Bar(x:="hi") 'VB 

Paramètres de réf.

Pause au niveau de la source

L’ajout d’une méthode remplacée par la même signature, sauf qu’un paramètre est transmis par référence au lieu de par valeur, entraînera l’impossibilité pour la source vb faisant référence à l’API de résoudre la fonction. Visual Basic n’a aucun moyen (?) De différencier ces méthodes au point d’appel, à moins qu’elles aient des noms d’argument différents. Par conséquent, une telle modification pourrait rendre les deux membres inutilisables à partir du code vb.

API avant changement:

 namespace SomeNamespace { public class Foo { public static void Bar(ssortingng x) { ... } } } 

API après modification:

 namespace SomeNamespace { public class Foo { public static void Bar(ssortingng x) { ... } public static void Bar(ref ssortingng x) { ... } } } 

Exemple de code client:

 Api.SomeNamespace.Foo.Bar(str) 

Champ à changement de propriété

Coupure au niveau binary / Coupure au niveau de la source

Outre la rupture évidente au niveau binary, cela peut provoquer une rupture au niveau de la source si le membre est transmis à une méthode par référence.

API avant changement:

 namespace SomeNamespace { public class Foo { public int Bar; } } 

API après modification:

 namespace SomeNamespace { public class Foo { public int Bar { get; set; } } } 

Exemple de code client:

 FooBar(ref Api.SomeNamespace.Foo.Bar); 

Modification de l’API:

  1. Ajouter l’atsortingbut [Obsolete] (vous avez couvert ceci en mentionnant des atsortingbuts, mais cela peut être un changement brisant lors de l’utilisation de warning-as-error).

Pause au niveau binary:

  1. Déplacement d’un type d’un assemblage à un autre
  2. Changer l’espace de nommage d’un type
  3. Ajout d’un type de classe de base à partir d’un autre assembly.
  4. Ajout d’un nouveau membre (événement protégé) qui utilise un type d’un autre assembly (Class2) en tant que contrainte d’argument de modèle.

     protected void Something() where T : Class2 { } 
  5. Modification d’une classe enfant (Class3) pour dériver d’un type dans un autre assembly lorsque la classe est utilisée comme argument de modèle pour cette classe.

     protected class Class3 : Class2 { } protected void Something() where T : Class3 { } 

La sémantique silencieuse au niveau de la source change:

  1. Ajouter / supprimer / modifier les remplacements de Equals (), GetHashCode () ou ToSsortingng ()

(pas sûr où ils correspondent)

Modifications du déploiement:

  1. Ajouter / supprimer des dépendances / références
  2. Mise à jour des dépendances avec les nouvelles versions
  3. Modification de la plate-forme cible entre x86, Itanium, x64 ou anycpu
  4. Construire / tester sur une autre installation de la structure (c’est-à-dire installer la version 3.5 sur une boîte .Net 2.0 permet les appels d’API qui nécessitent alors .Net 2.0 SP2)

Bootstrap / Modification de la configuration:

  1. Ajout / Suppression / Modification des options de configuration personnalisées (par exemple, les parameters App.config)
  2. Avec l’utilisation intensive de l’IoC / DI dans les applications actuelles, il est parfois nécessaire de reconfigurer et / ou de modifier le code d’amorçage pour un code dépendant de DI.

Mettre à jour:

Désolé, je ne me suis pas rendu compte que la seule raison pour laquelle cela cassait pour moi était que je les utilisais dans des contraintes de modèle.

Ajout de méthodes de surcharge pour supprimer l’utilisation des parameters par défaut

Type de rupture: changement de sémantique silencieuse au niveau de la source

Comme le compilateur transforme les appels de méthode avec des valeurs de paramètre par défaut manquantes en un appel explicite avec la valeur par défaut du côté appelant, la compatibilité pour le code compilé existant est donnée; une méthode avec la bonne signature sera trouvée pour tout le code précédemment compilé.

De l’autre côté, les appels sans utilisation de parameters facultatifs sont maintenant compilés en tant qu’appel à la nouvelle méthode à laquelle il manque le paramètre facultatif. Tout fonctionne toujours correctement, mais si le code appelé réside dans un autre assembly, le code nouvellement compilé qui l’appelle dépend désormais de la nouvelle version de cet assembly. Le déploiement d’assemblys appelant le code refactorisé sans déployer également l’assembly dans lequel réside le code refactorisé entraîne des exceptions de «méthode introuvable».

API avant le changement

  public int MyMethod(int mandatoryParameter, int optionalParameter = 0) { return mandatoryParameter + optionalParameter; } 

API après changement

  public int MyMethod(int mandatoryParameter, int optionalParameter) { return mandatoryParameter + optionalParameter; } public int MyMethod(int mandatoryParameter) { return MyMethod(mandatoryParameter, 0); } 

Exemple de code qui fonctionnera toujours

  public int CodeNotDependentToNewVersion() { return MyMethod(5, 6); } 

Exemple de code qui dépend maintenant de la nouvelle version lors de la compilation

  public int CodeDependentToNewVersion() { return MyMethod(5); } 

Renommer une interface

Kinda of Break: source et binary

Langues concernées: probablement toutes, testées en C #.

API avant modification:

 public interface IFoo { void Test(); } public class Bar { IFoo GetFoo() { return new Foo(); } } 

API après modification:

 public interface IFooNew // Of the exact same definition as the (old) IFoo { void Test(); } public class Bar { IFooNew GetFoo() { return new Foo(); } } 

Exemple de code client qui fonctionne mais qui est cassé par la suite:

 new Bar().GetFoo().Test(); // Binary only break IFoo foo = new Bar().GetFoo(); // Source and binary break