65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Protobuf 驱动微服务和 REST API 的开发

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2020年10月4日

CPOL

10分钟阅读

viewsIcon

16249

利用 Protobuf 和代码生成工具驱动微服务和 REST API 的开发

引言

自2011年“微服务”一词首次使用以来,微服务架构如今已成为一个常用术语。从那时起,许多组织都采用了这种模式,以支持全球范围内的运营。这种架构模式最初是为了解决面向服务架构(SOA)中的一个主要问题而设计的。因此,我们可以将微服务看作是SOA的一种专业化,旨在提供真正的服务独立性、数据主权和持续部署。

但是,微服务架构所提供的所有好处都伴随着代价。它增加了在大量微服务中维护一致性、服务发现、文档和监控的复杂性。

为了强制执行一致性,我们可以利用像 RabbitMQ 这样的代理来发送和处理 集成事件。另一方面,对于服务监控,我们可以使用像 Linkerd 这样的服务网格。对于服务发现和文档,可以使用 API 网关 将所有内部微服务聚合到一个 REST API 中。

因此,开发和维护微服务解决方案可能既耗时又乏味。因为除了关注服务层的业务逻辑外,您还需要通过 Web API 暴露服务,开发 API 网关以及客户端应用程序的客户端库或 SDK。

本文的目的是向您展示如何使用 协议缓冲区语言 来生成代码并更快地构建微服务解决方案。通过这种方法,我们可以节省时间并专注于业务逻辑,而不是编写 API 网关和客户端库来与内部服务进行通信。

此外,您可以使用 aspnet core 集成测试 来测试与 aspnet 基础设施(包括数据库支持和文件系统)集成的业务逻辑,从而编写健壮的代码。

常见的微服务架构如下图所示。

背景

Protocol Buffer 是一种平台无关的语言,由谷歌定义,用于以更高效的方式序列化结构化数据,可以把它想象成 Json,但更小更快。您可以使用一种简单的语言定义数据结构,然后可以使用特殊生成的源代码轻松地将结构化数据写入各种数据流并从中读取。

除了定义数据结构的消息之外,您还可以在文件中声明服务和 rpcs(端点)。 文件扩展名为 .protoProtobuf 语言通常与 Grpc 一起使用。在 Grpc 中,您可以定义包含 rpcs(也称为端点)的服务,此外还可以生成不同语言的客户端库。但是 Grpc 有一个缺点,例如,它可能难以从基于 Web 的应用程序调用。因此 Grpc 主要用于内部服务。

这就是 CybtansSDK 基于 proto 的生成器被设计出来的原因,它旨在提供 Grpc 和基于 REST 的服务的最佳结合。因此,Web 应用程序可以使用 JSON 消费服务,并且内部通信也可以使用更快的二进制序列化。

CybtansSDK 入门

CybtansSDK 是一个开源项目,它允许您为 C# 生成代码,以便使用 .proto 文件中定义的消息、服务和 rpc 来开发 AspNetCore 微服务。

该工具的主要优势在于能够为 C#Typescript 生成 服务接口数据传输对象(即 DTOs)、API 控制器网关客户端库。最重要的是,它可以为所有生成的代码编写文档。

自动生成 API 网关的另一个优点是消除了开发和维护另一个 RESTfull API 的复杂性。因此,您可以将所有微服务聚合到一个 REST API 中,从而方便服务发现、文档和应用程序集成。使用此模式还可以获得其他好处:

  • 使客户端不受应用程序如何划分为微服务的影响
  • 使客户端不受确定服务实例位置问题的影响
  • 为每个客户端提供最佳 API
  • 减少请求/往返次数。例如,API 网关使客户端能够通过一次往返从多个服务检索数据。更少的请求也意味着更少的开销并改善用户体验。API 网关对于移动应用程序至关重要。
  • 通过将调用多个服务的逻辑从客户端移至 API 网关来简化客户端
  • 将“标准”公共 Web 友好型 API 协议转换为内部使用的任何协议

那么,让我们从一个例子开始。首先下载 cybtans cli 代码生成器,然后解压 zip 文件并将 .exe 所在的文件夹添加到您的路径中,以便于使用。然后使用 dotnet cli 或 Visual Studio 生成一个解决方案。例如:

dotnet new sln -n MySolution

现在,让我们生成一个用于管理产品目录的微服务项目结构。在命令提示符或 PowerShell 窗口中运行以下命令。例如:

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

此命令遵循约定,并在 Catalog 文件夹下生成多个项目。其中一些项目描述如下:

  • Catalog.Client: 包含微服务 C# 客户端库的 .NET Standard 项目
  • Catalog.Models: 包含微服务 DTO、请求和响应消息的 .NET Standard 项目
  • Catalog.RestApi: 包含微服务 Rest API 的 AspNetCore 项目
  • Catalog.Services: 包含微服务业务逻辑或服务的 .NET Core 项目
  • Catalog.Services.Tests: 微服务的集成测试
  • Proto: 微服务协议缓冲区定义

从 proto 文件生成 C# 代码

除了生成的项目,还创建了一个名为 cybtans.json 的 json 文件。该文件包含 cybtans-cli [解决方案文件夹] 命令的配置设置。这些设置指定了代码生成器使用的主要 proto 文件,如下所示:

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

现在让我们修改 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;
}

除了上面的消息,现在让我们定义一个服务和一些操作(即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";
    };
}

您可以通过运行以下命令生成 C# 代码。您需要提供 cybtans.json 所在的路径。该工具会在所有子目录中递归搜索此配置文件。

cybtans-cli .

消息默认在 Models 项目中生成,使用包名作为主命名空间,如下所示:

例如,CatalogItemDto 类的代码如下所示。您可能会注意到 CatalogItemDtoAccesor,这个类是为了提供额外的元数据而生成的,用于检查属性类型以及设置/获取属性值而无需使用反射。

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

Cybtans.Serialization 包利用 IReflectorMetadata 接口来加速对象序列化为 二进制 格式,这种格式比 JSON 更紧凑和高效。此格式用于服务间通信,例如 API 网关 - 微服务 通信。因此,Web 应用程序可以使用 JSON 消费网关的端点,而网关可以使用二进制格式与上游服务通信。

服务接口(即契约)默认在如下图所示的文件夹中生成。请注意代码是如何使用 proto 文件中的 description 选项进行文档化的。此 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);        
    }
}

Cybtans 代码生成器创建 API 控制器以暴露服务层。API 控制器默认在如下所示的文件夹中生成。您只需关心实现服务接口即可。

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

该工具可以使用 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);    
    }
}

使用 API 网关

为了添加 API 网关,让我们使用 dotnet cli 或 Visual Studio 创建一个 aspnet core 项目。您需要添加对 Catalog.ClientsCatalog.Models 项目的引用。然后按照如下所示,在 ConfigureServices 方法中注册 Catalog 客户端接口:

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

现在让我们修改 cybtans.json 以生成 Gateway 的控制器,如下所示:

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

运行 cybtans-cli .,代码将生成在指定的路径中,如下所示:

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

生成 Typescript 代码

此外,我们还可以使用 fetch apiAngular HttpClient 为 Typescript 生成服务和模型接口。为了生成 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"
        }
      ]
    }
  ]
}

在这个例子中,我们正在为两个 Web 应用程序生成 TypeScript 代码,一个用 TypeScript 编写的 React 应用程序,另一个是 Angular 应用程序。运行生成器后,生成的代码会存储在如下所示的文件夹中。

消息默认在 models.ts 文件中生成。Angular 和 React 的代码是相同的。

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

另一方面,React 和 Angular 的最终 services 是不同的,本例中 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));
    }
}

而 Angular 的 services 使用了 HttpClient,默认在 service.ts 中生成,如下图所示:

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

生成的服务类支持 FormData 用于多部分上传,并为下载的文件提供 Response 对象作为 blob。此外,您可以在 Angular 中使用拦截器来设置基本 URL 和身份验证令牌。另一方面,当使用 fetch API 时,您可以提供一个代理函数来设置身份验证令牌和附加头。

从 C# 类生成消息和服务

通常,在使用 Entity Framework 采用 Code First 方法时,您会使用类定义数据模型,并使用 EF 约定或 Fluent API 定义关系。您可以添加迁移来创建和应用数据库更改。

另一方面,您不会直接从服务中暴露数据模型。相反,您将数据模型映射到数据传输对象,即 dtos。通常,在 proto 文件中用消息定义所有 dtos 可能既繁琐又耗时。幸运的是,cybtans-cli 可以生成一个包含消息和常见的数据操作(如 readcreateupdatedelete)的 proto 文件。您所需要做的就是指定 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"
        }
      ]
    }
  ]
}  

类型为 message 的步骤定义了生成消息的 AssemblyFileProtoFile 定义了输出的 proto,而 Output 指定了生成通用服务实现的 微服务 文件夹。现在我们可以更改 Catalog.proto 文件,如下所示:

syntax = "proto3";

import "./Domain.proto";

package Catalog;

Catalog.proto 文件就像 cybtans-cli 的主入口点。您可以使用 import 语句在其他 proto 中包含定义。此外,您可以通过定义具有相同名称但具有额外字段或 rpcs 的消息或服务来扩展已导入 proto 中声明的消息或服务。

为了从程序集生成消息和服务,您需要将 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; }        
    }
}

关注点

值得注意的是,您可以看到 cybtans cli 如何缩短微服务解决方案的开发时间。同时,通过在微服务层面采用分层架构,提高了代码质量。此外,由服务表示的业务逻辑独立于底层基础设施(如 aspnetcore)和传输层(如 API 控制器)。此外,通过使用自动生成的 API 网关控制器,您可以更轻松地集成前端应用程序,并为前端开发人员提供有用的文档。

历史

  • 2020年10月4日:初始版本
© . All rights reserved.