Beste methoden voor het ontwerpen van een bemiddelde dienst
Volg de algemene richtlijnen en beperkingen die worden beschreven voor RPC-interfaces voor StreamJsonRpc.
Daarnaast zijn de volgende richtlijnen van toepassing op brokered services.
Methodehandtekeningen
Alle methoden moeten een CancellationToken parameter als laatste parameter gebruiken. Deze parameter moet meestal niet een optionele parameter zijn, zodat aanroepers per ongeluk het argument weglaten. Zelfs als de implementatie van de methode naar verwachting triviaal is, kan de client met een CancellationToken zijn eigen aanvraag annuleren voordat deze naar de server wordt verzonden. Hierdoor kan de implementatie van de server zich ook ontwikkelen tot iets duurders zonder dat u de methode hoeft bij te werken om later annulering toe te voegen als een optie.
Overweeg te voorkomen dat meerdere overbelastingen van dezelfde methode op uw RPC-interface. Hoewel overload-oplossing meestal werkt (en er tests moeten worden geschreven om te controleren of dit wel het geval is), is het afhankelijk van die probeert argumenten te deserialiseren op basis van de parametertypen van elke overbelasting, wat resulteert in uitzonderingen bij de eerste poging die worden gegooid als een gebruikelijk onderdeel van het kiezen van een overbelasting. Omdat we het aantal eerste kans uitzonderingen die optreden in succespaden willen minimaliseren, is het beter om slechts één methode met een specifieke naam te hebben.
Parameter en retourtypen
Houd er rekening mee dat alle argumenten en retourwaarden die worden uitgewisseld via RPC alleen gegevenszijn. Ze worden allemaal geserialiseerd en via de kabel verzonden. Methoden die u op deze gegevenstypen definieert, werken alleen op die lokale kopie van de gegevens en kunnen niet worden gecommuniceerd met de RPC-service die deze heeft geproduceerd. De enige uitzonderingen op dit serialisatiegedrag zijn de exotische typen waarvoor StreamJsonRpc speciale ondersteuning heeft.
Overweeg om ValueTask<T>
boven Task<T>
te gebruiken als het retourtype van methoden, omdat ValueTask<T>
minder toewijzingen heeft.
Bij het gebruik van de niet-generieke variëteit (bijvoorbeeld Task en ValueTask) is het minder belangrijk, maar ValueTask kan nog steeds de voorkeur hebben.
Houd rekening met gebruiksbeperkingen voor ValueTask<T>
zoals beschreven in die API. Dit blogbericht en video kan handig zijn om te bepalen welk type u ook wilt gebruiken.
Aangepaste gegevenstypen
Overweeg om alle gegevenstypen te definiëren die onveranderbaar zijn, waardoor het delen van de gegevens in een proces veiliger is zonder te kopiëren en het idee voor consumenten te versterken dat ze de gegevens die ze ontvangen niet kunnen wijzigen in reactie op een query zonder een andere RPC te plaatsen.
Definieer uw gegevenstypen als class
in plaats van struct
wanneer u ServiceJsonRpcDescriptor.Formatters.UTF8gebruikt, waardoor de kosten van (mogelijk herhaald) 'boxing' worden vermeden wanneer u Newtonsoft.Json gebruikt.
Boksen niet optreden bij het gebruik van ServiceJsonRpcDescriptor.Formatters.MessagePack, zodat structs mogelijk een geschikte optie zijn als u zich aan die formatter houdt.
Overweeg het implementeren van IEquatable<T> en het overschrijven van GetHashCode() en Equals(Object) methoden voor uw gegevenstypen, waardoor de client gegevens die zijn ontvangen efficiënt kan opslaan, vergelijken en hergebruiken op basis van of deze gelijk is aan gegevens die op een ander moment zijn ontvangen.
Gebruik de DiscriminatedTypeJsonConverter<TBase> ter ondersteuning van het serialiseren van polymorfe typen met behulp van JSON.
Verzamelingen
Gebruik leesbare verzamelingeninterfaces in RPC-methodehandtekeningen (bijvoorbeeld IReadOnlyList<T>) in plaats van betontypen (bijvoorbeeld List<T> of T[]
), waardoor mogelijk efficiëntere deserialisatie mogelijk is.
Vermijd IEnumerable<T>.
Het ontbreken van een Count
eigenschap leidt tot inefficiënte code en impliceert mogelijke late generatie van gegevens, die niet van toepassing is in een RPC-scenario.
Gebruik in plaats daarvan IReadOnlyCollection<T> voor niet-geordende verzamelingen of IReadOnlyList<T> voor geordende verzamelingen.
Overweeg IAsyncEnumerable<T>. Elk ander verzamelingstype of IEnumerable<T> leidt ertoe dat de hele verzameling in één bericht wordt verzonden. Met behulp van IAsyncEnumerable<T> kan een klein eerste bericht worden verzonden en krijgt de ontvanger de middelen om net zoveel items uit de verzameling op te halen als ze willen, waarbij het asynchroon wordt opgesomd. Meer informatie over dit nieuwe patroon.
Waarnemerspatroon
Overweeg het gebruik van het ontwerppatroon van de waarnemer in uw interface. Dit is een eenvoudige manier voor de klant om zich te abonneren op data zonder de vele valkuilen die van toepassing zijn op het traditionele eventingmodel dat in de volgende sectie wordt beschreven.
Het waarnemerspatroon kan zo eenvoudig zijn als dit:
Task<IDisposable> SubscribeAsync(IObserver<YourDataType> observer);
De typen IDisposable en IObserver<T> die hierboven worden gebruikt, zijn twee van de exotische typen in StreamJsonRpc, waardoor ze een speciaal gemarshald gedrag krijgen in plaats van simpelweg als gegevens te worden geserialiseerd.
Gebeurtenissen
Gebeurtenissen kunnen om verschillende redenen problematisch zijn ten opzichte van RPC en we raden in plaats daarvan het hierboven beschreven waarnemerspatroon aan.
Houd er rekening mee dat de service geen inzicht heeft in het aantal gebeurtenis-handlers dat de client heeft gekoppeld wanneer de service en de client zich in afzonderlijke processen bevinden. JsonRpc voegt altijd precies één handler toe die verantwoordelijk is voor het doorgeven van de gebeurtenis aan de client. De client kan nul of meer handlers aan de verre zijde hebben gekoppeld.
De meeste RPC-clients hebben geen event handlers ingesteld wanneer ze eerst verbonden zijn. Vermijd het indienen van de eerste gebeurtenis totdat de client een methode 'Subscribe*' op uw interface heeft aangeroepen om de interesse en gereedheid voor het ontvangen van gebeurtenissen aan te geven.
Als uw gebeurtenis een delta in de status aangeeft (bijvoorbeeld een nieuw item dat is toegevoegd aan een verzameling), kunt u overwegen alle eerdere gebeurtenissen op te geven of alle huidige gegevens te beschrijven alsof deze nieuw is in het gebeurtenisargument wanneer een client zich abonneert om ze te helpen 'synchroniseren' met niets dan gebeurtenisafhandelingscode.
Overweeg om extra argumenten te accepteren voor de hierboven genoemde methode 'Subscribe*', als de client mogelijk interesse wil hebben in een subset van gegevens of meldingen, om het netwerkverkeer en de CPU te verminderen die nodig zijn om deze meldingen door te sturen.
Overweeg geen methode aan te bieden die de huidige waarde retourneert als u ook een gebeurtenis beschikbaar maakt om wijzigingsmeldingen te ontvangen, of klanten actief ontmoedigen om deze te gebruiken in combinatie met de gebeurtenis. Een client die zich abonneert op een gebeurtenis voor gegevens en een methode aanroept om de huidige waarde op te halen, loopt het risico veranderingen in de waarde mis te lopen en het risico een wijzigingsgebeurtenis te missen of moet uitzoeken hoe een wijzigingsgebeurtenis in de ene thread moet worden afgestemd op de waarde die in een andere thread is verkregen. Deze zorg is algemeen voor elke interface, niet alleen wanneer het over RPC gaat.
Naamgevingsconventies
- Gebruik het achtervoegsel
Service
op RPC-interfaces en een eenvoudigI
voorvoegsel. - Gebruik het
Service
achtervoegsel niet voor klassen in uw SDK. Uw bibliotheek of RPC-wrapper moet een naam gebruiken die precies beschrijft wat deze doet, waardoor de term 'service' wordt vermeden. - Vermijd de term 'op afstand' in interface- of lidnamen. Denk eraan dat brokered services idealiter evenzeer van toepassing zijn op lokale scenario's als op externe scenario's.
Problemen met versiecompatibiliteit
We willen dat een bemiddelingsservice die wordt blootgesteld aan andere extensies of die via Live Share wordt weergegeven, voorwaarts- en achterwaartscompatibel is, wat betekent dat we moeten aannemen dat een client ouder of nieuwer is dan de service en dat de functionaliteit ongeveer gelijk moet zijn aan die van de minst geavanceerde van de twee toepasselijke versies.
Laten we eerst de terminologie voor 'brekende wijzigingen' bekijken:
binaire doorbraak: een API-wijziging die ertoe zou leiden dat andere beheerde code die is gecompileerd op basis van een eerdere versie van de assembly, bij uitvoering niet kan worden gebonden aan de nieuwe versie. Voorbeelden zijn:
- De handtekening van een bestaand openbaar lid wijzigen.
- De naam van een openbaar lid wijzigen.
- Een openbaar type verwijderen.
- Een abstract lid aan een type toevoegen of een lid aan een interface toevoegen.
Maar de volgende zijn niet binaire wijzigingen die fouten veroorzaken:
- Een niet-abstract lid toevoegen aan een klasse of struct.
- Een volledige (niet-abstracte) interface-implementatie toevoegen aan een bestaand type.
protocol-doorbrekende wijziging: een wijziging in de geserialiseerde vorm van een bepaald gegevenstype of RPC-methode aanroep, zodat de externe partij deze niet correct kan deserialiseren en verwerken. Voorbeelden zijn:
- Vereiste parameters toevoegen aan een RPC-methode.
- Het verwijderen van een lid uit een gegevenstype dat eerder gegarandeerd niet null was.
- Het toevoegen van een vereiste dat een methode-aanroep moet worden geplaatst voordat andere bestaande bewerkingen worden uitgevoerd.
- Een kenmerk toevoegen, verwijderen of wijzigen in een veld of eigenschap waarmee de geserialiseerde naam van de gegevens in dat lid wordt bestuurd.
- (MessagePack): de eigenschap DataMemberAttribute.Order of integer
KeyAttribute
van een bestaand lid wijzigen.
Maar de volgende zijn geen protocolbrekende wijzigingen:
- Een optioneel lid toevoegen aan een gegevenstype.
- Leden toevoegen aan RPC-interfaces.
- Optionele parameters toevoegen aan bestaande methoden.
- Het wijzigen van een parametertype dat een geheel getal of float aangeeft in een parameter met een grotere lengte of precisie (bijvoorbeeld
int
inlong
offloat
indouble
). - De naam van een parameter wijzigen. Dit is technisch gezien een breuk voor klanten die gebruikmaken van JSON-RPC benoemde argumenten, maar klanten die de ServiceJsonRpcDescriptor gebruiken, maken standaard gebruik van positionele argumenten en worden niet beïnvloed door een wijziging van de parameternaam. Dit heeft niets te maken met of client-broncode de syntaxis van benoemde argumenten gebruikt, waarbij een parameternaam een bronbrede wijziging zou zijn.
ingrijpende gedragswijziging: een wijziging in de implementatie van een brokered service die gedrag toevoegt of aanpast, zodat oudere clients mogelijk storingen ondervinden. Voorbeelden zijn:
- Er wordt geen lid meer geïnitialiseerd van een gegevenstype dat voorheen altijd werd geïnitialiseerd.
- Het gooien van een uitzondering bij een voorwaarde die voorheen succesvol kon worden voltooid.
- Retourneert een fout met een andere foutcode dan eerder is geretourneerd.
Maar de volgende zijn geen gedragsbrekende wijzigingen:
- Het genereren van een nieuw uitzonderingstype (omdat alle uitzonderingen toch in RemoteInvocationException zijn verpakt).
Wanneer ingrijpende wijzigingen vereist zijn, kunnen deze veilig worden uitgevoerd door een nieuwe service-moniker te registreren en aan te bieden. Deze moniker kan dezelfde naam delen, maar met een hoger versienummer. De oorspronkelijke RPC-interface kan herbruikbaar zijn als er geen wijziging in binaire fouten is. Anders definieert u een nieuwe interface voor de nieuwe serviceversie. Voorkom dat bestaande klanten worden onderbroken door ook de oudere versie te registreren, aan te bieden en te ondersteunen.
We willen dergelijke belangrijke wijzigingen voorkomen, met uitzondering van het toevoegen van leden aan RPC-interfaces.
Leden toevoegen aan RPC-interfaces
Voeg
U kunt leden toevoegen aan RPC-interfaces die een brokered service definiëren. Dit is geen wijziging die het protocol doorbreekt en is alleen een binaire breuk voor degenen die de service implementeren, maar vermoedelijk zou u de service ook bijwerken om het nieuwe element te implementeren. Aangezien onze richtlijnen is dat niemand de RPC-interface moet implementeren, behalve de brokered-service zelf (en tests moeten mocking frameworks gebruiken), mag het toevoegen van een lid aan een RPC-interface niemand breken.
Deze nieuwe leden moeten xml-documentopmerkingen hebben die bepalen welke serviceversie dat lid voor het eerst heeft toegevoegd. Als een nieuwere client een methode aanroept op een oudere service die de methode niet implementeert, kan die client RemoteMethodNotFoundExceptionopvangen. Maar die client kan (en moet waarschijnlijk) de fout voorspellen en de aanroep in de eerste plaats vermijden. Best practices voor het toevoegen van leden aan bestaande services zijn onder andere:
- Als dit de eerste wijziging is binnen een release van uw service: Verhoog de minor versie van de naam van uw service wanneer u het element toevoegt en de nieuwe descriptor definieert.
- Wer uw service bij om de nieuwe versie te registreren en aan te bieden, naast, de oude versie.
- Als u een cliënt van uw bemiddelde dienst hebt, werkt u de cliënt bij om de nieuwere versie aan te vragen en valt u terug naar de oudere versie als de nieuwere als null terugkomt.