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:
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:
{
"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.
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.
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.
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.
using System;
using Cybtans.Serialization;
using System.ComponentModel;
namespace Catalog.Models
{
[Description("The Catalog Item")]
public partial class CatalogItemDto : IReflectorMetadataProvider
{
private static readonly CatalogItemDtoAccesor __accesor = new CatalogItemDtoAccesor();
[Description("The name of the Catalog Item")]
public string Name {get; set;}
[Description("The description of the Catalog Item")]
public string Description {get; set;}
[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
{
....
}
}
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.
using System;
using System.Threading.Tasks;
using Catalog.Models;
using System.Collections.Generic;
namespace Catalog.Services
{
public partial interface ICatalogItemService
{
Task<GetAllCatalogItemResponse> GetAll(GetAllRequest request);
Task<CatalogItemDto> Get(GetCatalogItemRequest request);
Task<CatalogItemDto> Create(CreateCatalogItemRequest request);
Task<CatalogItemDto> Update(UpdateCatalogItemRequest request);
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.
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
{
[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;
}
[System.ComponentModel.Description("Return all the items in the Catalog")]
[HttpGet]
public Task<GetAllCatalogItemResponse> GetAll([FromQuery]GetAllRequest __request)
{
return _service.GetAll(__request);
}
[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);
}
[System.ComponentModel.Description("Create a Catalog Item")]
[HttpPost]
public Task<CatalogItemDto> Create([FromBody]CreateCatalogItemRequest __request)
{
return _service.Create(__request);
}
[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);
}
[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.
using System;
using Refit;
using Cybtans.Refit;
using System.Net.Http;
using System.Threading.Tasks;
using Catalog.Models;
namespace Catalog.Clients
{
[ApiClient]
public interface ICatalogItemService
{
[Get("/api/CatalogItem")]
Task<GetAllCatalogItemResponse> GetAll(GetAllRequest request = null);
[Get("/api/CatalogItem/{request.Id}")]
Task<CatalogItemDto> Get(GetCatalogItemRequest request);
[Post("/api/CatalogItem")]
Task<CatalogItemDto> Create([Body]CreateCatalogItemRequest request);
[Put("/api/CatalogItem/{request.Id}")]
Task<CatalogItemDto> Update([Body]UpdateCatalogItemRequest request);
[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:
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>();
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath, true);
});
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:
{
"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.
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
{
[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;
}
[System.ComponentModel.Description("Return all the items in the Catalog")]
[HttpGet]
public Task<GetAllCatalogItemResponse> GetAll([FromQuery]GetAllRequest __request)
{
return _service.GetAll(__request);
}
[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);
}
[System.ComponentModel.Description("Create a Catalog Item")]
[HttpPost]
public Task<CatalogItemDto> Create([FromBody]CreateCatalogItemRequest __request)
{
return _service.Create(__request);
}
[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);
}
[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:
{
"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.
export interface CatalogBrandDto {
brand: string;
id: number;
}
export interface CatalogItemDto {
name: string;
description: string;
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:
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();
}
}
export class CatalogItemService extends BaseCatalogService {
constructor(fetch:Fetch, options:CatalogOptions){
super(fetch, options);
}
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));
}
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(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(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(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:
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;
}
@Injectable({
providedIn: 'root',
})
export class CatalogItemService {
constructor(private http: HttpClient) {}
getAll(request: GetAllRequest): Observable<GetAllCatalogItemResponse> {
return this.http.get<GetAllCatalogItemResponse>
(`/api/CatalogItem${ getQueryString(request) }`, {
headers: new HttpHeaders({ Accept: 'application/json' }),
});
}
get(request: GetCatalogItemRequest): Observable<CatalogItemDto> {
return this.http.get<CatalogItemDto>(`/api/CatalogItem/${request.id}`, {
headers: new HttpHeaders({ Accept: 'application/json' }),
});
}
create(request: CreateCatalogItemRequest): Observable<CatalogItemDto> {
return this.http.post<CatalogItemDto>(`/api/CatalogItem`, request, {
headers: new HttpHeaders
({ Accept: 'application/json', 'Content-Type': 'application/json' }),
});
}
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(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:
{
"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:
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.
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