Partage via


Meilleures pratiques pour la conception d’un service réparti

Suivez les instructions générales et les restrictions documentées pour les interfaces RPC pour StreamJsonRpc.

En outre, les instructions suivantes s’appliquent aux services répartités.

Signatures de méthode

Toutes les méthodes doivent prendre un paramètre CancellationToken comme dernier paramètre. Ce paramètre doit généralement pas être un paramètre facultatif afin que les appelants soient moins susceptibles d’omettre accidentellement l’argument. Même si l’implémentation de la méthode est censée être triviale, fournir un CancellationToken permet au client d’annuler sa propre requête avant qu’il ne soit transmis au serveur. Il permet également à l’implémentation du serveur d’évoluer en quelque chose de plus coûteux sans avoir à mettre à jour la méthode pour ajouter l’annulation en tant qu’option ultérieurement.

Envisagez d’éviter les multiples surcharges de la même méthode sur votre interface RPC. Bien que la résolution de surcharge fonctionne généralement (et que les tests doivent être écrits pour vérifier qu'elle le fait), elle s'appuie sur le fait que essaie de désérialiser des arguments en fonction des types de paramètres de chaque surcharge, ce qui provoque des exceptions de première chance comme partie normale du processus de sélection d'une surcharge. Comme nous voulons réduire le nombre d’exceptions de première chance levées dans des scénarios de réussite, il est préférable d’avoir seulement une seule méthode avec un nom donné.

Types de paramètres et de retour

Rappelez-vous que tous les arguments et valeurs de retour échangés via RPC sont simplement des données. Ils sont tous sérialisés et envoyés sur le fil. Toutes les méthodes que vous définissez sur ces types de données fonctionnent uniquement sur cette copie locale des données et n’ont aucun moyen de communiquer avec le service RPC qui l’a produite. Les seules exceptions à ce comportement de sérialisation sont les types exotiques , pour lesquels StreamJsonRpc bénéficie d'une prise en charge spéciale.

Envisagez d’utiliser ValueTask<T> sur Task<T> comme type de retour de méthodes, car ValueTask<T> entraîne moins d’allocations. Lorsque vous utilisez la variété non générique (par exemple, Task et ValueTask) il est moins important, mais ValueTask peut toujours être préférable. Tenez compte des restrictions d’utilisation sur ValueTask<T> comme indiqué sur cette API. Ce billet de blog et cette vidéo peuvent également être utiles pour décider du type à utiliser.

Types de données personnalisés

Envisagez de définir tous les types de données comme immuables, ce qui permet un partage plus sûr des données au sein d’un processus sans copier et permet de renforcer l’idée aux consommateurs qu’ils ne peuvent pas modifier les données qu’ils reçoivent en réponse à une requête sans placer un autre RPC.

Définissez vos types de données comme class plutôt que struct lors de l’utilisation de ServiceJsonRpcDescriptor.Formatters.UTF8, ce qui évite le coût de l’encapsulation (potentiellement répétée) lors de l’utilisation de Newtonsoft.Json. L'encapsulation ne se produit pas lors de l'utilisation de ServiceJsonRpcDescriptor.Formatters.MessagePack, les structs peuvent donc être une option appropriée si vous êtes déterminé à utiliser ce formateur.

Envisagez d’implémenter des méthodes de IEquatable<T> et de substitution de GetHashCode() et de Equals(Object) sur vos types de données, ce qui permet au client de stocker, comparer et réutiliser efficacement les données reçues selon qu’elles correspondent aux données reçues à un autre moment.

Utilisez le DiscriminatedTypeJsonConverter<TBase> pour prendre en charge la sérialisation de types polymorphes à l’aide de JSON.

Collections

Utilisez des interfaces de collections en lecture seule dans des signatures de méthode RPC (par exemple, IReadOnlyList<T>) plutôt que des types concrets (par exemple, List<T> ou T[]), ce qui permet une désérialisation potentiellement plus efficace.

Évitez IEnumerable<T>. Son absence de propriété Count entraîne un code inefficace et implique une génération tardive possible de données, qui ne s’applique pas dans un scénario RPC. Utilisez IReadOnlyCollection<T> pour les collections non triées ou IReadOnlyList<T> pour les collections ordonnées à la place.

Prenez le cas de IAsyncEnumerable<T>. Tout autre type de collection ou IEnumerable<T> entraîne l’envoi de la collection entière dans un message. L’utilisation de IAsyncEnumerable<T> permet d’obtenir un petit message initial et fournit au récepteur les moyens d’obtenir autant d’éléments de la collection que vous le souhaitez, en l’énumérant de manière asynchrone. En savoir plus sur ce nouveau motif.

Modèle d’observateur

Envisagez d’utiliser le modèle de conception de l’observateur dans votre interface. Il s’agit d’un moyen simple pour le client de s’abonner aux données sans les nombreux pièges qui s’appliquent au modèle d’événementing traditionnel décrit dans la section suivante.

Le modèle d’observateur peut être aussi simple que celui-ci :

Task<IDisposable> SubscribeAsync(IObserver<YourDataType> observer);

Les types IDisposable et IObserver<T> utilisés ci-dessus sont deux des types exotiques dans StreamJsonRpc. Ils ont donc un comportement de marshalling spécial au lieu d’être sérialisés simplement en tant que données.

Événements

Les événements peuvent être problématiques sur RPC pour plusieurs raisons et nous recommandons plutôt le modèle d’observateur décrit ci-dessus.

N’oubliez pas que le service n’a aucune visibilité sur le nombre de gestionnaires d’événements attachés par le client lorsque le service et le client se trouvent dans des processus distincts. JsonRpc attachera toujours exactement un gestionnaire responsable de la propagation de l’événement au client. Le client peut avoir zéro ou plusieurs gestionnaires attachés de l’autre côté.

La plupart des clients RPC n’ont pas de gestionnaires d’événements câblés lorsqu’ils sont connectés pour la première fois. Évitez de déclencher le premier événement jusqu’à ce que le client ait appelé une méthode « Subscribe* » sur votre interface pour indiquer l’intérêt et la préparation à recevoir des événements.

Si votre événement indique un delta dans l’état (par exemple, un nouvel élément ajouté à une collection), envisagez de déclencher tous les événements passés ou de décrire toutes les données actuelles comme si elles sont nouvelles dans l’argument d’événement lorsqu’un client s’abonne pour les aider à « synchroniser » sans aucun code de gestion des événements.

Envisagez d’accepter des arguments supplémentaires sur la méthode « Subscribe* » mentionnée ci-dessus si le client peut souhaiter exprimer l’intérêt d’un sous-ensemble de données ou de notifications, afin de réduire le trafic réseau et le processeur requis pour transférer ces notifications.

Envisagez de ne pas proposer de méthode qui retourne la valeur actuelle si vous exposez également un événement pour recevoir des notifications de modification, ou découragez activement les clients de l’utiliser en combinaison avec l’événement. Un client s’abonnant à un événement pour les données et appelant une méthode pour obtenir la valeur actuelle risque de ne pas suivre les changements de cette valeur. Il peut ainsi manquer un événement de modification ou ne pas savoir comment concilier un événement de modification sur un thread avec la valeur obtenue sur un autre thread. Cette préoccupation est générale pour n’importe quelle interface, et non seulement lorsqu’elle est sur RPC.

Conventions d’affectation de noms

  • Utilisez le suffixe Service sur les interfaces RPC et un préfixe de I simple.
  • N’utilisez pas le suffixe Service pour les classes de votre Kit de développement logiciel (SDK). Votre wrapper RPC ou bibliothèque doit utiliser un nom qui décrit exactement ce qu’il fait, en évitant le terme « service ».
  • Évitez le terme « distant » dans les noms d’interface ou de membre. N’oubliez pas que les services répartits s’appliquent autant dans les scénarios locaux que dans les scénarios distants.

Problèmes de compatibilité des versions

Nous voulons que tout service réparti donné exposé à d’autres extensions ou exposé sur Live Share soit compatible vers l’avant et vers l’arrière, ce qui signifie que nous devrions supposer qu’un client peut être plus ancien ou plus récent que le service et que la fonctionnalité doit être à peu près égale à celle des deux versions applicables.

Tout d’abord, examinons la terminologie des changements cassants :

  • Changement cassant binaire : une modification de l’API qui empêcherait d'autres codes managés compilés avec une version antérieure de l’assembly de se lier à la nouvelle au moment de l’exécution. Voici quelques exemples :

    • Modification de la signature d’un membre public existant.
    • Renommage d’un membre public.
    • Suppression d’un type public.
    • Ajout d’un membre abstrait à un type ou de n'importe quel membre à une interface.

    Toutefois, les éléments suivants ne sont pas des changements cassants binaires :

    • Ajout d’un membre non abstrait à une classe ou à un struct.
    • Ajout d’une implémentation d’interface complète (non abstraite) à un type existant.
  • changement cassant le protocole: modification de la forme sérialisée d’un type de données ou d’un appel de méthode RPC afin que la partie distante ne puisse pas désérialiser et traiter correctement. Voici quelques exemples :

    • Ajout de paramètres requis à une méthode RPC.
    • Suppression d’un membre d’un type de données qui était auparavant garanti comme non nul.
    • Ajout d’une exigence indiquant qu’un appel de méthode doit être placé avant d’autres opérations préexistantes.
    • Ajout, suppression ou modification d’un attribut sur un champ ou une propriété qui contrôle le nom sérialisé des données de ce membre.
    • (MessagePack) : modification de la propriété DataMemberAttribute.Order ou de l'entier KeyAttribute d'un membre existant.

    Toutefois, les éléments suivants ne sont pas des changements cassant le protocole :

    • Ajout d’un membre facultatif à un type de données.
    • Ajout de membres aux interfaces RPC.
    • Ajout de paramètres facultatifs à des méthodes existantes.
    • Modification d’un type de paramètre qui représente un entier ou float en un avec une longueur ou une précision supérieure (par exemple, int à long ou float à double).
    • Renommage d’un paramètre. Cela pose techniquement un problème pour les clients qui utilisent des arguments nommés JSON-RPC, mais les clients utilisant le ServiceJsonRpcDescriptor emploient par défaut des arguments positionnels et ne seraient pas affectés par une modification du nom de paramètre. Cela n’a rien à voir avec le fait que le code source client utilise la syntaxe d’argument nommé, auquel cas un renommage de paramètre serait un changement cassant de la source.
  • Changement cassant comportemental : modification apportée à l’implémentation d’un service réparti qui ajoute ou modifie le comportement de sorte que les clients plus anciens puissent mal fonctionner. Voici quelques exemples :

    • Ne plus initialiser un membre d’un type de données qui était auparavant toujours initialisé.
    • Levée d’une exception dans une condition qui avait l'habitude d’être remplie avec succès.
    • Renvoi d’une erreur avec un code d’erreur différent de celui retourné précédemment.

    Toutefois, les éléments suivants ne sont pas des changements cassant le comportement :

    • Levée d’un nouveau type d’exception (car toutes les exceptions sont de toute façon enveloppées dans RemoteInvocationException).

Lorsque des changements cassants sont requis, ils peuvent être effectués en toute sécurité en inscrivant et en proposant un nouveau nom de service. Ce moniker peut partager le même nom, mais avec un numéro de version supérieur. L'interface RPC d'origine pourrait être réutilisable s'il n'y a pas de changement rompant la compatibilité binaire. Sinon, définissez une nouvelle interface pour la nouvelle version du service. Évitez de casser les anciens clients en continuant d’inscrire, de proposer et de prendre en charge l’ancienne version.

Nous voulons éviter tous les changements cassants, à l'exception de l'ajout de membres aux interfaces RPC.

Ajout de membres à des interfaces RPC

Ne pas ajouter de membres à une interface de rappel client RPC, car de nombreux clients peuvent implémenter cette interface et l’ajout de membres entraînerait une levée TypeLoadException par le CLR lorsque ces types sont chargés mais n’implémentent pas les nouveaux membres de l’interface. Si vous devez ajouter des membres à appeler sur un client de rappel RPC cible, définissez une nouvelle interface (qui peut dériver de l'originale), puis suivez le processus standard pour offrir votre service réparti en incrémentant le numéro de version et offrir un descripteur spécifiant le type d'interface client mis à jour.

Vous pouvez ajouter des membres aux interfaces RPC qui définissent un service réparti. Il ne s’agit pas d’un changement cassant le protocole, et il ne s’agit que d’un changement cassant binaire pour ceux qui implémentent le service, mais il est probable que vous mettiez également à jour le service pour implémenter le nouveau membre. Étant donné notre conseil est que personne ne doit implémenter l’interface RPC à l’exception du service courtier lui-même (et que les tests doivent utiliser des cadres de simulation), l’ajout d’un membre à une interface RPC ne devrait désavantager personne.

Ces nouveaux membres doivent avoir des commentaires de documentation xml qui identifient la version du service qui a d’abord ajouté ce membre. Si un client plus récent appelle la méthode sur un service plus ancien qui n’implémente pas la méthode, ce client peut intercepter RemoteMethodNotFoundException. Mais ce client peut (et probablement devrait) prédire l’échec et éviter l’appel en premier lieu. Les meilleures pratiques pour l’ajout de membres à des services existants sont les suivantes :

  • S'il s'agit du premier changement dans une version de votre service : augmentez la version mineure du nom de votre service lorsque vous ajoutez le membre et déclarez le nouveau descripteur.
  • Mettez à jour votre service pour inscrire et proposer la nouvelle version en plus de l'ancienne version.
  • Si vous avez un client de votre service réparti, mettez à jour votre client pour demander la version la plus récente et revenir à la demande de l’ancienne version si la version la plus récente revient en tant que null.