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 CancellationToken paramètre comme dernier paramètre. Ce paramètre ne 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, la fourniture d’un client permet au client d’annuler CancellationToken sa propre demande avant qu’elle ne soit transmise 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 plusieurs 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 la tentative de désérialisation des arguments en fonction des types de paramètres de chaque surcharge, ce qui entraîne la levée de premières exceptions comme partie régulière de la sélection d’une surcharge. Comme nous voulons réduire le nombre d’exceptions de première chance levées dans des chemins de réussite, il est préférable d’avoir simplement une seule méthode avec un nom donné.
Types de paramètres et de retour
N’oubliez pas que tous les arguments et valeurs renvoyées échangés sur RPC sont uniquement 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 a une prise en charge spéciale.
Envisagez de l’utiliser ValueTask<T>
Task<T>
comme type de retour de méthodes, car ValueTask<T>
cela entraîne moins d’allocations.
Lorsque vous utilisez la variété non générique (par exemple, Task et ValueTask) elle est moins importante, mais ValueTask peut toujours être préférable.
Sachez que les restrictions d’utilisation sont ValueTask<T>
documentées sur cette API. Ce billet de blog et cette vidéo peuvent être utiles pour déterminer le type à utiliser également.
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 plutôt que class
struct
lors de l’utilisation ServiceJsonRpcDescriptor.Formatters.UTF8, ce qui évite le coût de boxing (potentiellement répété) lors de l’utilisation de Newtonsoft.Json.
La boxe ne se produit pas lors de l’utilisation ServiceJsonRpcDescriptor.Formatters.MessagePack de structs peut être une option appropriée si vous êtes validé dans ce formateur.
Envisagez d’implémenter IEquatable<T> et de remplacer et Equals(Object) de GetHashCode() remplacer des méthodes 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 sont égales aux données reçues à un autre moment.
Utilisez la prise en charge de la sérialisation des types polymorphes à l’aide DiscriminatedTypeJsonConverter<TBase> de JSON.
Collections
Utilisez des interfaces de collections en lecture seule dans les 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é entraîne un Count
code inefficace et implique une génération tardive de données possible, qui ne s’applique pas dans un scénario RPC.
Utilisez IReadOnlyCollection<T> plutôt pour les collections non ordonnées ou IReadOnlyList<T> pour les collections ordonnées.
Prenez le cas de IAsyncEnumerable<T>. Tout autre type de collection ou IEnumerable<T> entraîne l’envoi de l’intégralité de la collection dans un message. L’utilisation IAsyncEnumerable<T> permet 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 énumérant de façon asynchrone. En savoir plus sur ce modèle nouveau.
Modèle 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 IDisposable types IObserver<T> utilisés ci-dessus sont deux des types exotiques dans StreamJsonRpc, de sorte qu’ils obtiennent un comportement spécialement marshalé plutôt que sérialisé en tant que données simples.
É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 attache 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 à l’extrême 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 qui s’abonne à un événement pour les données et appelle une méthode pour obtenir la valeur actuelle se distingue par rapport aux modifications apportées à cette valeur et qui manque un événement de modification ou ne sait pas comment rapprocher 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 sur les
Service
interfaces RPC et un préfixe simpleI
. - N’utilisez pas le
Service
suffixe 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 : modification d’API qui entraînerait l’échec de la liaison au moment de l’exécution à un autre code managé compilé par rapport à une version précédente de l’assembly. 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 à un membre à une interface.
Toutefois, les changements cassants binaires ne sont pas les suivants :
- 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 apportée à 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 le désérialiser correctement et la traiter. Voici quelques exemples :
- Ajout de paramètres requis à une méthode RPC.
- Suppression d’un membre d’un type de données qui était précédemment garanti qu’il n’était pas null.
- 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é ou
KeyAttribute
de l’entier DataMemberAttribute.Order d’un membre existant.
Toutefois, les modifications suivantes ne sont pas des changements cassants de 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
verslong
oufloat
versdouble
). - Renommage d’un paramètre. Cela s’interrompt techniquement aux clients qui utilisent des arguments nommés JSON-RPC, mais les clients utilisant les ServiceJsonRpcDescriptor arguments positionnels d’utilisation par défaut et ne sont pas affectés par une modification de nom de paramètre. Cela n’a rien à voir avec si le code source du client utilise la syntaxe d’argument nommé, auquel un changement de paramètre serait une modification cassant la source.
Changement cassant comportemental : modification apportée à l’implémentation d’un service réparti qui ajoute ou modifie le comportement de telle sorte que les clients plus anciens peuvent mal fonctionner. Voici quelques exemples :
- L’initialisation d’un membre d’un type de données précédemment initialisé était toujours initialisé.
- Levée d’une exception sous une condition qui pouvait se terminer correctement.
- Renvoi d’une erreur avec un code d’erreur différent de celui retourné précédemment.
Toutefois, les changements cassants comportementaux suivants ne sont pas les suivants :
- Levée d’un nouveau type d’exception (car toutes les exceptions sont encapsulées RemoteInvocationException de toute façon).
Lorsque des modifications cassantes sont requises, elles peuvent être effectuées en toute sécurité en inscrivant et en promettant un nouveau moniker de service. Ce moniker peut partager le même nom, mais avec un numéro de version supérieur. L’interface RPC d’origine peut être réutilisable s’il n’existe aucune modification cassant binaire. Sinon, définissez une nouvelle interface pour la nouvelle version du service. Évitez de rompre les anciens clients en continuant à s’inscrire, à proffer et à prendre en charge également l’ancienne version.
Nous voulons éviter toutes ces modifications cassants, à l’exception de l’ajout de membres aux interfaces RPC.
Ajout de membres à des interfaces RPC
N’ajoutez pas de membres à une interface de rappel client RPC, car de nombreux clients peuvent implémenter cette interface et ajouter des membres entraînent la levée TypeLoadException du 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 une cible de rappel client RPC, définissez une nouvelle interface (qui peut dériver de l’original), puis suivez le processus standard pour proffer votre service réparti avec un numéro de version incrémenté et offrir un descripteur avec le type d’interface client mis à jour spécifié.
Vous pouvez ajouter des membres aux interfaces RPC qui définissent un service réparti. Ce n’est pas un changement cassant de protocole et n’est qu’un changement cassant binaire pour ceux qui implémentent le service, mais probablement vous mettez à jour le service pour implémenter le nouveau membre également. Étant donné que nos conseils sont que personne ne doit implémenter l’interface RPC à l’exception du service réparti lui-même (et les tests doivent utiliser des frameworks fictifs), l’ajout d’un membre à une interface RPC ne doit pas interrompre n’importe qui.
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 : faites passer la version mineure sur votre moniker de service lorsque vous ajoutez le membre et déclarez le nouveau descripteur.
- Mettez à jour votre service pour inscrire et proffer 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.