Compartilhar via


Consumir um serviço agenciado

Este documento descreve todo o código, padrões e cuidados relevantes para a aquisição, uso geral e descarte de qualquer serviço agenciado. Para aprender a usar um serviço agenciado específico depois de adquirido, procure a documentação específica desse serviço agenciado.

Com todo o código neste documento, é altamente recomendável ativar o recurso de tipos de referência anuláveis do C#.

Recuperar um IServiceBroker

Para adquirir um serviço agenciado, primeiro você deve ter uma instância do IServiceBroker. Quando seu código está sendo executado no contexto do MEF (Managed Extensibility Framework) ou de um VSPackage, normalmente recomendamos o agente de serviço global.

Os próprios serviços agenciados devem usar o IServiceBroker que é atribuído a eles quando seu alocador de serviço é invocado.

O agente de serviços global

O Visual Studio oferece duas maneiras de adquirir o agente de serviços global.

Use GlobalProvider.GetServiceAsync para solicitar o SVsBrokeredServiceContainer:

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

A partir do Visual Studio 2022, o código em execução em uma extensão ativada por MEF pode importar o agente de serviços global:

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

Observe o argumento typeof para o atributo Import, que é necessário.

Cada solicitação para o IServiceBroker global produz uma nova instância de um objeto que serve como uma exibição do contêiner de serviço agenciado global. Essa instância exclusiva do agente de serviços permite que seu cliente receba eventos AvailabilityChanged que são exclusivos para uso desse cliente. Recomendamos que cada cliente/classe em sua extensão adquira seu próprio agente de serviços usando qualquer uma das abordagens acima, em vez de adquirir uma instância e compartilhá-la em toda a extensão. Esse padrão também incentiva padrões de codificação seguros em que um serviço agenciado não deve estar usando o agente de serviço global.

Importante

As implementações de IServiceBroker normalmente não implementam IDisposable, mas esses objetos não podem ser coletados enquanto os manipuladores AvailabilityChanged existirem. Certifique-se de equilibrar a adição/remoção de manipuladores de eventos, principalmente quando o código pode descartar o agente de serviço durante o tempo de vida do processo.

Agentes de serviço específicos do contexto

O uso do agente de serviço adequado é um requisito importante do modelo de segurança de serviços agenciados, especialmente no contexto de sessões do Live Share.

Os serviços agenciados são ativados com seus próprios IServiceBroker e devem usar essa instância para qualquer uma de suas próprias necessidades de serviço agenciado, incluindo serviços oferecidos com Proffer. Esse código fornece um BrokeredServiceFactory que recebe um agente de serviço a ser usado pelo serviço agenciado instanciado.

Recuperar um proxy de serviço agenciado

A recuperação de um serviço agenciado normalmente é feita com o método GetProxyAsync.

O método GetProxyAsync exige um ServiceRpcDescriptor e a interface de serviço como um argumento de tipo genérico. A documentação sobre o serviço agenciado que você está solicitando deve indicar onde obter o descritor e qual interface usar. Para serviços agenciados incluídos no Visual Studio, a interface a ser usada deve aparecer na documentação do IntelliSense no descritor. Saiba como encontrar descritores para serviços agenciados do Visual Studio em Descobrir os Serviços Agenciados Disponíveis.

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

Assim como acontece com todas as solicitações de serviço agenciado, o código anterior ativa uma nova instância de um serviço agenciado. Depois de usar o serviço, o código anterior descarta o proxy à medida que a execução sai do bloco using.

Importante

Todo proxy recuperado deve ser descartado, mesmo que a interface de serviço não derive do IDisposable. O descarte é importante porque o proxy geralmente tem recursos de E/S que o apoiam e impedem que ele seja coletado como lixo. O descarte encerra a E/S, permitindo que o proxy seja coletado como lixo. Use uma conversão condicional para IDisposable para descarte e prepare-se para a falha da conversão para evitar uma exceção para proxies null ou proxies que não implementam de fato IDisposable.

Certifique-se de instalar o pacote NuGet Microsoft.ServiceHub.Analyzers mais recente e manter as regras do analisador ISBxxxx habilitadas para ajudar a evitar vazamentos desse tipo.

A disposição do proxy resulta no descarte do serviço agenciado que foi dedicado a esse cliente.

Se o código exigir um serviço agenciado e não puder concluir seu trabalho quando o serviço não estiver disponível, você poderá exibir uma caixa de diálogo de erro para o usuário se o código possuir a experiência do usuário em vez de lançar uma exceção.

Destinos de RPC do cliente

Alguns serviços agenciados aceitam ou exigem um destino RPC (Chamada de Procedimento Remoto) do cliente para "retornos de chamada". Essa opção ou requisito deve estar presente na documentação desse serviço agenciado específico. Para serviços agenciados do Visual Studio, essas informações devem ser incluídas na documentação do IntelliSense no descritor.

Nesse caso, um cliente pode fornecer isso usando ServiceActivationOptions.ClientRpcTarget da seguinte forma:

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

Chamar o proxy do cliente

O resultado da solicitação de um serviço agenciado é uma instância da interface de serviço implementada por um proxy. Esse proxy encaminha chamadas e eventos em cada direção, com algumas diferenças importantes no comportamento do que se poderia esperar ao chamar o serviço diretamente.

Padrão do observador

Se o contrato de serviço usar parâmetros do tipo IObserver<T>, você poderá saber mais sobre como construir esse tipo em Como implementar um observador.

Um ActionBlock<TInput> pode ser adaptado para implementar IObserver<T> com o método de extensão AsObserver. A classe System.Reactive.Observer da estrutura Reactive é outra alternativa para implementar a interface por conta própria.

Exceções lançadas a partir do proxy

  • Espere o lançamento de RemoteInvocationException para qualquer exceção lançada do serviço agenciado. A exceção original pode ser encontrada no InnerException. Esse comportamento é natural para um serviço hospedado remotamente porque é um comportamento do JsonRpc. Quando o serviço é local, o proxy local encapsula todas as exceções da mesma maneira para que o código do cliente possa ter apenas um caminho de exceção que funcione para serviços locais e remotos.
    • Verifique a propriedade ErrorCode caso a documentação do serviço sugira que códigos específicos sejam definidos com base em condições específicas nas quais você pode ramificar.
    • Um conjunto mais amplo de erros é comunicado pela captura de RemoteRpcException, que é o tipo base para o RemoteInvocationException.
  • Espere o lançamento de ConnectionLostException a partir de qualquer chamada quando a conexão com um serviço remoto cair ou o processo que hospeda o serviço falhar. Isso é preocupante principalmente quando o serviço pode ser adquirido remotamente.

Armazenamento em cache do proxy

Há uma certa despesa na ativação de um serviço agenciado e proxy associado, principalmente quando o serviço vem de um processo remoto. Quando o uso frequente de um serviço agenciado garante armazenamento em cache do proxy em muitas chamadas em uma classe, o proxy pode ser armazenado em um campo nessa classe. A classe que contém o serviço deve ser descartável e descartar o proxy dentro de seu método Dispose. Considere este exemplo:

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

O código anterior está aproximadamente correto, mas não leva em conta as condições de corrida entre Dispose e SayHiAsync. O código também não leva em conta eventos AvailabilityChanged que devem levar ao descarte do proxy adquirido anteriormente e à reaquisição do proxy na próxima vez que for necessário.

A classe ServiceBrokerClient foi projetada para lidar com essas condições de corrida e invalidação para ajudar a manter seu próprio código simples. Considere este exemplo atualizado que armazena em cache o proxy usando esta classe auxiliar:

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

O código anterior ainda é responsável por descartar o ServiceBrokerClient e cada aluguel de um proxy. As condições de corrida entre o descarte e o uso do proxy são tratadas pelo objeto ServiceBrokerClient, que descartará cada proxy armazenado em cache no momento do seu próprio descarte ou quando o último aluguel desse proxy for liberado, o que ocorrer por último.

Observações importantes sobre o ServiceBrokerClient

Escolher entre IServiceBroker e ServiceBrokerClient

Ambos são fáceis de usar e o padrão provavelmente deve ser IServiceBroker.

Categoria IServiceBroker ServiceBrokerClient
Intuitivo Sim Sim
Requer descarte Não Sim
Gerencia a vida útil do proxy Não. O proprietário deve descartar o proxy quando terminar de usá-lo. Sim, permanecem vivos e são reutilizados enquanto forem válidos.
Aplicável a serviços sem estado Sim Sim
Aplicável a serviços com estado Sim No
Apropriado quando manipuladores de eventos são adicionados ao proxy Sim No
Evento a ser notificado quando o proxy antigo for invalidado AvailabilityChanged Invalidated

ServiceBrokerClient fornece uma forma conveniente de você obter reutilização rápida e frequente de um proxy, em casos em que você não se importa se o serviço subjacente é alterado abaixo de você entre as operações de nível superior. Mas se você se preocupa com essas coisas e deseja gerenciar o tempo de vida de seus proxies por conta própria, ou precisa de manipuladores de eventos (o que implica a necessidade de gerenciar o tempo de vida do proxy), use IServiceBroker.

Resiliência a interrupções de serviço

Alguns tipos de interrupções de serviço são possíveis com serviços agenciados:

Falhas de ativação de serviço agenciado

Quando uma solicitação de serviço agenciado pode ser atendida por um serviço disponível, mas o alocador de serviço lança uma exceção sem tratamento, um ServiceActivationFailedException é lançado de volta ao cliente para que ele possa entender e relatar a falha ao usuário.

Quando uma solicitação de serviço agenciado não pode ser correspondida a nenhum serviço disponível, null é retornado ao cliente. Nesse caso, AvailabilityChanged será levantado quando e se esse serviço estiver disponível posteriormente.

A solicitação de serviço pode ser recusada não porque o serviço não está lá, mas porque a versão oferecida é inferior à versão solicitada. Seu plano de fallback pode incluir repetir a solicitação de serviço com versões inferiores que seu cliente sabe que existem e com as quais pode interagir.

Se/quando a latência de todas as verificações de versão com falha se tornar perceptível, o cliente poderá solicitar o VisualStudioServices.VS2019_4Services.RemoteBrokeredServiceManifest para ter uma ideia completa de quais serviços e versões estão disponíveis a partir de uma fonte remota.

Manipular conexões perdidas

Um proxy de serviço agenciado adquirido com êxito pode falhar devido a uma conexão perdida ou a uma falha no processo que o hospeda. Após essa interrupção, qualquer chamada feita nesse proxy resultará no lançamento de ConnectionLostException.

Um cliente de serviço agenciado pode detectar e reagir proativamente a essas quedas de conexão manipulando o evento Disconnected. Para alcançar esse evento, um proxy deve ser convertido para IJsonRpcClientProxy obter o objeto JsonRpc. Essa conversão deve ser feita condicionalmente para que ela falhe normalmente quando o serviço for local.

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

Manipular alterações de disponibilidade de serviço

Os clientes de serviço agenciado podem receber notificações de quando eles devem consultar novamente um serviço agenciado que consultaram anteriormente manipulando o evento AvailabilityChanged. Os manipuladores desse evento devem ser adicionados antes de solicitar um serviço agenciado para garantir que um evento gerado logo após uma solicitação de serviço não seja perdido devido a uma condição de corrida.

Quando um serviço agenciado é solicitado apenas durante a execução de um método assíncrono, não recomendamos manipular esse evento. O evento é mais relevante para clientes que armazenam seu proxy por longos períodos, de modo que eles precisem compensar as alterações de serviço e estejam em posição de atualizar seu proxy.

Esse evento pode ser gerado em qualquer thread, possivelmente de forma simultânea ao código que está usando um serviço que o evento está descrevendo.

Várias mudanças de estado podem levar ao aumento desse evento, incluindo:

  • A abertura ou fechamento de uma solução ou pasta.
  • O início de uma sessão do Live Share.
  • Um serviço agenciado registrado dinamicamente que acabou de ser descoberto.

Um serviço agenciado afetado só resulta na geração desse evento para clientes que solicitaram esse serviço anteriormente, independentemente de essa solicitação ter sido atendida ou não.

O evento é gerado no máximo uma vez por serviço após cada solicitação para esse serviço. Por exemplo, se o cliente solicitar o serviço A e o serviço B sofrer uma alteração de disponibilidade, nenhum evento será gerado para esse cliente. Depois, quando o serviço A sofrer uma alteração de disponibilidade, o cliente receberá o evento. Se o cliente não solicitar novamente o serviço A, as alterações de disponibilidade subsequentes para A não resultarão em mais notificações para esse cliente. Assim que o cliente solicitar novamente A, ele se tornará elegível para receber a próxima notificação sobre esse serviço.

O evento é gerado quando um serviço se torna disponível, não está mais disponível ou sofre uma alteração de implementação que exige que todos os clientes de serviço anteriores façam uma nova consulta para o serviço.

O ServiceBrokerClient manipula eventos de alteração de disponibilidade em relação a proxies armazenados em cache automaticamente, descartando os proxies antigos quando qualquer aluguel for devolvido e solicitando uma nova instância do serviço quando e se seu proprietário solicitar uma. Essa classe pode simplificar consideravelmente seu código quando o serviço é sem estado e não exige que seu código anexe manipuladores de eventos ao proxy.

Recuperar um pipe de serviço agenciado

Embora o acesso a um serviço agenciado por meio de um proxy seja a técnica mais comum e conveniente, em cenários avançados, pode ser preferível ou necessário solicitar um pipe para esse serviço para que o cliente possa controlar diretamente o RPC ou comunicar qualquer outro tipo de dados diretamente.

Um pipe para o serviço agenciado pode ser obtido por meio do método GetPipeAsync. Esse método usa um ServiceMoniker em vez de um ServiceRpcDescriptor porque os comportamentos RPC fornecidos por um descritor não são necessários. Quando há um descritor, você pode obter o apelido dele por meio da propriedade ServiceRpcDescriptor.Moniker.

Embora os pipes estejam vinculados à E/S, eles não são elegíveis para coleta de lixo. Evite vazamentos de memória completando sempre esses pipes quando eles não forem mais usados.

No trecho a seguir, um serviço agenciado é ativado e o cliente tem um pipe direto para ele. Em seguida, o cliente envia o conteúdo de um arquivo para o serviço e se desconecta.

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

Testar clientes de serviço agenciado

Os serviços agenciados são uma dependência razoável a ser simulada ao testar sua extensão. Ao simular um serviço agenciado, recomendamos usar uma estrutura de simulação que implemente a interface em seu nome e injetar o código necessário para os membros específicos que seu cliente invocará. Isso permite que seus testes continuem compilando e sejam executados sem interrupções quando os membros são adicionados à interface de serviço agenciado.

Ao usar o Microsoft.VisualStudio.Sdk.TestFramework para testar sua extensão, seu teste pode incluir código padrão para oferecer um serviço fictício que o código do cliente pode consultar e executar. Por exemplo, suponhamos que você queira simular o serviço agenciado VisualStudioServices.VS2022.FileSystem em seus testes. Você pode oferecer a simulação com este código:

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

O contêiner de serviço agenciado simulado não exige que um serviço oferecido seja registrado primeiro, como o próprio Visual Studio.

Seu código em teste pode adquirir o serviço agenciado normalmente, porém, no teste, ele obterá sua simulação em vez da real que obteria durante a execução no 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);
}