Contratos públicos, cómo garantizar su coherencia.

  • ¿Su sistema consta de muchos servicios interconectados?
  • ¿Todavía actualiza manualmente el código de servicio al cambiar la API pública?
  • los cambios en sus servicios a menudo socavan el trabajo de otros, y otros desarrolladores lo odian por esto?

Si respondió que sí al menos una vez, ¡bienvenido!

Condiciones


Contratos públicos, especificaciones : interfaces públicas a través de las cuales puede interactuar con el servicio. En el texto significan lo mismo.

Sobre qué es el artículo


Aprenda a reducir el tiempo dedicado al desarrollo de servicios web utilizando herramientas para la descripción unificada de contratos y la generación automática de código.

El uso adecuado de las técnicas y herramientas que se describen a continuación le permitirá implementar rápidamente nuevas funciones y no romper las antiguas.

¿Cómo se ve el problema?


Hay un sistema que consta de varios servicios. Estos servicios se asignan a diferentes equipos.



Los servicios al consumidor dependen del proveedor de servicios.
El sistema evoluciona y un día el proveedor de servicios cambia sus contratos públicos.



Si los servicios al consumidor no están listos para el cambio, entonces el sistema deja de funcionar por completo.



Cómo resolver este problema


El equipo de servicio del proveedor arreglará todo


Esto se puede hacer si el equipo proveedor posee el área temática de otros servicios y tiene acceso a sus repositorios git. Esto funcionará solo en proyectos pequeños cuando haya pocos servicios dependientes. Esta es la opción más barata. Si es posible, debes usarlo.

Actualice el código de su servicio al equipo de consumidores.


¿Por qué otros se rompen, pero estamos reparando?

Sin embargo, la pregunta principal es cómo arreglar su servicio, ¿cómo se ve el contrato ahora? Debe conocer el nuevo código de servicio del proveedor o comunicarse con su equipo. Pasamos tiempo estudiando el código e interactuando con otro equipo.

Piense qué hacer para evitar que aparezca el problema


La opción más razonable a largo plazo. Considérelo en la siguiente sección.

Cómo prevenir la manifestación de un problema.


El ciclo de vida del desarrollo de software se puede representar en tres etapas: diseño, implementación y pruebas.

Cada uno de los pasos debe ampliarse de la siguiente manera:

  1. en la etapa de diseño, defina declarativamente contratos;
  2. durante la implementación, generamos código de servidor y cliente bajo contratos;
  3. Al realizar las pruebas, verificamos los contratos e intentamos tener en cuenta las necesidades del cliente (CDC).

Cada uno de los pasos se explica más adelante como un ejemplo de nuestro problema.

Cómo se ve el problema con nosotros




Así es como se ve nuestro ecosistema.
Los círculos son servicios y las flechas son canales de comunicación entre ellos.

Frontend es una aplicación cliente basada en la web.

La mayoría de las flechas conducen al Servicio de Almacenamiento. Almacena documentos. Este es el servicio más importante. Después de todo, nuestro producto es un sistema de gestión de documentos electrónicos.

Si este servicio cambia sus contratos, el sistema dejará de funcionar inmediatamente.



Las fuentes de nuestro sistema están escritas principalmente en C #, pero también hay servicios en Go y Python. En este contexto, no importa lo que hagan los otros servicios en la figura.



Cada servicio tiene su propia implementación de cliente para trabajar con el servicio de almacenamiento. Al cambiar los contratos, debe actualizar manualmente el código en cada proyecto.

Me gustaría alejarme de la actualización manual hacia la automática. Esto ayudará a aumentar la velocidad a la que cambia el código del cliente y reducirá los errores. Los errores son errores tipográficos en la URL, errores por descuido, etc.

Sin embargo, este enfoque no corrige errores en la lógica empresarial del cliente. Solo puede ajustarlo manualmente.

Del problema a la tarea


En nuestro caso, es necesario implementar la generación automática de código de cliente.
Al hacerlo, se debe considerar lo siguiente:

  • lado del servidor: los controladores ya están escritos;
  • el navegador es un cliente del servicio;
  • Los servicios se comunican a través de HTTP.
  • La generación debe estar sintonizada. Por ejemplo, para admitir JWT.

Preguntas


En el curso de la resolución del problema, surgieron preguntas:

  • qué herramienta elegir;
  • cómo obtener contratos;
  • dónde colocar los contratos;
  • dónde colocar el código del cliente;
  • en que punto hacer la generacion.

Las siguientes son respuestas a estas preguntas.

Qué herramienta elegir


Las herramientas para trabajar con contratos se presentan en dos direcciones: RPC y REST.



RPC puede entenderse como una simple llamada remota, mientras que REST requiere condiciones adicionales para verbos HTTP y URL.

Las diferencias en la llamada RPC y REST se presentan aquí.
RPC: llamada a procedimiento 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


Herramientas


La tabla muestra una comparación de herramientas para trabajar con REST y RPC.
PropiedadesOpenapiWsdlAhorrogRPC
Un tipoDESCANSORpc
PlataformaNo depende
LenguaNo depende
Secuencia de desarrollo *codifique primero, especifique primerocodifique primero, especifique primeroespecificaciones primerocodifique primero, especifique primero
Protocolo de transporteHTTP / 1.1cualquiera (REST requiere HTTP)propioHTTP / 2
Verespecificaciónmarco de referencia
ComentarioUmbral de entrada bajo, mucha documentaciónRedundancia XML, SOAP, etc.Alto umbral de entrada, poca documentaciónUmbral de entrada promedio, mejor documentación
Código primero : primero escribimos la parte del servidor y luego obtenemos contratos. Es conveniente cuando el lado del servidor ya está escrito. No es necesario describir manualmente los contratos.

Especificaciones primero : primero definimos los contratos, luego obtenemos la parte del cliente y la parte del servidor de ellos. Es conveniente al comienzo del desarrollo cuando todavía no hay código. La

salida

WSDL no es adecuada debido a su redundancia.

Apache Thrift es demasiado exótico y difícil de aprender.

GRPC requiere net Core 3.0 y net Standard 2.1. En el momento del análisis, se utilizaron net Core 2.2 y net Standard 2.0. No hay soporte GRPC en el navegador fuera de la caja, se requiere una solución adicional. GRPC utiliza Protobuf y serialización binaria HTTP / 2. Debido a esto, el rango de utilidades para probar API como Postman, etc., se está reduciendo. Las pruebas de carga a través de algunos JMeter pueden requerir un esfuerzo adicional. No es adecuado, cambiar a GRPC requiere muchos recursos.

OpenAPI no requiere actualizaciones adicionales. Captura una gran cantidad de herramientas que admiten trabajar con REST y esta especificación. Lo seleccionamos

Herramientas para trabajar con OpenAPI


La tabla muestra una comparación de herramientas para trabajar con OpenAPI.
HerramientasSwashbuckleNSwagOpenapitools
Versiones de especificación admitidasPuede generar especificaciones en formato OpenApi v2, v3
Código de primer apoyoAhi estaAhi estaNo
Idiomas de servidor compatiblesNoC #Un montón de
Idiomas de cliente compatiblesNoC #, TypeScript, AngularJS, Angular (v2 +), API window.fetchUn montón de
Configuraciones de generaciónNoAhi estaAhi esta
VerPaquete NugetPaquete Nuget + utilidad separadaUtilidad separada
La conclusión de

Swashbuckle no es adecuada, porque le permite obtener solo la especificación. Para generar código de cliente, debe usar una solución adicional.

OpenApiTools es una herramienta interesante con un montón de configuraciones, pero no admite código primero. Su ventaja es la capacidad de generar código de servidor en muchos idiomas.

NSwag es conveniente porque es un paquete Nuget. Es fácil conectarse al construir un proyecto. Admite todo lo que necesitamos: primer enfoque de código y generación de código de cliente en c #. Lo seleccionamos

Dónde concertar contratos. Cómo acceder a servicios a contratos


Aquí hay soluciones para organizar el almacenamiento por contrato. Las soluciones se enumeran en orden de complejidad creciente.

  • La carpeta del proyecto del servicio del proveedor es la opción más fácil. Si necesita ejecutar el enfoque, elíjalo.
  • Una carpeta compartida es una opción válida si los proyectos deseados están en el mismo repositorio. A la larga, será difícil mantener la integridad de los contratos en la carpeta. Esto puede requerir una herramienta adicional para dar cuenta de diferentes versiones de contratos, etc.
  • repositorio separado para especificaciones: si los proyectos están en repositorios diferentes, entonces los contratos deben colocarse en un lugar público. Las desventajas son las mismas que la carpeta compartida.
  • a través del servicio API (swagger.ui, swaggerhub), un servicio separado que se ocupa de la gestión de especificaciones.

Decidimos usar la opción más simple: almacenar contratos en la carpeta del proyecto del proveedor de servicios. Esto es suficiente para nosotros en esta etapa, entonces, ¿por qué pagar más?

¿En qué punto generas


Ahora debe decidir en qué punto realizar la generación de código.
Si se compartieran los contratos, los servicios al consumidor podrían recibir los contratos y generar el código ellos mismos si fuera necesario.

Decidimos colocar los contratos en la carpeta con el proyecto del proveedor de servicios. Esto significa que la generación se puede realizar después del ensamblaje del proyecto de servicio del proveedor.

Dónde colocar el código del cliente


El código del cliente se generará por contrato. Queda por averiguar dónde colocarlo.
Parece una buena idea poner el código del cliente en un proyecto separado StorageServiceClientProxy. Cada proyecto podrá conectar este ensamblaje.

Beneficios de esta solución:

  • el código del cliente está cerca de su servicio y está constantemente actualizado;
  • los consumidores pueden usar el enlace al proyecto dentro de un repositorio.

Desventajas

  • no funcionará si necesita generar un cliente en otra parte del sistema, por ejemplo, un repositorio diferente. Se resuelve utilizando al menos una carpeta compartida para contratos;
  • Los consumidores deben estar escritos en el mismo idioma. Si necesita un cliente en otro idioma, debe usar OpenApiTools.

Sujetamos NSwag


Atributos del controlador


Necesito decirle a NSwag cómo generar la especificación correcta para nuestros controladores.

Para hacer esto, necesita organizar los 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 defecto, NSwag no puede generar la especificación correcta para la aplicación / octeto-flujo tipo MIME. Por ejemplo, esto puede suceder cuando se transfieren archivos. Para solucionar esto, debe escribir su atributo y procesador para crear la especificación.

[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() { // ... }

Procesador para generar especificaciones para operaciones de archivo


La idea es que puede escribir su atributo y procesador para manejar este atributo.

Colgamos el atributo en el controlador, y cuando NSwag lo cumpla, lo procesará utilizando nuestro procesador.

Para implementar esto, NSwag proporciona las clases OpenApiOperationProcessorAttribute e IOperationProcessor.

En nuestro proyecto, hicimos nuestros herederos:

  • FileUploadOperationAttribute: OpenApiOperationProcessorAttribute
  • FileUploadOperationProcessor: IOperationProcessor

Lea más sobre el uso de procesadores aquí.

Configuración de NSwag para la generación de especificaciones y código


En las secciones principales de config 3:

  • tiempo de ejecución: especifica el tiempo de ejecución de .net. Por ejemplo, NetCore22;
  • documentGenerator: describe cómo generar una especificación;
  • codeGenerators: define cómo generar código de acuerdo con la especificación.

NSwag contiene un montón de configuraciones, lo cual es confuso al principio.

Para mayor comodidad, puede usar NSwag Studio. Al usarlo, puede ver en tiempo real cómo varias configuraciones afectan el resultado de la generación de código o las especificaciones. Después de eso, seleccione manualmente la configuración seleccionada en el archivo de configuración.

Lea más sobre la configuración aquí

Generamos la especificación y el código del cliente al ensamblar el proyecto del proveedor de servicios.


Para que después del ensamblaje del proyecto del proveedor de servicios, se generen la especificación y el código, haga lo siguiente:

  1. Creamos un proyecto WebApi para el cliente.
  2. Escribimos una configuración para Nswag CLI - Nswag.json (descrito en la sección anterior).
  3. Escribimos un PostBuild Target dentro del proyecto del proveedor de servicios csproj.

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

  • $ (NSwagExe_Core22) ejecutar nswag.json - ejecuta la utilidad NSwag bajo .bet runtine netCore 2.2 con la configuración nswag.json

Target hace lo siguiente:

  1. NSwag genera una especificación a partir de un conjunto de servicios de proveedor.
  2. NSwag genera código de cliente según la especificación.

Después de cada ensamblaje del proyecto del proveedor de servicios, el proyecto del cliente se actualiza.
El proyecto del cliente y el proveedor de servicios están dentro de la misma solución.
El montaje se lleva a cabo como parte de la solución. La solución está configurada para que el proyecto del cliente se ensamble después del proyecto de servicio del proveedor.

NSwag también le permite personalizar la generación de especificaciones / códigos de manera imperativa a través de la API del software.

Cómo agregar soporte para JWT


Necesitamos proteger nuestro servicio de solicitudes no autorizadas. Para esto, usaremos tokens JWT. Deben enviarse en los encabezados de cada solicitud HTTP para que el proveedor de servicios pueda verificarlos y decidir si cumplir con la solicitud o no.

Más información acerca de JWT aquí jwt.io .

La tarea se reduce a la necesidad de modificar los encabezados de la solicitud HTTP saliente.
Para hacer esto, el generador de código NSwag puede generar un punto de extensión: el método CreateHttpRequestMessageAsync. Dentro de este método hay acceso a la solicitud HTTP antes de enviarla.

Ejemplo 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);
    }


Conclusión


Elegimos la opción con OpenAPI, porque Es fácil de implementar y las herramientas para trabajar con esta especificación están altamente desarrolladas.

Conclusiones sobre la API abierta y GRPC:

API abierta

  • la especificación es detallada;
  • , ;
  • ;
  • .

GRPC

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

Por lo tanto, recibimos una especificación basada en el código ya escrito de los controladores. Para hacer esto, era necesario colgar atributos especiales en los controladores.

Luego, en base a la especificación recibida, implementamos la generación del código del cliente. Ahora no necesitamos actualizar manualmente el código del cliente.

Se han realizado estudios en el área de versiones y pruebas de contratos. Sin embargo, no fue posible probar todo en la práctica debido a la falta de recursos.

Versiones de contratos públicos


¿Por qué versionar contratos públicos?


Después de los cambios en los proveedores de servicios, todo el sistema debe permanecer en un estado operativo constante.

Se deben evitar los cambios importantes en la API pública para no dañar a los clientes.

Opciones de solucion


Sin versionar contratos públicos


El propio equipo proveedor de servicios repara los servicios al consumidor.



Este enfoque no funcionará si el equipo del proveedor de servicios no tiene acceso a los repositorios de servicios al consumidor o carece de competencias. Si no hay tales problemas, puede hacerlo sin versionar.



Usar versiones de contratos públicos


El equipo del proveedor de servicios deja la versión anterior de los contratos.



Este enfoque no tiene los inconvenientes del anterior, pero agrega otras dificultades.
Debe decidir lo siguiente:

  • qué herramienta usar;
  • cuándo presentar una nueva versión;
  • cuánto tiempo mantener versiones antiguas.

Qué herramienta usar


La tabla muestra las características de OpeanAPI y GRPC asociadas con el control de versiones.
gRPCOpenapi
Atributo de versiónEn el nivel de protobuf, hay un paquete de atributos [packageName]. [Versión]En el nivel de especificación, hay atributos basePath (para URL) y Versión
Atributo obsoleto para métodosSí, pero el generador de código no lo tiene en cuenta en C #Sí, está marcado como Obsoleto
. NSwag no es compatible con el código primero, debe escribir su procesador
Atributo en desuso para parámetrosEstá marcado como ObsoletoSí, está marcado como Obsoleto
. NSwag no es compatible con el código primero, debe escribir su procesador
En desuso significa que ya no vale la pena usar esta API.

Ambas herramientas admiten versión y atributos obsoletos.

Si usa OpenAPI y el primer enfoque de código, nuevamente necesita escribir procesadores para crear la especificación correcta.

Cuando presentar una nueva versión


La nueva versión debe introducirse cuando los cambios en los contratos no conservan la compatibilidad con versiones anteriores.

¿Cómo verificar que los cambios violen la compatibilidad entre las versiones nuevas y antiguas de los contratos?


Cuánto tiempo mantener versiones


No hay una respuesta correcta a esta pregunta.

Para eliminar la compatibilidad con la versión anterior, debe saber quién utiliza su servicio.

Será malo si se elimina la versión y alguien más la usa. Es especialmente difícil si no controlas a tus clientes.

Entonces, ¿qué se puede hacer en esta situación?

  • notifique a los clientes que la versión anterior ya no será compatible. En este caso, podemos perder los ingresos del cliente;
  • admite todo el conjunto de versiones. El costo del soporte de software está creciendo;
  • Para responder a esta pregunta, debe preguntarle a la empresa: ¿los ingresos de los antiguos clientes exceden el costo de soportar versiones anteriores de software? ¿Podría ser más rentable pedirles a los clientes que actualicen?

El único consejo en esta situación es prestar más atención a los contratos públicos para reducir la frecuencia de sus cambios.

Si los contratos públicos se usan en un sistema cerrado, puede usar el enfoque de los CDC. Así podemos saber cuándo los clientes dejaron de usar versiones anteriores del software. Después de eso, puede eliminar el soporte de la versión anterior.

Conclusión


Use el control de versiones solo si no puede prescindir de él. Si decide usar el control de versiones, cuando diseñe contratos, considere la compatibilidad de versiones. Debe lograrse un equilibrio entre el costo de soportar versiones anteriores y los beneficios que proporciona. También vale la pena decidir cuándo puede dejar de admitir la versión anterior.

Prueba de contratos y CDC


Esta sección está iluminada superficialmente, como No hay requisitos previos serios para la implementación de este enfoque.

Contratos impulsados ​​por el consumidor (CDC)


Los CDC son la respuesta a la pregunta de cómo garantizar que el proveedor y el consumidor usen los mismos contratos. Estos son algún tipo de pruebas de integración destinadas a verificar los contratos.
La idea es la siguiente:

  1. El consumidor describe el contrato.
  2. El proveedor implementa este contrato en casa.
  3. Este contrato se utiliza en el proceso de CI en el consumidor y el proveedor. Si se viola el proceso, alguien ha dejado de cumplir con el contrato.

Pacto


PACT es una herramienta que implementa esta idea.

  1. El consumidor escribe pruebas usando la biblioteca PACT.
  2. Estas pruebas se convierten en un artefacto - archivo de pacto. Contiene información sobre los contratos.
  3. El proveedor y el consumidor usan el archivo del pacto para ejecutar las pruebas.

Durante las pruebas de cliente, se crea un apéndice de proveedor, y durante las pruebas de proveedor, se crea un apéndice de cliente. Ambos trozos usan un archivo de pacto.

Se puede lograr un comportamiento de creación de código auxiliar similar a través del Validador simulado de Swagger bitbucket.org/atlassian/swagger-mock-validator/src/master .

Enlaces útiles sobre Pact





Cómo se pueden incrustar CDC en CI


  • desplegando usted mismo Pact + Pact broker;
  • compre una solución SaaS Pact Flow preparada.

Conclusión


Se necesita un pacto para garantizar el cumplimiento del contrato. Se mostrará cuando los cambios en los contratos violen las expectativas de los servicios al consumidor.

Esta herramienta es adecuada cuando el proveedor se adapta al cliente: el cliente. Esto solo es posible dentro de un sistema aislado.

Si está haciendo un servicio para el mundo exterior y no sabe quiénes son sus clientes, entonces Pact no es para usted.

All Articles