Partilhar via


Fornecer um serviço intermediado

Um serviço intermediado consiste nos seguintes elementos:

Cada um dos itens da lista anterior é descrito em detalhes nas seções a seguir.

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

A interface de serviço

A interface de serviço pode ser uma interface .NET padrão (geralmente escrita em C#), mas deve estar em conformidade com as diretrizes definidas pelo tipo derivado do ServiceRpcDescriptorque seu serviço usará para garantir que a interface possa ser usada em RPC quando o cliente e o serviço forem executados em processos diferentes. Essas restrições geralmente incluem que propriedades e indexadores não são permitidos, e a maioria ou todos os métodos retornam Task ou outro tipo de retorno compatível com assíncrono.

O ServiceJsonRpcDescriptor é o tipo derivado recomendado para serviços intermediados. Essa classe utiliza a biblioteca StreamJsonRpc quando o cliente e o serviço exigem RPC para se comunicar. StreamJsonRpc aplica certas restrições na interface de serviço, conforme descrito aqui .

A interface pode derivar de IDisposable, System.IAsyncDisposableou até mesmo Microsoft.VisualStudio.Threading.IAsyncDisposable mas isso não é exigido pelo sistema. Os proxies de cliente gerados implementarão IDisposable de qualquer forma.

Uma interface de serviço de calculadora simples pode ser declarada assim:

public interface ICalculator
{
    ValueTask<double> AddAsync(double a, double b, CancellationToken cancellationToken);
    ValueTask<double> SubtractAsync(double a, double b, CancellationToken cancellationToken);
}

Embora a implementação dos métodos nessa interface possa não garantir um método assíncrono, sempre usamos assinaturas de método assíncrono nessa interface porque essa interface é usada para gerar o proxy de cliente que pode invocar esse serviço remotamente, o que certamente garante uma assinatura de método assíncrono.

Uma interface pode declarar eventos que podem ser usados para notificar seus clientes sobre eventos que ocorrem no serviço.

Além dos eventos ou do padrão de design do observador, um serviço intermediado que precisa "ligar de volta" para o cliente pode definir uma segunda interface que serve como o contrato que um cliente deve implementar e fornecer através da propriedade ServiceActivationOptions.ClientRpcTarget ao solicitar o serviço. Essa interface deve estar em conformidade com os mesmos padrões de design e restrições que a interface de serviço intermediada, mas com restrições adicionais de controle de versão.

Revise Práticas recomendadas para projetar um serviço intermediado para obter dicas sobre como projetar uma interface RPC eficiente e à prova do futuro.

Pode ser útil declarar essa interface em um assembly distinto do assembly que implementa o serviço para que seus clientes possam fazer referência à interface sem que o serviço tenha que expor mais de seus detalhes de implementação. Também pode ser útil distribuir o assembly de interface como um pacote NuGet para outras extensões como referência, reservando a sua própria extensão para distribuir a implementação do serviço.

Considere direcionar o assembly que declara sua interface de serviço para netstandard2.0 para garantir que seu serviço possa ser facilmente invocado a partir de qualquer processo .NET, esteja ele executando o .NET Framework, .NET Core, .NET 5 ou posterior.

Testes

Os testes automatizados devem ser escritos em conjunto com o serviço interface para verificar a preparação da interface para RPC.

Os testes devem verificar se todos os dados passados pela interface são serializáveis.

Você pode achar a classe BrokeredServiceContractTestBase<TInterface,TServiceMock> do pacote Microsoft.VisualStudio.Sdk.TestFramework.Xunit útil para derivar a sua classe de teste de interface a partir dela. Essa classe inclui alguns testes básicos de convenção para sua interface, métodos para ajudar com afirmações comuns, como teste de eventos, e muito mais.

Metodologia

Assuma que todos os argumentos e o valor de retorno foram serializados completamente. Se você estiver usando a classe base de teste mencionada acima, seu código pode ter esta aparência:

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

Considere testar a resolução de sobrecarga se você declarar vários métodos com o mesmo nome. Você pode adicionar um campo internal ao seu serviço simulado para cada método nele que armazena argumentos para esse método para que o método de teste possa chamar o método e, em seguida, verificar se o método correto foi invocado com os argumentos certos.

Eventos

Todos os eventos declarados em sua interface também devem ser testados quanto à prontidão para RPC. Os eventos gerados de um serviço intermediado não causam uma falha de teste se falharem durante a serialização RPC porque os eventos são "disparar e esquecer".

Se você estiver usando a classe base de teste mencionada acima, esse comportamento já está incorporado em alguns métodos auxiliares e pode ter esta aparência (com partes inalteradas omitidas para brevidade):

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

Implementação do serviço

A classe de serviço deve implementar a interface RPC declarada na etapa anterior. Um serviço pode implementar IDisposable ou quaisquer outras interfaces além da usada para RPC. O proxy gerado no cliente implementa apenas a interface de serviço, IDisposablee possivelmente algumas outras interfaces selecionadas para suportar o sistema, de modo que uma conversão para outras interfaces implementadas pelo serviço falhará no cliente.

Considere o exemplo de calculadora usado acima, que implementamos aqui:

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

Como os próprios corpos do método não precisam ser assíncronos, encapsulamos explicitamente o valor de retorno em um tipo de retorno de ValueTask<TResult> construído para estar em conformidade com a interface de serviço.

Implementando o padrão de projeto observável

Se você oferecer uma assinatura de observador na interface do serviço, ela poderá ter esta aparência:

Task<IDisposable> SubscribeAsync(IObserver<YourDataType> observer);

O argumento IObserver<T> normalmente precisará ultrapassar a vida útil desta chamada de método para que o cliente possa continuar a receber atualizações após a conclusão da chamada de método, até que o cliente descarte o valor de IDisposable retornado. Para facilitar isso, a sua classe de serviço pode incluir uma coleção de assinaturas IObserver<T> para que quaisquer atualizações feitas ao seu estado sejam enumeradas e atualizem todos os assinantes. Certifique-se de que a enumeração de sua coleção é thread-safe em relação uns aos outros e, especialmente, com as mutações nessa coleção que podem ocorrer por meio de assinaturas adicionais ou eliminações dessas assinaturas.

Certifique-se de que todas as atualizações publicadas via OnNext mantenham a ordem em que as alterações de estado foram introduzidas no seu serviço.

Todas as assinaturas devem, em última análise, ser encerradas com uma chamada para OnCompleted ou OnError para evitar vazamentos de recursos nos sistemas cliente e RPC. Isto inclui a eliminação de serviços, em que todas as restantes subscrições devem ser explicitamente preenchidas.

Saiba mais sobre como o padrão de design do observadorcomo implementar um provedor de dados observável e, particularmente, com RPC em mente.

Serviços descartáveis

A sua classe de serviço não precisa ser descartável, mas os serviços que o são serão descartados quando o cliente eliminar o proxy para o seu serviço ou a conexão entre o cliente e o serviço for perdida. As interfaces descartáveis são testadas nesta ordem: System.IAsyncDisposable, Microsoft.VisualStudio.Threading.IAsyncDisposable, IDisposable. Somente a primeira interface dessa lista que sua classe de serviço implementa será usada para descartar o serviço.

Tenha em mente a segurança das roscas ao considerar o descarte. Seu método Dispose pode ser chamado em qualquer thread enquanto outro código em seu serviço está em execução (por exemplo, se uma conexão estiver sendo descartada).

Lançar exceções

Ao lançar exceções, considere lançar com um ErrorCode de específico para controlar o código de erro recebido pelo cliente no . Fornecer aos clientes um código de erro pode permitir que eles ajam com base na natureza do erro mais eficazmente do que ao analisar mensagens ou tipos de exceção.

De acordo com a especificação JSON-RPC, os códigos de erro DEVEM ser maiores que -32000, incluindo números positivos.

Consumir outros serviços intermediados

Quando um serviço intermediado requer acesso a outro serviço intermediado, recomendamos o uso do IServiceBroker fornecido à sua fábrica de serviços, e isso é particularmente importante quando o registo do serviço intermediado define o sinalizador AllowTransitiveGuestClients.

Para estar em conformidade com esta diretriz, se nosso serviço de calculadora tivesse necessidade de outros serviços intermediados para implementar seu comportamento, modificaríamos o construtor para aceitar uma 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;
    }

    // ...
}

Saiba mais sobre como proteger um serviço intermediado e consumindo serviços intermediados.

Serviços com estado

Estado por cliente

Uma nova instância dessa classe será criada para cada cliente que solicita o serviço. Um campo na classe Calculator acima armazenaria um valor que poderia ser exclusivo para cada cliente. Suponhamos que adicionemos um contador que incrementa cada vez que uma operação é executada:

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

Seu serviço intermediado deve ser escrito para seguir práticas seguras para threads. Ao usar o ServiceJsonRpcDescriptorrecomendado, as conexões remotas com clientes podem incluir a execução simultânea dos métodos do serviço, conforme descrito em este documento. Quando o cliente compartilha um processo e AppDomain com o serviço, o cliente pode chamar seu serviço simultaneamente de vários threads. Uma implementação thread-safe do exemplo acima pode usar Interlocked.Increment(Int32) para incrementar o campo operationCounter.

Estado compartilhado

Se houver um estado que seu serviço precisará compartilhar entre todos os seus clientes, esse estado deve ser definido em uma classe distinta que é instanciada pelo seu VS Package e passada como um argumento para o construtor do seu serviço.

Suponha que queremos que o operationCounter definido acima conte todas as operações de todos os clientes do serviço. Precisaríamos elevar o campo para esta nova classe estadual:

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

Agora temos uma maneira elegante e testável de gerenciar o estado compartilhado em várias instâncias de nosso serviço Calculator. Mais tarde, ao escrever o código para oferecer o serviço, veremos como essa classe State é criada uma vez e compartilhada com cada instância do serviço Calculator.

É especialmente importante ser thread-safe ao lidar com o estado compartilhado, uma vez que não se pode fazer qualquer suposição sobre vários clientes a agendar as suas chamadas de forma a que nunca sejam feitas simultaneamente.

Se a sua classe de estado partilhada precisar aceder a outros serviços intermediados, deverá usar o mediador de serviços global em vez de usar um dos mediadores de serviço contextuais atribuídos a uma instância individual do seu serviço intermediado. A utilização do agente de serviço global dentro de um serviço intermediado traz consigo implicações de segurança quando o sinalizador ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients é ativado.

Preocupações de segurança

A segurança é uma consideração para o seu serviço intermediado se ele estiver registrado com a bandeira ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients, o que o expõe a um possível acesso por outros usuários em outras máquinas que estão participando de uma sessão compartilhada do Live Share.

Consulte Como proteger um serviço intermediado e implemente as mitigações de segurança necessárias antes de definir o flag AllowTransitiveGuestClients.

O apelido de serviço

Um serviço intermediado deve ter um nome serializável e uma versão opcional pela qual um cliente pode solicitar o serviço. Um ServiceMoniker é um invólucro conveniente para estas duas informações.

Um moniker de serviço é análogo ao nome completo qualificado do assembly de um tipo CLR (Common Language Runtime). Ele deve ser globalmente exclusivo e, portanto, deve incluir o nome da sua empresa e talvez o nome da sua extensão como prefixos para o nome do serviço em si.

Pode ser útil definir esse apelido em um campo static readonly para uso em outro lugar:

public static readonly ServiceMoniker Moniker = new ServiceMoniker("YourCompany.Extension.Calculator", new Version("1.0"));

Embora a maioria dos usos do seu serviço possa não usar seu moniker diretamente, um cliente que se comunica por pipes em vez de um proxy exigirá o moniker.

Embora uma versão seja opcional em um apelido, o fornecimento de uma versão é recomendado, pois oferece aos autores de serviços mais opções para manter a compatibilidade com os clientes em mudanças comportamentais.

O descritor de serviço

O descritor de serviço combina o moniker de serviço com os comportamentos necessários para executar uma conexão RPC e criar um proxy local ou remoto. O descritor é responsável por converter efetivamente sua interface RPC em um protocolo de fio. Este descritor de serviço é uma instância de um tipo derivado de ServiceRpcDescriptor. O descritor deve ser disponibilizado para todos os clientes que usarão um proxy para acessar este serviço. A oferta do serviço também requer este descritor.

O Visual Studio define um desses tipos derivados e recomenda seu uso para todos os serviços: ServiceJsonRpcDescriptor. Esse descritor utiliza StreamJsonRpc para suas conexões RPC e cria um proxy local de alto desempenho para serviços locais que emula alguns dos comportamentos remotos, como encapsular exceções lançadas pelo serviço em RemoteInvocationException.

O ServiceJsonRpcDescriptor suporta a configuração da classe JsonRpc para codificação JSON ou MessagePack do protocolo JSON-RPC. Recomendamos a codificação MessagePack porque é mais compacta e pode ter 10 vezes mais desempenho.

Podemos definir um descritor para o nosso serviço de calculadora da seguinte forma:

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

Como você pode ver acima, uma escolha de formatador e delimitador está disponível. Como nem todas as combinações são válidas, recomendamos uma destas combinações:

ServiceJsonRpcDescriptor.Formatters ServiceJsonRpcDescriptor.MessageDelimiters Melhor para
MessagePack BigEndianInt32LengthHeader Alto desempenho
UTF8 (JSON) HttpLikeHeaders Interoperabilidade com outros sistemas JSON-RPC

Ao especificar o objeto como o parâmetro final, a conexão RPC compartilhada entre cliente e serviço é apenas um canal em umMultiplexingStream , que é compartilhado com a conexão JSON-RPC para permitir a transferência eficiente de grandes dados binários através deJSON-RPC.

A estratégia ExceptionProcessing.ISerializable faz com que as exceções lançadas do seu serviço sejam serializadas e preservadas como o Exception.InnerException para o RemoteInvocationException lançado no cliente. Sem essa configuração, informações de exceção menos detalhadas estão disponíveis no cliente.

Dica: Exponha seu descritor como ServiceRpcDescriptor em vez de qualquer tipo derivado que você usa como um detalhe de implementação. Isso dá-lhe mais flexibilidade para alterar os detalhes da implementação mais tarde, sem causar alterações que comprometam a API.

Inclua uma referência à interface do serviço no comentário do documento xml no descritor para facilitar o consumo do serviço pelos usuários. Consulte também a interface que seu serviço aceita como destino RPC do cliente, se aplicável.

Alguns serviços mais avançados também podem aceitar ou exigir um objeto de destino RPC do cliente que esteja em conformidade com alguma interface. Para esse caso, use um construtor ServiceJsonRpcDescriptor com um parâmetro Type clientInterface para especificar a interface da qual o cliente deve fornecer uma instância.

Controle de versão do descritor

Com o tempo, você pode querer incrementar a versão do seu serviço. Nesse caso, deves definir um descritor para cada versão que desejas suportar, usando um ServiceMoniker específico da versão para cada uma delas. O suporte a várias versões simultaneamente pode ser bom para compatibilidade com versões anteriores e geralmente pode ser feito com apenas uma interface RPC.

O Visual Studio segue este padrão com a sua classe VisualStudioServices, definindo o ServiceRpcDescriptor original como uma propriedade virtual sob a classe aninhada que representa a primeira versão que adicionou esse serviço intermediado. Quando precisamos alterar o protocolo de conexão ou adicionar/alterar a funcionalidade do serviço, o Visual Studio declara uma propriedade override em uma classe aninhada com versão posterior que retorna um novo ServiceRpcDescriptor.

Para um serviço definido e oferecido por uma extensão do Visual Studio, pode ser suficiente declarar outra propriedade do descritor ao lado do original. Por exemplo, suponha que seu serviço 1.0 usou o formatador UTF8 (JSON) e você percebe que mudar para o MessagePack proporcionaria um benefício significativo de desempenho. Como a alteração do formatador é uma mudança que quebra o protocolo de comunicação, isso requer o aumento do número de versão do serviço intermediado, bem como um segundo descritor. Os dois descritores juntos podem ter esta aparência:

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

Embora declaremos dois descritores (e mais tarde teremos que oferecer e registrar dois serviços) que podemos fazer isso com apenas uma interface de serviço e implementação, mantendo a sobrecarga para suportar várias versões de serviço bastante baixa.

Oferecendo o serviço

O seu serviço intermediado deve ser criado quando uma solicitação chega, sendo organizado através de uma etapa chamada o oferecimento do serviço.

A fábrica de serviços

Use GlobalProvider.GetServiceAsync para solicitar o SVsBrokeredServiceContainer. Em seguida, ligue para IBrokeredServiceContainer.Proffer nesse contêiner para oferecer seu serviço.

No exemplo abaixo, oferecemos um serviço usando o campo CalculatorService declarado anteriormente, que está configurado como uma instância de ServiceRpcDescriptor. Passamos para a nossa fábrica de serviços, que é um delegado BrokeredServiceFactory.

IBrokeredServiceContainer container = await AsyncServiceProvider.GlobalProvider.GetServiceAsync<SVsBrokeredServiceContainer, IBrokeredServiceContainer>();
container.Proffer(
    CalculatorService,
    (moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(new CalculatorService()));

Um serviço intermediado normalmente é instanciado uma vez por cliente. Isso é um desvio de outros serviços (VS, Visual Studio), que normalmente são instanciados uma vez e compartilhados entre todos os clientes. Criar uma instância do serviço por cliente permite uma melhor segurança, pois cada serviço e/ou sua conexão pode reter o estado por cliente sobre o nível de autorização em que o cliente opera, qual é seu CultureInfo preferido, etc. Como veremos a seguir, ele também permite serviços mais interessantes que aceitam argumentos específicos para essa solicitação.

Importante

Uma fábrica de serviços que se desvia dessa diretriz e retorna uma instância de serviço compartilhado em vez de uma nova para cada cliente nunca ter seu serviço implementado IDisposable, uma vez que o primeiro cliente a descartar seu proxy levará à eliminação da instância de serviço compartilhado antes que outros clientes terminem de usá-la.

No caso mais avançado em que o construtor CalculatorService requer um objeto de estado compartilhado e um IServiceBroker, podemos propor a fábrica desta forma:

var state = new CalculatorService.State();
container.Proffer(
    CalculatorService,
    (moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(new CalculatorService(state, serviceBroker)));

A variável local state é fora fábrica de serviços e, portanto, é criada apenas uma vez e compartilhada em todos os serviços instanciados.

Ainda mais avançado, se o serviço exigisse acesso ao ServiceActivationOptions (por exemplo, para invocar métodos no objeto de destino RPC do cliente) que também poderia ser transmitido:

var state = new CalculatorService.State();
container.Proffer(CalculatorService, (moniker, options, serviceBroker, cancellationToken) =>
    new ValueTask<object?>(new CalculatorService(state, serviceBroker, options)));

Neste caso, o construtor de serviço pode ter esta aparência, assumindo que os ServiceJsonRpcDescriptor foram criados com typeof(IClientCallbackInterface) como um de seus argumentos de construtor:

internal class Calculator(State state, IServiceBroker serviceBroker, ServiceActivationOptions options)
{
    this.state = state;
    this.serviceBroker = serviceBroker;
    this.options = options;
    this.clientCallback = (IClientCallbackInterface)options.ClientRpcTarget;
}

Esse campo clientCallback agora pode ser invocado sempre que o serviço quiser invocar o cliente, até que a conexão seja descartada.

O BrokeredServiceFactory delegado toma um ServiceMoniker como parâmetro no caso de a fábrica de serviços ser um método partilhado que cria múltiplos serviços ou versões distintas do serviço com base no identificador. Esse apelido vem do cliente e inclui a versão do serviço que ele espera. Ao encaminhar esse moniker para o construtor de serviço, o serviço pode emular o comportamento peculiar de versões de serviço específicas para corresponder ao que o cliente pode esperar.

Evita usar o delegado AuthorizingBrokeredServiceFactory com o método IBrokeredServiceContainer.Proffer, a menos que utilizes o IAuthorizationService dentro da tua classe de serviço intermediada. Esse IAuthorizationService deve ser descartado com sua classe de serviço intermediada para evitar um vazamento de memória.

Suporte a várias versões do seu serviço

Ao incrementar a versão em seu ServiceMoniker, você deve oferecer cada versão do seu serviço intermediado para a qual você pretende responder às solicitações do cliente. Isso é feito chamando o método IBrokeredServiceContainer.Proffer com cada ServiceRpcDescriptor que você ainda suporta.

Oferecer o seu serviço com uma versão null servirá como um "catch all" que corresponderá a qualquer pedido do cliente para o qual não exista uma versão precisa correspondente a um serviço registado. Por exemplo, você pode oferecer seu serviço 1.0 e 1.1 com versões específicas e também registrar seu serviço com uma versão null. Nesses casos, os clientes que solicitam seu serviço com 1.0 ou 1.1 invocam a fábrica de serviços oferecida para essas versões exatas, enquanto um cliente que solicita a versão 8.0 leva à sua fábrica de serviços oferecida com versão nula sendo invocada. Como a versão solicitada pelo cliente é fornecida à fábrica de serviços, a fábrica pode então tomar uma decisão sobre como configurar o serviço para esse cliente específico ou se deve retornar null para significar uma versão sem suporte.

Um pedido de cliente para um serviço com uma versão nullapenas corresponde a um serviço registado e oferecido com uma versão null.

Considere um caso em que você publicou muitas versões do seu serviço, várias das quais são compatíveis com versões anteriores e, portanto, podem compartilhar uma implementação de serviço. Podemos utilizar a opção catch-all para evitar ter que oferecer repetidamente cada versão individual da seguinte maneira:

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

Registar o serviço

A oferta de um serviço intermediado para o contêiner de serviço intermediado global será lançada, a menos que o serviço tenha sido registrado primeiro. O registro fornece um meio para que o contêiner saiba com antecedência quais serviços intermediados podem estar disponíveis e qual pacote VS carregar quando forem solicitados para executar o código de oferta. Isso permite que o Visual Studio seja iniciado rapidamente, sem carregar todas as extensões com antecedência, mas seja capaz de carregar a extensão necessária quando solicitado por um cliente de seu serviço intermediado.

A inscrição pode ser feita aplicando o ProvideBrokeredServiceAttribute à sua classe derivada AsyncPackage. Este é o único lugar onde o ServiceAudience pode ser definido.

[ProvideBrokeredService("YourCompany.Extension.Calculator", "1.0", Audience = ServiceAudience.Local)]

O Audience padrão é ServiceAudience.Process, que expõe o seu serviço intermediado apenas a outro código dentro do mesmo processo. Ao definir ServiceAudience.Local, você opta por expor seu serviço intermediado a outros processos pertencentes à mesma sessão do Visual Studio.

Se o seu de serviço intermediado precisar ser exposto a hóspedes do Live Share, o Audience deverá incluir ServiceAudience.LiveShareGuest e a propriedade ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients definida como true. Definir esses sinalizadores pode introduzir sérias vulnerabilidades de segurança e não deve ser feito sem primeiro estar em conformidade com as orientações em Como proteger um serviço intermediado.

Ao incrementar a versão em seu ServiceMoniker, você deve registrar cada versão do seu serviço intermediado que você pretende responder às solicitações do cliente. Ao oferecer suporte a mais do que a versão mais recente do seu serviço intermediado, você ajuda a manter a compatibilidade com versões anteriores para clientes da sua versão mais antiga do serviço intermediado, o que pode ser especialmente útil ao considerar o cenário do Live Share em que cada versão do Visual Studio que está compartilhando a sessão pode ser uma versão diferente.

Registar o seu serviço com uma versão null servirá como uma solução abrangente que irá cobrir qualquer pedido do cliente para o qual não exista uma versão precisa para um serviço registado. Por exemplo, você pode registrar seu serviço 1.0 e 2.0 com versões específicas, e também registrar seu serviço com uma versão null.

Use o MEF para oferecer e registrar seu serviço

Isso requer o Visual Studio 2022 Update 2 ou posterior.

Um serviço intermediado pode ser exportado via MEF em vez de usar um pacote do Visual Studio, conforme descrito nas duas seções anteriores. Isso tem compensações a considerar:

Compromisso Oferta de pacotes Exportação MEF
Disponibilidade ✅ O Serviço Brokered está imediatamente disponível no arranque do VS. ⚠️ O serviço mediado pode ter a sua disponibilidade atrasada até que o MEF tenha sido inicializado no processo. Isso geralmente é rápido, mas pode levar vários segundos quando o cache MEF está obsoleto.
Preparação multiplataforma ⚠Código específico para Visual Studio no Windows deve ser criado. ✅ O serviço intermediado no seu assembly pode ser carregado tanto no Visual Studio para Windows quanto no Visual Studio para Mac.

Para exportar seu serviço intermediado via MEF em vez de usar pacotes VS:

  1. Confirme que não tem nenhum código relacionado com as duas últimas secções. Em particular, você não deve ter nenhum código que ligue para IBrokeredServiceContainer.Proffer e não deve aplicar o ProvideBrokeredServiceAttribute ao seu pacote (se houver).
  2. Implemente a interface IExportedBrokeredService em sua classe de serviço intermediada.
  3. Evite quaisquer dependências da thread principal no construtor ou ao definir propriedades importadas. Use o método IExportedBrokeredService.InitializeAsync para inicializar seu serviço intermediado, onde as dependências de thread principal são permitidas.
  4. Aplique o ExportBrokeredServiceAttribute à sua classe de serviço intermediada, especificando as informações sobre seu apelido de serviço, público e quaisquer outras informações relacionadas ao registro necessárias.
  5. Se sua classe exigir eliminação, implemente IDisposable em vez de IAsyncDisposable, já que o MEF é dono do tempo de vida do seu serviço e suporta apenas a eliminação síncrona.
  6. Verifique se o arquivo source.extension.vsixmanifest lista o projeto que contém o serviço intermediado como um assembly MEF.

Como parte do MEF, o seu serviço intermediado pode importar qualquer outra parte do MEF no escopo padrão. Ao fazer isso, certifique-se de usar System.ComponentModel.Composition.ImportAttribute em vez de System.Composition.ImportAttribute. Isso ocorre porque o ExportBrokeredServiceAttribute deriva de System.ComponentModel.Composition.ExportAttribute e usar o mesmo namespace MEF em todo um tipo é necessário.

Um serviço intermediado é único em ser capaz de importar algumas exportações especiais:

  • IServiceBroker, que deverá ser utilizado para adquirir outros serviços de corretagem.
  • ServiceMoniker, que pode ser útil quando você exporta várias versões do seu serviço intermediado e precisa detetar qual versão o cliente solicitou.
  • ServiceActivationOptions, que pode ser útil quando você precisa que seus clientes forneçam parâmetros especiais ou um destino de retorno de chamada do cliente.
  • AuthorizationServiceClient, que pode ser útil quando você precisa executar verificações de segurança, conforme descrito em Como proteger um serviço intermediado. Este objeto não precisa ser descartado pela sua classe, pois será descartado automaticamente quando o serviço intermediado for descartado.

O seu serviço intermediado não deve utilizar o MEF ImportAttribute para adquirir outros serviços intermediados. Em vez disso, ele pode [Import]IServiceBroker e consultar serviços intermediados da maneira tradicional. Saiba mais em Como consumir um serviço intermediado.

Aqui está um exemplo:

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

Exportando várias versões do seu serviço intermediado

O ExportBrokeredServiceAttribute pode ser aplicado ao seu serviço intermediado várias vezes para oferecer várias versões do seu serviço intermediado.

Sua implementação da propriedade IExportedBrokeredService.Descriptor deve retornar um descritor com um apelido que corresponda ao que o cliente solicitou.

Considere este exemplo, onde o serviço de calculadora exportou 1.0 com formatação UTF8 e, posteriormente, adiciona uma exportação 1.1 para aproveitar os ganhos de desempenho do uso da formatação 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 partir do Visual Studio 2022 Atualização 12 (17.12), um serviço com versão null pode ser exportado para corresponder a qualquer solicitação de cliente para o serviço, independentemente da versão, incluindo uma solicitação com uma versão null. Esse serviço pode retornar null da propriedade Descriptor para rejeitar uma solicitação do cliente quando ela não oferece uma implementação da versão solicitada pelo cliente.

Rejeitar um pedido de serviço

Um serviço intermediado pode rejeitar a solicitação de ativação de um cliente lançando do método InitializeAsync. O arremesso faz com que uma ServiceActivationFailedException seja jogada de volta para o cliente.