Freigeben über


Bewährte Methoden für das Entwerfen eines vermittelten Diensts

Befolgen Sie die allgemeinen Richtlinien und Einschränkungen, die für RPC-Schnittstellen für StreamJsonRpc dokumentiert sind.

Darüber hinaus gelten die folgenden Richtlinien für vermittelte Dienste.

Methodensignaturen

Alle Methoden sollten einen CancellationToken Parameter als letzten Parameter verwenden. Dieser Parameter sollte in der Regel kein optionaler Parameter sein, sodass Aufrufer das Argument weniger wahrscheinlich weglassen. Auch wenn die Implementierung der Methode als trivial zu erwarten ist, ermöglicht es dem CancellationToken Client, seine eigene Anforderung abzubrechen, bevor sie an den Server übertragen wird. Es ermöglicht auch, dass sich die Implementierung des Servers in etwas teurer weiterentwickeln kann, ohne die Methode aktualisieren zu müssen, um eine Abbruchoption später hinzuzufügen.

Erwägen Sie , mehrere Überladungen derselben Methode auf der RPC-Schnittstelle zu vermeiden. Während die Überladungsauflösung in der Regel funktioniert (und Tests geschrieben werden sollten, um zu überprüfen, ob dies der Fall ist), basiert sie auf dem Versuch , Argumente basierend auf den Parametertypen jeder Überladung zu deserialisieren, was zu ersten Zufallsausnahmen führt, die als regulärer Teil der Auswahl einer Überladung ausgelöst werden. Da wir die Anzahl der ersten Zufallsausnahmeregelungen minimieren möchten, die in Erfolgspfaden ausgelöst werden, empfiehlt es sich, einfach nur eine Methode mit einem bestimmten Namen zu haben.

Parameter- und Rückgabetypen

Denken Sie daran, dass alle Argumente und Rückgabewerte, die über RPC ausgetauscht werden, nur Daten sind. Sie werden alle serialisiert und über das Kabel gesendet. Alle Methoden, die Sie für diese Datentypen definieren, funktionieren nur mit dieser lokalen Kopie der Daten und haben keine Möglichkeit, mit dem RPC-Dienst zu kommunizieren, der sie erstellt hat. Die einzigen Ausnahmen dieses Serialisierungsverhaltens sind die exotischen Typen , für die StreamJsonRpc spezielle Unterstützung hat.

Erwägen Sie die Verwendung ValueTask<T>Task<T> als Rückgabetyp von Methoden, da ValueTask<T> weniger Zuordnungen anfallen. Bei der Verwendung der nicht generischen Sorte (z Task . B. und ValueTask) ist es weniger wichtig, kann aber ValueTask dennoch bevorzugt werden. Beachten Sie die Verwendungseinschränkungen ValueTask<T> , die in dieser API dokumentiert sind. Dieser Blogbeitrag und dieses Video können hilfreich sein, um zu entscheiden, welche Art ebenfalls verwendet werden soll.

Benutzerdefinierte Datentypen

Erwägen Sie, alle Datentypen so zu definieren, dass sie unveränderlich sind, was eine sicherere Freigabe der Daten über einen Prozess hinweg ermöglicht, ohne zu kopieren und die Idee für Verbraucher zu stärken, dass sie die empfangenen Daten nicht als Reaktion auf eine Abfrage ändern können, ohne ein anderes RPC zu platzieren.

Definieren Sie Die Datentypen nicht classstruct bei der Verwendung ServiceJsonRpcDescriptor.Formatters.UTF8, sondern vermeiden Sie die Kosten für das (potenziell wiederholte) Boxen bei Verwendung von Newtonsoft.Json. Boxen tritt nicht auf, wenn Sie diese Struktur verwenden ServiceJsonRpcDescriptor.Formatters.MessagePack , kann eine geeignete Option sein, wenn Sie diesem Formatierer zugesichert sind.

Erwägen Sie die Implementierung IEquatable<T> und Außerkraftsetzung GetHashCode() und Equals(Object) Methoden für Ihre Datentypen, wodurch der Client daten, die empfangen werden, effizient speichern, vergleichen und wiederverwenden kann, je nachdem, ob er datengleicht, die zu einem anderen Zeitpunkt empfangen werden.

Verwenden Sie die Unterstützung für die DiscriminatedTypeJsonConverter<TBase> Serialisierung von polymorphen Typen mithilfe von JSON.

Sammlungen

Verwenden Sie Readonly-Auflistungsschnittstellen in RPC-Methodensignaturen (z IReadOnlyList<T>. B. ) anstelle konkreter Typen (z List<T> . B. oder T[]), was eine potenziell effizientere Deserialisierung ermöglicht.

Vermeiden Sie IEnumerable<T>. Das Fehlen einer Count Eigenschaft führt zu ineffizientem Code und impliziert mögliche späte Generierung von Daten, die in einem RPC-Szenario nicht gelten. Wird stattdessen für ungeordnete Auflistungen oder IReadOnlyList<T> für sortierte Sammlungen verwendetIReadOnlyCollection<T>.

Gehen Sie von IAsyncEnumerable<T> aus. Jeder andere Sammlungstyp oder IEnumerable<T> führt dazu, dass die gesamte Sammlung in einer Nachricht gesendet wird. Die Verwendung IAsyncEnumerable<T> ermöglicht eine kleine anfängliche Nachricht und stellt dem Empfänger die Möglichkeit bereit, genauso viele Elemente aus der Sammlung wie gewünscht abzurufen und asynchron aufzulisten. Erfahren Sie mehr über dieses neuartige Muster.

Observer-Muster

Erwägen Sie die Verwendung des Beobachterentwurfsmusters in Ihrer Benutzeroberfläche. Dies ist eine einfache Möglichkeit für den Client, Daten ohne die vielen Fallstricke zu abonnieren, die für das herkömmliche Ereignismodell gelten, das im nächsten Abschnitt beschrieben wird.

Das Beobachtermuster kann so einfach sein:

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

Die IDisposable oben verwendeten Typen IObserver<T> sind zwei der exotischen Typen in StreamJsonRpc, sodass sie speziell gemarstisches Verhalten erhalten, anstatt als bloße Daten serialisiert zu werden.

Ereignisse

Ereignisse können aus mehreren Gründen problematisch sein und empfehlen stattdessen das oben beschriebene Beobachtermuster.

Beachten Sie, dass der Dienst keinen Einblick in die Anzahl der Ereignishandler hat, die der Client angefügt hat, wenn sich der Dienst und der Client in separaten Prozessen befinden. JsonRpc fügt immer genau einen Handler an, der für die Weitergabe des Ereignisses an den Client verantwortlich ist. Der Client verfügt möglicherweise über null oder mehr Handler, die auf der far-Seite angefügt sind.

Die meisten RPC-Clients verfügen nicht über Ereignishandler, die bei der ersten Verbindung verkabelt sind. Vermeiden Sie das Auslösen des ersten Ereignisses, bis der Client eine "Subscribe*"-Methode auf Ihrer Schnittstelle aufgerufen hat, um Interesse und Bereitschaft zum Empfangen von Ereignissen anzugeben.

Wenn Ihr Ereignis ein Delta im Zustand angibt (z. B. ein neues Element, das einer Auflistung hinzugefügt wurde), erwägen Sie, alle vergangenen Ereignisse auszuheben oder alle aktuellen Daten zu beschreiben, als ob es im Ereignisargument neu ist, wenn ein Client abonniert, um sie bei der Synchronisierung mit nichts als Ereignisbehandlungscode zu unterstützen.

Erwägen Sie, zusätzliche Argumente für die oben Erwähnung oben beschriebene Methode "Subscribe*" zu akzeptieren, wenn der Client interesse an einer Teilmenge von Daten oder Benachrichtigungen äußern möchte, um den Netzwerkdatenverkehr und die CPU zu reduzieren, die zum Weiterleiten dieser Benachrichtigungen erforderlich ist.

Erwägen Sie, keine Methode anzubieten, die den aktuellen Wert zurückgibt, wenn Sie auch ein Ereignis verfügbar machen, um Änderungsbenachrichtigungen zu erhalten, oder verhindern Sie aktiv, dass Clients sie in Kombination mit dem Ereignis verwenden. Ein Client, der ein Ereignis für Daten abonniert und eine Methode aufruft, um den aktuellen Wert abzurufen, steht für das Rennen gegen Änderungen an diesem Wert und fehlt entweder ein Änderungsereignis oder weiß nicht, wie ein Änderungsereignis in einem Thread mit dem wert in einem anderen Thread abgestimmt werden kann. Dieses Problem ist für jede Schnittstelle allgemein – nicht nur, wenn sie über RPC erfolgt.

Benennungskonventionen

  • Verwenden Sie das Service Suffix für RPC-Schnittstellen und ein einfaches I Präfix.
  • Verwenden Sie das Service Suffix nicht für Klassen in Ihrem SDK. Ihre Bibliothek oder Ihr RPC-Wrapper sollte einen Namen verwenden, der genau beschreibt, was es tut, und vermeiden Sie den Begriff "Dienst".
  • Vermeiden Sie den Begriff "remote" in Schnittstellen- oder Membernamen. Denken Sie daran, dass brokerierte Dienste idealerweise in lokalen Szenarien wie remote angewendet werden.

Kompatibilitätsprobleme bei der Version

Wir möchten, dass jeder vermittelte Dienst, der anderen Erweiterungen verfügbar gemacht oder über Live Share verfügbar gemacht wird, vorwärts und abwärtskompatibel ist, was bedeutet, dass wir davon ausgehen sollten, dass ein Client älter oder neuer als der Dienst ist und dass die Funktionalität ungefähr dem der geringeren der beiden anwendbaren Versionen entspricht.

Sehen wir uns zunächst die bahnbrechende Änderungsterminologie an:

  • Binäre Unterbrechungsänderung: Eine API-Änderung, die einen anderen verwalteten Code verursachen würde, der mit einer früheren Version der Assembly kompiliert wurde, kann zur Laufzeit nicht an die neue gebunden werden. Beispiele:

    • Ändern der Signatur eines vorhandenen öffentlichen Mitglieds.
    • Umbenennen eines öffentlichen Mitglieds.
    • Entfernen eines öffentlichen Typs.
    • Hinzufügen eines abstrakten Elements zu einem Typ oder einem beliebigen Element zu einer Schnittstelle.

    Es folgen jedoch keine binären Unterbrechungsänderungen:

    • Hinzufügen eines nicht abstrakten Elements zu einer Klasse oder Struktur.
    • Hinzufügen einer vollständigen (nicht abstrakten) Schnittstellenimplementierung zu einem vorhandenen Typ.
  • Protokollbruchänderung: Eine Änderung an der serialisierten Form eines Datentyps oder rpc-Methodenaufrufs, sodass die Remotepartei sie nicht ordnungsgemäß deserialisieren und verarbeiten kann. Beispiele:

    • Hinzufügen erforderlicher Parameter zu einer RPC-Methode.
    • Entfernen eines Elements aus einem Datentyp, der zuvor garantiert nicht null war.
    • Hinzufügen einer Anforderung, dass ein Methodenaufruf vor anderen bereits vorhandenen Vorgängen platziert werden muss.
    • Hinzufügen, Entfernen oder Ändern eines Attributs für ein Feld oder eine Eigenschaft, das den serialisierten Namen der Daten in diesem Element steuert.
    • (MessagePack): Ändern der DataMemberAttribute.Order Eigenschaft oder KeyAttribute ganzzahl eines vorhandenen Elements.

    Es folgen jedoch keine protokollbrechenden Änderungen:

    • Hinzufügen eines optionalen Elements zu einem Datentyp.
    • Hinzufügen von Mitgliedern zu RPC-Schnittstellen.
    • Hinzufügen optionaler Parameter zu vorhandenen Methoden.
    • Ändern eines Parametertyps, der eine ganze Zahl oder einen Gleitkommawert mit einer größeren Länge oder Genauigkeit darstellt (z int . B. an long oder float an double).
    • Umbenennen eines Parameters. Dies ist technisch für Clients, die JSON-RPC-benannte Argumente verwenden, aber Clients, die ServiceJsonRpcDescriptor standardmäßig positionsbezogene Argumente verwenden, und würden nicht durch eine Änderung des Parameternamens beeinträchtigt werden. Dies hat nichts damit zu tun, ob der Clientquellcode benannte Argumentsyntax verwendet, zu der ein Parameter umbenennen würde, um eine quellbrechende Änderung zu handeln.
  • Verhaltensbruchänderung: Eine Änderung der Implementierung eines brokerierten Diensts, der das Verhalten hinzufügt oder ändert, sodass ältere Clients fehlschlagen können. Beispiele:

    • Das Initialisieren eines Elements eines Datentyps, der zuvor immer initialisiert wurde, wird nicht mehr initialisiert.
    • Auslösen einer Ausnahme unter einer Bedingung, die zuvor erfolgreich abgeschlossen werden konnte.
    • Zurückgeben eines Fehlers mit einem anderen Fehlercode als zuvor zurückgegeben.

    Es folgen jedoch keine Verhaltensbruchänderungen:

Wenn Unterbrechungen erforderlich sind, können sie sicher vorgenommen werden, indem sie einen neuen Service moniker registrieren und abweisen. Dieser Moniker kann denselben Namen haben, aber mit einer höheren Versionsnummer. Die ursprüngliche RPC-Schnittstelle kann wiederverwendbar sein, wenn keine binäre Unterbrechungsänderung vorhanden ist. Definieren Sie andernfalls eine neue Schnittstelle für die neue Dienstversion. Vermeiden Sie es, alte Clients zu unterbrechen, indem Sie weiterhin registrieren, bereitstellen und auch die ältere Version unterstützen.

Wir möchten alle derartigen änderungen vermeiden, mit Ausnahme des Hinzufügens von Membern zu RPC-Schnittstellen.

Hinzufügen von Mitgliedern zu RPC-Schnittstellen

Fügen Sie keine Member zu einer RPC-Clientrückrufschnittstelle hinzu, da viele Clients diese Schnittstelle implementieren und Member hinzufügen würden, TypeLoadException wenn diese Typen geladen werden, aber nicht die neuen Schnittstellenmitglieder implementieren. Wenn Sie Mitglieder hinzufügen müssen, um ein RPC-Clientrückrufziel aufzurufen, definieren Sie eine neue Schnittstelle (die vom Original abgeleitet werden kann), und befolgen Sie dann den Standardprozess für die Bereitstellung Ihres brokerierten Diensts mit einer inkrementierten Versionsnummer und bieten einen Deskriptor mit dem angegebenen aktualisierten Clientschnittstellentyp an.

Sie können Member zu RPC-Schnittstellen hinzufügen, die einen brokerierten Dienst definieren. Dies ist keine protokollgebrochene Änderung, und es handelt sich nur um eine binäre Änderung, die den Dienst implementiert, aber vermutlich aktualisieren Sie den Dienst, um auch das neue Mitglied zu implementieren. Da unsere Anleitung darin besteht, dass niemand die RPC-Schnittstelle implementieren sollte, mit Ausnahme des brokerierten Diensts selbst (und Tests sollten Mocking Frameworks verwenden), sollte das Hinzufügen eines Mitglieds zu einer RPC-Schnittstelle niemanden unterbrechen.

Diese neuen Member sollten XML-Dokumentkommentare enthalten, die identifizieren, welche Dienstversion das Mitglied zuerst hinzugefügt hat. Wenn ein neuerer Client die Methode für einen älteren Dienst aufruft, der die Methode nicht implementiert, kann dieser Client abfangen RemoteMethodNotFoundException. Aber dieser Client kann (und sollte wahrscheinlich) den Fehler vorhersagen und den Anruf an erster Stelle vermeiden. Zu den bewährten Methoden zum Hinzufügen von Mitgliedern zu vorhandenen Diensten gehören:

  • Wenn dies die erste Änderung innerhalb einer Version Ihres Diensts ist: Heben Sie die Nebenversion ihres Dienstmonikers auf, wenn Sie das Mitglied hinzufügen und den neuen Deskriptor deklarieren.
  • Aktualisieren Sie Ihren Dienst, um die neue Version zusätzlich zur alten Version zu registrieren und zu nutzen.
  • Wenn Sie über einen Client Ihres brokerierten Diensts verfügen, aktualisieren Sie Ihren Client so, dass er die neuere Version anfordert, und fallback, um die ältere Version anzufordern, wenn der neuere als NULL zurückkommt.