Click here to Skip to main content
15,126,317 members
Articles / Web Development / ASP.NET / ASP.NET Core
Article
Posted 4 Oct 2020

Stats

10.5K views
17 bookmarked

Driving Development of Microservices and REST APIs with Protobuf

Rate me:
Please Sign up or sign in to vote.
5.00/5 (14 votes)
6 Oct 2020CPOL10 min read
Leveraging Protobuf and code generation tools to drive the development of microservices and REST APIs
This article shows how to use protocol buffers aka protobuf and code generation tools to speed up the development of microservice architectures and REST APIs.

Introduction

Microservice architecture is a common term nowadays since 2011 when the term was used for the first time. Since then, the pattern has been adopted by many organizations to power operations at global scale. The architectural pattern was initially designed to solve one of the main problems in Service Oriented Architecture or SOA. So we can talk about microservices as being a specialization of SOA which aim to provide true service independency, data sovereignty and continuous deployment.

But all the benefits a microservice architecture provides comes with a cost. It increases the complexity for maintaining consistency, service discovering, documentation and monitoring in a large set of microservices.

For enforcing the consistency, we can leverage a broker like RabbitMQ for sending and processing integration events. On the other hand, for service monitoring, we can use a service mesh like Linkerd. For service discovering and documentation, an API Gateway can be used to aggregate all the internal microservice's in just one REST API.

So developing and maintaining a microservice solution can be time consuming and tedious. Because in addition to focus in the business logic at the service layer, you will have to take care of exposing the services through a Web API, develop an API Gateway and client libraries or SDKs for client applications.

The purpose of this article is to show you how to use the protocol buffer language in order to generate code and build microservice solutions faster. Using this approach, we can save time and focus on the business logic, instead of writing API Gateways and Client Libraries for communicating with the internal services.

In addition, you can write robust code by using aspnet core integration test for testing the business logic integrated with the aspnet infrastructure including database support and filesystem.

A common microservice architecture is shown below.

Background

Protocol Buffer is a platform-agnostic language defined at Google to serialize structured data in a more efficient way, think of Json, but smaller and faster. You define the structure of your data using a simple language, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams.

In addition to messages which define the data structure, you can declare services and rpcs (endpoints) in files with the .proto extension. The Protobuf language is commonly used with Grpc. In Grpc you can define services with rpcs aka endpoints, in addition clients libraries for different languages can be generated as well.  But Grpc has a drawback, like for example it can be difficult to be called from web based apps. Therefore Grpc is mostly use for internal services. 

That is why the CybtanSDK proto-based generator was designed, to offer the best of Grpc and REST-based services. Therefore, the services can be consumed by the web application using JSON and also use faster binary serialization for internal communications.

Getting Started with CybtansSDK

CybtansSDK is a open source project that alllows you to generate code for C# in order to develop microservices with AspNetCore using messages, services and rpcs defined in the .proto files.

The main advantage this tool provides is the generation of Service Interfaces, Data Transfer Objects aka Dtos, API Controllers, Gateways and Client Libraries for C# and Typescript. Most importantly, it can write documentation for all the code generated.

Another advantage of having an automatically generated API Gateway is to remove the complexity of developing and maintaining another RESTfull API. So you can aggregate all the microservices in just one REST API, facilitating service discovering, documentation and apps integration. Other benefits you can get using this pattern are:

  • Insulates the clients from how the application is partitioned into microservices
  • Insulates the clients from the problem of determining the locations of service instances
  • Provides the optimal API for each client
  • Reduces the number of requests/roundtrips. For example, the API gateway enables clients to retrieve data from multiple services with a single round-trip. Fewer requests also means less overhead and improves the user experience. An API gateway is essential for mobile applications.
  • Simplifies the client by moving logic for calling multiple services from the client to API gateway
  • Translates from a “standard” public web-friendly API protocol to whatever protocols are used internally

So let's get started with an example. First download the cybtans cli code generator ,then extract the zip file and add the folder where the .exe is located to your path in order to facilitate the usage. Then generate a solution with the dotnet cli or Visual Studio. For example:

PowerShell
dotnet new sln -n MySolution

Now let's generate a microservice project structure for managing a Catalog of products. Run the following command in a command prompt or powershell windows. For example:

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

This command follows a convention and generates several projects under the Catalog folder. Some of the projects are described below:

  • Catalog.Client: .NET Standard project with the microservice's client library for C#
  • Catalog.Models: .NET Standard project with the microservice's Dtos, request and response messages
  • Catalog.RestApi: AspNetCore project with the microservice's Rest API
  • Catalog.Services: .NET Core project with the microservice's business logic or services
  • Catalog.Services.Tests: The microservice's integration tests
  • Proto: The microservice protobuff definitions

Generating C# Code from proto Files

Along with the projects generated, a json file was created with the name cybtans.json. This file contains the configuration settings for the command cybtans-cli [solution folder]. Those settings specify the main proto file used by the code generator as shown below:

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

Now let's modify the Proto/Catalog.proto file to define the data structures for the Catalog microservice, notice how the package statement is used to define the microservice name.

proto
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;
}

In addition to the message above, let's define now a service and some operations (aka rpcs), let's also define some additional messages for the rpc's requests and responses data structure.

proto
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";
    };
}

You can generate the csharp code by running the command shown below. You need to provide the path where the cybtans.json is located. The tools search for this configuration file recursively in all the subdirectories.

PowerShell
cybtans-cli .

The messages are generated in the Models project by default using the package's name as the main namespace as shown below:

For example, the code for the CatalogItemDto class is shown below. You may notice the CatalogItemDtoAccesor, this class is generated in order to provide additional metadata for inspecting property types and setting/getting property values without using reflection.

C#
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
       ....
    }
}

The IReflectorMetadata interface is leveraged by the Cybtans.Serialization package in order to speed up the serialization of objects into a binary format that is more compact and efficient than JSON. This format is used for inter-service communications like API Gateway - Microservice communications. Therefore web apps can use JSON for consuming the Gateway's endpoints and instead the Gateway can use a binary format for communicating with the upstream services.

The service interface aka contract is generated by default in the folder shown below. Notice how the code is documented using the description option in the proto file. This description can help to keep you REST APIs documented ,with the benefits of improving maintenance and integration with frontend apps.

C#
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);        
    }
}

The Cybtans code generator creates API Controllers for exposing the service layer. The API Controller is generated by default in the folder shown below. All you need to care about is implementing the service interface.

The code for the API Controller is shown below as a reference. It's up to you to register the service implementation with the ServiceCollection in order be injected into the controller's constructor.

C#
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);
        }
    }
}

The tool can generate type safe client using Refit interfaces as shown in the folder below. You can use this client for calling the service endpoints from integration tests or frontend apps.

C#
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);    
    }
}

Using an API Gateway

In order to add an API Gateway, let's create an aspnet core project either using dotnet cli or Visual Studio. You need to add a reference to the Catalog.Clients and Catalog.Models projects. Then register the Catalog client  interfaces in the ConfigureServices method as shown below:

C#
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);            
        }
            
       .....
      
    }
}

Now let's modify the cybtans.json in order to generate the Gateway's controllers as shown below:

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

Run cybtans-cli ., and the code is generated in the path specified as shown below:

The code for the CatalogItemServiceController Gateway's Controller is shown below as a reference. It's practically identical to the Catalog's service Controller but instead of using the service interface, it uses the generated Refit client interface Catalog.Clients.ICatalogItemService.

C#
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);
        }
    }
}

Generating Typescript Code

In addition, we can also generate services and model interfaces for Typescript using the fetch api or Angular HttpClient. In order to generate the Typescript code, we need to modify the cybtans.json and add the Clients option as shown below:

JavaScript
{
  "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"
        }
      ]
    }
  ]
}

In this example, we are generating typescript code for two web apps, one written in react with typescript and the other in angular. After running the generator ,the resulting code is generated in the folder shown below:

The messages are generated in the models.ts file by default. The code is identical for both angular and react.

tscript
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>;
}

On the other hand the resulting services for react and angular are different , the react version in this case leverage the fetch api. The services are generated in the services.ts file by default as shown below:

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

While the services for the angular used the HttpClient and are generated by default in the service.ts as shown below:

JavaScript
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' }),
      });
    }
}

The generated service classes support FormData for multipart uploads and provides the Response object for downloaded files as blobs. In addition, you can use an Interceptor in angular for setting the base url and authentication tokens. On the other hand when using the fetch API, you can provide a proxy function for setting authentication tokens an additional headers.

Generating Message and Service from C# Classes

Generally when using Entity Framework with a Code First approach, you define the data models with classes and the relationships using ef conventions or the Fluent API. You can add migrations to create and apply changes to the database.

On the other hand, you don't expose the data models directly from your service. Instead, you map the data models to data transfer objects aka dtos. Generally, it can be tedious and time consuming to define all the dtos with messages in the proto file. Fortunately, the cybtans-cli can generate a proto file with the messages and common data operations like read, create, update and delete. All you need to do is specify a step in the cybtans.json as shown below:

JavaScript
{
  "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"
        }
      ]
    }
  ]
}  

The step with type message defines the AssemblyFile from where message are generated, the ProtoFile defines the output proto and the Output specify the microservice folder for generating common service implementations. Now we can change the Catalog.proto file as shown below:

Python
syntax = "proto3";

import "./Domain.proto";

package Catalog;

The Catalog.proto file is like the main entry point for the cybtans-cli. You can include definitions in others protos by using the import statement. Moreover, you can extend a message or service declared in an imported proto by defining a message or service with the same name but with additional fields or rpcs.

In order to generate messages and services from an assembly, you need to add the GenerateMessageAttribute attribute to the classes like for example as shown below. The message and services are generated in the Domain.proto file.

C#
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; }        
    }
}

Points of Interest

As a point of interest, you can notice how the cybtans cli can decrease the development time for microservice solutions. At the same time it improves the code quality by using a layered architecture at the microservice level. Moreover, the business logic represented by the services is independent of the underlying infrastructure like aspnetcore and the transport layer like the API Controllers. Also by using an autogenerated API Gateway Controllers you can integrate frontend apps with less effort and provides useful documentation for frontend developers.

History

  • 4th October, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Ansel Castro
Software Developer (Senior)
Uruguay Uruguay
Senior Software Engineer with more than 8 years of experience in the industry. Graduated from Computer Science ,focused on .NET and Java technologies with special interest on Computer Graphics, Compilers , Languages and Machine Learning.

Comments and Discussions

 
QuestionSuggestion: attach the resulting generated code to the article, so readers can look at result without going thru all the steps to install and run. thank you Pin
Roman Ivantsov12-Oct-20 10:21
professionalRoman Ivantsov12-Oct-20 10:21 
PraiseRe: Suggestion: attach the resulting generated code to the article, so readers can look at result without going thru all the steps to install and run. thank you Pin
Nelek7-Nov-20 5:27
protectorNelek7-Nov-20 5:27 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.