Condividi tramite


Utilizzare un servizio negoziato

Questo documento descrive tutto il codice, i modelli e le avvertenze relative all'acquisizione, all'uso generale e all'eliminazione di qualsiasi servizio negoziato. Per informazioni sull'uso di un particolare servizio negoziato dopo l'acquisizione, cercare la documentazione specifica per il servizio negoziato.

Con tutto il codice in questo documento, è consigliabile attivare la funzionalità dei tipi riferimento nullable di C#.

Recupero di un IServiceBroker

Per acquisire un servizio broker, è prima necessario disporre di un'istanza di IServiceBroker. Quando il codice è in esecuzione nel contesto di MEF (Managed Extensibility Framework) o un VSPackage, in genere si vuole che il service broker globale.

I servizi negoziati devono usare l'oggetto IServiceBroker assegnato quando viene richiamata la factory di servizio .

Service Broker globale

Visual Studio offre due modi per acquisire il service broker globale.

Usare GlobalProvider.GetServiceAsync per richiedere :SVsBrokeredServiceContainer

IBrokeredServiceContainer container = await AsyncServiceProvider.GlobalProvider.GetServiceAsync<SVsBrokeredServiceContainer, IBrokeredServiceContainer>();
IServiceBroker serviceBroker = container.GetFullAccessServiceBroker();

A partire da Visual Studio 2022, il codice in esecuzione in un'estensione attivata da MEF può importare il service broker globale:

[Import(typeof(SVsFullAccessServiceBroker))]
IServiceBroker ServiceBroker { get; set; }

Si noti l'argomento typeof dell'attributo Import, obbligatorio.

Ogni richiesta per il globale IServiceBroker produce una nuova istanza di un oggetto che funge da visualizzazione nel contenitore del servizio negoziato globale. Questa istanza univoca di Service Broker consente al client di ricevere AvailabilityChanged eventi univoci per l'uso del client. È consigliabile che ogni client/classe nell'estensione acquisisca il proprio service broker usando uno degli approcci precedenti anziché acquisire un'istanza e condividerla nell'intera estensione. Questo modello incoraggia anche modelli di codifica sicuri in cui un servizio negoziato non deve usare il Service Broker globale.

Importante

Le implementazioni di IServiceBroker non implementano IDisposablein genere , ma questi oggetti non possono essere raccolti mentre AvailabilityChanged esistono gestori. Assicurarsi di bilanciare l'aggiunta/rimozione dei gestori eventi, in particolare quando il codice potrebbe rimuovere il service broker durante la durata del processo.

Broker di servizi specifici del contesto

L'uso del service broker appropriato è un requisito importante del modello di sicurezza dei servizi negoziati, in particolare nel contesto delle sessioni di Live Share.

I servizi negoziati vengono attivati autonomamente IServiceBroker e devono usare questa istanza per qualsiasi esigenza di servizio negoziata, inclusi i servizi offerti con Proffer. Tale codice fornisce un BrokeredServiceFactory oggetto che riceve un service broker da usare dal servizio broker di cui è stata creata un'istanza.

Recupero di un proxy di servizio negoziato

Il recupero di un servizio negoziato viene in genere eseguito con il GetProxyAsync metodo .

Il GetProxyAsync metodo richiederà un ServiceRpcDescriptor oggetto e l'interfaccia del servizio come argomento di tipo generico. La documentazione relativa al servizio broker richiesto deve indicare dove ottenere il descrittore e l'interfaccia da usare. Per i servizi negoziati inclusi in Visual Studio, l'interfaccia da usare dovrebbe essere visualizzata nella documentazione di IntelliSense nel descrittore. Informazioni su come trovare i descrittori per i servizi negoziati di Visual Studio in Individuazione dei servizi negoziati disponibili.

IServiceBroker broker; // Acquired as described earlier in this topic
IMyService? myService = await broker.GetProxyAsync<IMyService>(serviceDescriptor, cancellationToken);
using (myService as IDisposable)
{
    Assumes.Present(myService); // Throw if service was not available
    await myService.SayHelloAsync();
}

Come per tutte le richieste di servizio negoziate, il codice precedente attiva una nuova istanza di un servizio negoziato. Dopo aver usato il servizio, il codice precedente elimina il proxy quando l'esecuzione esce dal using blocco.

Importante

Ogni proxy recuperato deve essere eliminato, anche se l'interfaccia del servizio non deriva da IDisposable. Lo smaltimento è importante perché il proxy include spesso risorse di I/O che lo impediscono di essere sottoposto a Garbage Collection. L'eliminazione termina l'I/O, consentendo al proxy di essere sottoposto a Garbage Collection. Usare un cast condizionale su IDisposable per l'eliminazione e prepararsi affinché il cast non riesca a evitare un'eccezione per null proxy o proxy che non implementano IDisposableeffettivamente .

Assicurarsi di installare il pacchetto NuGet Microsoft.ServiceHub.Analyzers più recente e mantenere abilitate le regole dell'analizzatore ISBxxxx per evitare tali perdite.

L'eliminazione del proxy comporta l'eliminazione del servizio brokerato dedicato a tale client.

Se il codice richiede un servizio negoziato e non può completarne il lavoro quando il servizio non è disponibile, potrebbe essere visualizzata una finestra di dialogo di errore per l'utente se il codice è proprietario dell'esperienza utente anziché generare un'eccezione.

Destinazioni RPC client

Alcuni servizi negoziati accettano o richiedono una destinazione RPC (Remote Procedure Call) client per i "callback". Tale opzione o requisito deve essere presente nella documentazione di quel particolare servizio negoziato. Per i servizi negoziati di Visual Studio, queste informazioni devono essere incluse nella documentazione di IntelliSense sul descrittore.

In questo caso, un client può specificarne uno usando ServiceActivationOptions.ClientRpcTarget il seguente:

IMyService? myService = await broker.GetProxyAsync<IMyService>(
    serviceDescriptor,
    new ServiceActivationOptions
    {
        ClientRpcTarget = new MyCallbackObject(),
    },
    cancellationToken);

Richiamo del proxy client

Il risultato della richiesta di un servizio negoziato è un'istanza dell'interfaccia del servizio implementata da un proxy. Questo proxy inoltra chiamate ed eventi ogni direzione, con alcune importanti differenze di comportamento rispetto a ciò che ci si potrebbe aspettare quando si chiama direttamente il servizio.

Schema Observer

Se il contratto di servizio accetta parametri di tipo IObserver<T>, è possibile ottenere altre informazioni su come costruire un tipo di questo tipo in Come implementare un osservatore.

Un ActionBlock<TInput> oggetto può essere adattato per implementare IObserver<T> con il AsObserver metodo di estensione. La classe System.Reactive.Observer del framework Reattivo è un'altra alternativa all'implementazione dell'interfaccia manualmente.

Eccezioni generate dal proxy

  • Si prevede RemoteInvocationException di generare un'eccezione per qualsiasi eccezione generata dal servizio negoziato. L'eccezione originale è reperibile in InnerException. Si tratta di un comportamento naturale per un servizio ospitato in remoto perché è un comportamento da JsonRpc. Quando il servizio è locale, il proxy locale esegue il wrapping di tutte le eccezioni nello stesso modo in modo che il codice client possa avere un solo percorso di eccezione che funziona per i servizi locali e remoti.
    • Controllare la proprietà se la ErrorCode documentazione del servizio suggerisce che i codici specifici vengono impostati in base a condizioni specifiche su cui è possibile creare un ramo.
    • Un set più ampio di errori viene comunicato intercettando RemoteRpcException, che è il tipo di base per .RemoteInvocationException
  • Si prevede ConnectionLostException che venga generata da qualsiasi chiamata quando la connessione a un servizio remoto viene eliminata o il processo che ospita il servizio si arresta in modo anomalo. Si tratta principalmente di problemi quando il servizio può essere acquisito in remoto.

Memorizzazione nella cache del proxy

L'attivazione di un servizio broker e del proxy associato comporta alcune spese, in particolare quando il servizio proviene da un processo remoto. Quando l'uso frequente di un servizio negoziato garantisce la memorizzazione nella cache del proxy tra molte chiamate in una classe, il proxy può essere archiviato in un campo in tale classe. La classe contenitore deve essere eliminabile ed elimina il proxy all'interno del relativo Dispose metodo. Si consideri questo esempio:

class MyExtension : IDisposable
{
    readonly IServiceBroker serviceBroker;
    IMyService? serviceProxy;

    internal MyExtension(IServiceBroker serviceBroker)
    {
        this.serviceBroker = serviceBroker;
    }

    async Task SayHiAsync(CancellationToken cancellationToken)
    {
        if (this.serviceProxy is null)
        {
            this.serviceProxy = await this.serviceBroker.GetProxyAsync<IMyService>(serviceDescriptor, cancellationToken);
            Assumes.Present(this.serviceProxy);
        }

        await this.serviceProxy.SayHelloAsync();
    }

    public void Dispose()
    {
        (this.serviceProxy as IDisposable)?.Dispose();
    }
}

Il codice precedente è approssimativamente corretto, ma non tiene conto delle race condition tra Dispose e SayHiAsync. Il codice non tiene AvailabilityChanged conto anche degli eventi che devono portare all'eliminazione del proxy acquisito in precedenza e alla riacquisizione del proxy alla successiva richiesta.

La ServiceBrokerClient classe è progettata per gestire queste condizioni di gara e invalidazione per semplificare la semplicità del codice. Si consideri questo esempio aggiornato che memorizza nella cache il proxy usando questa classe helper:

class MyExtension : IDisposable
{
    readonly ServiceBrokerClient serviceBrokerClient;

    internal MyExtension(IServiceBroker serviceBroker)
    {
        this.serviceBrokerClient = new ServiceBrokerClient(serviceBroker);
    }

    async Task SayHiAsync(CancellationToken cancellationToken)
    {
        using var rental = await this.serviceBrokerClient.GetProxyAsync<IMyService>(descriptor, cancellationToken);
        Assumes.Present(rental.Proxy); // Throw if service is not available
        IMyService myService = rental.Proxy;
        await myService.SayHelloAsync();
    }

    public void Dispose()
    {
        // Disposing the ServiceBrokerClient will dispose of all proxies
        // when their rentals are released.
        this.serviceBrokerClient.Dispose();
    }
}

Il codice precedente è comunque responsabile dell'eliminazione ServiceBrokerClient di e di ogni noleggio di un proxy. Race conditions between disposal and use of the proxy are handled by the ServiceBrokerClient object, which will dispose of each cached proxy at the time of its own disposal or when the last rental of that proxy has been released, whichever comes last.

Avvertenze importanti per quanto riguarda il ServiceBrokerClient

Scelta tra IServiceBroker e ServiceBrokerClient

Entrambi sono descrittivi e l'impostazione predefinita dovrebbe essere IServiceBroker.

Categoria IServiceBroker ServiceBrokerClient
Facile da usare
Richiede l'eliminazione No
Gestisce la durata del proxy No. Il proprietario deve eliminare il proxy quando viene usato. Sì, vengono mantenuti attivi e riutilizzati finché sono validi.
Applicabile per i servizi senza stato
Applicabile per i servizi con stato No
Appropriato quando i gestori eventi vengono aggiunti al proxy No
Evento per notificare quando il proxy precedente viene invalidato AvailabilityChanged Invalidated

ServiceBrokerClient offre un modo pratico per ottenere un riutilizzo rapido e frequente di un proxy, in cui non è importante se il servizio sottostante viene modificato da sotto tra le operazioni di primo livello. Tuttavia, se ti interessa queste cose e vuoi gestire la durata dei proxy manualmente o hai bisogno di gestori eventi (che implica la necessità di gestire la durata del proxy), devi usare IServiceBroker.

Resilienza alle interruzioni del servizio

Esistono alcuni tipi di interruzioni del servizio possibili con i servizi negoziati:

Errori di attivazione del servizio negoziata

Quando una richiesta di servizio negoziata può essere soddisfatta da un servizio disponibile, ma la factory del servizio genera un'eccezione non gestita, viene generata un'eccezione ServiceActivationFailedException al client in modo che possa comprendere e segnalare l'errore all'utente.

Quando non è possibile trovare una corrispondenza con una richiesta di servizio negoziata con qualsiasi servizio disponibile, null viene restituita al client. In questo caso, AvailabilityChanged verrà generato quando e se tale servizio diventa disponibile in un secondo momento.

La richiesta di servizio potrebbe essere rifiutata non perché il servizio non è presente, ma perché la versione offerta è inferiore alla versione richiesta. Il piano di fallback potrebbe includere la ripetizione di tentativi della richiesta di servizio con versioni precedenti con cui il client sa esistere ed è in grado di interagire con.

Se/quando la latenza da tutti i controlli delle versioni non riuscite diventa evidente, il client può richiedere il VisualStudioServices.VS2019_4Services.RemoteBrokeredServiceManifest per ottenere un'idea completa dei servizi e delle versioni disponibili da un'origine remota.

Gestione delle connessioni eliminate

Un proxy del servizio negoziato acquisito correttamente potrebbe non riuscire a causa di una connessione eliminata o di un arresto anomalo del processo che lo ospita. Dopo un'interruzione di questo tipo, qualsiasi chiamata effettuata su tale proxy comporterà la ConnectionLostException creazione di un'eccezione.

Un client del servizio negoziato può rilevare e reagire in modo proattivo a tali eliminazioni di connessione gestendo l'evento Disconnected . Per raggiungere questo evento, è necessario eseguire il cast di un proxy per IJsonRpcClientProxy ottenere l'oggetto JsonRpc . Questo cast deve essere eseguito in modo condizionale in modo da non riuscire correttamente quando il servizio è locale.

if (this.myService is IJsonRpcClientProxy clientProxy)
{
    clientProxy.JsonRpc.Disconnected += JsonRpc_Disconnected;
}

void JsonRpc_Disconnected(object? sender, JsonRpcDisconnectedEventArgs args)
{
    if (args.Reason == DisconnectedReason.RemotePartyTerminated)
    {
        // consider reacquisition of the service.
    }
}

Gestione delle modifiche alla disponibilità del servizio

I client del servizio negoziato possono ricevere notifiche di quando devono eseguire una query per un servizio negoziato per cui in precedenza hanno eseguito una query per la gestione dell'evento AvailabilityChanged . I gestori di questo evento devono essere aggiunti prima di richiedere un servizio negoziato per assicurarsi che un evento generato poco dopo che viene effettuata una richiesta di servizio non venga perso a causa di una race condition.

Quando viene richiesto un servizio negoziato solo per la durata dell'esecuzione di un metodo asincrono, la gestione di questo evento non è consigliata. L'evento è più rilevante per i client che archiviano il proxy per periodi prolungati in modo che debbano compensare le modifiche al servizio e che siano in grado di aggiornare il proxy.

Questo evento può essere generato in qualsiasi thread, possibilmente contemporaneamente al codice che usa un servizio che l'evento descrive.

Diverse modifiche di stato possono causare la generazione di questo evento, tra cui:

  • Una soluzione o una cartella aperta o chiusa.
  • Avvio di una sessione di Live Share.
  • Servizio negoziato registrato dinamicamente che è stato appena individuato.

Un servizio broker interessato comporta solo la generazione di questo evento ai client che hanno richiesto in precedenza tale servizio, indipendentemente dal fatto che la richiesta sia stata soddisfatta o meno.

L'evento viene generato al massimo una volta per servizio dopo ogni richiesta di tale servizio. Ad esempio, se il servizio A e il servizio B richiede il servizio A e il servizio B riscontrano una modifica della disponibilità, non verrà generato alcun evento a tale client. In un secondo momento, quando il servizio A sperimenta una modifica della disponibilità, il client riceverà l'evento. Se il client non richiede nuovamente il servizio A, le successive modifiche alla disponibilità per A non genereranno altre notifiche a tale client. Quando il client richiede di nuovo A , diventa idoneo a ricevere la notifica successiva relativa a tale servizio.

L'evento viene generato quando un servizio diventa disponibile, non è più disponibile o presenta una modifica di implementazione che richiede a tutti i client del servizio precedenti di eseguire una nuova query per il servizio.

Gestisce ServiceBrokerClient automaticamente gli eventi di modifica della disponibilità relativi ai proxy memorizzati nella cache eliminando i proxy precedenti quando vengono restituiti i noleggi e richiedendo una nuova istanza del servizio quando e se il proprietario ne richiede uno. Questa classe può semplificare notevolmente il codice quando il servizio è senza stato e non richiede che il codice allega i gestori eventi al proxy.

Recupero di una pipe del servizio negoziata

Anche se l'accesso a un servizio negoziato tramite un proxy è la tecnica più comune e pratica, in scenari avanzati può essere preferibile o necessario richiedere una pipe a tale servizio in modo che il client possa controllare direttamente rpc o comunicare direttamente qualsiasi altro tipo di dati.

È possibile ottenere una pipe al servizio negoziato tramite il GetPipeAsync metodo . Questo metodo accetta invece di perché ServiceMoniker ServiceRpcDescriptor i comportamenti RPC forniti da un descrittore non sono necessari. Quando si dispone di un descrittore, è possibile ottenere il moniker da esso tramite la ServiceRpcDescriptor.Moniker proprietà .

Anche se le pipe sono associate a operazioni di I/O non idonee per l'operazione di Garbage Collection. Evitare perdite di memoria completando sempre queste pipe quando non verranno più usate.

Nel frammento di codice seguente viene attivato un servizio broker e il client ha una pipe diretta. Il client invia quindi il contenuto di un file al servizio e si disconnette.

async Task SendMovieAsync(string movieFilePath, CancellationToken cancellationToken)
{
    IServiceBroker serviceBroker;
    IDuplexPipe? pipe = await serviceBroker.GetPipeAsync(serviceMoniker, cancellationToken);
    if (pipe is null)
    {
        throw new InvalidOperationException($"The brokered service '{serviceMoniker}' is not available.");
    }

    try
    {
        // Open the file optimized for async I/O
        using FileStream fs = new FileStream(movieFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true);
        await fs.CopyToAsync(pipe.Output.AsStream(), cancellationToken);
    }
    catch (Exception ex)
    {
        // Complete the pipe, passing through the exception so the remote side understands what went wrong.
        await pipe.Input.CompleteAsync(ex);
        await pipe.Output.CompleteAsync(ex);
        throw;
    }
    finally
    {
        // Always complete the pipe after successfully using the service.
        await pipe.Input.CompleteAsync();
        await pipe.Output.CompleteAsync();
    }
}

Test dei client di servizi negoziati

I servizi negoziati sono una dipendenza ragionevole da simulare durante il test dell'estensione. Quando si simula un servizio broker, è consigliabile usare un framework fittizio che implementa l'interfaccia per conto dell'utente e inserisce il codice necessario per i membri specifici che il client richiamerà. Ciò consente ai test di continuare a compilare ed eseguire senza interruzioni quando i membri vengono aggiunti all'interfaccia del servizio negoziata.

Quando si usa Microsoft.VisualStudio.Sdk.TestFramework per testare l'estensione, il test può includere codice standard per creare un servizio fittizio su cui il codice client può eseguire query ed eseguire. Si supponga, ad esempio, di voler simulare il servizio brokerato VisualStudioServices.VS2022.FileSystem nei test. È possibile profferre la simulazione con questo codice:

IBrokeredServiceContainer sbc = await AsyncServiceProvider.GlobalProvider.GetServiceAsync<SVsBrokeredServiceContainer, IBrokeredServiceContainer>();
Mock<IFileSystem> mockFileSystem = new Mock<IFileSystem>();
sbc.Proffer(VisualStudioServices.VS2022.FileSystem, (ServiceMoniker moniker, ServiceActivationOptions options, IServiceBroker serviceBroker, CancellationToken cancellationToken) => new ValueTask<object?>(mockFileSystem.Object));

Il contenitore di servizi broker simulato non richiede la registrazione di un servizio proffered prima come fa Visual Studio stesso.

Il codice sottoposto a test può acquisire il servizio negoziato come di consueto, ad eccezione del fatto che durante il test otterrà la simulazione anziché quella reale che otterrebbe durante l'esecuzione in Visual Studio:

IBrokeredServiceContainer sbc = await AsyncServiceProvider.GlobalProvider.GetServiceAsync<SVsBrokeredServiceContainer, IBrokeredServiceContainer>();
IServiceBroker serviceBroker = sbc.GetFullAccessServiceBroker();
IFileSystem? proxy = await serviceBroker.GetProxyAsync<IFileSystem>(VisualStudioServices.VS2022.FileSystem);
using (proxy as IDisposable)
{
    Assumes.Present(proxy);
    await proxy.DeleteAsync(new Uri("file://some/file"), recursive: false, null, this.TimeoutToken);
}