Freigeben über


Bereitstellen eines vermittelten Dienstes

Ein vermittelter Dienst besteht aus den folgenden Elementen:

Die einzelnen Elemente in der vorherigen Liste werden in den folgenden Abschnitten ausführlich beschrieben.

Bei allen Codes in diesem Artikel wird die Aktivierung der Funktion Nullwerte zulassende Verweistypen von C# dringend empfohlen.

Die Dienstschnittstelle

Die Dienstschnittstelle kann eine .NET-Standardschnittstelle (häufig in C# geschrieben) sein, sollte jedoch den Richtlinien entsprechen, die vom vom ServiceRpcDescriptorabgeleiteten Typ festgelegt werden, den Ihr Dienst verwendet, um sicherzustellen, dass die Schnittstelle über RPC verwendet werden kann, wenn der Client und der Dienst in verschiedenen Prozessen ausgeführt werden. Diese Einschränkungen umfassen in der Regel, dass Eigenschaften und Indexer nicht zulässig sind, und die meisten oder alle Methoden geben Task oder einen anderen asynchron kompatiblen Rückgabetyp zurück.

Die ServiceJsonRpcDescriptor ist der empfohlene abgeleitete Typ für brokerierte Dienste. Diese Klasse verwendet die StreamJsonRpc-Bibliothek, wenn der Client und der Dienst RPC für die Kommunikation benötigen. StreamJsonRpc wendet bestimmte Einschränkungen für die Dienstschnittstelle an, wie hier beschrieben.

Die Schnittstelle kann von IDisposable, System.IAsyncDisposableoder sogar Microsoft.VisualStudio.Threading.IAsyncDisposable abgeleitet werden, dies ist jedoch nicht vom System erforderlich. Die generierten Clientproxys implementieren IDisposable bei beiden Varianten.

Eine einfache Rechnerdienstschnittstelle kann wie folgt deklariert werden:

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

Obwohl die Implementierung der Methoden auf dieser Schnittstelle möglicherweise keine asynchrone Methode garantiert, verwenden wir immer asynchrone Methodensignaturen auf dieser Schnittstelle, da diese Schnittstelle verwendet wird, um den Clientproxy zu generieren, der diesen Dienst remote aufrufen kann, was sicherlich eine asynchrone Methodensignatur garantiert.

Eine Schnittstelle kann Ereignisse deklarieren, die verwendet werden können, um ihre Clients über Ereignisse zu benachrichtigen, die am Dienst auftreten.

Neben Ereignissen oder dem Beobachterentwurfsmuster kann ein vermittelter Dienst, der den Client „zurückrufen“ muss, eine zweite Schnittstelle definieren, die als Vertrag dient, den ein Client implementieren und über die Eigenschaft ServiceActivationOptions.ClientRpcTarget bereitstellen muss, wenn er den Dienst anfordert. Eine solche Schnittstelle sollte den gleichen Entwurfsmustern und Einschränkungen entsprechen wie die brokerierte Dienstschnittstelle, aber mit zusätzlichen Einschränkungen für die Versionsverwaltung.

Tipps zum Entwurf einer leistungsfähigen, zukunftssicheren RPC-Schnittstelle finden Sie unter Bewährte Methoden für das Entwerfen eines vermittelten Diensts.

Es kann hilfreich sein, diese Schnittstelle in einer unterschiedlichen Assembly von der Assembly zu deklarieren, die den Dienst implementiert, damit seine Clients auf die Schnittstelle verweisen können, ohne dass der Dienst mehr Implementierungsdetails verfügbar machen muss. Es kann auch hilfreich sein, die Schnittstelle als NuGet-Paket für andere Erweiterungen bereitzustellen, damit andere darauf verweisen können, während Sie Ihre eigene Erweiterung zurückhalten, um die Implementierung des Dienstes bereitzustellen.

Erwägen Sie die Ausrichtung der Assembly, die Ihre Dienstschnittstelle für netstandard2.0 deklariert, um sicherzustellen, dass Ihr Dienst von jedem .NET-Prozess aus problemlos aufgerufen werden kann, unabhängig davon, ob .NET Framework, .NET Core, .NET 5 oder höher ausgeführt wird.

Testen

Automatisierte Tests sollten zusammen mit Ihrem Dienst Schnittstelle geschrieben werden, um die RPC-Bereitschaft der Schnittstelle zu überprüfen.

Die Tests sollten überprüfen, ob alle daten, die über die Schnittstelle übergeben werden, serialisierbar sind.

Möglicherweise finden Sie die BrokeredServiceContractTestBase<TInterface,TServiceMock> Klasse aus dem Microsoft.VisualStudio.Sdk.TestFramework.Xunit Pakets nützlich, um Ihre Schnittstellentestklasse davon abzuleiten. Diese Klasse enthält einige grundlegende Konventionstests für Ihre Schnittstelle, Methoden zur Unterstützung gängiger Assertionen wie Ereignistests und vieles mehr.

Methodik

Bestätigen Sie, dass jedes Argument und der Rückgabewert vollständig serialisiert wurden. Wenn Sie die oben erwähnte Testbasisklasse verwenden, sieht Ihr Code möglicherweise wie folgt aus:

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

Erwägen Sie das Testen der Überladungsauflösung, wenn Sie mehrere Methoden mit demselben Namen deklarieren. Möglicherweise fügen Sie ihrem Modelldienst für jede Methode ein internal Feld hinzu, das Argumente für diese Methode speichert, damit die Testmethode die Methode aufrufen kann, und überprüfen Sie dann, ob die richtige Methode mit den richtigen Argumenten aufgerufen wurde.

Ereignisse

Alle ereignisse, die auf der Schnittstelle deklariert sind, sollten ebenfalls auf die RPC-Bereitschaft getestet werden. Ereignisse, die von einem vermittelten Dienst ausgelöst werden, verursachen keinen Testfehler, wenn sie während der RPC-Serialisierung fehlschlagen, da Ereignisse „ausgelöst und vergessen“ werden.

Wenn Sie die oben erwähnte Testbasisklasse verwenden, ist dieses Verhalten bereits in einige Hilfsmethoden integriert und kann wie folgt aussehen (mit unveränderten Teilen, die aus Platzgründen weggelassen werden):

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

Implementieren des Diensts

Die Dienstklasse sollte die rpc-Schnittstelle implementieren, die im vorherigen Schritt deklariert ist. Ein Dienst kann IDisposable oder andere Schnittstellen implementieren, die über die für RPC verwendete Schnittstelle hinausgehen. Der auf dem Client generierte Proxy implementiert nur die Dienstschnittstelle, IDisposableund möglicherweise ein paar andere Auswahlschnittstellen zur Unterstützung des Systems, sodass eine Umwandlung zu anderen vom Dienst implementierten Schnittstellen auf dem Client fehlschlägt.

Betrachten Sie das oben verwendete Rechnerbeispiel, das wir hier implementieren:

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

Da die Methodentexte selbst nicht asynchron sein müssen, schließen wir den Rückgabewert explizit in einen konstruierten ValueTask<TResult> Rückgabetyp um, der der Dienstschnittstelle entspricht.

Implementieren des feststellbaren Entwurfsmusters

Wenn Sie ein Beobachterabonnement auf Ihrer Dienstschnittstelle anbieten, sieht es möglicherweise wie folgt aus:

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

Das Argument IObserver<T> muss normalerweise die Dauer dieses Methodenaufrufs überdauern, damit der Client weiterhin Updates empfängt, nachdem der Methodenaufruf abgeschlossen ist, bis der Client den zurückgegebenen IDisposable-Wert verworfen hat. Damit dies möglich ist, kann die Dienstklasse eine Sammlung von IObserver<T>-Abonnements enthalten, die bei jeder Aktualisierung des Zustands aufgezählt wird, sodass alle Abonnenten aktualisiert werden. Achten Sie darauf, dass die Aufzählung Ihrer Sammlung in Bezug aufeinander threadsicher ist. Dies gilt besonders für Mutationen in dieser Sammlung, die durch zusätzliche Abonnements oder Beendigungen dieser Abonnements auftreten können.

Achten Sie darauf, dass alle über OnNext veröffentlichten Updates die Reihenfolge beibehalten, in der Statusänderungen für Ihren Dienst eingeführt wurden.

Alle Abonnements sollten letztendlich mit einem Aufruf von OnCompleted oder OnError beendet werden, um Ressourcenlecks auf dem Client- und RPC-System zu vermeiden. Dies gilt auch für die Dienstverwerfung, bei der alle verbleibenden Abonnements explizit abgeschlossen werden müssen.

Erfahren Sie mehr über das Beobachterentwurfsmuster, die Implementierung eines beobachtbaren Datenanbieters und insbesondere unter Berücksichtigung von RPC.

Verwerfbare Dienste

Ihre Dienstklasse muss nicht verwerfbar sein, aber Dienste, die verwerfbar sind, werden verworfen, wenn der Client seinen Proxy zu Ihrem Dienst verwirft oder die Verbindung zwischen Client und Dienst unterbrochen wird. Auf verwerfbare Schnittstellen wird in dieser Reihenfolge getestet: System.IAsyncDisposable, Microsoft.VisualStudio.Threading.IAsyncDisposable, IDisposable. Nur die erste Schnittstelle aus dieser Liste, die ihre Dienstklasse implementiert, wird verwendet, um den Dienst zu löschen.

Bedenken Sie die Threadsicherheit bei der Entsorgung. Ihre Dispose-Methode kann für jeden Thread aufgerufen werden, während anderer Code in Ihrem Dienst ausgeführt wird (z. B. wenn eine Verbindung gelöscht wird).

Auslösen von Ausnahmen

Beim Auslösen von Ausnahmen sollten Sie LocalRpcException mit einem bestimmten ErrorCode auslösen, um den vom Client im RemoteInvocationException empfangenen Fehlercode zu steuern. Wenn Clients ein Fehlercode bereitgestellt wird, können sie nach der Art des Fehlers besser branchen als durch das Analysieren von Ausnahmemeldungen oder -typen.

Je nach JSON-RPC Spezifikation müssen Fehlercodes größer als -32000 sein, einschließlich positiver Zahlen.

Verbrauch anderer brokerter Dienste

Wenn ein vermittelter Dienst selbst Zugriff auf einen anderen vermittelten Dienst benötigt, sollten Sie den IServiceBroker verwenden, der seiner Dienstfactory zur Verfügung gestellt wird. Besonders wichtig ist dies, wenn bei der Registrierung des vermittelten Diensts das Flag AllowTransitiveGuestClients gesetzt wird.

Wenn unser Rechnerdienst andere von Maklern vermittelte Dienste benötigen würde, um sein Verhalten zu implementieren, würden wir den Konstruktor so ändern, dass er eine IServiceBrokerakzeptiert, um dieser Richtlinie zu entsprechen.

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

    // ...
}

Erfahren Sie mehr darüber, wie Sie einen vermittelten Dienst sichern und vermittelte Dienste nutzen.

Zustandsbehaftete Dienste

Zustand je Client

Für jeden Client, der den Dienst anfordert, wird eine neue Instanz dieser Klasse erstellt. Ein Feld in der obigen Calculator Klasse würde einen Wert speichern, der für jeden Client eindeutig sein kann. Angenommen, wir fügen einen Indikator hinzu, der jedes Mal erhöht wird, wenn ein Vorgang ausgeführt wird:

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

Ihr vermittelter Dienst sollte so geschrieben sein, dass er threadsichere Verfahren anwendet. Wenn Sie den empfohlenen ServiceJsonRpcDescriptor verwenden, können Remoteverbindungen mit Clients die parallele Ausführung der Methoden Ihres Diensts beinhalten, wie in diesem Dokument beschrieben. Wenn der Client einen Prozess und eine AppDomain mit dem Dienst teilt, ruft der Client Ihren Dienst möglicherweise gleichzeitig aus mehreren Threads auf. Eine threadsichere Implementierung des obigen Beispiels kann Interlocked.Increment(Int32) verwenden, um das feld operationCounter zu erhöhen.

Freigegebener Zustand

Wenn es einen Zustand gibt, den Ihr Dienst für alle seine Clients gemeinsam nutzen muss, sollte dieser Zustand in einer eindeutigen Klasse definiert werden, die von Ihrem VS-Paket instanziiert und als Argument an den Konstruktor Ihres Dienstes übergeben wird.

Angenommen, wir möchten, dass die oben definierte operationCounter alle Vorgänge auf allen Clients des Diensts zählen soll. Dann muss das Feld in diese neue Zustandsklasse aufgenommen werden:

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

Jetzt haben wir eine elegante, testbare Möglichkeit, den gemeinsamen Status in mehreren Instanzen unseres Calculator-Dienstes zu verwalten. Später beim Schreiben des Codes zum Bereitstellen des Diensts wird gezeigt, wie diese State-Klasse einmal erstellt und für jede Instanz des Calculator-Diensts freigegeben wird.

Die Threadsicherheit ist beim Umgang mit freigegebenen Zuständen besonders wichtig, da nicht davon ausgegangen werden kann, dass mehrere Clients ihre Aufrufe so planen, dass sie niemals parallel erfolgen.

Wenn eine freigegebene Zustandsklasse auf andere vermittelte Dienste zugreifen muss, sollte sie den globalen Service Broker verwenden und nicht einen der kontextbezogenen, die einer einzelnen Instanz des vermittelten Diensts zugewiesen sind. Wenn das Flag gesetzt ist, wirkt sich die Verwendung des globalen Service Brokers innerhalb eines vermittelten Diensts auf die ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients aus.

Sicherheitsbedenken

Sicherheit ist ein wichtiger Aspekt für Ihren vermittelten Dienst, wenn er mit dem ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients-Flag registriert ist, was möglichen Zugriff durch andere Benutzer auf anderen Computern ermöglicht, die an einer geteilten Live Share-Sitzung teilnehmen.

Lesen Sie Vorgehensweise: Sichern eines vermittelten Diensts, und ergreifen Sie die erforderlichen Risikominderungen, bevor Sie das AllowTransitiveGuestClients-Flag festlegen.

Der Dienstmoniker

Ein brokerisierter Dienst muss über einen serialisierbaren Namen und eine optionale Version verfügen, mit der ein Client den Dienst anfordern kann. Ein ServiceMoniker ist ein praktischer Wrapper für diese beiden Informationselemente.

Ein Dienstmoniker entspricht dem vollständigen Namen mit Assemblyqualifikation eines CLR-Typs (Common Language Runtime). Es muss global eindeutig sein und sollte daher Ihren Firmennamen und vielleicht Ihren Erweiterungsnamen als Präfixe für den Dienstnamen selbst einschließen.

Es kann nützlich sein, diesen Moniker in einem static readonly-Feld zur Verwendung an anderer Stelle zu definieren.

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

Während die meisten Anwendungen Ihres Dienstes Ihren Moniker möglicherweise nicht direkt nutzen, benötigt ein Client, der über Pipes anstelle eines Proxys kommuniziert, den Moniker.

Die Angabe einer Version ist bei einem Moniker zwar optional, wird aber empfohlen, da sie den Dienstautoren mehr Möglichkeiten bietet, die Kompatibilität mit Clients bei Verhaltensänderungen aufrechtzuerhalten.

Der Dienstdeskriptor

Der Dienstdeskriptor kombiniert den Dienstmoniker mit den Verhaltensweisen, die zum Ausführen einer RPC-Verbindung erforderlich sind, und erstellt einen lokalen oder Remoteproxy. Der Deskriptor ist dafür verantwortlich, Ihre RPC-Schnittstelle effektiv in ein Drahtprotokoll umzuwandeln. Dieser Dienstdeskriptor ist eine Instanz eines vom ServiceRpcDescriptorabgeleiteten Typs. Der Deskriptor muss allen Clients zur Verfügung gestellt werden, die einen Proxy für den Zugriff auf diesen Dienst verwenden. Für die Bereitstellung des Diensts ist auch dieser Deskriptor erforderlich.

Visual Studio definiert einen solchen abgeleiteten Typ und empfiehlt die Verwendung für alle Dienste: ServiceJsonRpcDescriptor. Dieser Deskriptor verwendet StreamJsonRpc für seine RPC-Verbindungen und erstellt einen leistungsfähigen lokalen Proxy für lokale Dienste, der einige der Remoteverhaltensweisen emuliert, z. B. das Umbrechen von Ausnahmen, die vom Dienst in RemoteInvocationExceptionausgelöst werden.

Die ServiceJsonRpcDescriptor unterstützt die Konfiguration der JsonRpc-Klasse für die JSON- oder MessagePack-Codierung des JSON-RPC-Protokolls. Wir empfehlen die MessagePack-Codierung, da sie kompakter ist und 10X leistungsfähiger sein kann.

Wir können einen Deskriptor für unseren Rechnerdienst wie folgt definieren:

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

Wie oben dargestellt, können Sie zwischen verschiedenen Formatierern und Trennzeichen wählen. Da nicht alle Kombinationen gültig sind, empfehlen wir eine der folgenden Kombinationen:

ServiceJsonRpcDescriptor.Formatters ServiceJsonRpcDescriptor.MessageDelimiters Am besten geeignet für
MessagePack BigEndianInt32LengthHeader Hochleistung
UTF8 (JSON) HttpLikeHeaders Interoperabilität mit anderen JSON-RPC Systemen

Indem das MultiplexingStream.Options-Objekt als letzter Parameter angegeben wird, wird die zwischen Client und Dienst gemeinsam genutzte RPC-Verbindung zu einem Kanal in einem MultiplexingStream, der zusammen mit der JSON-RPC Verbindung eine effiziente Übertragung umfangreicher Binärdaten über JSON-RPC ermöglicht.

Die Strategie ExceptionProcessing.ISerializable bewirkt, dass Ausnahmen, die von einem Dienst ausgelöst werden, serialisiert und als Exception.InnerException der auf dem Client ausgelösten RemoteInvocationException erhalten bleiben. Ohne diese Einstellung sind weniger detaillierte Ausnahmeinformationen auf dem Client verfügbar.

Tipp: Machen Sie Ihren Deskriptor als ServiceRpcDescriptor anstelle eines abgeleiteten Typs verfügbar, den Sie als Implementierungsdetail verwenden. Dies bietet Ihnen mehr Flexibilität, um Implementierungsdetails später zu ändern, ohne api-änderungen zu unterbrechen.

Fügen Sie einen Verweis auf Ihre Dienstschnittstelle in den XML-Dokumentkommentar zu Ihrem Deskriptor ein, um Benutzern die Nutzung Ihres Diensts zu erleichtern. Verweisen Sie auch auf die Schnittstelle, die Ihr Dienst als Client-RPC-Ziel akzeptiert, falls zutreffend.

Einige komplexere Dienste akzeptieren oder erfordern möglicherweise auch ein RPC-Zielobjekt vom Client, das einer Schnittstelle entspricht. Verwenden Sie für einen solchen Fall einen ServiceJsonRpcDescriptor-Konstruktor mit einem Type clientInterface-Parameter, um die Schnittstelle anzugeben, von der der Client eine Instanz angeben soll.

Versionsverwaltung des Deskriptors

Im Laufe der Zeit möchten Sie die Version Ihres Diensts erhöhen. In einem solchen Fall sollten Sie einen Deskriptor für jede Version definieren, die Sie unterstützen möchten, indem Sie für jede Version eine eindeutige versionspezifische ServiceMoniker verwenden. Die gleichzeitige Unterstützung mehrerer Versionen kann aus Gründen der Abwärtskompatibilität gut sein und kann in der Regel nur mit einer RPC-Schnittstelle erfolgen.

Visual Studio befolgt dieses Muster mit der VisualStudioServices-Klasse, indem der ursprüngliche ServiceRpcDescriptor als virtual-Eigenschaft unter der geschachtelten Klasse definiert wird, die die erste Version darstellt, die den vermittelten Dienst hinzugefügt hat. Wenn wir das Wire-Protokoll oder die Funktionalität des Dienstes hinzufügen/ändern müssen, deklariert Visual Studio eine override-Eigenschaft in einer später versionierten geschachtelten Klasse, die eine neue ServiceRpcDescriptorzurückgibt.

Für einen von einer Visual Studio-Erweiterung definierten dienst kann es ausreichen, eine andere Deskriptoreigenschaft neben dem Original zu deklarieren. Angenommen, Ihr 1.0-Dienst hat den UTF8-Formatierer (JSON) verwendet, und Sie stellen fest, dass der Wechsel zu MessagePack zu einem erheblichen Leistungsvorteil führen würde. Da es sich beim Ändern des Formatierers um einen Breaking Change des Wire-Protokolls handelt, muss die Versionsnummer des vermittelten Diensts erhöht und ein zweiter Deskriptor verwendet werden. Die beiden Deskriptoren könnten wie folgt aussehen:

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

Obwohl wir zwei Deskriptoren deklarieren (und später zwei Dienste anbieten und registrieren müssen), können wir dies mit nur einer Dienstschnittstelle und Implementierung erreichen, wodurch der Aufwand für die Unterstützung mehrerer Dienstversionen recht gering bleibt.

Bereitstellen des Diensts

Ihr vermittelte Dienst muss erstellt werden, wenn eine Anforderung eingeht, die durch einen Schritt namens "Anbieten des Dienstes" angeordnet wird.

Die Dienstleistungsfabrik

Verwenden Sie GlobalProvider.GetServiceAsync zum Anfordern des SVsBrokeredServiceContainer. Rufen Sie dann IBrokeredServiceContainer.Proffer für diesen Container auf, um Ihren Dienst anzubieten.

Im folgenden Beispiel bieten wir einen Dienst unter Verwendung des zuvor deklarierten CalculatorService-Feldes an, das auf eine Instanz eines ServiceRpcDescriptorfestgelegt ist. Wir übergeben unsere Dienstfactory, bei der es sich um einen BrokeredServiceFactory-Delegat handelt.

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

Ein brokerierter Dienst wird in der Regel einmal pro Client instanziiert. Dies ist eine Abweichung von anderen Visual Studio-Diensten (VS), die in der Regel einmal instanziiert und gemeinsam von allen Clients genutzt werden. Das Erstellen einer Instanz des Diensts pro Client ermöglicht eine bessere Sicherheit, da jeder Dienst und/oder seine Verbindung den Status pro Client über die Autorisierungsstufe, an der der Client arbeitet, beibehalten kann, was seine bevorzugte CultureInfo ist usw. Wie wir als Nächstes sehen, ermöglicht es auch interessantere Dienste, die für diese Anforderung spezifische Argumente akzeptieren.

Wichtig

Bei einer Dienstfactory, die von dieser Richtlinie abweicht und anstelle einer neuen Instanz eine freigegebene Dienstinstanz an alle Clients zurückgibt, sollte der Dienst IDisposable implementieren, da der erste Client, der seinen Proxy verwirft, dazu führt, dass die freigegebene Dienstinstanz verworfen wird, während andere Clients sie noch verwenden.

Im fortgeschritteneren Fall, in dem der CalculatorService-Konstruktor ein gemeinsam verwendetes Zustandsobjekt und ein IServiceBrokererfordert, können wir die Factory wie folgt bereitstellen:

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

Die lokale Variable state befindet sich außerhalb der Dienstfactory und wird daher nur einmal erstellt und für alle instanziierten Dienste freigegeben.

Wenn der Dienst noch fortgeschrittener ist und Zugriff auf die ServiceActivationOptions benötigt (z. B. um Methoden auf dem Client-RPC-Zielobjekt aufzurufen), könnten diese ebenfalls übergeben werden:

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

In diesem Fall könnte der Dienstkonstruktor wie folgt aussehen, vorausgesetzt, die ServiceJsonRpcDescriptor wurden mit typeof(IClientCallbackInterface) als eines der Konstruktorargumente erstellt:

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

Dieses clientCallback Feld kann jetzt aufgerufen werden, wenn der Dienst den Client aufrufen möchte, bis die Verbindung gelöscht wird.

Der Delegat BrokeredServiceFactory nimmt einen ServiceMoniker als Parameter an, falls die Dienstfactory eine freigegebene Methode ist, die mehrere Dienste oder verschiedene Versionen des Diensts basierend auf dem Moniker erstellt. Dieser Moniker stammt vom Client und enthält die Version des Diensts, die erwartet wird. Durch die Weiterleitung dieses Monikers an den Dienstkonstruktor kann der Dienst das eigenartige Verhalten bestimmter Dienstversionen emulieren, um den Erwartungen des Clients zu entsprechen.

Verwenden Sie den Delegat AuthorizingBrokeredServiceFactory nicht mit der Methode IBrokeredServiceContainer.Proffer, es sei denn, Sie verwenden den IAuthorizationService innerhalb der Klasse des vermittelten Diensts. Dieser IAuthorizationServicemuss mit der Klasse des vermittelten Diensts verworfen werden, damit kein Arbeitsspeicherverlust entsteht.

Unterstützen mehrerer Versionen Ihres Diensts

Wenn Sie die Version für den ServiceMoniker erhöhen, müssen Sie alle Versionen des vermittelten Diensts bereitstellen, die auf Clientanforderungen reagieren sollen. Dazu rufen Sie die IBrokeredServiceContainer.Proffer-Methode mit jedem ServiceRpcDescriptor auf, den Sie weiterhin unterstützen.

Die Bereitstellung des Diensts mit einer null-Version dient als „Catch-all“, mit dem bei allen Clientanforderungen, bei der es keine genaue Version für einen registrierten Dienst gibt, ein Treffer erzielt wird. Zum Beispiel können Sie Ihren 1.0- und 1.1-Dienst mit spezifischen Versionen anbieten und Ihren Dienst auch mit einer null-Version registrieren. In solchen Fällen rufen Clients, die den Dienst mit Version 1.0 oder 1.1 anfordern, die von Ihnen für diese Versionen bereitgestellte Dienstfactory auf, während ein Client, der Version 8.0 anfordert, die von Ihnen bereitgestellte Dienstfactory mit der Version Null aufruft. Da die vom Client angeforderte Version der Dienstfactory bereitgestellt wird, kann die Factory dann eine Entscheidung darüber treffen, wie der Dienst für diesen bestimmten Client konfiguriert wird oder ob null zurückgegeben werden soll, um eine nicht unterstützte Version zu kennzeichnen.

Eine Clientanforderung für einen Dienst mit einer null-Version kann nur für einen Dienst verwendet werden, der mit einer null-Version registriert und bereitgestellt wird.

Berücksichtigen Sie einen Fall, in dem Sie viele Versionen Ihres Diensts veröffentlicht haben, von denen mehrere abwärtskompatibel sind und somit eine Dienstimplementierung teilen können. Wir können die Catch-All-Option nutzen, um zu vermeiden, dass jede einzelne Version wie folgt wiederholt proffert werden muss:

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

Registrieren des Diensts

Wenn ein vermittelter Dienst für den globalen Container für vermittelte Dienste bereitgestellt wird und der Dienst nicht zuvor registriert wurde, führt dies zu einem Fehler. Die Registrierung bietet dem Container eine Möglichkeit, im Voraus zu wissen, welche brokerierten Dienste verfügbar sein können und welches VS-Paket geladen werden soll, wenn sie angefordert werden, um den Proffering-Code auszuführen. Dadurch kann Visual Studio schnell starten, ohne alle Erweiterungen im Voraus zu laden, aber die erforderliche Erweiterung laden können, wenn sie von einem Client seines brokerierten Diensts angefordert wird.

Die Registrierung kann erfolgen, indem Sie die ProvideBrokeredServiceAttribute auf Ihre von AsyncPackageabgeleitete Klasse anwenden. Dies ist die einzige Stelle, an der die ServiceAudience festgelegt werden kann.

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

Der Standard-Audience ist ServiceAudience.Process, der Ihren vermittelten Dienst nur für anderen Code innerhalb desselben Prozesses verfügbar macht. Indem Sie ServiceAudience.Localfestlegen, entscheiden Sie sich, Ihren vermittelten Dienst anderen Prozessen derselben Visual Studio-Sitzung zugänglich zu machen.

Wenn der vermittelte Dienst für Live Share-Gäste verfügbar gemacht werden muss, muss die AudienceServiceAudience.LiveShareGuest enthalten und die Eigenschaft ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients muss auf true festgelegt sein. Das Festlegen dieser Flags kann schwerwiegende Sicherheitsrisiken mit sich bringen und sollte nicht durchgeführt werden, ohne zuvor den Anleitungen in Wie man einen vermittelten Dienst sichertzu entsprechen.

Wenn Sie die Version für den ServiceMoniker erhöhen, müssen Sie alle Versionen des vermittelten Diensts registrieren, die auf Clientanforderungen reagieren sollen. Indem Sie mehr als die neueste Version Ihres brokerierten Diensts unterstützen, können Sie die Abwärtskompatibilität für Clients Ihrer älteren brokered-Dienstversion beibehalten, was besonders hilfreich sein kann, wenn Sie das Live Share-Szenario in Betracht ziehen, in dem jede Version von Visual Studio, die die Sitzung freigibt, eine andere Version sein kann.

Die Registrierung des Diensts mit einer null-Version dient als „Catch-all“, mit dem bei allen Clientanforderungen, bei der es keine genaue Version für einen registrierten Dienst gibt, ein Treffer erzielt wird. Beispielsweise können Sie Ihren 1.0- und 2.0-Dienst mit bestimmten Versionen registrieren und Ihren Dienst auch mit einer null-Version registrieren.

Verwenden von MEF zum Profferen und Registrieren Ihres Diensts

Dies erfordert Visual Studio 2022 Update 2 oder höher.

Ein Brokerdienst kann über MEF exportiert werden, anstatt ein Visual Studio-Paket zu verwenden, wie in den vorherigen beiden Abschnitten beschrieben. Dies hat Kompromisse zu berücksichtigen:

Kompromiss Paketangebot MEF-Export
Verfügbarkeit ✅ Der vermittelte Dienst ist sofort beim VS-Start verfügbar. ⚠ Die Verfügbarkeit des vermittelten Diensts kann sich verzögern, bis MEF im Prozess initialisiert wurde. Dies ist in der Regel schnell, kann jedoch mehrere Sekunden dauern, wenn der MEF-Cache veraltet ist.
Plattformübergreifende Bereitschaft ⚠️ Spezifischer Code für Visual Studio unter Windows muss erstellt werden. ✅ Der brokerierte Dienst in Ihrer Assembly kann in Visual Studio für Windows sowie in Visual Studio für Mac geladen werden.

So exportieren Sie Ihren brokerierten Dienst über MEF, anstatt VS-Pakete zu verwenden:

  1. Vergewissern Sie sich, dass sie keinen Code im Zusammenhang mit den letzten beiden Abschnitten haben. Insbesondere sollten Sie keinen Code haben, der IBrokeredServiceContainer.Proffer aufruft, und ProvideBrokeredServiceAttribute nicht auf Ihr Paket anwenden (falls vorhanden).
  2. Implementieren Sie die IExportedBrokeredService Schnittstelle in Ihrer brokerierten Dienstklasse.
  3. Vermeiden Sie Hauptthread-Abhängigkeiten in Ihrem Konstruktor oder beim Importieren von Eigenschaftensetzern. Verwenden Sie die IExportedBrokeredService.InitializeAsync-Methode zum Initialisieren Ihres intermediären Dienstes, bei dem Haupt-Thread-Abhängigkeiten zulässig sind.
  4. Wenden Sie die ExportBrokeredServiceAttribute auf Ihre vermittelte Dienstklasse an, indem Sie die Informationen zu Ihrem Dienstmoniker, der Zielgruppe und allen anderen erforderlichen Registrierungsinformationen angeben.
  5. Wenn Ihre Klasse die Entsorgung erfordert, implementieren Sie IDisposable und nicht IAsyncDisposable da MEF die Lebensdauer Ihres Diensts besitzt und nur die synchrone Entsorgung unterstützt.
  6. Achten Sie darauf, dass das Projekt, das den vermittelten Dienst enthält, in der Datei source.extension.vsixmanifest als MEF-Assembly aufgeführt ist.

Als MEF-Teil kann der vermittelte Dienst alle anderen MEF-Teile im Standardbereich importieren. Achten Sie dabei darauf, System.ComponentModel.Composition.ImportAttribute anstelle von System.Composition.ImportAttributezu verwenden. Dies liegt daran, dass die ExportBrokeredServiceAttribute von System.ComponentModel.Composition.ExportAttribute abgeleitet wird und derselbe MEF-Namespace innerhalb eines Typs verwendet wird.

Ein vermittelter Dienst ist einzigartig darin, dass er besondere Exporte importieren kann.

  • IServiceBroker, der verwendet werden muss, um andere vermittelte Dienste zu erwerben.
  • ServiceMoniker, was nützlich sein kann, wenn Sie mehrere Versionen Ihres brokerierten Diensts exportieren und erkennen müssen, welche Version der Client angefordert hat.
  • ServiceActivationOptions, was hilfreich sein kann, wenn Sie Ihre Kunden auffordern, spezielle Parameter oder ein Client-Rückrufziel bereitzustellen.
  • AuthorizationServiceClient, die nützlich sein können, wenn Sie Sicherheitsüberprüfungen ausführen müssen, wie in Anleitung zur Sicherung eines vermittelten Dienstesbeschrieben. Dieses Objekt muss nicht von der Klasse verworfen werden, da es automatisch verworfen wird, wenn der vermittelte Dienst beendet wird.

Der vermittelte Dienst darf das von MEF ImportAttribute verwenden, um andere vermittelte Dienste zu erwerben. Stattdessen kann er [Import]IServiceBroker und vermittelte Dienste auf herkömmliche Weise abfragen. Erfahren Sie mehr in Wie sie einen brokerierten Dienstnutzen.

Hier ist ein Beispiel:

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

Exportieren mehrerer Versionen Ihres brokerierten Diensts

Die ExportBrokeredServiceAttribute kann mehrmals auf Ihren brokerierten Dienst angewendet werden, um mehrere Versionen Ihres brokerierten Diensts anzubieten.

Die Implementierung der Eigenschaft IExportedBrokeredService.Descriptor muss einen Deskriptor mit einem Moniker zurückgeben, der dem vom Client angeforderten Moniker entspricht.

Betrachten Sie dieses Beispiel, bei dem der Rechendienst 1.0 im UTF8-Format exportierte und später einen Export von 1.1 hinzufügte, um von den Leistungsgewinnen der MessagePack-Formatierung zu profitieren.

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

Ab Visual Studio 2022 Update 12 (17.12) kann ein null versionierter Dienst exportiert werden, um jeder Clientanforderung für den Dienst zu entsprechen, unabhängig von der Version, auch wenn es sich um eine Anforderung mit einer null-Version handelt. Ein solcher Dienst kann null von der eigenschaft Descriptor zurückgeben, um eine Clientanforderung abzulehnen, wenn er keine Implementierung der vom Client angeforderten Version anbietet.

Ablehnen einer Serviceanfrage

Ein vermittelter Dienst kann die Aktivierungsanforderung eines Clients zurückweisen, indem er in der InitializeAsync-Methode eine Ausnahme auslöst. Das Auslösen bewirkt, dass eine ServiceActivationFailedException an den Client ausgelöst wird.