Fornecer um serviço agenciado
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 moniker de serviço para atribuir um nome e uma versão ao serviço.
- Um descritor que combina o apelido 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 seu serviço agenciado com um pacote VS ou faça as duas coisas com o MEF (a Managed Extensibility Framework).
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 o recurso de tipos de referência anuláveis do C#.
A interface do 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 de ServiceRpcDescriptor que 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 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 devolução compatível com assíncrono.
O ServiceJsonRpcDescriptor é o tipo derivado recomendado para serviços agenciados. Essa classe utiliza a biblioteca StreamJsonRpc quando o cliente e o serviço exigem que o RPC se comunique. StreamJsonRpc aplica determinadas restrições na interface de serviço, conforme descrito aqui.
A interface pode derivar de IDisposable, System.IAsyncDisposable ou até mesmo de Microsoft.VisualStudio.Threading.IAsyncDisposable , mas isso não é exigido pelo sistema. Os proxies de cliente gerados implementarão o IDisposable de qualquer maneira.
Uma interface simples de serviço de calculadora 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 agenciado que precisa "retornar a chamada" para o cliente pode definir uma segunda interface que serve como o contrato que um cliente deve 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 agenciado, mas com restrições adicionais no controle de versão.
Leia as Práticas recomendadas para criar um serviço agenciado para obter dicas sobre como projetar uma interface RPC de alto desempenho e preparada para o futuro.
Pode ser útil declarar essa interface em um assembly diferente 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 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
para garantir que seu serviço possa ser facilmente invocado de qualquer processo .NET, esteja ele executando .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 de 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 asserções comuns, como teste de eventos e muito mais.
Métodos
Declare que todos os argumentos e o valor de retorno foram completamente serializados. Se você estiver usando a classe base de teste mencionada acima, seu código pode ficar semelhante ao seguinte:
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 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
Todos os eventos declarados em sua interface também devem ser testados quanto à prontidão de RPC. Os eventos gerados de um serviço agenciado não causam uma falha de teste se falharem durante a serialização de RPC porque os eventos são do tipo "disparar e esquecer".
Se você estiver usando a classe base de teste mencionada acima, esse comportamento já estará embutido em alguns métodos auxiliares e pode ter esta aparência (com partes inalteradas omitidas para fins de 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));
}
}
Implementar o serviço
A classe de serviço deve implementar a interface de 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 IDisposable e, possivelmente, algumas outras interfaces selecionadas para suportar o sistema, portanto, uma conversão para outras interfaces implementadas pelo serviço falhará no cliente.
Considere o exemplo da 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 ValueTask<TResult> construído para que fique em conformidade com a interface de serviço.
Implementar 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á sobreviver ao tempo de vida dessa chamada de método para que o cliente possa continuar recebendo atualizações após a conclusão da chamada de método até que o cliente descarte o valor IDisposable retornado. Para facilitar isso, sua classe de serviço pode incluir uma coleção de assinaturas IObserver<T> que todas as atualizações feitas em seu estado enumerariam para atualizar todos os assinantes. Certifique-se de que a enumeração de sua coleção seja thread-safe em relação umas às outras e, especialmente, com as mutações nessa coleção que podem ocorrer por meio de assinaturas adicionais ou descartes dessas assinaturas.
Tome cuidado para que todas as atualizações postadas por meio de 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 OnCompleted ou OnError para evitar vazamentos de recursos no cliente e nos sistemas de RPC. Isso inclui o descarte do serviço, em que 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 o são serão descartados quando o cliente descartar seu proxy em 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.
Lembre-se da thread-safety 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 cair).
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 ramifiquem 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.
Consumir outros serviços agenciados
Quando um serviço agenciado em si requer acesso a outro serviço agenciado, recomendamos o uso do IServiceBroker que é fornecido ao seu alocador de serviço, mas é especialmente importante quando o registro do serviço agenciado define o sinalizador AllowTransitiveGuestClients.
Para estar em conformidade com essa diretriz, se nosso serviço de calculadora precisar 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 consumir serviços agenciados.
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 classe Calculator
acima armazenaria um valor que pode ser exclusivo para cada cliente.
Suponha que adicionamos um contador que é incrementado 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 agenciado deve ser escrito para seguir práticas thread-safe.
Ao usar o ServiceJsonRpcDescriptor recomendado, as conexões remotas com clientes podem incluir a execução simultânea dos métodos do seu 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 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 é instanciada pelo pacote VS 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 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 do 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 todas as instâncias do serviço Calculator
.
É especialmente importante ser thread-safe ao lidar com o estado compartilhado, pois 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 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 agente de serviço global em um serviço agenciado traz consigo implicações de segurança quando o sinalizador ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients é definido.
Questões de segurança
A segurança é uma consideração para o serviço agenciado se ele estiver registrado com o sinalizador 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 compartilhada do Live Share.
Leia Como proteger um serviço agenciado e faça as mitigações de segurança necessárias antes de definir o sinalizador AllowTransitiveGuestClients.
O moniker do serviço
Um serviço agenciado deve ter um nome e uma versão opcional serializáveis pelos quais um cliente pode solicitar o serviço. O ServiceMoniker é um wrapper conveniente para essas duas informações.
Um apelido de serviço é análogo ao nome completo qualificado pelo 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 extensão como prefixos para o próprio nome do serviço.
Pode ser útil definir esse moniker 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, é recomendável fornecer uma versão, já que isso oferece aos autores de serviço mais opções para manter a compatibilidade com clientes em alterações comportamentais.
O descritor do 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 sua interface RPC em um protocolo de conexão de forma eficiente. 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. A oferta do 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 em suas conexões RPC e cria um proxy local de alto desempenho para serviços locais que emula alguns dos comportamentos remotos, como o encapsulamento de exceções geradas pelo serviço no RemoteInvocationException.
O ServiceJsonRpcDescriptor suporta a configuração da classe JsonRpc para codificação JSON ou MessagePack do protocolo JSON-RPC. Recomendamos a codificação de MessagePack porque ela é mais compacta e pode ter desempenho dez vezes melhor.
Podemos definir um descritor para 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 MultiplexingStream.Options
como o 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 por JSON-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 são disponibilizadas no cliente.
Dica: exponha seu descritor como ServiceRpcDescriptor em vez de qualquer tipo derivado que você usa como um detalhe de implementação. Isso oferece mais flexibilidade para alterar os detalhes de implementação posteriormente sem alterações significativas 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 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.
Nesse 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, 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 um ServiceMoniker exclusivo e específico da versão 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 classe VisualStudioServices definindo o original ServiceRpcDescriptor como uma propriedade virtual
na 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 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 tenha usado o formatador UTF8 (JSON), e você perceba que alternar para o MessagePack proporcionaria um benefício significativo de desempenho. Como mudar o formatador é uma alteração de quebra de protocolo de conexão, isso requer incrementar o número de versão do serviço agenciado e um segundo descritor. Os dois descritores juntos podem ter a seguinte 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 a 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 bem baixa.
Oferecer o serviço
Seu serviço agenciado deve ser criado quando uma solicitação chega, que é organizada por meio de uma etapa chamada oferta do serviço.
O alocador de serviço
Use GlobalProvider.GetServiceAsync para solicitar o SVsBrokeredServiceContainer. Em seguida, chame IBrokeredServiceContainer.Proffer para esse contêiner para oferecer seu serviço.
No exemplo abaixo, oferecemos um serviço usando o campo CalculatorService
declarado anteriormente, que é definido como uma instância de um ServiceRpcDescriptor.
Passamos nosso alocador de serviço, que é um delegado de 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. Isso é diferente de outros serviços VS (Visual Studio), 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 é o seu CultureInfo preferido etc. Como veremos a seguir, isso também permite serviços mais interessantes que aceitam argumentos específicos para essa solicitação.
Importante
Um alocador de serviço que se desvia dessa diretriz e retorna uma instância de serviço compartilhada em vez de uma nova para cada cliente nunca deve fazer seu serviço implementar IDisposable, pois o primeiro cliente a descartar seu proxy levará ao descarte da instância de serviço compartilhada 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 o alocador 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, logo, é 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 passado:
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 o ServiceJsonRpcDescriptor foi criado 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 o ServiceMoniker como parâmetro caso o alocador de serviço seja um método compartilhado que cria vários serviços ou diferentes versões do serviço com base no apelido. Esse moniker 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 AuthorizingBrokeredServiceFactory com o método IBrokeredServiceContainer.Proffer, a menos que você use o IAuthorizationService dentro da classe do serviço agenciado. Isso IAuthorizationServicedeve ser descartado com sua classe de serviço agenciado para evitar 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 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 você ainda dá suporte.
A oferta de seu serviço com uma versão null
funcionará como um "pega-tudo" que corresponderá a qualquer solicitação do cliente para a qual não exista uma correspondência de versão precisa com um serviço registrado.
Por exemplo, é possível 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 o alocador de serviço que você ofereceu para essas versões exatas, enquanto um cliente que solicita a versão 8.0 leva à chamada do alocador de serviço oferecido com versão nula.
Como a versão solicitada pelo cliente é fornecida ao alocador de serviço, o alocador pode 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.
Uma solicitação de cliente para um serviço com uma versão null
only corresponde apenas a um serviço registrado 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 pega-tudo para não precisar oferecer repetidamente cada versão individual da seguinte forma:
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
Oferecer um serviço agenciado para o contêiner de serviço agenciado global ativará um lançamento, a menos que o serviço tenha sido registrado primeiro. O registro fornece um meio para o contêiner saber com antecedência quais serviços agenciados podem estar disponíveis e qual pacote VS carregar quando eles 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 o serviço agenciado for solicitado por um cliente.
O registro pode ser feito aplicando o ProvideBrokeredServiceAttribute à sua classe derivada de AsyncPackage. 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.Local, você opta por expor seu serviço agenciado a outros processos pertencentes à mesma sessão do Visual Studio.
Se o serviço agenciado precisar ser exposto a convidados do Live Share, o Audience deverá incluir ServiceAudience.LiveShareGuest, e a propriedade ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients definida como true
.
A configuração desses sinalizadores pode introduzir graves vulnerabilidades de segurança e não deve ser feita sem primeiro estar em conformidade com as diretrizes de Como proteger um serviço agenciado.
Ao incrementar a versão em seu ServiceMoniker, você deve 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 da versão mais antiga do serviço agenciado, 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.
O registro de seu serviço com uma versão null
funcionará como um "pega-tudo" que corresponderá a qualquer solicitação do cliente para a qual não exista uma versão precisa com um serviço registrado.
Por exemplo, é possível 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 Update 2 ou posterior.
Um serviço agenciado pode ser exportado por meio do 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 pacote | Exportação do MEF |
---|---|---|
Disponibilidade | ✅ O serviço agenciado está disponível imediatamente na inicialização do VS. | ⚠️ A disponibilidade do serviço agenciado pode atrasar até que o MEF seja inicializado no processo. Isso geralmente é rápido, mas pode levar vários segundos quando o cache do MEF está obsoleto. |
Prontidão multiplataforma | ⚠️ 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 no Visual Studio para Mac. |
Para exportar seu serviço agenciado via 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
IExportedBrokeredService
em sua classe de serviço agenciado. - Evite dependências de thread principal em seu construtor ou importe setters de propriedade. Use o método
IExportedBrokeredService.InitializeAsync
para inicializar o serviço agenciado, em que as dependências do thread principal são permitidas. - Aplique o
ExportBrokeredServiceAttribute
à sua classe de serviço agenciado, especificando as informações sobre o moniker de serviço, o público-alvo e qualquer outra informação relacionada ao registro necessária. - Se sua classe exigir descarte, implemente IDisposable em vez de IAsyncDisposable, já que o MEF possui o tempo de vida do serviço e dá suporte apenas ao descarte síncrono.
- Verifique se o arquivo
source.extension.vsixmanifest
lista o projeto que contém o serviço agenciado como um assembly do MEF.
Como uma parte do MEF, seu serviço agenciado 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 usa o mesmo namespace do MEF sempre que um tipo é necessário.
Um serviço agenciado é ú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, 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 por sua classe, pois ele será descartado automaticamente quando seu serviço agenciado for descartado.
Seu serviço agenciado não deve usar o ImportAttribute do MEF para adquirir outros serviços agenciados.
Em vez disso, ele pode [Import]
IServiceBroker consultar 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);
}
}
Exportar várias versões do serviço agenciado
O ExportBrokeredServiceAttribute
pode ser aplicado ao seu serviço agenciado várias vezes para oferecer várias versões do seu serviço agenciado.
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, 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!;
}
A partir da Atualização 12 do Visual Studio 2022 (17.12), um serviço com controle de versão null
pode ser exportado para corresponder a qualquer solicitação do 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 não oferece uma implementação da versão solicitada pelo cliente.
Rejeitar uma solicitação de serviço
Um serviço agenciado pode rejeitar a solicitação de ativação de um cliente ao lançar o método InitializeAsync. O lançamento faz com que a ServiceActivationFailedException seja devolvido ao cliente.