Delen via


Een brokered service bieden

Een brokered service bestaat uit de volgende elementen:

Elk van de items in de voorgaande lijst wordt uitgebreid beschreven in de volgende secties.

Met alle code in dit artikel wordt het activeren van de null-verwijzingstypen van C# functie ten zeerste aanbevolen.

De service-interface

De service-interface kan een standaard .NET-interface zijn (vaak geschreven in C#), maar moet voldoen aan de richtlijnen die zijn ingesteld door het ServiceRpcDescriptor-afgeleide type dat uw service gebruikt om ervoor te zorgen dat de interface kan worden gebruikt via RPC wanneer de client en service in verschillende processen worden uitgevoerd. Deze beperkingen omvatten doorgaans dat eigenschappen en indexeerfuncties niet zijn toegestaan en de meeste of alle methoden retourneren Task of een ander asynchroon compatibel retourtype.

De ServiceJsonRpcDescriptor is het aanbevolen afgeleide type voor bemiddelde diensten. Deze klasse maakt gebruik van de StreamJsonRpc-bibliotheek wanneer de client en service RPC nodig hebben om te communiceren. StreamJsonRpc past bepaalde beperkingen toe op de serviceinterface, zoals hier beschreven.

De interface kan worden afgeleid van IDisposable, System.IAsyncDisposableof zelfs Microsoft.VisualStudio.Threading.IAsyncDisposable, maar dit is niet vereist door het systeem. De gegenereerde clientproxy's implementeren IDisposable in beide gevallen.

Een eenvoudige rekenmachineservice-interface kan als volgt worden gedeclareerd:

public interface ICalculator
{
    ValueTask<double> AddAsync(double a, double b, CancellationToken cancellationToken);
    ValueTask<double> SubtractAsync(double a, double b, CancellationToken cancellationToken);
}

Hoewel de implementatie van de methoden op deze interface mogelijk geen asynchrone methode rechtvaardigt, gebruiken we altijd asynchrone methodehandtekeningen op deze interface omdat deze interface wordt gebruikt om de clientproxy te genereren die deze service op afstand kan aanroepen, wat zeker een handtekening voor asynchrone methoden rechtvaardigt.

Een interface kan gebeurtenissen declareren die kunnen worden gebruikt om hun clients op de hoogte te stellen van gebeurtenissen die plaatsvinden bij de service.

Buiten gebeurtenissen of het ontwerppatroon van de waarnemer kan een brokered service die 'terugbellen' naar de client moet worden verzonden, een tweede interface definiëren die fungeert als het contract dat een client moet implementeren en leveren via de eigenschap ServiceActivationOptions.ClientRpcTarget bij het aanvragen van de service. Een dergelijke interface moet voldoen aan dezelfde ontwerppatronen en -beperkingen als de brokered service-interface, maar met extra beperkingen voor versiebeheer.

Bekijk Best Practices voor het ontwerpen van een Brokered Service voor tips over het ontwerpen van een presterende, toekomstbestendige RPC-interface.

Het kan handig zijn om deze interface te declareren in een afzonderlijke assembly van de assembly waarmee de service wordt geïmplementeerd, zodat de clients naar de interface kunnen verwijzen zonder dat de service meer details van de implementatie moet weergeven. Het kan ook handig zijn om de interface-assembly als een NuGet-pakket te verzenden voor andere extensies waarnaar kan worden verwezen, terwijl u uw eigen extensie reserveert om de service-implementatie te verzenden.

Overweeg de assembly te richten die uw service-interface declareert voor netstandard2.0 om ervoor te zorgen dat uw service eenvoudig kan worden aangeroepen vanuit een .NET-proces, ongeacht of .NET Framework, .NET Core, .NET 5 of hoger wordt uitgevoerd.

Testen

Geautomatiseerde tests moeten naast uw service interface worden geschreven om de RPC-gereedheid van de interface te controleren.

De tests moeten controleren of alle gegevens die via de interface worden doorgegeven, serialiseerbaar zijn.

Mogelijk vindt u de BrokeredServiceContractTestBase<TInterface,TServiceMock>-klasse uit de Microsoft.VisualStudio.Sdk.TestFramework.Xunit pakket nuttig om uw interfacetestklasse af te leiden. Deze klasse bevat enkele eenvoudige conventietests voor uw interface, methoden om te helpen bij veelvoorkomende asserties, zoals het testen van gebeurtenissen en meer.

Methoden

Bevestig dat elk argument en de retourwaarde volledig zijn geserialiseerd. Als u de bovenstaande testbasisklasse gebruikt, ziet uw code er mogelijk als volgt uit:

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);
    }
}

Overweeg overbelastingsresolutie te testen als u meerdere methoden met dezelfde naam declareert. U kunt een internal veld toevoegen aan uw mockservice voor elke methode die argumenten voor die methode opslaat, zodat de testmethode de methode kan aanroepen en vervolgens controleert of de juiste methode is aangeroepen met de juiste argumenten.

Gebeurtenissen

Alle gebeurtenissen die zijn gedeclareerd op uw interface, moeten ook worden getest op RPC-gereedheid. Gebeurtenissen die worden gegenereerd vanuit een brokered service, niet een testfout veroorzaken als ze mislukken tijdens RPC-serialisatie omdat gebeurtenissen 'brand en vergeten' zijn.

Als u de hierboven genoemde testbasisklasse gebruikt, is dit gedrag al ingebouwd in een aantal helpermethoden en kan dit er als volgt uitzien (met ongewijzigde onderdelen die zijn weggelaten om kort te zijn):

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));
    }
}

De service implementeren

De serviceklasse moet de RPC-interface implementeren die in de vorige stap is gedeclareerd. Een service kan IDisposable of andere interfaces implementeren buiten de interface die wordt gebruikt voor RPC. De proxy die op de client wordt gegenereerd, implementeert alleen de serviceinterface, IDisposableen mogelijk enkele andere select-interfaces ter ondersteuning van het systeem, zodat een cast naar andere interfaces die door de service zijn geïmplementeerd, mislukt op de client.

Bekijk het bovenstaande rekenmachinevoorbeeld, dat we hier implementeren:

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);
    }
}

Omdat de methodeteksten zelf niet asynchroon hoeven te zijn, verpakken we de retourwaarde expliciet in een samengesteld ValueTask<TResult> retourtype om te voldoen aan de service-interface.

Het waarneembare ontwerppatroon implementeren

Als u een waarnemersabonnement op uw service-interface aanbiedt, kan dit er als volgt uitzien:

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

Het argument IObserver<T> moet doorgaans de levensduur van deze methodeaanroep overleven, zodat de client updates kan blijven ontvangen nadat de methodeaanroep is voltooid totdat de client de geretourneerde IDisposable waarde heeft verwijderd. Om dit mogelijk te maken, kan uw dienstklasse een verzameling IObserver<T>-abonnementen bevatten die bij iedere wijziging in uw status worden opgesomd om alle abonnees bij te werken. Zorg ervoor dat de opsomming van uw collectie thread-safe is ten opzichte van elkaar en vooral met de mutaties in die verzameling die kunnen plaatsvinden via extra abonnementen of opzeggingen van die abonnementen.

Zorg ervoor dat alle updates die via OnNext worden geplaatst, de volgorde behouden waarin statuswijzigingen aan uw service zijn toegevoegd.

Alle abonnementen moeten uiteindelijk worden beëindigd met een aanroep van OnCompleted of OnError om resourcelekken op de client- en RPC-systemen te voorkomen. Dit geldt ook voor de beëindiging van diensten waarbij alle resterende abonnementen expliciet moeten worden afgerond.

Meer informatie over het waarnemersontwerppatroon, het implementeren van een waarneembare gegevensprovider en met name met RPC in gedachten.

Wegwerpservices

Uw serviceklasse hoeft niet wegwerpbaar te zijn, maar services die worden verwijderd wanneer de client de proxy naar uw service verwijdert of de verbinding tussen de client en de service is verbroken. Wegwerpinterfaces worden in deze volgorde getest: System.IAsyncDisposable, Microsoft.VisualStudio.Threading.IAsyncDisposable, IDisposable. Alleen de eerste interface uit deze lijst die door uw serviceklasse wordt geïmplementeerd, wordt gebruikt om de service te verwijderen.

Houd rekening met de veiligheid van threads bij het overwegen van de afhandeling. Uw Dispose methode kan worden aangeroepen op een thread terwijl andere code in uw service wordt uitgevoerd (bijvoorbeeld als er een verbinding wordt verbroken).

Uitzonderingen opwerpen

Bij het genereren van uitzonderingen kunt u overwegen om LocalRpcException met een specifieke ErrorCode- te genereren om de foutcode te beheren die door de client in de RemoteInvocationExceptionis ontvangen. Door clients een foutcode te leveren, kunnen ze vertakken op basis van de aard van de fout, beter dan het parseren van uitzonderingsberichten of -typen.

Volgens de JSON-RPC specificatie moeten foutcodes groter zijn dan -32000, inclusief positieve getallen.

Andere bemiddelde diensten gebruiken

Wanneer een brokered service zelf toegang nodig heeft tot een andere brokered service, raden we aan om gebruik te maken van de IServiceBroker die aan de servicefactory wordt geleverd, vooral wanneer bij de brokered serviceregistratie de AllowTransitiveGuestClients-vlag wordt ingesteld.

Om aan deze richtlijn te voldoen als onze calculatorservice andere brokered services nodig had om het gedrag te implementeren, zouden we de constructor wijzigen om een IServiceBrokerte accepteren:

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;
    }

    // ...
}

Meer informatie over het beveiligen van een bemiddelde dienst en het gebruiken van bemiddelde diensten.

Toestandsafhankelijke services

Toestand per klant

Er wordt een nieuw exemplaar van deze klasse gemaakt voor elke client die de service aanvraagt. In een veld in de bovenstaande Calculator klasse wordt een waarde opgeslagen die mogelijk uniek is voor elke client. Stel dat we een teller toevoegen die wordt verhoogd telkens wanneer een bewerking wordt uitgevoerd:

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);
    }
}

Uw brokered service moet zodanig worden geschreven dat het draadveilige methoden volgt. Wanneer u de aanbevolen ServiceJsonRpcDescriptorgebruikt, kunnen externe verbindingen met clients gelijktijdig uitvoering geven aan de methoden van uw service, zoals beschreven in dit document . Wanneer de client een proces en AppDomain deelt met de service, kan de client uw service gelijktijdig aanroepen vanuit meerdere threads. Een thread-veilige implementatie van het bovenstaande voorbeeld kan Interlocked.Increment(Int32) gebruiken om het operationCounter veld te verhogen.

Gedeelde status

Als er een status is die uw service moet delen voor alle clients, moet deze status worden gedefinieerd in een afzonderlijke klasse die wordt geïnstantieerd door uw VS Package en als argument wordt doorgegeven aan de constructor van uw service.

Stel dat we willen dat de hierboven gedefinieerde operationCounter alle bewerkingen voor alle clients van de service telt. We moeten het veld naar deze nieuwe statusklasse tillen:

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);
    }
}

We hebben nu een elegante, testbare manier om de gedeelde status te beheren over meerdere exemplaren van onze Calculator-service. Later bij het schrijven van de code om de service te profferen, zien we hoe deze State klasse eenmaal wordt gemaakt en wordt gedeeld met elk exemplaar van de Calculator-service.

Het is vooral belangrijk om thread-safe te zijn bij het werken met gedeelde status, omdat er geen aanname kan worden gedaan over het plannen van aanroepen door meerdere clients, op een manier dat ze nooit gelijktijdig plaatsvinden.

Als uw gedeelde statusklasse toegang nodig heeft tot andere brokered services, moet deze de globale servicebroker gebruiken in plaats van een van de contextuele services die zijn toegewezen aan een afzonderlijk exemplaar van uw brokered-service. Het gebruik van de globale servicebroker binnen een brokered-service heeft gevolgen voor beveiliging wanneer de vlag ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients is ingesteld.

Beveiligingsproblemen

Beveiliging is een overweging voor uw brokered-service als deze is geregistreerd bij de vlag ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients, waarmee het mogelijk wordt gemaakt voor toegang door andere gebruikers op andere computers die deelnemen aan een gedeelde livesharesessie.

Bekijk Hoe je een bemiddelde dienst beveiligt en neem de nodige beveiligingsmaatregelen vóór je de AllowTransitiveGuestClients vlag instelt.

De dienstbijnaam

Een brokered service moet een serialiseerbare naam en een optionele versie hebben waarmee een client de service kan aanvragen. Een ServiceMoniker is een handige wrapper voor deze twee stukjes informatie.

Een service moniker is vergelijkbaar met de assembly-gekwalificeerde volledige naam van een CLR-type (Common Language Runtime). Het moet wereldwijd uniek zijn en moet daarom uw bedrijfsnaam en mogelijk uw extensienaam als voorvoegsels voor de servicenaam zelf bevatten.

Het kan handig zijn om deze moniker in een static readonly veld te definiëren voor gebruik elders:

public static readonly ServiceMoniker Moniker = new ServiceMoniker("YourCompany.Extension.Calculator", new Version("1.0"));

Hoewel de meeste toepassingen van uw service uw moniker mogelijk niet rechtstreeks gebruiken, vereist een client die via pijpen communiceert in plaats van een proxy de moniker.

Hoewel een versie optioneel is op een moniker, wordt het opgeven van een versie aanbevolen omdat het serviceauteurs meer opties biedt voor het onderhouden van compatibiliteit met clients bij gedragswijzigingen.

De beschrijving van de service

De servicedescriptor combineert de servicemoniker met het gedrag dat is vereist voor het uitvoeren van een RPC-verbinding en het maken van een lokale of externe proxy. De descriptor is verantwoordelijk om uw RPC-interface effectief te converteren naar een wire-protocol. Deze servicedescriptor is een exemplaar van een ServiceRpcDescriptor-afgeleid type. De descriptor moet beschikbaar worden gesteld aan alle clients die een proxy gebruiken voor toegang tot deze service. Voor het profferen van de service is ook deze descriptor vereist.

Visual Studio definieert een dergelijk afgeleid type en beveelt het gebruik ervan aan voor alle services: ServiceJsonRpcDescriptor. Deze descriptor maakt gebruik van StreamJsonRpc voor de RPC-verbindingen en maakt een lokale proxy met hoge prestaties voor lokale services die enkele van het externe gedrag emuleren, zoals wrapping-uitzonderingen die door de service in RemoteInvocationExceptionworden gegenereerd.

De ServiceJsonRpcDescriptor ondersteunt het configureren van de JsonRpc-klasse voor JSON- of MessagePack-codering van het JSON-RPC-protocol. We raden MessagePack-codering aan omdat deze compacter is en 10x beter presteert.

We kunnen als volgt een descriptor definiëren voor onze rekenmachineservice:

/// <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);

Zoals u hierboven kunt zien, is een keuze uit formatter en scheidingsteken beschikbaar. Aangezien niet alle combinaties geldig zijn, raden we een van deze combinaties aan:

ServiceJsonRpcDescriptor.Formatters ServiceJsonRpcDescriptor.MessageDelimiters Het beste voor
MessagePack BigEndianInt32LengthHeader Hoge prestaties
UTF8 (JSON) HttpLikeHeaders Interoperabiliteit met andere JSON-RPC systemen

Door het MultiplexingStream.Options-object op te geven als de laatste parameter, is de RPC-verbinding die wordt gedeeld tussen de client en de service slechts één kanaal op een MultiplexingStream-, die wordt gedeeld met de JSON-RPC-verbinding met efficiënte overdracht van grote binaire gegevens via JSON-RPC-mogelijk maakt.

De ExceptionProcessing.ISerializable-strategie zorgt ervoor dat uitzonderingen die door uw service worden gegenereerd, worden geserialiseerd en bewaard als de Exception.InnerException bij de RemoteInvocationException die op de client wordt gegooid. Zonder deze instelling is minder gedetailleerde uitzonderingsinformatie beschikbaar op de client.

Tip: geef uw descriptor weer als ServiceRpcDescriptor in plaats van een afgeleid type dat u als implementatiedetails gebruikt. Dit biedt u meer flexibiliteit om later implementatiedetails te wijzigen zonder wijzigingen die fouten veroorzaken in de API.

Neem een verwijzing naar uw service-interface op in de xml-documentopmerking over uw descriptor, zodat gebruikers uw service gemakkelijker kunnen gebruiken. Raadpleeg ook de interface die uw service accepteert als het RPC-doel van de client, indien van toepassing.

Sommige geavanceerdere services accepteren of vereisen mogelijk ook een RPC-doelobject van de client die voldoet aan een bepaalde interface. Gebruik in dat geval een ServiceJsonRpcDescriptor constructor met een Type clientInterface parameter om de interface op te geven waarvan de client een exemplaar moet opgeven.

Versiebeheer van de descriptor

Na verloop van tijd wilt u mogelijk de versie van uw service verhogen. In dat geval moet u een descriptor definiëren voor elke versie die u wilt ondersteunen, met behulp van een unieke versiespecifieke ServiceMoniker voor elke versie. Het ondersteunen van meerdere versies tegelijk kan goed zijn voor achterwaartse compatibiliteit en kan meestal worden uitgevoerd met slechts één RPC-interface.

Visual Studio volgt dit patroon met de VisualStudioServices-klasse door de oorspronkelijke ServiceRpcDescriptor te definiëren als een virtual-eigenschap binnen de geneste klasse die de eerste release vertegenwoordigt die die brokered-service heeft toegevoegd. Wanneer we het wire-protocol moeten wijzigen of functionaliteit van de service moeten toevoegen/wijzigen, declareert Visual Studio een eigenschap override in een latere geneste klasse die een nieuwe ServiceRpcDescriptorretourneert.

Voor een service die is gedefinieerd en geproffereerd door een Visual Studio-extensie, kan het volstaan om een andere beschrijvingseigenschap naast het origineel te declareren. Stel dat uw 1.0-service de UTF8-indeling (JSON) heeft gebruikt en u realiseert zich dat het overschakelen naar MessagePack een aanzienlijk prestatievoordeel oplevert. Omdat het wijzigen van de formatter een wijziging is die het wire-protocol doorbreekt, moet zowel het brokered serviceversienummer als een tweede descriptor worden verhoogd. De twee beschrijvingen zouden er zo uit kunnen zien:

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 });

Ondanks dat we twee descriptors definiëren (en later moeten we twee services aanbieden en registreren), kunnen we dit doen met slechts één service-interface en implementatie, waardoor de overhead voor het ondersteunen van meerdere serviceversies behoorlijk laag blijft.

De service profferen

Uw bemiddelde dienst moet worden gemaakt wanneer er een aanvraag binnenkomt, wat wordt geregeld via een stap die het aanbieden van de dienst wordt genoemd.

De servicefactory

Gebruik GlobalProvider.GetServiceAsync om de SVsBrokeredServiceContaineraan te vragen. Roep vervolgens IBrokeredServiceContainer.Proffer aan voor die container om uw service te profferen.

In het onderstaande voorbeeld wordt een service geproffereerd met behulp van het CalculatorService veld dat eerder is gedeclareerd, die is ingesteld op een exemplaar van een ServiceRpcDescriptor. We geven het door aan onze service factory, een BrokeredServiceFactory gedelegeerde.

IBrokeredServiceContainer container = await AsyncServiceProvider.GlobalProvider.GetServiceAsync<SVsBrokeredServiceContainer, IBrokeredServiceContainer>();
container.Proffer(
    CalculatorService,
    (moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(new CalculatorService()));

Een brokered service wordt doorgaans eenmaal per client geïnstantieerd. Dit is een afwijking van andere VS-services (Visual Studio), die doorgaans eenmaal worden geïnstantieerd en gedeeld door alle clients. Door één instantie van de dienst per client te maken, kan de beveiliging worden verbeterd, omdat elke dienst en/of de bijbehorende verbinding de status per client kan bijhouden over het autorisatieniveau waarop de client werkt, wat hun voorkeursinstelling voor CultureInfo is, enzovoorts. Zoals we hierna zullen zien, biedt het ook meer interessante diensten die argumenten accepteren die specifiek zijn voor dit verzoek.

Belangrijk

Een servicefactory die afwijkt van deze richtlijn en een gedeeld service-exemplaar retourneert in plaats van een nieuw exemplaar voor elke client, moet nooit zijn service IDisposableimplementeren, omdat de eerste client die de proxy moet verwijderen, leidt tot verwijdering van het gedeelde service-exemplaar voordat andere clients deze gebruiken.

In het meer geavanceerde geval waarin de CalculatorService constructor een gedeeld statusobject en een IServiceBrokervereist, kunnen we de fabriek als volgt aanbieden:

var state = new CalculatorService.State();
container.Proffer(
    CalculatorService,
    (moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(new CalculatorService(state, serviceBroker)));

De state lokale variabele is buiten de servicefabriek en wordt dus slechts één keer gemaakt en wordt gedeeld met alle geïnstantieerde services.

Nog geavanceerder is het, als de service toegang tot de ServiceActivationOptions nodig had (bijvoorbeeld om methoden aan te roepen op het RPC-doelobject van de client) die eveneens kan worden doorgegeven.

var state = new CalculatorService.State();
container.Proffer(CalculatorService, (moniker, options, serviceBroker, cancellationToken) =>
    new ValueTask<object?>(new CalculatorService(state, serviceBroker, options)));

In dit geval kan de serviceconstructor er als volgt uitzien, ervan uitgaande dat de ServiceJsonRpcDescriptor zijn gemaakt met typeof(IClientCallbackInterface) als een van de constructorargumenten:

internal class Calculator(State state, IServiceBroker serviceBroker, ServiceActivationOptions options)
{
    this.state = state;
    this.serviceBroker = serviceBroker;
    this.options = options;
    this.clientCallback = (IClientCallbackInterface)options.ClientRpcTarget;
}

Dit clientCallback veld kan nu worden aangeroepen wanneer de service de client wil aanroepen, totdat de verbinding wordt verwijderd.

De BrokeredServiceFactory gedelegeerde gebruikt een ServiceMoniker als parameter in het geval de servicefactory een gedeelde methode is waarmee meerdere services of afzonderlijke versies van de service worden gemaakt op basis van de moniker. Deze moniker is afkomstig van de client en bevat de versie van de service die ze verwachten. Door deze moniker door te sturen naar de serviceconstructor, kan de service het vreemde gedrag van bepaalde serviceversies emuleren zodat deze overeenkomt met wat de client kan verwachten.

Vermijd het gebruik van de AuthorizingBrokeredServiceFactory gemachtigde met de IBrokeredServiceContainer.Proffer methode, tenzij u de IAuthorizationService in uw brokered serviceklasse gebruikt. Deze IAuthorizationServicemoet worden verwijderd met uw brokered serviceklasse om geheugenlekken te voorkomen.

Ondersteuning voor meerdere versies van uw service

Wanneer u de versie van uw ServiceMonikerverhoogt, moet u elke versie van uw gebrokereerde service aanbieden waarmee u wilt reageren op klantverzoeken. Dit wordt gedaan door de IBrokeredServiceContainer.Proffer methode aan te roepen met elke ServiceRpcDescriptor die u nog steeds ondersteunt.

Het aanbieden van uw service met een null-versie fungeert als een 'catch all' die overeenkomt met elke cliëntaanvraag waarbij er geen exacte versie-overeenkomst bestaat met een geregistreerde dienst. U kunt bijvoorbeeld uw 1.0- en 1.1-service profferen met specifieke versies en uw service registreren met een null versie. In dergelijke gevallen roept een client die uw service aanvraagt met 1.0 of 1.1 de door u aangeboden service-factory voor die exacte versies aan, terwijl een client die versie 8.0 aanvraagt, leidt tot het aanroepen van uw zonder versie aangeboden service-factory. Omdat de aangevraagde versie van de client aan de servicefactory wordt verstrekt, kan de factory vervolgens een beslissing nemen over het configureren van de service voor deze specifieke client of het retourneren van null om een niet-ondersteunde versie te ondertekenen.

Een clientaanvraag voor een service met een null versie alleen overeenkomsten op een service die is geregistreerd en met een null versie wordt geleverd.

Overweeg een geval waarin u veel versies van uw service hebt gepubliceerd, waarvan verschillende achterwaarts compatibel zijn en dus een service-implementatie kunnen delen. We kunnen de catch-all-optie gebruiken om te voorkomen dat elke afzonderlijke versie herhaaldelijk moet worden geprofferd als volgt:

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.
    }));

De service registreren

Het aanbieden van een bemiddelde dienst aan de wereldwijde container voor bemiddelde diensten zal een fout geven, tenzij de dienst eerst is geregistreerd. Registratie biedt de container de mogelijkheid om vooraf te weten welke bemiddelde diensten beschikbaar kunnen zijn en welk VS Package moet worden geladen wanneer deze worden aangevraagd, zodat de code kan worden uitgevoerd. Hierdoor kan Visual Studio snel opstarten, zonder alle extensies vooraf te laden, maar kan de vereiste extensie wel laden wanneer dit wordt aangevraagd door een client van de brokered-service.

Registratie kan worden uitgevoerd door de ProvideBrokeredServiceAttribute toe te passen op uw AsyncPackage-afgeleide klasse. Dit is de enige plaats waar de ServiceAudience kan worden ingesteld.

[ProvideBrokeredService("YourCompany.Extension.Calculator", "1.0", Audience = ServiceAudience.Local)]

De standaard Audience is ServiceAudience.Process, waarmee uw brokered service alleen beschikbaar wordt gemaakt voor andere code binnen hetzelfde proces. Door ServiceAudience.Localin te stellen, kiest u ervoor om uw brokered service beschikbaar te maken voor andere processen die behoren tot dezelfde Visual Studio-sessie.

Als uw brokered service-moet worden blootgesteld aan livesharegasten, moet de AudienceServiceAudience.LiveShareGuest bevatten en de eigenschap ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients ingesteld op true. Het instellen van deze vlaggen kan ernstige beveiligingsproblemen veroorzaken en mag niet worden uitgevoerd zonder eerst te voldoen aan de richtlijnen in How to Secure a Brokered Service.

Wanneer u de versie op uw ServiceMonikerverhoogt, moet u elke versie van uw bemiddelde dienst registreren waarvoor u wilt reageren op klantaanvragen. Door meer dan de meest recente versie van uw brokered-service te ondersteunen, kunt u achterwaartse compatibiliteit behouden voor clients van uw oudere brokered-serviceversie. Dit kan met name handig zijn bij het overwegen van het Live Share-scenario waarbij elke versie van Visual Studio die de sessie deelt, mogelijk een andere versie is.

Het registreren van uw service met een null-versie fungeert als een 'catch all' die overeenkomt met elke clientaanvraag waarvoor een exacte versie met een geregistreerde service niet bestaat. U kunt bijvoorbeeld uw 1.0- en 2.0-service registreren bij specifieke versies en ook uw service registreren met een null-versie.

MEF gebruiken om uw service te profferen en te registreren

Hiervoor is Visual Studio 2022 Update 2 of hoger vereist.

Een brokered service kan worden geëxporteerd via MEF in plaats van een Visual Studio-pakket te gebruiken, zoals beschreven in de vorige twee secties. Dit heeft compromissen om rekening mee te houden:

Afweging Pakket aanbod MEF-export
Beschikbaarheid ✅ Brokered service is direct beschikbaar bij het opstarten van Visual Studio. ️ ⚠Brokered service kan vertraagd zijn in beschikbaarheid totdat MEF in het proces is geïnitialiseerd. Dit is meestal snel, maar kan enkele seconden duren wanneer de MEF-cache verouderd is.
Platformoverschrijdende gereedheid ⚠Visual Studio-specifieke code voor Windows moet worden geschreven. ✅ De brokered-service in uw assembly kan worden geladen in Visual Studio voor Windows en Visual Studio voor Mac.

Uw brokered-service exporteren via MEF in plaats van VS-pakketten te gebruiken:

  1. Controleer of er geen code is gerelateerd aan de laatste twee secties. U mag met name geen code bevatten die IBrokeredServiceContainer.Proffer aanroept en moet u de ProvideBrokeredServiceAttribute niet op uw pakket toepassen (indien van toepassing).
  2. Implementeer de IExportedBrokeredService-interface in uw brokered-serviceklasse.
  3. Vermijd afhankelijkheden van de hoofdthread in uw constructor of het importeren van eigenschapssetters. Gebruik de IExportedBrokeredService.InitializeAsync-methode voor het initialiseren van uw brokeredservice, waarbij afhankelijkheden van de hoofdthread zijn toegestaan.
  4. Pas de ExportBrokeredServiceAttribute toe op uw brokered serviceklasse, waarbij u de informatie opgeeft over uw service moniker, doelgroep en andere vereiste registratiegegevens.
  5. Als uw klasse verwijdering vereist, implementeert u IDisposable in plaats van IAsyncDisposable omdat MEF eigenaar is van de levensduur van uw service en alleen synchrone verwijdering ondersteunt.
  6. Zorg ervoor dat uw source.extension.vsixmanifest-bestand het project met uw brokered service opneemt als een MEF-assembly.

Als mef-onderdeel kan uw brokered service- andere MEF-onderdelen importeren in het standaardbereik. Als u dit doet, moet u System.ComponentModel.Composition.ImportAttribute gebruiken in plaats van System.Composition.ImportAttribute. Dit komt doordat ExportBrokeredServiceAttribute is afgeleid van System.ComponentModel.Composition.ExportAttribute en dat het gebruik van dezelfde MEF-naamruimte binnen een type vereist is.

Een bemiddelde dienst is uniek in het vermogen om een paar speciale uitvoerproducten te importeren.

  • IServiceBroker, die moet worden gebruikt voor het verkrijgen van andere bemiddelde diensten.
  • ServiceMoniker, wat handig kan zijn wanneer u meerdere versies van uw brokered-service exporteert en moet detecteren welke versie de client heeft aangevraagd.
  • ServiceActivationOptions, wat handig kan zijn wanneer u wilt dat uw klanten speciale parameters of een callbackdoel voor de klant opgeven.
  • AuthorizationServiceClient, dat nuttig kan zijn wanneer u beveiligingscontroles moet uitvoeren zoals beschreven in Hoe u een bemiddelde dienstbeveiligt. Dit object hoeft niet te worden opgeruimd door uw klasse, omdat het automatisch wordt opgeruimd wanneer uw brokered service wordt opgeruimd.

Uw bemiddelde dienst mag niet MEF's ImportAttribute gebruiken om andere bemiddelde diensten aan te schaffen. In plaats daarvan kan het [Import]IServiceBroker op de traditionele manier en brokered services opvragen. Meer informatie vindt u in Hoe u een bemiddelde dienst verbruikt.

Hier volgt een voorbeeld:

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);
    }
}

Meerdere versies van uw brokered-service exporteren

De ExportBrokeredServiceAttribute kan meerdere keren worden toegepast op uw brokered-service om meerdere versies van uw brokered-service aan te bieden.

De implementatie van de eigenschap IExportedBrokeredService.Descriptor moet een descriptor retourneren met een moniker die overeenkomt met de waarde die de client heeft aangevraagd.

Bekijk dit voorbeeld, waarbij de rekenmachineservice 1.0 met UTF8-opmaak heeft geëxporteerd en later een 1.1-export toevoegt om te profiteren van de prestatiewinsten van het gebruik van MessagePack-opmaak.

[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!;
}

Vanaf Visual Studio 2022 Update 12 (17.12) kan een null versieservice worden geëxporteerd zodat deze overeenkomt met elke clientaanvraag voor de service, ongeacht de versie, inclusief een aanvraag met een null versie. Een dergelijke service kan null retourneren vanuit de eigenschap Descriptor om een clientaanvraag te weigeren wanneer er geen implementatie van de door de client aangevraagde versie wordt aangeboden.

Een serviceaanvraag weigeren

Een brokered service kan de activeringsaanvraag van een client weigeren door de methode InitializeAsync te genereren. Het werpen veroorzaakt dat een ServiceActivationFailedException teruggestuurd wordt naar de client.