使用Protobuf推动微服务和REST API的开发(译文)
By S.F.
本文链接 https://www.kyfws.com/news/driving-development-of-microservices-and-rest-apis/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 18 分钟阅读 - 8716 个词 阅读量 0使用Protobuf推动微服务和REST API的开发(译文)
原文地址:https://www.codeproject.com/Articles/5280533/Driving-Development-of-Microservices-and-REST-APIs
原文作者:Ansel Castro
译文由本站翻译
前言
Leveraging Protobuf and code generation tools to drive the development of microservices and REST APIs 利用Protobuf和代码生成工具来驱动微服务和REST API的开发 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. 本文介绍了如何使用协议缓冲区(也称为protobuf)和代码生成工具来加快微服务体系结构和REST API的开发.
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.
自2011年首次使用微服务架构以来,微服务架构已成为当今的通用术语.从那时起,许多组织就采用了这种模式来推动全球规模的运营.最初设计该体系结构模式是为了解决面向服务的体系结构或SOA中的主要问题之一.因此,我们可以说微服务是SOA的一种专业化,旨在提供真正的服务独立性,数据主权和连续部署.
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.
为了加强一致性,我们可以利用RabbitMQ之类的代理来发送和处理集成事件.另一方面,对于服务监视,我们可以使用Linkerd之类的服务网格.对于服务发现和文档,可以使用API网关将所有内部微服务聚合到一个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.
因此,开发和维护微服务解决方案既耗时又乏味.因为除了专注于服务层的业务逻辑之外,您还需要通过Web API公开服务,开发API网关以及用于客户端应用程序的客户端库或SDK.
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.
本文的目的是向您展示如何使用协议缓冲区语言来生成代码并更快地构建微服务解决方案.使用这种方法,我们可以节省时间并专注于业务逻辑,而不必编写API网关和客户端库来与内部服务进行通信.
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.
此外,您可以使用aspnet核心集成测试编写健壮的代码来测试与aspnet基础结构集成的业务逻辑,包括数据库支持和文件系统.
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.
Protocol Buffer是Google定义的与平台无关的语言,可以以更有效的方式序列化结构化数据,就像Json一样,但体积更小,速度更快.您可以使用一种简单的语言定义数据的结构,然后可以使用生成的特殊源代码轻松地在各种数据流之间读写结构化数据.
*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. *
除了定义数据结构的消息外,您还可以在扩展名为.proto的文件中声明服务和rpcs(端点). Protobuf语言通常与Grpc一起使用.在Grpc中,您可以定义具有rpcs aka端点的服务,此外,还可以生成用于不同语言的客户端库.但是Grpc有一个缺点,例如,很难从基于Web的应用程序中调用它.因此Grpc主要用于内部服务.
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.
这就是设计基于CybtanSDK原型的生成器的原因,以提供最佳的Grpc和基于REST的服务.因此,Web应用程序可以使用JSON来使用服务,还可以使用更快的二进制序列化进行内部通信.
Getting Started with CybtansSDK(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.
CybtansSDK是一个开源项目,可让您生成C#代码,以便使用.proto文件中定义的消息,服务和rpcs使用AspNetCore开发微服务. .
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.
该工具提供的主要优点是生成服务接口,又称为Dto的数据传输对象,API控制器,网关和C#和Typescript的客户端库.最重要的是,它可以为所有生成的代码编写文档.
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:
具有自动生成的API网关的另一个优势是消除了开发和维护另一个RESTfull API的复杂性.因此,您可以将所有微服务聚合到一个REST API中,从而促进服务发现,文档和应用程序集成.使用此模式可获得的其他好处是:
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:
因此,让我们开始一个例子.首先下载cybtans cli代码生成器,然后解压缩zip文件并添加.exe所在的文件夹到您的路径以方便使用.然后使用dotnet cli或Visual Studio生成解决方案.例如:
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:
现在,让我们生成一个用于管理产品目录的微服务项目结构.在命令提示符或Powershell窗口中运行以下命令.例如:
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文件夹下生成多个项目.一些项目描述如下:
- Catalog.Client (Catalog.Client)
- Catalog.Models (目录模型)
- Catalog.RestApi (Catalog.Rest Api)
- Catalog.Services (目录服务)
- Catalog.Services.Tests (Catalog.Services.Tests)
- Proto (原型)
Generating C# Code from proto Files(从原型文件生成C#代码)
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 mainproto
file used by the code generator as shown below:
连同生成的项目一起,创建了一个名为cybtans.json的json文件.该文件包含命令" cybtans-cli [解决方案文件夹]“的配置设置.这些设置指定代码生成器使用的主要proto
文件,如下所示:
{
"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 theCatalog
microservice, notice how thepackage
statement is used to define the microservice name.
现在,让我们修改Proto/Catalog.proto文件以定义” Catalog"微服务的数据结构,请注意如何使用" package"语句定义微服务名称.
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.
除了上面的消息外,让我们现在定义一个服务和一些操作(aka rpcs),还为rpc的请求和响应数据结构定义一些其他消息.
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.
您可以通过运行以下命令来生成csharp代码.您需要提供cybtans.json所在的路径.工具在所有子目录中递归搜索此配置文件.
cybtans-cli .
The messages are generated in the Models project by default using the package’s name as the main namespace as shown below:
默认情况下,消息是在Models项目中使用包的名称作为主要名称空间生成的,如下所示:
For example, the code for the
CatalogItemDto
class is shown below. You may notice theCatalogItemDto
Accesor, this class is generated in order to provide additional metadata for inspecting property types and setting/getting property values without using reflection.
例如,下面显示了" CatalogItemDto"类的代码.您可能会注意到CatalogItemDto
Accesor,此类的生成是为了提供其他元数据,用于检查属性类型和设置/获取属性值,而无需使用反射.
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 theCybtans.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 likeAPI 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.
Cybtans.Serialization软件包利用了IReflectorMetadata接口,以加快将对象序列化为二进制格式的速度,该格式比JSON更为紧凑和高效.此格式用于服务间通信,例如" API网关"-“微服务"通信.因此,Web应用程序可以使用JSON来使用网关的端点,而网关可以使用二进制格式与上游服务进行通信.
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.
默认情况下,在以下所示的文件夹中生成服务接口(也称为合同).请注意,如何使用原型文件中的description选项记录代码.此说明可帮助您记录REST API,并具有改善维护和与前端应用程序集成的好处.
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.
Cybtans代码生成器创建用于公开服务层的API控制器.默认情况下,在以下所示的文件夹中会生成API控制器.您需要关心的是实现服务接口.
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.
下面显示了API控制器的代码作为参考.您需要在ServiceCollection中注册服务实现,以便将其注入到控制器的构造函数中.
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.
该工具可以使用Refit界面生成类型安全的客户端,如下面的文件夹所示.您可以使用此客户端从集成测试或前端应用程序调用服务端点.
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(使用API网关)
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:
为了添加API网关,让我们使用dotnet cli或Visual Studio创建一个aspnet核心项目.您需要添加对Catalog.Clients和Catalog.Models项目的引用.然后在ConfigureServices
方法中注册目录客户端接口,如下所示:
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:
现在让我们修改cybtans.json来生成网关的控制器,如下所示:
{
"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:
运行cybtans-cli.,代码将在指定的路径中生成,如下所示:
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.
下面显示了” CatalogItemServiceController"网关控制器的代码作为参考.它实际上与Catalog的服务控制器相同,但是它不使用服务接口,而是使用生成的Refit客户端接口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
{
/// <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:
此外,我们还可以使用fetch api或Angular HttpClient.为了生成Typescript代码,我们需要修改cybtans.json并添加Clients
选项,如下所示:
{
"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:
在此示例中,我们为两个Web应用程序生成了打字稿代码,一个Web应用程序以打字稿的形式编写,另一个以angular编写.运行生成器后,将在下面显示的文件夹中生成结果代码:
The messages are generated in the models.ts file by default. The code is identical for both angular and react.
默认情况下,消息是在models.ts文件中生成的.角度和反应的代码相同.
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:
另一方面,为react和angular提供的服务是不同的,在这种情况下,react版本利用fetch api.默认情况下,服务在services.ts文件中生成,如下所示:
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 theHttpClient
and are generated by default in the service.ts as shown below:
如下图所示,虽然默认情况下在service.ts中使用了用于HttpClient的角度的services HttpClient.
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.
生成的服务类支持用于分段上传的FormData,并以blob的形式为下载的文件提供响应对象.此外,您可以使用角度拦截器来设置基本URL和身份验证令牌.另一方面,在使用访存API时,您可以提供代理功能,用于将身份验证令牌设置为其他标头.
Generating Message and Service from C# Classes(从C#类生成消息和服务)
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.
通常,在将Entity Framework与Code First方法中,您可以使用ef约定或Fluent API使用类和关系定义数据模型.您可以添加迁移以创建更改并将更改应用到数据库.
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 likeread
,create
,update
anddelete
. All you need to do is specify a step in the cybtans.json as shown below:
另一方面,您不会直接从服务中公开数据模型.而是将数据模型映射到数据传输对象(也称为dtos).通常,用原始文件中的消息定义所有dto可能很繁琐且耗时.幸运的是," cybtans-cli"可以生成带有消息和常见数据操作(如" read"," create"," update"和" delete")的原型文件.您需要做的就是在cybtans.json中指定一个步骤,如下所示:
{
"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 theAssemblyFile
from wheremessage
are generated, theProtoFile
defines the output proto and theOutput
specify the microservice folder for generating common service implementations. Now we can change the Catalog.proto file as shown below:
类型为message的步骤定义了从中生成message的AssemblyFile,ProtoFile定义了输出proto,Output指定了用于生成公共服务实现的微服务文件夹.现在,我们可以更改Catalog.proto文件,如下所示:
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 theimport
statement. Moreover, you can extend a message or service declared in animport
ed proto by defining a message or service with the same name but with additional fields or rpcs.
Catalog.proto文件类似于" cybtans-cli"的主要入口点.您可以使用ʻimport`语句将定义包含在其他原型中.此外,您可以通过定义具有相同名称但带有其他字段或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.
为了从程序集中生成消息和服务,您需要向类中添加GenerateMessageAttribute
属性,如下所示.该消息和服务在Domain.proto文件中生成.
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.
有趣的是,您会注意到" cybtans cli"如何减少微服务解决方案的开发时间.同时,它通过在微服务级别使用分层架构来提高代码质量.此外,服务所代表的业务逻辑独立于诸如aspnetcore之类的基础架构以及诸如API Controllers之类的传输层.此外,通过使用自动生成的API网关控制器,您可以轻松集成前端应用程序,并为前端开发人员提供有用的文档.
History(历史)
- th (日)
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
C# CEO Architect Dev Intermediate Advanced REST microservice ASP.NET-Core 新闻 翻译