Azure 最佳实践 - API设计与实现(1)

如今大多Web应用程序都有API,客户端可以使用这些API来与服务交互。设计良好的WebAPI应包含:


平台独立性。不管API的内部实现方式如何,任何客户端都应该能够调用该API。这就需要使用标准的协议并使用客户端和 Web服务一致的数据格式。
随服务演变。WebAPI应能在不影响客户端应用程序的情况下自我改进和添加功能。随着API的演化,现有客户端应用程序应能够继续运行而无需进行任何修改。所有功能应对客户端可见,并被充分使用。


本文阐述了在设计WebAPI时应考虑的问题。


REST简介
在2000年,RoyFielding提议使用表述性状态转移(REST)作为设计Web服务的设计方法。REST是基于超媒体构建分布式系统的架构样式。REST独立于任何基础协议,并且不一定绑定到HTTP。但是,最常见的REST实现使用HTTP协议协议,本文重点介绍如何设计使用HTTP的REST API。


基于HTTP的REST主要优势在于它使用的开放标准,并且客户端不会被绑定在API的实现。例如,可以使用ASP.NET编写RESTWeb服务,而客户端应用程序能够使用发送HTTP请求和接HTTP响应的任何语言或工具集。


下面是使用HTTP设计RESTfulAPI时的一些主要原则:
REST API 围绕资源设计,资源是客户端可访问的任何类型的对象、数据或服务。
每个资源有一个标识符,即唯一标识该资源的URI。例如,特定客户订单的 URI 可能是:


http://adventure-works.com/orders/1


客户端通过交换资源表达形式来与服务交互。许多WebAPI使用JSON作为交换格式。例如,对上面URI发出GET请求可能返回以下HTTP响应:


{"orderId":1,"orderValue":99.90,"productId":1,"quantity":1}


REST_API使用统一的接口,这有助于分离客户端和服务的实现。对于HTTP构建的REST_API,统一接口包括使用标准HTTP谓词对资源执行操作。最常见的操作是 GET、POST、PUT、PATCH和DELETE。


REST—API请求是无状态的。HTTP请求应是相互独立并可按任意顺序发生,因此保留请求间的状态信息并不可行。存储信息的唯一位置是在资源本身,并且每个请求应是原子操作。这个约束可让Web服务获得高度可伸缩性,因为可以解除客户端与服务器的耦合。任何服务器可以处理来自任何客户端的任何请求。而其他因素可能会限制可伸缩性。例如,许多Web服务向唯一的数据存储写入数据,因此可能难以横向扩展。(数据分区一文介绍了横向扩展数据存储的策略。)https://docs.microsoft.com/en-us/azure/architecture/best-practices/data-partitioning


REST—API由超链接驱动。例如,下面显示了某个订单的JSON表示形式。该表示形式包含了用于获取或更新与该订单关联的客户链接。


{
    "orderID":3,
    "productID":2,
    "quantity":4,
    "orderValue":16.60,
    "links": [
        {"rel":"product","href":"http://adventure-works.com/customers/3", "action":"GET" },
        {"rel":"product","href":"http://adventure-works.com/customers/3", "action":"PUT" } 
    ]
}

2008 年,Leonard Richardson提议对Web API 使用以下成熟度模型:
级别 0:定义URI,所有操作是对此URI发POST请求。
级别 1:为各个资源单独创建 URI。
级别 2:使用HTTP方法来定义对资源执行的操作。
级别 3:使用超媒体(HATEOAS,如下所述)。
根据Fielding的定义,级别3对应于某个真正的RESTfulAPI。在真实场景中,许多发布的Web API大致都处于级别2。


围绕资源来组织API
要专注于WebAPI所公开的业务实体。例如,在电子商务系统中,主实体是客户和订单。可以通过发送包含订单信息的HTTPPOST请求来创建订单。 HTTP响应中包含下单是否成功。 如果可能,资源URI应基于名词(资源)而不是动词(对资源执行的操作)。


http://adventure-works.com/orders // 推荐
http://adventure-works.com/create-order // 不推荐


资源不应与物理数据项耦合。例如,订单资源内部实现可以为关系数据库中的多个表,但以单实体的形式暴露给客户端。要避免创建只是镜像数据库内部结构的API。REST旨在为实体建模,并为应用程序可对这些实体执行的操作进行建模。不应把内部实现暴露给客户端。实体通常会被分组为实体集(如订单、客户)。集合不同于集合项的资源,应具有自身的URI。例如,以下URI可以表示订单集合:


http://adventure-works.com/orders


向集合URI发送HTTPGET请求可检索集合项列表。集合中的每个项也有自身的唯一URI。对项的URI发出HTTPGET请求会返回该项的详细信息。


在URI中采用一致的命名约定。一般而言,有效的做法是对集合的URI使用复数名词。最好是将集合和项的URI组织成层次结构。例如,/customers是客户集合的路径,/customers/5 是 ID 为5的客户的路径。这种方法有助于使WebAPI保持直观。此外,有许多Web API 框架基于URI参数进行路由,因此,可以对路径 /customers/{id}自定义路由。


还需要考虑不同类型资源之间的关系,以及如何将这些关系暴露在API。例如,/customers/5/orders可以表示客户5的所有订单。也要使用类似于/orders/99/customer的URI来表示从订单到客户的关联。但是,过度扩展可能会变得难以实现。更好的解决方案是在 HTTP响应消息的正文中提供指向关联资源的可导航链接。 稍后介绍的使用HATEOAS(https://docs.microsoft.com/en-us/azure/architecture/best-practices/api-design#using-the-hateoas-approach-to-enable-navigation-to-related-resources)进行资源导航详细介绍了此机制。


在更复杂的系统中,往往会提供URI(例如/customers/1/orders/99/products),使客户端能够使用多级关系进行导航。但是,如果资源之间的关系发生更改,多级关系的复杂性可能很难维护也不够灵活。相反,尽量让URI保持简单。应用程序获取对某个资源的引用后,再使用该引用查找与该资源相关的项目。例如可将前面的查询改为先用URI/customers/1/orders查找客户1的所有订单,再使用/orders/99/products以查找此订单中的产品。


提示
避免使用 集合/项/集合 的资源URI。
另一个因素是所有Web请求都会在Web服务器上负载。请求越多,负载越大。因此,尽量避免公开大量“琐碎”WebAPI。此类API可能需要客户端应用程序发送多次请求才能找到它所需的数据。因此建议将数据非规范化,并将相关信息合并为可使用单次请求来检索的实体集。但是,需要筛掉客户端并不需要的数据,读这种方法进行权衡。检索大型对象可能增大请求的延迟时间,并产生额外的带宽成本。 有关这些性能对立模式的详细信息,请参阅琐碎I/O(https://docs.microsoft.com/en-us/azure/architecture/antipatterns/chatty-io/index)和超量提取(https://docs.microsoft.com/en-us/azure/architecture/antipatterns/extraneous-fetching/index)。


避免WebAPI与底层数据源之间产生依赖。例如,如果数据存储在关系数据库中,则WebAPI不需要将每个表都公开为资源集合。事实上,这是一种非常粗略的设计。考虑将WebAPI看作是数据库的抽象层。如有必要,可在数据库与WebAPI之间引入映射层。这样,对于底层数据库方案所做的更改就不会影响到客户端应用程序。


最后,可能无法将WebAPI实现的每个操作都映射到特定资源。可对这种资源无关的请求直接处理,调用某个函数并将结果作为HTTP响应消息直接返回。例如,实现简单计算器操作(例如,加法和减法)的WebAPI可公开这些操作为伪资源的URI,并使用查询字符串来传所需的参数。例如,向URI/add?operand1=99&operand2=1发出GET请求会返回正文包含值100的响应消息。但是,尽量避免使用这种URI。


根据HTTP方法来定义操作
HTTP协议定义了大量为请求赋于语义的方法。大多数 RESTful Web API 使用的常见 HTTP 方法是:
GET 用于检索位于指定URI的资源表示形式。 响应消息的正文中包含所请求资源的详细信息。
POST 在指定的URI创建新资源。请求消息的正文提供新资源的详细信息。注意,POST可能触发异常创建资源的操作。
PUT 在指定的URI创建或替换资源。请求消息的正文指定要创建或更新的资源。
PATCH 对资源执行部分更新。请求正文指定需要完成的资源更改集合。
DELETE 删除位于指定URI处的资源。


请求的影响应取决于资源是集合还是单个项。下表汇总了电子商务示例中大多数 RESTful实现所采用的常见约定。 请注意,可能并非实现所有这些请求;这取决于特定方案。
资源              POST            GET                 PUT                                         DELETE
/customers      创建新客户            检索所有客户         批量更新客户                                 删除所有客户
/customers/1      错误            检索客户1的详细信息 如果客户1存在,则更新其详细信息             删除客户 1
/customers/1/orders  创建客户1的新订单 检索客户1的所有订单 批量更新客户1的订单                         删除客户 1 的所有订单


POST、PUT 和 PATCH 之间的差异可能会引起混淆。


POST请求创建资源。服务器为新资源分配URI,并将该URI返回给客户端。在REST模型中,我们经常向集合使用POST请求。新资源被添加到集合中。 有些情况下,POST请求将待处理数据提交到现有资源,不应创建任何新资源。
PUT请求创建或更新现有资源。客户端指定资源的URI。请求正文中包含资源的完整表示形式。如果已存在相同URI的资源,则替换该资源。否则创建新资源(如果服务器支持此操作)。PUT请求往往应用于单项资源(例如特定的客户)而不是集合。服务器可能支持通过PUT更新,但不支持通过PUT执行创建。是否支持通过PUT执行创建取决于在创建某个资源之前,客户端能否以有意义的方式向该资源分配URI。如果不能,则可使用POST来创建资源,并使用PUT或PATCH来执行更新。
PATCH请求对现有资源执行部分更新。客户端指定资源的URI。请求正文指定对资源更改部分的集合。这比使用PUT更高效,因为客户端只发送更改,而无需发送资源的全部表示形式。理论上,如果受服务器的支持,PATCH也可以创建新资源(对一个“null”资源指定一组更新)。
PUT请求必须是幂等的。如果客户端多次提交同一个PUT请求,结果应始终相同(使用相同的值修改相同的资源)。而无法保证POST和PATCH请求的幂等性。


WEB API要符合HTTP语义
这部分会阐述在设计符合HTTP规范的API时一些典型注意事项。但是,本部分不会介绍每种可能的细节或方案。如有疑问,请查阅HTTP规范。


媒体类型


如上文所述,客户端和服务器交换资源的表示形式。例如,在POST请求中,请求正文包含要创建的资源表示形式。在GET请求中,响应正文包含已提取的资源的表示形式。

在HTTP协议中,格式是媒体类型(也称为MIME类型)指定的。对于非二进制数据,大多数WebAPI支持JSON(application=application/json),可能还支持XML(application=application/xml)。

请求或响应中的Content-Type标头指定表示形式的格式。下面是包含JSON数据的POST请求示例:

POST http://adventure-works.com/orders HTTP/1.1
Content-Type: application/json; charset=utf-8
Content-Length: 57
{"Id":1,"Name":"Gizmo","Category":"Widgets","Price":1.99}
如果服务器不支持媒体类型,则应返回HTTP状态码415(不支持的媒体类型)。客户端请求可包含一个Accept标头,该标头包含客户端可以接受的、来自服务器响应消息中的媒体类型列表。例如:

GET http://adventure-works.com/orders/2 HTTP/1.1
Accept: application/json

如果服务器无法找到匹配的媒体类型,应返回HTTP状态代码406(不可接受)。

GET
GET请求成功通常会返回HTTP状态代码 200(正常)。 如果找不到资源,该方法应返回404(未找到)。


POST
如果POST方法创建了新资源,则会返回HTTP状态代码201(已创建)。新资源的URI包含在响应的Location标头中。响应正文中包含了资源的表示形式。如果该方法完成了处理但未创建新资源,则可以返回HTTP状态代码200并在响应正文中包含操作的结果。或者,如果没有可返回的结果,该方法可以返回HTTP状态代码204(无内容)但不包含任何响应正文。如果客户端将无效数据放入请求,服务器应返回HTTP状态代码400(错误请求)。响应正文中可以包含有关错误的其他信息,或包含可提供更多详细信息的URI链接。


PUT方法
与POST方法一样,如果PUT方法创建了新资源,则会返回HTTP状态码201(已创建)。如果该方法更新了现有资源,则会返回200(正常)或204(无内容)。在某些情况下,可能无法更新现有资源。在这种情况下,考虑返回HTTP状态代码409(冲突)。可以考虑对集合中的多个资源进行批量HTTPPUT操作。PUT请求应指定集合的URI,而请求正文则应指定要修改的资源详细信息。这样可帮助减少交互成本从而提高性能。


PATCH 方法
客户端可使用PATCH请求向现有资源发送一组修补文档形式的更新。服务器将处理补丁的实体以执行更新。补丁文档不会描述整个资源,而只描述一组更改。PATCH方法的规范(RFC5789)未定义补丁文档的特定格式。格式必须从请求的媒体类型来推断。


JSON也许是WebAPI的最常用数据格式。有两种基于JSON的补丁格式,分别称作“JSON补丁”和“JSON合并补丁”。JSON合并补丁更简单一些。补丁文档的结构与原JSON资源相同,但只包含要进行更改或添加的字段子集。此外,可通过在补丁文档中为字段值指定null来进行字段删除。(如果原始资源包含显式null值,并不适合使用合并修补的方式,否则导致被删除。)


例如,假设原资源的JSON表示形式为:


{ 
    "name":"gizmo",
    "category":"widgets",
    "color":"blue",
    "price":10
}

下面是合并补丁的JSON请求:


{ 
    "price":12,
    "color":null,
    "size":"small"
}




这段代码告知服务器要更新“price”,删除“color”并添加“size”。而“name”和“category”保持不变。有关JSON合并补丁的详细信息,请参阅RFC7396。JSON合并补丁的媒体类型是“application/merge-patch+json”。由于补丁文档中的null具有特殊的含义,因此如果原资源包含显式的null值,则不适合使用合并补丁。此外,修补文档不会指定服务器应用更新的顺序。而这项限制是否造成影响具体取决于具体数据和域。RFC6902中定义的JSON补丁格式更灵活。它以操作序列的形式指定要进行的更改。操作包括添加、删除、替换、复制和测试(以验证值)。JSON修补的媒体类型是“application/json-patch+json”。


下面是在处理PATCH请求时可能遇到的错误,以及相应的 HTTP 状态码。


错误状态                                        HTTP 状态代码
修补文档格式不支持。                             415(媒体类型不受支持)
修补文档的格式不正确。                         400(错误请求)
修补文档有效,但资源状态更改失败。                 409(冲突)


DELETE 
如果删除操作成功,Web服务器应返回HTTP状态代码204,指示已成功处理该请求,但响应正文不包含其他信息。如果资源不存在,Web 服务器可以返回404(未找到)。


异步
有时,POST、PUT、PATCH或DELETE操作可能需要经过一段时间的处理才能完成。如果需要等待该操作完成后才向客户端发送响应,可会造成超时。在这种情况下,考虑将该操作异步实现。先返回HTTP状态代码202(已接受),指示该请求已被接受并进行处理,但尚未完成。


应公开一个用于返回异步请求状态的终端,使客户端能够通过轮询来监视状态。在202响应的Location标头中包含状态的URI。例如:


HTTP/1.1 202 Accepted
Location: /api/status/12345


如果客户端向此终结点发送GET请求,响应中应包含该请求的当前状态。(可选)响应中还可以包含预计完成时间,或用于取消操作的URI。


HTTP/1.1 200 OK
Content-Type: application/json

{
    "status":"In progress",
    "link": { "rel":"cancel", "method":"delete", "href":"/api/status/12345"
}

如果异步操作创建了新资源,则该操作完成后,状态终结点应返回状态代码303(查看其他)。在303响应中,包含一个Location标头用于提供新资源的URI:


HTTP/1.1 303 See Other
Location: /api/orders/12345


有关详细信息,请参阅REST中的异步操作。


数据筛选和分页
通过单一URI来获取资源所有集合可能会导致应用程序在只需一部分信息时返回了大量不需要的数据。例如,客户端应用程序需要查找成本超过特定值的所有订单。可以从/orders的URI检索所有订单,然后在客户端再筛选。显然,这样做的效率非常低下。它浪费了托管WebAPI的服务器网络带宽和计算能力。


API应允许在URI的查询字符串中传递筛选器,例如/orders?minCost=n。然后WebAPI负责分析并处理查询字符串中的minCost参数并在服务器端只返回筛选后的结果。对集合资源执行GET请求可能返回大量数据。应将限制WebAPI返回的数据量。可在查询字符串指定要检索的最大项数以及在集合中的起始偏移量。 例如:
/orders?limit=25&offset=50


此外,考虑对返回的项数实施上限,以防止服务被恶意攻击。若要方便客户端应用程序,返回分页数据的GET请求还应包含某种形式的元数据,指示集合中可用资源总数。还可以考虑其他智能分页策略;有关详细信息,请参阅API设计说明:智能分页http://bizcoder.com/api-design-notes-smart-paging


可以提供一个字段作为排序参数(例如/orders?sort=ProductID),使用类似的策略对提取的数据排序。但是,此方法会对缓存产生负面影响,因为查询字符串参数构成了许多缓存数据键资源标识符的一部分。如果每个项包含大量数据,可以扩展此方法来控制每个项返回的字段。例如,可以逗号分隔的字段列表的方式来设计查询字符串参数,例如/orders?fields=ProductID,Quantity。为查询字符串中的所有可选参数提供有意义的默认值。例如,如果实现分页,将limit参数设为10,将offset参数设为0;如果实现排序,将排序参数设为资源键;如果支持映射,将fields参数设为资源中的所有字段。


支持大型二进制资源的部分响应
资源可能包含大二进制字段,例如文件或图像。要解决不可靠和间歇性连接问题并缩短响应时间,考虑对这类资源分块检索。为此,对于针对大型资源发出的GET请求,WebAPI应支持Accept-Ranges标头。此标头指示GET操作支持“部分获取”。客户端应用程序可以提交GET请求,用于返回所指定字节范围内的资源子集。


此外,考虑对这些资源实现HEAD请求。HEAD请求与GET请求类似,不过,前者只返回描述资源的HTTP标头和空消息正文。客户端应用程序可以发HEAD请求以确定是否要使用部分GET请求来获取某资源。 例如:


HEAD http://adventure-works.com/products/10?fields=productImage HTTP/1.1


响应消息的示例:
HTTP/1.1 200 OK


Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 4580


Content-Length标头指定资源的总大小,Accept-Ranges标头指示GET的数据范围。客户端应用程序可以使用这种方式来加载大图中的一部分。第一个请求使用Range标头提取前2500个字节:


GET http://adventure-works.com/products/10?fields=productImage HTTP/1.1
Range: bytes=0-2499


响应消息返回的HTTP状态代码206指示这是部分响应。Content-Length标头指定消息正文中返回的实际字节数(不是资源的大小),Content-Range标头指示这是该资源的哪一部分(第0到2499字节,总共4580个字节):


HTTP/1.1 206 Partial Content


Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 2500
Content-Range: bytes 0-2499/4580
[...]


客户端应用程序的后续请求可以检索资源的剩余部分。


使用HATEOAS完成资源导航
REST的主要动机之一是它应能够导航整个资源集,而无需事先了解URI的背后逻辑。每个GET请求通过响应中包含的超链接来查找与所请求的对象直接相关的资源信息,还应为其提供描述每个资源的操作信息。该原则称为HATEOAS或应用程序状态引擎的超文本。该系统实际上是有限状态机,每个请求的响应包含从一种状态转为另一种状态所需的信息;任何其他信息都不应是必需的。


备注
当前没有任何标准或规范定义如何为HATEOAS原则建模。本节中所示的示例说明了一个可能的解决方案。
例如,若要处理订单与客户之间的关系,可以在订单的表示形式中包含链接,指定下单客户可执行的操作。下面是一种可能的数据表示形式:
{
  "orderID":3,
  "productID":2,
  "quantity":4,
  "orderValue":16.60,
  "links":[
    {
      "rel":"customer",
      "href":"http://adventure-works.com/customers/3", 
      "action":"GET",
      "types":["text/xml","application/json"] 
    },
    {
      "rel":"customer",
      "href":"http://adventure-works.com/customers/3", 
      "action":"PUT",
      "types":["application/x-www-form-urlencoded"]
    },
    {
      "rel":"customer",
      "href":"http://adventure-works.com/customers/3",
      "action":"DELETE",
      "types":[]
    },
    {
      "rel":"self",
      "href":"http://adventure-works.com/orders/3", 
      "action":"GET",
      "types":["text/xml","application/json"]
    },
    {
      "rel":"self",
      "href":"http://adventure-works.com/orders/3", 
      "action":"PUT",
      "types":["application/x-www-form-urlencoded"]
    },
    {
      "rel":"self",
      "href":"http://adventure-works.com/orders/3", 
      "action":"DELETE",
      "types":[]
    }]
}
在上例中,links数组包含一组链接。每个链接表示对相关实体可执行的操作。每个链接的数据包含关系("customer")、URI (http://adventure-works.com/customers/3)、HTTP方法和支持的MIME类型。这是客户端应用程序操作所需的全部信息。links 数组还包含一些已检索资源本身的自引用信息。关系为self。返回的链接集可能会根据资源的状态发生更改。这就是为何将超文本称作“应用程序状态引擎”的原因。


对 RESTful Web API 进行版本控制
WebAPI一直保持静态的可能性很小。随着业务需求的变化,可能会添加新的资源集合,资源之间的关系可能会更改,并可能会修改资源中的数据结构。虽然更新WebAPI以处理新需求是一个相对简单的过程,但必须考虑此类更改对使用WebAPI的客户端应用程序所造成的影响。问题在于尽管设计和实现WebAPI的开发人员可以完全控制该API,但开发人员对客户端应用程序不具有相同程度的控制,因为这些客户端应用程序可能是由第三方组件生成的。主要原则是要让现有客户端应用程序能够持续地正常运行,同时允许新客户端应用程序能够使用新功能和新资源。版本控制使WebAPI可以指定它所公开的功能和资源,并且客户端应用程序可以对特定版本功能或资源进行请求。以下各节介绍了几种不同的方法,其中每种方法都有其自己的优势和不足。


无版本控制
这是最简单的方法,它对于一些内部API来说可能是可以接受的。较大的更改可以表示为新资源或新链接。向现有资源添加内容对客户端来说可能不是大更改,因为不应查询该内容的客户端会直接忽略这些改动。


例如,向URI http://adventure-works.com/customers/3发出的请求应包含客户端所需的id、name和address字段的客户信息:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8


{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}

备注
为简单起见,这部分所示的示例HTTP响应没有包含HATEOAS链接。
如果DateCreated字段已添加到客户端的资源结构中,响应将如下所示:


HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8


{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":"1 Microsoft Way Redmond WA 98053"}

现有的客户端程序可能会保持正常工作(如果能够忽略无法识别的字段),而新客户端应用程序则可处理新字段。但是如果对资源结构进行了更根本的更改(如删除或重命名字段)或资源之间的关系发生更改,则这些更改可能构成了重大更改,从而导致现有客户端应用程序无法正常运行。在这些情况下应考虑以下方法之一。


URI 版本控制
每次修改WebAPI或资源结构时,向每个资源的URI添加版本号。以前存在的URI应继续运行,并提供原始结构的资源。继续前面的示例,如果将address字段重构为包含地址多个构成部分的子字段(例如streetAddress、city、state和zipCode),则此版本的资源可通过包含版本号的URI(如 http://adventure-works.com/v2/customers/3)来公开:


HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8


{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":{"streetAddress":"1 Microsoft Way","city":"Redmond","state":"WA","zipCode":98053}}

这种版本控制机制非常简单,但需要将请求路由到相应终结点服务器。可随着WebAPI经过多次迭代而变得成熟,服务器必须能够支持多种不同版本,可能变得难以维护。此外,在所有版本下客户端应用程序都要提取相同数据(客户3),因此URI实在不应该因版本而有所不同。这也增加了HATEOAS实现的复杂性,因为所有链接都需要在其URI中包括版本号。


查询字符串版本控制
要在HTTP请求后面的查询字符串中指定资源版本参数而不是提供多个URI,例如http://adventure-works.com/customers/3?version=2。如果version参数在旧客户端应用中省略,则应有默认值(例如1)。这种方法具有语义优势(即同一资源始终从同一URI进行检索),但它依赖于代码处理请求以分析查询字符串并发送相应的HTTP响应。此方法与URI版本控制一样,增加了实现HATEOAS的复杂性。


备注
某些旧版本Web浏览器和Web代理不会缓存URI所包含查询字符串请求的响应。这可能会对使用了该WebAPI的Web应用程序性能产生不利影响。


标头(HEADER)版本控制
对资源标头进行版本控制,而不是使用查询参数来指定版本号。 这种方法需要客户端应用程序将相应标头添加到所有请求中,如果省略了版本标头,处理客户端请求的代码可以使用默认值(版本1)。下面的示例使用了名为Custom-Header的自定义标头。此标头的值指示Web API的版本。


版本 1:
GET http://adventure-works.com/customers/3 HTTP/1.1
Custom-Header: api-version=1
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8


{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}

版本 2:
GET http://adventure-works.com/customers/3 HTTP/1.1
Custom-Header: api-version=2
HTTP


HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":{"streetAddress":"1 Microsoft Way","city":"Redmond","state":"WA","zipCode":98053}}

注意,与前面两个方法一样,实现HATEOAS需要在链接中包含相应的自定义标头。


媒体类型(Media Type)版本控制
如本指南前面所述,当客户端应用程序向Web服务器发送GET请求时,应使用Accept标头规定它可以处理的内容的格式。通常,Accept标头的用途是让客户端应用程序指定响应的正文应是XML、JSON还是客户端可以处理的其他格式。 但是,可以定义包括以下信息的自定义媒体类型:该信息使客户端应用程序可以指定所需的资源版本。下面的示例演示了将Accept标头指定为application/vnd.adventure-works.v1+json。vnd.adventure-works.v1元素向Web服务器指定它应返回资源的版本1,而json元素则指定响应正文的格式应为JSON:


GET http://adventure-works.com/customers/3 HTTP/1.1
Accept: application/vnd.adventure-works.v1+json


服务器代码负责处理Accept标头并尽可能采用该值(客户端应用程序可以在Accept标头中指定多种格式,在这种情况下,Web服务器可以在其中选择最适合的格式用于响应正文)。Web服务器使用Content-Type标头来确认响应正文中的数据格式:


HTTP/1.1 200 OK
Content-Type: application/vnd.adventure-works.v1+json; charset=utf-8
{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}

如果 Accept 标头未指定任何媒体类型,则Web服务器可以返回HTTP406(不可接受)响应消息或返回默认媒体类型的消息。此方法可以说是最纯粹的版本控制机制并能够自然地适用于HATEOAS,后者可以在资源链接中包含相关数据的MIME类型。


备注
在选择版本控制策略时,还应考虑对性能的影响,尤其是在Web服务器上的缓存。URI版本控制和查询字符串版本控制都是缓存友好的,因为同一URI/查询字符串组合每次都会指向相同的数据。标头版本控制和媒体类型版本控制机制通常需要其他逻辑来检查自定义标头或Accept标头中的值。在大型环境中,使用不同版本的WebAPI的多个客户端可能会在服务器端缓存中产生大量重复数据。如果客户端应用使用代理与Web服务器进行通信,而代理有自己的缓存机制,并且代理没有在缓存中保留请求数据的副本,而只是将请求转发到Web服务器,则这个问题可能会变得很严重。


Open API 计划
OpenAPI 计划(https://www.openapis.org/)由一个行业协会创建,目的是标准化供应商的RESTAPI说明。作为该计划的一部分,Swagger2.0规范被重新命名为OpenAPI规范(OAS),并引入Open API计划。


建议为WebAPI采用OpenAPI。 考虑的要点:
OpenAPI规范附加了一组有关如何设计REST API的强制准则。这有益于互操作性,但在设计API时需多加注意,要符合规范。
OpenAPI 首推协议优先的方法,而不是实现优先的方法。协议优先意味着首先设计API协议(接口),然后实现协议。
Swagger之类的工具可以从API协议生成客户端库或文档。有关示例,请参阅有关使用Swagger的 ASP.NET Web API帮助页。
(https://docs.microsoft.com/en-us/aspnet/core/tutorials/web-api-help-pages-using-swagger?tabs=visual-studio)


详细信息
Microsoft REST API 准则(https://github.com/Microsoft/api-guidelines/blob/master/Guidelines.md)。设计公共REST API的详细建议。
REST指南(http://restcookbook.com/)。有关构建 RESTful API 的介绍。
Web API 核对清单(https://mathieu.fenniak.net/the-api-checklist/)。 设计和实现Web API时要考虑的因素。
开放API计划(https://www.openapis.org/)。有关开放式 API 的文档和实现的详细信息。

猜你喜欢

转载自blog.csdn.net/csharp25/article/details/80503157