Práticas recomendadas para projetar um serviço agenciado
Siga as orientações gerais e restrições documentadas para interfaces RPC para StreamJsonRpc.
Além disso, as diretrizes a seguir se aplicam aos serviços intermediados.
Assinaturas de método
Todos os métodos devem ter um CancellationToken parâmetro como seu último parâmetro. Esse parâmetro geralmente não deve ser um parâmetro opcional para que os chamadores tenham menos probabilidade de omitir acidentalmente o argumento. Mesmo que se espere que a implementação do método seja trivial, fornecer um CancellationToken permite que o cliente cancele sua própria solicitação antes que ela seja transmitida ao servidor. Ele também permite que a implementação do servidor evolua para algo mais caro sem ter que atualizar o método para adicionar o cancelamento como uma opção mais tarde.
Considere evitar várias sobrecargas do mesmo método em sua interface RPC. Embora a resolução de sobrecarga geralmente funcione (e os testes devem ser escritos para verificar se funciona), ela depende da tentativa de desserializar argumentos com base nos tipos de parâmetros de cada sobrecarga, resultando em exceções de primeira chance sendo lançadas como uma parte regular da escolha de uma sobrecarga. Como queremos minimizar o número de exceções de primeira chance lançadas em caminhos de sucesso, é preferível simplesmente ter apenas um método com um nome próprio.
Tipos de parâmetro e retorno
Lembre-se de que todos os argumentos e valores de retorno trocados por RPC são apenas dados. Todos eles são serializados e enviados por fio. Quaisquer métodos definidos nesses tipos de dados operam apenas nessa cópia local dos dados e não têm como se comunicar com o serviço RPC que os produziu. As únicas exceções a esse comportamento de serialização são os tipos exóticos para os quais StreamJsonRpc tem suporte especial.
Considere usar ValueTask<T>
over Task<T>
como o tipo de retorno de métodos, pois ValueTask<T>
incorre em menos alocações.
Ao usar a variedade não genérica (por exemplo, e ValueTask) é menos importante, Task mas ValueTask ainda pode ser preferível.
Esteja ciente das restrições ValueTask<T>
de uso conforme documentado nessa API. Esta postagem de blog e vídeo pode ser útil para decidir qual tipo usar também.
Tipos de dados personalizados
Considere definir todos os tipos de dados como imutáveis, o que permite o compartilhamento mais seguro dos dados em um processo sem copiar e ajuda a reforçar a ideia para os consumidores de que eles não podem alterar os dados que recebem em resposta a uma consulta sem colocar outro RPC.
Defina seus tipos de dados como class
em vez de struct
ao usar , o que evita o custo de boxe (potencialmente repetido) ao usar ServiceJsonRpcDescriptor.Formatters.UTF8Newtonsoft.Json.
Boxe não ocorre ao usarServiceJsonRpcDescriptor.Formatters.MessagePack, então structs pode ser uma opção adequada se você estiver comprometido com esse formatador.
Considere implementar IEquatable<T> e substituir GetHashCode() métodos em Equals(Object) seus tipos de dados, o que permite que o cliente armazene, compare e reutilize com eficiência os dados recebidos com base no fato de serem iguais aos dados recebidos em outro momento.
Use o para dar suporte à serialização de tipos polimórficos DiscriminatedTypeJsonConverter<TBase> usando JSON.
Coleções
Use interfaces de coleções somente leitura em assinaturas de método RPC (por exemplo, ) em vez de tipos concretos (por exemplo, IReadOnlyList<T>List<T> ou T[]
), o que permite uma desserialização potencialmente mais eficiente.
Evite IEnumerable<T>.
Sua falta de uma propriedade leva a um código ineficiente e implica em possível geração tardia de dados, o que não se aplica em um Count
cenário de RPC.
Use IReadOnlyCollection<T> para coleções não ordenadas ou IReadOnlyList<T> para coleções ordenadas.
Considere o IAsyncEnumerable<T>. Qualquer outro tipo de coleção ou IEnumerable<T> resultará no envio de toda a coleção em uma mensagem. O uso IAsyncEnumerable<T> permite uma pequena mensagem inicial e fornece ao receptor os meios para obter quantos itens da coleção quiser, enumerando-a de forma assíncrona. Saiba mais sobre esse novo padrão.
Padrão do observador
Considere usar o padrão de design de observador em sua interface. Essa é uma maneira simples de o cliente assinar dados sem as muitas armadilhas que se aplicam ao modelo de eventos tradicional descrito na próxima seção.
O padrão do observador pode ser tão simples quanto este:
Task<IDisposable> SubscribeAsync(IObserver<YourDataType> observer);
Os IDisposable e IObserver<T> tipos usados acima são dois dos tipos exóticos em StreamJsonRpc, então eles obtêm um comportamento especialmente empacotado em vez de serem serializados como meros dados.
Eventos
Os eventos podem ser problemáticos sobre RPC por várias razões e recomendamos o padrão de observador descrito acima.
Lembre-se de que o serviço não tem visibilidade de quantos manipuladores de eventos o cliente anexou quando o serviço e o cliente estão em processos separados. JsonRpc sempre anexará exatamente um manipulador responsável por propagar o evento para o cliente. O cliente pode ter zero ou mais manipuladores conectados no lado oposto.
A maioria dos clientes RPC não terá manipuladores de eventos conectados quando forem conectados pela primeira vez. Evite aumentar o primeiro evento até que o cliente tenha invocado um método "Subscribe*" em sua interface para indicar interesse e prontidão para receber eventos.
Se seu evento indicar um delta no estado (por exemplo, um novo item adicionado a uma coleção), considere gerar todos os eventos passados ou descrever todos os dados atuais como se fossem novos no argumento de evento quando um cliente se inscrever para ajudá-los a "sincronizar" com nada além de código de manipulação de eventos.
Considere aceitar argumentos extras no método "Subscribe*" mencionado acima se o cliente quiser expressar interesse em um subconjunto de dados ou notificações, para reduzir o tráfego de rede e a CPU necessários para encaminhar essas notificações.
Considere não oferecer um método que retorne o valor atual se você também estiver expondo um evento para receber notificações de alteração ou desencorajar ativamente os clientes de usá-lo em combinação com o evento. Um cliente que assina um evento para dados e chama um método para obter o valor atual pode correr contra alterações nesse valor e perder um evento de alteração ou não saber como reconciliar um evento de alteração em um thread com o valor obtido em outro thread. Essa preocupação é geral para qualquer interface, não apenas quando está sobre RPC.
Convenções de nomenclatura
- Use o sufixo em interfaces RPC e um prefixo
Service
simplesI
. - Não use o sufixo
Service
para classes em seu SDK. Sua biblioteca ou wrapper RPC deve usar um nome que descreva exatamente o que ele faz, evitando o termo "serviço". - Evite o termo "remoto" em nomes de interface ou membros. Lembre-se de que os serviços intermediados são aplicados de forma ideal tanto em cenários locais quanto remotos.
Preocupações de compatibilidade de versão
Queremos que qualquer serviço intermediado que esteja exposto a outras extensões ou exposto pelo Live Share seja compatível com frente e para trás, o que significa que devemos assumir que um cliente pode ser mais antigo ou mais recente do que o serviço e que a funcionalidade deve ser aproximadamente igual à da menor das duas versões aplicáveis.
Primeiro, vamos rever a terminologia de mudança de quebra:
Alteração de quebra binária: uma alteração de API que faria com que outro código gerenciado compilado em uma versão anterior do assembly falhasse ao vincular em tempo de execução à nova. Os exemplos incluem:
- Alterar a assinatura de um membro público existente.
- Renomear um membro público.
- Removendo um tipo público.
- Adicionar um membro abstrato a um tipo ou qualquer membro a uma interface.
Mas as seguintes alterações não são de quebra binária:
- Adicionar um membro não abstrato a uma classe ou struct.
- Adicionar uma implementação de interface completa (não abstrata) a um tipo existente.
Alteração de quebra de protocolo: uma alteração na forma serializada de algum tipo de dados ou chamada de método RPC de modo que a parte remota não possa desserializá-la e processá-la corretamente. Os exemplos incluem:
- Adicionando parâmetros necessários a um método RPC.
- Remover um membro de um tipo de dados que anteriormente era garantido como não nulo.
- Adicionando um requisito de que uma chamada de método deve ser colocada antes de outras operações pré-existentes.
- Adicionar, remover ou alterar um atributo em um campo ou propriedade que controla o nome serializado dos dados nesse membro.
- (MessagePack): alterando a propriedade ou
KeyAttribute
o DataMemberAttribute.Order número inteiro de um membro existente.
Mas as mudanças a seguir não são de quebra de protocolo:
- Adicionar um membro opcional a um tipo de dados.
- Adicionando membros a interfaces RPC.
- Adicionando parâmetros opcionais a métodos existentes.
- Alterar um tipo de parâmetro que representa um inteiro ou flutuante para um com maior comprimento ou precisão (por exemplo,
int
para oufloat
paralong
double
). - Renomeando um parâmetro. Isso tecnicamente é interrompido para clientes que usam argumentos nomeados JSON-RPC, mas clientes que usam o uso de argumentos posicionais por padrão e não seriam afetados por uma alteração de nome de ServiceJsonRpcDescriptor parâmetro. Isso não tem nada a ver com se o código-fonte do cliente usa a sintaxe de argumento nomeado, para a qual uma renomeação de parâmetro seria uma alteração de quebra de origem.
Mudança de comportamento: uma alteração na implementação de um serviço intermediado que adiciona ou altera o comportamento de modo que os clientes mais antigos podem funcionar mal. Os exemplos incluem:
- Não inicializando mais um membro de um tipo de dados que sempre foi inicializado anteriormente.
- Lançar uma exceção sob uma condição que anteriormente poderia ser concluída com êxito.
- Retornando um erro com um código de erro diferente do que foi retornado anteriormente.
Mas não são mudanças de quebra de comportamento:
- Lançando um novo tipo de exceção (porque todas as exceções são encapsuladas de RemoteInvocationException qualquer maneira).
Quando as alterações de quebra são necessárias, elas podem ser feitas com segurança, registrando-se e oferecendo um novo apelido de serviço. Esse apelido pode compartilhar o mesmo nome, mas com um número de versão maior. A interface RPC original pode ser reutilizável se não houver nenhuma alteração de quebra binária. Caso contrário, defina uma nova interface para a nova versão do serviço. Evite quebrar clientes antigos continuando a registrar, oferecer e oferecer suporte à versão mais antiga também.
Queremos evitar todas essas alterações de quebra, exceto para adicionar membros às interfaces RPC.
Adicionando membros a interfaces RPC
Não adicione membros a uma interface de retorno de chamada do cliente RPC, pois muitos clientes podem implementar essa interface e adicionar membros resultaria na liberação TypeLoadException do CLR quando esses tipos são carregados, mas não implementam os novos membros da interface. Se você precisar adicionar membros para invocar em um destino de retorno de chamada de cliente RPC, defina uma nova interface (que pode derivar do original) e, em seguida, siga o processo padrão para oferecer seu serviço agenciado com um número de versão incrementado e ofereça um descritor com o tipo de interface de cliente atualizado especificado.
Você pode adicionar membros a interfaces RPC que definem um serviço agenciado. Esta não é uma alteração de quebra de protocolo, e é apenas uma alteração de quebra binária para aqueles que implementam o serviço, mas presumivelmente você estaria atualizando o serviço para implementar o novo membro também. Como nossa orientação é que ninguém deve implementar a interface RPC, exceto o próprio serviço intermediado (e os testes devem usar estruturas simuladas), adicionar um membro a uma interface RPC não deve quebrar ninguém.
Esses novos membros devem ter comentários xml doc que identificam qual versão de serviço adicionou esse membro pela primeira vez. Se um cliente mais recente chamar o método em um serviço mais antigo que não implementa o método, esse cliente poderá capturar RemoteMethodNotFoundException. Mas esse cliente pode (e provavelmente deve) prever a falha e evitar a chamada em primeiro lugar. As práticas recomendadas para adicionar membros a serviços existentes incluem:
- Se esta for a primeira alteração dentro de uma versão do seu serviço: Aumente a versão secundária no seu moniker de serviço quando adicionar o membro e declare o novo descritor.
- Atualize seu serviço para se registrar e oferecer a nova versão, além da versão antiga.
- Se você tiver um cliente do serviço agenciado, atualize o cliente para solicitar a versão mais recente e volte a solicitar a versão mais antiga se a versão mais recente voltar como nula.