Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Um serviço agenciado consiste nos seguintes elementos:
- uma interface que declara a funcionalidade do serviço e serve como um contrato entre o serviço e seus clientes.
- Uma implementação dessa interface.
- um identificador de serviço para atribuir um nome e uma versão ao serviço.
- Um descritor que combina o moniker de serviço com o comportamento para lidar com RPC (Chamada de Procedimento Remoto) quando necessário.
- Ofereça o alocador de serviço e registre o serviço agenciado com um pacote VS ou ambos com MEF (Managed Extensibility Framework).
Cada um dos itens na lista anterior é descrito em detalhes nas seções a seguir.
Com todo o código neste artigo, é altamente recomendável ativar o recurso 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 ServiceRpcDescriptorque seu serviço usará para garantir que a interface possa ser usada no RPC quando o cliente e o serviço forem executados em processos diferentes.
Essas restrições normalmente 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 agenciados. Essa classe utiliza a biblioteca de StreamJsonRpc quando o cliente e o serviço exigem que o RPC se comunique. O StreamJsonRpc aplica determinadas restrições na interface de serviço como 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 maneira.
Uma interface de serviço de calculadora simples pode ser declarada desta forma:
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 justificar um método assíncrono, sempre usamos assinaturas de método assíncrono nessa interface porque essa interface é usada para gerar o proxy 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 agenciado que precisa realizar um "retorno de chamada" para o cliente pode definir uma segunda interface que atua como um contrato que o cliente precisa implementar e fornecer por meio da propriedade ServiceActivationOptions.ClientRpcTarget 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 ao controle de versão.
Examine as Práticas recomendadas para criar um serviço agenciado para obter dicas sobre como criar uma interface RPC com desempenho 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 referenciar a 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 que outras extensões possam fazer referência, enquanto reserva sua própria extensão para enviar a implementação do serviço.
Considere direcionar o assembly que declara sua interface de serviço como netstandard2.0 para garantir que seu serviço possa ser facilmente invocado de qualquer processo do .NET, seja executando .NET Framework, .NET Core, .NET 5 ou posterior.
Testes
Os testes automatizados devem ser escritos junto com a interface de serviço para verificar a preparação para RPC da interface.
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 sua classe de teste de interface. Essa classe inclui alguns testes básicos de convenção para sua interface, métodos para ajudar com declarações comuns, como testes de evento e muito mais.
Métodos
Afirme que todos os argumentos e o valor retornado foram serializados completamente. Se você estiver usando a classe base de teste mencionada acima, seu código poderá 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 nomes idênticos.
Você pode adicionar um campo internal 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 certo foi invocado com os argumentos certos.
Eventos
Todos os eventos declarados em sua interface também devem ser testados quanto à preparação para RPC. Os eventos gerados de um serviço agenciado não causam uma falha de teste, se falharem durante a serialização do RPC, porque os eventos são "disparados e esquecidos".
Se você estiver usando a classe base de teste mencionada acima, esse comportamento já está integrado a 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));
}
}
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 qualquer outra interface além daquela usada para RPC. O proxy gerado no cliente implementa apenas a interface de serviço, IDisposablee, possivelmente, algumas outras interfaces selecionadas para dar suporte ao sistema, portanto, 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 retornado em um tipo de retorno ValueTask<TResult> construído 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 esta aparência:
Task<IDisposable> SubscribeAsync(IObserver<YourDataType> observer);
O argumento IObserver<T> normalmente precisará permanecer ativo durante a vida útil dessa chamada de método para que o cliente possa continuar a receber atualizações até a conclusão da chamada desse método, até que o cliente descarte o valor de IDisposable retornado. Para facilitar isso, sua classe de serviço pode incluir uma coleção de assinaturas IObserver<T>, de modo que quaisquer atualizações feitas em seu estado sejam enumeradas para atualizar todos os assinantes. Verifique se a enumeração da coleção é thread-safe em relação umas às outras e, principalmente, com as mutações nessa coleção que podem ocorrer por meio de assinaturas adicionais ou baixas dessas assinaturas.
Tome cuidado para que todas as atualizações postadas por meio de OnNext mantenham a ordem na qual as alterações de estado foram introduzidas no 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. Isso inclui a baixa do serviço, na qual todas as assinaturas restantes devem ser concluídas explicitamente.
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
A classe de serviço não precisa ser descartável, mas os serviços que forem descartáveis serão descartados quando o cliente descartar o proxy do 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 do thread ao considerar a baixa. Seu método Dispose pode ser chamado em qualquer thread enquanto outro código em seu serviço estiver em execução (por exemplo, se uma conexão estiver sendo descartada).
Lançar 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 tomem decisões com base na natureza do erro melhor 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.
Consumindo outros serviços agenciados
Quando o próprio serviço agenciado requer acesso a outro serviço agenciado, recomendamos o uso do IServiceBroker que é fornecido ao alocador de serviço, mas é especialmente importante quando o registro de serviço agenciado define o sinalizador de AllowTransitiveGuestClients.
Para estar em conformidade com essa diretriz, se nosso serviço de calculadora precisasse de outros serviços agenciados 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 como consumir serviços agenciados.
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 pode ser exclusivo para cada cliente.
Suponha que adicionemos um contador que incrementa sempre 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);
}
}
O serviço agenciado deve ser escrito para seguir as práticas de thread-safe.
Ao usar o ServiceJsonRpcDescriptor recomendado, as conexões remotas com 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 um AppDomain com o serviço, ele pode chamar o serviço simultaneamente a partir de várias 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 deverá ser definido em uma classe distinta que seja instanciada pelo seu pacote VS e passada como argumento ao 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 nesta nova classe de estado:
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 serviço de Calculator.
Posteriormente, 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, pois não é possível supor que vários clientes agendarão as chamadas de forma que nunca ocorram simultaneamente.
Se a classe de estado compartilhado precisar acessar outros serviços agenciados, ela deverá usar o agente de serviço global, em vez de um dos contextuais atribuídos a uma instância individual do serviço agenciado. O uso do global service broker em um serviço agenciado acarreta implicações de segurança quando o sinalizador ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients é ativado.
Preocupações com a segurança
A segurança é uma consideração para seu serviço intermediado se ele estiver registrado com a flag ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients, o que o expõe a um possível acesso de outros usuários em outros computadores que estão participando de uma sessão Live Share compartilhada.
Revise Como proteger um serviço intermediado e tome as mitigações de segurança necessárias antes de definir o sinalizador AllowTransitiveGuestClients.
O apelido do serviço
Um serviço agenciado deve ter um nome serializável e uma versão opcional pela qual um cliente pode solicitar o serviço. Um ServiceMoniker é um wrapper prático para essas duas informações.
Um moniker de serviço é análogo ao nome totalmente qualificado para assembly de um tipo CLR (Common Language Runtime). Ele deve ser globalmente exclusivo e, portanto, deve incluir o nome da sua empresa e talvez seu nome de extensão como prefixos para o próprio nome do serviço.
Pode ser útil definir esse moniker em um campo de static readonly para uso em outro lugar:
public static readonly ServiceMoniker Moniker = new ServiceMoniker("YourCompany.Extension.Calculator", new Version("1.0"));
Embora a maior parte do serviço possa não usar o 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 moniker, é recomendável fornecer uma, pois isso oferece aos autores de serviço mais opções para manter a compatibilidade com os clientes, apesar de 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 transmissão. Esse 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 esse serviço. Oferecer o serviço também requer esse 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 exceções de encapsulamento geradas pelo serviço em RemoteInvocationException.
O ServiceJsonRpcDescriptor dá suporte à configuração da classe JsonRpc para codificação JSON ou MessagePack do protocolo JSON-RPC. Recomendamos a codificação MessagePack porque ela é mais compacta e pode ter um desempenho 10X maior.
Podemos definir um descritor para nosso serviço de calculadora como este:
/// <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 | Melhor para |
|---|---|---|
| MessagePack | BigEndianInt32LengthHeader | Alto desempenho |
| UTF8 (JSON) | HttpLikeHeaders | Interoperabilidade com outros sistemas de JSON-RPC |
Ao especificar o objeto MultiplexingStream.Options como o parâmetro final, a conexão RPC compartilhada entre cliente e serviço é apenas um canal em um MultiplexingStream, que é compartilhado com a conexão JSON-RPC para habilitar a transferência eficiente de dados binários grandes por JSON-RPC.
A estratégia de ExceptionProcessing.ISerializable faz com que as exceções geradas do serviço sejam serializadas e preservadas, à medida que o Exception.InnerException para o RemoteInvocationException é gerado no cliente. Sem essa configuração, informações de exceção menos detalhadas estão disponíveis no cliente.
Dica: exponha o descritor como ServiceRpcDescriptor em vez de qualquer tipo derivado usado como um detalhe de implementação. Isso oferece mais flexibilidade para alterar os detalhes da implementação posteriormente sem que a API altere as alterações.
Inclua uma referência à interface de serviço no comentário do documento XML no descritor para facilitar aos usuários o consumo do seu serviço. Faça referência também à interface que seu serviço aceita como o 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
Ao longo do tempo, talvez você queira incrementar a versão do serviço. Nesse caso, você deve definir um descritor para cada versão que deseja suportar, usando uma ServiceMoniker exclusiva para cada versão. Dar 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 a classe VisualStudioServices ao definir o ServiceRpcDescriptor original como uma propriedade virtual dentro da classe aninhada que representa a primeira versão que adicionou esse serviço agenciado.
Quando precisamos alterar o protocolo de transmissã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 de 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 MessagePack proporcionaria um benefício de desempenho significativo. Como a alteração do formatador é uma alteração de quebra de protocolo de transmissão, ela requer o incremento do 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 tenhamos que disponibilizar e registrar dois serviços), 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, o que é organizado por meio de um processo chamado proposta do serviço.
A fábrica de serviços
Use GlobalProvider.GetServiceAsync para solicitar o SVsBrokeredServiceContainer. Em seguida, chame IBrokeredServiceContainer.Proffer nesse contêiner para oferecer o serviço.
No exemplo a seguir, oferecemos um serviço usando o campo CalculatorService declarado anteriormente, que é definido como uma instância de ServiceRpcDescriptor.
Transferimos para ela nosso alocador de serviço, 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 agenciado normalmente é instanciado uma vez por cliente. Essa é uma saída de outros serviços do VS (Visual Studio), que normalmente são instanciados uma vez e compartilhados em 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 podem manter o estado por cliente sobre o nível de autorização em que o cliente opera, qual é a CultureInfo preferida dele etc. Como veremos a seguir, ele também permite serviços mais interessantes que aceitam argumentos específicos a essa solicitação.
Importante
Um alocador de serviço 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 o serviço implementado IDisposable, já que o primeiro cliente a descartar o proxy levará à baixa 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 oferecer 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 está fora do alocador de serviço 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 poderiam ser passados:
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 esta aparência, supondo 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 delegado BrokeredServiceFactory usa um ServiceMoniker como um parâmetro, caso o alocador de serviço seja um método compartilhado que cria vários serviços ou versões distintas do serviço com base no moniker. Esse moniker vem do cliente e inclui a versão do serviço que eles esperam. 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 AuthorizingBrokeredServiceFactory com o método IBrokeredServiceContainer.Proffer, a menos que você use o IAuthorizationService dentro da classe de serviço agenciada. Esse IAuthorizationServicedeve ser descartado com a classe de serviço agenciada para evitar um vazamento de memória.
Suporte a várias versões do serviço
Ao incrementar a versão no ServiceMoniker, você deve oferecer cada versão do serviço agenciado para a qual pretende responder às solicitações do cliente. Isso é feito chamando o método IBrokeredServiceContainer.Proffer com cada ServiceRpcDescriptor que ainda tem suporte.
Oferecer o serviço com uma versão null servirá como um "catch all" que corresponderá a qualquer solicitação do cliente para a qual não exista uma correspondência exata da versão com um serviço registrado.
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 o serviço com 1.0 ou 1.1 invocam o alocador de serviço que você ofereceu para essas versões exatas, enquanto um cliente que solicita a versão 8.0 leva à invocação do alocador de serviço que você ofereceu para versões não especificadas.
Como a versão solicitada pelo cliente é fornecida ao service factory, a fábrica pode tomar uma decisão sobre como configurar o serviço para esse cliente específico ou se deseja retornar null para significar uma versão sem suporte.
Uma solicitação de cliente para um serviço com uma versão nullapenas corresponde a um serviço registrado e oferecido com uma versão null.
Considere um caso em que você publicou muitas versões do 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 a necessidade de 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.
}));
Registrando o serviço
A oferta de um serviço agenciado para o contêiner de serviço global agenciado será gerada, a menos que o serviço tenha sido registrado pela primeira vez. O registro fornece um meio para o contêiner saber com antecedência quais serviços agenciados podem estar disponíveis e qual pacote Visual Studio (VS) deverá ser carregado, quando eles forem solicitados para executar o código de disponibilização. Isso permite que o Visual Studio inicie rapidamente, sem carregar todas as extensões com antecedência, mas pode carregar a extensão necessária quando solicitado por um cliente de seu serviço agenciado.
O registro pode ser realizado aplicando o ProvideBrokeredServiceAttribute à classe derivada de AsyncPackage. Este é o único lugar onde o ServiceAudience pode ser configurado.
[ProvideBrokeredService("YourCompany.Extension.Calculator", "1.0", Audience = ServiceAudience.Local)]
O Audience padrão é ServiceAudience.Process, que expõe o serviço agenciado apenas a outro código dentro do mesmo processo. Ao definir ServiceAudience.Local, você aceita expor seu serviço agenciado a outros processos que pertencem à mesma sessão do Visual Studio.
Se o serviço agenciado precisar ser exposto aos convidados 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 no ServiceMoniker, você precisa registrar cada versão do serviço agenciado para a qual pretende responder às solicitações do cliente. Ao dar 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 do Live Share em que cada versão do Visual Studio que está compartilhando a sessão pode ser uma versão diferente.
Registrar seu serviço com uma versão null servirá como um 'pega-tudo' que corresponderá a qualquer solicitação de cliente para o qual uma versão precisa com um serviço registrado não exista.
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.
Usar 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:
| Compromisso | Oferta de pacote | exportação de MEF |
|---|---|---|
| Disponibilidade | ✅ O serviço intermediado está disponível imediatamente na inicialização do Visual Studio. | ⚠️ A disponibilidade do serviço agenciado pode ser atrasada até que o MEF tenha sido inicializado no decorrer do processo. Isso geralmente é rápido, mas pode levar vários segundos quando o cache MEF está obsoleto. |
| Prontidão entre plataformas | ⚠️ O código específico do Visual Studio para Windows precisa ser criado. | ✅ O serviço agenciado no assembly pode ser carregado no Visual Studio para Windows, assim como no Visual Studio para Mac. |
Para exportar seu serviço agenciado por meio do MEF em vez de usar pacotes VS:
- Confirme se 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).
- Implemente a interface
IExportedBrokeredServiceem sua classe de serviço agenciada. - Evite as dependências do thread principal no construtor ou ao importar setters de propriedade. Utilize o método
IExportedBrokeredService.InitializeAsyncpara inicializar o serviço agenciado, no qual são permitidas as dependências do thread principal. - Aplique o
ExportBrokeredServiceAttributeà classe de serviço intermediada, especificando as informações sobre o moniker de serviço, o público-alvo e quaisquer outras informações necessárias relacionadas ao registro. - Se sua classe exigir descarte, implemente IDisposable em vez de IAsyncDisposable, pois o MEF gerencia o ciclo de vida do seu serviço e oferece suporte apenas para descarte síncrono.
- Verifique se o arquivo
source.extension.vsixmanifestlista o projeto que contém o serviço agenciado como um assembly MEF.
Como parte do MEF, o serviço agenciado pode importar qualquer outra parte do MEF como parte do escopo padrão.
Ao fazer isso, use System.ComponentModel.Composition.ImportAttribute em vez de System.Composition.ImportAttribute.
Isso ocorre porque o ExportBrokeredServiceAttribute deriva de System.ComponentModel.Composition.ExportAttribute e o uso do mesmo namespace MEF em um tipo é necessário.
Um serviço de corretagem é único por poder importar algumas exportações especiais.
- IServiceBroker, que deve ser usado para adquirir outros serviços agenciados.
- ServiceMoniker, que pode ser útil quando você exporta várias versões do serviço agenciado e precisa detectar qual versão o cliente solicitou.
- ServiceActivationOptions, o que pode ser útil quando você exige que os 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 classe, pois será eliminado automaticamente quando o serviço agenciado for descartado.
O serviço agenciado não deve usar o ImportAttribute do MEF para adquirir outros serviços agenciados.
Em vez disso, ele pode [Import]IServiceBroker e consultar os serviços agenciados da maneira tradicional.
Saiba mais em Como consumir um serviço agenciado.
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 múltiplas versões do serviço agenciado
O ExportBrokeredServiceAttribute pode ser aplicado ao serviço intermediado várias vezes para oferecer múltiplas versões do serviço intermediado.
Sua implementação da propriedade IExportedBrokeredService.Descriptor deve retornar um descritor com um moniker 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, adiciona uma exportação 1.1 para aproveitar as vitórias de desempenho do uso da formatação do 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 serviço de um cliente, independentemente da versão, incluindo uma solicitação com versão null.
Esse serviço pode retornar null da propriedade Descriptor para rejeitar uma solicitação de cliente quando ele não oferece uma implementação da versão solicitada pelo cliente.
Rejeitando uma solicitação de serviço
Um serviço agenciado pode rejeitar a solicitação de ativação de um cliente lançando uma exceção no método InitializeAsync. O lançamento faz com que um ServiceActivationFailedException seja lançado para o cliente.