公共合同,如何确保其一致性

  • 您的系统是否包含许多互连的服务?
  • 更改公共API时还手动更新服务代码?
  • 服务的更改通常会破坏其他人的工作,而其他开发人员为此讨厌吗?

如果您至少回答了一次,那么欢迎您!

条款


公共合同,规范 -您可以通过其与服务进行交互的公共接口。在文本中,它们是同一意思。

这篇文章是关于什么


了解如何使用用于合同的统一描述和自动代码生成的工具来减少开发Web服务的时间。

正确使用下面介绍的技术和工具,可以使您快速推出新功能,而又不会破坏旧功能。

问题是什么样的


有一个包含多个服务的系统。这些服务分配给不同的团队。



消费者服务取决于服务提供商。
系统不断发展,有一天,服务提供商更改了其公共合同。



如果消费者服务尚未准备好进行更改,则系统将停止完全运行。



如何解决这个问题呢


供应商服务团队将解决所有问题


如果供应商团队拥有其他服务的主题区域并有权访问其git存储库,则可以完成此操作。这仅在依赖服务很少的小型项目中有效。这是最便宜的选择。如果可能,应该使用它。

向客户团队更新您的服务代码


为什么其他人坏了,但我们正在修理?

但是,主要问题是如何修复您的服务,合同现在看起来像什么?您需要学习新的提供商服务代码或联系他们的团队。我们花时间研究代码并与另一个团队进行交互。

考虑采取什么措施防止问题出现


从长远来看,最合理的选择。在下一节中考虑它。

如何预防问题的显现


软件开发的生命周期可以分为三个阶段:设计,实施和测试。

每个步骤都需要扩展如下:

  1. 在设计阶段以声明方式定义合同;
  2. 在实施过程中,我们根据合同生成服务器和客户端代码;
  3. 在测试时,我们检查合同并尝试考虑客户需求(CDC)。

作为我们问题的一个示例,将进一步解释每个步骤。

问题对我们的影响




这就是我们的生态系统。
圆圈是服务,箭头是它们之间的沟通渠道。

前端是基于Web的客户端应用程序。

大多数箭头指向存储服务。它存储文件。这是最重要的服务。毕竟,我们的产品是一个电子文档管理系统。

如果该服务更改了合同,系统将立即停止工作。



我们系统的源代码主要是用c#编写的,但是Go和Python中也有服务。在这种情况下,图中的其他服务做什么无关紧要。



每个服务都有其自己的客户端实现,用于与存储服务一起使用。更改合同时,必须手动更新每个项目中的代码。

我想从手动更新转向自动更新。这将有助于提高客户端代码更改的速度并减少错误。错误是URL中的错字,由于粗心而导致的错误等。

但是,这种方法不能解决客户端业务逻辑中的错误。您只能手动调整。

从问题到任务


在我们的情况下,需要实现客户端代码的自动生成。
为此,应考虑以下几点:

  • 服务器端-控制器已被写入;
  • 浏览器是该服务的客户端;
  • 服务通过HTTP进行通信。
  • 一代必须调整。例如,要支持JWT。

问题


在解决问题的过程中,出现了以下问题:

  • 选择哪种工具;
  • 如何获得合同;
  • 在哪里放置合同;
  • 客户代码放置在哪里;
  • 在什么时候做一代。

以下是这些问题的答案。

选择哪种工具


合同处理工具从两个方向提供-RPC和REST。



RPC可以理解为只是一个远程调用,而REST需要HTTP动词和URL的附加条件。

此处介绍了RPC和REST调用的区别。
RPC-远程过程调用REST Representational State Transfer
, HTTP- URL
Restaurant:8080/Orders/PlaceOrderPOSTRestaurant:8080/Orders
Restaurant:8080/Orders/GetOrder?OrderNumber=1GETRestaurant:8080/Orders/1
Restaurant:8080/Orders/UpdateOrderPUTRestaurant:8080/Orders/1


工具类


下表显示了使用REST和RPC的工具的比较。
物产OpenapiWSDL节约gRPC
一种休息Rpc
平台不依赖
不依赖
开发顺序*代码优先,规范优先代码优先,规范优先规格优先代码优先,规范优先
传输协议HTTP / 1.1任何(REST需要HTTP)拥有HTTP / 2
视图规格构架
评论低入门门槛,大量文档XML冗余,SOAP等入门门槛高,文档少平均入学门槛,文件更完善
首先编写代码 -首先我们编写服务器部分,然后再获取合同。服务器端已被写入时,这很方便。无需手动描述合同。

首先指定规格 -首先定义合同,然后从合同中获取客户端部分和服务器部分。在没有代码的情况下,在开发开始时很方便。 WSDL

输出

由于其冗余而不合适。

Apache Thrift太过奇特,很难学习。

GRPC需要net Core 3.0和net Standard 2.1。在分析时,使用的是net Core 2.2和net Standard 2.0。开箱即用的浏览器不提供GRPC支持,需要其他解决方案。GRPC使用Protobuf和HTTP / 2二进制序列化。因此,用于测试API(例如Postman等)的实用程序的范围正在缩小。通过某些JMeter进行负载测试可能需要额外的精力。不适合,切换到GRPC需要大量资源。

OpenAPI不需要其他更新。它吸引了众多支持使用REST和此规范的工具。我们选择它。

使用OpenAPI的工具


下表显示了使用OpenAPI的工具的比较。
工具类花花公子瓦格Openapi工具
支持的规范版本可以生成OpenApi v2,v3格式的规范
代码优先支持没有
支持的服务器语言没有C #许多
支持的客户语言没有C#,TypeScript,AngularJS,Angular(v2 +),window.fetch API许多
世代设定没有
视图Nuget包Nuget包+单独的实用程序单独的实用程序


Swashbuckle 的结论不合适,因为 仅允许您获取规格。要生成客户端代码,您需要使用其他解决方案。

OpenApiTools是一个有趣的工具,具有许多设置,但它首先不支持代码。它的优点是能够以多种语言生成服务器代码。

NSwag的方便之处在于它是一个Nuget软件包。建立项目时很容易连接。支持我们需要的一切:代码优先方法和c#中的客户端代码生成。我们选择它。

在哪里安排合同。如何获得合同服务


这是组织合同存储的解决方案。列出的解决方案按复杂度递增的顺序排列。

  • 提供程序服务项目文件夹是最简单的选项。如果您需要运行该方法,则选择它。
  • 如果所需项目在同一存储库中,则共享文件夹是有效选项。从长远来看,将很难维护文件夹中合同的完整性。这可能需要使用其他工具来解释合同的不同版本等。
  • 规范的单独存储库-如果项目位于不同的存储库中,则应将合同放置在公共场所。缺点与共享文件夹相同。
  • 通过服务API(swagger.ui,swaggerhub)进行处理-一种单独的服务,用于处理规范管理。

我们决定使用最简单的选项-将合同存储在服务提供商的项目文件夹中。在现阶段,这对我们来说已经足够了,那么为什么要付出更多呢?

在什么时候产生


现在,您需要决定在什么时候执行代码生成。
如果共享合同,则消费者服务可以接收合同并在必要时自行生成代码。

我们决定将合同放在服务提供商项目的文件夹中。这意味着可以在组装供应商服务项目本身之后完成生成。

放置客户端代码的位置


客户代码将通过合同生成。它仍然是找出放置它的位置。
将客户端代码放在单独的StorageServiceClientProxy项目中似乎是个好主意。每个项目都将能够连接该装配体。

该解决方案的优势:

  • 客户代码接近其服务,并且不断更新;
  • 消费者可以在一个存储库中使用指向项目的链接。

缺点:

  • 如果您需要在系统的另一部分(例如,不同的存储库)中生成客户端,则将无法使用。它至少使用一个共享合同文件夹来解决。
  • 消费者必须使用相同的语言编写。如果您需要其他语言的客户端,则需要使用OpenApiTools。

我们系上NSwag


控制器属性


需要告诉NSwag如何为我们的控制器生成正确的规范。

为此,您需要安排属性。

[Microsoft.AspNetCore.Mvc.Routing.Route("[controller]")]  //  url
[Microsoft.AspNetCore.Mvc.ApiController] //     
public class DescriptionController : ControllerBase {
[NSwag.Annotations.OpenApiOperation("GetDescription")] //    
[Microsoft.AspNetCore.Mvc.ProducesResponseType(typeof(ConversionDescription), 200)] //    200  
[Microsoft.AspNetCore.Mvc.ProducesResponseType(401)] //    401
[Microsoft.AspNetCore.Mvc.ProducesResponseType(403)] //    403
[Microsoft.AspNetCore.Mvc.HttpGet("{pluginName}/{binaryDataId}")] //  url
public ActionResult<ConversionDescription> GetDescription(string pluginName, Guid binaryDataId) { 
 // ... 
}

默认情况下,NSwag无法为MIME类型的application / octet-stream生成正确的规范。例如,在传输文件时可能会发生这种情况。要解决此问题,您需要编写属性和处理器以创建规范。

[Microsoft.AspNetCore.Mvc.Route("[controller]")]
[Microsoft.AspNetCore.Mvc.ApiController]
public class FileController : ControllerBase {
[NSwag.Annotations.OpenApiOperation("SaveFile")]
[Microsoft.AspNetCore.Mvc.ProducesResponseType(401)]
[Microsoft.AspNetCore.Mvc.ProducesResponseType(403)]
[Microsoft.AspNetCore.Mvc.HttpPost("{pluginName}/{binaryDataId}/{fileName}")]
[OurNamespace.FileUploadOperation] //  
public async Task SaveFile() { // ... }

用于生成文件操作规范的处理器


想法是您可以编写属性和处理器来处理此属性。

我们将属性挂在控制器上,当NSwag遇到该属性时,它将使用我们的处理器对其进行处理。

为了实现这一点,NSwag提供了OpenApiOperationProcessorAttribute和IOperationProcessor类。

在我们的项目中,我们成为继承人:

  • FileUploadOperationAttribute:OpenApiOperationProcessorAttribute
  • FileUploadOperationProcessor:IOperationProcessor

在此处阅读有关使用处理器的更多信息。

用于规范和代码生成的NSwag配置


在配置3个主要部分中:

  • 运行时-指定.net运行时。例如,NetCore22;
  • documentGenerator-描述如何生成规范;
  • codeGenerators-定义如何根据规范生成代码。

NSwag包含许多设置,一开始会令人困惑。

为了方便起见,您可以使用NSwag Studio。使用它,您可以实时查看各种设置如何影响代码生成或规范的结果。之后,在配置文件中手动选择选定的设置。

在此处阅读有关配置设置的更多信息

组装服务提供商的项目时,我们会生成规范和客户代码


为了在组装服务提供者项目之后,生成规范和代码,请执行以下操作:

  1. 我们为客户端创建了一个WebApi项目。
  2. 我们为Nswag CLI编写了一个配置-Nswag.json(在上一节中进行了介绍)。
  3. 我们在csproj服务提供商项目中编写了一个PostBuild Target。

<Target Name="GenerateWebApiProxyClient“ AfterTargets="PostBuildEvent">
<Exec Command="$(NSwagExe_Core22) run nswag.json”/>

  • $(NSwagExe_Core22)运行nswag.json-使用nswag.json配置在.bet runtine netCore 2.2下运行NSwag实用程序

目标执行以下操作:

  1. NSwag从供应商服务程序集生成规范。
  2. NSwag根据规范生成客户端代码。

在服务提供者项目的每次组装之后,将更新客户端项目。
客户和服务提供商的项目在同一解决方案内。
组装是解决方案的一部分。该解决方案配置为应在供应商的服务项目之后组装客户的项目。

NSwag还允许您通过软件API强制性地自定义规范/代码生成。

如何添加对JWT的支持


我们需要保护我们的服务免受未经授权的请求。为此,我们将使用JWT令牌。必须在每个HTTP请求的标头中发送它们,以便服务提供者可以检查它们并决定是否满足该请求。

关于JWT这里更多信息jwt.io

任务归结为需要修改传出HTTP请求的标头。
为此,NSwag代码生成器可以生成扩展点-CreateHttpRequestMessageAsync方法。在此方法内部,可以在发送HTTP请求之前对其进行访问。

代码示例
protected Task<HttpRequestMessage> CreateHttpRequestMessageAsync(CancellationToken cancellationToken) {
      var message = new HttpRequestMessage();

      if (!string.IsNullOrWhiteSpace(this.AuthorizationToken)) {
        message.Headers.Authorization =
          new System.Net.Http.Headers.AuthenticationHeaderValue(BearerScheme, this.AuthorizationToken);
      }

      return Task.FromResult(message);
    }


结论


我们选择了OpenAPI选项,因为 它易于实施,并且已高度开发了适用于此规范的工具。

关于OpenAPI和GRPC的结论:

OpenAPI

  • 该规范是冗长的;
  • , ;
  • ;
  • .

GRPC

  • , URL, HTTP ..;
  • OpenAPI;
  • ;
  • ;
  • HTTP/2.

因此,我们收到了基于控制器已编写代码的规范。为此,必须在控制器上悬挂特殊属性。

然后,根据收到的规范,我们实现了客户端代码的生成。现在,我们不需要手动更新客户端代码。

已经在版本和测试合同领域进行了研究。但是,由于缺乏资源,不可能在实践中测试整个过程。

版本化公共合同


为什么要对公共合同进行版本控制?


在更改服务提供商后,整个系统必须保持一致的可操作状态。

必须避免破坏公共API的更改,以免破坏客户端。

解决方案选项


不对公共合同进行版本控制


服务提供商团队本身会修复消费者服务。



如果服务提供商团队无法访问消费者服务存储库或缺乏能力,则此方法将行不通。如果没有此类问题,则无需版本控制。



使用公共合同的版本控制


服务提供商团队保留合同的先前版本。



这种方法没有上一个方法的缺点,但是增加了其他困难。
您需要确定以下内容:

  • 使用哪种工具;
  • 何时推出新版本;
  • 保留旧版本多长时间。

使用哪个工具


下表显示了与版本控制相关的OpeanAPI和GRPC的功能。
gRPCOpenapi
版本属性在protobuf级别,有一个属性包[packageName]。在规范级别,有basePath(用于URL)和Version属性
方法不推荐使用的属性是的,但是C#下的代码生成器未考虑在内是的,它被标记为过时
。代码不首先支持NSwag,您需要编写处理器
参数不推荐使用的属性标记为过时是的,它被标记为过时
。代码不首先支持NSwag,您需要编写处理器
弃用意味着该API不再值得使用。

两种工具都支持版本和不推荐使用的属性。

如果您使用OpenAPI和代码优先方法,则再次需要编写处理器来创建正确的规范。

何时推出新版本


当合同更改不保留向后兼容性时,必须引入新版本。

如何验证更改是否违反了新旧合同之间的兼容性?


版本维持多长时间


这个问题没有正确答案。

要删除对旧版本的支持,您需要知道谁在使用您的服务。

如果删除该版本,而其他人使用它,那将是不好的。如果您不控制客户,这尤其困难。

那么在这种情况下可以做什么?

  • 通知客户不再支持旧版本。在这种情况下,我们可能会失去客户收入;
  • 支持整个版本集。软件支持的成本在增长;
  • 要回答这个问题,您需要问业务-老客户的收入是否超过了支持旧版本软件的成本?要求客户升级会更有利可图吗?

在这种情况下,唯一的建议是要更加注意公共合同,以减少更改的频率。

如果在封闭系统中使用公共合同,则可以使用CDC方法。因此,我们可以找出客户何时停止使用旧版本的软件。在那之后,您可以删除旧版本的支持。

结论


仅当您无法使用版本控制时,才使用它。如果决定使用版本控制,则在设计合同时,请考虑版本兼容性。在支持较旧版本的成本与其提供的好处之间必须取得平衡。还值得确定何时可以停止支持旧版本。

测试合同和疾病预防控制中心


本节以浅色显示,例如 实施此方法没有严格的先决条件。

消费者驱动合同(CDC)


CDC是如何确保供应商和消费者使用相同合同的问题的答案。这些是旨在检查合同的某种集成测试。
这个想法如下:

  1. 消费者描述合同。
  2. 供应商在家中执行此合同。
  3. 此合同在消费者和供应商的CI流程中使用。如果违反了流程,则表示某人已停止遵守合同。

协议


PACT是实现此想法的工具。

  1. 使用者使用PACT库编写测试。
  2. 这些测试将转换为工件-pact文件。它包含有关合同的信息。
  3. 提供者和消费者使用契约文件来运行测试。

在客户端测试期间,将创建提供者存根,而在供应商测试期间,将创建客户端存根。这两个存根都使用pact文件。

可以通过Swagger Mock Validator bitbucket.org/atlassian/swagger-mock-validator/src/master实现类似的存根创建行为

关于契约的有用链接





如何将CDC嵌入到CI中


  • 自己部署Pact + Pact经纪人;
  • 购买现成的Pact Flow SaaS解决方案。

结论


需要签订协议以确保合同合规。它将显示合同变更何时违反了消费者服务的期望。

当供应商适应客户-客户时,此工具适用。这仅在隔离的系统内部可行。

如果您要为外界提供服务,而又不知道您的客户是谁,那么Pact不适合您。

All Articles