API REST con Protobuf

por Ansel Castro

Aprovechar las herramientas de generación de código y Protobuf para impulsar el desarrollo de microservicios y API REST. Este artículo muestra cómo utilizar búferes de protocolo, también conocidos como protobuf, y herramientas de generación de código para acelerar el desarrollo de arquitecturas de microservicios y API REST.

Introducción

La arquitectura de microservicio es un término común hoy en día desde 2011 cuando se utilizó por primera vez. Desde entonces, el patrón ha sido adoptado por muchas organizaciones para impulsar las operaciones a escala global. El patrón arquitectónico fue diseñado inicialmente para resolver uno de los principales problemas en la Arquitectura Orientada a Servicios o SOA. Entonces, podemos hablar de microservicios como una especialización de SOA que tiene como objetivo proporcionar una verdadera independencia de servicio, soberanía de datos y despliegue continuo.

API REST con Protobuf
API REST con Protobuf

Pero todos los beneficios que ofrece una arquitectura de microservicio tienen un costo. Aumenta la complejidad para mantener la coherencia, el descubrimiento de servicios, la documentación y la supervisión en un gran conjunto de microservicios.

Para hacer cumplir la coherencia, podemos aprovechar un corredor como RabbitMQ para enviar y procesar eventos de integración . Por otro lado, para el monitoreo del servicio, podemos usar una malla de servicios como Linkerd . Para el descubrimiento de servicios y la documentación, se puede utilizar una puerta de enlace API para agregar todos los microservicios internos en una sola API REST.

Por lo tanto, desarrollar y mantener una solución de microservicio puede llevar mucho tiempo y ser tedioso. Porque además de enfocarte en la lógica empresarial en la capa de servicio, tendrás que encargarte de exponer los servicios a través de una API web, desarrollar una API Gateway y bibliotecas cliente o SDK para aplicaciones cliente.

El propósito de este artículo es mostrarle cómo utilizar el lenguaje de búfer de protocolo para generar código y crear soluciones de microservicio más rápidamente. Con este enfoque, podemos ahorrar tiempo y centrarnos en la lógica empresarial, en lugar de escribir API Gateways y Client Libraries para comunicarnos con los servicios internos.

Además, puede escribir código robusto utilizando la prueba de integración del núcleo de aspnet para probar la lógica empresarial integrada con la infraestructura de aspnet, incluido el soporte de la base de datos y el sistema de archivos.

A continuación, se muestra una arquitectura de microservicio común.

Imagen 1

Antecedentes

Protocol Buffer es un lenguaje independiente de la plataforma definido en Google para serializar datos estructurados de una manera más eficiente, piense en Json, pero más pequeño y más rápido. Usted define la estructura de sus datos usando un lenguaje simple, luego puede usar código fuente generado especial para escribir y leer fácilmente sus datos estructurados hacia y desde una variedad de flujos de datos.

Además de los mensajes que definen la estructura de datos, puede declarar servicios y rpcs (puntos finales) en archivos con la .protoextensión. El  lenguaje Protobuf se usa comúnmente con Grpc . En Grpc puede definir servicios con rpcs, también conocidos como puntos finales, además, también se pueden generar bibliotecas de clientes para diferentes idiomas. Pero Grpc tiene un inconveniente, como por ejemplo, puede ser difícil ser llamado desde aplicaciones basadas en web. Por lo tanto, Grpc se utiliza principalmente para servicios internos. 

Es por eso que el generador basado en proto CybtanSDK fue diseñado para ofrecer lo mejor de los servicios basados ​​en Grpc y REST. Por lo tanto, los servicios pueden ser consumidos por la aplicación web usando JSON y también usar una serialización binaria más rápida para las comunicaciones internas.

Introducción a CybtansSDK

CybtansSDK es un proyecto de código abierto que le permite generar código para C # con el fin de desarrollar microservicios con AspNetCore utilizando mensajes, servicios y rpcs definidos en los archivos .proto .

La principal ventaja que proporciona esta herramienta es la generación de interfaces de servicio , objetos de transferencia de datos , también conocidos como Dtos, controladores API , puertas de enlace y bibliotecas cliente para C # y Typescript . Lo más importante es que puede escribir documentación para todo el código generado.

Otra ventaja de tener una API Gateway generada automáticamente es eliminar la complejidad de desarrollar y mantener otra API RESTfull. Por lo tanto, puede agregar todos los microservicios en una sola API REST, lo que facilita el descubrimiento de servicios, la documentación y la integración de aplicaciones. Otros beneficios que puede obtener con este patrón son:

  • Aísla a los clientes de cómo la aplicación está dividida en microservicios.
  • Aísla a los clientes del problema de determinar la ubicación de las instancias de servicio
  • Proporciona la API óptima para cada cliente.
  • Reduce el número de solicitudes / viajes de ida y vuelta. Por ejemplo, la puerta de enlace API permite a los clientes recuperar datos de varios servicios con un solo viaje de ida y vuelta. Menos solicitudes también significa menos gastos generales y mejora la experiencia del usuario. Una puerta de enlace API es esencial para las aplicaciones móviles.
  • Simplifica al cliente moviendo la lógica para llamar a múltiples servicios desde el cliente a la puerta de enlace API
  • Se traduce de un protocolo API “estándar” público compatible con la web a cualquier protocolo que se utilice internamente.

Así que comencemos con un ejemplo. Primero descargue el generador de código cybtans cli , luego extraiga el archivo zip y agregue la carpeta donde se encuentra el .exe a su ruta para facilitar el uso. Luego genere una solución con dotnet cli o Visual Studio. Por ejemplo:Ocultar   código de copia

dotnet new sln -n MySolution

Ahora generemos una estructura de proyecto de microservicio para administrar un Catálogo de productos. Ejecute el siguiente comando en un símbolo del sistema o Windows de PowerShell. Por ejemplo:Ocultar   código de copia

cybtans-cli service -n Catalog -o ./Catalog -sln .\MySolution.sln

Este comando sigue una convención y genera varios proyectos en la carpeta Catálogo . Algunos de los proyectos se describen a continuación:

  • Catalog.Client: Proyecto .NET Standard con la biblioteca cliente del microservicio para C #
  • Catalog.Models: Proyecto .NET Standard con Dtos del microservicio, mensajes de solicitud y respuesta
  • Catalog.RestApi: Proyecto AspNetCore con la API Rest del microservicio
  • Catalog.Services: Proyecto .NET Core con la lógica o los servicios de negocio del microservicio
  • Catalog.Services.Tests: Pruebas de integración del microservicio
  • Proto: Las definiciones de protobuff de microservicio

Generando código C # a partir de archivos proto

Junto con los proyectos generados, se creó un archivo json con el nombre  cybtans.json . Este archivo contiene las opciones de configuración del comando cybtans-cli [solution folder]. Esos ajustes especifican el protoarchivo principal utilizado por el generador de código como se muestra a continuación:Ocultar   código de copia

{
  "Service": "Catalog",
  "Steps": [  
    {
      "Type": "proto",
      "Output": ".",
      "ProtoFile": "./Proto/Catalog.proto"      
    }
  ]
}

Ahora modifiquemos el archivo Proto / Catalog.proto para definir las estructuras de datos para el Catalogmicroservicio, observe cómo packagese usa la declaración para definir el nombre del microservicio.Ocultar   Shrink    Copiar código

syntax = "proto3";

package Catalog;

message CatalogBrandDto {
    string brand = 1;
    int32 id = 2;
}

message CatalogItemDto {
    option description = "Catalog Item's Data";

    string name = 1 [description = "The name of the Catalog Item"];
    string description = 2 [description = "The description of the Catalog Item"];
    decimal price = 3 [description = "The price of the Catalog Item"];
    string pictureFileName = 4;
    string pictureUri = 5 [optional = true];
    int32 catalogTypeId = 6 [optional = true];
    CatalogTypeDto catalogType = 7;
    int32 catalogBrandId = 8;
    CatalogBrandDto catalogBrand = 9;
    int32 availableStock = 10;
    int32 restockThreshold = 11;
    int32 maxStockThreshold = 12;
    bool onReorder = 13;
    int32 id = 14;
}

message CatalogTypeDto {
    string type = 1;
    int32 id = 2;
}

Además del mensaje anterior, definamos ahora un servicio y algunas operaciones (también conocidas como rpcs), también definamos algunos mensajes adicionales para la estructura de datos de solicitudes y respuestas de rpc.Ocultar   Shrink    Copiar código

message GetAllRequest {
    string filter = 1 [optional = true];
    string sort = 2 [optional = true];
    int32 skip = 3 [optional = true];
    int32 take = 4 [optional = true];
}

message GetCatalogItemRequest {
    int32 id = 1;
}

message UpdateCatalogItemRequest {
    int32 id = 1;
    CatalogItemDto value = 2 [(ts).partial = true];
}

message DeleteCatalogItemRequest{
    int32 id = 1;
}

message GetAllCatalogItemResponse {
    repeated CatalogItemDto items = 1;
    int64 page = 2;
    int64 totalPages = 3;
    int64 totalCount = 4;
}

message CreateCatalogItemRequest {
    CatalogItemDto value = 1 [(ts).partial = true];
}

service CatalogItemService {
    option (prefix) ="api/CatalogItem";
    option (description) = "Items Catalog Service";

    rpc GetAll(GetAllRequest) returns (GetAllCatalogItemResponse){        
        option method = "GET";
        option description = "Return all the items in the Catalog";        
    };

    rpc Get(GetCatalogItemRequest) returns (CatalogItemDto){    
        option template = "{id}"; 
        option method = "GET";
        option description = "Return an Item given its Id";
    };

    rpc Create(CreateCatalogItemRequest) returns (CatalogItemDto){            
        option method = "POST";
        option description = "Create a Catalog Item";
        
    };

    rpc Update(UpdateCatalogItemRequest) returns (CatalogItemDto){            
        option template = "{id}"; 
        option method = "PUT";
        option description = "Update a Catalog Item";
    };

    rpc Delete(DeleteCatalogItemRequest) returns (void){
        option template = "{id}"; 
        option method = "DELETE";
        option description = "Delete a Catalog Item given its Id";
    };
}

Puede generar el código csharp ejecutando el comando que se muestra a continuación. Debe proporcionar la ruta donde se encuentra cybtans.json . Las herramientas buscan este archivo de configuración de forma recursiva en todos los subdirectorios.Ocultar   código de copia

cybtans-cli .

Los mensajes se generan en el proyecto Modelos de forma predeterminada utilizando el nombre del paquete como espacio de nombres principal, como se muestra a continuación:

Imagen 2

Por ejemplo, el código de la CatalogItemDto clase se muestra a continuación. Puede notar que CatalogItemDtoAccesor, esta clase se genera para proporcionar metadatos adicionales para inspeccionar tipos de propiedad y establecer / obtener valores de propiedad sin usar la reflexión.Ocultar   Shrink    Copiar código

using System;
using Cybtans.Serialization;
using System.ComponentModel;

namespace Catalog.Models
{
    /// <summary>
    /// The Catalog Item
    /// </summary>
    [Description("The Catalog Item")]
    public partial class CatalogItemDto : IReflectorMetadataProvider
    {
        private static readonly CatalogItemDtoAccesor __accesor = new CatalogItemDtoAccesor();
        
        /// <summary>
        /// The name of the Catalog Item
        /// </summary>
        [Description("The name of the Catalog Item")]
        public string Name {get; set;}
        
        /// <summary>
        /// The description of the Catalog Item
        /// </summary>
        [Description("The description of the Catalog Item")]
        public string Description {get; set;}
        
        /// <summary>
        /// The price of the Catalog Item
        /// </summary>
        [Description("The price of the Catalog Item")]
        public decimal Price {get; set;}
        
        public string PictureFileName {get; set;}
        
        public string PictureUri {get; set;}
        
        public int CatalogTypeId {get; set;}
        
        public CatalogTypeDto CatalogType {get; set;}
        
        public int CatalogBrandId {get; set;}
        
        public CatalogBrandDto CatalogBrand {get; set;}
        
        public int AvailableStock {get; set;}
        
        public int RestockThreshold {get; set;}
        
        public int MaxStockThreshold {get; set;}
        
        public bool OnReorder {get; set;}
        
        public int Id {get; set;}
        
        public IReflectorMetadata GetAccesor()
        {
            return __accesor;
        }
    }    
    
    public sealed class CatalogItemDtoAccesor : IReflectorMetadata
    {
      // Code omitted for brevity
       ....
    }
}

El paquete IReflectorMetadata aprovecha la interfaz Cybtans.Serialization para acelerar la serialización de objetos en un formato binario que es más compacto y eficiente que JSON. Este formato se utiliza para comunicaciones entre servicios como API GatewayMicroservicecomunicaciones. Por lo tanto, las aplicaciones web pueden usar JSON para consumir los puntos finales de Gateway y, en su lugar, Gateway puede usar un formato binario para comunicarse con los servicios ascendentes.

La interfaz de servicio, también conocida como contrato, se genera de forma predeterminada en la carpeta que se muestra a continuación. Observe cómo se documenta el código utilizando la opción de descripción en el archivo proto. Esta descripción puede ayudarlo a mantener sus API REST documentadas, con los beneficios de mejorar el mantenimiento y la integración con las aplicaciones frontend.

Imagen 3

Ocultar   Shrink    Copiar código

using System;
using System.Threading.Tasks;
using Catalog.Models;
using System.Collections.Generic;

namespace Catalog.Services
{
    /// <summary>
    /// Items Catalog Service
    /// </summary>
    public partial interface ICatalogItemService 
    {        
        /// <summary>
        /// Return all the items in the Catalog
        /// </summary>
        Task<GetAllCatalogItemResponse> GetAll(GetAllRequest request);
        
        /// <summary>
        /// Return an Item given its Id
        /// </summary>
        Task<CatalogItemDto> Get(GetCatalogItemRequest request);        
        
        /// <summary>
        /// Create a Catalog Item
        /// </summary>
        Task<CatalogItemDto> Create(CreateCatalogItemRequest request);        
        
        /// <summary>
        /// Update a Catalog Item
        /// </summary>
        Task<CatalogItemDto> Update(UpdateCatalogItemRequest request);        
        
        /// <summary>
        /// Delete a Catalog Item given its Id
        /// </summary>
        Task Delete(DeleteCatalogItemRequest request);        
    }
}

El generador de código Cybtans crea controladores API para exponer la capa de servicio. El controlador API se genera de forma predeterminada en la carpeta que se muestra a continuación. Todo lo que necesita es implementar la interfaz de servicio.

Imagen 4

El código para el controlador API se muestra a continuación como referencia. Depende de usted registrar la implementación del servicio con el ServiceCollectionpara que se inyecte en el constructor del controlador.Ocultar   Shrink    Copiar código

using System;
using Catalog.Services;
using Catalog.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Cybtans.AspNetCore;

namespace Catalog.Controllers
{
    /// <summary>
    /// Items Catalog Service
    /// </summary>
    [System.ComponentModel.Description("Items Catalog Service")]
    [Route("api/CatalogItem")]
    [ApiController]
    public partial class CatalogItemServiceController : ControllerBase
    {
        private readonly ICatalogItemService _service;
        
        public CatalogItemServiceController(ICatalogItemService service)
        {
            _service = service;
        }
        
        /// <summary>
        /// Return all the items in the Catalog
        /// </summary>
        [System.ComponentModel.Description("Return all the items in the Catalog")]
        [HttpGet]
        public Task<GetAllCatalogItemResponse> GetAll([FromQuery]GetAllRequest __request)
        {
            return _service.GetAll(__request);
        }
        
        /// <summary>
        /// Return an Item given its Id
        /// </summary>
        [System.ComponentModel.Description("Return an Item given its Id")]
        [HttpGet("{id}")]
        public Task<CatalogItemDto> Get(int id, [FromQuery]GetCatalogItemRequest __request)
        {
            __request.Id = id;
            return _service.Get(__request);
        }
        
        /// <summary>
        /// Create a Catalog Item
        /// </summary>
        [System.ComponentModel.Description("Create a Catalog Item")]
        [HttpPost]
        public Task<CatalogItemDto> Create([FromBody]CreateCatalogItemRequest __request)
        {
            return _service.Create(__request);
        }
        
        /// <summary>
        /// Update a Catalog Item
        /// </summary>
        [System.ComponentModel.Description("Update a Catalog Item")]
        [HttpPut("{id}")]
        public Task<CatalogItemDto> 
               Update(int id, [FromBody]UpdateCatalogItemRequest __request)
        {
            __request.Id = id;
            return _service.Update(__request);
        }
        
        /// <summary>
        /// Delete a Catalog Item given its Id
        /// </summary>
        [System.ComponentModel.Description("Delete a Catalog Item given its Id")]
        [HttpDelete("{id}")]
        public Task Delete(int id, [FromQuery]DeleteCatalogItemRequest __request)
        {
            __request.Id = id;
            return _service.Delete(__request);
        }
    }
}

La herramienta puede generar un cliente seguro de tipos utilizando las interfaces Refit como se muestra en la carpeta a continuación. Puede utilizar este cliente para llamar a los puntos finales del servicio desde las pruebas de integración o las aplicaciones frontend.

Imagen 5

Ocultar   Shrink    Copiar código

using System;
using Refit;
using Cybtans.Refit;
using System.Net.Http;
using System.Threading.Tasks;
using Catalog.Models;

namespace Catalog.Clients
{
    /// <summary>
    /// Items Catalog Service
    /// </summary>
    [ApiClient]
    public interface ICatalogItemService
    {        
        /// <summary>
        /// Return all the items in the Catalog
        /// </summary>
        [Get("/api/CatalogItem")]
        Task<GetAllCatalogItemResponse> GetAll(GetAllRequest request = null);
        
        /// <summary>
        /// Return an Item given its Id
        /// </summary>
        [Get("/api/CatalogItem/{request.Id}")]
        Task<CatalogItemDto> Get(GetCatalogItemRequest request);
        
        /// <summary>
        /// Create a Catalog Item
        /// </summary>
        [Post("/api/CatalogItem")]
        Task<CatalogItemDto> Create([Body]CreateCatalogItemRequest request);
        
        /// <summary>
        /// Update a Catalog Item
        /// </summary>
        [Put("/api/CatalogItem/{request.Id}")]
        Task<CatalogItemDto> Update([Body]UpdateCatalogItemRequest request);
        
        /// <summary>
        /// Delete a Catalog Item given its Id
        /// </summary>
        [Delete("/api/CatalogItem/{request.Id}")]
        Task Delete(DeleteCatalogItemRequest request);    
    }
}

Usando una puerta de enlace API

Para agregar una puerta de enlace API , creemos un proyecto principal aspnet utilizando dotnet cli o Visual Studio. Debe agregar una referencia al catálogo . Clientes y Proyectos de Catalog.Models . Luego, registre las interfaces de cliente de Catalog en el ConfigureServicesmétodo que se muestra a continuación:Ocultar   Shrink    Copiar código

using System;
using System.IO;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using Cybtans.AspNetCore;
using Catalog.Clients;

namespace Gateway
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }
       
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "Shop", Version = "v1" });
                c.OperationFilter<SwachBuckleOperationFilters>();
                c.SchemaFilter<SwachBuckleSchemaFilters>();

                // Set the comments path for the Swagger JSON and UI.
                var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
                var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
                c.IncludeXmlComments(xmlPath, true);
            });

            //Register all the refit interfaces located in the ICatalogItemService assembly 
            //decorated with the [ApiClient] attribute            
            services.AddClients("http://catalog.restapi", 
                                 typeof(ICatalogItemService).Assembly);            
        }
            
       .....
      
    }
}

Ahora modifiquemos cybtans.json para generar los Gatewaycontroladores como se muestra a continuación:Ocultar   código de copia

{
  "Service": "Catalog",
  "Steps": [  
    {
      "Type": "proto",
      "Output": ".",
      "ProtoFile": "./Proto/Catalog.proto",
      "Gateway": "../Gateway/Controllers/Catalog"
    }
  ]
}

Ejecutar cybtans-cli .y el código se genera en la ruta especificada como se muestra a continuación:

Imagen 6

El código para el controlador de la CatalogItemServiceController puerta de enlace se muestra a continuación como referencia. Es prácticamente idéntico al controlador de servicio de Catalog, pero en lugar de utilizar la interfaz de servicio, utiliza la interfaz de cliente Refit generada Catalog.Clients.ICatalogItemService .Ocultar   Shrink    Copiar código

using System;
using Catalog.Clients;
using Catalog.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Cybtans.AspNetCore;

namespace Catalog.Controllers
{
    /// <summary>
    /// Items Catalog Service
    /// </summary>
    [System.ComponentModel.Description("Items Catalog Service")]
    [Route("api/CatalogItem")]
    [ApiController]
    public partial class CatalogItemServiceController : ControllerBase
    {
        private readonly ICatalogItemService _service;
        
        public CatalogItemServiceController(ICatalogItemService service)
        {
            _service = service;
        }
        
        /// <summary>
        /// Return all the items in the Catalog
        /// </summary>
        [System.ComponentModel.Description("Return all the items in the Catalog")]
        [HttpGet]
        public Task<GetAllCatalogItemResponse> GetAll([FromQuery]GetAllRequest __request)
        {
            return _service.GetAll(__request);
        }
        
        /// <summary>
        /// Return an Item given its Id
        /// </summary>
        [System.ComponentModel.Description("Return an Item given its Id")]
        [HttpGet("{id}")]
        public Task<CatalogItemDto> Get(int id, [FromQuery]GetCatalogItemRequest __request)
        {
            __request.Id = id;
            return _service.Get(__request);
        }
        
        /// <summary>
        /// Create a Catalog Item
        /// </summary>
        [System.ComponentModel.Description("Create a Catalog Item")]
        [HttpPost]
        public Task<CatalogItemDto> Create([FromBody]CreateCatalogItemRequest __request)
        {
            return _service.Create(__request);
        }
        
        /// <summary>
        /// Update a Catalog Item
        /// </summary>
        [System.ComponentModel.Description("Update a Catalog Item")]
        [HttpPut("{id}")]
        public Task<CatalogItemDto> 
               Update(int id, [FromBody]UpdateCatalogItemRequest __request)
        {
            __request.Id = id;
            return _service.Update(__request);
        }
        
        /// <summary>
        /// Delete a Catalog Item given its Id
        /// </summary>
        [System.ComponentModel.Description("Delete a Catalog Item given its Id")]
        [HttpDelete("{id}")]
        public Task Delete(int id, [FromQuery]DeleteCatalogItemRequest __request)
        {
            __request.Id = id;
            return _service.Delete(__request);
        }
    }
}

Generación de código mecanografiado

Además, también podemos generar servicios e interfaces de modelos para Typescript usando la api fetch o Angular HttpClient . Para generar el código Typecript, necesitamos modificar cybtans.json y agregar la Clientsopción como se muestra a continuación:Ocultar   código de copia

{
  "Service": "Catalog",
  "Steps": [
    {
      "Type": "proto",
      "Output": ".",
      "ProtoFile": "./Proto/Catalog.proto",
      "Gateway": "../Gateway/Controllers/Catalog",
      "Clients": [
        {
          "Output": "./typescript/react/src/services",
          "Framework": "react"
        },
        {
          "Output": "./typescript/angular/src/app/services",
          "Framework": "angular"
        }
      ]
    }
  ]
}

En este ejemplo, estamos generando código mecanografiado para dos aplicaciones web, una escrita en react con mecanografiado y la otra en angular. Después de ejecutar el generador, el código resultante se genera en la carpeta que se muestra a continuación:

Imagen 7

Los mensajes se generan en el archivo models.ts de forma predeterminada. El código es idéntico tanto para angular como para reaccionar.Ocultar   Shrink    Copiar código

export interface CatalogBrandDto {
  brand: string;
  id: number;
}

/** The Catalog Item */
export interface CatalogItemDto {
  /** The name of the Catalog Item */
  name: string;
  /** The description of the Catalog Item */
  description: string;
  /** The price of the Catalog Item */
  price: number;
  pictureFileName: string;
  pictureUri: string;
  catalogTypeId: number;
  catalogType?: CatalogTypeDto|null;
  catalogBrandId: number;
  catalogBrand?: CatalogBrandDto|null;
  availableStock: number;
  restockThreshold: number;
  maxStockThreshold: number;
  onReorder: boolean;
  id: number;
}

export interface CatalogTypeDto {
  type: string;
  id: number;
}

export interface GetAllRequest {
  filter?: string;
  sort?: string;
  skip?: number|null;
  take?: number|null;
}

export interface GetCatalogItemRequest {
  id: number;
}

export interface UpdateCatalogItemRequest {
  id: number;
  value?: Partial<CatalogItemDto|null>;
}

export interface DeleteCatalogItemRequest {
  id: number;
}

export interface GetAllCatalogItemResponse {
  items?: CatalogItemDto[]|null;
  page: number;
  totalPages: number;
  totalCount: number;
}

export interface CreateCatalogItemRequest {
  value?: Partial<CatalogItemDto|null>;
}

Por otro lado, el resultado  services de reaccionar y angular son diferentes, la versión de reacción en este caso aprovecha la  api de búsqueda.  Los servicios se generan en el  archivo services.ts de forma predeterminada como se muestra a continuación:Ocultar   Shrink    Copiar código

import { 
  GetAllRequest,
  GetAllCatalogItemResponse,
  GetCatalogItemRequest,
  CatalogItemDto,
  CreateCatalogItemRequest,
  UpdateCatalogItemRequest,
  DeleteCatalogItemRequest,
 } from './models';

export type Fetch = (input: RequestInfo, init?: RequestInit)=> Promise<Response>;
export type ErrorInfo = {status:number, statusText:string, text: string };

export interface CatalogOptions{
    baseUrl:string;
}

class BaseCatalogService {
    protected _options:CatalogOptions;
    protected _fetch:Fetch;    

    constructor(fetch:Fetch, options:CatalogOptions){
        this._fetch = fetch;
        this._options = options;
    }

    protected getQueryString(data:any): string|undefined {
        if(!data)
            return '';
        let args = [];
        for (let key in data) {
            if (data.hasOwnProperty(key)) {                
                let element = data[key];
                if(element !== undefined && element !== null && element !== ''){
                    if(element instanceof Array){
                        element.forEach(e=> args.push(key + '=' + 
                        encodeURIComponent(e instanceof Date ? e.toJSON(): e)));
                    }else if(element instanceof Date){
                        args.push(key + '=' + encodeURIComponent(element.toJSON()));
                    }else{
                        args.push(key + '=' + encodeURIComponent(element));
                    }
                }
            }
        }

       return args.length > 0 ? '?' + args.join('&') : '';    
    }

    protected getFormData(data:any): FormData {
        let form = new FormData();
        if(!data)
            return form;
        
        for (let key in data) {
            if (data.hasOwnProperty(key)) {                
                let value = data[key];
                if(value !== undefined && value !== null && value !== ''){
                    if (value instanceof Date){
                        form.append(key, value.toJSON());
                    }else if(typeof value === 'number' || 
                    typeof value === 'bigint' || typeof value === 'boolean'){
                        form.append(key, value.toString());
                    }else if(value instanceof File){
                        form.append(key, value, value.name);
                    }else if(value instanceof Blob){
                        form.append(key, value, 'blob');
                    }else if(typeof value ==='string'){
                        form.append(key, value);
                    }else{
                        throw new Error(`value of ${key} 
                        is not supported for multipart/form-data upload`);
                    }
                }
            }
        }
        return form;
    }

    protected getObject<T>(response:Response): Promise<T>{
        let status = response.status;
        if(status >= 200 && status < 300 ){            
            return response.json();
        }     
        return response.text().then((text) => 
        Promise.reject<T>({  status, statusText:response.statusText, text }));        
    }

    protected getBlob(response:Response): Promise<Response>{
        let status = response.status;        

        if(status >= 200 && status < 300 ){             
            return Promise.resolve(response);
        }
        return response.text().then((text) => 
        Promise.reject<Response>({  status, statusText:response.statusText, text }));
    }

    protected ensureSuccess(response:Response): Promise<ErrorInfo|void>{
        let status = response.status;
        if(status < 200 || status >= 300){
            return response.text().then((text) => 
            Promise.reject<ErrorInfo>({  status, statusText:response.statusText, text }));        
        }
        return Promise.resolve();
    }
}

/** Items Catalog Service */
export class CatalogItemService extends BaseCatalogService {  

    constructor(fetch:Fetch, options:CatalogOptions){
        super(fetch, options);        
    }
    
    /** Return all the items in the Catalog */
    getAll(request:GetAllRequest) : Promise<GetAllCatalogItemResponse> {
        let options:RequestInit = { method: 'GET', headers: { Accept: 'application/json' }};
        let endpoint = this._options.baseUrl+`/api/CatalogItem`+this.getQueryString(request);
        return this._fetch(endpoint, options).then
                    ((response:Response) => this.getObject(response));
    }
    
    /** Return an Item given its Id */
    get(request:GetCatalogItemRequest) : Promise<CatalogItemDto> {
        let options:RequestInit = { method: 'GET', headers: { Accept: 'application/json' }};
        let endpoint = this._options.baseUrl+`/api/CatalogItem/${request.id}`;
        return this._fetch(endpoint, options).then
                    ((response:Response) => this.getObject(response));
    }
    
    /** Create a Catalog Item */
    create(request:CreateCatalogItemRequest) : Promise<CatalogItemDto> {
        let options:RequestInit = { method: 'POST', 
        headers: { Accept: 'application/json', 'Content-Type': 'application/json' }};
        options.body = JSON.stringify(request);
        let endpoint = this._options.baseUrl+`/api/CatalogItem`;
        return this._fetch(endpoint, options).
               then((response:Response) => this.getObject(response));
    }
    
    /** Update a Catalog Item */
    update(request:UpdateCatalogItemRequest) : Promise<CatalogItemDto> {
        let options:RequestInit = { method: 'PUT', 
        headers: { Accept: 'application/json', 'Content-Type': 'application/json' }};
        options.body = JSON.stringify(request);
        let endpoint = this._options.baseUrl+`/api/CatalogItem/${request.id}`;
        return this._fetch(endpoint, options).then
               ((response:Response) => this.getObject(response));
    }
    
    /** Delete a Catalog Item given its Id */
    delete(request:DeleteCatalogItemRequest) : Promise<ErrorInfo|void> {
        let options:RequestInit = 
        { method: 'DELETE', headers: { Accept: 'application/json' }};
        let endpoint = this._options.baseUrl+`/api/CatalogItem/${request.id}`;
        return this._fetch(endpoint, options).then
               ((response:Response) => this.ensureSuccess(response));
    }
}

Mientras que services para el angular se usa HttpClient y se generan de forma predeterminada en service.ts como se muestra a continuación:Ocultar   Shrink    Copiar código

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { HttpClient, HttpHeaders, HttpEvent, HttpResponse } from '@angular/common/http';
import { 
  GetAllRequest,
  GetAllCatalogItemResponse,
  GetCatalogItemRequest,
  CatalogItemDto,
  CreateCatalogItemRequest,
  UpdateCatalogItemRequest,
  DeleteCatalogItemRequest,
 } from './models';

function getQueryString(data:any): string|undefined {
  if(!data) return '';
  let args = [];
  for (let key in data) {
      if (data.hasOwnProperty(key)) {                
          let element = data[key];
          if(element !== undefined && element !== null && element !== ''){
              if(element instanceof Array){
                  element.forEach(e=>args.push(key + '=' + 
                  encodeURIComponent(e instanceof Date ? e.toJSON(): e)) );
              }else if(element instanceof Date){
                  args.push(key + '=' + encodeURIComponent(element.toJSON()));
              }else{
                  args.push(key + '=' + encodeURIComponent(element));
              }
          }
      }
  }

  return args.length > 0 ? '?' + args.join('&') : '';
}

function getFormData(data:any): FormData {
    let form = new FormData();
    if(!data)
        return form;
        
    for (let key in data) {
        if (data.hasOwnProperty(key)) {                
            let value = data[key];
            if(value !== undefined && value !== null && value !== ''){
                if (value instanceof Date){
                    form.append(key, value.toJSON());
                }else if(typeof value === 'number' || 
                typeof value === 'bigint' || typeof value === 'boolean'){
                    form.append(key, value.toString());
                }else if(value instanceof File){
                    form.append(key, value, value.name);
                }else if(value instanceof Blob){
                    form.append(key, value, 'blob');
                }else if(typeof value ==='string'){
                    form.append(key, value);
                }else{
                    throw new Error(`value of ${key} is not supported 
                          for multipart/form-data upload`);
                }
            }
        }
    }
    return form;
}

/** Items Catalog Service */
@Injectable({
  providedIn: 'root',
})
export class CatalogItemService {

    constructor(private http: HttpClient) {}
    
    /** Return all the items in the Catalog */
    getAll(request: GetAllRequest): Observable<GetAllCatalogItemResponse> {
      return this.http.get<GetAllCatalogItemResponse>
      (`/api/CatalogItem${ getQueryString(request) }`, {
          headers: new HttpHeaders({ Accept: 'application/json' }),
      });
    }
    
    /** Return an Item given its Id */
    get(request: GetCatalogItemRequest): Observable<CatalogItemDto> {
      return this.http.get<CatalogItemDto>(`/api/CatalogItem/${request.id}`, {
          headers: new HttpHeaders({ Accept: 'application/json' }),
      });
    }
    
    /** Create a Catalog Item */
    create(request: CreateCatalogItemRequest): Observable<CatalogItemDto> {
      return this.http.post<CatalogItemDto>(`/api/CatalogItem`, request, {
          headers: new HttpHeaders
          ({ Accept: 'application/json', 'Content-Type': 'application/json' }),
      });
    }
    
    /** Update a Catalog Item */
    update(request: UpdateCatalogItemRequest): Observable<CatalogItemDto> {
      return this.http.put<CatalogItemDto>(`/api/CatalogItem/${request.id}`, request, {
          headers: new HttpHeaders
          ({ Accept: 'application/json', 'Content-Type': 'application/json' }),
      });
    }
    
    /** Delete a Catalog Item given its Id */
    delete(request: DeleteCatalogItemRequest): Observable<{}> {
      return this.http.delete<{}>(`/api/CatalogItem/${request.id}`, {
          headers: new HttpHeaders({ Accept: 'application/json' }),
      });
    }
}

Las clases de servicio generadas admiten FormData para cargas de varias partes y proporcionan el objeto Response para los archivos descargados como blobs. Además, puede usar un interceptor en angular para configurar la URL base y los tokens de autenticación. Por otro lado, al utilizar la API de recuperación, puede proporcionar una función de proxy para configurar tokens de autenticación y encabezados adicionales.

Generación de mensajes y servicios a partir de clases C #

Generalmente, cuando se usa Entity Framework con un enfoque de Code First , se definen los modelos de datos con clases y las relaciones mediante convenciones ef o la API Fluent. Puede agregar migraciones para crear y aplicar cambios a la base de datos.

Por otro lado, no expone los modelos de datos directamente desde su servicio. En su lugar, asigna los modelos de datos a los objetos de transferencia de datos, también conocidos como dtos. Generalmente, puede ser tedioso y lento definir todos los dtos con mensajes en el archivo proto. Afortunadamente, la cybtans-clipuede generar un archivo proto con los mensajes de datos y operaciones comunes como readcreateupdatedelete. Todo lo que necesita hacer es especificar un paso en cybtans.json como se muestra a continuación:Ocultar   Shrink    Copiar código

{
  "Service": "Catalog",
  "Steps": [
    {
      "Type": "messages",
      "Output": ".",
      "ProtoFile": "./Proto/Domain.proto",
      "AssemblyFile": "./Catalog.RestApi/bin/Debug/netcoreapp3.1/Catalog.Domain.dll"
    },
    {
      "Type": "proto",
      "Output": ".",
      "ProtoFile": "./Proto/Catalog.proto",
      "Gateway": "../Gateway/Controllers/Catalog",
      "Clients": [
        {
          "Output": "./typecript/react/src/services",
          "Framework": "react"
        },
        {
          "Output": "./typecript/angular/src/app/services",
          "Framework": "angular"
        }
      ]
    }
  ]
}  

El paso con tipo messagedefine AssemblyFiledesde donde se generan los mensajes, ProtoFiledefine el protocolo de salida y Outputespecifica la carpeta de microservicio para generar implementaciones de servicios comunes. Ahora podemos cambiar el archivo Catalog.proto como se muestra a continuación:Ocultar   código de copia

syntax = "proto3";

import "./Domain.proto";

package Catalog;

El archivo Catalog.proto es como el punto de entrada principal para cybtans-cli. Puede incluir definiciones en otros protos utilizando la importdeclaración. Además, puede extender un mensaje o servicio declarado en un proto importado definiendo un mensaje o servicio con el mismo nombre pero con campos adicionales o rpcs.

Para generar mensajes y servicios a partir de un ensamblado, debe agregar el GenerateMessageAttributeatributo a las clases como, por ejemplo, como se muestra a continuación. El mensaje y los servicios se generan en el archivo Domain.proto .Ocultar   Shrink    Copiar código

using Cybtans.Entities;
using System.ComponentModel;

namespace Catalog.Domain
{
    [Description("The Catalog Item")]
    [GenerateMessage(Service = ServiceType.Default)]
    public class CatalogItem:Entity<int>
    {        
        [Description("The name of the Catalog Item")]
        public string Name { get; set; }

        [Description("The description of the Catalog Item")]
        public string Description { get; set; }

        public decimal Price { get; set; }

        public string PictureFileName { get; set; }

        public string PictureUri { get; set; }

        public int CatalogTypeId { get; set; }

        public CatalogType CatalogType { get; set; }

        public int CatalogBrandId { get; set; }

        public CatalogBrand CatalogBrand { get; set; }

        public int AvailableStock { get; set; }

        [Description("Available stock at which we should reorder")]
        public int RestockThreshold { get; set; }

        [Description("Maximum number of units that can be in-stock at any time 
                    (due to physical/logistical constraints in warehouses")]
        public int MaxStockThreshold { get; set; }

        [Description("True if item is on reorder")]
        public bool OnReorder { get; set; }        
    }
}

Puntos de interés

Como punto de interés, puede observar cómo cybtans cli puede disminuir el tiempo de desarrollo de las soluciones de microservicio. Al mismo tiempo, mejora la calidad del código mediante el uso de una arquitectura en capas a nivel de microservicio. 

Además, la lógica empresarial representada por los servicios es independiente de la infraestructura subyacente como aspnetcore y de la capa de transporte como los controladores API. Además, mediante el uso de controladores de puerta de enlace API generados automáticamente, puede integrar aplicaciones frontend con menos esfuerzo y proporciona documentación útil para los desarrolladores frontend.

Fuente: https://www.codeproject.com/Articles/5280533/Driving-Development-of-Microservices-and-REST-APIs

Deja un comentario