龙空技术网

使用 ASP.NET Core 构建 Web API:3 REST 指导和约束

启辰8 79

前言:

而今各位老铁们对“jquery3api”大体比较关注,同学们都想要分析一些“jquery3api”的相关内容。那么小编在网摘上收集了一些有关“jquery3api””的相关内容,希望大家能喜欢,小伙伴们一起来了解一下吧!

本章涵盖

回顾六个 REST 指导约束在核心 ASP.NET 中设置和配置 CORS 和缓存技术了解反向代理和 CDN 服务的作用使用用例示例按需实现代码采用具有 HATEOAS 功能的统一接口使用 Swagger/OpenAPI 添加 API 文档和版本控制

现在我们已经启动并运行了一个最小的 Web API 样板,我们准备回顾第 1 章中简要介绍的具象状态传输 (REST) 属性和约束,并了解如何在 ASP.NET Core 中实现它们。具体来说,我们将在现有的MyBGList项目中添加一些内置和第三方服务和中间件,以实现真正的RESTful状态。本章介绍了关注点分离、状态管理、缓存、幂等性、API 版本控制、超媒体作为应用程序状态引擎 (HATEOAS) 和跨域资源共享 (CORS) 等概念。请放心,我们不会过多地纠缠于理论。本章介绍如何在 Core 中将这些主题付诸实践 ASP.NET。

在本章结束时,您将能够通过解决此处讨论的概念和编程技术的一些总结练习来测试您的知识。主要目标是了解基于 REST 的 Web 应用程序和真正的 RESTful Web API 之间的区别,为接下来的事情铺平道路。

3.1 REST 引导约束

让我们从重新审视六个 REST 指导约束开始。为简单起见,我们将遵循第 1 章中使用的相同顺序。

3.1.1 客户端-服务器方法

为了强制实施此约束,我们必须将服务器的关注点与客户端的关注点分开。实际上,我们的网络API

只能响应客户端发起的请求,因此无法自行发出请求对各个客户端的位置、环境、技术、体系结构和底层实现没有约束或依赖,因此能够增长、发展、体验变化,甚至在一无所知的情况下从头开始重建

如果我们查看现有的 Web API 项目,我们可以看到这种方法基本上已经到位。控制器和最小 API 仅用于返回标准 HTTP 响应;他们不需要知道有关请求客户端的任何信息。但是,默认 ASP.NET 核心设置强制实施 HTTP 安全机制,该机制可能会限制某些客户端的位置和/或技术。这种机制是任何 Web 开发人员的基本主题。

跨源资源共享

CORS是2004年提出的一种基于HTTP标头的机制,允许VoiceXML浏览器进行安全的跨源数据请求。后来,它被万维网联盟(W3C)的WebApps工作组正式确定为工作草案,主要浏览器供应商参与其中,并在2009年左右开始实施它;然后该草案于 3 年 2014 月被接受为 W<>C 建议。在所有主要浏览器中实现的当前CORS规范包含在Web超文本技术工作组(WHATWG)的Fetch Living Standard中。

定义WHATWG是一个由对不断发展的HTML和相关技术感兴趣的人组成的社区。该工作组由苹果公司,Mozilla基金会和Opera Software的代表于2004年成立。

CORS 的目的是允许浏览器使用从脚本(如 XMLHttpRequest 和 Fetch API)发起的 HTTP 请求来访问资源,当这些资源位于托管脚本的域以外的域中时。在没有这种机制的情况下,浏览器会阻止这些“外部”请求,因为它们会破坏同源策略,该策略仅在托管脚本的页面和请求的资源的协议、端口(如果指定)和主机值相同时才允许它们。

提示有关同源策略的其他信息,请查看

为了理解这个概念,让我们使用当前的 Web API 方案创建一个实际示例。假设我们想使用我们之前发布到 mybglist-api.com 域的MyBGList Web API来提供使用JavaScript框架(如Angular)创建的棋盘游戏Web应用程序,该应用程序位于不同的域(例如 webapp.com)。客户端的实现依赖于以下假设:

浏览器客户端对加载 Angular 应用的 webapp.com/ index.xhtml 页面执行初始 HTTP GET 请求。webapp.com/index.xhtml 页返回包含 HTML 内容的 HTTP 200 - OK 响应,其中包括两个附加引用:一个指向 /app.js 文件的 <script> 元素,该文件包含 Web 应用程序的 Angular JavaScript 代码。指向 /style.css 文件的 <link> 元素,该文件包含 Web 应用的 UI 样式。检索时,/app.js 文件会自动启动 Angular 应用程序,该应用程序对 mybglist-api.com/BoardGames 端点执行 XMLHttpRequest,由我们的 MyBGList Web API 的 BoardGamesController 处理,以检索要显示的棋盘游戏列表。

图 3.1 显示了这些请求。

图3.1 同源和跨源客户端-服务器交互

如我们所见,第一个 HTTP 请求(webapp.com/index.xhtml 页面)指向 webapp.com 域。此初始请求定义。随后的两个请求(/app.js 和 /style.css)将发送到与第一个请求相同的源,因此它们是同源请求。但是最后一个 HTTP 请求(mybglist-api.com/BoardGames)指向不同的域,违反了同源策略。如果我们希望浏览器执行该调用,我们可以使用 CORS 指示它放宽策略并允许来自外部源的 HTTP 请求。

注意请务必了解,同源策略是由浏览器控制的安全机制,而不是由服务器控制的安全机制。换句话说,这不是服务器发送的 HTTP 响应错误,因为客户端未获得授权;它是客户端在完成 HTTP 请求、接收响应并检查随之返回的标头以确定要执行的操作后应用的块。从这个假设出发,我们可以很容易地理解,服务器发送的CORS设置具有告诉客户端要阻止什么和不阻止什么的功能。

所有现代浏览器都使用两种主要技术来检查 CORS 设置并应用它们:简单模式,它使用与获取访问的资源相同的 HTTP 请求/响应,以及预检模式,它涉及额外的 HTTP OPTIONS 请求。 下一节将简要说明这些方法之间的差异。

CORS实用指南

有关从服务器和客户端角度看 CORS 的更多信息,请查看 Monsur Hossain () 的 CORS in Action: Create and Consumption Cross-Native API

简单请求与印前检查请求

每当客户端脚本启动 HTTP 请求时,浏览器都会检查它是否满足一组特定的要求(例如使用标准 HTTP 方法、标头和内容),这些要求可用于将其定义为简单。如果满足所有这些要求,则正常处理 HTTP 请求。CORS 通过检查访问控制允许源标头并确保它符合发出调用的脚本的来源来处理。此方法是一个简单的请求。

注意定义简单请求的要求记录在 中。

如果 HTTP 请求不满足任何要求,浏览器会将其置于暂停状态,并在其之前自动发出抢占式 HTTP OPTIONS 请求。浏览器使用此预检请求来确定服务器的确切 CORS 功能并采取相应措施。

正如我们很容易理解的那样,简单请求和预检请求之间的重要区别在于,前者直接请求(并接收)我们想要获取的内容,而后者在发出请求之前请求服务器许可。

印前检查请求提供了额外的安全层,因为事先检查并应用 CORS 设置。预检请求使用三个标头使服务器了解后续请求的特征:

访问控制请求方法 - 请求的 HTTP 方法访问控制请求标头 - 将与请求一起发送的自定义标头列表源 - 启动调用的脚本的源

如果服务器配置为处理此类请求,则服务器将使用以下 HTTP 响应标头进行应答,以指示是否允许后续 HTTP 请求:

访问控制允许源 - 允许发出请求的源(如果允许任何源,则使用 * 通配符)。这与简单请求使用的标头相同。访问控制允许标头 - 允许的 HTTP 标头的逗号分隔列表。访问控制允许方法 - 允许的 HTTP 方法的逗号分隔列表。访问控制最长期限 - 此预检请求的结果可以缓存多长时间(以秒为单位)。

浏览器检查预检请求的响应值,以确定是否发出后续 HTTP 请求。

注意当使用客户端框架(如 Angular 或 ReactJS)实现 Web 应用程序时,软件开发人员通常不需要深入研究 CORS 的技术细节;浏览器透明地处理所有这些,并以适当的方式与服务器通信。但是,由于我们正在处理故事的服务器端部分,因此我们需要知道允许什么和不允许什么。

这是足够的理论。让我们看看如何在 Web API 项目中实现 CORS。

实施 CORS

在 ASP.NET Core中,可以通过专用服务设置CORS,这使我们有机会定义默认策略和/或各种命名策略。与往常一样,必须在 Program.cs 文件的服务容器中添加此类服务。

我们可以使用不同的策略来允许特定源、HTTP 标头(用于预检请求,如前所述)和/或方法的 CORS。如果我们想允许对所有源、标头和方法的跨源请求,我们可以使用以下设置添加服务:

builder.Services.AddCors(options =>    options.AddDefaultPolicy(cfg => {        cfg.AllowAnyOrigin();        cfg.AllowAnyHeader();        cfg.AllowAnyMethod();    }));

但是,以这种方式配置 CORS 意味着对所有将采用默认策略的端点禁用同源策略,从而带来不平凡的安全问题。因此,定义限制性更强的默认策略会更安全,保留命名策略的宽松设置。下面的代码展示了我们如何通过定义两个策略来实现该目标:

一个默认策略,它接受来自一组受限制的已知来源的每个 HTTP 标头和方法,我们可以在需要时安全地设置一个名为“AnyOrigin”的策略,它接受来自每个人的所有内容,我们可以根据具体情况将其用于我们希望为任何客户端(包括我们不知道的客户端)提供的一组有限的端点

以下是我们如何改进程序.cs文件中的代码片段:

builder.Services.AddCors(options => {    options.AddDefaultPolicy(cfg => {        cfg.WithOrigins(builder.Configuration["AllowedOrigins"]);        cfg.AllowAnyHeader();        cfg.AllowAnyMethod();    });    options.AddPolicy(name: "AnyOrigin",        cfg => {            cfg.AllowAnyOrigin();            cfg.AllowAnyHeader();            cfg.AllowAnyMethod();        });    });

我们传递给 WithOrigins() 方法的值将由服务器在 Access-Control-Allow-Origin 标头中返回,该标头向客户端指示哪些源应被视为有效。由于我们使用配置设置来定义源以允许默认策略,因此我们需要设置它们。打开 appSettings.json 文件,并添加新的“AllowedOrigins”项,如以下清单所示。

清单 3.1 appSettings.json 文件

{  "Logging": {    "LogLevel": {      "Default": "Information",      "Microsoft.AspNetCore": "Warning"    }  },  "AllowedHosts": "*",  "AllowedOrigins": "*",  "UseDeveloperExceptionPage": false}

在此示例中,我们使用文本值“*”,该值可用作通配符,以允许任何源在请求没有凭据时访问资源。如果请求设置为允许 Cookie、授权标头或 TLS 客户端证书等凭据,则无法使用“*”通配符,并且会导致错误。

警告强制实施此类行为要求服务器和客户端确认可以在请求中包含凭据并指定特定源,以减少 CORS 中出现跨站点请求伪造 (CSRF) 漏洞的可能性。原因很简单:具有凭据的请求可能用于读取受限数据和/或执行数据更新,因此它们需要额外的安全措施。

现在我们已经定义了 CORS 策略,我们需要学习如何应用它们。

应用 CORS

ASP.NET 核心为我们提供了三种启用 CORS 的方法:

CORS 中间件终结点路由[启用Cors] 属性

CORS 中间件是最容易使用的技术,因为它将选定的 CORS 策略应用于应用的所有终结点(控制器、最小 API 等)。在我们的方案中,全局设置我们之前定义的默认策略可能很有用。通过在授权中间件之前将以下代码行添加到我们的 Program.cs 文件中来设置它:

// ... app.UseCors(); app.UseAuthorization(); // ...

我们必须将该代码放在那里是有原因的:中间件组件添加到程序.cs文件中的顺序定义了调用它们的顺序。如果我们希望将 CORS 中间件应用于控制器和最小 API 处理的端点,以及全局授权设置考虑在内,则需要在任何这些端点之前添加它。

注意有关中间件顺序和最佳实践的更多信息,请阅读 ASP.NET Core 官方文档的以下页面:

如果我们想将“AnyOrigin”命名策略应用于所有端点而不是默认策略,我们可以通过通过以下方式指定策略名称来添加 CORS 中间件:

// ... app.UseCors("AnyOrigin"); // ...

但是,这种方法毫无意义,因为当我们想要放宽所有源的同源策略时,该命名策略显然仅适用于某些边缘情况。对于我们的 BoardGamesController 的操作方法来说,情况绝对不是这样,除非我们可以让任何 Web 应用程序不受限制地使用它。但对于当前由最小 API 处理的 /error 和 /error/test 路由,此方法可能是可以接受的。让我们使用端点路由方法将“AnyOrigin”命名策略应用于它们:

// ... // Minimal APIapp.MapGet("/error", () => Results.Problem())    .RequireCors("AnyOrigin");app.MapGet("/error/test", () => { throw new Exception("test"); })    .RequireCors("AnyOrigin"); // ...

正如我们所看到的,端点路由允许我们使用 RequireCors() 扩展方法在每个端点的基础上启用 CORS。此方法适用于非默认命名策略,因为它使我们能够更好地控制选择支持它们的终结点。但是,如果我们想将其用于控制器而不是最小 API 方法,我们将没有相同级别的粒度,因为它只能全局应用(适用于所有控制器):

// ... app.MapControllers()    .RequireCors("AnyOrigin"); // ...

此外,使用 RequireCors() 扩展方法启用 CORS 目前不支持自动预检请求,原因如 中所述。

由于所有这些原因,终结点路由目前不是建议的方法。幸运的是,Core 允许的第三种也是最后一种技术提供了相同的粒度 ASP.NET [EnableCors] 属性,这也是Microsoft推荐的基于每个端点实现 CORS 的方法。以下是我们如何在最小 API 方法中实现它,替换之前基于 RequireCors() 扩展方法的端点路由技术:

// ... // Minimal APIapp.MapGet("/error", [EnableCors("AnyOrigin")] () =>    Results.Problem());app.MapGet("/error/test", [EnableCors("AnyOrigin")] () =>    { throw new Exception("test"); }); // ...

要使用此属性,我们还需要添加对 Microsoft.AspNetCore 的引用。程序.cs文件开头的 Cors 命名空间按以下方式排列:

using Microsoft.AspNetCore.Cors;

[EnableCors] 属性的一大优点是它可以分配给任何控制器和/或操作方法,从而使我们能够以简单有效的方式实现 CORS 命名策略。

注意在不指定命名策略作为参数的情况下使用 [EnableCors] 属性将应用默认策略,这在我们的方案中是多余的,因为我们已经使用 CORS 中间件在全局基础上添加了它。稍后,我们将在需要时使用此类属性使用命名策略覆盖默认策略。

3.1.2 无状态

无状态约束在 RESTful API 开发中尤为重要,因为它阻止了我们的 Web API 执行大多数 Web 应用程序执行的操作:将客户端的一些信息存储在服务器上,并在后续调用中检索它(使用会话 cookie 或类似技术)。更一般地说,实施无状态方法意味着我们必须限制自己使用方便的 ASP.NET Core功能(如会话状态和应用程序状态管理)以及负载平衡技术(如会话亲和性和粘性会话)。所有与会话相关的信息必须完全保留在客户端上,客户端负责在自己的一侧存储和处理它们。此功能通常由前端状态管理库(如 Redux、Akita、ngrx 和 Elf)处理。

在当前方案中,由于我们使用了没有身份验证支持的最小 ASP.NET 核心 Web API 模板,因此可以说我们已经合规。我们当前的项目没有启用会话状态所需的服务和中间件(主要通过内置的 Microsoft.AspNetCore.Session 命名空间提供)。这一事实也意味着,如果我们需要对特定调用进行身份验证和/或将某些端点限制为授权客户端,例如添加、更新或删除棋盘游戏或读取一些保留数据,我们将无法从这些方便的技术中受益。每当我们想做这些事情时,我们都必须向客户端提供所有必需的信息,以在其端创建和维护会话状态。我们将在第9章中学习如何做到这一点,其中介绍了JSON Web Token(JWT),基于令牌的身份验证和其他RESTful技术。

会话状态和应用程序统计信息e,以及服务器为识别与客户端相关的请求、交互和上下文信息而存储的任何数据,与资源状态或与服务器返回的响应相关的任何其他状态无关。这些类型的状态不仅在 RESTful API 中是允许的,而且还构成了必需的约束,如 next 部分所述。

3.1.3 可缓存性

术语缓存在与 IT 相关的上下文中使用时,是指旨在存储数据以使其以更少的工作量可用于进一步请求的系统、组件或模块。在处理基于 HTTP 的 Web 应用程序时,缓存通常用于将频繁访问(请求)的内容存储在请求-响应生命周期内的不同位置。在此上下文中,大多数可用的缓存技术和机制可以分为三个主要组:

服务器端缓存(也称为应用程序缓存)— 一种缓存系统,它使用内置服务或第三方提供程序(如 Memcached、Redis、Couchbase)、托管抽象层(如 Amazon ElastiCache)或标准数据库管理系统 (DBMS)将数据保存到键/值存储客户端缓存(也称为浏览器缓存或响应缓存)- HTTP 规范中定义的一种缓存机制,它依赖于多个 HTTP 响应标头(包括过期、缓存控制、上次修改和 ETag),可以从服务器设置这些标头来控制客户端的缓存行为中间缓存(也称为代理缓存、反向代理缓存或内容交付网络 [CDN] 缓存)— 一种响应缓存技术,它使用专用服务(代理)和/或依赖于针对加速和地理分布式内容分发(CDN 提供程序)优化的第三方服务来存储缓存数据,遵循与客户端相同的基于 HTTP 标头的规则缓存

在以下部分中,我们将使用一些方便的 ASP.NET Core 功能来设置和配置这些缓存方法。我们还将在第8章中探讨其中的大部分。

服务器端缓存

能够将频繁访问的数据存储在高性能存储(如系统内存)中传统上被认为是 Web 应用程序的有效实现技术,因为它允许我们保护数据提供程序(DBMS、网络资源或文件系统)免受大量并发请求的压力。但是,由于这些数据源中的大多数现在都有自己的缓存功能和机制,因此构建集中式应用程序级缓存的想法越来越没有吸引力,尤其是在处理高度分散的架构风格(如面向服务的体系结构 (SOA) 和微服务)时。一般来说,我们可以说使用不同服务提供的几种缓存方法是可取的,因为它允许我们微调每个数据源的缓存要求,通常会导致更好的整体性能。

也就是说,在几种情况下,服务器端缓存可能是一个可行的选择。由于我们计划将棋盘游戏数据存储在专用的 DBMS 中,因此我们可以考虑缓存一些常用的性能密集型数据库查询的结果,假设检索到的数据不会随时间而频繁更改。我们将在第 8 章中详细讨论该主题,其中介绍了一些基于 Microsoft.Extensions.Caching.Memory NuGet 包的数据库优化策略;在此之前,我们将重点介绍客户端缓存。

客户端缓存

与主要用于存储从后端服务和 DBMS 查询检索的数据的服务器端缓存不同,客户端缓存侧重于 Web 应用程序提供的内容:HTML 页面、JSON 输出、JavaScript、CSS、图像和多媒体文件。这种内容通常称为静态,因为它通常使用标准文本或二进制文件存储在文件系统中。但是,此定义在形式上并不总是正确的,因为大多数HTML和JSON内容都是动态检索的,然后由服务器动态呈现,然后与HTML响应一起发送。我们的 Web API 发送的 JSON 数据就是这种情况。

客户端缓存的一个主要优点是,顾名思义,它不需要服务器端活动,因为它完全满足它所应用的 HTTP 请求。此行为可以在性能、延迟和带宽优化方面带来巨大的优势,并大大减少服务器端负载。这些好处在 HTTP/1.1 规范(RFC 2616 第 13 节 “HTTP 中的缓存”)中有明确说明:

HTTP/1.1 缓存的目标是消除在许多情况下发送请求的需要,并在许多其他情况下消除发送完整响应的需要。前者减少了许多操作所需的网络往返次数;[...]后者降低了网络带宽要求;[...]

由于所有这些原因,我们可以说客户端缓存是当我们使用任何基于 HTTP 的应用程序时要实现的最重要的缓存技术,尤其是在我们处理 RESTful 接口时。

响应缓存

正如我们已经知道的,响应缓存由 HTTP 标头控制,这些标头指定我们希望客户端(以及任何中间代理、CDN 或其他服务)如何缓存每个 HTTP 响应。最相关的是 Cache-Control,它可用于指定多个缓存指令,解释谁可以缓存响应(公共、私有、无缓存、无存储)、缓存持续时间(最大期限)、与过时相关的信息(最大过时、必须重新验证)和其他设置。理想情况下,请求/响应链上的所有缓存系统和服务都遵循此类指令。

ASP.NET Core 让我们有机会使用 [ResponseCache] 属性配置这些标头(及其指令),该属性可应用于任何控制器或最小 API 方法。可以使用以下属性配置 [响应缓存] 属性:

持续时间 - 确定 Cache-Control 标头的最大期限值,该标头控制缓存响应的持续时间(以秒为单位)。位置 - 确定谁可以缓存响应:“如果允许客户端和代理,则为”任意“,”专用“表示仅允许客户端,”无“表示禁用响应。 这些值分别在 Cache-Control 标头中设置公共、专用或无缓存指令。NoStore - 设置为 true 时,将缓存控制标头值设置为 no-store,从而禁用缓存。此配置通常用于错误页面,因为它们通常包含引发错误的特定请求的唯一信息,这些信息对缓存没有意义。

提示[ResponseCache] 属性是 Microsoft.AspNetCore.Mvc 命名空间的一部分。出于这个原因,我们需要在程序文件(对于最小 API)和/或我们想要使用它的控制器文件中添加引用.cs。

让我们看看如何在现有方法中实现此属性,从 Program.cs 文件中的最小 API 错误处理程序开始。由于我们正在处理错误响应消息,因此我们可以利用此机会使用 NoStore 属性来防止任何人通过以下方式缓存它们:

// ... app.MapGet("/error",    [EnableCors("AnyOrigin")]    [ResponseCache(NoStore = true)] () =>    Results.Problem());app.MapGet("/error/test",    [EnableCors("AnyOrigin")]    [ResponseCache(NoStore = true)] () =>    { throw new Exception("test"); }); // ...

我们可以对 BoardGamesController 使用不同的方法,因为它旨在返回我们可能想要缓存合理时间的棋盘游戏列表。以下是我们如何为该响应设置最长期限为 60 秒的公共缓存:

// ... [HttpGet(Name = "GetBoardGames")][ResponseCache(Location = ResponseCacheLocation.Any, Duration = 60)]public IEnumerable<BoardGame> Get() // ...

现在就够了。每当我们添加其他操作方法时,我们都会定义进一步的响应缓存规则。

中间缓存

从 Web 开发的角度来看,中间缓存类似于客户端缓存,因为这两种技术都按照 HTTP 响应标头中指定的规则将数据存储在服务器外部。出于这个原因,我们完成的实现将无缝地适用于它们,假设我们将 Location 属性设置为 Public,如前所述。

两种缓存方法之间的主要区别是在体系结构级别设置的。术语“中间”意味着缓存的资源存储在位于客户端和服务器之间的服务器(或多个)中,而不是存储在前者的本地驱动器上。此方法涉及三个重要概念:

每个缓存的资源都可用于为多个客户端提供服务。中间缓存也是共享缓存,因为来自不同对等方的多个 HTTP 请求可以发出相同的缓存响应。中间缓存服务器必须位于客户端和服务器之间,以便它可以通过提供缓存的响应(不调用服务器)或将其转发到服务器(并可能缓存它以用于进一步的请求)来应答每个传入调用。整个缓存机制对客户端不可见,以至于它们大多无法分辨响应是来自原始服务器还是来自缓存(除非所有者想要明确让他们知道)。

图 3.2 显示了中间缓存的工作原理以及它如何与客户端缓存“堆叠”。这些概念适用于代理、反向代理和 CDN 服务,无论它们在物理(或逻辑)上位于何处。

图 3.2 客户端缓存和中间缓存概览

从技术角度来看,我们可以看到中间缓存服务的工作方式类似于浏览器的本地缓存,位于请求和响应之间,并根据 HTTP 标头进行操作。但是,可扩展性性能的提高要高得多,尤其是当我们处理对相同内容的多个同时请求时。这些好处对于大多数 Web 应用程序至关重要,并且应该增加设置和配置专用服务器或服务的复杂性。

警告在实现中间缓存时,我们基本上是创建一些与 URL 相关的内容(HTML 页面、JSON 数据、二进制文件等)的“缓存副本”,无论服务器使用任何身份验证和授权逻辑在第一个请求/响应周期中呈现该内容,这些副本通常都可以访问。作为一般规则,中间缓存服务应用于仅缓存公开可用的内容和资源,将受限数据和个人数据留给私有缓存方法(或无缓存),以防止潜在的关键数据安全问题。

3.1.4 分层系统

此约束进一步采用了客户端-服务器方法已经强制执行的关注点分离原则,将其应用于各种服务器组件。RESTful 架构可以从服务分布式方法中受益匪浅,在这种方法中,多个(微)服务协同工作以创建可扩展的模块化系统。我们在具体场景中采用了这种模式,因为我们的棋盘游戏 API 是更广泛的 SOA 生态系统的一部分,至少有两个服务器端组件:Web API 本身,它处理传入的 HTTP 请求以及与第三方的整个数据交换过程,以及 DBMS,它安全地提供数据。理想情况下,Web API 和 DBMS 可以部署在同一 Web 场甚至不同服务器场中的不同服务器上。

从该设置开始,我们可以通过添加两个额外的分散式架构组件来进一步扩展分层系统概念:反向代理服务器,它将安装在我们控制的虚拟机中,以及由第三方提供商托管的 CDN 服务,如 Cloudflare。图 3.3 显示了包含新组件的更新架构 SOA 图。

图 3.3 使用 CDN 和反向代理更新的 MyBGList SOA

Web API 仍然扮演着同样的关键角色,但我们在 API 服务器和客户端之间增加了两个层,这肯定会提高我们系统在重负载下的整体性能和可用性:

反向代理将允许我们实现高效的中间缓存机制,并为几种负载平衡技术(例如边缘-源模式)铺平道路,以提高我们应用程序的水平可扩展性。CDN 服务将添加中间缓存层,降低大多数国家和地区的网络延迟,并通过减少服务器的带宽使用来节省成本。

我们将在第 12 章中积极实施此计划,以便在生产环境中部署应用。

3.1.5 按需代码

当 Roy Fielding 写关于 REST 的论文时,JavaScript 还处于早期阶段,AJAX 首字母缩略词不存在,XMLHttpRequest 模型对大多数开发人员来说是未知的。很难想象,在接下来的几年里,随着数千个JavaScript驱动的库(JQuery)和Web框架(Angular,React,Vue.js等)的出现,我们将见证一场真正的革命。

按需代码 (COD) 可选约束现在比以前更容易理解。简而言之,我们可以将其描述为 RESTful API 返回可执行代码(如 JavaScript)的能力,以便为客户端提供额外的功能。这正是当我们在HTML页面中使用<script>元素来检索包含Angular应用程序的组合JavaScript文件时发生的情况。下载并执行文件后,浏览器会加载一个应用程序,该应用程序可以呈现UI组件,与用户交互,甚至执行进一步的HTTP请求以获取其他内容(或代码)。

在我们给定的场景中,我们几乎没有机会遵守这个可选约束,因为我们主要希望返回 JSON 数据。但是,在搁置此主题之前,我将展示如何通过使用仅包含几行源代码的 ASP.NET 核心和最小 API 来实现 COD。

假设我们的开发团队被要求提供一个端点,该端点可用于检查调用客户端是否支持 JavaScript 并提供结果的视觉证明。这种情况是开发可以返回一些JavaScript COD的端点的绝佳机会。

我们将设置一个 /cod/test/ 路由,该路由将使用一些包含 <script> 元素的 HTML 进行响应,JavaScript 代码用于在成功时呈现视觉警报,并使用 <noscript> 标记在失败时呈现纯文本消息。下面是一个最小 API 方法,我们可以将其添加到我们的程序.cs文件中,就在我们添加的其他 MapGet 方法的正下方:

app.MapGet("/cod/test",    [EnableCors("AnyOrigin")]    [ResponseCache(NoStore = true)] () =>    Results.Text("<script>" +        "window.alert('Your client supports JavaScript!" +        "\\r\\n\\r\\n" +        $"Server time (UTC): {DateTime.UtcNow.ToString("o")}" +        "\\r\\n" +        "Client time (UTC): ' + new Date().toISOString());" +        "</script>" +        "<noscript>Your client does not support JavaScript</noscript>",        "text/html"));

如我们所见,我们还使用 [EnableCors] 和 [ResponseCache] 属性来设置一些 CORS 和缓存规则。我们希望启用 CORS(因为我们使用了“AnyOrigin”命名策略),但我们不希望客户端或代理缓存 HTTP 响应。

提示[ResponseCache] 属性需要在 Program.cs 文件的顶部添加以下引用:使用 Microsoft.AspNetCore.Mvc。

要测试我们所做的工作,请在调试模式下启动项目并导航到以下 URL,该 URL 对应于我们新添加的终结点:https://localhost:40443/cod/test。如果我们做得很好,如果我们的浏览器支持 JavaScript,我们应该得到如图 3.4 所示的“可视化”结果。

图 3.4 启用 JavaScript 的 COD 测试

我们发送给客户端的示例 JavaScript 代码呈现的警报窗口很好地演示了 COD,因为它同时显示了服务器计算的数据和客户端计算的数据。当我们在这里时,我们不妨也测试<noscript>的结果。按 Ctrl+Shift+I(或 F12)访问浏览器的开发人员控制台;然后单击齿轮图标以访问“设置”页面,并选中“禁用 JavaScript”复选框(图 3.5)。

图 3.5 在边缘Microsoft禁用 JavaScript

警告这些命令假设我们使用的是基于 Chromium 的浏览器,例如 Microsoft Edge 或 Google Chrome。不同的浏览器/引擎需要不同的方法。例如,在 Mozilla Firefox 中,我们可以通过在搜索栏中键入 about:config 来关闭 JavaScript,接受免责声明,并将 javascript.enabled 切换值从 true 更改为 false。

之后,在不关闭“设置”窗口的情况下,按 F5 重新加载 /cod/test/ 终结点。因为 JavaScript 现在被禁用(暂时),我们应该看到图 3.6 所示的负面结果。

图 3.6 禁用 JavaScript 的 COD 测试

3.1.6 统一接口

我留到最后的 RESTful 约束也是通常需要最长才能正确实现的约束,也可以说是最难理解的。毫不奇怪,Roy Fielding在他的论文“架构风格和基于网络的软件架构的设计”()的第5.1.5节中给出了统一接口的最佳定义,其中他说

将 REST 体系结构样式与其他基于网络的样式区分开来的核心特征是它强调组件之间的统一接口。通过将通用性的软件工程原理应用于组件接口,简化了整体系统架构,提高了交互的可见性。实现与它们提供的服务分离,这鼓励了独立可进化性。

此语句意味着 RESTful Web API 以及数据应为客户端提供检索相关对象所需的所有操作和资源,即使事先不知道它们。换句话说,API 不仅必须返回请求的数据,还必须让客户端知道它是如何工作的以及如何使用它来请求更多数据。但是我们如何才能让客户明白该怎么做呢?

采用统一的界面是我们可以做的。我们可以将统一接口视为标准化、结构化、机器友好的自我文档,包含在任何 HTTP 响应中,解释已检索的内容以及(最重要的是)下一步该做什么。

注意Fielding的声明和整个统一接口概念长期以来一直被许多Web开发人员误解和低估,他们更喜欢关注其他REST约束。这种行为在 2000 年代开始变得普遍,最终促使菲尔丁在他的个人博客 () 上的一篇著名的 7 帖子中修改了这个主题。

REST 规范不会强制开发人员采用特定的接口。但它们确实提供了四个指导原则,我们应该遵循这些原则来实现可行的统一接口方法:

资源标识 - 必须统一标识每个资源,例如使用通用资源标识符 (URI) 并由标准格式(例如 JSON)表示。通过制图表达进行操作 - 发送到客户端的表示应包含足够的信息来修改或删除资源,以及添加更多相同类型的资源(如果客户端有权这样做)。自描述性消息 - 发送到客户端的表示形式应包含处理接收的数据所需的所有信息。为此,我们可以通过使用 JSON(使用哪种 HTTP 方法、预期的 MIME 类型等)以及使用 HTTP 标头和元数据(用于缓存信息、字符集等)添加相关信息来做到这一点。HATEOAS - 客户端应该能够与应用程序进行交互,而无需任何特定知识,而不仅仅是对超媒体的一般理解。换句话说,通过表示进行的操纵只能通过标准的描述性链接来处理(和记录)。

为了更好地理解这些概念,让我们根据当前方案调整它们。我们的 BoardGamesController 当前返回以下 JSON 结构:

[  {    "id": <int>,    "name": <string>,    "year": <int>  }]

这种结构意味着每当我们的客户请求棋盘游戏列表时,我们的 Web API 都会使用他们正在寻找的对象进行响应。 。仅此而已。相反,假设我们要采用统一接口 REST 约束,它应该做的是返回这些对象以及一些描述性链接,以告知客户端如何更改这些资源并请求更多同类资源。

实施 HATEOAS

我们当前的 BoardGamesController 的 Get() 操作方法当前返回一个动态对象,其中包含使用 BoardGame 普通旧 CLR 对象 (POCO) 类(BoardGame.cs 文件)创建的对象数组。我们在第 2 章中做出了这个选择,使用方便的 C# 功能(匿名类型)通过我们的 ASP.NET Core Web API 快速有效地提供数据。但是,如果我们想采用统一的接口来返回数据和描述性链接,我们需要切换到更结构化的方法。

注意有关 C# 匿名类型的详细信息,请参阅 中的官方文档。

我们希望将当前匿名类型替换为基本数据传输对象 (DTO) 类,该类可以包含一个或多个泛型类型的记录,以及我们要提供给客户端的描述性链接。泛型类型是特定类型的占位符,只要声明和实例化该对象的实例,就可以定义该占位符。一个完美的例子是内置的 List<T> 类型,我们可以在 C# 中使用它来声明和实例化构造的类型列表,方法是在尖括号内指定特定的类型参数:

var intList = new List<int>();var stringList = new List<string>();var bgList = new List<BoardGame>();

此代码段使用单个类定义创建三个单独的类型安全对象,这要归功于该类接受泛型类型。这正是我们需要创建统一接口 DTO 的原因。

定义大多数 Web 开发人员应该熟悉 DTO。简而言之,DTO 是 POCO 类,可用于仅向请求客户端公开有关该对象的相关信息。基本思想是将响应数据与数据访问层返回的数据分离。我将在第 4 章中更多地讨论这个概念,我们将代码示例替换为由 SQL Server 和实体框架核心提供支持的实际数据提供程序。

在项目的根目录中创建新的 /DTO/ 文件夹。从现在开始,我们将把所有 DTO 放在该文件夹中。然后使用 Visual Studio 的解决方案资源管理器添加两个新文件:

LinkDTO.cs - 将承载描述性链接的类RestDTO.cs - 包含将发送到客户端的数据和链接的类

这两个类都有一个简单的结构。RestDTO 类是 <T> 泛型类型的容器,它将托管实际数据,LinkDTO 类将包含描述性链接。下面的清单显示了 LinkDTO 类的代码。

清单 3.2 LinkDTO.cs文件

namespace MyBGList.DTO{    public class LinkDTO    {        public LinkDTO(string href, string rel, string type)        {            Href = href;            Rel = rel;            Type = type;        }         public string Href { get; private set; }         public string Rel { get; private set; }         public string Type { get; private set; }    }}

清单 3.3 显示了 RestDTO 类的代码。

清单 3.3 RestDTO.cs文件

namespace MyBGList.DTO{    public class RestDTO<T>    {        public List<LinkDTO> Links { get; set; } = new List<LinkDTO>();         public T Data { get; set; } = default!;    }}

C 泛型类、方法和类型参数

有关 C# 泛型类、方法和类型参数的其他信息,请参阅以下官方文档:

现在我们有了这两个类,我们可以重构 BoardGamesController 的 Get() 操作方法来使用它们,替换我们现有的实现。我们希望使用 RestDTO 类更改现有的匿名类型,并使用 LinkDTO 类以结构化方式添加描述性链接。打开 BoardGamesController.cs 文件,并按以下方式执行这些更新:

public RestDTO<BoardGame[]> Get()           ❶{    return new RestDTO<BoardGame[]>()       ❷    {        Data = new BoardGame[] {            new BoardGame() {                Id = 1,                Name = "Axis & Allies",                Year = 1981            },            new BoardGame() {                Id = 2,                Name = "Citadels",                Year = 2000            },            new BoardGame() {                Id = 3,                Name = "Terraforming Mars",                Year = 2016            }        },        Links = new List<LinkDTO> {      ❸            new LinkDTO(                Url.Action(null, "BoardGames", null, Request.Scheme)!,                "self",                "GET"),        }    };}

更改返回值

使用 RestDTO 更改匿名类型

添加描述性链接

我们进行了三项主要更改:

将以前的 IEnumerable<BoardGame> 返回值替换为新的 RestDTO<BoardGame[]> 返回值,这就是我们现在要返回的值。将以前仅包含数据的匿名类型替换为包含数据和描述性链接的新 RestDTO 类型。添加了 HATEOAS 描述性链接。目前,我们支持与棋盘游戏相关的单个端点;因此,我们使用“自我”关系引用添加了它。

注意此 HATEOAS 实现要求我们手动将描述性链接添加到每个操作方法。我们这样做是为了简单起见,因为这是理解我们到目前为止所做工作的逻辑的好方法。但是,如果我们在所有控制器和操作方法中采用通用开发标准,则可以使用帮助程序或工厂方法以及一堆参数自动填充 RestDTO.Links 属性。

如果我们现在运行我们的项目并调用 /BoardGames/ 端点,我们将得到以下内容:

{    "data": [        {            "id": 1,            "name": "Axis & Allies",            "year": 1981        },        {            "id": 2,            "name": "Citadels",            "year": 2000        },        {            "id": 3,            "name": "Terraforming Mars",            "year": 2016        }    ],    "links": [        {            "href": ";,            "rel": "self",            "type": "GET"        }    ]}

我们的统一界面已启动并运行。从现在开始,我们需要坚持下去,根据需要对其进行改进和扩展,以便客户始终知道如何处理我们给定的数据。

注意为简单起见,我们不会重构现有的最小 API 方法来使用 RestDTO 类型,因为这些方法仅用于测试目的。一旦我们学会了如何正确处理实际错误,我们将从代码库中删除它们。

类还是记录?

我们可以使用相对较新的 C# 记录类型(在 C# 9 中引入),而不是使用标准 C# 类类型创建 DTO。这两种类类型之间的主要区别在于记录使用基于值的相等性,这意味着如果两个记录变量的所有字段值都相等,则认为它们相等。相反,仅当两个类变量具有相同的类型并引用相同的对象时,它们才被视为相等。

C# 记录类型可以很好地替代用于定义 DTO 的标准类类型,因为基于值的相等性对于处理表示 JSON 数据的对象实例非常有用。由于我们的方案中不需要此类功能,因此我们使用“旧”类类型创建了第一个 DTO。

提示有关 C# 记录类型的更多信息,我强烈建议查看以下Microsoft文档教程:

本节结束了我们对 REST 约束的了解。现在,我们已经掌握了在本书其余部分坚持这些最佳实践所需的所有知识。以下部分介绍了几个主题,这些主题与任何 REST 约束并不严格相关,但可用于提高 Web API 的整体可用性和可读性:

API 文档,旨在以人类可读的方式为开发人员和非开发人员公开我们的 API 的使用API 版本控制,保留各种版本的历史记录并执行重大更改,而不会中断与客户端的现有集成

如果正确实现,这两个功能可以大大提高 Web API 的有效性,因为它们直接影响开发和使用时间,从而提高客户满意度并降低总体成本。实现每个功能都需要一组不同的工作和任务,我将在以下各节中介绍。

3.2 接口文档

基于 Web 的产品往往发展迅速,不仅在开发阶段,而且在产品发布后的后续部署周期之间也是如此。持续(和不可避免的)改进是技术的一个经过验证的、内在的、几乎是本体论的特征,也是敏捷方法经常被用来处理它的主要原因。修复错误、添加新功能、引入安全要求以及处理性能和稳定性问题是任何 Web 项目都必须处理才能成功的任务。

Web API 也不例外。但是,与标准网站或服务不同,它们没有可以显示更改并让用户了解它们的用户界面。例如,如果WordPress博客在帖子中添加了评论部分,则访问者很有可能会看到新功能,了解其工作原理并立即开始使用它。如果我们对我们的 Web API 做同样的事情,也许添加一个带有一组端点的全新 CommentsController ,这不太可能发生。使用与我们的数据交互的客户端和服务的各种开发人员注意到新功能的可能性很低。如果我们对现有功能进行一些更改或改进,也会发生同样的情况。如果该博客选择允许访问者在发送评论后更改他们的评论,那么一个新的编辑按钮就足够了。但是,如果我们向控制器添加 Edit() 操作方法,除非我们找到一种方法让相关方了解这一事实,否则没有人会知道。这就是为什么我们应该找到一种方法来记录我们的API。

在瞬息万变的环境中,这项活动是有代价的。为我们的开发团队以及希望使用我们的API和/或与之集成的任何外部第三方维护和更新此文档可能会变得困难和昂贵,除非我们找到一种方法以最小的努力自动完成工作。这项任务正是OpenAPI(以前称为Swagger)可以做的。

3.2.1 开放接口简介

OpenAPI 规范是一种基于 JSON 的接口描述语言,用于记录、使用和可视化 RESTful Web 服务和 API。它被称为Swagger,直到2015年,它被Swagger团队捐赠给OpenAPI计划。OpenAPI 已经成为 RESTful API 中使用最多、最广泛认可的文档标准。

注意有关 Swagger 和 OpenAPI 的更多信息,请查看 Joshua S. Ponelat 和 Lukas L. Rosenstock () 的 Designing API with Swagger and OpenAPI。

OAS的主要目的是在服务器和客户端之间定义标准化的契约,使后者能够了解前者的工作原理,并在不访问源代码的情况下与其可用端点进行交互。此外,这样的“合约”旨在与语言无关且人类可读,允许机器和人类理解API应该做什么。从这个角度来看,OpenAPI 与本章前面讨论的统一接口和 HATEOAS 密切相关。由于它可用于多种用途,因此建议将其视为一个单独的主题。

3.2.2 ASP.NET 核心组件

.NET 框架提供了两种可用于任何 ASP.NET Core Web 应用程序的 OpenAPI 实现:Swashbuckle 和 NSwag。为了简单起见,我们将使用Swashbuckle,它随Visual Studio ASP.NET Core Web API模板一起提供,我们用于创建MyBGList项目。

在第 2 章中,当我们使用 Visual Studio 创建 Web API 项目时,我们选择保持选中“启用 OpenAPI 支持”复选框。这一选择使我们能够启动并运行 Swashbuckle 服务和中间件。通过打开 Program.cs 文件并查看以下代码行来检查它:

// ... builder.Services.AddSwaggerGen();    ❶ // ... if (app.Environment.IsDevelopment()){    app.UseSwagger();                ❷    app.UseSwaggerUI();              ❸}

招摇的 JSON 生成器服务

招摇中间件来服务生成的 JSON

用于启用用户界面的 Swagger 中间件

Swagger 生成器服务创建一个 swagger.json 文件,该文件使用 OpenAPI 规范描述我们的 Web API 中可用的所有端点。第一个中间件使用可配置的端点(默认值为 /swagger/v1/swagger.json)公开此类 JSON 文件,第二个中间件启用方便的用户界面,允许用户查看和浏览文档。要查看 swagger.json 文件,请在调试模式下启动应用程序并导航到 https://localhost:40443/swagger/v1/swagger.json。要访问 SwaggerUI(使用 JSON 文件作为文档源),请导航到 https://localhost:40443/swagger。

正如我们通过查看 UI 主页看到的那样,JSON 文件是使用 OAS 版本 3.0(根据 Swagger 生成器默认设置)创建的,并且理所当然地认为我们正在处理 API 的 1.0 版。大多数默认设置(包括为 swagger.json 文件提供服务的终结点的 URL)都可以使用服务和中间件选项进行更改。我们可以从 OAS 3.0 切换到 2.0,添加 swagger.json 文件,在索引.xhtml页面中包含自定义样式表文件,等等。

关于虚张声势的几句话

为了简单起见,我现在不会深入研究Swashbuckle的配置设置。我将在第11章中详细讨论它们。有关其他信息,我建议查看官方文档:

在我们当前的情况下,默认设置已经足够好了,至少目前是这样。通过查看图 3.7 可以看到,SwaggerUI 已经包含了自我们开始使用项目以来添加的所有端点(和返回类型),并且将自动为我们将在本书的其余部分添加的端点继续这样做。

图 3.7 MyBGList Web API 项目的 SwaggerUI 主页

SwaggerUI 已经包含了自我们开始使用项目以来添加的所有控制器和最小 API 端点,以及它们的 DTO 返回类型,并且将继续为我们将在本书的其余部分添加的所有内容执行此操作。

UI 显示的每个终结点和架构类型都可以展开;使用右侧句柄查看完整的文档信息。细节级别非常高,这就是为什么公开信息的中间件在程序.cs文件中设置在一个条件块内,使其仅在开发环境中可用。Visual Studio模板在这里表现得很保守,因为它不知道我们是否要公开共享此类信息。这样,我们将能够在整个开发阶段从 SwaggerUI 中受益,而不会在“鲁莽”部署的情况下共享我们的整个端点结构。我们暂时没有理由改变这种方便的行为,因为我们不打算很快发布我们的应用程序。Swashbuckle的所有好东西都已经设置好了,我们不需要做任何事情。

3.3 API 版本控制

如果我们再看一下 SwaggerUI,我们会注意到屏幕右上角的“选择定义”下拉列表(图 3.8)。

图 3.8 SwaggerUI 的 API 版本选择器

我们可以使用此列表在提供给 UI 的 Swagger 文档文件之间切换。因为我们只使用默认文件,该文件具有项目的名称,并且默认为版本1.0,所以此列表包含一个MyBGList v1条目。我们可以生成(和添加)多个 swagger.json 文件并将它们全部提供给 UI,以便我们能够同时处理和记录不同的版本。在我解释如何实现此结果之前,明智的做法是花几分钟时间解释什么是 API 版本控制,最重要的是,为什么我们应该考虑它。

3.3.1 了解版本控制

在软件开发中,对产品进行版本控制意味着为每个版本分配唯一的版本号。这是软件包以及中间件包和库的常见做法,因为它允许开发人员和用户跟踪产品的每个版本。将此概念应用于 Web 应用程序可能被认为是奇怪的,因为应用程序通常发布到单个静态 URI(主机名、域或文件夹),并且任何新版本都会覆盖以前的版本。

如前所述,Web 应用程序(包括 Web API)旨在在其整个生命周期中不断发展。每次我们应用更改并将其部署到生产中时,都存在正常工作的功能停止工作的风险。回归错误、接口修改、类型不匹配和其他向后不兼容问题可能会导致中断性更改。单词中断不是隐喻性的,因为与客户端以及使用 API 的任何第三方的现有系统集成很有可能会中断。

采用版本控制系统并将其应用于我们的 API 可以降低风险。我们可以在新位置发布新版本,而不是将更新直接应用于 API,强制所有客户端立即使用它,该位置可通过各种技术(例如 URI 或标头)访问,而无需删除或替换旧版本。因此,最新版本和旧版本同时在线,使客户可以选择立即采用新版本或坚持使用以前的版本,直到他们准备好接受更改。

这种情况听起来很棒,对吧?不幸的是,事情并没有那么简单。

3.3.2 我们真的应该使用版本吗?

API 版本控制可以帮助我们和我们的客户减轻中断性变更的影响,但它也增加了复杂性和成本。以下是最显着缺点的简要列表:

复杂性增加 — 采用版本控制策略的全部意义在于使同一 API 的多个版本同时在生产环境中保持在线。这种策略可能会导致可用端点的数量大幅增加,这意味着必须处理大量额外的源代码。与 DRY 的冲突——第 1 章介绍了不要重复自己 (DRY) 原则。如果我们想坚持这个原则,我们应该为代码的每个组件提供一个单一的、明确的和权威的表示。API 版本控制通常指向相反的方向,这称为“写入所有内容两次”或“每次写入 (WET”)。安全性和稳定性问题 - 添加功能并不是更新 Web API 的唯一原因。有时,我们被迫这样做是为了修复可能对我们的系统产生负面影响的错误、性能问题或安全漏洞,或者采用已知更稳定和安全的新标准。如果我们在保持旧版本可用的同时这样做,我们就为这些麻烦敞开了大门。在最坏的情况下,恶意第三方可能会使用我们多年前修复的错误来利用我们的 Web API,因为易受攻击的端点仍然可用。错误的心态——这个缺点比以前的缺点更微妙,但又是相关的。依赖 API 版本控制可能会影响我们思考和发展应用的方式。我们可能会试图以向后兼容的方式重构我们的系统,以便旧版本仍然能够使用它或与之兼容。这种做法不仅很容易影响我们的源代码方法,还会影响我们项目的其他架构层:数据库模式、设计模式、业务逻辑、数据检索策略、DTO 结构等。我们可以放弃采用新版本的第三方库,因为它的新界面与我们想要保持在线的旧版本的 API 不兼容,因为一些客户端仍在积极使用它 - 他们这样做是因为我们允许他们这样做。

罗伊·菲尔丁(Roy Fielding)在几个场合指出了这些缺点中的大多数和其他缺点。2013 年 3 月,在 Adobe Evolve 大会上的一次演讲中,他就如何在 RESTful Web 服务(图 9.<>)中使用一个词进行 API 版本控制提供了一些建议:don't。

Roy Fielding 对 API 版本控制的看法

罗伊·菲尔丁(Roy Fielding)在整个演讲中使用的46张幻灯片可以在 下载。一年后,他在InfoQ的一次长篇采访中对这个概念进行了更详细的解释,可以在 上找到。

图 3.9 API 版本控制建议

我并不是说 API 版本控制总是不好的做法。在某些情况下,失去向后兼容性的风险非常高,以至于这些缺点是可以接受的。假设我们需要对处理在线支付的 Web API 执行重大更新,该 API 被全球数百万网站、服务和用户积极使用。我们绝对不希望在没有给用户某种宽限期的情况下对现有界面进行重大更改。在这种情况下,API 版本控制可能会有所帮助(并节省大量资金)。

但是,该示例不适用于我们的MyBGList Web API。我们的 RESTful API 旨在不断发展,我们希望我们的客户接受同样的道路,因此我们不会给他们任何借口让他们陷入过去。出于这个原因,我将解释如何实现现有API的新版本,然后遵循不同的路径。

3.3.3 实现版本控制

假设我们的开发团队被要求为与棋盘游戏相关的新移动应用程序引入对现有 BoardGamesController 进行简单但具有开创性的更改,例如将 RestDTO 类型的 Data 属性重命名为 Items。从代码复杂性的角度来看,实现此要求是不费吹灰之力的;我们谈论的是一行更新的代码。但是一些网站使用当前的API,我们不想危及他们一直在努力实现的系统集成。在这种情况下,我们能做的最好的事情就是设置并发布新版本的 API,同时保持当前版本可用。

API 版本控制技术

REST 不提供任何 API 版本控制的规范、指南或最佳实践。最常见的方法依赖于以下技术:

URI 版本控制 - 为每个版本使用不同的网域或网址细分受众群,例如 api.example.com/v1/methodNameQueryString 版本控制 - URI 版本控制的变体,改用 GET 参数,例如 api.example.com/methodName?api-version=1.0路由版本控制 - URI 版本控制的另一种变体,使用不同的路由,例如 api.example.com/methodName-v1媒体类型版本控制 - 使用标准接受 HTTP 标头指示版本,例如接受:application/json;api-version=2.0标头版本控制 - 使用自定义 HTTP 标头指示版本,例如接受版本:2.0

基于分段的 URI 版本控制是迄今为止最常用的方法,它通过使用表示版本 ID 的唯一 URL 段(也称为路径段)来分隔各个版本。下面是从 PayPal API 中获取的示例:

警告这些 URL 不可公开访问,除非你拥有有效的访问令牌,否则不会加载;它们仅供参考。

这些 URL 取自 ,其中包含最新的 API 文档和各种服务的建议终结点。如我们所见,订单 API 当前使用版本 2,而目录产品 API(在撰写本文时)仍停留在版本 1。正如我之前所说,API 版本控制可以帮助我们优雅地处理重大更改,即使是在端点的基础上,因为我们选择保持在线的所有版本都可以同时使用,并且(或应该)保证工作。同时,复杂性的增加是显而易见的。我们将采用相同的基于段的 URI 版本控制方法来完成分配的任务。

格式和约定

多年来使用最广泛的版本控制格式是语义版本控制,也称为 SemVer,可以按以下方式总结:MAJOR。次要。补丁。最新的 SemVer 版本(毫不奇怪,它采用了自己的约定)是 2.0.0。必须根据以下规则更改这些数字:

主要 - 当我们进行向后不兼容的 API 更改时MINOR—当我们以向后兼容的方式添加功能时PATCH—当我们进行向后兼容的错误修复时

SemVer 规范还允许使用预发行标签、元数据和其他扩展。为简单起见,我们将坚持基础知识,这对于我们当前的需求来说绰绰有余。有关其他信息,请参阅 的官方规范。

ASP.NET 核心 API 版本控制

现在我们知道了我们想要做什么,我们终于可以进入实现部分了。但是因为我们不想拘泥于我们将在本书的其余部分实现的版本控制系统,我们将创建一个单独的项目并将所有内容放在那里。以下是我们需要做的来创建这样的克隆:

在 Visual Studio 的解决方案资源管理器中,右键单击 MyBGList 解决方案,然后从上下文菜单中选择“添加新项目>”。选择我们在第 2 章中使用的相同 ASP.NET 核心 Web API 模板,并使用相同的设置。为新项目指定一个独特的名称,例如MyBGList_ApiVersion。删除新项目的 /控制器/ 文件夹和根文件。将 MyBGList 项目的 /Controller/ 文件夹、/DTO/ 文件夹和根文件(我们到目前为止一直在使用的项目)复制到新项目中。

现在我们有了项目的干净副本,我们可以使用它来玩 API 版本控制,而不会弄乱其他代码库。

提示请放心,有几种替代方案 - 可以说更好 - 方法来做同样的事情。例如,如果我们使用版本控制系统(如 Git),我们可以创建现有项目的 ApiVersioning 分支,而不是添加新的分支。但是,由于本练习仅用于演示目的,因此我们希望同时访问两个代码库。有关进一步的参考,您可以在本书的第 2 章的 GitHub 存储库中找到MyBGList_ApiVersion项目。

设置版本控制服务

在 ASP.NET Core 中实现基于 SemVer 的 API 版本控制系统的最有效方法依赖于安装以下 NuGet 包:

Microsoft.AspNetCore.Mvc.VersioningMicrosoft.AspNetCore.Mvc.Versioning.ApiExplorer

若要安装它们,请右键单击 MyBGList 项目的根节点,然后从上下文菜单中选择“管理 NuGet 包”。然后使用搜索框查找这些包并安装它们。或者,打开 Visual Studio 的包管理器控制台,并键入以下命令:

PM> Install-Package Microsoft.AspNetCore.Mvc.Versioning -Version 5.0.0PM> Install-Package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer -➥ Version 5.0.0

如果你更喜欢使用 dotnet 命令行界面,下面是从项目的根文件夹发出的命令:

> dotnet add package Microsoft.AspNetCore.Mvc.Versioning --version 5.0.0> dotnet add package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer --➥ version 5.0.0

注意在撰写本文时,5 年 0 月发布的 .NET 0 版本 2021.5.6 是这两个包的最新版本。此版本主要与 .NET <> 兼容,在最小 API 支持方面存在一些小缺陷(我们将在后面看到)。

Microsoft.AspNetCore.Mvc.Versioning 命名空间包含许多有用的功能,这些功能通过几行代码实现版本控制。这些功能包括 [ApiVersion] 属性,我们可以使用它为任何控制器或最小 API 方法分配一个或多个版本,以及 [MapToApiVersion] 属性,它允许我们对控制器的操作方法执行相同的操作。但是,在使用这些功能之前,我们需要设置几个必需的版本控制服务。此外,由于我们使用的是 OpenAPI,因此我们需要更改现有中间件的配置,以便 Swashbuckle 可以识别和使用版本化的路由。与往常一样,所有这些任务都需要在程序.cs文件中完成。打开该文件,并在 CORS 服务配置下添加以下行:

// ... builder.Services.AddApiVersioning(options => {    options.ApiVersionReader = new UrlSegmentApiVersionReader();  ❶    options.AssumeDefaultVersionWhenUnspecified = true;    options.DefaultApiVersion = new ApiVersion(1, 0);}); builder.Services.AddVersionedApiExplorer(options => {    options.GroupNameFormat = "'v'VVV";                           ❷    options.SubstituteApiVersionInUrl = true;                     ❸}); // ...

启用 URI 版本控制

设置 API 版本控制格式

将 {apiVersion} 占位符替换为版本号

新代码还需要以下命名空间引用,可以将其添加到文件顶部:

using Microsoft.AspNetCore.Mvc.Versioning;using Microsoft.OpenApi.Models;

我们添加的两个服务及其配置设置为 Web API 提供了设置 URI 版本控制所需的信息。简而言之,我们告诉服务我们要使用 URI 版本控制,定义要使用的版本控制格式,并将 {version:apiVersion} 占位符替换为实际版本号。这个方便的功能允许我们动态地为控制器和最小 API 方法设置“版本化”路由,正如我们稍后将看到的那样。

提示如果我们想使用替代版本控制技术,例如 QueryString 或 HTTP 标头,我们可以替换 UrlSegmentApiVersionReader 或将其与其他版本读取器之一组合:QueryStringApiVersionReader、HeaderApiVersionReader 和/或 MediaTypeApiVersionReader。

更新 Swagger 配置

现在,我们需要配置 Swagger 生成器服务,以便为我们要支持的每个版本创建一个 JSON 文档文件。假设我们需要版本 1.0(现有版本)和版本 2.0(新版本),我们希望将其配置为分别使用 /v1/ 和 /v2/ URL 片段:

builder.Services.AddSwaggerGen(options => {    options.SwaggerDoc(        "v1",        new OpenApiInfo { Title = "MyBGList", Version = "v1.0" });    options.SwaggerDoc(        "v2",        new OpenApiInfo { Title = "MyBGList", Version = "v2.0" });});

此代码需要以下命名空间引用:

using Microsoft.OpenApi.Models;

之后,我们需要确保SwaggerUI将加载swagger.json文件。向下滚动到 SwaggerUI 中间件,并添加以下配置设置:

app.UseSwaggerUI(options => {    options.SwaggerEndpoint(        $"/swagger/v1/swagger.json",        $"MyBGList v1");    options.SwaggerEndpoint(        $"/swagger/v2/swagger.json",        $"MyBGList v2");});

最后但并非最不重要的一点是,由于我们选择了 URI 版本控制,因此我们需要更改控制器和最小 API 的现有路由,以正确支持 /v1/ 和 /v2/ URL 片段。为此,我们可以利用版本控制服务提供的 {version:ApiVersion} 占位符,该占位符将替换为 HTTP 请求中使用的实际版本号,因为我们已在 Program.cs 文件中将 SubstituteApiVersionInUrl 选项设置为 true。

将版本控制添加到最小 API

让我们将版本控制计划应用于最小 API,因为它们位于 Program.cs 文件中。向下滚动到它们,并按以下方式更新现有代码:

app.MapGet("/v{version:ApiVersion}/error",    [ApiVersion("1.0")]    [ApiVersion("2.0")]    [EnableCors("AnyOrigin")]    [ResponseCache(NoStore = true)] () =>    Results.Problem()); app.MapGet("/v{version:ApiVersion}/error/test",    [ApiVersion("1.0")]    [ApiVersion("2.0")]    [EnableCors("AnyOrigin")]    [ResponseCache(NoStore = true)] () =>    { throw new Exception("test"); });app.MapGet("/v{version:ApiVersion}/cod/test",    [ApiVersion("1.0")]    [ApiVersion("2.0")]    [EnableCors("AnyOrigin")]    [ResponseCache(NoStore = true)] () =>    Results.Text("<script>" +        "window.alert('Your client supports JavaScript!" +        "\\r\\n\\r\\n" +        $"Server time (UTC): {DateTime.UtcNow.ToString("o")}" +        "\\r\\n" +        "Client time (UTC): ' + new Date().toISOString());" +        "</script>" +        "<noscript>Your client does not support JavaScript</noscript>",        "text/html"));

如我们所见,我们使用 [ApiVersion] 属性为每个方法分配一个或多个版本号。此外,我们正在使用 {version:ApiVersion} 占位符更改现有路由以预置版本号。占位符将替换为 HTTP 请求中包含的 URL 片段指定的实际版本号:/v1/ 或 /v2/,具体取决于客户端要使用的版本。

注意因为我们的任务分配不需要这些最小 API 方法在 1.0 和 2.0 版本中的行为不同,所以我们能做的最好的事情就是优化我们的源代码并使其尽可能干燥,就是将它们配置为处理两个版本。这样做将确保无论客户端使用哪个 URL 片段,都将执行所有这些方法,而无需我们复制它们。

文件夹和命名空间版本控制

现在我们已经将最小 API 方法适应了新版本的方法,我们可以切换到我们的 BoardGamesController。这一次,我们将被迫在一定程度上复制我们的源代码;我们的任务分配要求这样的控制器(及其操作方法)在版本 1 和版本 2 中的行为不同,因为它预计返回不同的 DTO。以下是我们需要做的:

在 /Controllers/ 根目录中,创建两个新的 /v1/ 和 /v2/ 文件夹。将 BoardGamesController.cs 文件移动到 /v1/ 文件夹,并将同一文件的其他副本放在 /v2/ 文件夹中。此操作会立即在 Visual Studio 中引发编译器错误,因为现在我们有一个重复的类名。若要修复此错误,请从 MyBGList 更改两个控制器的命名空间。控制器分别到 MyBGList.Controllers.v1 和 MyBGList.Controllers.v2。在 /DTO/ 根目录中,创建两个新的 /v1/ 和 /v2/ 文件夹。将 LinkDTO.cs 和 RestDTO.cs 文件移动到 /v1/ 文件夹,并将 RestDTO.cs 文件的其他副本放在 /v2/ 文件夹中。同样,此操作将引发编译器错误。若要修复此错误,请将现有命名空间替换为 MyBGList.DTO.v1 和 MyBGList.DTO.v2,并将 /v1/LinkDTO.cs 文件的命名空间从 MyBGList.DTO 更改为 MyBGList.DTO.v1。此操作会在两个 BoardGamesControllers.cs 文件中引发更多错误,因为这两个文件都具有对 MyBGList.DTO的现有使用引用:更改为 MyBGList.DTO.v1 对于 v1 控制器,更改为 MyBGList.DTO.v2 对于 v2。 v2 控制器将无法再找到 LinkDTO 类,因为我们没有为该类创建 v2 版本以保持我们的代码库尽可能干燥。通过添加显式引用并将 v2 控制器中的 LinkDTO 引用更改为 DTO.v1.LinkDTO 来修复此错误:

Links = new List<DTO.v1.LinkDTO> {   ❶    new DTO.v1.LinkDTO(              ❶        Url.Action(null, "BoardGames", null, Request.Scheme)!,        "self",        "GET"),}

添加对 v1 命名空间的显式引用

在所有这些文件复制和命名空间重命名任务之后,我们应该得到如图 3.10 所示的结构。

图3.10 MyBGList_ApiVersion项目结构

正如我们所看到的,我们创建了两个需要在版本 2 中更改类型的新“实例”:RestDTO,它包含我们被要求重命名的属性,以及 BoardGamesController,这是将为它提供服务的操作方法。现在,我们有了实施所需更改所需的一切。

更新 v2

让我们从 RestDTO.cs 文件开始。我们需要更改放入 /v2/ 文件夹中的实例,因为我们希望另一个实例保留其当前结构和行为。打开 /DTO/v2/RestDTO.cs 文件,并按以下方式将“数据”属性重命名为“项”:

public T Items { get; set; } = default!;

就是这样。现在我们终于可以更改 BoardGamesController 的版本 2 来处理新的属性(和路由)。打开 /Controllers/v2/BoardgamesController.cs 文件。从要求我们实现的重大更改开始,现在归结为更新一行代码:

// ... Items = new BoardGame[] { // ...

现在我们需要将此 BoardGamesController 显式分配给版本 2,并修改其路由以接受与该版本对应的路径段。这两个任务都可以使用 [路由] 和 [ApiVersion] 属性,如下所示:

// ...[Route("v{version:apiVersion}/[controller]")][ApiController][ApiVersion("2.0")]public class BoardGamesController : ControllerBase // ...

我们必须对 v1 控制器执行相同的操作,我们仍然需要将其分配给版本 1:

// ... [Route("v{version:apiVersion}/[controller]")][ApiController][ApiVersion("1.0")]public class BoardGamesController : ControllerBase // ...

正如我们所看到的,我们在这里也使用了 {version:apiVersion} 占位符。我们可以使用文字字符串,因为每个控制器都单声绑定到单个版本:

[Route("v1/[controller]")]

但是,尽可能使用占位符绝对是一种很好的做法,因为它允许我们配置控制器来处理多个版本,就像我们对最小 API 方法所做的那样。

测试 API 版本控制

现在我们终于可以测试到目前为止我们所做的工作了。在调试模式下运行MyBGList_ApiVersion项目,并在浏览器中键入 以访问 SwaggerUI。如果我们正确执行了所有操作,我们应该看到新的 /v1/BoardGame 端点,如图 3.11 所示。

图 3.11 使用 API 版本控制的 SwaggerUI

如果我们查看屏幕右上角的“选择定义”下拉列表,我们可以看到我们可以切换到 MyBGList v2,其中包含 /v2/BoardGames 端点。我们的 API 版本控制项目按预期工作 — 至少在大多数情况下是这样。

警告说实话,图片中缺少一些东西。如果我们将图 3.11 与我们之前非版本化的 MyBGList 项目的 SwaggerUI 主页进行比较,我们会发现下拉列表中缺少最小 API 方法,并且它们未列在 swagger.json 文件中。我们现在用于查找应用的所有可用终结点(我们安装的第二个 NuGet 包的一部分)的版本化 ApiExplorer 于 2021 年 6 月发布,在引入 .NET 6 和最小 API 之前,因此它无法检测到它们的存在。还记得我所说的缺少一些 .NET 6 支持吗?这就是我所说的陷阱。幸运的是,这个问题只影响Swashbuckle。对于两个版本的 API,我们的最小 API 方法仍将按预期工作;但是,它们不存在于 Swagger 文档文件中,也无法显示在 SwaggerUI 中。理想情况下,当 NuGet 包更新为完全支持所有 .NET <> 和最小 API 功能时,将修复此小缺点。

3.4 练习

在进入第 4 章之前,请花一些时间通过做一些总结练习来测试您对本章所涵盖主题的了解。每个练习都模拟产品所有者给出的任务分配,我们必须通过扮演MyBGList开发团队的角色来实现。我们将要处理的项目是MyBGList_ApiVersion,它具有最强大的功能,也是最完整的。

提示练习的解决方案可在 GitHub 的 /Chapter_03/Exercises/ 文件夹中找到。若要测试它们,请将MyBGList_ApiVersion项目中的相关文件替换为该文件夹中的文件,然后运行应用。

3.4.1 CORS

创建一个新的 CORS 策略,该策略仅接受使用 HTTP GET 方法(具有任何源和标头)执行的跨源请求。将其称为“AnyOrigin_GetOnly”,并将其分配给处理任何 API 版本的 <ApiVersion>/cod/test 路由的最小 API 方法。

3.4.2 客户端缓存

通过以下方式更新 BoardGamesController 的 Get() 方法(仅限 API 版本 2)的现有缓存规则:

确保响应将缓存控制 HTTP 标头设置为专用。将最长期限更改为 120 秒。

然后切换到 API v1 并通过将缓存控制 HTTP 标头设置为无存储来禁用缓存。

3.4.3 货到付款

创建一个新的 CodeOnDemandController(仅限版本 2),它没有构造函数,没有 ILogger 支持,并且有两个处理以下路由的操作方法:

/v2/CodeOnDemand/Test - 此终结点必须返回与当前处理 <apiVersion>/cod/test 路由的最小 API 方法相同的响应,并具有相同的 CORS 和缓存设置。使用 ContentResult 返回值作为操作方法和 Content() 方法返回响应文本。/v2/CodeOnDemand/Test2 - 此终结点必须返回与 Test() 操作方法相同的响应,并具有相同的 CORS 和缓存设置。此外,它需要接受整数类型的可选 addMinutes GET 参数。如果存在此参数,则必须在将其发送到客户端之前将其添加到服务器时间,以便 HTTP 请求可以更改脚本呈现的警报窗口中显示的服务器时间 (UTC) 值。

必须将这两种操作方法配置为仅接受 HTTP GET 方法。

3.4.4 API 文档和版本控制

根据以下要求将新版本(版本 3)添加到我们的 API:

不支持任何现有的最小 API 路由/终结点不支持任何 BoardGameController 路由/端点仅支持 /v3/CodeOnDemand/Test2 路由/终结点(请参阅前面的练习),但必须重命名 addMinutes GET 参数 minutesToAdd 而不影响版本 2

新版本还必须有自己的 swagger.json 文档文件,并且必须像其他版本一样显示在 SwaggerUI 中。

总结了解 REST 约束及其在整个项目生命周期(从整体架构到低级实现工作)中的影响是构建 HTTP API 的必要步骤,这些 API 可以有效地应对不断变化的实体(如 Web)带来的挑战。ASP.NET Core 可以极大地帮助实现大多数 RESTful 约束,因为它附带了几个内置功能、组件和工具,可以强制执行 REST 最佳实践和准则。CORS 是一种基于 HTTP 标头的机制,可用于允许服务器通过允许服务器从一个或多个外部(第三方)源加载资源来放宽浏览器安全设置。CORS 可以在 ASP.NET Core 中实现,这要归功于一组内置的服务和中间件,这允许我们使用属性来定义控制器和/或最小 API 方法的默认规则和自定义策略。响应缓存技术可以在性能、延迟和带宽优化方面为 Web API 带来实质性优势。响应缓存可以使用一组给定的 HTTP 标头来实现,这些标头可以与响应一起发送以影响客户端(浏览器缓存)以及代理服务器和/或 CDN 服务等中间方。HTTP 标头很容易通过使用 [ResponseCache] 属性在 Core ASP.NET 设置。采用统一的接口来标准化使用超媒体的Web API发送的数据可以帮助客户端了解它下一步可以做什么,在可进化性方面具有巨大的优势。尽管 REST 没有提供有关如何创建统一接口的具体指导,但 ASP.NET Core 可以通过实现响应 DTO 来处理此要求,该响应 DTO 不仅返回请求的数据,还返回一组结构化的链接和元数据,客户端可以使用这些链接和元数据来理解整体逻辑并可能请求更多数据。与大多数 Web 应用程序不同,Web API 没有用户界面,无法让用户了解他们可能会遇到的频繁更改。通过引入标准化描述语言(如 OpenAPI,以前称为 Swagger)解决了这一限制,读者和解析器可以获取这些语言,以人类可读的方式显示 API 文档。Swagger/OpenAPI可以通过使用Swashbuckle在 ASP.NET Core中实现,Swashbuckle是一个开源项目,提供一组服务和中间件来为Web API生成Swagger文档。API 版本控制是迭代 API 的不同版本并同时保持它们可用的过程。采用 API 版本控制过程可以为我们的项目带来一些稳定性和可靠性,因为它允许我们优雅地引入重大更改,而无需强迫每个客户端适应或停止工作。它还带有一些不平凡的缺点,在某些情况下可能超过好处。ASP.NET 核心通过两个Microsoft维护的 NuGet 包提供广泛的 API 版本控制功能,这些功能可帮助最大程度地降低此方法不可避免地带来的复杂性。

标签: #jquery3api