Udostępnianie usługi obsługiwanej przez brokera
Usługa brokerowana składa się z następujących elementów:
- Interfejs , który deklaruje funkcjonalność usługi i służy jako kontrakt między usługą a jej klientami.
- Implementacja tego interfejsu.
- Nazwa usługi służąca do przypisywania nazwy i wersji do usługi.
- Deskryptor łączący moniker usługi z zachowaniem obsługi wywołania procedury zdalnej (zdalne wywołanie procedury) w razie potrzeby.
- Albo proffer the service factory and register your brokered service with a VS package, ordoe mef (Managed Extensibility Framework).
Poszczególne elementy na powyższej liście zostały szczegółowo opisane w poniższych sekcjach.
W przypadku wszystkich kodu w tym artykule zdecydowanie zaleca się aktywowanie funkcji typów odwołań dopuszczających wartość null języka C#.
Interfejs usługi
Interfejs usługi może być standardowym interfejsem platformy .NET (często napisanym w języku C#), ale powinien być zgodny z wytycznymi określonymi przez typ pochodny, który będzie używany przez ServiceRpcDescriptorusługę w celu zapewnienia, że interfejs może być używany przez RPC, gdy klient i usługa działają w różnych procesach.
Te ograniczenia zwykle obejmują, że właściwości i indeksatory są niedozwolone, a większość lub wszystkie metody zwracają Task
lub inny typ zwracany asynchroniczny.
Jest ServiceJsonRpcDescriptor to zalecany typ pochodny dla usług obsługiwanych przez brokera. Ta klasa korzysta z StreamJsonRpc biblioteki, gdy klient i usługa wymagają wywołania procedury RPC do komunikacji. StreamJsonRpc stosuje pewne ograniczenia dotyczące interfejsu usługi zgodnie z opisem w tym miejscu.
Interfejs może pochodzić z IDisposable, System.IAsyncDisposablelub nawet Microsoft.VisualStudio.Threading.IAsyncDisposable , ale nie jest to wymagane przez system. Wygenerowane serwery proxy klienta będą implementować IDisposable w dowolny sposób.
Prosty interfejs usługi kalkulatora może zostać zadeklarowany w następujący sposób:
public interface ICalculator
{
ValueTask<double> AddAsync(double a, double b, CancellationToken cancellationToken);
ValueTask<double> SubtractAsync(double a, double b, CancellationToken cancellationToken);
}
Chociaż implementacja metod w tym interfejsie może nie uzasadniać metody asynchronicznej, zawsze używamy sygnatur metod asynchronicznych w tym interfejsie, ponieważ ten interfejs służy do generowania serwera proxy klienta, który może wywołać tę usługę zdalnie, co z pewnością gwarantuje sygnaturę metody asynchronicznej.
Interfejs może deklarować zdarzenia, które mogą służyć do powiadamiania klientów o zdarzeniach występujących w usłudze.
Poza zdarzeniami lub wzorcem projektu obserwatora usługa brokerowana, która musi "wywołać z powrotem" do klienta, może zdefiniować drugi interfejs, który służy jako kontrakt, który klient musi zaimplementować i dostarczyć za pośrednictwem ServiceActivationOptions.ClientRpcTarget właściwości podczas żądania usługi. Taki interfejs powinien być zgodny ze wszystkimi tymi samymi wzorcami projektowymi i ograniczeniami co interfejs usługi obsługiwanej przez brokera, ale z dodanymi ograniczeniami dotyczącymi przechowywania wersji.
Zapoznaj się z najlepszymi rozwiązaniami dotyczącymi projektowania usługi brokera, aby uzyskać wskazówki dotyczące projektowania wydajnego, przyszłego interfejsu RPC.
Może być przydatne zadeklarowanie tego interfejsu w oddzielnym zestawie od zestawu, który implementuje usługę, aby jej klienci mogli odwoływać się do interfejsu bez konieczności uwidocznienia większej liczby szczegółów implementacji. Może być również przydatne dostarczenie zestawu interfejsu jako pakietu NuGet dla innych rozszerzeń do odwołania podczas rezerwowania własnego rozszerzenia w celu wysłania implementacji usługi.
Rozważ ukierunkowanie zestawu, który deklaruje interfejs usługi, aby netstandard2.0
upewnić się, że usługa może być łatwo wywoływana z dowolnego procesu platformy .NET, niezależnie od tego, czy jest uruchomiony program .NET Framework, .NET Core, .NET 5 lub nowszy.
Testowanie
Testy automatyczne powinny być zapisywane razem z interfejsem usługi, aby zweryfikować gotowość RPC interfejsu.
Testy powinny sprawdzić, czy wszystkie dane przekazywane przez interfejs można serializować.
Możesz znaleźć klasę BrokeredServiceContractTestBase<TInterface,TServiceMock> z pakietu Microsoft.VisualStudio.Sdk.TestFramework.Xunit przydatnego do uzyskania klasy testowej interfejsu. Ta klasa zawiera podstawowe testy konwencji dla interfejsu, metody ułatwiające typowe asercji, takie jak testowanie zdarzeń i nie tylko.
Metody
Twierdzenie, że każdy argument i wartość zwracana zostały całkowicie serializowane. Jeśli używasz powyższej klasy bazowej testowej, kod może wyglądać następująco:
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);
}
}
Rozważ przetestowanie rozpoznawania przeciążeń, jeśli deklarujesz wiele metod o tej samej nazwie.
Możesz dodać internal
pole do usługi makiety dla każdej metody, która przechowuje argumenty dla tej metody, aby metoda testowa mogła wywołać metodę, a następnie sprawdzić, czy właściwa metoda została wywołana z odpowiednimi argumentami.
Zdarzenia
Wszystkie zdarzenia zadeklarowane w interfejsie powinny być również testowane pod kątem gotowości RPC. Zdarzenia wywoływane z usługi brokera nie powodują niepowodzenia testu, jeśli kończą się niepowodzeniem podczas serializacji RPC, ponieważ zdarzenia są "uruchamiane i zapominane".
Jeśli używasz klasy bazowej testowej wymienionej powyżej, to zachowanie jest już wbudowane w niektóre metody pomocnicze i może wyglądać następująco (z niezmienionymi częściami pominiętymi dla zwięzłości):
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));
}
}
Implementowanie usługi
Klasa usługi powinna zaimplementować interfejs RPC zadeklarowany w poprzednim kroku. Usługa może implementować IDisposable lub inne interfejsy poza tym, które są używane do RPC. Serwer proxy wygenerowany na kliencie implementuje tylko interfejs usługi , IDisposablei prawdopodobnie kilka innych interfejsów wybranych do obsługi systemu, więc rzutowanie do innych interfejsów implementowanych przez usługę zakończy się niepowodzeniem na kliencie.
Rozważmy przykład kalkulatora użyty powyżej, który implementujemy tutaj:
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);
}
}
Ponieważ same jednostki metody nie muszą być asynchroniczne, jawnie opakowujemy wartość zwracaną w skonstruowanym typie zwracanym ValueTask<TResult> , aby był zgodny z interfejsem usługi.
Implementowanie obserwowalnego wzorca projektowego
Jeśli zaoferujesz subskrypcję obserwatora w interfejsie usługi, może to wyglądać następująco:
Task<IDisposable> SubscribeAsync(IObserver<YourDataType> observer);
IObserver<T> Argument zazwyczaj musi przetrwać okres istnienia tego wywołania metody, aby klient mógł nadal odbierać aktualizacje po zakończeniu wywołania metody do momentu usunięcia zwróconej IDisposable wartości przez klienta. Aby ułatwić to, klasa usługi może zawierać kolekcję IObserver<T> subskrypcji, które wszystkie aktualizacje wprowadzone w stanie będą następnie wyliczane w celu zaktualizowania wszystkich subskrybentów. Upewnij się, że wyliczenie kolekcji jest bezpieczne wątkowo w odniesieniu do siebie, a zwłaszcza z mutacjami w tej kolekcji, które mogą wystąpić za pośrednictwem dodatkowych subskrypcji lub dyspozycji tych subskrypcji.
Zadbaj o to, aby wszystkie aktualizacje publikowane za pośrednictwem OnNext zachowywały kolejność, w jakiej zmiany stanu zostały wprowadzone w usłudze.
Wszystkie subskrypcje powinny zostać ostatecznie zakończone wywołaniem lub OnCompleted OnError uniknąć przecieków zasobów w systemach klienta i RPC. Obejmuje to usuwanie usług, w których wszystkie pozostałe subskrypcje powinny zostać jawnie ukończone.
Dowiedz się więcej na temat wzorca projektowania obserwatora, sposobu implementowania widocznego dostawcy danych, a szczególnie z myślą o RPC.
Usługi jednorazowe
Klasa usługi nie jest wymagana do jednorazowego użytku, ale usługi, które zostaną usunięte, gdy klient usunie serwer proxy do usługi lub połączenie między klientem a usługą zostanie utracone. Interfejsy jednorazowe są testowane w tej kolejności: System.IAsyncDisposable, Microsoft.VisualStudio.Threading.IAsyncDisposable, IDisposable. Tylko pierwszy interfejs z tej listy, który implementuje klasa usługi, będzie używany do usuwania usługi.
Należy pamiętać o bezpieczeństwie wątków podczas rozważania usuwania. Metoda Dispose może być wywoływana w dowolnym wątku, podczas gdy inny kod w usłudze jest uruchomiony (na przykład jeśli połączenie zostało porzucone).
Zgłaszanie wyjątków
Podczas zgłaszania wyjątków rozważ zgłoszenie LocalRpcException za pomocą określonego kodu błędu , aby kontrolować kod błędu odebrany przez klienta w pliku RemoteInvocationException. Dostarczanie klientom kodu błędu może umożliwić ich rozgałęzianie na podstawie charakteru błędu lepiej niż analizowanie komunikatów wyjątków lub typów.
Zgodnie ze specyfikacją JSON-RPC kody błędów MUSZĄ być większe niż -32000, w tym liczby dodatnie.
Korzystanie z innych usług obsługiwanych przez brokera
Gdy sama usługa obsługiwana przez brokera wymaga dostępu do innej usługi obsługiwanej przez brokera, zalecamy użycie IServiceBroker tej usługi w swojej fabryce usług, ale jest to szczególnie ważne, gdy rejestracja usługi obsługiwanej przez brokera ustawia flagę AllowTransitiveGuestClients .
Aby spełnić te wytyczne, jeśli nasza usługa kalkulatora potrzebuje innych usług obsługiwanych przez brokera w celu zaimplementowania jego zachowania, zmodyfikujemy konstruktora, aby akceptował element IServiceBroker:
internal class Calculator : ICalculator
{
private readonly State state;
private readonly IServiceBroker serviceBroker;
internal class Calculator(State state, IServiceBroker serviceBroker)
{
this.state = state;
this.serviceBroker = serviceBroker;
}
// ...
}
Dowiedz się więcej na temat zabezpieczania usługi obsługiwanej przez brokera i korzystania z usług obsługiwanych przez brokera.
Usługi stanowe
Stan poszczególnych klientów
Dla każdego klienta, który żąda usługi, zostanie utworzone nowe wystąpienie tej klasy.
Pole w powyższej Calculator
klasie będzie przechowywać wartość, która może być unikatowa dla każdego klienta.
Załóżmy, że dodajemy licznik, który zwiększa się za każdym razem, gdy wykonywana jest operacja:
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);
}
}
Usługa brokera powinna być napisana w celu przestrzegania praktyk bezpiecznych wątkowo.
W przypadku korzystania z zalecanych ServiceJsonRpcDescriptorpołączeń zdalnych z klientami mogą obejmować współbieżne wykonywanie metod usługi zgodnie z opisem w tym dokumencie.
Gdy klient współudzieli proces i element AppDomain z usługą, klient może wywoływać usługę współbieżnie z wielu wątków.
Implementacja bezpieczna wątkowo w powyższym przykładzie może służyć Interlocked.Increment(Int32) do przyrostowania operationCounter
pola.
Stan udostępniony
Jeśli istnieje stan, że usługa będzie musiała współużytkować wszystkich swoich klientów, ten stan powinien być zdefiniowany w odrębnej klasie utworzonej przez pakiet VS i przekazany jako argument do konstruktora usługi.
Załóżmy, operationCounter
że chcemy, aby zdefiniowane powyżej liczyć wszystkie operacje we wszystkich klientach usługi.
Musimy podnieść pole do nowej klasy stanu:
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);
}
}
Teraz mamy elegancki, testowalny sposób zarządzania stanem udostępnionym w wielu wystąpieniach naszej Calculator
usługi.
Później podczas pisania kodu w celu profferowania usługi zobaczymy, jak ta State
klasa jest tworzona raz i udostępniana każdemu wystąpieniu Calculator
usługi.
Szczególnie ważne jest, aby zapewnić bezpieczeństwo wątków podczas pracy ze stanem udostępnionym, ponieważ nie można założyć, że nie można założyć wokół wielu klientów planowania ich wywołań, tak aby nigdy nie były wykonywane współbieżnie.
Jeśli udostępniona klasa stanu musi uzyskiwać dostęp do innych usług obsługiwanych przez brokera, powinna używać globalnego brokera usług, a nie jednego z kontekstowych przypisanych do pojedynczego wystąpienia usługi brokera. Użycie globalnego brokera usług w ramach usługi obsługiwanej przez brokera niesie ze sobą implikacje bezpieczeństwa po ustawieniu ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients flagi.
Kwestie dotyczące bezpieczeństwa
Zabezpieczenia są istotne dla usługi obsługiwanej przez brokera, jeśli jest ona zarejestrowana za ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients pomocą flagi, która uwidacznia dostęp innych użytkowników na innych maszynach uczestniczących w udostępnionej sesji live share.
Przed ustawieniem flagi AllowTransitiveGuestClients zapoznaj się z tematem Jak zabezpieczyć usługę brokerowaną i podjąć niezbędne środki zaradcze zabezpieczeń.
Moniker usługi
Usługa brokerowana musi mieć nazwę z możliwością serializacji i opcjonalną wersję, za pomocą której klient może zażądać usługi. Jest ServiceMoniker to wygodna otoka dla tych dwóch informacji.
Pseudonim usługi jest analogiczny do kwalifikowanej przez zestaw pełnej nazwy typu CLR (Common Language Runtime). Musi być globalnie unikatowa i dlatego powinna zawierać nazwę firmy, a być może nazwę rozszerzenia jako prefiksy samej nazwy usługi.
Może być przydatne zdefiniowanie tego monikera w polu do użycia w static readonly
innym miejscu:
public static readonly ServiceMoniker Moniker = new ServiceMoniker("YourCompany.Extension.Calculator", new Version("1.0"));
Chociaż większość zastosowań usługi może nie używać bezpośrednio twojego monikera, klient, który komunikuje się za pośrednictwem potoków zamiast serwera proxy, będzie wymagał moniker.
Chociaż wersja jest opcjonalna w moniker, udostępnienie wersji jest zalecane, ponieważ zapewnia autorom usług więcej opcji utrzymania zgodności z klientami w ramach zmian behawioralnych.
Deskryptor usługi
Deskryptor usługi łączy nazwę usługi z zachowaniami wymaganymi do uruchomienia połączenia RPC i utworzenia lokalnego lub zdalnego serwera proxy. Deskryptor jest odpowiedzialny za efektywne konwertowanie interfejsu RPC na protokół przewodowy. Ten deskryptor usługi jest wystąpieniem typu pochodnego ServiceRpcDescriptor. Deskryptor musi być udostępniany wszystkim klientom, którzy będą używać serwera proxy do uzyskiwania dostępu do tej usługi. Proffering the service również wymaga tego deskryptora.
Program Visual Studio definiuje jeden taki typ pochodny i zaleca użycie go dla wszystkich usług: ServiceJsonRpcDescriptor. Ten deskryptor korzysta StreamJsonRpc z połączeń RPC i tworzy lokalny serwer proxy o wysokiej wydajności dla usług lokalnych, które emulują niektóre zachowania zdalne, takie jak zawijanie wyjątków zgłaszanych przez usługę w programie RemoteInvocationException.
Program ServiceJsonRpcDescriptor obsługuje konfigurowanie JsonRpc klasy na potrzeby kodowania JSON lub MessagePack protokołu JSON-RPC. Zalecamy kodowanie MessagePack, ponieważ jest bardziej kompaktowe i może być 10X bardziej wydajne.
Możemy zdefiniować deskryptor dla naszej usługi kalkulatora w następujący sposób:
/// <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);
Jak widać powyżej, dostępny jest wybór ogranicznika i ogranicznika. Ponieważ nie wszystkie kombinacje są prawidłowe, zalecamy jedną z tych kombinacji:
ServiceJsonRpcDescriptor.Formatters | ServiceJsonRpcDescriptor.MessageDelimiters | Optymalne zastosowanie |
---|---|---|
MessagePack | BigEndianInt32LengthHeader | Wysoka wydajność |
UTF8 (JSON) | HttpLikeHeaders | Współdziałanie z innymi systemami JSON-RPC |
Określając MultiplexingStream.Options
obiekt jako końcowy parametr, połączenie RPC współużytkowane przez klienta i usługę jest tylko jednym kanałem w multiplexingStream, który jest współużytkowany z połączeniem JSON-RPC w celu umożliwienia wydajnego transferu dużych danych binarnych za pośrednictwem JSON-RPC.
Strategia ExceptionProcessing.ISerializable powoduje, że wyjątki zgłaszane z usługi są serializowane i zachowywane jako Exception.InnerException RemoteInvocationException zgłoszone na kliencie. Bez tego ustawienia na kliencie są dostępne mniej szczegółowe informacje o wyjątkach.
Porada: Uwidacznianie deskryptora jako ServiceRpcDescriptor zamiast dowolnego typu pochodnego, którego używasz jako szczegółów implementacji. Zapewnia to większą elastyczność zmiany szczegółów implementacji później bez zmian powodujących niezgodność interfejsu API.
Dołącz odwołanie do interfejsu usługi w komentarzu do dokumentu XML dla deskryptora, aby ułatwić użytkownikom korzystanie z usługi. Zapoznaj się również z interfejsem akceptowanym przez usługę jako element docelowy RPC klienta, jeśli ma to zastosowanie.
Niektóre bardziej zaawansowane usługi mogą również akceptować lub wymagać obiektu docelowego RPC od klienta zgodnego z interfejsem.
W takim przypadku należy użyć ServiceJsonRpcDescriptor konstruktora z parametrem Type clientInterface
, aby określić interfejs, z jakim klient powinien podać wystąpienie.
Przechowywanie wersji deskryptora
W czasie możesz zwiększać wersję usługi. W takim przypadku należy zdefiniować deskryptor dla każdej wersji, którą chcesz obsługiwać, przy użyciu unikatowej wersji specyficznej ServiceMoniker dla każdego z nich. Obsługa wielu wersji jednocześnie może być dobra dla zgodności z poprzednimi wersjami i zwykle może być wykonywana tylko za pomocą jednego interfejsu RPC.
Program Visual Studio jest zgodny z tą klasą, VisualStudioServices definiując oryginał ServiceRpcDescriptor jako właściwość w klasie zagnieżdżonej, która reprezentuje pierwszą wersję, która dodała tę usługę virtual
brokera.
Gdy musimy zmienić protokół przewodowy lub dodać/zmienić funkcjonalność usługi, program Visual Studio deklaruje override
właściwość w nowszej zagnieżdżonej klasie, która zwraca nowy ServiceRpcDescriptorelement .
W przypadku usługi zdefiniowanej i proffered przez rozszerzenie programu Visual Studio może wystarczyć zadeklarowanie innej właściwości deskryptora obok oryginalnej. Załóżmy na przykład, że usługa 1.0 korzystała z formatowania UTF8 (JSON) i zdajesz sobie sprawę, że przejście na pakiet MessagePack przyniesie znaczną korzyść z wydajności. Ponieważ zmiana formatera jest zmianą powodującą niezgodność protokołu przewodowego, wymaga przyrostowego numeru wersji usługi brokera i drugiego deskryptora. Dwa deskryptory mogą wyglądać następująco:
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 });
Mimo że deklarujemy dwa deskryptory (a później będziemy musieli wywnioskować i zarejestrować dwie usługi), że możemy to zrobić za pomocą tylko jednego interfejsu usługi i implementacji, utrzymując obciążenie związane z obsługą wielu wersji usług dość niskie.
Proffering the service (Proffering the service)
Usługa brokera musi zostać utworzona, gdy pojawi się żądanie, które jest rozmieszczane za pośrednictwem kroku o nazwie proffering the service.
Fabryka usług
Użyj polecenia GlobalProvider,GetServiceAsync aby zażądać pliku SVsBrokeredServiceContainer. Następnie wywołaj ten kontener, aby wywnioskować IBrokeredServiceContainer.Proffer usługę.
W poniższym przykładzie profferujemy usługę przy użyciu CalculatorService
pola zadeklarowanego wcześniej, które jest ustawione na wystąpienie klasy ServiceRpcDescriptor.
Przekazujemy ją do naszej fabryki usług, która jest pełnomocnikiem BrokeredServiceFactory .
IBrokeredServiceContainer container = await AsyncServiceProvider.GlobalProvider.GetServiceAsync<SVsBrokeredServiceContainer, IBrokeredServiceContainer>();
container.Proffer(
CalculatorService,
(moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(new CalculatorService()));
Usługa brokerowana jest zwykle tworzone raz na klienta. Jest to odejście od innych usług VS (Visual Studio), które są zwykle tworzone raz i współużytkowane przez wszystkich klientów. Utworzenie jednego wystąpienia usługi na klienta pozwala na lepsze zabezpieczenia, ponieważ każda usługa i/lub jego połączenie może zachować stan poszczególnych klientów na temat poziomu autoryzacji, na którym działa klient, jakie jest ich preferowane CultureInfo itp. Jak zobaczymy dalej, umożliwia również bardziej interesujące usługi, które akceptują argumenty specyficzne dla tego żądania.
Ważne
Fabryka usług, która odbiega od tych wytycznych i zwraca wystąpienie usługi udostępnionej zamiast nowego do każdego klienta, nigdy nie powinno mieć usługi implementowania IDisposable, ponieważ pierwszy klient do usunięcia serwera proxy doprowadzi do usunięcia wystąpienia usługi udostępnionej, zanim inni klienci będą z niego korzystać.
W bardziej zaawansowanym przypadku, w którym CalculatorService
konstruktor wymaga współużytkowanego obiektu stanu i IServiceBrokerobiektu , możemy wyłusić fabrykę w następujący sposób:
var state = new CalculatorService.State();
container.Proffer(
CalculatorService,
(moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(new CalculatorService(state, serviceBroker)));
Zmienna lokalna state
znajduje się poza fabryką usług i dlatego jest tworzona tylko raz i współużytkowana we wszystkich wystąpieniach usług.
Jeszcze bardziej zaawansowane, jeśli usługa wymagała dostępu do ServiceActivationOptions obiektu (na przykład do wywoływania metod na obiekcie docelowym RPC klienta), które można również przekazać:
var state = new CalculatorService.State();
container.Proffer(CalculatorService, (moniker, options, serviceBroker, cancellationToken) =>
new ValueTask<object?>(new CalculatorService(state, serviceBroker, options)));
W takim przypadku konstruktor usługi może wyglądać następująco, zakładając ServiceJsonRpcDescriptor , że element został utworzony przy typeof(IClientCallbackInterface)
użyciu jako jeden z argumentów konstruktora:
internal class Calculator(State state, IServiceBroker serviceBroker, ServiceActivationOptions options)
{
this.state = state;
this.serviceBroker = serviceBroker;
this.options = options;
this.clientCallback = (IClientCallbackInterface)options.ClientRpcTarget;
}
To clientCallback
pole można teraz wywołać za każdym razem, gdy usługa chce wywołać klienta, dopóki połączenie nie zostanie usunięte.
Delegat BrokeredServiceFactory przyjmuje ServiceMoniker jako parametr w przypadku, gdy fabryka usług jest udostępnioną metodą, która tworzy wiele usług lub odrębnych wersji usługi na podstawie moniker. Ten pseudonim pochodzi od klienta i zawiera wersję oczekiwanej przez nich usługi. Przekazując ten moniker do konstruktora usługi, usługa może emulować dziwaczne zachowanie określonych wersji usługi, aby dopasować się do tego, czego może oczekiwać klient.
Unikaj używania delegata AuthorizingBrokeredServiceFactory z IBrokeredServiceContainer.Proffer metodą , chyba że użyjesz klasy usługi obsługiwanej IAuthorizationService przez brokera. Aby uniknąć przecieku pamięci, należy go IAuthorizationService usunąć z klasy usługi obsługiwanej przez brokera.
Obsługa wielu wersji usługi
Gdy zwiększasz wersję w systemie ServiceMoniker, musisz wywnioskować każdą wersję usługi obsługiwanej przez brokera, dla której zamierzasz odpowiadać na żądania klientów. Jest to wykonywane przez wywołanie IBrokeredServiceContainer.Proffer metody z każdym ServiceRpcDescriptor , który nadal obsługujesz.
Proffering your service with a null
version will serve as a "catch all", który będzie zgodny z każdym żądaniem klienta, dla którego dokładna wersja jest zgodna z zarejestrowaną usługą nie istnieje.
Na przykład możesz przeprowadzić proffering usługi 1.0 i 1.1 z określonymi wersjami, a także zarejestrować usługę null
w wersji.
W takich przypadkach klienci żądający usługi z wersją 1.0 lub 1.1 wywołują fabrykę usług, którą proffered wykonano dla tych dokładnych wersji, podczas gdy klient żądający wersji 8.0 prowadzi do wywoływanej fabryki usług w wersji null.
Ponieważ żądana wersja klienta jest dostarczana do fabryki usług, fabryka może następnie podjąć decyzję o tym, jak skonfigurować usługę dla tego konkretnego klienta lub czy powrócić null
do oznaczania nieobsługiwanej wersji.
Żądanie klienta dla usługi z wersją null
jest zgodne tylko w usłudze zarejestrowanej i proffered z wersją null
.
Rozważmy przypadek, w którym opublikowano wiele wersji usługi, z których kilka jest zgodnych z poprzednimi wersjami i w związku z tym może udostępniać implementację usługi. Możemy użyć opcji catch-all, aby uniknąć konieczności wielokrotnego profferowania poszczególnych wersji w następujący sposób:
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.
}));
Rejestrowanie usługi
Proffering a brokered service to the global brokered service container will throw, chyba że usługa została po raz pierwszy zarejestrowana. Rejestracja zapewnia kontenerowi możliwość wcześniejszego poznania, które usługi obsługiwane przez brokera mogą być dostępne i które pakiety PROGRAMU VS mają zostać załadowane, gdy są wymagane w celu wykonania kodu profferingu. Dzięki temu program Visual Studio może szybko uruchomić się bez konieczności wcześniejszego ładowania wszystkich rozszerzeń, ale może załadować wymagane rozszerzenie, gdy jest wymagane przez klienta usługi obsługiwanej przez brokera.
Rejestrację można wykonać, stosując element ProvideBrokeredServiceAttribute do AsyncPackageklasy -pochodnej. Jest to jedyne miejsce, w którym ServiceAudience można ustawić.
[ProvideBrokeredService("YourCompany.Extension.Calculator", "1.0", Audience = ServiceAudience.Local)]
Wartość domyślna Audience to ServiceAudience.Process, która uwidacznia usługę brokera tylko w innym kodzie w ramach tego samego procesu. ServiceAudience.LocalUstawiając opcję , wyrażasz zgodę na uwidacznianie usługi brokera innym procesom należącym do tej samej sesji programu Visual Studio.
Jeśli usługa obsługiwana przez brokera musi być widoczna dla gości live share, Audience właściwość musi zawierać ServiceAudience.LiveShareGuest wartość i właściwość ustawioną ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients na true
.
Ustawienie tych flag może powodować poważne luki w zabezpieczeniach i nie powinno być wykonywane bez uprzedniej zgodności ze wskazówkami w temacie Jak zabezpieczyć usługę brokera.
Gdy zwiększasz wersję w systemie ServiceMoniker, musisz zarejestrować każdą wersję usługi obsługiwanej przez brokera, dla której zamierzasz odpowiadać na żądania klientów. Obsługa więcej niż najnowszej wersji usługi obsługiwanej przez brokera pomaga zachować zgodność z poprzednimi wersjami dla klientów starszej wersji usługi brokera, co może być szczególnie przydatne w przypadku scenariusza live share, w którym każda wersja programu Visual Studio, która udostępnia sesję, może być inną wersją.
Zarejestrowanie usługi przy null
użyciu wersji będzie służyć jako "catch all", który będzie zgodny z każdym żądaniem klienta, dla którego dokładna wersja z zarejestrowaną usługą nie istnieje.
Możesz na przykład zarejestrować usługę 1.0 i 2.0 w określonych wersjach, a także zarejestrować usługę null
w wersji.
Używanie mef do proffer i rejestrowania usługi
Wymaga to programu Visual Studio 2022 Update 2 lub nowszego.
Usługę brokera można wyeksportować za pośrednictwem mef zamiast używać pakietu programu Visual Studio zgodnie z opisem w dwóch poprzednich sekcjach. Ma to kompromisy, które należy wziąć pod uwagę:
Kompromis | Package proffer | Eksport MEF |
---|---|---|
Dostępność | ✅ Usługa brokerowana jest dostępna natychmiast podczas uruchamiania programu VS. | ⚠✔ Usługa brokera może być opóźniona w dostępności do czasu zainicjowania MEF w procesie. Zwykle jest to szybkie, ale może potrwać kilka sekund, gdy pamięć podręczna MEF jest nieaktualna. |
Gotowość międzyplatformowa | ⚠✔ Program Visual Studio dla określonego kodu systemu Windows musi być utworzony. | ✅Usługa brokera w zestawie może zostać załadowana w programie Visual Studio dla systemu Windows, a także Visual Studio dla komputerów Mac. |
Aby wyeksportować usługę brokera za pośrednictwem protokołu MEF zamiast używać pakietów PROGRAMU VS:
- Upewnij się, że nie masz kodu związanego z dwoma ostatnimi sekcjami. W szczególności nie powinien istnieć kod wywołujący IBrokeredServiceContainer.Proffer element i nie powinien stosować ProvideBrokeredServiceAttribute elementu do pakietu (jeśli istnieje).
- Zaimplementuj
IExportedBrokeredService
interfejs w klasie usługi obsługiwanej przez brokera. - Unikaj wszelkich zależności wątków głównych w konstruktorze lub importowaniu zestawów właściwości.
IExportedBrokeredService.InitializeAsync
Użyj metody inicjowania usługi obsługiwanej przez brokera, gdzie dozwolone są zależności wątku głównego. - Zastosuj element
ExportBrokeredServiceAttribute
do klasy usługi obsługiwanej przez brokera, określając informacje dotyczące nazwy usługi, odbiorców i wszelkich innych wymaganych informacji związanych z rejestracją. - Jeśli klasa wymaga usunięcia, zaimplementuj IDisposable , a nie IAsyncDisposable ponieważ MEF jest właścicielem okresu istnienia usługi i obsługuje tylko synchroniczne usuwanie.
- Upewnij się, że plik
source.extension.vsixmanifest
zawiera listę projektów zawierających usługę brokera jako zestaw MEF.
W ramach mef Twoja usługa brokerowana może importować dowolną inną część MEF w zakresie domyślnym.
W tym celu pamiętaj, aby użyć System.ComponentModel.Composition.ImportAttribute zamiast System.Composition.ImportAttribute.
Jest to spowodowane tym, że ExportBrokeredServiceAttribute
wymagane jest użycie System.ComponentModel.Composition.ExportAttribute tej samej przestrzeni nazw MEF i używanie jej w całym typie.
Usługa brokerowana jest unikatowa w celu importowania kilku specjalnych eksportów:
- IServiceBroker, które powinny być używane do uzyskiwania innych usług obsługiwanych przez brokera.
- ServiceMoniker, co może być przydatne podczas eksportowania wielu wersji usługi brokera i konieczności wykrycia, której wersji zażądał klient.
- ServiceActivationOptions, co może być przydatne, gdy klienci muszą podać specjalne parametry lub cel wywołania zwrotnego klienta.
- AuthorizationServiceClient, co może być przydatne, gdy konieczne jest przeprowadzenie kontroli zabezpieczeń zgodnie z opisem w temacie Jak zabezpieczyć usługę brokera. Ten obiekt nie musi być usuwany przez klasę, ponieważ zostanie on usunięty automatycznie po usunięciu usługi brokera.
Twoja usługa brokera nie może korzystać z usług MEF ImportAttribute do uzyskiwania innych usług obsługiwanych przez brokera.
Zamiast tego usługa może [Import]
IServiceBroker wykonywać zapytania dotyczące usług obsługiwanych przez brokera w tradycyjny sposób.
Dowiedz się więcej w temacie Jak korzystać z usługi obsługiwanej przez brokera.
Oto przykład:
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);
}
}
Eksportowanie wielu wersji usługi obsługiwanej przez brokera
Można ExportBrokeredServiceAttribute
go wielokrotnie stosować do usługi obsługiwanej przez brokera w celu zaoferowania wielu wersji usługi obsługiwanej przez brokera.
Implementacja IExportedBrokeredService.Descriptor
właściwości powinna zwrócić deskryptor z pseudonimem zgodnym z tym, którego zażądał klient.
Rozważmy ten przykład, w którym usługa kalkulatora wyeksportowała 1.0 z formatowaniem UTF8, a następnie dodaje eksport 1.1, aby cieszyć się wydajnością przy użyciu formatowania MessagePack.
[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!;
}
Począwszy od programu Visual Studio 2022 Update 12 (17.12), null
można wyeksportować wersję usługi w celu dopasowania do dowolnego żądania klienta dla usługi niezależnie od wersji, w tym żądania z wersją null
.
Taka usługa może wrócić null
z Descriptor
właściwości, aby odrzucić żądanie klienta, gdy nie oferuje implementacji wersji żądanej przez klienta.
Odrzucanie żądania obsługi
Usługa obsługiwana przez brokera może odrzucić żądanie aktywacji klienta, zgłaszając InitializeAsync z metody . Rzutowanie powoduje ServiceActivationFailedException , że element zostanie odesłany do klienta.