Compartilhar via


Fornecer um serviço intermediado

Um serviço agenciado consiste nos seguintes elementos:

Cada um dos itens acima são descritos em detalhes abaixo.

Com todo o código neste tópico, a ativação do recurso de tipos de referência anuláveis do C# é altamente recomendada.

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 ServiceRpcDescriptortipo derivado que seu serviço usará para garantir que a interface possa ser usada sobre 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íncronos.

O ServiceJsonRpcDescriptor é o tipo derivado recomendado para serviços intermediados. Essa classe utiliza a StreamJsonRpc biblioteca 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, ou mesmoMicrosoft.VisualStudio.Threading.IAsyncDisposable, System.IAsyncDisposablemas isso não é exigido pelo sistema. Os proxies de cliente gerados serão implementados IDisposable de qualquer maneira.

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 do 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 "chamar de volta" para o cliente pode definir uma segunda interface que sirva como o contrato que um cliente deve implementar e fornecer através da ServiceActivationOptions.ClientRpcTarget propriedade ao solicitar o serviço. Essa interface deve estar em conformidade com todos os mesmos padrões de design e restrições que a interface de serviço agenciada, mas com restrições adicionais sobre controle de versão.

Analise as Práticas recomendadas para projetar um serviço agenciado para obter dicas sobre como projetar uma interface RPC eficiente e preparada para o 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 precise expor mais detalhes de sua implementação. Também pode ser útil enviar o assembly de interface como um pacote NuGet para outras extensões referenciarem, reservando sua própria extensão para enviar a implementação do serviço.

Considere direcionar o assembly que declara sua interface de serviço para netstandard2.0 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.

Testando

Os testes automatizados devem ser gravados junto com a interface de serviço para verificar a prontidão RPC da interface .

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

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

Métodos

Assuma que cada argumento e o valor de retorno foram serializados completamente. Se você estiver usando a classe base de teste mencionada acima, isso pode ter a seguinte 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 internal campo ao seu serviço fictício 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 corretos.

Eventos

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

Se você estava usando a classe base de teste mencionada acima, esse comportamento já está incorporado em alguns métodos auxiliares e pode se parecer com isso (com partes inalteradas omitidas por 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));
    }
}

Implementando o 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 daquela usada para RPC. O proxy gerado no cliente implementará apenas a interface de serviço e, possivelmente, algumas outras interfaces selecionadas para oferecer suporte ao sistema, IDisposablede 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 corpos do método em si não precisam ser assíncronos, encapsulamos explicitamente o valor de retorno em um tipo de retorno construído ValueTask<TResult> para estar em conformidade com a interface de serviço.

Implementando o padrão de design observável

Se você oferecer uma assinatura de observador em sua interface de serviço, ela poderá ter a seguinte aparência:

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

O IObserver<T> argumento normalmente precisará sobreviver ao tempo de vida dessa 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 retornado IDisposable . Para facilitar isso, sua classe de serviço pode incluir uma coleção de IObserver<T> assinaturas que quaisquer atualizações feitas em seu estado enumerariam para atualizar todos os assinantes. Certifique-se de que a enumeração de sua coleção seja segura 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.

Tome cuidado para que todas as atualizações postadas por via OnNext mantenham a ordem em que as alterações de estado foram introduzidas em seu serviço.

Todas as assinaturas devem ser encerradas com uma chamada para ou OnError para OnCompleted evitar vazamentos de recursos nos sistemas cliente e RPC. Isso inclui o descarte de serviços, onde todas as assinaturas restantes devem ser explicitamente concluídas.

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

Serviços descartáveis

Sua classe de serviço não precisa ser descartável, mas os serviços que são serão descartados quando o cliente descartar seu proxy para seu serviço ou a conexão entre cliente e serviço for perdida. As interfaces descartáveis são testadas nesta ordem: System.IAsyncDisposable, , IDisposableMicrosoft.VisualStudio.Threading.IAsyncDisposable. 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 dos fios ao considerar o descarte. Seu Dispose método pode ser chamado em qualquer thread enquanto outro código em seu serviço está sendo executado (por exemplo, se uma conexão está sendo descartada).

Acionamento de exceções

Ao lançar exceções, considere lançar LocalRpcException com um ErrorCode específico para controlar o código de erro recebido pelo cliente no RemoteInvocationException. Fornecer aos clientes um código de erro pode permitir que eles se ramificam com base na natureza do erro melhor do que 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.

Consumindo outros serviços intermediados

Quando um serviço agenciado em si requer acesso a outro serviço agenciado, recomendamos o uso do que é fornecido à sua fábrica de serviços, mas é especialmente importante quando o registro do serviço intermediado define o IServiceBrokerAllowTransitiveGuestClients sinalizador.

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 um 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 agenciado e consumir serviços intermediados.

Serviço com estado

Estado por cliente

Uma nova instância dessa classe será criada para cada cliente que solicitar o serviço. Um campo na Calculator classe acima armazenaria um valor que poderia ser exclusivo para cada cliente. Suponha que adicionemos um contador que incrementa toda 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 de thread-safe. Ao usar o , as conexões remotas recomendadas ServiceJsonRpcDescriptorcom clientes podem incluir a execução simultânea dos métodos do serviço, conforme descrito neste 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 ser usada Interlocked.Increment(Int32) para incrementar o operationCounter campo.

Estado compartilhado

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

Suponha que queremos que o operationCounter definido acima conte todas as operações em todos os clientes do serviço. Precisaríamos elevar o campo para essa 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 do nosso Calculator serviço. Mais tarde, ao escrever o código para oferecer o serviço, veremos como essa State classe é criada uma vez e compartilhada com cada instância do Calculator serviço.

É especialmente importante ser thread-safe ao lidar com o estado compartilhado, porque nenhuma suposição pode ser feita em torno de vários clientes agendando suas chamadas de modo que elas nunca sejam feitas simultaneamente.

Se sua classe de estado compartilhada precisar acessar outros serviços agenciados, ela deverá usar o agente de serviços global em vez de um dos contextuais atribuídos a uma instância individual do serviço agenciado. O uso do agente de serviços global em um serviço agenciado traz consigo implicações de segurança quando o ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients sinalizador é definido.

Questões de segurança

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

Revise Como proteger um serviço agenciado e faça as mitigações de segurança necessárias antes de definir o AllowTransitiveGuestClients sinalizador.

O apelido de serviço

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

Um moniker de serviço é análogo ao nome completo qualificado para montagem de um tipo CLR. Ele deve ser globalmente exclusivo e, portanto, deve incluir o nome da sua empresa e talvez o nome da extensão como prefixos para o próprio nome do serviço.

Pode ser útil definir esse apelido em um static readonly campo 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 apelido diretamente, um cliente que se comunica por pipes em vez de um proxy exigirá o moniker.

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. Esse descritor de serviço é uma instância de um ServiceRpcDescriptortipo derivado. O descritor deve ser disponibilizado para todos os clientes que usarão um proxy para acessar esse serviço. A oferta do serviço também requer esse descritor.

Visual Studio define um tipo derivado 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 no RemoteInvocationException.

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

Podemos definir um descritor para o nosso serviço de calculadora assim:

/// <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 opção 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 Mais adequado para
MessagePack BigEndianInt32LengthHeader Alto desempenho
UTF8 (JSON) HttpLikeHeaders Interoperabilidade com outros sistemas JSON-RPC

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

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

Dica: Exponha seu descritor como em vez de qualquer tipo derivado que você usa como ServiceRpcDescriptor um detalhe de implementação. Isso lhe dá mais flexibilidade para alterar os detalhes da implementação posteriormente sem alterações de interrupção da 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 com um ServiceJsonRpcDescriptorType clientInterface parâmetro para especificar a interface da qual o cliente deve fornecer uma instância.

Controle de versão do descritor

Com o tempo, talvez você queira incrementar a versão do seu serviço. Nesse caso, você deve definir um descritor para cada versão que deseja suportar, usando uma versão ServiceMoniker específica exclusiva para cada uma. 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 esse padrão com sua VisualStudioServices classe, definindo o original ServiceRpcDescriptor como uma virtual propriedade sob a classe aninhada que representa a primeira versão que adicionou esse serviço agenciado. Quando precisamos alterar o protocolo de conexão ou adicionar/alterar a funcionalidade do serviço, o Visual Studio declara uma propriedade em uma override classe aninhada com versão posterior que retorna um novo ServiceRpcDescriptorarquivo .

Para um serviço definido e oferecido por uma extensão do Visual Studio, pode ser suficiente declarar outra propriedade 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 alteração de quebra de protocolo de fio, é necessário incrementar o número de versão do serviço agenciado e 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

Seu serviço intermediado deve ser criado quando uma solicitação chega, que é organizada por meio de uma etapa chamada oferta do serviço.

A fábrica de serviços

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

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

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

Normalmente, um serviço agenciado é instanciado uma vez por cliente. Isso é um desvio de outros serviços VS, que normalmente são instanciados uma vez e compartilhados entre todos os clientes. A criação de 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 é sua preferência CultureInfo , 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 deve ter seu serviço implementadoIDisposable, pois o primeiro cliente a descartar seu proxy levará ao descarte da instância de serviço compartilhado antes que outros clientes terminem de usá-la.

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

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

A state variável local está fora da fábrica de serviços e, portanto, é criada apenas uma vez e compartilhada entre 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)));

Nesse caso, o construtor de serviço pode ter a seguinte aparência, supondo que o 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 clientCallback campo agora pode ser invocado sempre que o serviço quiser invocar o cliente, até que a conexão seja descartada.

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

O BrokeredServiceFactory delegado usa um como parâmetro caso a fábrica de serviços seja um ServiceMoniker método compartilhado que cria vários serviços com base no moniker. 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.

Evite usar o delegado com o método, a menos que você use o AuthorizingBrokeredServiceFactoryIBrokeredServiceContainer.Proffer dentro de sua classe de IAuthorizationService serviço agenciada. Isso IAuthorizationServicedeve ser descartado com sua classe de serviço agenciada para evitar um vazamento de memória.

Registrando o serviço

A oferta de um serviço agenciado para o contêiner de serviço agenciado 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 inicie 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 agenciado.

O registro pode ser feito aplicando-se o ProvideBrokeredServiceAttribute à sua AsyncPackageclasse derivada. Este é o único lugar onde o ServiceAudience pode ser definido.

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

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

Se o serviço intermediado precisar ser exposto aos convidados do Live Share, o Audience deve incluir ServiceAudience.LiveShareGuest e a ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients propriedade definida como true . A configuração desses sinalizadores pode introduzir sérias vulnerabilidades de segurança e não deve ser feita sem antes estar em conformidade com as diretrizes em Como proteger um serviço agenciado.

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

Use o MEF para oferecer e registrar seu serviço

Isso requer o Visual Studio 2022 Atualização 2 ou posterior.

Um serviço agenciado 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 serem consideradas:

Vantagens e desvantagens Oferta de pacotes Exportação MEF
Disponibilidade ✅ O serviço agenciado está disponível imediatamente na inicialização do VS. ⚠️ O serviço intermediado pode ser atrasado na disponibilidade 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 entre plataformas ⚠️ O código específico do Visual Studio para Windows deve ser criado. ✅ O serviço agenciado em seu assembly pode ser carregado no Visual Studio para Windows, bem como Visual Studio para Mac.

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

  1. Confirme que você não tem nenhum código relacionado às duas últimas seções. Em particular, você não deve ter nenhum código que chame IBrokeredServiceContainer.Proffer e não deve aplicar o ProvideBrokeredServiceAttribute ao seu pacote (se houver).
  2. Implemente a IExportedBrokeredService interface em sua classe de serviço agenciada.
  3. Evite quaisquer dependências de thread principal em seu construtor ou setters de propriedade de importação. Use o IExportedBrokeredService.InitializeAsync método para inicializar seu serviço agenciado, onde as dependências de thread principal são permitidas.
  4. Aplique o ExportBrokeredServiceAttribute à sua classe de serviço agenciada, especificando as informações sobre o moniker do serviço, o público e quaisquer outras informações relacionadas ao registro necessárias.
  5. Se sua classe exigir descarte, implemente IDisposable em vez de já que IAsyncDisposable o MEF possui a vida útil do serviço e oferece suporte apenas ao descarte síncrono.
  6. Certifique-se de que seu source.extension.vsixmanifest arquivo lista o projeto que contém seu serviço intermediado como um assembly MEF.

Como uma parte do MEF, 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 as ExportBrokeredServiceAttribute derivações de System.ComponentModel.Composition.ExportAttribute e usando o mesmo namespace MEF em todo um tipo são necessárias.

Um serviço de agenciamento é único em poder importar algumas exportações especiais:

  • IServiceBroker, que deve ser utilizado para adquirir outros serviços intermediados.
  • ServiceMoniker, que pode ser útil quando você exporta várias versões do seu serviço agenciado e precisa detectar qual versão o cliente solicitou.
  • ServiceActivationOptions, que pode ser útil quando você exige 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 agenciado. Esse objeto não precisa ser descartado pela sua classe, pois ele será descartado automaticamente quando o serviço intermediado for descartado.

Seu serviço intermediado não deve usar MEF's ImportAttribute para adquirir outros serviços intermediados. Em vez disso, ele pode [Import]IServiceBroker e consulta para 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 agenciado

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 IExportedBrokeredService.Descriptor propriedade deve retornar um descritor com um apelido que corresponda ao que o cliente solicitou.

Considere este exemplo, em que o serviço de calculadora exportou 1.0 com formatação UTF8 e, posteriormente, adicionou 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!;
}