Fornire un servizio negoziato
Un servizio negoziato è costituito dagli elementi seguenti:
- Interfaccia che dichiara la funzionalità del servizio e funge da contratto tra il servizio e i relativi client.
- Implementazione di tale interfaccia.
- Moniker del servizio per assegnare un nome e una versione al servizio.
- Descrittore che combina il moniker del servizio con il comportamento per la gestione di RPC (Remote Procedure Call) quando necessario.
- Eseguire il proffering della factory del servizio e registrare il servizio negoziato con un pacchetto DI Visual Studio oppure eseguire entrambe le operazioni con MEF (Managed Extensibility Framework).
Ognuno degli elementi nell'elenco precedente è descritto in dettaglio nelle sezioni seguenti.
Con tutto il codice in questo articolo, è consigliabile attivare la funzionalità dei tipi di riferimento nullable di C#.
Interfaccia del servizio
L'interfaccia del servizio può essere un'interfaccia .NET standard (spesso scritta in C#), ma deve essere conforme alle linee guida impostate dal tipo derivato da ServiceRpcDescriptorche il servizio userà per garantire che l'interfaccia possa essere usata su RPC quando il client e il servizio vengono eseguiti in processi diversi.
Queste restrizioni includono in genere che le proprietà e gli indicizzatori non sono consentiti e la maggior parte o tutti i metodi restituiscono Task
o un altro tipo restituito compatibile con asincrona.
ServiceJsonRpcDescriptor è il tipo derivato consigliato per i servizi negoziati. Questa classe usa la StreamJsonRpc libreria quando il client e il servizio richiedono la comunicazione RPC. StreamJsonRpc applica alcune restrizioni all'interfaccia del servizio, come descritto qui.
L'interfaccia può derivare da IDisposable, System.IAsyncDisposableo anche se Microsoft.VisualStudio.Threading.IAsyncDisposable non è richiesta dal sistema. I proxy client generati implementeranno IDisposable entrambi i modi.
Un'interfaccia del servizio calcolatrice semplice può essere dichiarata come segue:
public interface ICalculator
{
ValueTask<double> AddAsync(double a, double b, CancellationToken cancellationToken);
ValueTask<double> SubtractAsync(double a, double b, CancellationToken cancellationToken);
}
Anche se l'implementazione dei metodi in questa interfaccia potrebbe non giustificare un metodo asincrono, usiamo sempre firme del metodo asincrono su questa interfaccia perché questa interfaccia viene usata per generare il proxy client che può richiamare questo servizio in remoto, che certamente garantisce una firma del metodo asincrono.
Un'interfaccia può dichiarare eventi che possono essere usati per notificare ai client eventi che si verificano nel servizio.
Oltre agli eventi o al modello di progettazione osservatore, un servizio negoziato che deve "richiamare" al client può definire una seconda interfaccia che funge da contratto che un client deve implementare e fornire tramite la proprietà durante la ServiceActivationOptions.ClientRpcTarget richiesta del servizio. Tale interfaccia deve essere conforme a tutti gli stessi modelli di progettazione e restrizioni dell'interfaccia del servizio negoziata, ma con restrizioni aggiunte al controllo delle versioni.
Vedere Procedure consigliate per la progettazione di un servizio negoziato per suggerimenti sulla progettazione di un'interfaccia RPC a prova di futuro efficiente.
Può essere utile dichiarare questa interfaccia in un assembly distinto dall'assembly che implementa il servizio in modo che i client possano fare riferimento all'interfaccia senza che il servizio esponga altri dettagli di implementazione. Può anche essere utile spedire l'assembly di interfaccia come pacchetto NuGet per altre estensioni a cui fare riferimento durante la prenotazione dell'estensione personalizzata per la spedizione dell'implementazione del servizio.
Prendere in considerazione la destinazione dell'assembly che dichiara l'interfaccia del servizio per netstandard2.0
assicurarsi che il servizio possa essere richiamato facilmente da qualsiasi processo .NET, indipendentemente dal fatto che esegua .NET Framework, .NET Core, .NET 5 o versione successiva.
Test
I test automatizzati devono essere scritti insieme all'interfaccia del servizio per verificare l'idoneità RPC dell'interfaccia.
I test devono verificare che tutti i dati passati attraverso l'interfaccia siano serializzabili.
È possibile trovare la BrokeredServiceContractTestBase<TInterface,TServiceMock> classe dal pacchetto Microsoft.VisualStudio.Sdk.TestFramework.Xunit utile per derivare la classe di test dell'interfaccia da . Questa classe include alcuni test di convenzione di base per l'interfaccia, metodi per facilitare l'uso di asserzioni comuni, ad esempio test di eventi e altro ancora.
Metodi
Asserire che ogni argomento e il valore restituito siano stati serializzati completamente. Se si usa la classe di base di test menzionata in precedenza, il codice potrebbe essere simile al seguente:
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);
}
}
Valutare la possibilità di testare la risoluzione dell'overload se si dichiarano più metodi con lo stesso nome.
È possibile aggiungere un internal
campo al servizio fittizio per ogni metodo che archivia gli argomenti per tale metodo in modo che il metodo test possa chiamare il metodo e quindi verificare che il metodo corretto sia stato richiamato con gli argomenti corretti.
Eventi
Tutti gli eventi dichiarati nell'interfaccia devono essere testati anche per la conformità RPC. Gli eventi generati da un servizio negoziato non causano un errore di test se hanno esito negativo durante la serializzazione RPC perché gli eventi sono "attivano e dimenticano".
Se si usa la classe di base di test menzionata in precedenza, questo comportamento è già integrato in alcuni metodi helper e potrebbe essere simile al seguente (con parti non modificate omesse per brevità):
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));
}
}
Implementazione del servizio
La classe del servizio deve implementare l'interfaccia RPC dichiarata nel passaggio precedente. Un servizio può implementare IDisposable o qualsiasi altra interfaccia oltre quella usata per RPC. Il proxy generato nel client implementa solo l'interfaccia del servizio, IDisposablee possibilmente alcune altre interfacce selezionate per supportare il sistema, quindi un cast ad altre interfacce implementate dal servizio avrà esito negativo nel client.
Si consideri l'esempio di calcolatrice usato in precedenza, che verrà implementato qui:
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);
}
}
Poiché i corpi del metodo stessi non devono essere asincroni, viene eseguito il wrapping esplicito del valore restituito in un tipo restituito costruito ValueTask<TResult> per essere conforme all'interfaccia del servizio.
Implementazione del modello di progettazione osservabile
Se si offre una sottoscrizione observer nell'interfaccia del servizio, potrebbe essere simile alla seguente:
Task<IDisposable> SubscribeAsync(IObserver<YourDataType> observer);
L'argomento IObserver<T> in genere dovrà durare la durata di questa chiamata al metodo in modo che il client possa continuare a ricevere gli aggiornamenti al termine della chiamata al metodo finché il client non elimina il valore restituito IDisposable . Per facilitare questa classe di servizio può includere una raccolta di IObserver<T> sottoscrizioni che tutti gli aggiornamenti apportati allo stato verranno quindi enumerati per aggiornare tutti i sottoscrittori. Assicurarsi che l'enumerazione della raccolta sia thread-safe rispetto l'una all'altra e soprattutto con le mutazioni su tale raccolta che possono verificarsi tramite sottoscrizioni aggiuntive o cessioni di tali sottoscrizioni.
Prestare attenzione che tutti gli aggiornamenti pubblicati tramite OnNext conservano l'ordine in cui sono state introdotte le modifiche di stato al servizio.
Tutte le sottoscrizioni devono essere terminate con una chiamata a OnCompleted o OnError per evitare perdite di risorse nei sistemi client e RPC. Ciò include l'eliminazione del servizio in cui tutte le sottoscrizioni rimanenti devono essere completate in modo esplicito.
Altre informazioni sul modello di progettazione osservatore, su come implementare un provider di dati osservabile e in particolare con RPC.
Servizi eliminabili
La classe di servizio non è necessaria per essere eliminabile, ma i servizi che verranno eliminati quando il client elimina il proxy al servizio o la connessione tra client e servizio viene persa. Le interfacce eliminabili vengono testate in questo ordine: System.IAsyncDisposable, Microsoft.VisualStudio.Threading.IAsyncDisposable, IDisposable. Solo la prima interfaccia di questo elenco implementata dalla classe di servizio verrà usata per eliminare il servizio.
Tenere presente la thread-safety quando si considera lo smaltimento. Il Dispose metodo può essere chiamato su qualsiasi thread mentre è in esecuzione un altro codice nel servizio( ad esempio, se viene eliminata una connessione).
Generazione di eccezioni
Quando si generano eccezioni, è consigliabile LocalRpcException generare un codice ErrorCode specifico per controllare il codice di errore ricevuto dal client in RemoteInvocationException. Fornire ai client un codice di errore può abilitarli per la creazione di rami in base alla natura dell'errore in modo migliore rispetto all'analisi dei messaggi o dei tipi di eccezione.
In base alla specifica JSON-RPC, i codici di errore DEVONO essere maggiori di -32000, inclusi i numeri positivi.
Utilizzo di altri servizi negoziati
Quando un servizio negoziato richiede l'accesso a un altro servizio negoziato, è consigliabile usare l'oggetto IServiceBroker fornito alla relativa factory di servizio, ma è particolarmente importante quando la registrazione del servizio negoziata imposta il AllowTransitiveGuestClients flag.
Per conformarsi a questa linea guida se il servizio calcolatrice aveva bisogno di altri servizi negoziati per implementarne il comportamento, è necessario modificare il costruttore per accettare un oggetto 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;
}
// ...
}
Altre informazioni su come proteggere un servizio negoziato e usare servizi negoziati.
Servizi con stato
Stato per client
Verrà creata una nuova istanza di questa classe per ogni client che richiede il servizio.
Un campo della Calculator
classe precedente archivierebbe un valore che potrebbe essere univoco per ogni client.
Si supponga di aggiungere un contatore che incrementa ogni volta che viene eseguita un'operazione:
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);
}
}
Il servizio negoziato deve essere scritto per seguire le procedure thread-safe.
Quando si usa , ServiceJsonRpcDescriptorle connessioni remote con i client possono includere l'esecuzione simultanea dei metodi del servizio, come descritto in questo documento.
Quando il client condivide un processo e AppDomain con il servizio, il client potrebbe chiamare il servizio contemporaneamente da più thread.
Un'implementazione thread-safe dell'esempio precedente può essere usata Interlocked.Increment(Int32) per incrementare il operationCounter
campo.
Stato condiviso
Se è presente uno stato che il servizio dovrà condividere tra tutti i client, questo stato deve essere definito in una classe distinta di cui è stata creata un'istanza dal pacchetto di Visual Studio e passata come argomento al costruttore del servizio.
Si supponga di voler operationCounter
contare tutte le operazioni definite in precedenza in tutti i client del servizio.
Sarebbe necessario sollevare il campo in questa nuova classe di stato:
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);
}
}
Ora è disponibile un modo elegante e testabile per gestire lo stato condiviso tra più istanze del Calculator
servizio.
Successivamente, quando si scrive il codice per il servizio, si vedrà come questa State
classe viene creata una sola volta e condivisa con ogni istanza del Calculator
servizio.
È particolarmente importante essere thread-safe quando si tratta di stato condiviso perché non è possibile fare ipotesi su più client che pianificano le chiamate in modo che non vengano mai effettuate simultaneamente.
Se la classe di stato condivisa deve accedere ad altri servizi negoziati, deve usare il service broker globale anziché uno dei servizi contestuali assegnati a una singola istanza del servizio negoziato. L'uso del service broker globale all'interno di un servizio negoziato comporta implicazioni per la sicurezza quando viene impostato il ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients flag.
Problemi di sicurezza
La sicurezza è una considerazione per il servizio negoziato se è registrato con il ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients flag , che lo espone ad altri utenti in altri computer che partecipano a una sessione di Live Share condivisa.
Vedere Come proteggere un servizio broker e adottare le mitigazioni di sicurezza necessarie prima di impostare il AllowTransitiveGuestClients flag.
Moniker del servizio
Un servizio negoziato deve avere un nome serializzabile e una versione facoltativa in base alla quale un client può richiedere il servizio. Un ServiceMoniker è un wrapper pratico per queste due informazioni.
Un moniker del servizio è analogo al nome completo completo dell'assembly di un tipo CLR (Common Language Runtime). Deve essere univoco a livello globale e deve quindi includere il nome della società e forse il nome dell'estensione come prefissi al nome del servizio stesso.
Può essere utile definire questo moniker in un static readonly
campo da usare altrove:
public static readonly ServiceMoniker Moniker = new ServiceMoniker("YourCompany.Extension.Calculator", new Version("1.0"));
Anche se la maggior parte degli usi del servizio potrebbe non usare direttamente il moniker, un client che comunica tramite pipe anziché un proxy richiederà il moniker.
Anche se una versione è facoltativa in un moniker, è consigliabile fornire una versione perché offre agli autori del servizio più opzioni per mantenere la compatibilità con i client nelle modifiche comportamentali.
Descrittore del servizio
Il descrittore del servizio combina il moniker del servizio con i comportamenti necessari per eseguire una connessione RPC e creare un proxy locale o remoto. Il descrittore è responsabile della conversione efficace dell'interfaccia RPC in un protocollo wire. Questo descrittore del servizio è un'istanza di un ServiceRpcDescriptortipo derivato da . Il descrittore deve essere reso disponibile a tutti i client che useranno un proxy per accedere a questo servizio. Per il proffering del servizio è necessario anche questo descrittore.
Visual Studio definisce un tipo derivato di questo tipo e ne consiglia l'uso per tutti i servizi: ServiceJsonRpcDescriptor. Questo descrittore usa StreamJsonRpc per le connessioni RPC e crea un proxy locale ad alte prestazioni per i servizi locali che emula alcuni dei comportamenti remoti, ad esempio il wrapping di eccezioni generate dal servizio in RemoteInvocationException.
ServiceJsonRpcDescriptor supporta la configurazione della classe per la JsonRpc codifica JSON o MessagePack del protocollo JSON-RPC. È consigliabile codifica MessagePack perché è più compatta e può essere 10X più efficiente.
È possibile definire un descrittore per il servizio calcolatrice come segue:
/// <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);
Come si può notare in precedenza, è disponibile una scelta di formattatore e delimitatore. Poiché non tutte le combinazioni sono valide, è consigliabile usare una di queste combinazioni:
ServiceJsonRpcDescriptor.Formatters | ServiceJsonRpcDescriptor.MessageDelimiters | Ideale per |
---|---|---|
MessagePack | BigEndianInt32LengthHeader | Prestazioni elevate |
UTF8 (JSON) | HttpLikeHeaders | Interoperabilità con altri sistemi JSON-RPC |
Specificando l'oggetto MultiplexingStream.Options
come parametro finale, la connessione RPC condivisa tra client e servizio è solo un canale su multiplexingStream, condiviso con la connessione JSON-RPC per consentire il trasferimento efficiente di dati binari di grandi dimensioni tramite JSON-RPC.
La ExceptionProcessing.ISerializable strategia fa sì che le eccezioni generate dal servizio vengano serializzate e mantenute come all'eccezione Exception.InnerExceptionRemoteInvocationException generata nel client. Senza questa impostazione, nel client sono disponibili informazioni meno dettagliate sulle eccezioni.
Suggerimento: esporre il descrittore come ServiceRpcDescriptor anziché qualsiasi tipo derivato usato come dettaglio di implementazione. Ciò offre maggiore flessibilità per modificare i dettagli di implementazione in un secondo momento senza modifiche di rilievo dell'API.
Includere un riferimento all'interfaccia del servizio nel commento del documento xml sul descrittore per semplificare l'utilizzo del servizio da parte degli utenti. Fare riferimento anche all'interfaccia accettata dal servizio come destinazione RPC client, se applicabile.
Alcuni servizi più avanzati possono anche accettare o richiedere un oggetto di destinazione RPC dal client conforme ad alcune interfacce.
Per questo caso, usare un ServiceJsonRpcDescriptor costruttore con un Type clientInterface
parametro per specificare l'interfaccia di cui il client deve fornire un'istanza.
Controllo delle versioni del descrittore
Nel corso del tempo è possibile incrementare la versione del servizio. In questo caso è necessario definire un descrittore per ogni versione che si desidera supportare, usando una versione univoca specifica ServiceMoniker per ogni versione. Il supporto simultaneo di più versioni può essere utile per la compatibilità con le versioni precedenti e in genere può essere eseguito con una sola interfaccia RPC.
Visual Studio segue questo modello con la relativa VisualStudioServices classe definendo l'originale ServiceRpcDescriptor come virtual
proprietà nella classe annidata che rappresenta la prima versione che ha aggiunto il servizio negoziato.
Quando è necessario modificare il protocollo wire o aggiungere/modificare la funzionalità del servizio, Visual Studio dichiara una override
proprietà in una classe annidata con versione successiva che restituisce un nuovo ServiceRpcDescriptoroggetto .
Per un servizio definito e profferato da un'estensione di Visual Studio, può essere sufficiente dichiarare un'altra proprietà descrittore accanto all'originale. Si supponga, ad esempio, che il servizio 1.0 abbia usato il formattatore UTF8 (JSON) e ci si rende conto che il passaggio a MessagePack offre un notevole vantaggio in termini di prestazioni. Poiché la modifica del formattatore è una modifica che causa un'interruzione del protocollo wire, richiede l'incremento del numero di versione del servizio negoziato e un secondo descrittore. I due descrittori insieme potrebbero avere un aspetto simile al seguente:
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 });
Anche se si dichiarano due descrittori (e versioni successive sarà necessario eseguire il proffering e registrare due servizi) che è possibile eseguire questa operazione con una sola interfaccia del servizio e implementazione, mantenendo il sovraccarico per supportare più versioni del servizio piuttosto basso.
Proffering del servizio
Il servizio negoziato deve essere creato quando arriva una richiesta, che viene disposta tramite un passaggio denominato proffering del servizio.
Factory del servizio
Utilizzare GlobalProvider.GetServiceAsync per richiedere .SVsBrokeredServiceContainer Chiamare IBrokeredServiceContainer.Proffer quindi su tale contenitore per eseguire il proffering del servizio.
Nell'esempio seguente viene eseguito il proffering di un servizio usando il CalculatorService
campo dichiarato in precedenza, che viene impostato su un'istanza di un oggetto ServiceRpcDescriptor.
Viene passata la factory di servizio, che è un BrokeredServiceFactory delegato.
IBrokeredServiceContainer container = await AsyncServiceProvider.GlobalProvider.GetServiceAsync<SVsBrokeredServiceContainer, IBrokeredServiceContainer>();
container.Proffer(
CalculatorService,
(moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(new CalculatorService()));
In genere viene creata un'istanza di un servizio negoziato una volta per ogni client. Si tratta di una partenza da altri servizi di Visual Studio (Visual Studio), che in genere vengono create un'istanza una sola volta e condivisi tra tutti i client. La creazione di un'istanza del servizio per ogni client consente una maggiore sicurezza perché ogni servizio e/o la relativa connessione può mantenere lo stato per client sul livello di autorizzazione a cui opera il client, qual è il valore preferito CultureInfo e così via. Come vedremo di seguito, consente anche servizi più interessanti che accettano argomenti specifici di questa richiesta.
Importante
Una service factory che devia da questa linea guida e restituisce un'istanza del servizio condiviso invece di una nuova a ogni client non deve mai implementare IDisposableil servizio , poiché il primo client da eliminare del proxy porterà all'eliminazione dell'istanza del servizio condiviso prima che altri client lo usino.
Nel caso più avanzato in cui il CalculatorService
costruttore richiede un oggetto stato condiviso e un IServiceBrokeroggetto , è possibile eseguire il proffer della factory come segue:
var state = new CalculatorService.State();
container.Proffer(
CalculatorService,
(moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(new CalculatorService(state, serviceBroker)));
La state
variabile locale si trova all'esterno della factory del servizio e quindi viene creata una sola volta e condivisa in tutti i servizi di cui è stata creata un'istanza.
Ancora più avanzata, se il servizio ha richiesto l'accesso a ServiceActivationOptions , ad esempio per richiamare i metodi nell'oggetto di destinazione RPC client, che potrebbe essere passato anche:
var state = new CalculatorService.State();
container.Proffer(CalculatorService, (moniker, options, serviceBroker, cancellationToken) =>
new ValueTask<object?>(new CalculatorService(state, serviceBroker, options)));
In questo caso il costruttore del servizio potrebbe essere simile al seguente, presupponendo che ServiceJsonRpcDescriptor sia stato creato con typeof(IClientCallbackInterface)
come uno degli argomenti del costruttore:
internal class Calculator(State state, IServiceBroker serviceBroker, ServiceActivationOptions options)
{
this.state = state;
this.serviceBroker = serviceBroker;
this.options = options;
this.clientCallback = (IClientCallbackInterface)options.ClientRpcTarget;
}
Questo clientCallback
campo può ora essere richiamato ogni volta che il servizio desidera richiamare il client, fino a quando la connessione non viene eliminata.
Il BrokeredServiceFactory delegato accetta come ServiceMoniker parametro nel caso in cui la service factory sia un metodo condiviso che crea più servizi o versioni distinte del servizio in base al moniker. Questo moniker proviene dal client e include la versione del servizio prevista. Inoltrando questo moniker al costruttore del servizio, il servizio può emulare il comportamento anomalo di determinate versioni del servizio in modo che corrispondano a ciò che il client potrebbe aspettarsi.
Evitare di usare il AuthorizingBrokeredServiceFactory delegato con il IBrokeredServiceContainer.Proffer metodo a meno che non si userà all'interno della IAuthorizationService classe del servizio negoziata. Questa IAuthorizationServiceoperazione deve essere eliminata con la classe di servizio negoziata per evitare una perdita di memoria.
Supporto di più versioni del servizio
Quando si incrementa la versione in ServiceMoniker, è necessario eseguire il proffering di ogni versione del servizio negoziato per cui si intende rispondere alle richieste client. A tale scopo, chiamare il IBrokeredServiceContainer.Proffer metodo con ognuno ServiceRpcDescriptor di essi ancora supportato.
La profferazione del servizio con una null
versione fungerà da 'catch all' che corrisponderà a qualsiasi richiesta client per la quale non esiste una versione precisa corrispondente a un servizio registrato.
Ad esempio, è possibile profferre il servizio 1.0 e 1.1 con versioni specifiche e registrare anche il servizio con una null
versione.
In questi casi, i client che richiedono il servizio con la versione 1.0 o 1.1 richiamano la factory del servizio profferta per tali versioni esatte, mentre un client che richiede la versione 8.0 comporta la chiamata della factory del servizio proffered con controllo delle versioni Null.
Poiché la versione richiesta dal client viene fornita alla factory del servizio, la factory può quindi prendere una decisione su come configurare il servizio per questo client specifico o se tornare null
a firmare una versione non supportata.
Una richiesta client per un servizio con una null
versione corrisponde solo a un servizio registrato e profferato con una null
versione.
Si consideri un caso in cui sono state pubblicate molte versioni del servizio, molte delle quali sono compatibili con le versioni precedenti e pertanto possono condividere un'implementazione del servizio. È possibile usare l'opzione catch-all per evitare di dover eseguire ripetutamente il proffer di ogni singola versione come indicato di seguito:
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.
}));
Registrazione del servizio
Il proffering di un servizio negoziato nel contenitore di servizi negoziati globale genererà un'eccezione a meno che il servizio non sia stato registrato per la prima volta. La registrazione consente al contenitore di conoscere in anticipo quali servizi negoziati possono essere disponibili e quali pacchetti vs caricare quando vengono richiesti per eseguire il codice di recapito. In questo modo Visual Studio può essere avviato rapidamente, senza caricare tutte le estensioni in anticipo, ma essere in grado di caricare l'estensione necessaria quando richiesto da un client del servizio negoziato.
La registrazione può essere eseguita applicando alla ProvideBrokeredServiceAttributeAsyncPackageclasse derivata da . Questo è l'unico luogo in cui può essere impostato .ServiceAudience
[ProvideBrokeredService("YourCompany.Extension.Calculator", "1.0", Audience = ServiceAudience.Local)]
Il valore predefinito Audience è ServiceAudience.Process, che espone il servizio negoziato solo ad altro codice all'interno dello stesso processo. Impostando ServiceAudience.Local, si sceglie di esporre il servizio negoziato ad altri processi appartenenti alla stessa sessione di Visual Studio.
Se il servizio negoziato devetrue
L'impostazione di questi flag può introdurre gravi vulnerabilità di sicurezza e non deve essere eseguita senza prima essere conforme alle indicazioni riportate in Come proteggere un servizio broker.
Quando si incrementa la versione in ServiceMoniker, è necessario registrare ogni versione del servizio negoziato per cui si intende rispondere alle richieste client. Supportando più della versione più recente del servizio negoziato, è possibile mantenere la compatibilità con le versioni precedenti per i client della versione precedente del servizio brokerato, che può essere particolarmente utile quando si considera lo scenario di Live Share in cui ogni versione di Visual Studio che condivide la sessione potrebbe essere una versione diversa.
La registrazione del servizio con una null
versione fungerà da 'catch all' che corrisponderà a qualsiasi richiesta client per la quale non esiste una versione precisa con un servizio registrato.
Ad esempio, è possibile registrare il servizio 1.0 e 2.0 con versioni specifiche e registrare anche il servizio con una null
versione.
Usare MEF per eseguire il proffering e registrare il servizio
Ciò richiede Visual Studio 2022 Update 2 o versione successiva.
Un servizio negoziato può essere esportato tramite MEF invece di usare un pacchetto di Visual Studio come descritto nelle due sezioni precedenti. Questo ha compromessi da considerare:
Compromesso | Pacchetto proffer | Esportazione MEF |
---|---|---|
Disponibilità | ✅ Il servizio negoziato è disponibile immediatamente all'avvio di Visual Studio. | ⚠️ Il servizio broker può essere ritardato nella disponibilità fino a quando MEF non è stato inizializzato nel processo. Questa operazione è in genere veloce, ma può richiedere alcuni secondi quando la cache MEF non è aggiornata. |
Idoneità multipiattaforma | ⚠️ È necessario creare codice specifico di Visual Studio per Windows. | ✅Il servizio negoziato nell'assembly può essere caricato in Visual Studio per Windows e Visual Studio per Mac. |
Per esportare il servizio negoziato tramite MEF invece di usare pacchetti di Visual Studio:
- Verificare che non sia presente alcun codice correlato alle ultime due sezioni. In particolare, non è necessario disporre di codice che chiama e IBrokeredServiceContainer.Proffer non deve applicare al ProvideBrokeredServiceAttribute pacchetto (se presente).
- Implementare l'interfaccia
IExportedBrokeredService
nella classe di servizio negoziata. - Evitare eventuali dipendenze del thread principale nel costruttore o importare setter di proprietà. Usare il
IExportedBrokeredService.InitializeAsync
metodo per inizializzare il servizio negoziato, in cui sono consentite le dipendenze dei thread principali. - Applicare alla
ExportBrokeredServiceAttribute
classe di servizio negoziata, specificando le informazioni sul moniker del servizio, sul gruppo di destinatari e su eventuali altre informazioni correlate alla registrazione necessarie. - Se la classe richiede l'eliminazione, implementare IDisposable anziché IAsyncDisposable perché MEF possiede la durata del servizio e supporta solo l'eliminazione sincrona.
- Verificare che il
source.extension.vsixmanifest
file elenchi il progetto contenente il servizio negoziato come assembly MEF.
Come parte MEF, il servizio brokerato può importare qualsiasi altra parte MEF nell'ambito predefinito.
In questo caso, assicurarsi di usare System.ComponentModel.Composition.ImportAttribute anziché System.Composition.ImportAttribute.
Ciò è dovuto al fatto che deriva ExportBrokeredServiceAttribute
da System.ComponentModel.Composition.ExportAttribute e che usa lo stesso spazio dei nomi MEF in un tipo è obbligatorio.
Un servizio negoziato è univoco per poter importare alcune esportazioni speciali:
- IServiceBroker, che deve essere usato per acquisire altri servizi negoziati.
- ServiceMoniker, che può essere utile quando si esportano più versioni del servizio negoziato ed è necessario rilevare la versione richiesta dal client.
- ServiceActivationOptions, che può essere utile quando è necessario che i client forniscano parametri speciali o una destinazione di callback client.
- AuthorizationServiceClient, che può essere utile quando è necessario eseguire controlli di sicurezza come descritto in Come proteggere un servizio broker. Questo oggetto non deve essere eliminato dalla classe, perché verrà eliminato automaticamente quando il servizio broker viene eliminato.
Il servizio negoziato non deve usare MEF ImportAttribute per acquisire altri servizi negoziati.
Può invece [Import]
IServiceBroker eseguire query per i servizi negoziati nel modo tradizionale.
Per altre informazioni, vedere Come usare un servizio negoziato.
Ecco un esempio:
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);
}
}
Esportazione di più versioni del servizio broker
Può ExportBrokeredServiceAttribute
essere applicato al servizio negoziato più volte per offrire più versioni del servizio negoziato.
L'implementazione della IExportedBrokeredService.Descriptor
proprietà deve restituire un descrittore con un moniker corrispondente a quello richiesto dal client.
Si consideri questo esempio, in cui il servizio calcolatrice esportato 1.0 con formattazione UTF8, quindi aggiunge successivamente un'esportazione 1.1 per ottenere le prestazioni migliori dell'uso della formattazione 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!;
}
A partire da Visual Studio 2022 Update 12 (17.12), un null
servizio con versione può essere esportato in modo che corrisponda a qualsiasi richiesta client per il servizio indipendentemente dalla versione inclusa una richiesta con una null
versione.
Tale servizio può restituire null
dalla Descriptor
proprietà per rifiutare una richiesta client quando non offre un'implementazione della versione richiesta dal client.
Rifiuto di una richiesta di servizio
Un servizio negoziato può rifiutare la richiesta di attivazione di un client generando dal InitializeAsync metodo . Se si genera un'eccezione ServiceActivationFailedException , viene generata nuovamente al client.