Bereitstellen eines vermittelten Diensts
Ein vermittelter Dienst besteht aus den folgenden Elementen:
- Eine Schnittstelle, die die Funktionalität des Diensts deklariert und als Vertrag zwischen dem Dienst und seinen Clients dient.
- Eine Implementierung dieser Schnittstelle.
- Ein Dienstmoniker, um dem Dienst einen Namen und eine Version zuzuweisen.
- Ein Deskriptor, der den Namen des Dienstes mit dem Verhalten für die Behandlung von RPC (Remote Procedure Call) kombiniert, falls erforderlich.
- Nutzen Sie entweder die Service Factory und die Registrierung Ihres vermittelten Dienstes mit einem VS-Paket oder beides mit MEF (dem Managed Extensibility Framework).
Jeder der Punkte in der vorangegangenen Liste wird in den folgenden Abschnitten ausführlich beschrieben.
Bei allen Codes in diesem Artikel wird die Aktivierung der nullbaren Referenztypen-Funktion von C# dringend empfohlen.
Die Dienstschnittstellen
Die Dienstschnittstelle kann eine standardmäßige .NET-Schnittstelle sein (häufig in C# geschrieben), sollte aber den Richtlinien entsprechen, die durch den von ServiceRpcDescriptorabgeleiteten Typ festgelegt sind, den Ihr Dienst verwendet, um sicherzustellen, dass die Schnittstelle über RPC verwendet werden kann, wenn Client und Dienst in unterschiedlichen 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 einen anderen asynchron kompatiblen Rückgabetyp zurückTask
.
Dies 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 auf der Dienstschnittstelle an, wie hier beschrieben.
Die Schnittstelle kann von IDisposable, System.IAsyncDisposableoder sogar Microsoft.VisualStudio.Threading.IAsyncDisposable abgeleitet werden, aber dies ist nicht vom System erforderlich. Die generierten Clientproxys implementieren IDisposable beide Methoden.
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.
Über Ereignisse oder das Beobachter-Entwurfsmuster hinaus 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.ClientRpcTargetbereitstellen 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.
Lesen Sie bewährte Methoden zum Entwerfen eines brokerierten Diensts für Tipps zum Entwerfen einer leistungsfähigen, zukunftssicheren RPC-Schnittstelle.
Es kann nützlich sein, diese Schnittstelle in einer anderen Assembly zu deklarieren als die Assembly, die den Dienst implementiert, sodass deren Clients auf die Schnittstelle verweisen können, ohne dass der Dienst weitere Implementierungsdetails preisgeben muss. Es kann auch hilfreich sein, die Schnittstellenassembly als NuGet-Paket für andere Erweiterungen zu versenden, um zu referenzieren, während Sie Ihre eigene Erweiterung reservieren, um die Dienstimplementierung zu versenden.
Erwägen Sie die Ausrichtung der Assembly, die Ihre Dienstschnittstelle deklariert, um netstandard2.0
sicherzustellen, dass Ihr Dienst von jedem .NET-Prozess aus einfach aufgerufen wird, unabhängig davon, ob .NET Framework, .NET Core, .NET 5 oder höher ausgeführt wird.
Testen
Automatisierte Tests sollten zusammen mit Ihrer Dienstschnittstelle 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-Paket hilfreich, um ihre Schnittstellentestklasse abzuleiten. Diese Klasse enthält einige grundlegende Konventionstests für Ihre Schnittstelle, Methoden zur Unterstützung gängiger Assertionen wie Ereignistests und vieles mehr.
Methoden
Bestätigen Sie, dass jedes Argument und der Rückgabewert vollständig serialisiert wurden. Wenn Sie die oben erwähnte Test-Basisklasse verwenden, könnte Ihr Code wie folgt aussehen:
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, in dem Argumente für diese Methode gespeichert werden, 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, führen nicht zu einem Testfehler, wenn sie während der RPC-Serialisierung fehlschlagen, da Ereignisse "fire and forget" sind.
Wenn Sie die oben erwähnte Test-Basisklasse verwenden, ist dieses Verhalten bereits in einige Hilfsmethoden integriert und könnte wie folgt aussehen (wobei unveränderte Teile der Kürze halber weggelassen wurden):
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 über die für RPC verwendete Schnittstelle hinaus oder andere Schnittstellen implementierenIDisposable. Der auf dem Client erzeugte Proxy implementiert nur die Schnittstelle des Dienstes IDisposable und möglicherweise einige andere ausgewählte Schnittstellen zur Unterstützung des Systems, sodass ein Cast auf andere vom Dienst implementierte Schnittstellen auf dem Client fehlschlagen wird.
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 Methodenteile selbst nicht asynchron sein müssen, verpacken wir den Rückgabewert explizit in einen konstruierten ValueTask<TResult>-Rückgabetyp, um der Schnittstelle des Dienstes zu entsprechen.
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 IObserver<T> Argument muss in der Regel die Lebensdauer dieses Methodenaufrufs überleben, damit der Client weiterhin Updates empfängt, nachdem der Methodenaufruf abgeschlossen ist, bis der Client den zurückgegebenen IDisposable Wert verworfen hat. Um dies zu erleichtern, kann Ihre Dienstklasse eine Sammlung von IObserver<T> Abonnements enthalten, die alle an Ihrem Status vorgenommenen Updates dann auflisten würden, um alle Abonnenten zu aktualisieren. Achten Sie darauf, dass die Aufzählung Ihrer Sammlung threadsicher ist und insbesondere mit den Mutationen in dieser Sammlung, die über zusätzliche Abonnements oder Entsorgungen dieser Abonnements auftreten können.
Achten Sie darauf, dass alle Updates, die über OnNext die Aktualisierung veröffentlicht wurden, die Reihenfolge beibehalten, in der Statusänderungen für Ihren Dienst eingeführt wurden.
Alle Abonnements sollten letztendlich mit einem Aufruf OnCompleted oder OnError um Ressourcenlecks auf dem Client- und RPC-System zu vermeiden, beendet werden. Dies gilt auch für die Dienstentsorgung, bei der alle verbleibenden Abonnements explizit abgeschlossen werden sollen.
Erfahren Sie mehr über das Beobachterentwurfsmuster, die Implementierung eines observierbaren Datenanbieters und insbesondere unter Berücksichtigung von RPC.
Einwegdienste
Ihre Dienstklasse muss nicht verfügbar sein, aber Dienste, die verworfen werden, wenn der Client seinen Proxy an Ihren Dienst verworfen oder die Verbindung zwischen Client und Dienst verloren geht. Einwegschnittstellen werden 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
Wenn Sie Ausnahmen auslösen, sollten Sie LocalRpcException mit einem bestimmten ErrorCode auslösen, um den vom Client im RemoteInvocationExceptionempfangenen Fehlercode zu steuern. Das Bereitstellen von Clients mit einem Fehlercode kann es ihnen ermöglichen, basierend auf der Art des Fehlers zu verzweigen, der besser als das Analysieren von Ausnahmemeldungen oder -typen ist.
Gemäß der JSON-RPC-Spezifikation müssen Fehlercodes größer als -32000 sein, einschließlich positiver Zahlen.
Verbrauch anderer vermittelte Dienste
Wenn ein vermittelter Dienst selbst Zugriff auf einen anderen vermittelten Dienst erfordert, empfehlen wir die Verwendung des IServiceBroker für die Dienstfactory bereitgestellten Diensts, es ist jedoch besonders wichtig, wenn die vermittelte Dienstregistrierung die AllowTransitiveGuestClients Kennzeichnung festlegt.
Um dieser Richtlinie zu entsprechen, wenn unser Rechnerdienst andere vermittelte Dienste benötigt hat, um sein Verhalten zu implementieren, würden wir den Konstruktor so ändern, dass er folgendes IServiceBrokerakzeptiert:
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
Status pro Client
Für jeden Client, der den Dienst anfordert, wird eine neue Instanz dieser Klasse erstellt.
Ein Feld in der Calculator
obigen 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 geschrieben werden, um threadsichere Methoden zu befolgen.
Bei Verwendung der empfohlenen ServiceJsonRpcDescriptor können Remoteverbindungen mit Clients die gleichzeitige Ausführung der Methoden Ihres Diensts wie in diesem Dokument beschrieben enthalten.
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 könnte Interlocked.Increment(Int32) verwendet werden, um das Feld operationCounter
zu erhöhen.
Freigegebener Zustand
Wenn der Dienst für alle Clients freigegeben werden muss, sollte dieser Zustand in einer eindeutigen Klasse definiert werden, die von Ihrem VS-Paket instanziiert und als Argument an den Konstruktor Ihres Diensts übergeben wird.
Angenommen, die operationCounter
oben definierte Definition soll alle Vorgänge für alle Clients des Diensts zählen.
Wir müssten das Feld in diese neue Zustandsklasse heben:
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 freigegebenen Zustand über mehrere Instanzen unseres Calculator
Diensts hinweg 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.
Es ist besonders wichtig, threadsicher zu sein, wenn es um den freigegebenen Zustand geht, da bei mehreren Clients keine Annahme möglich ist, ihre Anrufe so zu planen, dass sie niemals gleichzeitig getätigt werden.
Wenn Ihre freigegebene Statusklasse auf andere vermittelte Dienste zugreifen muss, sollte sie den globalen Dienstbroker anstelle einer der Kontexte verwenden, die einer einzelnen Instanz Ihres vermittelten Diensts zugewiesen sind. Die Verwendung des globalen Servicebrokers innerhalb eines vermittelten Diensts trägt zu den Sicherheitsauswirkungen bei, wenn die ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients Kennzeichnung festgelegt wird.
Sicherheitsaspekte
Sicherheit ist eine Berücksichtigung für Ihren vermittelten Dienst, wenn er mit der ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients Kennzeichnung registriert ist, wodurch er für den möglichen Zugriff durch andere Benutzer auf anderen Computern verfügbar gemacht wird, die an einer freigegebenen Live-Freigabesitzung teilnehmen.
Überprüfen Sie, wie Sie einen Vermittelten Dienst sichern und die erforderlichen Sicherheitsminderungen ergreifen, bevor Sie die AllowTransitiveGuestClients Kennzeichnung festlegen.
COM-Dienstmoniker
Ein vermittelter Dienst muss einen serialisierbaren Namen und eine optionale Version haben, mit der ein Client den Dienst anfragen kann. Ein ServiceMoniker ist ein praktischer Wrapper für diese beiden Informationselemente.
Ein Dienstname entspricht dem vollständigen Assembly-qualifizierten Namen 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 Feld für die Verwendung an anderer static readonly
Stelle zu definieren:
public static readonly ServiceMoniker Moniker = new ServiceMoniker("YourCompany.Extension.Calculator", new Version("1.0"));
Während die meisten Verwendungen Ihres Diensts Ihren Moniker möglicherweise nicht direkt verwenden, erfordert ein Client, der über Rohre kommuniziert, anstelle eines Proxys den Moniker.
Die Angabe einer Version ist bei einem Moniker zwar optional, wird aber empfohlen, da sie den Autor*innen von Diensten mehr Möglichkeiten bietet, die Kompatibilität mit Clients bei Verhaltensänderungen zu gewährleisten.
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 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 RemoteInvocationException ausgelöst werden.
ServiceJsonRpcDescriptor unterstützt die Konfiguration der Klasse für die JsonRpc JSON- oder MessagePack-Codierung des JSON-RPC-Protokolls. Wir empfehlen die MessagePack-Kodierung, da sie kompakter ist und eine 10-fach höhere Leistung bieten 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 sie oben sehen können, steht eine Auswahl an Formatierern und Trennzeichen zur Verfügung. Da nicht alle Kombinationen gültig sind, empfehlen wir eine der folgenden Kombinationen:
ServiceJsonRpcDescriptor.Formatters | ServiceJsonRpcDescriptor.MessageDelimiters | Am besten geeignet für |
---|---|---|
MessagePack | BigEndianInt32LengthHeader | Hohe Leistung |
UTF8 (JSON) | HttpLikeHeaders | Interoperabilität mit anderen JSON-RPC-Systemen |
Durch Angeben des MultiplexingStream.Options
Objekts als letzten Parameter ist die zwischen Client und Dienst gemeinsam genutzte RPC-Verbindung nur ein Kanal in einem MultiplexingStream, der mit der JSON-RPC-Verbindung geteilt wird, um eine effiziente Übertragung großer Binärdaten über JSON-RPC zu ermöglichen.
Die ExceptionProcessing.ISerializable-Strategie bewirkt, dass Ausnahmen, die von Ihrem Dienst ausgelöst werden, serialisiert und beibehalten werden, wie Exception.InnerException dies RemoteInvocationException auf dem Client ausgelöst wird. Ohne diese Einstellung sind weniger detaillierte Ausnahmeinformationen auf dem Client verfügbar.
Tipp: Machen Sie Ihren Deskriptor 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 bereitstellen 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 jeweils eine eindeutige ServiceMoniker Version 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 folgt diesem Muster mit seiner VisualStudioServices-Klasse, indem das Original ServiceRpcDescriptor als virtual
-Eigenschaft unter der geschachtelten Klasse definiert wird, die die erste Version darstellt, die diesen vermittelten Dienst hinzugefügt hat.
Wenn wir das Wire-Protokoll oder die Funktion zum Hinzufügen/Ändern des Diensts ändern müssen, deklariert Visual Studio eine override
-Eigenschaft in einer späteren versionierten geschachtelten Klasse, die eine neue ServiceRpcDescriptor zurückgibt.
Für einen von einer Visual Studio-Erweiterung definierten dienst kann es ausreichen, einen anderen Eigenschaftendeskriptor 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 eine Änderung des Wire-Protokolls handelt, muss die Versionsnummer des vermittelten Diensts und ein zweiter Deskriptor erhöht werden. Die beiden Beschreibungen können 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 tun, wodurch der Aufwand für die Unterstützung mehrerer Dienstversionen ziemlich gering bleibt.
Bereitstellen des Diensts
Ihr vermittelter Dienst muss erstellt werden, wenn eine Anfrage eingeht. Dies wird über einen Schritt namens „Anbieten des Dienstes“ arrangiert.
Die Dienstfactory
Verwenden Sie GlobalProvider.GetServiceAsync zum Anfordern der SVsBrokeredServiceContainer. Rufen Sie IBrokeredServiceContainer.Proffer dann in diesen Container auf, um Ihren Dienst zu nutzen.
Im folgenden Beispiel bieten wir einen Dienst unter Verwendung des zuvor deklarierten Felds CalculatorService
an, das auf eine Instanz von ServiceRpcDescriptor gesetzt ist.
Wir übergeben es unsere Dienstfactory, die eine BrokeredServiceFactory Stellvertretung ist.
IBrokeredServiceContainer container = await AsyncServiceProvider.GlobalProvider.GetServiceAsync<SVsBrokeredServiceContainer, IBrokeredServiceContainer>();
container.Proffer(
CalculatorService,
(moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(new CalculatorService()));
Ein vermittelter Dienst wird in der Regel einmal pro Client instanziiert. Dies ist eine Abweichung von anderen VS (Visual Studio)-Diensten, die in der Regel einmal instanziiert und von allen Clients gemeinsam genutzt werden. Das Erstellen einer Instanz des Dienstes pro Client bietet die Möglichkeit, die Sicherheit zu erhöhen, da jeder Dienst und/oder seine Verbindung den Status pro Client in Bezug auf die Berechtigungsstufe des Clients, seine bevorzugte CultureInfo-Stufe usw. speichern kann. Wie wir gleich sehen werden, bietet dies auch die Möglichkeit für interessantere Dienste, die spezifische Argumente für diese Anfrage akzeptieren.
Wichtig
Eine Dienstfactory, die von dieser Richtlinie abweicht und eine Instanz für gemeinsame Dienste zurückgibt, anstatt eine neue Instanz für jeden Client zurückgibt, sollte niemals über die Dienstimplementierung IDisposableverfügen, da der erste Client, der seinen Proxy verworfen hat, zur Entsorgung der Instanz des gemeinsamen Diensts führt, bevor andere Clients ihn verwenden.
Im fortgeschritteneren Fall, in dem für den CalculatorService
-Konstruktor ein freigegebenes Zustandsobjekt und ein freigegebenes IServiceBroker-Zustandsobjekt erforderlich ist, können wir die Factory wie folgt nutzen:
var state = new CalculatorService.State();
container.Proffer(
CalculatorService,
(moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(new CalculatorService(state, serviceBroker)));
Die state
lokale Variable befindet sich außerhalb der Service Factory und wird daher nur einmal erstellt und von allen instanziierten Diensten gemeinsam genutzt.
Noch fortgeschrittener, wenn der Dienst Zugriff auf die ServiceActivationOptions (z. B. zum Aufrufen von Methoden für das Client-RPC-Zielobjekt) benötigt, die ebenfalls übergeben werden können:
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, wobei davon ausgegangen wird, dass ServiceJsonRpcDescriptor mit typeof(IClientCallbackInterface)
als eines der Konstruktorargumente erstellt wurde:
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 BrokeredServiceFactory Delegat nimmt ein ServiceMoniker als Parameter an, falls die Service Factory eine gemeinsam genutzte Methode ist, die mehrere Dienste oder verschiedene Versionen des Dienstes auf der Grundlage des Monikers erstellt. Dieser Moniker stammt vom Kunden und enthält die Version des von ihnen erwarteten Dienstes. Durch die Weiterleitung dieses Monikers an den Dienstkonstruktor kann der Dienst das eigenartige Verhalten bestimmter Dienstversionen emulieren, um den Erwartungen des Clients zu entsprechen.
Vermeiden Sie die Verwendung des AuthorizingBrokeredServiceFactory-Delegaten mit der IBrokeredServiceContainer.Proffer-Methode, es sei denn, Sie verwenden die IAuthorizationService innerhalb ihrer vermittelten Dienstklasse. Dies IAuthorizationServicemuss mit Ihrer vermittelten Dienstklasse verworfen werden, um einen Speicherverlust zu vermeiden.
Unterstützung für mehrere Versionen Ihres Dienstes
Wenn Sie die Version auf Ihrem ServiceMonikerComputer erhöhen, müssen Sie jede Version Ihres brokerierten Diensts nutzen, für die Sie auf Clientanforderungen reagieren möchten. Dazu rufen Sie die IBrokeredServiceContainer.Proffer-Methode mit jeder ServiceRpcDescriptor auf, die Sie weiterhin unterstützen.
Wenn Sie Ihren Dienst mit einer null
-Version anbieten, dient dies als "Catch-All", der auf jede Anfrage eines Clients passt, für die keine genaue Versionsübereinstimmung mit einem registrierten Dienst existiert.
Sie können z. B. Ihren Dienst 1.0 und 1.1 mit bestimmten Versionen anbieten und Ihren Dienst auch mit einer null
Version registrieren.
In solchen Fällen rufen Clients, die Ihren Dienst mit 1.0 oder 1.1 anfragen, die Service Factory auf, die Sie für genau diese Versionen angeboten haben, während ein Client, der die Version 8.0 anfragt, dazu führt, dass die von Ihnen angebotene Service Factory mit der Null-Version aufgerufen wird.
Da die vom Client angefragte Version der Service Factory zur Verfügung gestellt wird, kann die Factory entscheiden, wie der Dienst für diesen bestimmten Client konfiguriert werden soll oder ob null
zurückgegeben werden soll, um eine nicht unterstützte Version zu signalisieren.
Eine Anfrage eines Clients nach einem Dienst mit null
einer -Version passt auf einen Dienst, der mit einer null
-Version registriert und angeboten wird.
Stellen Sie sich einen Fall vor, in dem Sie viele Versionen Ihres Dienstes veröffentlicht haben, von denen mehrere abwärtskompatibel sind und somit eine gemeinsame Implementierung haben können. Wir können die Catch-All-Option verwenden, um zu vermeiden, dass wir jede einzelne Version wie folgt anbieten müssen:
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 Sie einen vermittelten Dienst an den globalen Container für den vermittelten Dienst senden, wird dieser ausgelöst, es sei denn, der Dienst wurde zuerst registriert. Die Registrierung bietet dem Container eine Möglichkeit, im Voraus zu wissen, welche vermittelten Dienste verfügbar sein können und welches VS-Paket geladen werden soll, wenn sie angefordert werden, um den Anbieter-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 vermittelten Diensts angefordert wird.
Die Registrierung kann durch Anwenden der ProvideBrokeredServiceAttribute auf Ihre AsyncPackageabgeleitete Klasse erfolgen. Dies ist der einzige Ort, an dem die ServiceAudience Einstellung erfolgen kann.
[ProvideBrokeredService("YourCompany.Extension.Calculator", "1.0", Audience = ServiceAudience.Local)]
Der Standardwert Audience ist ServiceAudience.Process, der Ihren vermittelten Dienst nur für anderen Code innerhalb desselben Prozesses verfügbar macht. Wenn Sie festlegen ServiceAudience.Local, können Sie Ihren vermittelten Dienst anderen Prozessen zur gleichen Visual Studio-Sitzung zuweisen.
Wenn Ihr vermittelter Dienst für Live Share-Gäste verfügbar gemacht werden, mussAudience enthalten ServiceAudience.LiveShareGuest und die ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients Eigenschaft auf true
gesetzt sein.
Das Festlegen dieser Flags kann zu schwerwiegenden Sicherheitsschwachstellen führen und sollte nicht ohne vorherige Beachtung der Anleitung in Wie man einen vermittelten Dienst sichert erfolgen.
Wenn Sie die Version auf Ihrem ServiceMonikerComputer erhöhen, müssen Sie jede Version Ihres vermittelten Diensts registrieren, für die Sie auf Clientanforderungen reagieren möchten. Indem Sie mehr als die neueste Version Ihres vermittelten Diensts unterstützen, können Sie die Abwärtskompatibilität für Clients Ihrer älteren vermittelten 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 Ihres Dienstes mit einer null
Version dient als "Catch-all", der bei jeder Anfrage eines Clients, für die es keine genaue Version mit einem registrierten Dienst gibt, einen Treffer erzielt.
Sie können z. B. Ihren Dienst 1.0 und 2.0 mit bestimmten Versionen registrieren und auch Ihren Dienst 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 vermittelter Dienst kann über MEF exportiert werden, anstatt ein Visual Studio-Paket zu verwenden, wie in den beiden vorherigen Abschnitten beschrieben. Dazu müssen Kompromisse berücksichtigt werden:
Kompromiss | Paketverwendung | MEF-Export |
---|---|---|
Verfügbarkeit | ✅ Der vermittelte Dienst ist sofort beim VS-Start verfügbar. | ⚠️ Die Verfügbarkeit des vermittelten Dienstes 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 | ⚠️ Es muss Visual Studio-spezifischer Code für Windows erstellt werden. | ✅Der vermittelte Dienst in Ihrer Assembly kann in Visual Studio für Windows sowie Visual Studio für Mac geladen werden. |
So exportieren Sie Ihren vermittelten Dienst über MEF, anstatt VS-Pakete zu verwenden:
- 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 Sie sollten ProvideBrokeredServiceAttribute nicht auf Ihr Paket anwenden (falls vorhanden).
- Implementieren Sie die
IExportedBrokeredService
Schnittstelle in Ihrer vermittelten Dienstklasse. - Vermeiden Sie Hauptthreadabhängigkeiten in Ihrem Konstruktor oder importieren Sie Eigenschaftensetter. Verwenden Sie die
IExportedBrokeredService.InitializeAsync
-Methode zum Initialisieren Ihres vermittelten Diensts, wobei Hauptthreadabhängigkeiten zulässig sind. - Wenden Sie die
ExportBrokeredServiceAttribute
-Klasse ihres vermittelten Diensts an, und geben Sie die Informationen zu Ihrem Dienstmoniker, Publikum und anderen erforderlichen Registrierungsinformationen an. - Wenn Ihre Klasse die Entsorgung erfordert, implementieren Sie IDisposable statt IAsyncDisposable, seit MEF die Lebensdauer Ihres Diensts und unterstützt nur die synchrone Entsorgung.
- Stellen Sie sicher, dass die
source.extension.vsixmanifest
-Datei das Projekt auflistet, das Ihren vermittelten Dienst als MEF-Assembly enthält.
Als MEF-Teil kann Ihre vermittelter Dienstalle anderen MEF-Teile im Standardumfang importieren.
Achten Sie dabei darauf, anstelle System.ComponentModel.Composition.ImportAttribute von System.Composition.ImportAttribute zu verwenden.
Dies liegt daran, dass die ExportBrokeredServiceAttribute
Ableitungen von System.ComponentModel.Composition.ExportAttribute und der Verwendung desselben MEF-Namespaces während eines Typs erforderlich sind.
Ein vermittelter Dienst ist einzigartig, um einige spezielle Exporte importieren zu können:
- IServiceBroker, der verwendet werden sollte, um andere vermittelte Dienste zu erwerben.
- ServiceMoniker, der nützlich sein kann, wenn Sie mehrere Versionen Ihres vermittelten Diensts exportieren und ermitteln müssen, welche Version der Client angefordert hat.
- ServiceActivationOptions, was hilfreich sein kann, wenn Ihre Clients spezielle Parameter oder ein Clientrückrufziel bereitstellen müssen.
- AuthorizationServiceClient, was hilfreich sein kann, wenn Sie Sicherheitsüberprüfungen durchführen müssen, wie in So sichern Sie einen vermittelten Dienstbeschrieben. Dieses Objekt muss nicht von Ihrer Klasse gelöscht werden, da es automatisch verworfen wird, wenn Ihr vermittelter Dienst verworfen wird.
Ihr vermittelter Dienst darf keine MEF ImportAttribute verwenden, um andere vermittelte Dienste zu erwerben.
Stattdessen kann [Import]
IServiceBroker und auf herkömmliche Weise nach vermittelten Diensten abfragen.
Weitere Informationen finden Sie unter Nutzen eines vermittelten Diensts.
Im Folgenden sehen Sie 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 vermittelten Diensts
Das ExportBrokeredServiceAttribute
kann mehrfach auf Ihren vermittelten Dienst angewendet werden, um mehrere Versionen Ihres vermittelten Dienstes anzubieten.
Ihre Implementierung der IExportedBrokeredService.Descriptor
Eigenschaft sollte einen Deskriptor mit einem Moniker zurückgeben, der dem vom Client angeforderten entspricht.
Betrachten Sie dieses Beispiel, in dem der Rechnerdienst 1.0 mit UTF8-Formatierung exportiert hat, und später einen Export von 1.1 hinzufügt, um die Leistung der Verwendung der MessagePack-Formatierung zu genießen.
[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 Client-Anfrage nach dem Dienst unabhängig von der Version zu entsprechen, einschließlich einer Anfrage mit einer null
Version.
Ein solcher Dienst kann null
aus der Descriptor
-Eigenschaft zurückgeben, um eine Client-Anfrage abzulehnen, wenn er keine Implementierung der vom Client angeforderten Version anbietet.
Ablehnung einer Anfrage für einen Dienst
Ein vermittelter Dienst kann die Aktivierungsanfrage eines Clients zurückweisen, indem er aus der Methode InitializeAsync einen Fehler auslöst. Das Auslösen bewirkt, dass ein ServiceActivationFailedException an den Client ausgelöst wird.