Tillhandahålla en förmedlad tjänst
En förmedlad tjänst består av följande element:
- Ett gränssnitt som deklarerar tjänstens funktioner och fungerar som ett kontrakt mellan tjänsten och dess klienter.
- En implementering av gränssnittet.
- En serviceidentifierare för att tilldela ett namn och en version till tjänsten.
- En beskrivare som kombinerar tjänstmonikern med beteende vid hantering av RPC (fjärrproceduranrop) när det är nödvändigt.
- Antingen erbjud tjänstfabriken och registrera din förmedlade tjänst med ett VS-paket, eller gör båda med MEF (Managed Extensibility Framework).
Vart och ett av objekten i föregående lista beskrivs i detalj i följande avsnitt.
Med all kod i den här artikeln rekommenderar vi starkt att du aktiverar C#:s null-referenstyper funktion.
Tjänstgränssnittet
Tjänstgränssnittet kan vara ett .NET-standardgränssnitt (skrivs ofta i C#), men bör följa riktlinjerna som anges av den ServiceRpcDescriptor-härledda typ som tjänsten använder för att säkerställa att gränssnittet kan användas via RPC när klienten och tjänsten körs i olika processer.
Dessa begränsningar omfattar vanligtvis att egenskaper och indexerare inte tillåts, och de flesta eller alla metoder returnerar Task
eller någon annan asynkron kompatibel returtyp.
ServiceJsonRpcDescriptor är den rekommenderade härledda typen för brokerade tjänster. Den här klassen använder StreamJsonRpc-biblioteket när klienten och tjänsten kräver RPC för att kommunicera. StreamJsonRpc tillämpar vissa begränsningar på tjänstgränssnittet, beskrivs som här .
Gränssnittet kan härledas från IDisposable, System.IAsyncDisposableeller till och med Microsoft.VisualStudio.Threading.IAsyncDisposable men detta krävs inte av systemet. De genererade klientproxyservrarna implementerar IDisposable åt båda hållen.
Ett enkelt gränssnitt för kalkylatorns tjänst kan deklareras så här:
public interface ICalculator
{
ValueTask<double> AddAsync(double a, double b, CancellationToken cancellationToken);
ValueTask<double> SubtractAsync(double a, double b, CancellationToken cancellationToken);
}
Även om implementeringen av metoderna i det här gränssnittet kanske inte kräver en asynkron metod använder vi alltid asynkrona metodsignaturer i det här gränssnittet eftersom det här gränssnittet används för att generera klientproxyn som kan anropa den här tjänsten via fjärranslutning, vilket säkert garanterar en asynkron metodsignatur.
Ett gränssnitt kan deklarera händelser som kan användas för att meddela sina klienter om händelser som inträffar i tjänsten.
Utöver händelser eller observatörsdesignmönstret kan en förmedlad tjänst som behöver "anropa tillbaka" till klienten definiera ett andra gränssnitt som fungerar som det kontrakt som en klient måste implementera och tillhandahålla via egenskapen ServiceActivationOptions.ClientRpcTarget när tjänsten begärs. Ett sådant gränssnitt bör överensstämma med samma designmönster och begränsningar som det förmedlade tjänstgränssnittet, men med ytterligare begränsningar för versionshantering.
Läs bästa praxis för att utforma en mäklad tjänst för tips om hur du utformar ett högpresterande, framtidssäkert RPC-gränssnitt.
Det kan vara användbart att deklarera det här gränssnittet i en separat sammansättning från den sammansättning som implementerar tjänsten så att dess klienter kan referera till gränssnittet utan att tjänsten behöver exponera mer av implementeringsinformationen. Det kan också vara användbart att skicka gränssnittssammansättningen som ett NuGet-paket för andra tillägg att referera till samtidigt som du reserverar ditt eget tillägg för att leverera tjänstimplementeringen.
Överväg att rikta in dig på den assembly som deklarerar ditt tjänstgränssnitt till att vara kompatibel med netstandard2.0
, för att säkerställa att tjänsten enkelt kan anropas från valfri .NET-process, oavsett om den kör .NET Framework, .NET Core, .NET 5 eller senare.
Testning
Automatiserade tester ska skrivas tillsammans med din tjänst gränssnitt för att verifiera gränssnittets RPC-beredskap.
Testerna bör kontrollera att alla data som skickas via gränssnittet är serialiserbara.
Du kan hitta BrokeredServiceContractTestBase<TInterface,TServiceMock>-klassen från Microsoft.VisualStudio.Sdk.TestFramework.Xunit paket som är användbart för att härleda din gränssnittstestklass från. Den här klassen innehåller några grundläggande konventionstester för ditt gränssnitt, metoder för att hjälpa till med vanliga påståenden som händelsetestning med mera.
Metoder
Kontrollera att varje argument och returvärdet serialiserades helt. Om du använder testbasklassen som nämns ovan kan koden se ut så här:
public interface IYourService
{
Task<bool> SomeOperationAsync(YourStruct arg1);
}
public static class Descriptors
{
public static readonly ServiceRpcDescriptor YourService = new ServiceJsonRpcDescriptor(
new ServiceMoniker("YourCompany.YourExtension.YourService", new Version(1, 0)),
clientInterface: null,
ServiceJsonRpcDescriptor.Formatters.MessagePack,
ServiceJsonRpcDescriptor.MessageDelimiters.BigEndianInt32LengthHeader,
new MultiplexingStream.Options { ProtocolMajorVersion = 3 })
.WithExceptionStrategy(StreamJsonRpc.ExceptionProcessing.ISerializable);
}
public class YourServiceMock : IYourService
{
internal YourStruct? SomeOperationArg1 { get; set; }
public Task<bool> SomeOperationAsync(YourStruct arg1, CancellationToken cancellationToken)
{
this.SomeOperationArg1 = arg1;
return true;
}
}
public class BrokeredServiceTests : BrokeredServiceContractTestBase<IYourService, YourServiceMock>
{
public BrokeredServiceTests(ITestOutputHelper logger)
: base(logger, Descriptors.YourService)
{
}
[Fact]
public async Task SomeOperation()
{
var arg1 = new YourStruct
{
Field1 = "Something",
};
Assert.True(await this.ClientProxy.SomeOperationAsync(arg1, this.TimeoutToken));
Assert.Equal(arg1.Field1, this.Service.SomeOperationArg1.Value.Field1);
}
}
Överväg att testa överbelastningsupplösning om du deklarerar flera metoder med samma namn.
Du kan lägga till ett internal
fält i din mock-tjänst för varje metod på den som lagrar argument för den metoden så att testmetoden kan anropa metoden och sedan kontrollera att rätt metod anropades med rätt argument.
Evenemang
Alla händelser som deklareras i gränssnittet bör också testas för RPC-beredskap. Händelser som genereras från en asynkron tjänst inte orsaka ett testfel om de misslyckas under RPC-serialiseringen eftersom händelserna är "eld och glöm".
Om du använder testbasklassen som nämns ovan är det här beteendet redan inbyggt i vissa hjälpmetoder och kan se ut så här (med oförändrade delar utelämnade för korthet):
public interface IYourService
{
event EventHandler<int> NewTotal;
}
public class YourServiceMock : IYourService
{
public event EventHandler<int>? NewTotal;
internal void RaiseNewTotal(int arg) => this.NewTotal?.Invoke(this, arg);
}
public class BrokeredServiceTests : BrokeredServiceContractTestBase<IYourService, YourServiceMock>
{
[Fact]
public async Task NewTotal()
{
await this.AssertEventRaisedAsync<int>(
(p, h) => p.NewTotal += h,
(p, h) => p.NewTotal -= h,
s => s.RaiseNewTotal(50),
a => Assert.Equal(50, a));
}
}
Implementera tjänsten
Tjänstklassen ska implementera RPC-gränssnittet som deklarerats i föregående steg. En tjänst kan implementera IDisposable eller andra gränssnitt utöver det som används för RPC. Proxyn som genereras på klienten implementerar endast tjänstgränssnittet, IDisposable, och eventuellt några andra utvalda gränssnitt för att stödja systemet, så att en gjutning till andra gränssnitt som implementeras av tjänsten misslyckas på klienten.
Tänk på kalkylatorexemplet som används ovan, som vi implementerar här:
internal class Calculator : ICalculator
{
public ValueTask<double> AddAsync(double a, double b, CancellationToken cancellationToken)
{
return new ValueTask<double>(a + b);
}
public ValueTask<double> SubtractAsync(double a, double b, CancellationToken cancellationToken)
{
return new ValueTask<double>(a - b);
}
}
Eftersom själva metodkropparna inte behöver vara asynkrona omsluter vi uttryckligen returvärdet i en konstruerad ValueTask<TResult> returtyp för att överensstämma med tjänstgränssnittet.
Implementera det observerbara designmönstret
Om du erbjuder en observatörsprenumeration i tjänstgränssnittet kan det se ut så här:
Task<IDisposable> SubscribeAsync(IObserver<YourDataType> observer);
Argumentet IObserver<T> behöver vanligtvis överleva livslängden för det här metodanropet så att klienten kan fortsätta att ta emot uppdateringar när metodanropet har slutförts tills klienten tar bort det returnerade IDisposable värdet. För att underlätta detta kan din tjänstklass innehålla en samling IObserver<T> prenumerationer som alla uppdateringar som görs i ditt tillstånd sedan skulle räkna upp för att uppdatera alla prenumeranter. Säkerställ att uppräkningen av din samling är trådsäker med avseende på varandra och särskilt med ändringarna i samlingen som kan inträffa genom ytterligare prenumerationer eller borttagningar av dessa prenumerationer.
Se till att alla uppdateringar som publiceras via OnNext behåller den ordning som tillståndsändringar infördes till din tjänst.
Alla prenumerationer bör slutligen avslutas med antingen ett anrop till OnCompleted eller OnError för att undvika resursläckor på klienten och RPC-systemen. Detta inkluderar när tjänster avvecklas där alla återstående prenumerationer ska avslutas tydligt.
Läs mer om mönstret för observatörsdesign, hur du implementerar en observerbar dataprovider och särskilt med RPC i åtanke.
Engångstjänster
Din tjänstklass behöver inte vara tillgänglig för borttagning, men tjänster som är det, kommer att tas bort när klienten tar bort sin proxy till din tjänst eller om anslutningen mellan klient och tjänst förloras. Engångsgränssnitt testas i den här ordningen: System.IAsyncDisposable, Microsoft.VisualStudio.Threading.IAsyncDisposable, IDisposable. Endast det första gränssnittet från den här listan som tjänstklassen implementerar används för att ta bort tjänsten.
Tänk på trådsäkerhet när du överväger bortskaffande. Din Dispose-metod kan anropas på valfri tråd medan annan kod i tjänsten körs (till exempel om en anslutning tas bort).
Utlösa undantag
När du utlöser undantag bör du överväga att utlösa LocalRpcException med en specifik ErrorCode- för att kontrollera felkoden som tas emot av klienten i RemoteInvocationException. Genom att tillhandahålla klienter med en felkod kan de bättre fatta beslut baserat på felets art, snarare än att tolka undantagsmeddelanden eller typer.
Enligt specifikationen JSON-RPC måste felkoderna vara större än -32000, inklusive positiva tal.
Använda andra mäklartjänster
När en mäklingstjänst själv kräver åtkomst till en annan mäklingstjänst rekommenderar vi att du använder IServiceBroker som tillhandahålls till tjänstfabriken, men det är särskilt viktigt när mäklingstjänstregistreringen anger flaggan AllowTransitiveGuestClients.
För att följa den här riktlinjen, om vår kalkylatortjänst hade behov av andra förmedlade tjänster för att implementera dess beteende, skulle vi ändra konstruktorn för att acceptera en IServiceBroker:
internal class Calculator : ICalculator
{
private readonly State state;
private readonly IServiceBroker serviceBroker;
internal class Calculator(State state, IServiceBroker serviceBroker)
{
this.state = state;
this.serviceBroker = serviceBroker;
}
// ...
}
Läs mer om hur du skyddar en ombudstjänst och förbrukar ombudstjänster.
Tillståndsbevarande tjänster
Tillstånd per klient
En ny instans av den här klassen skapas för varje klient som begär tjänsten.
Ett fält i Calculator
-klassen ovan skulle lagra ett värde som kan vara unikt för varje klient.
Anta att vi lägger till en räknare som ökar varje gång en åtgärd utförs:
internal class Calculator : ICalculator
{
int operationCounter;
public ValueTask<double> AddAsync(double a, double b, CancellationToken cancellationToken)
{
this.operationCounter++;
return new ValueTask<double>(a + b);
}
public ValueTask<double> SubtractAsync(double a, double b, CancellationToken cancellationToken)
{
this.operationCounter++;
return new ValueTask<double>(a - b);
}
}
Dina förmedlade tjänster bör skrivas för att följa trådsäkra metoder.
När du använder den rekommenderade ServiceJsonRpcDescriptorkan fjärranslutningar med klienter innebära samtidig körning av metoder för din tjänst, enligt beskrivningen i det här dokumentet .
När klienten delar en process och AppDomain med tjänsten kan klienten anropa tjänsten samtidigt från flera trådar.
En trådsäker implementering av exemplet ovan kan använda Interlocked.Increment(Int32) för att öka fältet operationCounter
.
Delat tillstånd
Om det finns tillstånd som tjänsten behöver dela över alla sina klienter bör det här tillståndet definieras i en distinkt klass som instansieras av VS-paketet och skickas som ett argument till tjänstens konstruktor.
Anta att vi vill att operationCounter
som definierats ovan ska räkna alla operationer över alla klienter i tjänsten.
Vi behöver lyfta fältet till den här nya tillståndsklassen.
internal class Calculator : ICalculator
{
private readonly State state;
internal Calculator(State state)
{
this.state = state;
}
public ValueTask<double> AddAsync(double a, double b, CancellationToken cancellationToken)
{
this.state.IncrementCounter();
return new ValueTask<double>(a + b);
}
public ValueTask<double> SubtractAsync(double a, double b, CancellationToken cancellationToken)
{
this.state.IncrementCounter();
return new ValueTask<double>(a - b);
}
internal class State
{
private int operationCounter;
internal int OperationCounter => this.operationCounter;
internal void IncrementCounter() => Interlocked.Increment(ref this.operationCounter);
}
}
Nu har vi ett elegant och testbart sätt att hantera delat tillstånd över flera instanser av vår Calculator
-tjänst.
Senare när du skriver koden för att erbjuda tjänsten ser vi hur den här State
-klassen skapas en gång och delas med varje instans av Calculator
-tjänsten.
Det är särskilt viktigt att vara trådsäker när du hanterar delat tillstånd eftersom inget antagande kan göras kring flera klienter som schemalägger sina anrop så att de aldrig görs samtidigt.
Om klassen för delat tillstånd behöver komma åt andra förmedlade tjänster bör den använda den globala tjänstförmedlaren i stället för någon av de kontextuella som tilldelats en enskild instans av din förmedlade tjänst. Att använda den globala tjänstkoordinatorn inom en asynkron tjänst medför säkerhetskonsekvenser när flaggan ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients har angetts.
Säkerhetsproblem
Säkerhet är en övervägning för din mäklade tjänst om den är registrerad med flaggan ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients, vilket exponerar den för åtkomst av andra användare på andra datorer som deltar i en delad Live Share-session.
Granska Hur du skyddar en förmedlad tjänst och vidta nödvändiga säkerhetsåtgärder innan du ställer in flaggan AllowTransitiveGuestClients.
Tjänstens moniker
En förmedlad tjänst måste ha ett serialiserbart namn och en valfri version genom vilken en klient kan begära tjänsten. En ServiceMoniker är en praktisk förpackning för dessa två delar av informationen.
En tjänstmoniker motsvarar det sammanställningskvalificerade fullständiga namnet på en CLR-typ (Common Language Runtime). Det måste vara globalt unikt och bör därför innehålla ditt företagsnamn och kanske ditt tilläggsnamn som prefix för själva tjänstnamnet.
Det kan vara användbart att definiera den här monikern i ett static readonly
fält för användning någon annanstans:
public static readonly ServiceMoniker Moniker = new ServiceMoniker("YourCompany.Extension.Calculator", new Version("1.0"));
Även om de flesta användningar av din tjänst kanske inte använder monikern direkt, kommer en klient som kommunicerar över rör i stället för en proxy att behöva monikern.
Även om en version är valfri för ett namn, rekommenderas att tillhandahålla en version eftersom det ger tjänsteutvecklare fler alternativ för att upprätthålla kompatibilitet med klienter vid förändringar i beteende.
Tjänstbeskrivningen
Tjänstbeskrivningen kombinerar tjänstmonikern med de beteenden som krävs för att köra en RPC-anslutning och skapa en lokal proxy eller fjärrproxy. Deskriptorn ansvarar för att effektivt konvertera ditt RPC-gränssnitt till ett nätverksprotokoll. Den här tjänstbeskrivningen är en instans av en ServiceRpcDescriptor-härledd typ. Beskrivningen måste göras tillgänglig för alla klienter som använder en proxy för att få åtkomst till den här tjänsten. För att kunna erbjuda tjänsten krävs även den här beskrivningen.
Visual Studio definierar en sådan härledd typ och rekommenderar att den används för alla tjänster: ServiceJsonRpcDescriptor. Den här beskrivningen använder StreamJsonRpc för sina RPC-anslutningar och skapar en lokal proxy med höga prestanda för lokala tjänster som emulerar några av fjärrbeteendena, till exempel omslutningsfel som genereras av tjänsten i RemoteInvocationException.
ServiceJsonRpcDescriptor stöder konfiguration av JsonRpc-klassen för JSON- eller MessagePack-kodning av JSON-RPC-protokollet. Vi rekommenderar MessagePack-kodning eftersom den är mer kompakt och kan vara 10X mer högpresterande.
Vi kan definiera en beskrivning för vår kalkylatortjänst så här:
/// <summary>
/// The descriptor for the calculator brokered service.
/// Use the <see cref="ICalculator"/> interface for the client proxy for this service.
/// </summary>
public static readonly ServiceRpcDescriptor CalculatorService = new ServiceJsonRpcDescriptor(
Moniker,
Formatters.MessagePack,
MessageDelimiters.BigEndianInt32LengthHeader,
new MultiplexingStream.Options { ProtocolMajorVersion = 3 })
.WithExceptionStrategy(StreamJsonRpc.ExceptionProcessing.ISerializable);
Som du ser ovan finns det ett val av formaterare och avgränsare. Eftersom inte alla kombinationer är giltiga rekommenderar vi någon av dessa kombinationer:
ServiceJsonRpcDescriptor.Formatters | ServiceJsonRpcDescriptor.MessageDelimiters | Bäst för |
---|---|---|
MessagePack | BigEndianInt32LengthHeader | Höga prestanda |
UTF8 (JSON) | HttpLikeHeaders | Interop med andra JSON-RPC-system |
Genom att ange MultiplexingStream.Options
-objektet som den sista parametern är RPC-anslutningen som delas mellan klienten och tjänsten endast en kanal i en MultiplexingStream, som delas med den JSON-RPC-anslutningen till för att aktivera effektiv överföring av stora binära data över JSON-RPC.
Strategin ExceptionProcessing.ISerializable orsakar att undantag som kastas från tjänsten serialiseras och bevaras som Exception.InnerException till den RemoteInvocationException som kastas på klienten. Utan den här inställningen är mindre detaljerad undantagsinformation tillgänglig på klienten.
Tips: Exponera beskrivningen som ServiceRpcDescriptor i stället för någon härledd typ som du använder som implementeringsinformation. Detta ger dig större flexibilitet att ändra implementeringsinformationen senare utan att API:et bryter mot ändringar.
Ta med en referens till tjänstgränssnittet i xml-dokumentkommentaren i beskrivningen för att göra det enklare för användarna att använda tjänsten. Referera även till gränssnittet som din tjänst accepterar som klientens RPC-mål, om tillämpligt.
Vissa mer avancerade tjänster kan också acceptera eller kräva ett RPC-målobjekt från klienten som överensstämmer med något gränssnitt.
I sådana fall använder du en ServiceJsonRpcDescriptor konstruktor med en Type clientInterface
parameter för att ange gränssnittet som klienten ska ange en instans av.
Versionshantering av beskrivningen
Med tiden kanske du vill öka versionen av tjänsten. I sådana fall bör du definiera en beskrivning för varje version som du vill stödja, med hjälp av en unik versionsspecifik ServiceMoniker för var och en. Stöd för flera versioner samtidigt kan vara bra för bakåtkompatibilitet och kan vanligtvis göras med bara ett RPC-gränssnitt.
Visual Studio följer det här mönstret med sin VisualStudioServices-klass genom att definiera den ursprungliga ServiceRpcDescriptor som en virtual
-egenskap i den kapslade klassen som representerar den första versionen som lade till den förmedlade tjänsten.
När vi behöver ändra trådprotokollet eller lägga till/ändra funktionen för tjänsten deklarerar Visual Studio en override
egenskap i en senare version av kapslad klass som returnerar en ny ServiceRpcDescriptor.
För en tjänst som definieras och erbjuds av ett Visual Studio-tillägg kan det räcka att deklarera en annan deskriptoregenskap bredvid originalet. Anta till exempel att din 1.0-tjänst använde UTF8-formatören (JSON) och du inser att en övergång till MessagePack skulle ge en betydande prestandafördel. Eftersom det är en protokolländring som bryter kompatibiliteten att ändra formateraren måste du höja versionsnumret för den förmedlade tjänsten och en andra deskriptor. De två beskrivningarna tillsammans kan se ut så här:
public static readonly ServiceJsonRpcDescriptor CalculatorService = new ServiceJsonRpcDescriptor(
new ServiceMoniker("YourCompany.Extension.Calculator", new Version("1.0")),
Formatters.UTF8,
MessageDelimiters.HttpLikeHeaders,
new MultiplexingStream.Options { ProtocolMajorVersion = 3 })
);
public static readonly ServiceJsonRpcDescriptor CalculatorService1_1 = new ServiceJsonRpcDescriptor(
new ServiceMoniker("YourCompany.Extension.Calculator", new Version("1.1")),
Formatters.MessagePack,
MessageDelimiters.BigEndianInt32LengthHeader,
new MultiplexingStream.Options { ProtocolMajorVersion = 3 });
Även om vi deklarerar två deskriptorer (och senare måste vi erbjuda och registrera två tjänster) att vi kan göra detta med bara ett tjänstgränssnitt och implementering, vilket behåller omkostnaderna för att stödja flera tjänstversioner ganska lågt.
Erbjud tjänsten
Din förmedlade tjänst måste skapas när en begäran kommer in, vilket ordnas via ett steg som kallas för att tillhandahålla tjänsten.
Tjänstfabriken
Använd GlobalProvider.GetServiceAsync för att begära SVsBrokeredServiceContainer. Anropa sedan IBrokeredServiceContainer.Proffer på containern för att erbjuda tjänsten.
I exemplet nedan erbjuder vi en tjänst med hjälp av fältet CalculatorService
som deklarerats tidigare, vilket är inställt på en instans av en ServiceRpcDescriptor.
Vi skickar vidare den till vår servicefabrik, som är en BrokeredServiceFactory delegerad.
IBrokeredServiceContainer container = await AsyncServiceProvider.GlobalProvider.GetServiceAsync<SVsBrokeredServiceContainer, IBrokeredServiceContainer>();
container.Proffer(
CalculatorService,
(moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(new CalculatorService()));
En förmedlad tjänst instansieras vanligtvis en gång per klient. Detta är en avvikelse från andra VS-tjänster (Visual Studio), som vanligtvis instansieras en gång och delas mellan alla klienter. Att skapa en instans av tjänsten per klient ger bättre säkerhet eftersom varje tjänst och/eller dess anslutning kan behålla tillståndet per klient om den auktoriseringsnivå som klienten arbetar på, vad de föredrar CultureInfo är osv. Som vi kommer att se härnäst möjliggör det också mer intressanta tjänster som accepterar argument som är specifika för den här begäran.
Viktig
En tjänstfabrik som avviker från den här riktlinjen och returnerar en delad tjänstinstans i stället för en ny till varje klient bör aldrig ha sin tjänst implementerad IDisposableeftersom den första klienten som tar bort proxyn leder till att den delade tjänstinstansen tas bort innan andra klienter används.
I det mer avancerade fallet där CalculatorService
konstruktorn kräver ett delat tillståndsobjekt och en IServiceBrokerkan vi erbjuda fabriken så här:
var state = new CalculatorService.State();
container.Proffer(
CalculatorService,
(moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(new CalculatorService(state, serviceBroker)));
Den state
lokala variabeln är utanför tjänstfabriken och skapas därför bara en gång och delas över alla instansierade tjänster.
Ännu mer avancerat, om tjänsten krävde åtkomst till ServiceActivationOptions (till exempel för att anropa metoder på klientens RPC-målobjekt) som också kunde skickas in:
var state = new CalculatorService.State();
container.Proffer(CalculatorService, (moniker, options, serviceBroker, cancellationToken) =>
new ValueTask<object?>(new CalculatorService(state, serviceBroker, options)));
I det här fallet kan tjänstkonstruktorn se ut så här, förutsatt att ServiceJsonRpcDescriptor skapades med typeof(IClientCallbackInterface)
som ett av konstruktorns argument:
internal class Calculator(State state, IServiceBroker serviceBroker, ServiceActivationOptions options)
{
this.state = state;
this.serviceBroker = serviceBroker;
this.options = options;
this.clientCallback = (IClientCallbackInterface)options.ClientRpcTarget;
}
Det här clientCallback
fältet kan nu anropas när tjänsten vill anropa klienten tills anslutningen tas bort.
Delegaten BrokeredServiceFactory tar en ServiceMoniker som parameter om tjänstfabrik är en delad metod som skapar flera tjänster eller distinkta versioner av tjänsten utifrån monikern. Den här monikern kommer från klienten och innehåller den version av tjänsten som de förväntar sig. Genom att vidarebefordra den här monikern till tjänstkonstruktorn kan tjänsten emulera det udda beteendet för vissa tjänstversioner så att det matchar vad klienten kan förvänta sig.
Undvik att använda delegeringen AuthorizingBrokeredServiceFactory med metoden IBrokeredServiceContainer.Proffer om du inte använder IAuthorizationService i din mappade tjänstklass. Den här IAuthorizationServicemåste tas bort med din förmedlade tjänstklass för att undvika en minnesläcka.
Stöd för flera versioner av tjänsten
När du ökar versionen på din ServiceMonikermåste du erbjuda varje version av din förmedlade tjänst som du avser att svara på klientbegäranden med. Detta görs genom att anropa metoden IBrokeredServiceContainer.Proffer med varje ServiceRpcDescriptor som du fortfarande stöder.
Att erbjuda din tjänst med en null
-version fungerar som en "catch all" som matchar alla klientbegäranden som en exakt version matchar med en registrerad tjänst inte finns.
Du kan till exempel erbjuda din 1.0- och 1.1-tjänst med specifika versioner och även registrera din tjänst med en null
version.
I sådana fall anropar klienter som begär din tjänst med 1.0 eller 1.1 tjänstfabriken som du erbjöd för dessa exakta versioner, medan en klient som begär version 8.0 leder till att din tjänstfabrik med null-version anropas.
Eftersom den begärda versionen av klienten tillhandahålls till tjänstfabriken kan fabriken sedan fatta ett beslut om hur tjänsten ska konfigureras för den här klienten eller om null
ska returneras för att ange en version som inte stöds.
En begäran från en klient för en tjänst med en null
version endast matchar en tjänst registrerad och erbjuden med en version null
.
Tänk dig ett fall där du har publicerat många versioner av tjänsten, varav flera är bakåtkompatibla och därmed kan dela en tjänstimplementering. Vi kan använda alternativet catch-all för att undvika att upprepade gånger behöva erbjuda varje enskild version på följande sätt:
const string ServiceName = "YourCompany.Extension.Calculator";
ServiceRpcDescriptor CreateDescriptor(Version? version) =>
new ServiceJsonRpcDescriptor(
new ServiceMoniker(ServiceName, version),
ServiceJsonRpcDescriptor.Formatters.MessagePack,
ServiceJsonRpcDescriptor.MessageDelimiters.BigEndianInt32LengthHeader);
IBrokeredServiceContainer container = await AsyncServiceProvider.GlobalProvider.GetServiceAsync<SVsBrokeredServiceContainer, IBrokeredServiceContainer>();
container.Proffer(
CreateDescriptor(new Version(2, 0)),
(moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(new CalculatorServiceV2()));
container.Proffer(
CreateDescriptor(null), // proffer a catch-all
(moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(moniker.Version switch {
{ Major: 1 } => new CalculatorService(), // match any v1.x request with our v1 service.
null => null, // We don't support clients that do not specify a version.
_ => null, // The client requested some other version we don't recognize.
}));
Registrera tjänsten
Att tillhandahålla en mäklad tjänst till containern för den globala mäklade tjänsten kommer att kasta ett fel om tjänsten inte har registrerats först. Registrering ger containern ett sätt att veta i förväg vilka asynkrona tjänster som kan vara tillgängliga och vilket VS-paket som ska läsas in när de begärs för att köra profferingkoden. Detta gör att Visual Studio kan starta snabbt, utan att läsa in alla tillägg i förväg, men fortfarande kunna läsa in det tillägg som krävs när en klient av dess förmedlade tjänst begär det.
Registreringen görs genom att tillämpa ProvideBrokeredServiceAttribute på din AsyncPackage-härledda klass. Det här är det enda stället där ServiceAudience kan anges.
[ProvideBrokeredService("YourCompany.Extension.Calculator", "1.0", Audience = ServiceAudience.Local)]
Standard Audience är ServiceAudience.Process, som endast exponerar din förmedlade tjänst för annan kod inom samma process. Genom att ange ServiceAudience.Localväljer du att exponera din förmedlade tjänst för andra processer som tillhör samma Visual Studio-session.
Om din asynkrona tjänst måste exponeras för Live Share-gäster måste Audience inkludera ServiceAudience.LiveShareGuest och egenskapen ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients inställd på true
.
Inställningen av dessa flaggor kan medföra allvarliga säkerhetsproblem och bör inte göras utan att först följa riktlinjerna i Hur du skyddar en förmedlad tjänst.
När du ökar versionen på din ServiceMonikermåste du registrera varje version av din förmedlade tjänst som du avser att använda för att svara på klientbegäranden. Genom att stödja mer än den senaste versionen av din asynkrona tjänst hjälper du till att upprätthålla bakåtkompatibilitet för klienter i din äldre asynkrona tjänstversion, vilket kan vara särskilt användbart när du överväger Live Share-scenariot där varje version av Visual Studio som delar sessionen kan vara en annan version.
Att registrera din tjänst med en null
-version fungerar som en "catch all", vilket matchar alla klientbegäranden där en exakt version av en registrerad tjänst inte finns.
Du kan till exempel registrera din 1.0- och 2.0-tjänst med specifika versioner och även registrera din tjänst med en null
version.
Använda MEF för att erbjuda och registrera din tjänst
Detta kräver Visual Studio 2022 Update 2 eller senare.
En förhandlad tjänst kan exporteras via MEF i stället för att använda ett Visual Studio Package som beskrivs i de två föregående avsnitten. Detta har avvägningar att överväga:
Avvägning | Paketproffer | MEF-export |
---|---|---|
Tillgänglighet | ✅ Brokered-tjänsten är tillgänglig omedelbart vid VS-start. | ⚠️ Förmedlad tjänst kan försenas i tillgänglighet tills MEF har initierats i processen. Detta är vanligtvis snabbt, men kan ta flera sekunder när MEF-cachen är inaktuell. |
Plattformsoberoende beredskap | ️ ⚠Visual Studio för Windows måste specifik kod skapas. | ✅ Den förmedlade tjänsten i din applikation kan laddas in i Visual Studio för Windows samt Visual Studio för Mac. |
Så här exporterar du din förmedlade tjänst via MEF i stället för att använda VS-paket:
- Bekräfta att du inte har någon kod relaterad till de två senaste avsnitten. I synnerhet bör du inte ha någon kod som anropar till IBrokeredServiceContainer.Proffer och bör inte tillämpa ProvideBrokeredServiceAttribute på ditt paket (om någon).
- Implementera
IExportedBrokeredService
-gränssnittet i din förmedlade tjänstklass. - Undvik huvudtrådsberoenden i konstruktorn eller vid import av egenskapsinställare. Använd metoden
IExportedBrokeredService.InitializeAsync
för att initiera din förmedlade tjänst, där beroende av huvudtråden är tillåtna. - Använd
ExportBrokeredServiceAttribute
på din förmedlade tjänstklass och ange information om din tjänstmoniker, målgrupp och all annan registreringsrelaterad information som krävs. - Om din klass kräver bortskaffande implementerar du IDisposable i stället för IAsyncDisposable eftersom MEF äger tjänstens livslängd och endast stöder synkront bortskaffande.
- Se till att din
source.extension.vsixmanifest
-fil listar projektet som innehåller din förmedlade tjänst som en MEF-sammansättning.
Som en MEF-del kan din förmedlade tjänst importera någon annan MEF-del i standardomfånget.
När du gör det bör du använda System.ComponentModel.Composition.ImportAttribute i stället för att System.Composition.ImportAttribute.
Det beror på att ExportBrokeredServiceAttribute
härleds från System.ComponentModel.Composition.ExportAttribute och att använda samma MEF-namnområde i en typ krävs.
En förmedlad tjänst är unik i att kunna importera några speciella exporter:
- IServiceBroker, som ska användas för att förvärva andra förmedlade tjänster.
- ServiceMoniker, vilket kan vara användbart när du exporterar flera versioner av din medlingstjänst och behöver identifiera vilken version klienten begärde.
- ServiceActivationOptions, vilket kan vara användbart när du kräver att dina klienter tillhandahåller särskilda parametrar eller ett mål för återanrop till klienten.
- AuthorizationServiceClient, vilket kan vara användbart när du behöver utföra säkerhetskontroller enligt beskrivningen i Så här skyddar du en förmedlad tjänst. Det här objektet behöver inte tas bort av din klass, eftersom det tas bort automatiskt när din förmedlade tjänst tas bort.
Din mäklad tjänst får inte använda MEF:s ImportAttribute för att förvärva andra mäklade tjänster.
I stället kan den [Import]
IServiceBroker och fråga efter förmedlade tjänster på traditionellt sätt.
Läs mer i Så här använder du en förmedlad tjänst.
Här är ett exempel:
using System;
using System.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.ServiceHub.Framework;
using Microsoft.ServiceHub.Framework.Services;
using Microsoft.VisualStudio.Shell.ServiceBroker;
[ExportBrokeredService("Calc", "1.0")]
internal class MefBrokeredService : IExportedBrokeredService, ICalculator
{
internal static ServiceRpcDescriptor SharedDescriptor { get; } = new ServiceJsonRpcDescriptor(
new ServiceMoniker("Calc", new Version("1.0")),
clientInterface: null,
ServiceJsonRpcDescriptor.Formatters.MessagePack,
ServiceJsonRpcDescriptor.MessageDelimiters.BigEndianInt32LengthHeader,
new MultiplexingStream.Options { ProtocolMajorVersion = 3 });
// IExportedBrokeredService
public ServiceRpcDescriptor Descriptor => SharedDescriptor;
[Import]
IServiceBroker ServiceBroker { get; set; } = null!;
[Import]
ServiceMoniker ServiceMoniker { get; set; } = null!;
[Import]
ServiceActivationOptions Options { get; set; }
// IExportedBrokeredService
public Task InitializeAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public ValueTask<int> AddAsync(int a, int b, CancellationToken cancellationToken = default)
{
return new(a + b);
}
public ValueTask<int> SubtractAsync(int a, int b, CancellationToken cancellationToken = default)
{
return new(a - b);
}
}
Exportera flera versioner av din förmedlade tjänst
Den ExportBrokeredServiceAttribute
kan tillämpas på din förmedlade tjänst flera gånger för att erbjuda flera versioner av din förmedlade tjänst.
Implementeringen av egenskapen IExportedBrokeredService.Descriptor
ska returnera en beskrivning med en moniker som matchar den som klienten begärde.
Tänk på det här exemplet, där kalkylatortjänsten exporterade 1.0 med UTF8-formatering, och sedan lägger till en 1.1-export för att kunna dra nytta av prestandavinsterna med att använda MessagePack-formatering.
[ExportBrokeredService("Calc", "1.0")]
[ExportBrokeredService("Calc", "1.1")]
internal class MefBrokeredService : IExportedBrokeredService, ICalculator
{
internal static ServiceRpcDescriptor SharedDescriptor1_0 { get; } = new ServiceJsonRpcDescriptor(
new ServiceMoniker("Calc", new Version("1.0")),
clientInterface: null,
ServiceJsonRpcDescriptor.Formatters.UTF8,
ServiceJsonRpcDescriptor.MessageDelimiters.HttpLikeHeaders,
new MultiplexingStream.Options { ProtocolMajorVersion = 3 });
internal static ServiceRpcDescriptor SharedDescriptor1_1 { get; } = new ServiceJsonRpcDescriptor(
new ServiceMoniker("Calc", new Version("1.1")),
clientInterface: null,
ServiceJsonRpcDescriptor.Formatters.MessagePack,
ServiceJsonRpcDescriptor.MessageDelimiters.BigEndianInt32LengthHeader,
new MultiplexingStream.Options { ProtocolMajorVersion = 3 });
// IExportedBrokeredService
public ServiceRpcDescriptor Descriptor =>
this.ServiceMoniker.Version == SharedDescriptor1_0.Moniker.Version ? SharedDescriptor1_0 :
this.ServiceMoniker.Version == SharedDescriptor1_1.Moniker.Version ? SharedDescriptor1_1 :
throw new NotSupportedException();
[Import]
ServiceMoniker ServiceMoniker { get; set; } = null!;
}
Från och med Visual Studio 2022 Update 12 (17.12) kan en null
version av tjänsten exporteras för att matcha alla klientbegäranden för tjänsten oavsett version, inklusive en begäran med en null
version.
En sådan tjänst kan returnera null
från egenskapen Descriptor
för att avvisa en klientbegäran när den inte erbjuder någon implementering av den version som klienten begärde.
Avvisa en tjänstbegäran
En förmedlad tjänst kan avvisa en klients aktiveringsbegäran genom att kasta ett undantag från metoden InitializeAsync. Kastande orsakar att en ServiceActivationFailedException skickas tillbaka till klienten.