使用Protobuf推动微服务和REST API的开发
介绍
自2011年首次使用微服务架构以来,微服务架构已成为当今的通用术语。从那时起,许多组织就采用了这种模式来推动全球规模的运营。最初设计该体系结构模式是为了解决面向服务的体系结构或SOA中的主要问题之一。因此,我们可以说微服务是SOA的一种专业化,旨在提供真正的服务独立性、数据主权和连续部署。
但是微服务架构提供的所有好处都需要付出代价。它增加了维护大量微服务中的一致性,服务发现、文档和监视的复杂性。
为了加强一致性,我们可以利用RabbitMQ之类的代理来发送和处理集成事件。另一方面,对于服务监视,我们可以使用Linkerd之类的服务网格。对于服务发现和文档记录,可以使用API网关将所有内部微服务聚合到一个REST API中。
因此,开发和维护微服务解决方案既耗时又乏味。因为除了专注于服务层的业务逻辑之外,您还需要通过Web API公开服务,开发API网关以及用于客户端应用程序的客户端库或SDK。
本文的目的是向您展示如何使用协议缓冲区语言(protocol buffer language)来生成代码并更快地构建微服务解决方案。使用这种方法,我们可以节省时间并专注于业务逻辑,而不必编写API网关和客户端库来与内部服务进行通信。
此外,您可以使用aspnet core集成测试来编写强大的代码,以测试与aspnet基础结构集成的业务逻辑,包括数据库支持和文件系统。
常见的微服务架构如下所示。
背景
协议缓冲(Protocol Buffer)是Google定义的与平台无关的语言,可以像Json一样以更有效的方式序列化结构化数据,但是更小、更快。您可以使用一种简单的语言定义数据的结构,然后可以使用生成的特殊源代码轻松地在各种数据流中写入和读取结构化数据。
除了定义数据结构的消息外,您还可以在带有.proto扩展名的文件中声明服务和rpcs(端点)。该Protobuf语言通常与GRPC一起使用。在Grpc中,您可以使用rpcs aka端点定义服务,此外,还可以生成用于不同语言的客户端库。但是Grpc有一个缺点,例如,很难从基于Web的应用程序中调用它。因此Grpc主要用于内部服务。
这就是设计基于CybtanSDK原型的生成器的原因,以提供最佳的Grpc和基于REST的服务。因此,Web应用程序可以使用JSON来使用服务,还可以使用更快的二进制序列化进行内部通信。
CybtansSDK入门
CybtansSDK是一个开源项目,可让您生成C#代码,以便使用.proto文件中定义的rpcs、消息、服务和使用AspNet Core开发微服务。
这个工具提供的主要优点是产生服务接口,数据传输对象又名DTO,API控制器,网关和客户端库为C#和Typescript。最重要的是,它可以为所有生成的代码编写文档。
具有自动生成的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:.NET Standard项目,带有用于C#的微服务的客户端库
- Catalog.Models:.NET Standard项目以及微服务的Dto、请求和响应消息
- Catalog.RestApi:具有微服务Rest API的AspNetCore项目
- Catalog.Services:带有微服务业务逻辑或服务的.NET Core项目
- Catalog.Services.Tests:微服务的集成测试
- Proto:微服务原型定义
从proto文件生成C#代码
连同生成的项目一起,创建了一个名称为cybtans.json的json文件 。该文件包含cybtans-cli [solution folder]命令的配置设置。这些设置指定了代码生成器使用的主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;
}
除了上面的消息,我们现在定义一个服务和一些操作(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";
};
}
您可以通过运行以下命令来生成csharp代码。您需要提供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 Gateway——Microservice通信。因此,Web应用程序可以使用JSON来使用网关的端点,而网关可以使用二进制格式与上游服务进行通信。
默认情况下,在以下所示的文件夹中生成服务接口(也称为契约)。请注意,如何使用proto文件中的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 Gateway,让我们使用dotnet cli或Visual Studio创建一个aspnet core项目。您需要添加引用到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);
}
.....
}
}
现在,让我们修改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 api或Angular 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 代码,一个Web应用程序以typescript 的形式编写,另一个以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,并为下载的文件(作为Blob)提供Response对象。此外,您可以使用angular拦截器来设置基本URL和身份验证令牌。另一方面,在使用访存API时,您可以提供代理功能,用于将身份验证令牌设置为其他标头。
从C#类生成消息和服务
通常,在将实体框架与“代码优先”方法一起使用时,可以使用ef约定或Fluent API使用类和关系定义数据模型。您可以添加迁移以创建更改并将更改应用到数据库。
另一方面,您不会直接从服务中公开数据模型。而是将数据模型映射到数据传输对象(也称为dtos)。通常,用原始文件中的消息定义所有dto可能很繁琐且耗时。幸运的是,cybtans-cli能够生成包含消息和通用数据操作(read、create、update和delete)的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类型步骤定义了AssemblyFile从何处生成消息,ProtoFile定义了输出原型,而Output指定了用于生成通用服务实现的微服务文件夹。现在,我们可以更改Catalog.proto文件,如下所示:
syntax = "proto3";
import "./Domain.proto";
package Catalog;
该Catalog.proto文件就像是cybtans-cli主要的入口点。您可以使用该import语句在其他原型中包含定义。此外,您可以通过定义具有相同名称但带有其他字段或rpcs的消息或服务来扩展在导入的原型中声明的消息或服务。
为了从程序集中生成消息和服务,您需要将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怎样减少微服务解决方案的开发时间。同时,它通过在微服务级别使用分层架构来提高代码质量。此外,服务所代表的业务逻辑独立于诸如aspnet core之类的基础架构以及诸如API Controllers之类的传输层。此外,通过使用自动生成的API网关控制器,您可以轻松集成前端应用程序,并为前端开发人员提供有用的文档。