Contratos públicos, como garantir sua consistência

  • Seu sistema consiste em muitos serviços interconectados?
  • ainda atualiza manualmente o código de serviço ao alterar a API pública?
  • as mudanças em seus serviços geralmente prejudicam o trabalho de outras pessoas, e outros desenvolvedores o odeiam por isso?

Se você respondeu sim pelo menos uma vez, então seja bem-vindo!

Termos e Condições


Contratos públicos, especificações - interfaces públicas através das quais você pode interagir com o serviço. No texto, eles significam a mesma coisa.

Sobre o que é o artigo


Aprenda como reduzir o tempo gasto no desenvolvimento de serviços da Web usando ferramentas para descrição unificada de contratos e geração automática de código.

O uso adequado das técnicas e ferramentas descritas abaixo permitirá que você implante rapidamente novos recursos e não quebre os antigos.

Como é o problema


Existe um sistema que consiste em vários serviços. Esses serviços são atribuídos a diferentes equipes.



Os serviços ao consumidor dependem do provedor de serviços.
O sistema evolui e um dia o provedor de serviços altera seus contratos públicos.



Se os serviços ao consumidor não estiverem prontos para mudança, o sistema deixará de funcionar completamente.



Como resolver este problema


A equipe de serviço do fornecedor corrigirá tudo


Isso pode ser feito se a equipe do fornecedor possuir a área de assunto de outros serviços e tiver acesso aos seus repositórios git. Isso funcionará apenas em pequenos projetos quando houver poucos serviços dependentes. Esta é a opção mais barata. Se possível, você deve usá-lo.

Atualize o código do seu serviço para a equipe do consumidor


Por que os outros estão quebrando, mas estamos reparando?

No entanto, a questão principal é como consertar seu serviço, como é o contrato agora? Você precisa aprender o novo código de serviço do provedor ou entrar em contato com sua equipe. Passamos um tempo estudando o código e interagindo com outra equipe.

Pense no que fazer para impedir que o problema apareça


A opção mais razoável a longo prazo. Considere isso na próxima seção.

Como evitar a manifestação de um problema


O ciclo de vida do desenvolvimento de software pode ser representado em três estágios: design, implementação e teste.

Cada uma das etapas precisa ser expandida da seguinte maneira:

  1. na fase de projeto, defina declarativamente os contratos;
  2. durante a implementação, geramos código de servidor e cliente sob contratos;
  3. durante o teste, verificamos contratos e tentamos levar em consideração as necessidades do cliente (CDC).

Cada uma das etapas é explicada abaixo com o exemplo do nosso problema.

Como o problema parece conosco




É assim que nosso ecossistema se parece.
Círculos são serviços e setas são canais de comunicação entre eles.

Frontend é um aplicativo cliente baseado na Web.

A maioria das setas leva ao Serviço de Armazenamento. Ele armazena documentos. Este é o serviço mais importante. Afinal, nosso produto é um sistema de gerenciamento eletrônico de documentos.

Se este serviço alterar seus contratos, o sistema deixará de funcionar imediatamente.



As fontes do nosso sistema são escritas principalmente em c #, mas também existem serviços no Go e Python. Nesse contexto, não importa o que os outros serviços na figura fazem.



Cada serviço tem sua própria implementação de cliente para trabalhar com o serviço de armazenamento. Ao alterar contratos, você deve atualizar manualmente o código em cada projeto.

Gostaria de me afastar da atualização manual para a automática. Isso ajudará a aumentar a taxa na qual o código do cliente é alterado e reduz os erros. Erros são erros de digitação no URL, erros devido a descuido, etc.

No entanto, essa abordagem não corrige erros na lógica de negócios do cliente. Você só pode ajustá-lo manualmente.

Do problema à tarefa


No nosso caso, é necessário implementar a geração automática de código do cliente.
Ao fazer isso, o seguinte deve ser considerado:

  • lado do servidor - os controladores já estão gravados;
  • o navegador é um cliente do serviço;
  • Os serviços se comunicam por HTTP.
  • a geração deve ser ajustada. Por exemplo, para suportar o JWT.

Questões


No decurso da solução do problema, surgiram perguntas:

  • qual ferramenta escolher;
  • como conseguir contratos;
  • onde colocar os contratos;
  • onde colocar o código do cliente;
  • em que ponto fazer a geração.

A seguir estão as respostas para essas perguntas.

Qual ferramenta escolher


As ferramentas para trabalhar com contratos são apresentadas em duas direções - RPC e REST.



O RPC pode ser entendido apenas como uma chamada remota, enquanto o REST requer condições adicionais para verbos e URLs HTTP.

As diferenças nas chamadas RPC e REST são apresentadas aqui.
RPC - Chamada de procedimento remotoREST Representational State Transfer
, HTTP- URL
Restaurant:8080/Orders/PlaceOrderPOSTRestaurant:8080/Orders
Restaurant:8080/Orders/GetOrder?OrderNumber=1GETRestaurant:8080/Orders/1
Restaurant:8080/Orders/UpdateOrderPUTRestaurant:8080/Orders/1


Ferramentas


A tabela mostra uma comparação de ferramentas para trabalhar com REST e RPC.
PropriedadesOpenapiWsdlThriftgRPC
Um tipoDESCANSARRpc
PlataformaNão depende
LínguaNão depende
Sequência de desenvolvimento *código primeiro, especificação primeirocódigo primeiro, especificação primeiroespecifique primeirocódigo primeiro, especificação primeiro
Protocolo de transporteHTTP / 1.1any (REST requer HTTP)próprioHTTP / 2
Visãoespecificaçãoestrutura
ComenteBaixo limite de entrada, muita documentaçãoRedundância XML, SOAP, etc.Limiar alto de entrada, pouca documentaçãoLimite médio de entrada, melhor documentação
Código primeiro - primeiro escrevemos a parte do servidor e, em seguida, obtemos contratos. É conveniente quando o lado do servidor já está gravado. Não há necessidade de descrever manualmente os contratos.

Especifique primeiro - primeiro definimos os contratos, depois obtemos a parte do cliente e a parte do servidor. É conveniente no início do desenvolvimento quando ainda não há código. A

saída

WSDL não é adequada devido à sua redundância.

O Apache Thrift é muito exótico e difícil de aprender.

O GRPC requer Net Core 3.0 e Net Standard 2.1. No momento da análise, o núcleo líquido 2.2 e o padrão líquido 2.0 foram utilizados. Não há suporte GRPC no navegador pronto para uso, é necessária uma solução adicional. O GRPC usa a serialização binária Protobuf e HTTP / 2. Por esse motivo, a variedade de utilitários para testar APIs como o Postman etc. está diminuindo. O teste de carga através de algum JMeter pode exigir um esforço adicional. Não é adequado, a mudança para o GRPC requer muitos recursos.

O OpenAPI não requer atualizações adicionais. Ele cativa uma abundância de ferramentas que suportam o trabalho com o REST e esta especificação. Nós selecionamos.

Ferramentas para trabalhar com OpenAPI


A tabela mostra uma comparação de ferramentas para trabalhar com o OpenAPI.
FerramentasswashbuckleNSwagOpenapitools
Versões de especificação suportadasPode gerar especificações no formato OpenApi v2, v3
Primeiro código de suporteHá simHá simNão
Idiomas de servidor suportadosNãoC #Muito de
Idiomas do cliente suportadosNãoAPI de C #, TypeScript, AngularJS, Angular (v2 +), window.fetchMuito de
Configurações de geraçãoNãoHá simHá sim
VisãoPacote de pepitasPacote Nuget + utilitário separadoUtilitário separado
A conclusão do

Swashbuckle não é adequada, porque permite que você obtenha apenas a especificação. Para gerar o código do cliente, você precisa usar uma solução adicional.

O OpenApiTools é uma ferramenta interessante com várias configurações, mas não suporta código primeiro. Sua vantagem é a capacidade de gerar código de servidor em vários idiomas.

O NSwag é conveniente, pois é um pacote Nuget. É fácil conectar-se ao criar um projeto. Oferece suporte a tudo o que precisamos: primeira abordagem de código e geração de código de cliente em c #. Nós selecionamos.

Onde organizar contratos. Como acessar serviços para contratos


Aqui estão as soluções para organizar o armazenamento do contrato. As soluções são listadas em ordem crescente de complexidade.

  • A pasta do projeto de serviço do provedor é a opção mais fácil. Se você precisar executar a abordagem, escolha-a.
  • uma pasta compartilhada é uma opção válida se os projetos desejados estiverem no mesmo repositório. A longo prazo, será difícil manter a integridade dos contratos na pasta. Isso pode exigir uma ferramenta adicional para explicar diferentes versões de contratos, etc.
  • repositório separado para especificações - se os projetos estiverem em repositórios diferentes, os contratos deverão ser colocados em um local público. As desvantagens são as mesmas da pasta compartilhada.
  • através da API de serviço (swagger.ui, swaggerhub) - um serviço separado que lida com o gerenciamento de especificações.

Decidimos usar a opção mais simples - armazenar contratos na pasta do projeto do provedor de serviços. Isso é o suficiente para nós nesta fase, então por que pagar mais?

Em que ponto você gera


Agora você precisa decidir em que ponto executar a geração de código.
Se os contratos fossem compartilhados, os serviços ao consumidor poderiam recebê-los e gerar o código, se necessário.

Decidimos colocar os contratos na pasta com o projeto do provedor de serviços. Isso significa que a geração pode ser feita após a montagem do próprio projeto de serviço do fornecedor.

Onde colocar o código do cliente


O código do cliente será gerado por contrato. Resta descobrir onde colocá-lo.
Parece uma boa ideia colocar o código do cliente em um projeto StorageServiceClientProxy separado. Cada projeto poderá conectar esta montagem.

Benefícios desta solução:

  • o código do cliente está próximo ao seu serviço e está constantemente atualizado;
  • os consumidores podem usar o link para o projeto em um repositório.

Desvantagens:

  • não funcionará se você precisar gerar um cliente em outra parte do sistema, por exemplo, um repositório diferente. É resolvido usando pelo menos uma pasta compartilhada para contratos;
  • os consumidores devem ser escritos no mesmo idioma. Se você precisar de um cliente em outro idioma, precisará usar o OpenApiTools.

Prendemos NSwag


Atributos do Controlador


É necessário informar ao NSwag como gerar a especificação correta para nossos controladores.

Para fazer isso, você precisa organizar os atributos.

[Microsoft.AspNetCore.Mvc.Routing.Route("[controller]")]  //  url
[Microsoft.AspNetCore.Mvc.ApiController] //     
public class DescriptionController : ControllerBase {
[NSwag.Annotations.OpenApiOperation("GetDescription")] //    
[Microsoft.AspNetCore.Mvc.ProducesResponseType(typeof(ConversionDescription), 200)] //    200  
[Microsoft.AspNetCore.Mvc.ProducesResponseType(401)] //    401
[Microsoft.AspNetCore.Mvc.ProducesResponseType(403)] //    403
[Microsoft.AspNetCore.Mvc.HttpGet("{pluginName}/{binaryDataId}")] //  url
public ActionResult<ConversionDescription> GetDescription(string pluginName, Guid binaryDataId) { 
 // ... 
}

Por padrão, o NSwag não pode gerar a especificação correta para o tipo de aplicativo MIME / octet-stream. Por exemplo, isso pode acontecer quando os arquivos são transferidos. Para corrigir isso, você precisa escrever seu atributo e processador para criar a especificação.

[Microsoft.AspNetCore.Mvc.Route("[controller]")]
[Microsoft.AspNetCore.Mvc.ApiController]
public class FileController : ControllerBase {
[NSwag.Annotations.OpenApiOperation("SaveFile")]
[Microsoft.AspNetCore.Mvc.ProducesResponseType(401)]
[Microsoft.AspNetCore.Mvc.ProducesResponseType(403)]
[Microsoft.AspNetCore.Mvc.HttpPost("{pluginName}/{binaryDataId}/{fileName}")]
[OurNamespace.FileUploadOperation] //  
public async Task SaveFile() { // ... }

Processador para gerar especificações para operações de arquivo


A ideia é que você possa escrever seu atributo e processador para lidar com esse atributo.

Penduramos o atributo no controlador e, quando o NSwag o encontrar, ele o processará usando nosso processador.

Para implementar isso, o NSwag fornece as classes OpenApiOperationProcessorAttribute e IOperationProcessor.

Em nosso projeto, fizemos nossos herdeiros:

  • FileUploadOperationAttribute: OpenApiOperationProcessorAttribute
  • FileUploadOperationProcessor: IOperationProcessor

Leia mais sobre o uso de processadores aqui.

Configuração do NSwag para geração de especificações e códigos


Nas 3 seções principais da configuração:

  • tempo de execução - Especifica o tempo de execução .net. Por exemplo, NetCore22;
  • documentGenerator - descreve como gerar uma especificação;
  • codeGenerators - define como gerar código de acordo com a especificação.

O NSwag contém várias configurações, o que é confuso no início.

Por conveniência, você pode usar o NSwag Studio. Com ele, é possível ver em tempo real como várias configurações afetam o resultado da geração ou das especificações do código. Depois disso, selecione manualmente as configurações selecionadas no arquivo de configuração.

Leia mais sobre as configurações aqui

Geramos a especificação e o código do cliente ao montar o projeto do provedor de serviços


Para que após a montagem do projeto do provedor de serviços, a especificação e o código sejam gerados, faça o seguinte:

  1. Criamos um projeto WebApi para o cliente.
  2. Escrevemos uma configuração para o Nswag CLI - Nswag.json (descrito na seção anterior).
  3. Escrevemos um destino PostBuild dentro do projeto do provedor de serviços csproj.

<Target Name="GenerateWebApiProxyClient“ AfterTargets="PostBuildEvent">
<Exec Command="$(NSwagExe_Core22) run nswag.json”/>

  • $ (NSwagExe_Core22) execute nswag.json - execute o utilitário NSwag em .bet runtine netCore 2.2 com a configuração nswag.json

O destino faz o seguinte:

  1. O NSwag gera uma especificação de um assembly de serviço do fornecedor.
  2. O NSwag gera o código do cliente conforme a especificação.

Após cada montagem do projeto do provedor de serviços, o projeto do cliente é atualizado.
O projeto do cliente e do provedor de serviços está na mesma solução.
A montagem ocorre como parte da solução. A solução está configurada para que o projeto do cliente seja montado após o projeto de serviço do fornecedor.

O NSwag também permite que você personalize imperativamente a especificação / geração de código através da API do software.

Como adicionar suporte ao JWT


Precisamos proteger nosso serviço contra solicitações não autorizadas. Para isso, usaremos tokens JWT. Eles devem ser enviados nos cabeçalhos de cada solicitação HTTP para que o provedor de serviços possa verificá-los e decidir se deve ou não atender à solicitação.

Mais informações sobre JWT aqui jwt.io .

A tarefa se resume à necessidade de modificar os cabeçalhos da solicitação HTTP de saída.
Para fazer isso, o gerador de código NSwag pode gerar um ponto de extensão - o método CreateHttpRequestMessageAsync. Dentro deste método, há acesso à solicitação HTTP antes de ser enviada.

Exemplo de código
protected Task<HttpRequestMessage> CreateHttpRequestMessageAsync(CancellationToken cancellationToken) {
      var message = new HttpRequestMessage();

      if (!string.IsNullOrWhiteSpace(this.AuthorizationToken)) {
        message.Headers.Authorization =
          new System.Net.Http.Headers.AuthenticationHeaderValue(BearerScheme, this.AuthorizationToken);
      }

      return Task.FromResult(message);
    }


Conclusão


Escolhemos a opção com OpenAPI, porque É fácil de implementar, e as ferramentas para trabalhar com esta especificação são altamente desenvolvidas.

Conclusões sobre OpenAPI e GRPC:

OpenAPI

  • a especificação é detalhada;
  • , ;
  • ;
  • .

GRPC

  • , URL, HTTP ..;
  • OpenAPI;
  • ;
  • ;
  • HTTP/2.

Assim, recebemos uma especificação baseada no código já escrito dos controladores. Para fazer isso, era necessário travar atributos especiais nos controladores.

Em seguida, com base nas especificações recebidas, implementamos a geração do código do cliente. Agora não precisamos atualizar manualmente o código do cliente.

Estudos foram realizados na área de contratos de versão e teste. No entanto, não foi possível testar tudo na prática devido à falta de recursos.

Controle de versão de contratos públicos


Por que versionar contratos públicos?


Após alterações nos provedores de serviços, todo o sistema deve permanecer em um estado operacional consistente.

Alterações de quebra na API pública devem ser evitadas para não prejudicar os clientes.

Opções de solução


Sem controle de versão de contratos públicos


A própria equipe do provedor de serviços corrige os serviços ao consumidor.



Essa abordagem não funcionará se a equipe do provedor de serviços não tiver acesso aos repositórios de serviços ao consumidor ou não possuir competências. Se não houver esses problemas, você poderá ficar sem o controle de versão.



Usar controle de versão de contratos públicos


A equipe do provedor de serviços deixa a versão anterior dos contratos.



Essa abordagem não apresenta os inconvenientes da anterior, mas acrescenta outras dificuldades.
Você precisa decidir o seguinte:

  • qual ferramenta usar;
  • quando introduzir uma nova versão;
  • quanto tempo manter versões antigas.

Qual ferramenta usar


A tabela mostra os recursos do OpeanAPI e GRPC associados ao controle de versão.
gRPCOpenapi
Atributo de versãoNo nível do protobuf, há um pacote de atributos [packageName]. [Version]No nível de especificação, existem os atributos basePath (para URL) e Versão
Atributo preterido para métodosSim, mas não levado em consideração pelo gerador de código em C #Sim, está marcado como Obsoleto.O
NSwag não é suportado com o código primeiro, você precisa escrever seu processador
Atributo preterido para parâmetrosEstá marcado como obsoletoSim, está marcado como Obsoleto.O
NSwag não é suportado com o código primeiro, você precisa escrever seu processador
Preterido significa que essa API não vale mais a pena usar.

Ambas as ferramentas oferecem suporte à versão e aos atributos preteridos.

Se você usar o OpenAPI e a primeira abordagem de código, novamente precisará escrever processadores para criar a especificação correta.

Quando introduzir uma nova versão


A nova versão deve ser introduzida quando alterações nos contratos não preservam a compatibilidade com versões anteriores.

Como verificar se as alterações violam a compatibilidade entre as versões nova e antiga dos contratos?


Quanto tempo manter as versões


Não há resposta certa para essa pergunta.

Para remover o suporte para a versão antiga, você precisa saber quem usa seu serviço.

Será ruim se a versão for removida e alguém a usar. É especialmente difícil se você não controla seus clientes.

Então, o que pode ser feito nessa situação?

  • notifique os clientes que a versão antiga não será mais suportada. Nesse caso, podemos perder a receita do cliente;
  • suporta todo o conjunto de versões. O custo do suporte de software está aumentando;
  • Para responder a essa pergunta, você precisa perguntar à empresa - a receita de clientes antigos excede o custo de suporte a versões antigas de software? Poderia ser mais rentável pedir aos clientes para atualizar?

O único conselho nessa situação é prestar mais atenção aos contratos públicos, a fim de reduzir a frequência de suas alterações.

Se contratos públicos forem usados ​​em um sistema fechado, você poderá usar a abordagem CDC. Para que possamos descobrir quando os clientes deixaram de usar versões mais antigas do software. Depois disso, você pode remover o suporte da versão antiga.

Conclusão


Use controle de versão somente se você não puder ficar sem ele. Se você decidir usar o controle de versão, ao criar contratos, considere a compatibilidade de versão. É preciso encontrar um equilíbrio entre o custo do suporte a versões mais antigas e os benefícios que ela oferece. Também vale a pena decidir quando você pode parar de oferecer suporte à versão antiga.

Testando contratos e CDC


Esta seção é iluminada superficialmente, como Não há pré-requisitos sérios para a implementação dessa abordagem.

Contratos orientados ao consumidor (CDC)


O CDC é a resposta para a questão de como garantir que o fornecedor e o consumidor usem os mesmos contratos. Esses são alguns tipos de testes de integração destinados a verificar contratos.
A ideia é a seguinte:

  1. O consumidor descreve o contrato.
  2. O fornecedor implementa este contrato em casa.
  3. Este contrato é usado no processo de IC no consumidor e fornecedor. Se o processo for violado, alguém deixou de cumprir o contrato.

Pacto


O PACT é uma ferramenta que implementa essa ideia.

  1. O consumidor grava testes usando a biblioteca PACT.
  2. Esses testes são convertidos em um arquivo de artefato-pacto. Ele contém informações sobre os contratos.
  3. O provedor e o consumidor usam o arquivo pacto para executar os testes.

Durante os testes do cliente, um stub do provedor é criado e, durante os testes do fornecedor, um stub do cliente é criado. Ambos os stubs usam um arquivo pacto.

Comportamento semelhante de criação de stub pode ser alcançado através do Swagger Mock Validator bitbucket.org/atlassian/swagger-mock-validator/src/master .

Links úteis sobre o Pacto





Como o CDC pode ser incorporado no IC


  • implantar o Pact + Pact broker;
  • adquira uma solução SaaS Pact Flow pronta para uso.

Conclusão


É necessário pacto para garantir a conformidade do contrato. Ele mostrará quando alterações nos contratos violam as expectativas dos serviços ao consumidor.

Essa ferramenta é adequada quando o fornecedor se adapta ao cliente - o cliente. Isso só é possível dentro de um sistema isolado.

Se você está prestando um serviço para o mundo exterior e não sabe quem são seus clientes, o Pact não é para você.

All Articles