Dela via


Bästa praxis för att utforma en förmedlad tjänst

Följ de allmänna riktlinjer och begränsningar som dokumenterats för RPC-gränssnitt för StreamJsonRpc.

Dessutom gäller följande riktlinjer för förmedlade tjänster.

Metodsignaturer

Alla metoder bör ha en CancellationToken parameter som sin sista parameter. Den här parametern bör vanligtvis inte vara en valfri parameter, så anropare är mindre benägna att oavsiktligt utelämna argumentet. Även om implementeringen av metoden förväntas vara trivial, ger ett CancellationToken klienten möjlighet att avbryta sin egen begäran innan den skickas till servern. Det gör också att serverns implementering kan utvecklas till något dyrare utan att behöva uppdatera metoden för att lägga till annullering som ett alternativ senare.

Överväg att undvika flera överlagringar av samma metod i RPC-gränssnittet. Även om överbelastningsmatchning vanligtvis fungerar (och tester bör skrivas för att verifiera att den gör det), förlitar den sig på försöker att deserialisera argument baserat på parametertyperna för varje överlagring, vilket resulterar i att undantag från första chansen utlöses som en vanlig del av att välja en överlagring. Eftersom vi vill minimera antalet förstahandsundantag som utlöses i framgångsrika körvägar är det bättre att bara ha en metod med ett visst namn.

Parameter- och returtyper

Kom ihåg att alla argument och returvärden som utbyts över RPC är bara data. De är alla serialiserade och skickas över tråden. Alla metoder som du definierar på dessa datatyper fungerar bara på den lokala kopian av data och har inget sätt att kommunicera tillbaka till RPC-tjänsten som producerade dem. De enda undantagen till det här serialiseringsbeteendet är de exotiska typer som StreamJsonRpc har särskilt stöd för.

Överväg att använda ValueTask<T> över Task<T> som returtyp eftersom ValueTask<T> medför färre allokeringar. När du använder den icke-generiska sorten (till exempel Task och ValueTask) är det mindre viktigt, men ValueTask kan fortfarande vara att föredra. Tänk på användningsbegränsningar för ValueTask<T> som dokumenteras i api:et. Det här blogginlägget och video kan vara till hjälp när du bestämmer vilken typ som ska användas också.

Anpassade datatyper

Överväg att definiera att alla datatyper är oföränderliga, vilket möjliggör säkrare delning av data i en process utan att kopiera och hjälper till att förstärka tanken för konsumenterna att de inte kan ändra de data de får som svar på en fråga utan att placera en annan RPC.

Definiera dina datatyper som class i stället för struct när du använder ServiceJsonRpcDescriptor.Formatters.UTF8, vilket undviker kostnaden för (eventuellt upprepad) boxning när du använder Newtonsoft.Json. Boxning inte inträffar när du använder ServiceJsonRpcDescriptor.Formatters.MessagePack så structs kan vara ett lämpligt alternativ om du är engagerad i den formatören.

Överväg att implementera IEquatable<T> och åsidosätta GetHashCode() och Equals(Object) metoder för dina datatyper, vilket gör det möjligt för klienten att effektivt lagra, jämföra och återanvända data som tagits emot baserat på om det är lika med data som tas emot vid en annan tidpunkt.

Använd DiscriminatedTypeJsonConverter<TBase> för att stödja serialiserande polymorfa typer med JSON.

Samlingar

Använd skrivskyddade collectionsgränssnitt i RPC-metodsignaturer (till exempel IReadOnlyList<T>) i stället för konkreta typer (till exempel List<T> eller T[]), vilket möjliggör mer effektiv deserialisering.

Undvik IEnumerable<T>. Dess brist på en Count-egenskap leder till ineffektiv kod och innebär eventuell sen datagenerering, vilket inte gäller i ett RPC-scenario. Använd IReadOnlyCollection<T> för osorterade samlingar eller IReadOnlyList<T> för ordnade samlingar i stället.

Överväg IAsyncEnumerable<T>. Alla andra samlingstyper eller IEnumerable<T> resulterar i att hela samlingen skickas i ett meddelande. Med hjälp av IAsyncEnumerable<T> kan du få ett litet initialt meddelande och ger mottagaren möjlighet att hämta lika många objekt från samlingen som de vill, och räkna upp det asynkront. Läs mer om det här nya mönstret.

Observatörsmönster

Överväg att använda mönstret för observatörsdesign i gränssnittet. Det här är ett enkelt sätt för klienten att prenumerera på data utan de många fallgropar som gäller för den traditionella händelsemodellen som beskrivs i nästa avsnitt.

Övervakningsmönstret kan vara så enkelt som det här:

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

De IDisposable och IObserver<T> typer som används ovan är två av de exotiska typerna i StreamJsonRpc, så de får specialhanterat beteende istället för att bara serialiseras som data.

Evenemang

Händelser kan vara problematiska över RPC av flera skäl och vi rekommenderar det övervakningsmönster som beskrivs ovan i stället.

Tänk på att tjänsten inte har någon insyn i hur många händelsehanterare klienten har kopplat när tjänsten och klienten är i separata processer. JsonRpc bifogar alltid exakt en hanterare som ansvarar för att sprida händelsen till klienten. Klienten kan ha noll eller fler hanterare kopplade på den fjärranslutna sidan.

De flesta RPC-klienter kommer inte att ha händelsehanterare kopplade när de först är anslutna. Undvik att höja den första händelsen tills klienten har anropat en "Prenumerera*"-metod i gränssnittet för att ange intresse och beredskap för att ta emot händelser.

Om din händelse anger en skillnad i tillståndet (till exempel ett nytt objekt som lagts till i en samling) kan du överväga att återkalla alla tidigare händelser eller beskriva nuvarande data som nya i händelseargumentet när en klient prenumererar, för att hjälpa dem att synkronisera enbart med hjälp av händelsehanteringskod.

Överväg att acceptera extra argument i metoden "Prenumerera*" som nämns ovan om klienten kanske vill uttrycka intresse för en delmängd av data eller meddelanden, för att minska nätverkstrafiken och processorn som krävs för att vidarebefordra dessa meddelanden.

Överväg att inte erbjuda en metod som returnerar det aktuella värdet om du även exponerar en händelse för att ta emot ändringsmeddelanden eller aktivt avråder klienter från att använda den i kombination med händelsen. En klient som prenumererar på en händelse för data och anropar en metod för att hämta det aktuella värdet riskerar att hamna i en tävlan mot förändringar av värdet och kan antingen missa en förändringshändelse eller inte veta hur man ska förena en förändringshändelse på en tråd med värdet erhållet på en annan tråd. Det här problemet är allmänt för alla gränssnitt – inte bara när det är över RPC.

Namngivning

  • Använd suffixet Service på RPC-gränssnitt och ett enkelt I prefix.
  • Använd inte suffixet Service för klasser i SDK:t. Biblioteket eller RPC-omslutningen bör använda ett namn som beskriver exakt vad det gör, vilket undviker termen "tjänst".
  • Undvik termen "remote" i gränssnitts- eller medlemsnamn. Kom ihåg att förmedlade tjänster helst tillämpas lika mycket i lokala scenarier som i fjärr.

Problem med versionskompatibilitet

Vi vill att en förmedlad tjänst som exponeras för andra tillägg eller via Live Share ska vara både framåt- och bakåtkompatibel, vilket innebär att vi bör anta att en klient kan vara äldre eller nyare än tjänsten och att funktionaliteten bör anpassa sig efter den av de två tillämpliga versionerna som är den lägre.

Först ska vi granska terminologin för brytande förändringar:

  • Binär förändring som inte är kompatibel med tidigare versioner: En API-ändring som skulle göra att annan hanterad kod som kompilerats mot en tidigare version av samlingen inte kunde bindas under körning till den nya. Exempel är:

    • Ändra signaturen för en befintlig offentlig medlem.
    • Byta namn på en offentlig medlem.
    • Tar bort en offentlig typ.
    • Lägga till en abstrakt medlem i en typ eller någon medlem i ett gränssnitt.

    Men följande är inte binära brytande ändringar:

    • Lägga till en icke-abstrakt medlem i en klass eller struct.
    • Lägga till en fullständig (inte abstrakt) gränssnittsimplementering till en befintlig typ.
  • Protokollbrytande ändring: En ändring av den serialiserade formen av någon datatyp eller RPC-metodanrop så att fjärrparten inte kan deserialisera och bearbeta den korrekt. Exempel är:

    • Lägga till obligatoriska parametrar i en RPC-metod.
    • Ta bort en medlem från en datatyp som tidigare garanterades vara icke-null.
    • Lägga till ett krav på att ett metodanrop måste göras före andra befintliga åtgärder.
    • Lägga till, ta bort eller ändra ett attribut i ett fält eller en egenskap som styr det serialiserade namnet på data i medlemmen.
    • (MessagePack): ändra egenskapen DataMemberAttribute.Order eller KeyAttribute heltal för en befintlig medlem.

    Men följande är inte protokollbrytande ändringar:

    • Lägga till en valfri medlem i en datatyp.
    • Lägga till medlemmar i RPC-gränssnitt.
    • Lägga till valfria parametrar i befintliga metoder.
    • Ändra en parametertyp som representerar ett heltal eller flyttal till ett med större längd eller precision (till exempel int till long eller float till double).
    • Byter namn på en parameter. Detta bryter tekniskt sett mot klienter som använder JSON-RPC namngivna argument, men klienter som använder ServiceJsonRpcDescriptor använder positionella argument som standard och påverkas inte av en ändring av parameternamnet. Detta har inget att göra med huruvida klientens källkod använder benämnd argumentsyntax, åt vilken en parameteromdöpning skulle vara en ändring som bryter källkod.
  • Beteendebrytande ändring: En ändring av implementeringen av en förmedlad tjänst som lägger till eller ändrar beteende så att äldre klienter kan sluta fungera ordentligt. Exempel är:

    • Initierar inte längre en medlem av en datatyp som tidigare alltid initierades.
    • Kastar ett undantag under ett villkor som tidigare kunde slutföras framgångsrikt.
    • Returnerar ett fel med en annan felkod än vad som returnerades tidigare.

    Men följande är inte ändringar som bryter mot tidigare beteende:

När icke-bakåtkompatibla ändringar krävs kan de göras på ett säkert sätt genom att registrera och erbjuda en ny tjänstmoniker. Den här monikern kan dela samma namn, men med ett högre versionsnummer. Det ursprungliga RPC-gränssnittet kan återanvändas om det inte finns någon ändring som bryter den binära kompatibiliteten. Annars definierar du ett nytt gränssnitt för den nya tjänstversionen. Undvik att bryta gamla klienter genom att fortsätta att registrera, erbjuda och stödja den äldre versionen också.

Vi vill undvika alla sådana störningar, förutom att lägga till medlemmar i RPC-gränssnitten.

Lägga till medlemmar i RPC-gränssnitt

inte lägga till medlemmar i ett RPC-klientanropsgränssnitteftersom många klienter kan implementera gränssnittet och lägga till medlemmar skulle leda till att CLR genererar TypeLoadException när dessa typer läses in men inte implementerar de nya gränssnittsmedlemmarna. Om du måste lägga till medlemmar för att anropa ett RPC-klientåteranropsmål definierar du ett nytt gränssnitt (som kan härledas från originalet) och följer sedan standardprocessen för att tillhandahålla din mäklade tjänst med ett förhöjt versionsnummer, samt erbjuder en beskrivning med den uppdaterade klientgränssnittstypen angiven.

Du kan lägga till medlemmar i RPC-gränssnitt som definierar en förmedlad tjänst. Detta är inte en protokollförändrande ändring och är endast en binär icke-bakåtkompatibel ändring för dem som implementerar tjänsten, men antagligen skulle du uppdatera tjänsten för att även implementera den nya medlemmen. Eftersom vår vägledning är att ingen ska implementera RPC-gränssnittet förutom den förmedlade tjänsten själv (och tester bör använda mockningsramverk) bör det inte påverka någon negativt att lägga till en medlem i ett RPC-gränssnitt.

Dessa nya medlemmar bör ha xml-dokumentkommentar som identifierar vilken tjänstversion som först lade till medlemmen. Om en nyare klient anropar metoden på en äldre tjänst som inte implementerar metoden kan klienten fånga RemoteMethodNotFoundException. Men klienten kan (och bör förmodligen) förutsäga felet och undvika anropet från början. Metodtips för att lägga till medlemmar i befintliga tjänster är:

  • Om det här är den första ändringen i en version av din tjänst: Uppdatera delversionen på din servicebeteckning när du lägger till medlemmen till systemet och deklarerar den nya deskriptorn.
  • Uppdatera tjänsten för att registrera och erbjuda den nya versionen utöver den gamla versionen.
  • Om du har en klient för din förmedlade tjänst, uppdatera klienten för att begära den nyare versionen och återgå till att begära den äldre versionen om den nyare kommer tillbaka som null.