构建-ASP-NET-REST-Web-服务-全-
构建 ASP.NET REST Web 服务(全)
零、前言
本书将带领读者了解 RESTful web 服务的设计,并利用 ASP.NET Core 框架来实现这些服务。 从 REST 背后的基本哲学出发,读者将了解设计和实现企业级 RESTful web 服务的步骤。 采用实际的方法,每一章都提供了可以应用于您自己环境的代码示例。 它带来了最新的。net Core 版本的强大功能,使用 MVC。 然后,本文超越了框架的使用,探讨了解决弹性、安全性和可伸缩性问题的方法。 读者将学习在 Web api 中处理安全性的技术,并了解如何实现单元和集成测试策略。 最后,本书将带您为 RESTful web 服务构建一个. net 客户机,以及一些可伸缩技术。
这本书是给谁的
本书面向那些想要学习使用最新的。NET Core 框架构建 RESTful web 服务的人。 为了更好地利用书中包含的代码示例,你应该具备 c#和。net Core 的基本知识。
这本书的内容
第 1 章,入门将涵盖计划阶段,解释如何根据我们的需求或问题陈述来确定一个完美的技术堆栈,以及 RESTful 和 RESTless 服务的基本方面。
第二章,Build Initial Framework - Layout Foundation of Application,将让你熟悉各种方法的概念,如 get, POST, PUT, DELETE 等。
第三章,用户注册与管理,将让您熟悉 ASP 的认证.NET Core 2.0、实体框架核心、基本身份验证和 OAuth 2.0。
第四章,商品目录,购物车,收银台,将帮助您了解 ASP 的复杂组件.NET Core,包括。NET Standard 2.0,同时构建电子商务应用的不同部分。
第 5 章,集成外部组件和处理将帮助你理解中间件,使用中间件实现日志记录,认证和资源限制。
第六章,测试 RESTful Web 服务,将使您熟悉测试范例、测试概念、存根和 mock、安全测试和集成测试。
第七章、持续集成和持续部署将让您通过 VSTS 和 Azure 熟悉 CI 和 CD 概念。
第八章、安全 RESTful Web 服务将帮助您了解各种安全技术,包括基本身份验证、XSS 攻击、数据加密。
第 9 章、*伸缩 RESTful 服务(Web 服务的性能)*将解释向内、向外伸缩以及各种伸缩模式。
第 10 章、*构建 Web 客户端(消费 Web 服务)*将教给读者 ASP. net 的基本知识.NET Core 和 Rest 客户端与 RestSharp。
第 11 章,微服务概述,通过 ASP 覆盖微服务中的生态系统,对微服务进行了概述。 净的核心。
从这本书中得到最大的收获
读者应该具备。net Core 和。net Standard 的知识,以及 c#、RESTful Services、Visual Studio 2017(作为一个 IDE)、Postman、Advanced Rest Client 和 Swagger 的基本知识。
要设置系统,读者的机器上应该有以下内容:
- Visual Studio 2017 Update 3 或更高版本(下载和安装说明请参考https://docs.microsoft.com/en-us/visualstudio/install/install-visual-studio)
- SQL Server 2008 R2 或更高版本(下载和安装说明请参考https://blogs.msdn.microsoft.com/bethmassi/2011/02/18/step-by-step-installing-sql-server-management-studio-2008-express-after-visual-studio-2010/)
- net 2.0 核心
下载示例代码文件
您可以从您的帐户www.packtpub.com下载本书的示例代码文件。 如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,将文件直接通过电子邮件发送给您。
你可以按以下步骤下载代码文件:
- 登录或注册在www.packtpub.com。
- 选择 SUPPORT 选项卡。
- 点击代码下载和勘误表。
- 在搜索框中输入书名,并按照屏幕上的说明操作。
下载文件后,请确保使用最新版本的解压或解压缩文件夹:
- 解压缩的软件/ 7 - zip 窗口
- Zipeg / iZip UnRarX Mac
- 7 - zip / PeaZip Linux
该书的代码包也托管在 GitHub 上的https://github.com/PacktPublishing/Building-RESTful-Web-services-with-DOTNET-Core。 如果代码有更新,它将在现有的 GitHub 存储库中更新。
我们还可以在https://github.com/PacktPublishing/中找到丰富的图书和视频目录中的其他代码包。 检查出来!
下载彩色图像
我们还提供了一个 PDF 文件,其中包含了本书中使用的屏幕截图/图表的彩色图像。 您可以从https://www.packtpub.com/sites/default/files/downloads/BuildingRESTfulWebServiceswithDOTNETCore_ColorImages.pdf下载。
约定使用
本书中使用了许多文本约定。
CodeInText:表示文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 url、用户输入和 Twitter 句柄。 下面是一个示例:“TheHeader必须作为信封的第一个子元素出现在 body 元素之前。”
一段代码设置如下:
<?xml version = "1.0"?>
<SOAP-ENV:Envelope
xmlns:SOAP-ENV = "http://www.w3.org/2001/12/soap-envelope"
SOAP-ENV:encodingStyle = "http://www.w3.org/2001/12/soap-encoding">
...
SOAP Message information goes here
...
</SOAP-ENV:Envelope>
当我们希望提请您注意代码块的特定部分时,相关的行或项以粗体显示:
{
// GET: api/Products
[HttpGet]
public IEnumerable<Product> Get()
{
return new Product[]
{
new Product(1, "Oats", new decimal(3.07)),
new Product(2, "Toothpaste", new decimal(10.89)),
new Product(3, "Television", new decimal(500.90))
};
}
}
粗体:表示新词条、重要词汇或在屏幕上看到的词汇。 例如,菜单或对话框中的单词会像这样出现在文本中。 下面是一个例子:“简单对象访问协议(SOAP)是一种基于 xml 的消息传递协议,用于在计算机之间交换信息。”
Warnings or important notes appear like this. Tips and tricks appear like this.
取得联系
我们欢迎读者的反馈。
一般反馈:发邮件feedback@packtpub.com,并在邮件主题中提及书名。 如果您对本书的任何方面有任何疑问,请发送电子邮件至questions@packtpub.com。
Errata:尽管我们已尽一切努力确保内容的准确性,但错误还是会发生。 如果您在这本书中发现了错误,请向我们报告,我们将不胜感激。 请访问www.packtpub.com/submit-errata,选择您的图书,点击勘误表提交链接,并输入详细信息。
盗版:如果您在互联网上发现我们作品的任何形式的非法拷贝,请提供我们的位置地址或网址。 请通过copyright@packtpub.com与我们联系,并提供相关材料的链接。
如果你有兴趣成为一名作家:如果你有一个你擅长的主题,并且你有兴趣写作或写一本书,请访问authors.packtpub.com。
评论
请留下评论。 一旦你阅读和使用这本书,为什么不在你购买它的网站上留下评论? 潜在的读者可以看到并使用您的公正意见来做出购买决定,我们在 Packt 可以理解您对我们的产品的看法,我们的作者可以看到您对他们的书的反馈。 谢谢你!
有关 Packt 的更多信息,请访问packtpub.com。
一、开始
现代 web 开发要求与服务器进行交互,而不需要任何麻烦。 这意味着,随着不同 UI 和后端框架的发展,开发人员需要找到一种方法,在没有任何依赖关系的情况下与任何可用框架共享数据。 这意味着应该有一种方式来共享来自服务器的数据与客户端,而不管他们的语言和框架。 为了使共享数据具有一致性,首先想到的是.xml和.json。 每个框架都支持这些格式。
在本章中,我们将研究一种架构风格,通过这种风格,我们可以从使用任何语言和框架编写的任何程序中获取或发送数据。 通过我们将要讨论的架构 REST,我们可以引入一些方法,客户机可以很容易地使用这些方法进行数据操作。
本章将涵盖以下主题:
- 基于 rest 的服务
- 为什么我们要使用 RESTful 服务? RESTful 和 RESTful 服务的区别
- 客户机-服务器体系结构
- ASP.NET Core 和 RESTful 服务
讨论基于 rest 的服务
REST代表表征状态转移。 它是一种体系结构风格,定义了一组构建 web 服务的指导方针。
什么是建筑风格? 它只是一个带有预定义原则的概念。 我们稍后将讨论这些原则。 当您遵循 REST 时,您实际上是在应用中实现了作为 REST 构建块的原则。
然而,REST 的实现肯定会因开发人员的不同而不同。 没有固定的实现风格。 不要与架构模式混淆,它们不是概念,而是实际的实现。 MVC 是一种体系结构模式,因为它有一个固定的结构,它定义了组件之间如何相互作用,而它们不能以不同的方式实现。
下面是一个基于 rest 的服务的非常简单的图:

为了简化问题,考虑前面的图表,它向您展示了具有某些方法的服务,例如GET、POST、PUT和DELETE。 这就是这种风格的意义所在。 当您设计服务时,它将包含所有这些方法(其中包含预期的操作),我们可以将其视为基于 rest 的服务,或者称为 RESTful 服务。 更重要的是,服务可以从任何平台和语言构建的应用中调用,因为服务具有标准的体系结构。
如前所述,RESTful 服务是一种支持 REST 的服务。 让我们来讨论一下 REST 的特征,这样我们就可以理解对 RESTful 服务的期望。
其他特点
web 服务的主要构建块是客户机和服务器架构。 从服务器发送的响应实际上是对客户机请求的应答。 这就像您在问一个问题,如果服务器找到了答案,它就会做出响应。 来自服务器的响应实际上是某种格式或表示形式的资源。 常见的格式有.json、.xml、.pdf、.doc等。
休息是无状态的。 无状态表示系统的状态总是不同的。 因此,当一个请求到达服务器时,它被送达并被遗忘。 因此,下一个请求不依赖于前一个请求的状态。 每个请求都由服务器独立处理。
请求在 HTTP 连接中执行。 它们各自采用u****niform 资源标识符(URI)的形式。 这个标识符帮助我们在 web 服务器上定位所需的资源。
Roy Fielding 的博士论文《Architectural Styles and the Design of Network-Based Software Architectures》中定义了 REST。 以下是从他的研究中摘录的一些要点:
- 像许多分布式体系结构一样,REST 增加了层、无状态和缓存。
- REST 提高了效率、互操作性和整体性能。
- REST 通过遵循一组关于如何识别和操作资源的规则,以及通过元数据简化关于其操作的描述的过程,从而引入一致性,以便传递的消息将是自解释的。 我们将更多地讨论这种均匀性,它被称为u****niform 接口。
- 由于 REST 是一种体系结构风格,因此可以使用任何语言或平台开发服务,只要它支持 HTTP。
You can read the whole dissertation at https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm.
面向资源的架构
web 上的每个资源都有一个唯一的标识符,也就是 URI。 统一资源定位器(URL)是当今 web 上使用的最常见的 URI 类型。 URLhttps://www.packtpub.com/用于标识和定位数据包发布站点。
让我们快速地看一下该架构的图片。 在下面的图中,客户端试图通过标识符(URL)访问资源。 资源存在于服务器上,并且有一个可以在请求时返回给客户端的表示:

顾名思义,URL 只绑定到一个资源; 因此,如果我想将某人指向某个资源,我可以很容易地在电子邮件、聊天等中共享该标识符。
如果这些标识符以公司或资源名称命名,则很容易记住。 最好的例子是www.google.com,它很容易记住,因为它的名字是谷歌。 因此,我们可以通过口碑传播资源链接,您可以在几秒钟内将其输入到浏览器中,如 Chrome 或 Firefox。
您可能会在特定的网页上发现超链接,这些超链接链接到另一个网站以获取另一个资源。 这意味着,由于超链接,资源现在是相互连接的。
这些相互连接的资源形成了面向资源的架构。 通过使用目标资源 URI,超链接可以方便地从一个资源导航到另一个资源。
For example, in HTML, you link to another resource through the anchor element. The following is one anchor element that links to Packt's IoT book catalog page:
<a href="https://www.packtpub.com/tech/Internet%20of%20 Things">Packt IoT Books</a>
默认情况下,锚元素呈现为带下划线的文本。 当你将鼠标悬停在它上面时,你可以在底部看到附加的 URI,如下图所示:

您可以单击锚定文本(Packt IoT Books),然后它会触发一个针对目标资源 URI 的GET请求。
请注意,当您单击超链接时,您将进入一个实际上是资源表示的 web 页面。 最常见的表示形式是 HTML 格式。 其他一些常见的格式是(X)HTML、JPEG、GIF、WMV、SWF、RSS、ATOM、CSS、JavaScript/JSON 等等。 当浏览器接收到其中一种表示时,它会尝试解析它,如果解析成功,则会呈现它以供查看。
尤里
关于资源,我们已经谈了很多。 它们实际上是我们在一个特定的网站上看到的页面。 然而,HTTP 中的资源不仅仅是 HTML 网页形式的简单文件。 通常,资源被定义为任何可以由 URI 唯一标识的信息片段,例如http://packtpub.com/。
让我们先讨论一下 uri。 URI 由几个组件组成:URI 方案名称(如http或ftp)是第一部分,后面是冒号字符。 冒号之后是层次结构部分:
<scheme name> : <hierarchical part> [ ? <query> ] [ # <fragment> ]
让我们分析一个 URI:
https://www.flipkart.com/men/tshirts/pr?sid=2oq%2Cs9b%2Cj9y
让我们分解前面的 URI:
- 方案名称为
https。 - 方案名称后面是层次结构部分
//www.flipkart.com/men/tshirts/pr。 分层部分从//开始。 - 层次结构部分还包含一个可选查询,在本例中为
sid=2oq%2Cs9b%2Cj9y。
下面是一个包含可选片段部分的 URI 示例:
https://en.wikipedia.org/wiki/Packt#PacktLib
其他约束
REST 由六个约束定义,如下图所示。 其中一个是可选的:

每个约束都强制执行要遵循的服务的设计决策。 如果没有遵循它,则不能将该服务标记为 RESTful 服务。 让我们逐个讨论这些约束条件。
客户机-服务器体系结构
客户端或服务的消费者不应该担心服务器如何处理数据并将其存储在数据库中。 类似地,服务器不需要依赖客户机的实现,特别是 UI。
想想一个没有太多 UI 的物联网设备或传感器。 然而,它与服务器交互以使用 api 存储数据,这些 api 被编程为在特定事件上触发。 假设您正在使用一个物联网设备,当您的汽车没有汽油时,它会提醒您。 当物联网设备中的传感器检测到汽油短缺时,它调用已配置的 API,然后最终向所有者发送警报。
这意味着客户端和服务器不是一个实体,彼此可以独立存在。 它们可以独立设计和发展。 现在您可能会问,在不了解服务器架构的情况下,客户机如何工作**,反之亦然? 嗯,这就是这些约束的意义所在。 当与客户端进行交互时,服务提供了关于其性质的足够信息:如何使用它,以及可以使用它执行哪些操作。
随着本节的继续,您将意识到客户机和服务器之间绝对没有关系,如果它们完美地遵守所有这些约束,它们可以完全解耦。
无状态的
术语无状态意味着应用在特定时间内保持的状态可能不会持续到下一个时刻。 RESTful 服务不维护应用的状态,因此它是无状态的。
RESTful 服务中的请求不依赖于过去的请求。 服务独立地处理每个请求。 另一方面,当执行请求时,有状态服务需要记录应用的当前状态,以便它可以按照需要执行下一个请求。
此外,由于没有这些复杂性,无状态服务变得非常容易托管。 由于我们不需要担心应用的状态,它变得容易实现,维护也变得顺畅。
缓存
为了避免在每个请求中生成相同的数据,有一种称为缓存的技术用于在客户端或服务器端存储数据。 这些缓存的数据可以在需要的时候用作进一步的参考。
在使用缓存时,正确地管理它是很重要的。 原因很简单。 我们存储的数据不会被来自服务器的新数据所取代。 虽然这是提高服务性能的一个优势,但同时,如果我们不小心在其生命周期中缓存和配置什么,我们可能最终会看到过时的数据。 例如,假设我们在网站上显示黄金的实时价格,并缓存了这个数据。 下次价格变化时,除非我们使之前存储的缓存过期,否则不会反映出来。
让我们来看看不同种类的 HTTP 头以及如何配置缓存:
| 头部 | 应用 |
| 日期 | 生成表示的日期和时间。 |
| 最后修改 | 服务器最后修改此表示的日期和时间。 |
| cache - control | 用于控制缓存的 HTTP 1.1 报头。 在这张表之后,我们将更详细地研究这一点。 |
| 到期 | 此头帮助标记此表示的过期日期和时间。 |
| 年龄 | 表示从服务器获取表示后的时间(以秒为单位)。 |
前面 5 个头的配置取决于服务的性质。 以提供黄金价格的服务为例,理想情况下,它的缓存年龄限制应该尽可能低,或者甚至关闭缓存,因为用户在每次引用该站点时都应该看到最新的结果。
然而,一个包含许多图片的网站几乎不会改变或更新它们。 在这种情况下,可以配置缓存来更长时间地存储它们。
这些头值与缓存控制头一致,以检查缓存结果是否仍然有效。
以下是缓存控制头中最常见的值:
| 指令 | 应用 |
| 公共 | 这是默认指令。 这允许每个组件缓存表示。 |
| 私人 | 只有客户端或服务器可以缓存表示。 然而,中间组件受到限制。 |
| no - cache /没有商店 | 有了这个值,我们可以关闭缓存。 |
| 信息 | 这个值是日期和时间在date报头中提到之后的秒数,它表示表示的有效性。 |
| s-maxage | 它的作用与 max-age 相同,但只针对中间缓存。 |
| must-revalidate | 这就是说,如果超过了最大年龄,则必须重新验证表示。 |
| proxy-validate | 这与 max-revalidate 的作用相同,但只针对中间缓存。 |
随需应变代码(可选)
正如短语随需应变所建议的,服务可能会尝试在客户端上执行代码来扩展功能。 然而,这是可选的,并不是每个服务都这样做。
考虑一个 web 应用的例子,它调用票务服务来获取所有可用的票务。 该服务希望始终在警报中显示此信息。 为了做到这一点,服务可以连同数据一起返回一个 JavaScript 代码,该代码有一条带有可用票证数量的警告消息。 因此,一旦客户端接收到服务的响应,就会执行警报并显示数据。
统一的接口
当我们遇到接口这个词时,我们首先想到的是解耦。 我们创建了具有松散耦合体系结构的接口,在 RESTful 中可以看到相同类型的体系结构。
在实现 REST 时,我们使用相同的概念将客户机与 REST 服务的实现解耦。 然而,为了实现客户机和服务之间的这种解耦,需要定义每个 RESTful 服务支持的标准。
注意前面一行中的单词standard。 我们在世界上有这么多的服务,很明显,消费者的数量超过了服务的数量。 因此,我们在设计服务时必须遵循一些规则,因为每个客户都应该轻松地理解服务。
REST 由四个接口约束定义:
- 资源标识:URI 用于标识资源。 该资源是一个 web 文档。
- 通过表示操作资源:当客户端拥有给定的资源以及任何元数据时,他们应该有足够的信息来修改或删除资源。 因此,例如,
GET意味着您想要检索关于 uri 标识的资源的数据。 您可以使用HTTP方法和 URI 来描述操作。 - 自描述消息:传递的消息应该包含足够的数据信息,以便理解和处理进一步的操作。 MIME 类型用于此目的。
- 超媒体作为应用状态(HATEOAS)的引擎:从服务返回的表示应该包含所有未来的操作作为链接。 这就像访问一个网站,在其中你会发现不同的超链接为你提供不同类型的可用操作。
HTTP 1.1 提供了一组方法,称为动词。 在我们的服务中实现这些动词将标志着它们是标准化的。 重要的动词如下:
| 方法 | 服务器上执行的操作 | 方法类型 |
| GET | 读/检索资源。 | 安全 |
| PUT | 插入一个新资源,或者更新已经存在的资源。 | 幂等 |
| POST | 插入一个新的资源。 也可以用来更新现有资源。 | Nonidempotent |
| DELETE | 删除资源。 | 幂等 |
| OPTIONS | 获取资源允许的所有操作的列表。 | 安全 |
| HEAD | 只返回没有响应体的响应头。 | 安全 |
除了方法类型列之外,前面的表是不言自明的。 我来澄清一下。
在服务上执行 afe 操作不会对资源的原始值产生任何影响。 因为GET、OPTIONS和HEAD动词只检索或读取与资源相关的内容,而不进行更新,所以它们是安全的。
*幂等(可重复)*操作无论执行多少次,结果都是相同的。 例如,当您执行DELETE或PUT操作时,您实际上是在操作特定的资源,并且可以毫无问题地重复该操作。
POST versus PUT: This is a very common topic of discussion on the internet, and one that is very easy to understand. Both POST and PUT can be used to insert or update a resource. However, POST is nonidempotent, meaning that it isn't repeatable. The reason is that each time you call using POST, it will create a new resource if you don't provide the exact URI of the resource. The next time you use POST, it will again create a new resource. However, in PUT, it will first validate the existence of the resource. If it exists, it will update it; otherwise, it will create it.
更多的解释
在所有可用的方法中,GET是最常用的方法,因为它用于获取资源。
HEAD方法将只返回带有空主体的响应头。 这通常只在我们不需要资源的整个表示时才需要。
方法用于获取资源上允许或可用操作的列表。
考虑以下请求:
OPTIONS http://packtservice.com/Authors/1 HTTP/1.1 HOST: packtservice
如果请求被授权和验证,它可能会返回如下内容:
200 OK Allow: HEAD, GET, PUT
响应实际上是说,只能使用所有这些方法调用服务。
确保您根据它们的规范使用 HTTP 方法。 如果您将服务设计为允许GET,但在其中执行删除操作,那么客户机将会感到困惑。 当它们尝试GET某件事时,它实际上会删除资源,这很奇怪。
下面是一个使用GET发出的请求,但它实际上删除了服务器内部的资源(想象一下):
GET http://packtservice.com/DeleteAuthor/1 HTTP/1.1 HOST: packtservice
前面的请求可能会起作用并删除资源,但这并不被认为是 RESTful 设计。 推荐的操作是使用DELETE方法删除资源,如下所示:
DELETE http://packtservice.com/Authors/1 HTTP/1.1 HOST: packtservice
说明 POST 与 PUT
POST、PUT 的使用可以总结为以下两点:
PUT是幂等的——它可以重复,每次都产生相同的结果。 如果资源不存在,它将创建它; 否则,它将更新它。POST是非幂等的——如果它被多次调用,将创建多个资源。
上述这些动词之间的差别只是一般的差别。 然而,有一个非常重要和显著的区别。 当使用PUT时,需要指定资源的完整 URI。 否则,它不会工作。 例如,下面的代码将不起作用,因为它没有指定作者的确切 URI,这可以通过指定一个 ID 来完成:
PUT http://packtservice.com/Authors/
要解决这个问题,你可以使用如下方法发送一个带有这个 URI 的 ID:
PUT http://packtservice.com/Authors/19
created/updated.
这意味着将处理 ID 为19的作者,但是如果该 ID 不存在,将首先创建它。 带有此 URI 的后续请求将被视为修改 ID 为19的作者资源的请求。
另一方面,如果我们像下面这样对一个POST请求执行同样的操作,它将使用已发布的数据创建一个新的 author 资源:
POST http://packtservice.com/Authors/
有趣的是,如果您重复这一操作,您将对具有相同数据的重复记录负责。 这就是为什么它在自然界中是非幂等的原因。
注意以下带有 ID 的带有POST的请求。 与PUT不同,POST不会将此作为新资源,如果该资源不存在的话。 它将始终被视为一个更新请求:
POST http://packtservice.com/Authors/19
updated.
以下是本节的重点:
PUT创建或更新一个资源,只要调用相同的 URI- 如果资源已经存在,则
PUT和POST的行为相同 - 没有 ID 的
POST将在每次触发资源时创建一个资源
分层系统
大多数现代应用都是使用多层设计的,基于 rest 的服务也应该如此。 在分层系统中,每一层只能看到或了解层次中的下一层。
分层架构有助于提高代码的可读性,隐藏复杂性,并提高代码的可维护性。 假设您有一个层,从身份验证到数据库操作,所有事情都在其中进行。 绝对不建议这样做,因为主要组件(如身份验证、业务逻辑和数据库操作)没有被分离出来。
因此,这个约束是对 RESTful 服务的期望,而且没有客户机实际上可以说它已连接到最后一层。
RESTful 服务的优缺点
下面是 RESTful 服务的一些优缺点:
优势
使用 RESTful 服务的优点如下:
- 不依赖于平台或任何编程语言
- 通过 HTTP 的标准化方法
- 它不会在服务器上存储客户机的状态
- 支持缓存
- 可访问的任何类型的客户端,如移动,web,或桌面
缺点
虽然 RESTful 服务有优点,但也有缺点。让我们来看看 RESTful 服务的缺点:
- 如果不正确地遵循标准,客户就很难理解这些标准
- 由于没有提供这样的元数据,文档就会出现问题
- 如果没有遵循这样的过程来限制对资源的访问,那么安全性就是一个问题
ASP.NET Core 和 RESTful 服务
. net Core 被定义为一个跨平台、开源、云准备和模块化的。net 平台,用于创建可以在任何地方(Windows、Linux 和 macOS)运行的现代 web 应用、微服务、库和控制台应用。
ASP.NET Core 是一个免费的开源 web 框架,是 ASP.NET 的下一代。 它是一个模块化的框架,由一些小的框架组件包组成,这些组件包可以在完整的。net framework、Windows 和跨平台的。net Core 上运行。
这个框架是彻底重写的。 它统一了以前分离的 ASP。 asp.net MVC 和 asp.net.NET Web API 转换为单个编程模型。
ASP.NET Web API 的建立是为了将 Web /HTTP 编程模型映射到。NET 框架编程模型。 它使用熟悉的结构,如控制器、动作、过滤器等,这些结构在 ASP 中使用。 净 MVC。
ASP.NET Web API 是在 asp.net 的基础上设计的.NET MVC 运行时,以及一些简化 HTTP 编程的组件。 我们可以利用 Web API 技术在。net 框架的服务器上执行操作; 然而,要实现 RESTful,我们应该遵循本章前面讨论过的标准。 幸运的是,Web API 自动管理 HTTP 的所有底层传输细节,同时维护所有必需的约束。
由于 Web API 提供的一致性,强制 RESTful 原则,诸如移动设备、Web 应用、云等客户端可以轻松地访问它,没有任何问题:

在 ASP.NET Core、MVC 和 Web API 是不同的,它们分别继承了Controller和ApiController类。 另一方面,在 ASP.NET Core,它们遵循相同的结构。
下面是 MVC 和 Web API 的解决方案资源管理器视图。 你可以看到它们有一个相似的结构:

下面是一个控制器,当我点击文件|新|项目| ASP 自动创建.NET Core Web Application | Web API。 你可以看到控制器的基类是Controller而不是ApiController:
namespace WebAPIExample.Controllers
{
[Route("api/[controller]")]
public class ValuesController : Controller
{
// GET api/values
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/values/5
[HttpGet("{id}")]
public string Get(int id)
{
return "value";
}
// POST api/values
[HttpPost]
public void Post([FromBody]string value)
{ }
// PUT api/values/5
[HttpPut("{id}")]
public void Put(int id, [FromBody]string value)
{ }
// DELETE api/values/5
[HttpDelete("{id}")]
public void Delete(int id)
{ }
}
}
现在不要担心代码; 我们将在本书的后面讨论一切。
总结
REST 定义了如何通过附加的约束使用统一接口、如何识别资源、如何通过表示操作资源以及如何包含使消息自描述的元数据。
web 是建立在 HTTP 的统一接口之上的,其重点是与资源及其表示进行交互。 REST 不绑定任何特定的平台或技术; web 是唯一一个完全体现 REST 的主要平台。 基于 rest 的 web 服务体系结构的基本风格是客户机-服务器。
在这里,客户端请求资源,服务器处理并响应所请求的资源。 服务器的响应是基于用户和平台独立的。 关注点分离是客户机-服务器约束背后的原则。 因为在客户机-服务器体系结构中,存储和用户界面分别由服务器和客户机扮演角色,所以它提高了跨多个平台的用户界面的可移植性。
我们应该为客户端开发人员记录每个资源和 URI。 我们可以使用任何格式来结构化我们的文档,但它应该包含关于资源、uri、可用方法和访问服务所需的任何其他信息的足够信息。
Swagger 是一种用于文档目的的工具,它在一个屏幕上提供了关于 API 端点的所有信息,在这个屏幕上您可以可视化 API 并通过发送参数来测试它。 开发人员可以使用另一个名为Postman的工具来测试 api。 这两种工具将在本书接下来的章节中通过示例进行解释。
ASP。 净 Web API 是一个开发环境构建开发 RESTful Web 服务允许应用发送和接收 HTTP 请求(Web 请求)很容易和类型的基础上执行操作请求,它(如提供用户信息时,考虑到他们的 ID,等等)。
基于 ASP 的 Web API 设计.NET Core 遵循与 MVC 相同的编程模型进行了简化。
在下一章中,我们将通过设置环境开始编码,并研究 Web API 中 HTTP 动词的各种基础知识。
二、建立初始框架——奠定应用的基础
在上一章中,我们讨论了 REST,它的特点,以及它是如何在 ASP 中实现的。 净的核心。 在本章中,我们将继续使用这些知识并设置开发应用的环境。
我们将开始构建应用的基本框架。我们将了解每个 HTTP 动词,它们如何工作,以及它们在 ASP 中的实现范例.NET Core Web API。 在此之前,我们将快速了解一下 SOAP 以及它与 REST 的不同之处。
在介绍动词的同时,我们将探讨一个非常易于使用的工具来分析 HTTP 请求和响应。
我们将涵盖以下议题:
- 所有关于 web 服务(REST 和 SOAP)的内容
- 运行开发服务器
- REST 动词和状态码
- ASP 中动词的实现.NET Core Web API
- 例子使用邮差
- SOAP 和 REST
- 带有 REST API 的单页应用模型
- 面向服务的体系结构(SOA)与 REST 的概述
肥皂
简单对象访问协议(SOAP)是一种基于 xml 的消息传递协议,用于在计算机之间交换信息。 SOAP 依赖应用层协议,最常超文本传输协议(HTTP)或【显示】简单邮件传输协议**(SMTP),为消息谈判和传播。 正如我们所讨论的 HTTP,它安装在每个操作系统上并运行在每个操作系统上,实现 SOAP 的 web 服务可以使用任何语言从任何平台调用。**
**# SOAP 结构
我们已经知道 SOAP 消息是一个 XML 文档,但是让我们通过图表来更好地看一下:

下面是对上述图表中各组成部分的描述:
- Envelope:SOAP 消息结构的强制元素。 定义消息的开始和结束。
- 报头:SOAP 消息的可选元素。 它包含了可以用来处理数据的 SOAP 消息的相关信息。
- 正文**:主体**:这是主体部分,它以 XML 结构包含实际消息。 显然,它是一个强制元素。
- Fault:如果在处理 SOAP 消息时发生任何错误,可以使用一个可选的 Fault 元素来提供关于这些错误的信息。
你一定在想到底是谁让我们遵循这种结构的。 嗯,有一个名为 W3 的组织为特定的技术提出标准。 他们对 SOAP 结构做了同样的工作。
您可以很容易地在http://www.w3.org/2001/12/soap-envelope找到有关 SOAP 信封的详细信息。 同样,您可以在http://www.w3.org/2001/12/soap-encoding上看到有关 SOAP 编码和数据类型的详细信息。
Whatever we discuss about the structure of the SOAP message is defined by the W3 organization. However, this organization constantly investigates ways to optimize structures and bring in more robust specifications from time to time. So, we have to update with the latest specifications provided by them and implement them accordingly.
下面的块描述了 SOAP 消息的一般结构:
<?xml version = "1.0"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV = "http://www.w3.org/2001/12/soap-envelope"
SOAP-ENV:encodingStyle = "http://www.w3.org/2001/12/soap-encoding">
<SOAP-ENV:Header>
...
...
</SOAP-ENV:Header>
<SOAP-ENV:Body>
...
...
<SOAP-ENV:Fault>
...
...
</SOAP-ENV:Fault>
...
</SOAP-ENV:Body>
</SOAP_ENV:Envelope>
接收方会收到关于整个 SOAP 消息的通知,并带有信封的指示。 这意味着,如果客户端接收到的消息中包含一个信封,那么该消息将被完全接收,客户端可以解析并使用它进行进一步处理。 因此,SOAP 信封扮演着打包整个消息的角色。
关于 SOAP 的要点
以下是关于 SOAP 的一些要点:
- 每个 SOAP 消息中的
Envelope都有根位置,这对所有 SOAP 消息都是强制性的。 - 一个 SOAP 信封中应该恰好有一个 body 元素。
- 元素是一个可选元素。 然而,如果存在,那么应该只有一个
Header元素。 Header必须作为信封的第一个子元素出现在 body 元素之前。- 命名空间前缀
ENV和元素Envelope用于构建一个 SOAP 信封。 (参考下面的例子。) - 属性定义文档中使用的数据类型。 这给出了消息中出现的数据类型的泛化。 如果该属性出现在任何 SOAP 元素上,它将对元素的内容和所有子元素应用编码规则。
下面是一个符合 v1.2 的 SOAP 消息示例:
<?xml version = "1.0"?>
<SOAP-ENV:Envelope
xmlns:SOAP-ENV = "http://www.w3.org/2001/12/soap-envelope"
SOAP-ENV:encodingStyle = "http://www.w3.org/2001/12/soap-encoding">
...
SOAP Message information goes here
...
</SOAP-ENV:Envelope>
带有 HTTP POST 的 SOAP
HTTP 报头中提到的Authors实际上是要调用的控制器或程序的 URL,其中包含POST操作方法。 一切都托管在www.packtpub.com。
POST /Authors HTTP/1.1
Host: www.packtpub.com
Content-Type: application/soap; charset="utf-8"
Content-Length: nnnn
<?xml version = "1.0"?>
<SOAP-ENV:Envelope
xmlns:SOAP-ENV = "http://www.w3.org/2001/12/soap-envelope"
SOAP-ENV:encodingStyle = " http://www.w3.org/2001/12/soap-encoding">
...
Message information goes here
...
</SOAP-ENV:Envelope>
休息
REST 是一种体系结构风格,用于在 web 上的计算机系统之间提供标准,以便系统能够轻松地相互通信。 遵循 REST 风格的服务通常被称为 RESTful 服务。
让我们讨论一下标记为 RESTful 的 web 服务的几个重要约束。
服务器和客户端是独立的
使用 REST,服务器和客户机之间没有限制或依赖。 两者都可以相互独立。 它只是客户机用来理解服务的 URL。 可以修改服务器上 web 服务的代码,而不必关心与之相关联的客户机,反之亦然。
这种分离有助于客户机/服务器体系结构自由地呼吸,没有任何障碍。 因此,设计应用并分离其核心业务逻辑变得很容易。 我的意思很简单。 应用的设计可以使用客户端技术完成,只要需要在数据库中进行与业务相关的操作,就会调用 RESTful web 服务。
然而,保持服务器和客户机模块化和独立取决于一个条件,即它们发送和接收的消息的格式。 它们都应该知道要发送和接收的消息格式。
由于用户界面与与业务和数据存储相关的操作分离,因此可以通过简化服务器组件来提高灵活性和可伸缩性。 此外,分离允许每个组件独立地发展。
REST 端点由某些 url 公开。 不同的客户端可以使用 URL 连接,然后执行预期的操作并返回响应。
在本书中,我们将构建一个具有最小操作的小型电子商务 web 服务,其中用户可以使用购物车并下订单。 这些操作将使用端点公开。 正如我们所讨论的,端点可以很容易地从不同类型的客户端使用,包括移动应用、web 应用、服务器端代码等等。
无国籍
这个概念很容易理解。 在服务器/客户端架构中,服务器需要知道哪个客户端正在向它请求数据,并相应地决定发送什么,不发送什么。
然而,REST 系统是无状态的。 这意味着服务器不需要知道客户机的状态,反之亦然。 这将最终消除每次请求传入时服务器识别客户机的开销。
但现在的问题是,客户机和服务器如何交互? 答案是通过适当的信息。 假设用户想要查看订单细节。 它只需通过发送订单的 ID 来询问服务器,服务器将以.json或.xml格式返回订单详细信息,客户端可以很容易地解析这些信息。 每个消息都有处理该问题所需的信息。
这些约束(连同其他一些约束,如缓存,分层系统,统一界面、和代码需求)当上实现一个 web 服务帮助 RESTful 应用实现可靠性、优化性能和可伸缩性。 原因是组件可以独立管理、无缺陷地更新和重用,而不会影响整个系统。
让我们在下一节中了解服务器和客户机之间的通信是如何发生的。
设置环境
在探索通信机制之前,让我们先建立开发环境。 我们将使用 Visual Studio 2017 作为示例。
打开 Visual Studio,执行我们最喜欢的步骤,文件|新|项目,它会打开一个对话窗口,其中有可用的模板,如下图所示:

选择 ASP.NET Core Web Application 如图所示。 别忘了在左边面板中选择。net Core。 现在一切看起来都很酷。
让我们点击确定,然后我们会进入另一个对话框,在那里我们可以选择更多与我们的 web 应用相关的模板。显然,我们会点击 web API,然后点击确定。

完成项目的创建。 它精心制作了所有必要的组件,并为我们创建了一个示例控制器ValuesController,如下图所示:

这里有一个有趣的事实。 注意,ValuesController类继承了Controller基类。 如果你熟悉 ASP 中的 Web API.NET 在 Core 之前,你可能知道基类是ApiController。 这一变化背后的原因是 API 和 MVC 结构之间的一致性。 Controller是 ASP 的基类。 净 MVC。 现在在 ASP.NET Core, MVC 和 Web API 模板继承相同的基类。 ASP.NET Core、MVC 和 Web API 被合并成一个编程模型。
运行应用
为了确保一切工作正常,让我们运行应用。

让我们在下面几节中讨论刚刚发生的事情。
烹饪是什么?
请注意 URLlocalhost:57571/api/values,它将请求发送到ValuesController,因为在控制器上定义的路由是[Route("api/[controller]")]。 按照惯例,控制器名称总是附加文本controller。 因此api/values对应ValuesController。
现在的问题是,它如何返回value1和value2。 这是因为我们通过浏览器直接访问 URL,浏览器最终向控制器发送了一个GET请求。 因为控制器已经有了一个Get方法,所以它被执行了。 Get方法如下:
// GET api/values
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
该方法返回一个字符串数组,该数组在浏览器中打印。 为了便于理解,URL 格式已经在方法*(api/values)*上面了。
有趣的事实
现在让我们做一些实验。 你将得到一个非常好的洞察在幕后发生了什么:
- 在控制器上增加另一个方法
Get12(),移除[HttpGet]方法:
public IEnumerable<string> Get12()
{
return new string[] { "value1", "value2", "value3" };
}
// GET api/values
//[HttpGet] - Remove this attribute
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
你认为产量会是多少? 这很有趣。 下面是输出:

这意味着它找到了两个GET方法,但不能决定执行哪一个。 注意,它们都没有被属性(如[HttpGet])修饰。
- 现在让我们计划返回属性并测试发生了什么。 但是,我们将装饰新的
Get12方法,而保留原有的Get方法,并保留注释了的属性。 所以,代码是:
[HttpGet]
public IEnumerable<string> Get12()
{
return new string[] { "value1", "value2", "value3" };
}
// GET api/values
//[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
让我们快速看一下我们对输出做了什么:

足够清晰! 执行了Get12方法,其原因是我们通过[HttpGet]属性显式地告诉它它是Get方法。
- 在这两个方法中添加一个属性可以体验更多的乐趣:
[HttpGet]
public IEnumerable<string> Get12()
{
return new string[] { "value1", "value2", "value3" };
}
// GET api/values
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
你能猜出输出吗? 是的,这和我们在没有属性AmbiguousActionException的两个方法中看到的一样,如下截图所示:

- 最后,让我们创建另一个名为
HelloWorld()的方法,该方法带有现有的属性。 让我们删除其他属性:
[HttpGet]
public string HelloWorld()
{
return "Hello World";
}
public IEnumerable<string> Get12()
{
return new string[] { "value1", "value2", "value3" };
}
// GET api/values
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
完美! 让我们看看输出。 它是浏览器中的 Hello World:

结论
从上述观察可以得出以下结论。 注意,我们讨论的是带有 URLapi/values的GET请求,这意味着我们讨论的是控制器中的所有非参数化操作方法。 阅读以下几点时,忽略带参数或其他属性的方法:
- 当我们访问一个不带任何参数的特定 Web API 控制器(例如
api/values)时,带有[HttpGet]属性的操作方法首先从控制器中搜索。 - 如果在非参数化方法中没有提到某个属性,那么. net Core 运行时在为请求选择一个操作方法时会感到困惑。
- 对操作方法的命名约定没有限制。 只要它是唯一没有
[HttpGet]属性的方法,或者是唯一有[HttpGet]属性的方法,那么当GET request进入 API 时,它将被完美地执行。
请求和响应
现在我们已经快速浏览了演示ValuesController,让我们来检查客户机如何发送请求和如何接收响应。
REST 请求通常包括以下内容:
- **HTTP 动词:**这表示请求希望在服务器上执行何种操作。
- **Header:**这个 REST 请求元素允许客户端传递更多关于请求的信息。
- **URL:**REST 请求要操作的资源的实际路径。
- **正文:**正文可以包含与资源相关的额外数据,用于识别或更新资源。 这是可选的。
HTTP 动词
以下是请求 REST 系统进行资源交互时使用的基本 HTTP 动词:
- **GET:**用于通过资源的标识或资源集合检索特定的资源
- **POST:**用于创建/插入新资源
- **PUT:**用于更新指定资源的标识
- **DELETE:**用于删除指定的资源
让我们试着逐个探索 REST 中这些动词的请求/响应机制。 我们将尝试设计一个具有基本操作的电子商务应用。 在第一阶段,我们将致力于产品,这是这些类型的应用的核心。
邮递员
为了测试 API,我们可以使用一个非常易用的工具Postman。 可从:https://www.getpostman.com/下载。 请下载并打开。 在下一节中,我们将看到如何通过 Postman 发送请求,并分析从 Web API 获得的响应。
得到
我将添加另一个名为ProductsController的控制器。 现在,让我们有一个简单的动作方法GET,它将返回一些产品。 这些产品目前在 action 方法中是硬编码的。 该方法看起来如下所示:
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
namespace DemoECommerceApp.Controllers
{
[Produces("application/json")]
[Route("api/[Controller]")]
public class ProductsController : Controller
{
// GET: api/Products
[HttpGet]
public IEnumerable<Product> Get()
{
return new Product[]
{
new Product(1, "Oats", new decimal(3.07)),
new Product(2, "Toothpaste", new decimal(10.89)),
new Product(3, "Television", new decimal(500.90))
};
}
}
}
属性为[Route]提供了定义良好的模板"api/[Controller]"。 这里,控制器名称为ProductsController。 当我们使用 URLapi/Products进行请求时,框架将搜索定义了该路由的控制器。 [Controller]占位符是一种特殊的命名约定,在运行时将被文本(控制器名称)Products替换。 但是,您可以直接使用控制器名编写完全限定模板,例如[Route (api/Products)]。
因此,这个GET方法将返回三个产品及其详细信息。 可以像下面这样设计Product类,使用构造函数来构建Product对象:
public class Product
{
public Product(int id, string name, decimal price)
{
Id = id;
Name = name;
Price = price;
}
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
做完了。 让我们通过GET request到Postman来分析 REST 中的请求和响应机制。 对于一个GET request来说,这很简单。 打开邮递员。 然后按照下面截图中提到的步骤:

Executing GET request in Postman
在步骤 1中,只需粘贴 URL,即本例中的http://localhost:57571/api/products。 已经为GET请求设置了其他所有内容。 您可以看到 URL 框左侧的请求类型为GET。 这意味着当前请求将是一个GET请求。 点击发送按钮,如步骤 2。
响应是显示在底部部分中的产品列表。 它是.json格式的。 请看下面的截图,显示了GET请求的响应:

现在您已经愉快地了解了GET是如何工作的,让我们来分析一下在幕后发生了什么。 客户端(这里是Postman)发送一个 HTTP 请求并得到一个响应。 在发送请求时,它还指定了请求头,服务器相应地发送响应头。
HTTP Headers enable the client and server to both send and receive additional information with the request and response respectively. This decides the exact behavior of the HTTP transaction. You can refer to the following resources to learn more about the headers. We will have a quick look at the headers in the next section:
- https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
在“邮差”中,点击代码**、**,如下截图所示:

单击此链接将打开一个模式,该模式将显示发送给服务器以服务请求的 HTTP 请求头。 看看下面的模态截图,它清楚地提到请求类型为GET,Host为 API 的 URL,然后是其他头,如Cache-Control和Postman-Token:

想知道这个GET调用的jQuery代码片段是什么样子的吗? 跟邮差在一起超级简单。 点击主界面的 Code,在包含语言的下拉菜单中选择jQuery。 (见下面的截图) 此外,通过从下拉列表中选择,您可以获得不同语言的代码。 复制快乐!

Response Header清晰显示在主页面,如下截图所示。 注意,这里提到了一个 Status 代码,在本例中是 200 OK。 那么,这段代码表示什么呢?
让我们在下一节讨论它。

Postman Response Headers
状态码
当服务器返回响应时,它包含状态码。 这些状态码通知客户机如何在服务器上执行请求。 作为一名开发者,你不需要知道每一个状态码(它们有很多),但你应该知道最常见的状态码以及它们的使用方式:
| 状态码 | 讲解 |
| 200 好了 | 成功 HTTP 请求的标准响应。 |
| 201 年创建 | 成功创建项时,HTTP 请求的标准响应。 |
| 204 没有内容 | 成功的 HTTP 请求的标准响应,如果响应体中没有返回任何内容。 |
| 400 错误请求 | 由于错误的请求语法、过大的请求大小或其他客户机错误,请求无法被处理。 |
| 403 年被禁止的 | 客户端没有访问请求资源的权限。 |
| 404 没有找到 | 此时无法找到资源。 它可能已经被删除,或者还不存在。 |
| 500 内部服务器错误 | 当处理服务器端代码时发生失败或异常时,就会出现此响应。 |
在默认情况下,以下 HTTP 动词需要服务器提供一些状态码:
GET:返回 200 OKPOST:返回 201 已创建PUT:返回 200 OKDELETE:如果操作失败,返回 204 NO CONTENT
我们已经看到 API 如何为GET请求返回 200 OK。 当我们继续学习其他动词时,我们将探索使用前面提到的代码返回的响应。
ASP.NET Core HTTP 属性
根据Internet Engineering Task Force (IETF) RFC-7231文档(https://tools.ietf.org/html/rfc7231),ASP. ASP. NET Core 实现了列出的 8 个 HTTP 动词中的 7 个 HTTP 属性。 框架中唯一从谓词列表中排除的是 HTTP TRACE 谓词。
下面是在 ASP 中提供的 HTTP 谓词属性的完整列表.NET Core:
HttpGetAttributeHttpPostAttributeHttpPutAttributeHttpDeleteAttributeHttpHeadAttributeHttpPatchAttributeHttpOptionsAttribute
由于动词名称与属性连接在一起,很明显,它们将用于各自的动词。 这些属性帮助框架理解什么动作方法与什么动词相关联。 考虑到这一点,它可以决定在控制器的请求到来时执行哪一个。
框架还提供了另一个路由的重要属性RouteAttribute。
还有一些更多的属性用于操作方法的参数,以帮助识别从请求的不同位置传递给 API 操作的参数,例如 URL、Body 等等。 以下是在框架中为动作参数提供的一些属性:
FromServicesAttributeFromRouteAttributeFromQueryAttributeFromBodyAttributeFromFormAttribute
帖子
POST 用于创建资源。 在本例中,我们将尝试使用对服务器的 POST 请求创建一个产品。 在此之前,我们将对我们的项目做一些更改。 你可以在 GitHub(https://github.com/PacktPublishing/Building-RESTful-Web-services-with-DOTNET-Core)上找到所有相关代码,所以完全不用担心!
为什么要等呢? 让我们把 Post 方法写如下:
// POST: api/Products
[HttpPost]
public async Task<IActionResult> Post([FromBody]Product product)
=> (await _productService.CreateProductAsync(product))
? (IActionResult)Created($"api/products/{product.Id}", product) // HTTP 201
: StatusCode(500); // HTTP 500
操作方法调用相关服务的CreateProductAsync并检查操作是否成功。 如果成功,则返回201,else500。 请注意,为了返回正确的状态代码,我们利用了IActionResult。 这个接口有一组很大的子类,可以通过Controller类访问。 由于继承自Controller基类,我们可以很容易地使用StatusCode等方法根据我们对资源执行的操作返回预期的状态。
在前一节中,我们提到,如果POST成功,我们应该得到 201 CREATED,如果失败,它应该发送一个通用的 500 Internal Server Error 响应。 这就是代码所做的。
还有一件有趣的事情,那就是:Created($"api/products/{product.Id}", product)。 这是Controller类中的一个方法,它将 URL 分配给位置,并将 201 分配给响应的状态代码。 不相信我? 好的,让我马上用邮差来证明。
请看以下来自邮差请求屏幕的截图:

注意,我们将产品的数据作为 JSON 传递,在创建产品之后,API 返回状态代码为 201 Created,并返回已创建新产品的 URL,即 API /products/1。 这意味着,当您将此 URL 作为GET请求运行时,您将收到新创建的产品详细信息。 很简单,不是吗?
正如您所看到的,传递的产品详细信息的数据类型是 JSON,但问题是,谁告诉服务器它是这种格式的? 好吧,那是用值application/json设置的请求头content-type。 你可以在最后的截图中看到。 默认编码charset=utf-8由Postman附加。
然而,有趣的是,为什么Postman知道我们想要的数据类型内容是 JSON? 它不能自动设置。 我让它这么做的。
设置任何类型的请求头的选项就在 URL 文本框的下方。 参考下面的截图,显示我已经设置了content-type标题:

对于前面提到的通过 ID 返回产品详细信息的GET请求,我们可以设计如下 action 方法:
// GET: api/Products/1
[HttpGet("{id}")]
public Task<Product> Get(int id)
=> _productService.GetOrderAsync(id);
这里,我们提供了一个从"{id}"到[HttpGet]的模板参数。 这将确保有一个 HTTP Get 路由(如api/orders/1)可用,其中 ID 是传递给GET请求的变量。
We have a service called ProductService which is implementing the interface IProductService and through the constructor of the controller, the service (dependency) is injected, which is called dependency injection. With .NET Core, it's very easy to handle dependencies with the built-in inversion of control container. If you are not getting what I am talking about, then I would highly recommend going through my other book on this topic, *Dependency Injection in .NET Core *(https://www.packtpub.com/application-development/dependency-injection-net-core-20).
把
HTTP PUT 动词是幂等的。 这意味着带有特定负载的第一个 HTTP PUT 请求将影响服务器和资源。 它将更新 ID 指定的资源。 但是,具有相同负载的后续 HTTP PUT 请求将导致与第一个请求相同的响应。
考虑下面的例子,我们将更新一个产品:
// PUT: api/Products/1
[HttpPut("{id}")]
public async Task<IActionResult> Put(int id, [FromBody]Product product)
=> (await _productService.UpdateProductAsync(id, product))
? Ok()
: StatusCode(500);
[HttpPut]属性由{id}模板提供,与[HttpGet]模板类似。 在PUT的情况下,它将从 URL 获取 ID 和Product对象的身体要求,由[FromBody]属性指定为我们做的POST在前一节中。
当 ID 和产品对象与参数绑定后,方法体开始执行,然后调用具有相同参数的服务方法UpdateProductAsync。 该方法将根据更新是否成功返回一个布尔值。 如果一切都成功了,我们将通过调用OK()方法返回 200 OK,否则,如果发生错误,将给出 500 Internal Server Error。
让我给你看一下《邮差》的截图:

如果PUT请求的 ID 已经过期,则可以返回另一个状态码 301 Moved permanent,这意味着在请求主体中传递的产品与 ID 不关联。 为了识别这种情况,我们需要相应地添加业务逻辑,以及是否可以验证 ID 是否与产品相关。 如果没有,我们可以简单地返回 301 Moved permanent 和产品当前实际存在的新 URL。
删除
理想情况下,DELETE请求应该删除该资源。 一旦操作成功,我们可以通过调用OK()方法发送一个 200 OK 状态码。
参考以下代码块:
// DELETE: api/Products/1
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
=> (await _productService.DeleteOrderAsync(id))
? (IActionResult)Ok()
: NoContent();
注意DeleteOrderAsync方法,该方法提供了要删除的产品的 ID。 现在,您可以从该方法返回一个布尔值,它将指示操作是否成功。 如果没有找到该 ID 对应的任何产品,只需返回false。 然后,我们会相应地决定返回给客户什么。
如果返回false,NoContent()可以用来返回状态码204。 如果资源已经被删除,并且客户端正在请求同样的资源,那么服务器将返回一个状态码 204 No Content。 这意味着服务器无法找到所请求的资源,因为它不再存在了。
看看邮差的截图。 查看状态码为200 OK即可删除成功:

SOAP 和 REST
以下是 SOAP 和 REST 之间的一些关键区别:
| 肥皂 | 【t】休息【t】 |
| 它是一种基于 xml 的消息协议。 | 它是一种建筑风格。 |
| WSDL 用于客户机和服务器之间的通信。 | XML 或 JSON 用于在客户机和服务器之间发送和接收数据。 |
| 通过调用 RPC 方法来调用服务。 | 服务通过 url 公开端点。 |
| 响应很容易被人类读懂。 | 响应以纯 XML 或 JSON 的形式进行读取。 |
| 数据传输通过 HTTP 进行。 它利用了 SMTP、FTP 等协议。 | REST 数据传输仅通过 HTTP 进行。 |
| 从 JavaScript 调用 SOAP 服务是很困难的。 | 从 JavaScript 调用 REST 服务非常容易。 |
单页面应用模型
传统上,在 web 应用中,客户端向服务器请求一个 web 页面。 然后,服务器在验证和验证请求之后(如果必要的话)用所请求的 HTML 页面响应客户机。 下一个对服务器的请求可能发生在用户点击页面上的某个链接、提交表单等等时。 服务器再次处理请求,并使用另一个 HTML 页面返回响应。
难道你不认为,与其获得整个 HTML 页面(这将是几乎相同的外观和感觉,作为最后加载的页面),我们应该获得我们需要的数据,并更新当前加载的页面本身,而不将其发送回服务器? 是的,现代 web 开发在这方面是可行的。 今天,我们只需要使用 Ajax 从服务器按需获取数据。 在接收到数据后,我们只需要用 JavaScript 或者 Angular 之类的客户端框架来更新 UI。
这就是我们所说的s****单页应用(SPA)。 在第一次向服务器发出请求时,服务器会响应应用的整个页面。与传统的 web 应用不同,后续的请求不会请求 HTML 页面,而是使用 Ajax 请求数据,其中内容类型通常是 JSON。 在获取数据之后,浏览器必须只更新页面中已更改的部分,而不是重新加载整个页面。 通过快速响应同一页面上的用户操作,SPA 确实改善了用户体验,因为重新加载页面会暂时转移用户的注意力。
然而,实现 SPA 并不容易,因为我们必须确保在需要时在页面上显示新的数据。 在这里,新兴技术,如 ASP.NET Web API 和 JavaScript 框架,如 AngularJS 和 CSS3,在设计 spa 时都会派上用场。
应用可以调用 REST API 的不同端点来执行某些任务,并在获得响应后更新 UI,而无需重新加载页面。
面向服务的体系结构
与 SPA 一样,Web API 在面向服务的体系结构(SOA)中扮演着重要的角色。 顾名思义,它是一种体系结构方法,处理从面向业务的角度将职责分离为独立的服务。 通常,可以使用 RESTful Web api 设计这些独立的服务或组件。
考虑一个电子商务应用,它具有不同的组件,例如订购、账单、支付处理、客户配置文件管理等等。 每个组件都有自己的业务逻辑,可以独立实现。
下图展示了这样一个具有独立组件的应用的图形视图:

为了使它们独立,可以为这些组件公开 RESTful api,只要它们满足身份验证和授权,任何客户机/应用(包括其他组件)都可以轻松使用这些 api。
下面是单片或传统应用体系结构与 SOA 的图示。 SOA 如何为相同的业务应用引入可重用组件是非常清楚的。 此外,通过 Web API 实现它们可以将其公开给任何应用使用:

SOA 实现超出了本书的范围,因为有很多关于 Web API 的内容需要我们在下一章中探讨。 无论我们在本书中对应用做什么,都将使用一个 Web API,但您可以将它们分开来构建一个更可伸缩的架构。
总结
我们从 SOAP 的一些基本知识开始,然后逐渐转向 REST。
本章介绍了基本的 REST 构建块以及它们实际上是如何工作的。 我们研究了广泛使用的 HTTP 动词返回的不同状态码。
要探索 Web API 的请求和响应周期,可以使用 Postman,在这里您不仅可以控制正在发送和接收的内容,而且还可以获得用于不同语言的代码,以便使用 API。
ASP.NET Core 属性可以绑定到控制器操作方法,使它们在路由和参数方面更具表现力和可管理性。
单页应用可以使用客户端技术和使用 ASP 轻松设计.NET Core Web api,以便页面可以在收到响应后立即更新,以获得顺畅的用户体验。
Web api 可以插入到面向服务的架构中,以实现提高可伸缩性的模块化设计。 在 api 的帮助下,通过分离整个体系结构的不同关键组件,我们可以更好地在不同的应用中重用组件,并将它们公开,以便任何人都可以使用它们。
在下一章中,我们将研究与身份验证相关的 Web API 体系结构的重要部分。**
三、用户注册及管理
我们在上一章中构建了应用的基础,在那里我们还详细探索了 HTTP 谓词,同时在 ASP 中创建控制器.NET Core Web API。
现在,我们正逐渐转向 API 的一个重要方面,即身份验证。 由于 API 易于访问,身份验证肯定是必需的组件。 限制请求并对其进行限制可以防止恶意攻击。
应用的用户(在我们的例子中是客户)需要一个注册表单/界面,系统可以获取他们的详细信息。 我们将看到如何使用 API 注册用户。
在您注册并获得客户的所有详细信息后,如电子邮件和密码,您就可以很容易地识别来自客户的请求。 等等,这很简单,但是我们需要遵循一些原则来验证用户以访问我们的资源。 这就是基本身份验证和OAuth 身份验证的作用。
本章将涵盖以下主题:
- 为什么要进行身份验证和限制请求?
- 使用 EF Core 引导我们的 REST API
- 向 REST API 添加基本身份验证
- 向我们的服务添加 Oauth 2.0 身份验证
- 定义基于客户机的 API 使用体系结构
为什么要进行身份验证和限制请求?
如果我告诉您某个特定国家的政府公开了一个 Web API,您可以使用它来获取其公民的所有详细信息,那么您首先会问我是否可以从该 API 中提取数据。 这正是我们将要讨论的。
所以,如果你把前面的例子,从 API 返回的数据将公民的敏感数据,如*,地址、电话号码,国家和社会安全号码。 政府不应该允许所有人访问这些数据。 通常只允许认证的源。 这意味着当您调用一个 API 时,您需要发送您的身份并请求它允许您操作数据。 如果标识错误或不在允许的源列表中,API 将拒绝该标识。 想象恐怖分子试图访问 API,您肯定会通过检测他们的身份来拒绝访问。*
*现在想象另一个场景,大学有一个 API,它发送特定课程的特定学期的结果。 许多其他网站会通过调用这个大学 API 在他们的网站上显示结果。 黑客使用代码块在循环中调用 API。 如果时间间隔太小,那么如果您收到服务器繁忙/服务器不可达消息,也不要感到惊讶。 这是因为,在短时间内处理大量请求时,服务器会过载并耗尽资源。
这就需要对 API 进行限制,不允许同一源在特定时间间隔内发出更多请求。 例如,如果有任何消费者访问我们的 API,如果消费者在过去 10 秒左右已经请求过该请求,我们将不允许该请求。
首先,在探讨其他概念之前,让我们先设计应用的数据库。
数据库设计
我们肯定会有一个Customers桌。 我们将在该表中存储客户信息,并使用该表的主键作为其他表中的引用,如Orders和Cart。
客户表可以设计如下。 你可以在这本书中找到名为FlixOneStore.sql的数据库脚本:

CRUD 操作将通过 API 在这些表上执行。 让我们从 API 对该表执行一些操作开始。 更准确地说,我们讨论的是客户注册和登录过程。
用户注册
让我们先将模型放入 API 中,这样我们就可以创建对象并在数据库中保存数据。 我们将使用Entity Framework Core(EF Core)版本 2.0.2。
使用 API 设置 EF
要使用 EF Core,需要安装以下包,可以从工具内部的 NuGet 包管理器下载安装:

此外,我们还需要另一个名为 Microsoft.EntityFrameworkCore.Tools 的包。 这将帮助我们从数据库中创建模型类:

现在,我们到了需要根据数据库表建模类的地方。 下面的 powershell 命令可以在包管理器控制台上执行,以创建Customers表的模型类:
Scaffold-DbContext "Server=.;Database=FlixOneStore;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -Tables Customers
我们在命令中提供了连接字符串,以便它连接到我们的数据库。
下面是我们刚刚讨论过的命令的两个重要部分:
-OutputDir Models:定义模型类将放置的文件夹。-Tables Customers:定义将被提取为模型类的表。 我们现在正在处理客户。
在执行之后,您将在Models文件夹中看到两个文件Customers.cs和FixOneStoreContext.cs。 Customers.cs文件如下所示:
using System;
namespace DemoECommerceApp.Models
{
public partial class Customers
{
public Guid Id { get; set; }
public string Gender { get; set; }
public string Firstname { get; set; }
public string Lastname { get; set; }
public DateTime Dob { get; set; }
public string Email { get; set; }
public Guid? Mainaddressid { get; set; }
public string Telephone { get; set; }
public string Fax { get; set; }
public string Password { get; set; }
public bool Newsletteropted { get; set; }
}
}
Configuring DbContext
可以在具有OnConfiguring和OnModelCreating方法的同一个文件夹中找到context类,该方法具有Customers的属性。
下面的代码块显示了FlixOneStoreContext类:
using Microsoft.EntityFrameworkCore;
namespace DemoECommerceApp.Models
{
public partial class FlixOneStoreContext : DbContext
{
public virtual DbSet<Customers> Customers { get; set; }
public FlixOneStoreContext(DbContextOptions<
FlixOneStoreContext> options)
: base(options)
{ }
// Code is commented below, because we are applying
dependency injection inside startup.
// protected override void OnConfiguring(
DbContextOptionsBuilder optionsBuilder)
// {
// if (!optionsBuilder.IsConfigured)
// {
//#warning To protect potentially sensitive information
in your connection string, you should move it out of
source code. See http://go.microsoft.com/fwlink/?LinkId=723263
for guidance on storing connection strings.
// optionsBuilder.UseSqlServer(@"Server=.;
Database=FlixOneStore;Trusted_Connection=True;");
// }
// }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Customers>(entity =>
{
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.Dob)
.HasColumnName("dob")
.HasColumnType("datetime");
entity.Property(e => e.Email)
.IsRequired()
.HasColumnName("email")
.HasMaxLength(110);
entity.Property(e => e.Fax)
.IsRequired()
.HasColumnName("fax")
.HasMaxLength(50);
entity.Property(e => e.Firstname)
.IsRequired()
.HasColumnName("firstname")
.HasMaxLength(50);
entity.Property(e => e.Gender)
.IsRequired()
.HasColumnName("gender")
.HasColumnType("char(1)");
entity.Property(e => e.Lastname)
.IsRequired()
.HasColumnName("lastname")
.HasMaxLength(50);
entity.Property(e => e.Mainaddressid).HasColumnName
("mainaddressid");
entity.Property(e => e.Newsletteropted).HasColumnName
("newsletteropted");
entity.Property(e => e.Password)
.IsRequired()
.HasColumnName("password")
.HasMaxLength(50);
entity.Property(e => e.Telephone)
.IsRequired()
.HasColumnName("telephone")
.HasMaxLength(50);
});
}
}
}
您是否注意到,我已经注释了OnConfiguring方法,并添加了一个构造函数,以便我们可以从启动时注入依赖项,用连接字符串初始化上下文? 做一下。
因此,在ConfigureServices启动过程中,我们将使用连接字符串将上下文添加到 services 集合中:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IProductService, ProductService>();
services.AddMvc();
var connection = @"Server=.;Database=FlixOneStore;
Trusted_Connection=True";
services.AddDbContext<FlixOneStoreContext>(
options => options.UseSqlServer(connection));
}
生成控制器
下一步是添加控制器。 为此,请参考以下步骤:
- 右键单击
Controller文件夹,然后单击 Add,然后单击 Controller。 你会在一个模态中看到创建不同类型控制器的选项:

- 选择带有动作的 API Controller,使用实体框架,然后单击 Add 按钮。 下面的截图显示了接下来发生的事情:

- 点击添加。瞧! 它完成了所有艰苦的工作,并使用 EF Core 创建了一个功能齐全的控制器,其中包含了所有主要的 HTTP 动词。 下面的代码块是仅使用
GET方法的控制器的一个小快照。 我已经删除了其他节省空间的方法:
// Removed usings for brevity.
namespace DemoECommerceApp.Controllers
{
[Produces("application/json")]
[Route("api/Customers")]
public class CustomersController : Controller
{
private readonly FlixOneStoreContext _context;
public CustomersController(FlixOneStoreContext context)
{
_context = context;
}
// GET: api/Customers
[HttpGet]
public IEnumerable<Customers> GetCustomers()
{
return _context.Customers;
}
// GET: api/Customers/5
[HttpGet("{id}")]
public async Task<IActionResult> GetCustomers
([FromRoute] Guid id)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var customers = await
_context.Customers.SingleOrDefaultAsync(m => m.Id == id);
if (customers == null)
{
return NotFound();
}
return Ok(customers);
}
// You will also find PUT POST, DELETE methods.
// These action methods are removed to save space.
}
}
这里有几点需要注意:
- 注意这里是如何通过将
FlixOneStoreContext注入构造函数来初始化它的。 此外,它将用于所有操作中与数据库相关的操作:
private readonly FlixOneStoreContext _context;
public CustomersController(FlixOneStoreContext context)
{
_context = context;
}
- 接下来要关注的是用于从操作返回结果的方法。 查看如何使用
BadRequest()、NotFound()、Ok()和NoContent()来返回客户端容易理解的正确 HTTP 响应代码。 当我们调用这些操作来执行真正的任务时,我们将看到它们稍后返回的代码。
从页面调用 API 来注册客户
为了简化工作,我设计了一个简单的 HTML 页面,其中包含客户记录的控件,如下所示。 我们将输入数据并尝试调用 API 来保存记录:
<div class="container">
<h2>Register for FlixOneStore</h2>
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2" for=
"txtFirstName">First Name:</label>
<div class="col-sm-3">
<input type="text" class="form-control" id=
"txtFirstName" placeholder=
"Enter first name" name="firstname">
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for=
"txtLastName">Last Name:</label>
<div class="col-sm-3">
<input type="text" class="form-control" id=
"txtLastName" placeholder=
"Enter last name" name="lastname">
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="txtEmail">
Email:</label>
<div class="col-sm-3">
<input type="email" class="form-control" id=
"txtEmail" placeholder=
"Enter email" name="email">
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="gender">
Gender:</label>
<div class="col-sm-3">
<label class="radio-inline"><input type="radio"
value="M" name="gender">Male</label>
<lable class="radio-inline"><input type="radio"
value="F" name="gender">Female</lable>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="txtDob">
Date of Birth:</label>
<div class="col-sm-3">
<input type="date" class="form-control" id="txtDob" />
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="txtMobile">
Mobile Number:</label>
<div class="col-sm-3">
<input type="text" class="form-control" id="txtMobile"
placeholder=
"Enter mobile number" />
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="txtFax">Fax:</label>
<div class="col-sm-3">
<input type="text" class="form-control" id="txtFax"
placeholder="Enter fax" />
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="txtPassword">
Password:</label>
<div class="col-sm-3">
<input type="password" class="form-control" id=
"txtPassword" placeholder=
"Enter password" name="pwd">
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="txtConfirmPassword">
Confirm Password:</label>
<div class="col-sm-3">
<input type="password" class="form-control"
id="txtConfirmPassword" placeholder=
"Enter password again" name="confirmpwd">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="button" class="btn btn-success"
id="btnRegister">Register</button>
</div>
</div>
</div>
</div>
我已经使用 bootstrap 和jQuery与我的代码。 您可以参考本书附件中的整个代码或参考https://github.com/PacktPublishing/Building-RESTful-Web-services-with-DOTNET-Core。
现在是代码的重要部分,在这里我们将调用 API 来存储客户记录。 参考以下代码块:
$(document).ready(function () {
$('#btnRegister').click(function () {
// Check password and confirm password.
var password = $('#txtPassword').val(),
confirmPassword = $('#txtConfirmPassword').val();
if (password !== confirmPassword) {
alert("Password and Confirm Password don't match!");
return;
}
// Make a customer object.
var customer = {
"gender": $("input[name='gender']:checked").val(),
"firstname": $('#txtFirstName').val(),
"lastname": $('#txtLastName').val(),
"dob": $('#txtDob').val(),
"email": $('#txtEmail').val(),
"telephone": $('#txtMobile').val(),
"fax": $('#txtFax').val(),
"password": $('#txtPassword').val(),
"newsletteropted": false
};
$.ajax({
url: 'http://localhost:57571/api/Customers',
type: "POST",
contentType: "application/json",
data: JSON.stringify(customer),
dataType: "json",
success: function (result) {
alert("A customer record created for: "
+ result.firstname + " " + result.lastname);
},
error: function (err) {
alert(err.responseText);
}
});
});
});
注意http://localhost:57571/api/CustomersURL 和POSTHTTP 方法。 这最终会调用 API 中名为PostCustomers的Post方法。 我们在表中肯定会有一些唯一性,在我们的例子中,我将电子邮件作为每个记录的唯一性。 这就是为什么我需要修改一下action方法:
// POST: api/Customers
[HttpPost]
public async Task<IActionResult> PostCustomers([FromBody] Customers customers)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// Unique mail id check.
if (_context.Customers.Any(x => x.Email == customers.Email))
{
ModelState.AddModelError("email", "User with mail id already
exists!");
return BadRequest(ModelState);
}
_context.Customers.Add(customers);
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException ex)
{
if (CustomersExists(customers.Id))
{
return new StatusCodeResult(StatusCodes.Status409Conflict);
}
else
{
throw;
}
}
return CreatedAtAction("GetCustomers", new { id = customers.Id },
customers);
}
我通过为模型属性电子邮件添加一个错误消息来返回BadRequest()。 我们很快就会看到它是如何显示在浏览器上的!
以下从浏览器捕获的图像显示了成功创建的客户:

一个成功注册的客户看起来类似于前面的图像,它显示了在 ajax 调用的success方法中提示的成功消息。
您可以在完成时对从action方法接收到的数据执行任何您想要的操作,因为它返回整个customer对象。 如果你不相信我,参考下面的截图从源代码窗口的调试器工具:

The response to the POST request with the new Customer created inside the jQuery Ajax success method
那么,这是谁干的? 简单地说,下面的 return 语句在POST方法中完成了所有神奇的事情:
return CreatedAtAction("GetCustomers", new { id = customers.Id }, customers);
这一行做了几件事:
- 发送状态码:201 创建为
POST动作成功创建资源。 - 使用资源的实际 URL 设置 Location 头。 如果您记得 RESTful 特征,那么在
POST操作之后,服务器应该发送资源的 URL。 这就是它的作用。
让我向您展示开发人员工具的网络选项卡,以证明我的观点。 您也可以使用Postman来分析它。 下面的截图显示了响应的详细信息:

The response received by a POST success request with Status Code and Location Header
Guid实际上是我们在数据库中的列类型中定义的Customer ID,我在Customer模型类构造函数中为其赋值。
现在,如果你复制这个 URL 并在你的浏览器或邮递员中打开它,你会得到客户的详细信息,如下截图所示:

让我们看一个带有已经存在的邮件 ID 的BadRequest()示例。 由于taditdash@gmail.com客户已经存在,发送具有相同电子邮件 ID 的另一个请求应该会向我们发送一个错误消息作为响应。 让我们来看看:

记住,我们添加了一行来检查电子邮件 ID 是否存在,并添加了一个ModelState错误。 现在已经在行动了。
For simplicity of the demo in this book, I am just saving a plain text password. You should not do that in the actual project. Implementing proper encryption for a password is a must.
至此,我将结束注册过程。 但是,在客户端和服务器端都有实现验证的余地。 您可以向Model类属性添加属性,使其更加可靠,这样您就不会从客户端获得错误的数据。 当ModelState验证失败时,发送BadRequest()响应。 可以将所需的电子邮件格式和密码比较属性添加到Model类中。
CORS
如果你在调用 API 动作时看到以下错误,那么你需要启用Cross Origin Resource Sharing(CORS):
Failed to load http://localhost:57571/api/Customers: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access. The response had HTTP status code 404.
要在所有起源中启用 CORS,请遵循前面显示的步骤:
- 安装
Microsoft.AspNetCore.CorsNuGet 包:

- 在
Startup ConfigureServices内部,添加以下代码来实现一个允许所有起源的 CORS 策略:
services.AddCors(options =>
{
options.AddPolicy("AllowAll",
builder =>
{
builder
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
- 在
Configure方法内部,在app.UseMvc();之前添加以下一行(这很重要):
app.UseCors("AllowAll");
现在,它应该像预期的那样工作。 如果您想更多地了解 CORS,请访问https://docs.microsoft.com/en-us/aspnet/core/security/cors?view=aspnetcore-2.1。
向 REST API 添加基本身份验证
现在我们注册了客户,我们可以进入身份验证过程了。 身份验证是验证一个客户是否是我们网站的有效用户。 我们已经有了他们的证件,因为他们使用我们的注册表注册。 当他们试图访问任何资源从我们的网站使用这些凭证,我们将首先核实,然后允许。
所有人都可以注册,不会被认证。 然而,当客户**想读他们的背景资料或删除账户,等等,然后我们需要身份验证,数据返回给用户实际受信任的应用的用户。
对于基本认证:
- 当客户端请求资源时,我们将从客户端获得用户名,这将是电子邮件 ID和密码。 这将与 HTTP 头一起发送。 当我们设计客户时,我们会看到这一点。
- 然后,这些数据将从数据库中进行验证。
- 如果找到,该操作将被允许,否则将发送一个
401 Unauthorized响应。
步骤 1 -添加(authorize)属性
让我们限制返回客户概要细节的操作方法,即CustomersController的GET方法,命名为GetCustomers([FromRoute] Guid id)。
当客户试图访问配置文件时,我们将验证以下两件事:
- 请求来自应用的可信用户。 这意味着,请求来自具有有效的电子邮件和密码的客户。
- 客户只能访问他们的配置文件。 为了检查这一点,我们将使用 URL 上被请求的客户的ID 验证客户的凭据(存在于请求中)。
让我们开始吧。 记住,我们的目标是实现以下几点:
[Authorize(AuthenticationSchemes = "Basic")]
public async Task<IActionResult> GetCustomers([FromRoute] Guid id)
现在,我们将把注意力集中在这个动作方法上,以理解这个概念。 您可以在这里看到Authorize属性,其中AuthenticationScheme定义为Basic。 这意味着我们必须告诉运行时基本身份验证是什么,以便它在进入操作方法之前先执行该身份验证。
如果身份验证成功,将执行操作方法,否则将向客户端发送 401 Unauthorized 响应。
步骤 2 -设计 BasicAuthenticationOptions 和 BasicAuthenticationHandler
首先,我们需要一个类来派生Microsoft.AspNetCore.Authentication中的AuthenticationSchemeOptions类,如下代码块所示:
using Microsoft.AspNetCore.Authentication;
namespace DemoECommerceApp.Security.Authentication
{
public class BasicAuthenticationOptions : AuthenticationSchemeOptions {}
}
为简单起见,它被留空,但可以用不同的属性加载它。 我们就不深入讨论了。
接下来,我们需要一个用于基本身份验证的处理程序,其中我们将有我们的实际逻辑:
public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOptions>
我们可以使用一个额外的DbContext参数作为它的类构造函数,因为我们将验证数据库中的客户详细信息:
public BasicAuthenticationHandler(IOptionsMonitor<BasicAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, FlixOneStoreContext context)
: base(options, logger, encoder, clock)
{
_context = context;
}
AuthenticationHandler<T>是一个抽象类,具有与身份验证相关的属性和方法。 目前,我们将覆盖两个方法HandleAuthenticateAsync和HandleChallengeAsync。 HandleAuthenticateAsync将具有验证客户的实际逻辑,HandleChallengeAsync用于处理 401 个挑战问题,这意味着无论何时您决定客户无效,都可以用这种方法编写代码来处理这种情况。
我们假设将在 HTTP 头中接收电子邮件和密码,该头名为Authorization,由分隔符冒号(:)分隔。 下面是从 header 中提取数据并验证其是否正确的代码:
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
// 1\. Verify if AuthorizationHeaderName present in the header.
// AuthorizationHeaderName is a string with value "Authorization".
if (!Request.Headers.ContainsKey(AuthorizationHeaderName))
{
// Authorization header not found.
return Task.FromResult(AuthenticateResult.NoResult());
}
// 2\. Verify if header is valid.
if (!AuthenticationHeaderValue.TryParse(Request.Headers
[AuthorizationHeaderName], out AuthenticationHeaderValue
headerValue))
{
// Authorization header is not valid.
return Task.FromResult(AuthenticateResult.NoResult());
}
// 3\. Verify is scheme name is Basic. BasicSchemeName is a string
// with value 'Basic'.
if (!BasicSchemeName.Equals(headerValue.Scheme, StringComparison.
OrdinalIgnoreCase))
{
// Authorization header is not Basic.
return Task.FromResult(AuthenticateResult.NoResult());
}
// 4\. Fetch email and password from header.
// If length is not 2, then authentication fails.
byte[] headerValueBytes = Convert.FromBase64String(headerValue.
Parameter);
string emailPassword = Encoding.UTF8.GetString(headerValueBytes);
string[] parts = emailPassword.Split(':');
if (parts.Length != 2)
{
return Task.FromResult(AuthenticateResult.Fail("Invalid Basic
Authentication Header"));
}
string email = parts[0];
string password = parts[1];
// 5\. Validate if email and password are correct.
var customer = _context.Customers.SingleOrDefault(x =>
x.Email == email && x.Password == password);
if (customer == null)
{
return Task.FromResult(AuthenticateResult.Fail("Invalid email
and password."));
}
// 6\. Create claims with email and id.
var claims = new[]
{
new Claim(ClaimTypes.Name, email),
new Claim(ClaimTypes.NameIdentifier, customer.Id.ToString())
};
// 7\. ClaimsIdentity creation with claims.
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
代码非常容易理解。 我在代码块上添加了步骤。 基本上,我们试图验证头,然后处理它,看看它们是否正确。 如果正确,则创建ClaimsIdentity对象,该对象可以在应用中进一步使用。 我们将在下一节中进行。 在每个步骤中,如果验证失败,我们将发送AuthenticateResult.NoResult()或AuthenticateResult.Fail()。
让我们将这个身份验证附加到 action 方法上,使用如下方法:
[HttpGet("{id}")]
[Authorize(AuthenticationSchemes = "Basic")]
public async Task<IActionResult> GetCustomers([FromRoute] Guid id)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var ident = User.Identity as ClaimsIdentity;
var currentLoggeedInUserId = ident.Claims.FirstOrDefault
(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
if (currentLoggeedInUserId != id.ToString())
{
// Not Authorized
return BadRequest("You are not authorized!");
}
var customers = await _context.Customers.SingleOrDefaultAsync
(m => m.Id == id);
if (customers == null)
{
return NotFound();
}
return Ok(customers);
}
步骤 3 -在启动时注册基本认证
看起来一切都设置好了,但是,我们在启动时漏掉了注册这个基本身份验证的步骤。 否则,如何调用BasicAuthenticationHandler处理程序? 看一看:
services.AddAuthentication("Basic")
.AddScheme<BasicAuthenticationOptions, BasicAuthenticationHandler>("Basic", null);
services.AddTransient<IAuthenticationHandler, BasicAuthenticationHandler>();
要测试 API,您可以设计一个 HTML 页面,通过使用Id从 API 获取详细信息来显示用户的概要文件。 你可以使用jQuery Ajax调用 API 并对接收到的结果进行操作:
$.ajax({
url: 'http://localhost:57571/api/Customers/
910D4C2F-B394-4578-8D9C-7CA3FD3266E2',
type: "GET",
contentType: "application/json",
dataType: "json",
headers: { 'Authorization': 'Basic ' + btoa
(email + ':' + password)},
success: function (result) {
// Work with result. Code removed for brevity.
},
error: function (err)
{
if (err.status == 401)
{
alert("Either wrong email and password or you are
not authorized to access the data!")
}
}
});
注意头文件部分,其中提到了Authorization头文件,其中email和password用冒号(:)分隔,并传递给了btoa方法,该方法负责 Base64 加密。 在你得到结果后,你可以做n许多事情。 下面的截图显示了它在页面上使用一些 bootstrap 设计:

现在,有一个重要的代码块应该包含在前面的处理程序代码中。 这是另一个应该被覆盖的HandleChallengeAsync方法。 这种方法的目的是处理身份验证失败的情况。
我们将只发送一个名为WWW-Authenticate的响应头,其值可以用realm设置。 先看看代码,然后我将解释:
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.Headers["WWW-Authenticate"] = $"Basic
realm=\"http://localhost:57571\", charset=\"UTF-8\"";
await base.HandleChallengeAsync(properties);
}
If a client tries to access a restricted resource or a resource that requires authentication, it's the server's responsibility to inform the client about the authentication type and related information. The WWW-Authenticate HTTP response header is set by the server that defines the authentication method that should be used to gain access to the restricted resource requested.
因此,很明显,WWW-Authenticate头与 401 Unauthorized 响应一起发送。 该字符串包含三个内容:身份验证类型、领域和字符。 域是身份验证将在其中有效的域或区域。
在我们的案例中,方案是Basic,Realm 是http://localhost:57571,Charset 是UTF-8。 因此,如果客户机提供的基本身份验证参数为Username和Password,那么这些参数在localhost:57571域中是有效的。
这就是它的意义。 因此,只需删除分配代码的头文件或将其注释出来以进行测试。 以下是 Chrome 开发工具的网络选项卡截图:

下面是一条警告消息的截图,我们在 Ajax 调用的错误方法中提供了它。 当 API 动作在没有任何凭据的情况下被调用时,就会发生这种情况:

向我们的服务添加 OAuth 2.0 身份验证
OAuth 是一个开放标准,api 使用它来控制客户端(如网站、桌面应用,甚至其他 api)对资源的访问。 然而,实现 OAuth 的 API 可以在不与第三方应用共享密码的情况下提供用户信息。
你一定见过一些网站,他们允许使用不同的服务登录,如 Facebook, Twitter,或谷歌,说的东西,如(针对 Facebook)登录 Facebook。 这意味着 Facebook 有一个 OAuth 服务器,它会通过你之前提供给 Facebook 的某个身份验证你的应用,并给你一个有一定有效性的访问令牌。 使用该令牌,您可以读取所需用户的配置文件。
以下是一些基本的 OAuth2.0 术语:
- **资源:**我们已经在前面的章节中定义了这个。 资源是我们需要保护的东西。 这可能是任何与我们系统有关的信息。
- **资源服务器:**该服务器将保护资源,主要是我们为访问电子商务数据库而设计的 API。
- **资源所有者:**授予我们访问特定资源的人。 大多数用户都是所有者,正如你所看到的,当你点击登录 Facebook 时,它会征求你的登录和同意。
- **客户端:**需要我们访问资源的应用。 在我们的例子中,当jQuery代码在我们设计的 HTML 页面上执行时,是浏览器试图访问资源。
- **访问令牌:**这实际上是这个体系结构的支柱。 我们将要设计的 OAuth 服务器应该提供一个使用用户凭据的令牌,以便随后访问我们的资源,因为我们知道 OAuth 标准告诉我们不要向客户端提供密码。
- **不记名令牌:**这是一种特殊类型的访问令牌,允许任何人轻松地使用令牌,这意味着,为了使用令牌访问资源,客户端不需要加密密钥或其他秘密密钥。 由于这比其他类型的令牌安全性差,无记名令牌只能在 HTTPs 上使用,并且应该在很短的时间内过期。
- **授权服务器:**向客户端提供访问令牌的服务器。
让我们开始将 OAuth 添加到 Web API 中。 我们将使用 IdentityServer4,它是一个免费的、开源的 OpenID 连接和 OAuth 2.0 框架。 净的核心。 项目可以在这里找到:https://github.com/IdentityServer。
IdentityServer(http://identityserver.io/)基于 OWIN/Katana,但据我们所知,它以 NuGet 包的形式发布和可用。 为了启动 IdentityServer,需要安装以下两个 NuGet 包:

The authorization server in a production scenario is ideally isolated from the main web API. But for this book, we will directly put that in the same Web API project for simplicity. We are not using the default ASP.NET Core Identity. We will be using our own set of tables. For instance, we will use our customer table details for verification.
步骤 1 -设计 Config 类
Config类包含授权服务器的重要细节,例如资源、客户端和用户。 在生成令牌时使用这些细节。 让我们设计:
public class Config
{
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource
(
"FlixOneStore.ReadAccess",
"FlixOneStore API",
new List<string>
{
JwtClaimTypes.Id,
JwtClaimTypes.Email,
JwtClaimTypes.Name,
JwtClaimTypes.GivenName,
JwtClaimTypes.FamilyName
}
),
new ApiResource("FlixOneStore.FullAccess", "FlixOneStore API")
};
}
}
ApiResource用于声明 API 的不同作用域和声明。 对于简单的情况,一个 API 可能有一个简单的资源结构,在这个结构中它可以访问所有的客户机。 然而,在典型的场景中,客户机可以被限制访问 API 的不同部分。 在声明客户机时,我们将使用这些资源来配置它们的范围和访问权限。 ReadAccess和FullAccess是两种不同的资源类型,客户机可以使用它们分别提供读访问和完全访问。
基本上,我们现在正在设计的方法将在Startup上调用。 这里,GetApiResources实际上是使用不同的设置创建两种类型的资源。 第一个是我们现在要处理的。 我们把它命名为FlixOneStore.ReadAccess。 您可以看到包含Id、Name等字符的字符串列表,这些是将用令牌生成并传递给客户的客户的详细信息。
让我们添加一个客户端的细节,我们将从那里使用授权服务器:
public static IEnumerable<Client> GetClients()
{
return new[]
{
new Client
{
Enabled = true,
ClientName = "HTML Page Client",
ClientId = "htmlClient",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secretpassword".Sha256())
},
AllowedScopes = { "FlixOneStore.ReadAccess" }
}
};
}
您可以根据需要添加许多客户机。 在该方法中,可以根据 OAuth 标准设置客户端 id、客户端 secret、、授权类型。 注意,密码设置为secretpassword。 你可以在这里设置任何字符串; 它可以是一个Guid。 这里,GrantType.ResourceOwnerPassword定义了验证传入请求以生成令牌的方式。
它对授权服务器说:“Hey 在请求体中查找username和password。” 还有其他类型的奖助金。 您可以通过官方文档链接了解更多信息。
你现在可能有问题了! 我们将用username和password做什么? 当然,我们会验证它们,但用什么呢? 答案是Customers表中的Email和Password字段。 我们没有做任何与连接授权服务器与Customers*表相关的事情。 这就是我们接下来要做的。 但在此之前,让我们在Startup中注册这些设置。
Just to make sure we are on the same page, we landed at the point where we are trying to generate a token from the Authorization Server in order to access our API.
步骤 2 -启动时注册 Config
对于注册,以下是我们在ConfigureServices方法里面要做的:
services.AddIdentityServer()
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddProfileService<ProfileService>()
.AddDeveloperSigningCredential();
我们在这里通过调用我们设计的方法来加载所有的配置设置,例如资源和客户端。 AddDeveloperSigningCredential在启动时添加一个临时密钥,仅在开发环境中,当我们没有任何证书来申请授权时使用。 您将为实际使用添加适当的证书细节。
在这里标记ProfileService。 这就是我在前一节中讨论的内容,它将用于根据数据库验证用户凭据。 我们一会儿会讲到。 首先,让我们测试 API,假设授权服务器已经设置好ProfileService。
现在来看看 API,我们需要在 API 的开头添加AuthenticationScheme来声明我们将使用什么身份验证。 为此,添加以下代码:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme =
JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme =
JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.Authority = "http://localhost:57571";
o.Audience = "FlixOneStore.ReadAccess";
o.RequireHttpsMetadata = false;
});
JwtBearerDefaults.AuthenticationScheme实际上是一个具有持钵值的字符串常量。 承载认证又称为令牌认证。 这意味着我们的客户端需要发送一个令牌来访问 API 的资源。 为了获得令牌,他们需要调用我们的授权服务器,该服务器在/connect/token可用。
注意,我们将Audience设置为FlixOneStore.ReadAccess,这是我们为客户端在内部配置中指定的。 简单地说,我们正在设置承载类型的身份验证。
步骤 3 -添加[Authorize]属性
下一步是将[Authorize]属性添加到 API 控制器操作中。 让我们用GetCustomers(id)方法来测试一下:
// GET: api/Customers/5
[HttpGet("{id}")]
[Authorize]
public async Task<IActionResult> GetCustomers([FromRoute] Guid id)
从Postman调用它会产生以下输出:

所以,我们的请求不再被批准了。 我们收到一个回复,说我们需要发送一个令牌来访问资源。 那我们去拿代币吧。
步骤 4 -获取令牌
为了获得令牌,我们需要调用位于/connect/token的授权服务器。
以下是来自Postman的屏幕,其中在http://localhost:57571/connect/tokenURL 上执行一个POST请求,请求体包含了验证客户机所需的所有参数。 以下是我们在步骤 1中的GetClients()方法中注册的细节:

哦! 这是一个糟糕的请求。 这是因为我们给客户传递了错误的秘密密码。 如果您还记得,我们将其设置为secretpassword,但将其作为secret传递。 这就是为什么它被拒绝了。
这里需要注意一些重要的事情。 要获取令牌:
- 我们需要向
/connect/tokenURL 发送一个POST请求。 因为我们在同一个应用中实现了服务器,所以这里的域与 API 相同。 - 我们需要将
Content-Type标题设置为application/x-www-form-urlencoded(这实际上是在屏幕截图的不同标签中)。 - 在请求的主体中,我们根据标准添加了 OAuth 所需的所有参数,它们与我们在配置类中拥有的完全匹配。
当我们按照要求正确发送一切时,我们将收到一个令牌,如下截图所示:

我们根据 OAuth 规范收到了不记名令牌响应。 它们是access_token,expires_in和token_type。 expires_in参数默认设置为 3600,单位为秒,即 1 小时。 1 小时后,此令牌将不再工作。 因此,在这个令牌到期之前,让我们用它快速调用我们的 API,看看它是否工作。
第 5 步-使用访问令牌调用 API
看看下面的图片,它显示了使用我们刚刚收到的令牌对 API 的调用:

Calling the API endpoint with the token in the authorization header
瞧! 这工作。 我只是复制了我得到的令牌,并以Bearer [Access Token]格式添加到授权头中,并发送了请求。 现在一切都很完美。
步骤 6 -添加 ProfileService 类
在我们讨论这一切的时候,我遗漏了我现在想解释的一部分。 当我们获取访问令牌时,如果你看到请求体,它是这样的:
grant_type=password&scope=FlixOneStore.ReadAccess&client_id=htmlClient&client_secret=secretpassword&username=taditdash@gmail.com&password=12345
关注username和password参数。 他们在这里是有原因的。 在生成令牌时,会对它们进行验证,是的,我们直接使用数据库进行验证。 让我们来看看。
IdentityServer4为此提供了两个接口,分别为IProfileService和IResourceOwnerPasswordValidator。
下面是实现该接口的ResourceOwnerPasswordValidator类。 记住,我们在客户端的配置中设置了AllowedGrantTypes = GrantTypes.ResourceOwnerPassword。 这就是为什么我们要这样做来验证用户的凭证:
public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
private readonly FlixOneStoreContext _context;
public ResourceOwnerPasswordValidator(FlixOneStoreContext context)
{
_context = context;
}
public async Task ValidateAsync(ResourceOwnerPassword
ValidationContext context)
{
try
{
var customer = await _context.Customers.SingleOrDefaultAsync
(m => m.Email == context.UserName);
if (customer != null)
{
if (customer.Password == context.Password)
{
context.Result = new GrantValidationResult(
subject: customer.Id.ToString(),
authenticationMethod: "database",
claims: GetUserClaims(customer));
return;
}
context.Result = new GrantValidationResult
(TokenRequestErrors.InvalidGrant,
"Incorrect password");
return;
}
context.Result = new GrantValidationResult
(TokenRequestErrors.InvalidGrant,
"User does not exist.");
return;
}
catch (Exception ex)
{
context.Result = new GrantValidationResult
(TokenRequestErrors.InvalidGrant,
"Invalid username or password");
}
}
public static Claim[] GetUserClaims(Customers customer)
{
return new Claim[]
{
new Claim(JwtClaimTypes.Id, customer.Id.ToString() ?? ""),
new Claim(JwtClaimTypes.Name, (
!string.IsNullOrEmpty(customer.Firstname) &&
!string.IsNullOrEmpty(customer.Lastname))
? (customer.Firstname + " " + customer.Lastname)
: String.Empty),
new Claim(JwtClaimTypes.GivenName, customer.Firstname ??
string.Empty),
new Claim(JwtClaimTypes.FamilyName, customer.Lastname ??
string.Empty),
new Claim(JwtClaimTypes.Email, customer.Email ?? string.Empty)
};
}
}
在前面的代码中标记粗体行。 ValidateAsync是向我们提供请求细节的方法,然后用数据库值对其进行验证。 如果匹配,则创建一个带有subject、authenticationMethod和claims的GrantValidationResult对象。
GetUserClaims帮助我们建立所有的索赔。 我们马上就会看到这些主张的实际用途。
We added a number of claims inside config with a list of ApiResources, such as Id, Name, Email, GivenName, and FamilyName. That means the server can return these details about the customer.
让我们跳到ProfileService:
public class ProfileService : IProfileService
{
private readonly FlixOneStoreContext _context;
public ProfileService(FlixOneStoreContext context)
{
_context = context;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext
profileContext)
{
if (!string.IsNullOrEmpty(profileContext.Subject.Identity.Name))
{
var customer = await _context.Customers
.SingleOrDefaultAsync(m => m.Email ==
profileContext.Subject.Identity.Name);
if (customer != null)
{
var claims = ResourceOwnerPasswordValidator.
GetUserClaims(customer);
profileContext.IssuedClaims = claims.Where(x =>
profileContext.RequestedClaimTypes.Contains(x.Type)).ToList();
}
}
else
{
var customerId = profileContext.Subject.Claims.FirstOrDefault
(x => x.Type == "sub");
if (!string.IsNullOrEmpty(customerId.Value))
{
var customer = await _context.Customers
.SingleOrDefaultAsync(u => u.Id ==
Guid.Parse(customerId.Value));
if (customer != null)
{
var claims =
ResourceOwnerPasswordValidator.GetUserClaims(customer);
profileContext.IssuedClaims = claims.Where(x =>
profileContext.RequestedClaimTypes.Contains(x.Type)).
ToList();
}
}
}
}
}
一个ProfileDataRequestContext对象中填充了我们在ApiResource中添加的所有声明。 请参考下面的屏幕截图,这是调试时请求的索赔列表:

这意味着我们需要从我们所做的客户记录中填充所有这些细节,并将其添加到IssuedClaims。
等一下! 我们为什么要这么做? 因为我们的配置告诉我们提供这个信息。 但是我们需要填写所有需要的信息吗? 不。 不一定。 我们可以根据自己的意愿提出多少索赔。
现在的大问题! 我们在哪里找到这些信息? 我们知道,在所有这些授权设置之后,我们得到了一个加密的令牌字符串。 你猜吗? 是的,所有这些信息实际上都驻留在令牌本身中。 不要相信我,相信下面的截图。 由于该令牌是一个 JWT 令牌,您可以使用https://jwt.io/来解码它,并查看里面是什么:

基于客户机的 API-consumption 架构
我们已经讨论了RESTful 服务、Web api 以及如何注册、认证和授权用户。 此外,我们确实稍微关注了服务的消费方面。 服务不仅设计为在Postman上测试,而且实际上是为不同类型的应用(桌面、web、移动、智能手表和物联网应用)消费。
虽然大多数现代应用都是基于基于 mvc 的架构,但在这些应用的控制器中仍然需要使用 web 服务。 基本上,我需要找到一种方法来毫无麻烦地从控制器调用服务。
要做到这一点,我不能调用Postman或任何其他第三方工具。 我需要的是能够与RESTful Web API交互的客户端或组件。 我只需要告诉客户,我需要*客户通过id*细节或一些标识符和其他由客户照顾,从调用 API,传递价值,并得到响应。 响应最终返回到控制器,然后我可以对其进行操作。
我们将在第 10 章、*构建 Web 客户端(消费 Web 服务)*中探讨如何用简单、快速、简单的步骤构建 REST 客户端。
总结
注册是一个非常常见,但非常重要的部分应用。 我们通过 API 为客户办理注册。 在此之前,我们学习了如何使用 EF Core 引导 API 控制器操作和建模类。 当我们做这些的时候,我们降落在 CORS 上,也学会了如何处理它。
渐渐地,我们转到身份验证部分,在那里我们详细讨论了基本身份验证。 它是一种通过客户(他们是我们 API 的用户)凭据(username和password)验证客户的机制,这些凭据随请求一起传递进来。
无记名或基于令牌的身份验证是我们研究的下一个主题,我们使用IdentityServer4实现了 OAuth 范例。 在这种情况下,客户端不能像在 basic 情况下那样通过username和password直接访问资源。 它首先需要的是一个令牌,它是由一个授权服务器根据客户机的请求生成的,客户机详细信息如客户机 id和客户机秘密。 然后,可以将令牌发送给 API,用于后续的受限资源访问请求。
在下一章,我们将这些知识来构建其他组件的 API,如购物车*、,订单项,付款。***
四、商品目录,购物车和结帐
本章将介绍电子商务应用的主要部分及其相关 API 端点的编码。
我们已经在前一章中讨论了用户注册和身份验证,我们将继承这些知识来帮助我们在本章中构建的不同控制器中实现安全性。
为了有效的展示产品和搜索产品,我们还将设计ProductsController。
之后,我们还将研究如何将产品放入购物车,讨论如何添加、更新和删除购物车中的商品。
最后,但并非最不重要的是,我们还将查看订单管理和处理。
在本章中,我们将涵盖以下主题:
- 实现不同的控制器
- 产品列表和产品搜索
- 添加、更新和删除购物车项目
- 对控制器施加安全性
- 订单处理和运输信息
实现控制器
由于我们将了解应用的核心功能,我们需要设计它的控制器,以便我们有 REST 端点来执行来自客户机的任务。 诸如产品清单*,产品搜索、加入购物车,订单,【显示】处理出货*可以用一个专用的控制器为每个函数。 这些控制器将负责在数据库上执行操作,因此我们需要为相关表建模类。 我们开始工作吧!**
**# 生成模型
下面这行代码可以在 Package Manager Console 中执行,为数据库中的所有表生成模型类:
Scaffold-DbContext "Server=.;Database=FlixOneStore;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -Force
前面的命令将填充Models文件夹中每个表的类文件,如下面的截图所示:

If you have not already done so, please refer to the database script in https://github.com/PacktPublishing/Building-RESTful-Web-services-with-DOTNET-Core to generate the database table for your application.
生成控制器
要为模型生成控制器,右键单击Controllers文件夹|添加|控制器| API 控制器与动作,使用实体框架。
首先,让我们从ProductsdetailsController开始,因为我们想首先向客户展示产品列表。
通过 scaffolding 生成的Productsdetail.cs模型类应该如下所示:
public partial class Productsdetail
{
public Guid Id { get; set; }
public Guid? Productid { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Url { get; set; }
public int Views { get; set; }
public Products Product { get; set; }
}
前面的代码还可以用于生成带有GET、POST、PUT和DELETE动作方法的控制器:(我们现在关注的是GetProductsdetail方法)。
// GET: api/Productsdetails
[HttpGet]
public IEnumerable<Productsdetail> GetProductsdetail()
{
return _context.Productsdetail;
}
您可以使用 Postman 快速测试您的控制器是否正在工作,如下图所示:

这里,URL 为http://localhost:57571/api/Productsdetails,类型为GET。 我们可以在结果框中看到结果,它以 JSON 格式向我们显示了产品细节的数组。 注意,我们通过在请求的 header 选项卡中使用application/json值设置contentType报头来发送此请求。
产品清单
现在让我们设计使用该端点所需的 jQuery 代码,以便在 web 页面上显示这些记录并列出可购买的产品。 应该如下所示:
function LoadProducts()
{
// Load products' details.
$.ajax({
url: 'http://localhost:57571/api/Productsdetails',
type: "GET",
contentType: "application/json",
dataType: "json",
success: function (result) {
$.each(result, function (index, value) {
$('#tblProducts')
.append('<tr><td>' +
'<h3>' + value.name + '</h3>' +
'<p>' + value.description + '</p>' +
'<a target="_blank" href=' + value.url + '>Amazon Link</a>' +
'<input type="button" style="float:right;"
class="btn btn-success" value="Add To Cart" />' +
'</td></tr>');
});
}
});
}
To get the code to call the API in different languages, you can click on the code link inside Postman and then select the desired language. We have already discussed this in previous chapters.
前面的方法调用端点http://localhost:57571/api/Productsdetails,并在用success方法接收记录后循环遍历记录。 在循环时,它构建一个 HTML 表行,将其附加到页面上已存在的表。
下面的截图是 jQuery 代码的反映,它显示了所有产品的细节:

注意,我们在顶部有一个搜索框,以及每个产品的“添加到购物车”按钮。 我们将在稍后讨论这些功能。
你注意到产品的主要参数,价格,没有显示吗? 这是因为价格没有出现在Productdetail表中。 那么,现在让我们来看看Product.cs模型类,如下所示:
public partial class Products
{
public Products()
{
Cart = new HashSet<Cart>();
CartAttributes = new HashSet<CartAttributes>();
OrdersProducts = new HashSet<OrdersProducts>();
ProductsAttributes = new HashSet<ProductsAttributes>();
Productsdetail = new HashSet<Productsdetail>();
Reviews = new HashSet<Reviews>();
}
public Guid Id { get; set; }
public int Qty { get; set; }
public string Model { get; set; }
public string Image { get; set; }
public decimal Price { get; set; }
public DateTime Addedon { get; set; }
public DateTime Modifiedon { get; set; }
public decimal Weight { get; set; }
public byte Status { get; set; }
public Guid? ManufactureId { get; set; }
public Guid? Taxclassid { get; set; }
public ICollection<Cart> Cart { get; set; }
public ICollection<CartAttributes> CartAttributes { get; set; }
public ICollection<OrdersProducts> OrdersProducts { get; set; }
public ICollection<ProductsAttributes> ProductsAttributes
{ get; set; }
public ICollection<Productsdetail> Productsdetail { get; set; }
public ICollection<Reviews> Reviews { get; set; }
}
显然,Product类包含了我们需要的所有内容,包括Name、Description、Url、Views等等,以Productdetail为参考点。 我们已经消费了ProductdetailsController的GET动作来展示我们的产品,现在是时候使用ProductsController阅读我们所有的产品了。
ProductsController的GET操作将返回Productdetail中所有的产品记录,如下:
// GET: api/Products
[HttpGet]
public IEnumerable<Products> GetProducts()
{
return _context.Products.Include(x => x.Productsdetail).ToList();
}
前面代码中的粗体代码片段是Include子句,它用于包含Productdetail的结果。 现在,我们不再调用/api/Productsdetails,而是调用/api/Products。
Calling this endpoint won't actually work, this is because of circular reference. If you observe both the Products and Productdetail models closely, you should see that both contain references to each other. This creates a problem when parsing to JSON. To avoid this, we need to write the following code inside Startup:
services.AddMvc()
.AddJsonOptions(options =>
{
options.SerializerSettings.ReferenceLoopHandling =
ReferenceLoopHandling.Ignore;
});
现在让我们看看在调用该端点时收到的产品响应,如下面的代码片段所示。 注意,实际上你会得到一个数组,但为了简洁起见,我们只向你显示一条记录:
{
"id": "98a95bb6-c573-450d-a470-0a637e126dd7",
"qty": 30,
"model": "A",
"image": "NA",
"price": 49.99,
"addedon": "2018-05-13T12:09:39.873",
"modifiedon": "2018-05-13T12:09:39.873",
"weight": 0.9,
"status": 1,
"manufactureId": null,
"taxclassid": null,
"cart": [],
"cartAttributes": [],
"ordersProducts": [],
"productsAttributes": [],
"productsdetail": [
{
"id": "c96ac991-6581-4675-b00c-439df3961f03",
"productid": "98a95bb6-c573-450d-a470-0a637e126dd7",
"name": "Dependency Injection in .NET Core 2.0",
"description": "Make use of constructors, parameters,
setters, and interface injection to write reusable and
loosely-coupled code",
"url": "https://www.amazon.com/Dependency-Injection-NET-Core-
loosely-coupled/dp/1787121305/ref=tmm_pap_swatch_0?
_encoding=UTF8&qid=1510939068&sr=8-3",
"views": 5000
}],
"reviews": []
}
现在,我们需要修改客户端代码,以反映Productdetail现在在Product对象内的事实,如下所示:
function LoadProducts()
{
// Load products' details.
$.ajax({
url: 'http://localhost:57571/api/Products',
type: "GET",
contentType: "application/json",
dataType: "json",
success: function (result) {
console.log(result);
$.each(result, function (index, value) {
$('#tblProducts')
.append('<tr><td>' +
'<h3>' + value.productsdetail[0].name + '</h3>' +
'<span class="spanPrice">Price: $' + value.price +
'</span>' +
'<p>' + value.productsdetail[0].description + '</p>' +
'<a target="_blank" href=' + value.productsdetail[0].url +
'>Amazon Link</a>' +
'<input type="button" style="float:right;" class="btn btn-
success" value="Add To Cart" />' +
'</td></tr>');
});
}
});
}
这很容易理解,不是吗? 在这里,您应该注意到我们对 URL 所做的更改,以及我们如何读取产品详细信息。 Productsdetail作为数组在Product对象中,因此它被写成value.productsdetail[0],其中value是产品对象。 我们还介绍了value.price。
你现在应该看到下面的截图,它已经更新了:

产品搜索
现在是时候实现搜索功能了,允许客户在搜索框中放入任何字符串来查找产品。 我们需要向 UI 添加一个搜索按钮,单击该按钮将接收输入的字符串并相应地获取记录。
首先,action方法需要接受客户输入的搜索文本作为参数; 目前GetProducts()不接受任何参数。
更新后的GetProducts()应该如下所示:
// GET: api/Products
[HttpGet]
public IEnumerable<Products> GetProducts(string searchText)
{
var products = _context.Products.Include(x =>
x.Productsdetail).ToList();
if (!string.IsNullOrEmpty(searchText))
products = products.Where(p => p.Productsdetail
.Any(pd => pd.Name.ToLower().Contains(searchText.ToLower())))
.ToList();
return products;
}
将考虑searchText参数,并通过位于Product对象内Productsdetail集合的Name字段中的图书标题过滤结果。 因此,Any用于检查searchText是否存在于Productsdetail对象中。
现在 API 已经准备好搜索了,让我们更新客户端代码,以发送如下参数:
function LoadProducts(searchText)
{
if (!searchText)
searchText = "";
// Load products' details.
$.ajax({
url: 'http://localhost:57571/api/Products?searchText=' +
searchText,
type: "GET",
// Other codes removed for brevity.
正如您在前面的代码片段中看到的,LoadProducts现在接受作为 URL 参数传递给 API 的searchText参数。 现在只需在调用时将参数值发送给该方法。
下面的代码演示了搜索函数,它获取文本并使用输入的值执行LoadProducts:
$('#btnSearch').click(function ()
{
var searchText = $('#txtSearch').val().trim();
if (searchText)
{
$('#tblProducts').empty();
LoadProducts(searchText);
}
});
下面的屏幕截图显示了这个功能的作用:

添加到购物车
我们现在准备好下一个重要的主题:所有关于添加到购物车! 然而,在实现这个函数之前,有一些事情值得注意。 在我们的应用中,我们不允许未知的用户添加到购物车,因为我们将在数据库中存储与购物车相关的任何信息。
实现安全
这就是安全性的用武之地,即身份验证。 如第 3 章、用户注册与管理中所述,基本身份验证可以在处理程序的帮助下应用,也可以在令牌的帮助下应用持有者身份验证。
首先,让我们使用与前面相同的步骤生成CartsController。 现在我们需要将[Authorize]属性直接应用到控制器,这样购物车中的所有操作都可以被验证。 我们的应用已经设置为处理无记名身份验证。
以下是CartsController的代码快照:
[Produces("application/json")]
[Route("api/Carts")]
[Authorize]
public class CartsController : Controller
由于[Authorize]属性,如果不提供访问令牌,该控制器将不允许您访问GET、POST、PUT和DELETE操作方法。
让我们开始在客户端设计一些与购物车相关的函数,并尝试调用该控制器中的动作方法,如下图所示:

客户端 AddToCart 函数
当客户点击 Add To Cart 时,信息被添加到另一个名为 My Cart 的 HTML 表中。 如果你为一个产品点击两次“添加到购物车”,它的数量将更新为 2,并相应地计算价格。 每次点击都假设一个特定产品的一个单元。
现在让我们深入研究代码。 下面的代码片段向我们展示了 JavaScript 的AddToCart函数:
function AddToCart(productId, productName, qty, price)
{
$('#tblCart tbody')
.append($('<tr>')
.attr('data-product-id', productId)
.append($('<td>').html(productName))
.append($('<td class="qty">').html(qty))
.append($('<td class="price">').html('$' + qty * price))
.append($('<td>')
.append($('<a>')
.attr('href', '#')
.append($('<span>').addClass('glyphicon glyphicon-trash'))
// For Delete Icon.
.click(function ()
{
// Delete Cart from Database.
})
)
)
);
// Add one Cart record in Database.
}
该函数接受参数productId、productName、qty和price,这些信息显示在 cart HTML 表中。
注意,在上面的图像中,每一行都有一个删除图标。 这是通过在包含锚的 span 中添加glyphicon而产生的,锚的click事件也已定义。 我们将在本章后面讨论删除功能。
另外,请注意已添加到该行的data-product-id属性。 这帮助我们唯一地识别一个购物车行,稍后您将看到这是如何帮助的。
现在我们已经准备好调用CartsController将购物车详细信息插入到数据库中。 然而,在这个方法中我们还需要一个东西。 毕竟,如果产品意外地被添加到购物车中会发生什么呢?
在这里,我们需要更新购物车,而不是仅仅向它添加产品,如下面的代码片段所示:
function AddToCart(productId, productName, qty, price)
{
// Check if item already present. If yes, increase the qty
and calculate price.
var cartItem = $('#tblCart').find('tr[data-product-id=' +
productId + ']');
if (cartItem.length > 0)
{
var qtyTd = cartItem.find('td.qty');
var newQty = parseInt(qtyTd.html()) + qty;
qtyTd.html(newQty);
cartItem.find('td.price').html('$' + (newQty * price).toFixed(2));
// Update Cart in Database: PUT /api/Carts/{id}
return;
}
$('#tblCart tbody')
.append($('<tr>')
.attr('data-product-id', productId)
.append($('<td>').html(productName))
.append($('<td class="qty">').html(qty))
.append($('<td class="price">').html('$' + qty * price))
.append($('<td>')
.append($('<a>')
.attr('href', '#')
.append($('<span>').addClass('glyphicon glyphicon-trash'))
.click(function () {
// Delete Cart from Database: DELETE /api/Carts/{id}
})
)
)
);
// Add one Cart record in Database: POST /api/Carts
}
很简单,不是吗? 首先,在产品 ID 的帮助下从购物车表中获取记录,然后相应地更新其数量和价格。 从该块开始,一个return语句确保该记录不再添加到表中。
现在,一切都应该在客户端工作。 现在,我们只需要调用 api 来涉及数据库操作,以便插入、更新和删除的任何行也会在服务器端更新。
API 调用 AddToCart
在本节中,我们将查看客户机进行的实际 API 调用。
POST – api/Carts
将数据插入购物车表可以通过调用POST操作来完成,该操作应该类似于CartsController中的代码块:
// POST: api/Carts
[HttpPost]
public async Task<IActionResult> PostCart([FromBody] Cart cart)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
_context.Cart.Add(cart);
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException)
{
if (CartExists(cart.Id))
{
return new StatusCodeResult(StatusCodes.Status409Conflict);
}
else
{
throw;
}
}
return CreatedAtAction("GetCart", new { id = cart.Id }, cart);
}
调用此操作的客户端函数可以设计如下:
function PostCart(customerId, productId, qty, finalPrice)
{
var cart =
{
Customerid: customerId,
Productid: productId,
Qty: qty,
Finalprice: finalPrice
};
$.ajax({
url: 'http://localhost:57571/api/Carts',
type: "POST",
contentType: "application/json",
dataType: "json",
data: JSON.stringify(cart),
success: function (result) {
console.log(result);
},
error: function (message) {
console.log(message.statusText);
}
});
}
简单,不是吗? 现在我们可以构建一个 cart 对象并向它发送一个POST操作。 你可以通过在我们的AddToCart()函数中调用下面的方法来尝试:
// Add one Cart record in Database: POST /api/Carts
PostCart('910D4C2F-B394-4578-8D9C-7CA3FD3266E2',
productId,
cartItem.find('td.qty').html(),
cartItem.find('td.price').html().replace('$', ''))
这里,第一个参数是Customerid,我们对其进行了硬编码。 Customerid可以为任何请求保存在会话存储中——尽管这被认为是一种有风险的做法。
Instead of sending the Customerid, you can send the email id to the POST action. Then using email id, you can get the Customerid, which can be used to insert a Cart record.
现在让我们运行我们的应用,并为特定的产品点击添加到购物车。 哦! 在开发工具中出现以下错误:

为什么会这样?
这个错误的原因其实很明显。 由于已将[Authorize]属性应用于控制器,现在对CartsController的每个调用都期望通过请求Email Id和Password生成 OAuth2.0 Authorize 服务器的令牌。
We already explored OAuth2.0 Authentication in detail.
为了继续我们的实现,我们将从 Postman 调用令牌服务器,并在我们的应用中使用它。理想情况下,当你收到一个未授权的错误时,你应该打开登录屏幕,以便用户可以登录。 如果验证了Email Id和Password,将返回一个令牌。 此令牌可用于进一步的请求,如 Add To Cart。
为了节省时间和空间,我们将直接使用 Postman 生成一个令牌,使用taditdash@gmail.com作为我们的电子邮件 ID,12345作为我们的密码*。 使用令牌的后续 Ajax 调用应该如下所示:*
$.ajax({
url: 'http://localhost:57571/api/Carts',
type: "POST",
contentType: "application/json",
dataType: "json",
data: JSON.stringify(cart),
headers: { "Authorization": "Bearer eyJhbGciOiJSUzI1NiIs...
[Long String Removed]" },
success: function (result)
{
var cartItem = $('#tblCart').find('tr[data-product-id=' +
productId + ']');
cartItem.attr('data-cart-id', result.id);
},
});
注意,为了简洁起见,我们在前面的代码片段中删除了标记字符串。 使用前面的代码,将创建一个 cart 记录,并且从 API 返回的数据将为我们提供关于该记录的所有细节。 您可以将购物车的Id存储在 HTML 行中(如前面代码块中所示),以便在更新或删除购物车记录时进行进一步处理。
下面的截图的元素标签在 Chrome 开发工具中说明了购物车记录的 ID 存储为一个data-cart-id属性:

PUT – api/Carts/
现在我们已经添加了一条购物车记录,接下来让我们在客户重复点击 Add to cart 按钮时更新该记录。 我们已经有了更新客户端表上的数量和价格的代码,所以我们只需要编写调用PUT端点来更新记录的代码,如下:
function PutCart(cartItem)
{
var cart =
{
Id: cartItem.attr('data-cart-id'),
Customerid: '910D4C2F-B394-4578-8D9C-7CA3FD3266E2',
Productid: cartItem.attr('data-product-id'),
Qty: cartItem.find('td.qty').html(),
Finalprice: cartItem.find('td.price').html().replace('$', '')
};
$.ajax({
url: 'http://localhost:57571/api/Carts/' + cart.Id,
type: "PUT",
contentType: "application/json",
dataType: "json",
data: JSON.stringify(cart),
headers: { "Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..." }
});
}
前面代码的重要部分是 URL,它还包含购物车Id,因为该路径实际上是api/Carts/{id}。 数据进入体内。
参数cartItem是可以从AddToCart函数传递的行,如下所示:
// Update Cart in Database: PUT /api/Carts/{id}
PutCart($('#tblCart').find('tr[data-product-id=' + productId + ']'));
API 操作应该如下所示:
// PUT: api/Carts/5
[HttpPut("{id}")]
public async Task<IActionResult> PutCart([FromRoute] Guid id, [FromBody] Cart cart)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (id != cart.Id)
{
return BadRequest();
}
_context.Entry(cart).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!CartExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
注意,id是从路由中读取的,因为它被属性[FromRoute]标记,而 cart 对象是从请求体中读取的,因为它被标记为[FromBody]。 如果一个 ID 没有与路由一起发送,客户端将收到一个 400 BadRequest 错误。
API 动作现在已经更新了记录的必要细节,如下图所示:

如你所见,我们已经点击了四次添加到购物车。 finalPrice按49.99 * 4计算。
DELETE – api/Carts/
Route/api/Carts/{id}告诉我们只需要将 cartId发送到 API; 其他一切都将由 API 处理,以便从数据库中删除该记录。
删除记录的操作方法如下:
// DELETE: api/Carts/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteCart([FromRoute] Guid id)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var cart = await _context.Cart.SingleOrDefaultAsync(m => m.Id == id);
if (cart == null)
{
return NotFound();
}
_context.Cart.Remove(cart);
await _context.SaveChangesAsync();
return Ok(cart);
}
客户端应用必须更新以允许此特性。 由于删除图标已经显示在 HTML 表的每个购物车行上,我们只需要在用户单击时将购物车的 ID 发送给 API。
下面的 JavaScript 函数可以用来删除购物车记录:
function DeleteCart(cartId)
{
$.ajax({
url: 'http://localhost:57571/api/Carts/' + cartId,
type: "DELETE",
contentType: "application/json",
headers: { "Authorization": "Bearer " + accessToken },
success: function (result) {
if (result.id) {
// Deleting the row from the html table.
var cartItem = $('#tblCart').find('tr[data-cart-id=' +
cartId + ']');
cartItem.remove();
}
}
});
}
如您所见,DeleteCart函数需要一个参数cartId,该参数将在单击删除图标时提供。 该函数调用类型为DELETE、Id和 URL 的 API。 成功删除后,购物车行将从 HTML 表中删除。
DeleteCart调用的代码块位于AddToCart内部,如下代码片段所示:
cartItem = $('#tblCart tbody')
.append($('<tr>')
.attr('data-product-id', productId)
.append($('<td>').html(productName))
.append($('<td class="qty">').html(qty))
.append($('<td class="price">').html('$' + qty * price))
.append($('<td>')
.append($('<a>')
.attr('href', '#')
.append($('<span>').addClass('glyphicon glyphicon-trash'))
.click(function () {
// Delete Cart from Database: DELETE /api/Carts/{id}
DeleteCart($(this).parents('tr').attr('data-cart-id'));
})
)
)
);
DeleteCart在显示删除图标的锚的单击事件中调用。 在事件内部,我们通过提取data-cart-id属性的值从行本身获得购物车Id。
订单
我们的购物车现在装满了所需产品的正确数量,所以现在是时候下订单了。 为此,我们需要调用另一个控制器——OrdersController。
以下两个表负责订购过程:
- Orders:存储发货地址详细信息、客户详细信息、订单状态等
- OrdersProducts:存储添加到购物车的产品、它们的价格和数量
Orders类是由我们最初所做的 scaffolding 生成的,它包含所有必要的信息。 让我们用这个类来生成控制器。 遵循与生成ProductsController、ProductsdetailsController和CartsController相同的过程来生成控制器。
The model and controller class can be found in the GitHub repository.
现在可以调用OrdersController的POST操作在客户端保存订单。 下面的代码是实现该功能的函数框架:
function PostOrders()
{
// 1\. Build order object to match the model class Orders.cs.
// 2\. Push cart items into order object as an array.
// 3\. Call POST /api/Orders.
}
现在让我们一步一步地解释这个问题。
UI 设计下一个订单
在继续进行下一步之前,我们需要向用户显示一个模态,用户可以在其中输入送货地址。 一旦单击 Place Order 按钮,模式将被打开,如下面的截图所示。

下面的代码片段演示了“Place Order”的点击事件(如果购物车项目出现,模式将被打开):
$('#btnPlaceOrder').click(function ()
{
var cartItems = $('#tblCart tbody tr');
// If Cart items present, then show modal to enter Shipping Address.
if (cartItems.length > 0) {
$('#Order').modal('show');
return;
}
alert("Please add items into the cart.");
});
Ajax 调用通过使用POST单击 Submit 将订单记录插入到数据库中。 下面的代码片段是 Submit 的点击事件:
$('#btnConfirmOrder').click(function () {
PostOrders();
});
客户端 PostOrder 函数
现在让我们来看看PostOrders所需的步骤。
构建 order 对象来匹配模型类 Orders.cs
在这里,我们必须从与运输信息相关的文本框中读取值,并将它们与Orders.cs的字段进行匹配,以构建一个对象。 OrdersProducts是表示模型类OrdersProducts.cs的数组。 每个订单都可以有多个与之相关的产品。
下面的代码实现了 order 对象:
// 1\. Build order object to match the model class Orders.cs.
var order = {
Customerid: customerId,
CustomerStreetaddress: $('#txtStreetAdd').val(),
Customercity: $('#txtCity').val(),
Customerstate: $('#txtState').val(),
Customerpostalcode: $('#txtPostalCode').val(),
Customercountry: $('#txtCountry').val(),
OrdersProducts: new Array()
};
将购物车项作为数组推入 order 对象
填充OrdersProducts数组是下一步,这可以通过循环遍历购物车表的行并将每个购物车行的详细信息推入数组来完成。 在循环中,从该行的属性或td中读取所有必要的值。 记住要形成一个对象,并将值赋给匹配模型类的字段名,如下所示:
// 2\. Push cart items into order object as an array.
$('#tblCart tbody tr').each(function ()
{
order.OrdersProducts.push(
{
Productid: $(this).attr('data-product-id'),
Productname: $(this).find('td.name').html(),
Productprice: $(this).attr('data-price'),
Finalprice: $(this).find('td.price').html().replace('$', ''),
Productqty: $(this).find('td.qty').html()
});
});
调用 POST / api /订单
太好了,我们找到目标了! 现在是时候调用带有POST请求的 API/api/Orders,以便我们的订单进入数据库,如下所示:
// 3\. Call POST /api/Orders.
$.ajax({
url: 'http://localhost:57571/api/Orders',
type: "POST",
contentType: "application/json",
dataType: "json",
data: JSON.stringify(order),
headers: { "Authorization": "Bearer " + accessToken },
success: function (result) {
alert("Order Placed Successfully.")
}
});
如果一切正常,你应该会看到如下截图:

但是我们忘记了一些东西; 虽然我们已经成功下了订单,但是我们需要清空购物车。 这可以通过调用PostOrders的success函数中的每个购物车项目DELETE /api/Carts来实现,如下所示:
success: function (result)
{
// Empty Cart.
$('#tblCart tbody tr').each(function () {
DeleteCart($(this).attr('data-cart-id'));
});
alert("Order Placed Successfully.");
},
我们已经从客户端探索了所有内容,所以现在是时候检查 API 了。
PostOrders API POST 方法
Orders 表看起来与我们发送给客户端的表有点不同。 在下面的屏幕截图中,注意框中标记的字段。 这些是我们不发送的字段,而是将在 action 方法中操作:

可以从 Customers 表中获取姓名、电子邮件和电话号码等字段。 Customerid是从客户端发送的,我们将使用它来获取这些细节,如下所示:
// POST: api/Orders
[HttpPost]
public async Task<IActionResult> PostOrders([FromBody] Orders orders)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// Retrieve customer details and add to order.
if (orders.Customerid != null)
{
var customer = _context.Customers.SingleOrDefault
(x => x.Id == orders.Customerid);
if (customer != null)
{
orders.Deliveryname = orders.Customername = customer.Firstname;
orders.Customeremail = customer.Email;
orders.Customertelephone = customer.Telephone;
}
}
...
下面的代码片段演示了用户如何复制他们的账单地址,以便它也是他们的送货地址:
// Copy customer address to delivery address.
orders.Deliverycity = orders.Customercity;
orders.Deliverycountry = orders.Customercountry;
orders.Deliverystreetaddress = orders.CustomerStreetaddress;
orders.Deliverypostalcode = orders.Customerpostalcode;
orders.Deliverystate = orders.Customerstate;
其他字段datapurchased、lastmodified、orderdatefinished设置为DateTime.Now。 细节如currency和currency_value将被设置为美元($)和零(0),我们还将Guid.NewGuid设置为shipingmethodid和paymentmethodid。
这些可以在 Orders 构造器中创建,如下所示:
public Orders()
{
Id = Guid.NewGuid();
OrderProductAttributes = new HashSet<OrderProductAttributes>();
OrdersProducts = new HashSet<OrdersProducts>();
Datepurchased = DateTime.Now;
Lastmodified = DateTime.Now;
Shipingmethodid = Guid.NewGuid();
Paymentmethodid = Guid.NewGuid();
Shippingcost = 0;
Orderdatefinished = DateTime.Now;
Currency = "$";
CurrencyValue = 0;
Orderstatus = "Placed";
}
注意,放置了Orderstatus。 这是在订单准备交付时由站点更新的内容。 随后的状态可能包括已批准、就绪、已装运、已交付等等。 如果您设计了管理屏幕,请确保将此字段更新与latsmodified和orderdatefinished一起处理。
The application demonstrated in this book is not production-ready. Generally, there should be a login page that works with OAuth2.0 Authentication. Basic validations on the API side, as well as the client side, also need to be handled. In this book, our application is built to showcase the concepts we are exploring, but you can definitely optimize our example and even build on top of it.
暴露航运细节
GET请求可以在OrdersController上使用一个订单 ID,以便第三方站点可以使用订单详细信息进行显示。 例如,许多快递公司公开了它们的 api,其他站点使用这些 api 来显示订单、发货和跟踪信息。
例如,让我们看看我们的GET方法OrdersController,它将 ID 作为参数:
// GET: api/Orders/5
[HttpGet("{id}")]
public async Task<IActionResult> GetOrders([FromRoute] Guid id)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var orders = await _context.Orders.Include
(o => o.OrdersProducts).SingleOrDefaultAsync(m => m.Id == id);
if (orders == null)
{
return NotFound();
}
return Ok(orders);
}
注意,Include子句用于包含OrdersProducts表的结果。 现在让我们对这个端点执行一个快速的 Postman 调用,以查看前面的订单的结果,如下面的截图所示:

在这里,您可以看到从 API 返回的与订单相关的每个细节及其产品。
总结
如果您已经读到了这本书的这一点,那么您应该已经使用 api 设计了一些很酷的东西。 做得好!
在本章中,我们使用ProductsController在客户端应用中显示产品列表。产品属性及其定价细节通过使用 Bootstrap、jQuery 和 HTML 设计的简单 UI 显示。
在ProductsController中使用searchString参数稍微修改了GET请求,帮助我们从 API 检索搜索结果。 客户端可以通过使用文本端点轻松实现搜索功能。
然后我们看了看购物车。 我们探索了如何使用CartsController操作来添加、更新和删除购物车条目,同时更新 UI。 在这个过程中,我们使用身份验证来实现控制器的安全性。
最后,我们将购物车中的物品转换为可视化的订单。 这是使用OrdersController完成的,它还可以用于向客户提供运输和跟踪信息。
在下一章中,我们将看看测试。net Core 中设计的 RESTful Web API 的不同技术。**
五、集成外部组件和处理器
到目前为止,我们一直在开发 FlixOneStore。 在前一章中,我们添加了购物车和运输设施。 然而,一些组织可能不需要这样的设施,因为一些组织在家里什么都有。 例如,我们的 FlixOneStore 需要一个外部组件来帮助我们跟踪分配和支付管理系统。
在本章中,我们将通过代码示例来讨论外部组件。 我们将主要讨论以下议题:
- 了解中间件
- 在中间件中将日志记录添加到 API 中
- 通过构建我们自己的中间件来拦截 HTTP 请求和响应
- JSON-RPC 用于 RPC 通信
了解中间件
顾名思义,中间件是一种连接两个不同或相似位置的软件。 在软件工程的世界中,中间件是一个软件组件,并被组装在应用管道中以处理请求和响应。
这些组件还可以检查请求是否应该传递给下一个组件,或者请求应该在触发/调用下一个组件之前还是之后由组件处理。 这个请求管道是通过使用请求委托构建的。 这个请求委托与每个 HTTP 请求交互。
看看下面来自 ASP 文档的引用.NET Core(https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/):
"Middleware is software that's assembled into an application pipeline to handle requests and responses."
看看下图,它展示了一个简单的中间件组件的例子:

要求代表
请求由Use、Run、Map和MapWhen扩展方法处理。 这些方法配置请求委托。
为了详细理解这一点,让我们使用ASP.NET Core创建一个虚拟项目。 完成以下步骤:
- 打开 Visual Studio。
- 转到文件|新建|项目,或按Ctrl+Shift+N。 参考以下截图:

Creating a new project using Visual Studio 2017
在新项目屏幕上,选择 ASP.NET Core Web 应用。
命名您的新项目(比如
Chap05_01),选择一个位置,然后单击 OK,如下图所示:

Selecting new project template
从新的 ASP.NET Core Web Application 模板界面,选择 API 模板。 确保你选择了。net Core 和 ASP。 2.0 NET Core。
单击“确定”,如下图所示:

- 解决方案资源管理器打开。 你会看到文件/文件夹结构,如下截图所示:

Showing file/folder structure of Chap05_01 project
从我们刚刚创建的虚拟项目中,打开Startup.cs文件并查看Configure方法,它包含以下代码:
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc();
}
前面的代码是不言自明的:它告诉系统通过启动Microsoft.AspNetCore.Builder.IApplicationBuilder的app.UseMvc()扩展方法将Mvc添加到请求管道中。
You can get more information on IApplicationBuilder at https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.iapplicationbuilder?view=aspnetcore-2.0.
它还指示系统在开发环境时使用特定的异常页面。 通过上述方法配置应用。
在下一节中,我们将详细讨论四种重要的IApplicationBuilder方法。
使用
方法将委托添加到应用请求管道中。 请看下面的截图,看看这个方法的签名:

Signature of Use method
正如我们在前一节中讨论的,中间件方法可以短路请求管道或将请求传递给下一个委托。
短路一个请求只是结束一个请求。
请看以下关于Use方法的代码:
public void Configure(IApplicationBuilder app)
{
async Task Middleware(HttpContext context, Func<Task> next)
{
//other stuff
await next.Invoke();
//other stuff
}
app.Use(Middleware);
}
在前面的代码中,我尝试在一个本地函数的帮助下解释Use方法的虚拟实现。 在这里,您可以看到Middleware正在调用或将请求传递给下一个委托,在await next.Invoke();之前或之后。 您可以编写/实现其他代码短语,但这些短语不应该向客户端发送响应,例如那些写入输出、生成 404 状态的代码短语等等。
局部函数是在方法中声明的方法,可以在方法本身的作用域内调用。 这些方法只能由其他方法使用。
运行
Run方法以与Use方法相同的方式向请求管道添加一个委托,但此方法终止请求管道。 请看下面的截图,看看这个方法的签名:

看看下面的代码:
public void Configure(IApplicationBuilder app, ILoggerFactory logger)
{
logger.AddConsole();
//add more stuff that does not responses client
async Task RequestDelegate(HttpContext context)
{
await context.Response.WriteAsync("This ends the request or
short circuits request.");
}
app.Run(RequestDelegate);
}
在前面的代码中,我试图说明Run终止请求管道。 这里,我使用了一个本地函数RequestDelegate。
您可以看到,在此之前我添加了一个控制台记录器,并且可以添加更多的代码短语,但不能添加那些将响应发送回客户机的代码短语。 这里,Run通过返回一个字符串结束。 运行 Visual Studio 或按F5-你会得到类似以下截图的输出:

地图
当您想要连接多个中间件实例时,Map方法会有所帮助。 为此,Map调用另一个请求委托。 请看下面的截图,看看这个方法的签名:

Signature of Map method
看看下面的代码:
public void Configure(IApplicationBuilder app)
{
app.UseMvc();
app.Map("/testroute", TestRouteHandler);
async Task RequestDelegate(HttpContext context)
{
await context.Response.WriteAsync("This ends the request or
short circuit request.");
}
app.Run(RequestDelegate);
}
在这段代码中,我添加了一个仅映射<url>/testroute的Map。 接下来是我们前面讨论过的相同的Run方法。 TestRoutehandler是私有方法。 看看下面的代码:
private static void TestRouteHandler(IApplicationBuilder app)
{
async Task Handler(HttpContext context)
{
await context.Response.WriteAsync("This is called from testroute.
" + "This ends the request or short circuit request.");
}
app.Run(Handler);
}
app.Run(Handler);之前是一个正常的委托。 现在,运行代码并查看结果。 它们应该类似如下截图:

您可以看到,web 应用的根显示了在Run委托方法中提到的字符串。 你会得到如下截图所示的输出:

在中间件的 API 中添加日志记录
简而言之,日志记录不过是在一个地方获取日志文件的过程或操作,以获取通信期间 api 中发生的事件或其他操作。 在本节中,我们将为我们的产品 api 实现日志记录。
在开始查看如何记录 api 的事件之前,让我们先快速查看一下现有的产品 api。
Refer to the *Request delegates *section to refresh your memory as to how you can create a new ASP.NET Core project.
下面的截图显示了我们产品 api 的项目结构:

以下是我们的Product模型:
public class Product
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Image { get; set; }
public decimal Price { get; set; }
public Guid CategoryId { get; set; }
public virtual Category Category { get; set; }
}
Product模型是一个类,它代表一个包含属性的产品。
下面是我们的存储库接口:
public interface IProductRepository
{
void Add(Product product);
IEnumerable<Product> GetAll();
Product GetBy(Guid id);
void Remove(Guid id);
void Update(Product product);
}
IProductRepository接口具有我们的 api 从产品的操作开始所需要的方法。
让我们来看看我们的ProductRepository课:
public class ProductRepository : IProductRepository
{
private readonly ProductContext _context;
public ProductRepository(ProductContext context) =>
_context = context;
public IEnumerable<Product> GetAll() => _context.Products.
Include(c => c.Category).ToList();
public Product GetBy(Guid id) => _context.Products.Include
(c => c.Category).FirstOrDefault(x => x.Id == id);
public void Add(Product product)
{
_context.Products.Add(product);
_context.SaveChanges();
}
public void Update(Product product)
{
_context.Update(product);
_context.SaveChanges();
}
public void Remove(Guid id)
{
var product = GetBy(id);
_context.Remove(product);
_context.SaveChanges();
}
}
类实现了IProductRepository接口。 前面的代码是不言自明的。
打开Startup.cs文件,添加以下代码:
services.AddScoped<IProductRepository, ProductRepository>();
services.AddDbContext<ProductContext>(
o => o.UseSqlServer(Configuration.GetConnectionString
("ProductConnection")));
services.AddSwaggerGen(swagger =>
{
swagger.SwaggerDoc("v1", new Info { Title = "Product APIs",
Version = "v1" });
});
对于我们的产品 api 的 Swagger 支持,您需要添加Swashbuckle.ASPNETCoreNuGet 包。
现在,打开appsettings.json文件并添加以下代码:
"ConnectionStrings":
{
"ProductConnection": "Data Source=.;Initial
Catalog=ProductsDB;Integrated
Security=True;MultipleActiveResultSets=True"
}
让我们看看我们的ProductController包含什么:
[HttpGet]
[Route("productlist")]
public IActionResult GetList()
{
return new
OkObjectResult(_productRepository.GetAll().
Select(ToProductvm).ToList());
}
上述代码是我们产品 api 的GET资源。 它调用ProductRepository的GetAll()方法,对响应进行转置,然后返回。 在前面的代码中,我们已经指示系统用ProductRepository类解析IProductRepository接口。 参考Startup类。
这里是转置响应的方法:
private ProductViewModel ToProductvm(Product productModel)
{
return new ProductViewModel
{
CategoryId = productModel.CategoryId,
CategoryDescription = productModel.Category.Description,
CategoryName = productModel.Category.Name,
ProductDescription = productModel.Description,
ProductId = productModel.Id,
ProductImage = productModel.Image,
ProductName = productModel.Name,
ProductPrice = productModel.Price
};
}
前面的代码接受一个Product类型的参数,然后返回一个ProductViewModel类型的对象。
下面的代码展示了如何注入我们的控制器构造函数:
private readonly IProductRepository _productRepository;
public ProductController(IProductRepository productRepository)
{
_productRepository = productRepository;
}
在前面的代码中,我们注入了我们的ProductRepository,并且它将在任何人调用产品 api 的任何资源时自动初始化。
现在,您可以开始使用应用了。 从菜单中运行应用或单击F5。 在 web 浏览器中,可以使用后缀/swagger作为地址的 URL。
For the complete source code, refer to the GitHub repository link at https://github.com/PacktPublishing/Building-RESTful-Web-services-with-DOTNET-Core.
它将显示 Swagger API 文档,如下面的截图所示:

单击GET /api/Product/productlist资源。 它将返回一个产品列表,如下图所示:

让我们为 API 实现日志记录。 请注意,为了使我们的演示简短,我没有添加复杂的场景来跟踪所有内容。 我正在添加简单的日志来展示日志功能。
要开始为我们的产品 api 实现日志记录,请在一个名为Logging的新文件夹中添加一个名为LogAction的新类。 下面是来自LogAction类的代码:
public class LogActions
{
public const int InsertProduct = 1000;
public const int ListProducts = 1001;
public const int GetProduct = 1002;
public const int RemoveProduct = 1003;
}
前面的代码包含的常量就是应用的操作,也称为事件。
更新我们的ProductController; 它现在看起来应该像以下代码:
private readonly IProductRepository _productRepository;
private readonly ILogger _logger;
public ProductController(IProductRepository productRepository, ILogger logger)
{
_productRepository = productRepository;
_logger = logger;
}
在前面的代码中,我们添加了一个来自依赖注入容器的ILogger接口(参见https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.0了解更多细节)。
让我们将日志功能添加到产品 API 的GET资源中:
[HttpGet]
[Route("productlist")]
public IActionResult GetList()
{
_logger.LogInformation(LogActions.ListProducts, "Getting all
products.");
return new
OkObjectResult(_productRepository.GetAll().Select(ToProductvm).
ToList());
}
前面的代码返回产品列表并记录信息。
为了测试这个场景,我们需要一个客户端或一个 API 工具,这样我们才能看到输出。 为此,我们将使用Postman扩展(参见https://www.getpostman.com/了解更多细节)。
首先,我们需要运行应用。 为此,打开 Visual Studio 命令提示符,移动到项目文件夹,然后传递命令dotnet run。 你会看到一个类似的消息如下面的截图所示:

现在,启动 Postman 并调用GET /api/product/productlist资源:

通过单击 Send 按钮,您将期望返回产品列表,但情况并非如此,如下截图所示:

前面的异常发生是因为我们在ProductController中使用了不可注入的非泛型类型。
因此,我们需要在我们的ProductController中做一些微小的改变。 看看下面的代码片段:
private readonly IProductRepository _productRepository;
private readonly ILogger<ProductController> _logger;
public ProductController(IProductRepository productRepository, ILogger<ProductController> logger)
{
_productRepository = productRepository;
_logger = logger;
}
在前面的代码中,我添加了一个泛型ILogger<ProductController>类型。 由于它是可注射的,它将自动得到解决。
Logging is slightly different in .NET Core 2.0 compared to its earlier versions. The implementation of the nongeneric ILogger is not available by default, but it is available for ILogger<T>. If you want to use nongeneric implementation, use ILoggerFactory instead of ILogger.
In this case, the constructor of our ProductController would look like the following:
private readonly IProductRepository _productRepository;
private readonly ILogger _logger;
public ProductController(IProductRepository productRepository, ILoggerFactory logger)
{
_productRepository = productRepository;
_logger = logger.CreateLogger("Product logger");
}
打开Program类并更新它。 它应该看起来像下面的代码片段:
public static void Main(string[] args)
{
var webHost = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.ConfigureAppConfiguration((hostingContext, config) =>
{
var env = hostingContext.HostingEnvironment;
config.AddJsonFile("appsettings.json", optional: true,
reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json",
optional: true, reloadOnChange: true);
config.AddEnvironmentVariables();
})
.ConfigureLogging((hostingContext, logging) =>
{
logging.AddConfiguration(hostingContext.Configuration.
GetSection("Logging"));
logging.AddConsole();
logging.AddDebug();
})
.UseStartup<Startup>()
.Build();
webHost.Run();
}
您还需要更新appsettings.json文件,并为记录器编写更多代码,使您的文件看起来像以下片段:
{
"ApplicationInsights":
{
"InstrumentationKey": ""
},
"Logging":
{
"IncludeScopes": false,
"Console":
{
"LogLevel":
{
"Default": "Warning",
"System": "Information",
"Microsoft": "Information"
}
}
},
"ConnectionStrings":
{
"ProductConnection": "Data Source=.;Initial
Catalog=ProductsDB;Integrated
Security=True;MultipleActiveResultSets=True"
}
}
现在,再次打开 Visual Studio 命令提示符并编写dotnet build命令。 它将构建项目,你将得到类似以下截图的消息:
【t】【t】
从这一点开始,如果你运行 Postman,它会给你结果,如下面的截图所示:

前面的代码添加了记录操作的功能。 你会收到类似的日志操作如下截图所示:
【t】【t】
这里,我们编写了一些使用默认ILogger的代码。 我们使用了默认方法来调用日志记录器; 然而,在某些情况下,我们需要一个定制的日志记录器。 在下一节中,我们将讨论如何为自定义日志记录器编写中间件。
通过构建我们自己的中间件来拦截 HTTP 请求和响应
在本节中,我们将为现有的应用创建我们自己的中间件。 在这个中间件中,我们将记录所有请求和响应。 让我们通过以下步骤:
打开 Visual Studio。
点击文件|打开|项目/解决方案(或按Ctrl+Shift+O),打开产品 api 的现有项目,如下截图所示:

- 找到解决方案文件夹,单击“打开”,如下图所示:

- 打开解决方案资源管理器,添加一个新文件夹,并通过右键单击项目名称将其命名为
Middleware,如下图所示:

- 右键单击
Middleware文件夹并选择 Add | New Item。 - 从 web 模板中,选择 Middleware Class 并将新文件命名为
FlixOneStoreLoggerMiddleware。 然后单击“Add”,如下图所示:

你的文件夹层次结构应该像下面的截图所示:

Thanks to Justin Williams who provided a solution for POST resources; his solution is available at https://github.com/JustinJohnWilliams/RequestLogging.
请看下面我们的FlixOneStoreLoggerMiddleware类的代码片段:
private readonly RequestDelegate _next;
private readonly ILogger<FlixOneLoggerMiddleware> _logger;
public FlixOneLoggerMiddleware(RequestDelegate next, ILogger<FlixOneLoggerMiddleware> logger)
{
_next = next;
_logger = logger;
}
在前面的代码中,我们只是利用内置的 DI 使用RequestDelegate来创建我们的自定义中间件。
下面的代码向我们展示了如何为日志连接所有的请求和响应:
public async Task Invoke(HttpContext httpContext)
{
_logger.LogInformation(await
GetFormatedRequest(httpContext.Request));
var originalBodyStream = httpContext.Response.Body;
using (var responseBody = new MemoryStream())
{
httpContext.Response.Body = responseBody;
await _next(httpContext);
_logger.LogInformation(await
GetFormatedResponse(httpContext.Response));
await responseBody.CopyToAsync(originalBodyStream);
}
}
请参考本章的请求委托部分,我们在这里讨论了中间件。 在前面的代码中,我们只是在ILogger泛型类型的帮助下记录请求和响应。 await _next(httpContext);线继续与请求管道连接。
打开Setup.cs文件,在Configure方法中添加以下代码:
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
//custom middleware
app.UseFlixOneLoggerMiddleware();
在前面的代码中,我们利用了ILoggerFactory并添加Console和Debug来记录请求和响应。 UseFlixOneLoggerMiddleware方法实际上是一种可拓方法。 为此,将以下代码添加到FlixOneStoreLoggerExtension类:
public static class FlixOneStoreLoggerExtension
{
public static IApplicationBuilder UseFlixOneLoggerMiddleware
(this IApplicationBuilder applicationBuilder)
{
return applicationBuilder.UseMiddleware<FlixOneLoggerMiddleware>();
}
}
现在,无论何时任何请求进入我们的产品 api,日志都应该出现,如下图所示:

在本节中,我们创建了一个自定义中间件,然后记录所有请求和响应。
JSON-RPC 用于 RPC 通信
JSON-RPC 是一种无状态的轻量级远程过程调用(RPC)协议。 该规范(即 JSON-RPC 2.0 规范(详见http://www.jsonrpc.org/specification)定义了各种数据结构及其处理规则。
下面几节中显示了规范中的主要对象。
请求对象
对象表示发送到服务器的任何调用/请求。 对象有以下成员:
- jsonrpc:JSON-RPC 协议版本字符串。 它的必须是准确的(在本例中是 2.0 版)。
- 方法:包含待修正方法名称的字符串。 以单词
rpc开头并后面是句号字符(U+002E 或 ASCII 46)的方法名对于 rpc 内部方法和扩展是受限制的,并且不能用于其他任何事情。 - 参数:支配参数值的结构化值。 在整个魔法过程中都要佩戴它。 该成员可以被删除。
- id:客户端固定的标识符,必须包含字符串、数字或null值。
响应对象
根据规范,每当对服务器进行调用时,必须有来自服务器的响应。 Response被表示为一个 JSON 对象,包含以下成员:
- jsonrpc:JSON-RPC 协议版本
- result:如果请求成功,则为需要的成员
- error:如果有错误,则是必需的成员
- id:必需成员
在本节中,我们概述了 JSON-RPC 规范 2.0。
总结
在本章中,我们讨论了关于支付网关、订单跟踪、通知服务等的外部 api /组件的集成。 我们还使用实际代码实现了它们的功能。
测试是帮助我们消除代码错误的一个过程。 对于所有想要使他们的代码干净和可维护的开发人员来说,这也是一种实践。 在下一章中,我们将讨论日常开发活动中的测试范例。 我们将讨论一些与测试范例相关的重要术语。 我们还将介绍关于这些术语的理论,然后我们将介绍代码示例,查看存根和模拟,并学习集成、安全性和性能测试。
六、测试 RESTful Web 服务
一个系统只有经过各种场景的测试才能成熟。 这些场景通常基于领域专家或现有生产环境的经验。 在生产环境中,系统总是有崩溃的可能,即使系统被称为完美系统。 对于 web 应用,由于性能故障、糟糕的用户体验等原因,这些条件甚至更为关键。 一个系统应该经过一个过程或一系列的发展原则来处理这类问题。 简单地说,我们必须测试这个系统。 测试是一个确保系统质量的过程。
换句话说,质量保证,或测试,是一种从不同方面评估系统的方法。 当系统需要测试以识别错误代码时,或者当我们希望评估其业务遵从性时,这个过程也很有用。
质量保证是对一个系统进行评估并确保其质量的过程。
测试完全依赖于系统的架构风格,它因系统而异; 一切都取决于我们如何为我们的测试方法或计划制定策略。
在本章中,我们将主要关注测试 RESTful 服务,并通过遵循测试驱动的开发方法使我们的代码更好。 在本章的最后,您将能够在日常开发活动中使用测试范例,了解存根,模拟对集成和安全性以及性能测试的理解。
在本章中,我们将涵盖以下主题:
- 测试范例(质量保证的基础,包括测试用例的创建)
- 测试 ASP.NET Core 控制器(单元测试)
- 存根和嘲笑
- 安全性测试
- 集成测试
- 假的物品
- 使用 Postman、Advanced RESTClient 等测试服务调用
- 用户验收测试
- 性能或负载测试
测试模式
在前面的部分中,我们看到测试和质量保证是软件开发周期中最重要的部分之一。 我们应该采取步骤设计一个测试软件的框架,这个框架被称为测试范例。
测试范式是测试的框架。 它基于一个人计划实现测试的方式。 简而言之,测试范例是一种测试方法。
测试方法是您决定如何创建测试用例的地方,包括它的语言是什么,您将如何记录测试用例,等等。 这还告诉您将如何执行测试方法(例如,使用黑盒测试)。
测试方法是在特定输入的基础上测试或验证特定输出的一种方法,而不需要知道系统的内部功能。
在我们创建测试用例或开发测试范例或测试框架之前,我们需要掌握一些重要的术语。
测试覆盖率和代码覆盖率
一般来说,覆盖率是指被覆盖的内容以及如何衡量覆盖率。 从开发人员的角度来看,在测试驱动开发中编写单元测试可以告诉我们如何以及覆盖哪些代码区域。
测试期间执行的代码的度量是代码覆盖率。 测试期间执行的测试用例的度量是测试覆盖率。
对代码进行了单元测试,并证明所覆盖的代码也经过了测试。 在这个代码覆盖率中,会涉及到很多东西,即代码行、函数、条件、表达式、API 资源等等。
软件测试术语请参见http://castb.org/wp-content/uploads/2014/05/istqb_glossary_of_testing_terms_v2.3.pdf。
测试覆盖率和代码覆盖率也可以覆盖以下任何测试类型:
- 单元测试
- 安全性测试
- 集成测试
在接下来的部分中,我们将使用代码示例详细查看这些测试。
任务、场景和用例
当某人使用一个测试范例时,他们应该知道任务、场景和用例。 在本节中,我们将详细讨论这些术语:
- Task:Task 是一个通用词,不仅与软件行业相关,而且也与许多其他行业相关。 这是一个需要完成的行为或工作。 完成任务有不同的方法,但任务的总体意图是它应该被完成。 在不同的领域,任务有不同的目的。 在 scrum 开发(https://whatis.techtarget.com/definition/storyboard)中,故事板或任务板帮助开发人员理解需要完成的工作。
下面的图表说明了任务的含义:

前面的图表是一个故事或任务板; 它显示了完成一本书所需的各种任务,从数据收集到技术审查。 市场上有很多免费或付费的工具来管理这些类型的任务。
- 场景:通常情况下,场景就是系统在与客户交互后发生故障的情况。 换句话说,场景是一种理解和详细编写步骤的方式。 例如,有一些情况可能导致系统的登录功能失败,这些情况将作为场景进行记录。 在软件测试中,场景也被称为测试场景。 一个场景通常导致一个或多个测试。
- 用例:用例是系统和用户之间可能的交互序列的集合。 它还可以是系统实现时应该评估的可能场景的集合。 这些用例更详细和记录,并被划分为不同的步骤,如下图所示:

在前面的图中,很明显,测试用例的子集是****测试场景,用例,是测试场景的超集。 无论何时您创建一个测试用例,它都来自于一个测试场景。****
****# 检查表
一般来说,清单只是一个项目列表,其中需要一个行动来实现一个目标。 清单可以是待办事项列表、日常活动列表或开发人员任务列表。
在测试领域中,列表可以包含要验证的测试用例、需要执行的测试列表,等等。 清单因人而异,开发人员因人而异,甚至组织与组织也各不相同,但清单的目的总是限制遗忘某些东西的人类行为。
漏洞和缺陷
术语 bug 和缺陷是行业中最常用的术语。 在一些组织中,这些术语可以互换使用。 但是,通常情况下,错误是与一些正确完成但执行了意外输出的事情相关的,例如,2 + 3 = 6。 另一方面,缺陷是在计划期间被遗漏的东西。
关于 bug 和缺陷需要注意的一些事情:
- 错误几乎总是由于需求的不纯实现,例如,错误地满足基本需求的代码
- bug 通常是在开发或测试阶段确定的
- 缺陷是与在生产过程中遗漏了客户或客户的设计或需求差距相关的
- 缺陷往往表明人为错误
- bug 可以在测试期间被发现时修复
- 缺陷会导致系统出现故障,从而导致设计问题
测试方法
通常,测试方法是说明如何执行测试的执行路径。 这些方法因系统而异; 如果一个系统需要协商方法,并不意味着另一个系统也需要协商方法。 不同的系统需要不同的测试方法。
测试方法是一种测试策略,它只是一个系统或项目的实现。
每个人都应该清楚地了解测试策略,以便创建的测试可以帮助团队的非技术成员(如涉众)理解系统是如何工作的。 这样的测试可以是自动化的,例如测试业务流,也可以是手动测试,由用户在用户验收测试系统上执行。
测试策略或方法有以下技术:
- 主动性**:这是一种早期的方法,并试图在从初始测试设计创建构建之前修复缺陷**
- Reactive:在这种方法中,一旦编码完成,就开始测试
测试金字塔
测试金字塔是一种策略或一种方法,用来定义在 RESTful 服务中应该测试什么。 换句话说,我们可以说测试金字塔帮助我们定义 RESTful 服务的测试范围。
The concept of the testing pyramid was developed by Mike Cohn (http://www.mountaingoatsoftware.com/blog/the-forgotten-layer-of-the-test-automation-pyramid) in 2009.
有各种各样的测试金字塔; 不同的作者通过指示他们如何放置或优先化他们的测试范围来描述这一点。
下图描述了与 Mike Cohn 定义的相同的概念:

让我们详细讨论一下这些层。
- 单元测试:单元测试以 ASP 开发的 RESTful 服务应用的单元测试小功能.NET Core
- RESTful 服务测试(验收测试):这些测试用于测试独立服务或与另一个(通常是外部)服务通信的服务
- GUI 测试(REST 客户端测试):这些测试属于将使用 RESTful 服务的客户端或使用者; 它们有助于用用户界面的某个方面测试整个系统,并且是端到端测试
我们将讨论关于用 ASP 开发的 RESTful 服务的应用的测试。 净的核心。
类型的测试
在前面的部分中,我们讨论了测试方法或测试策略。 这些策略决定了我们将如何对系统进行测试。 在本节中,我们将讨论应用中使用的各种类型的测试。
测试 ASP。 核心控制器(单元测试)
单元测试通常测试单个函数调用,以确保测试程序的最小部分。 因此,这些测试旨在验证特定的功能,而不考虑其他组件。 在这里,测试策略可以派上用场,并确保系统的最佳质量保证将得到执行。 当使用测试驱动开发(TDD)方法时,它增加了更多的功能。
您可以在https://github.com/garora/TDD-Katas的 Katas 帮助下学习和实践 TDD。
我们将通过一个代码示例来讨论这个问题。 在我们继续之前,请先看看以下先决条件:
- Visual Studio 2017 Update 3 或更高版本
- .NET Core 2.0 或更高版本
- c# 7.0 或更高版本
- ASP.NET Core 2.0 或更高版本
- 实体框架 Core 2.0 或更高版本
- xUnit 和 MS 测试
- moq 框架
为考试做准备
在本节中,我们将创建一个 ASP.NET Core API,然后进行单元测试。
完成以下步骤创建您的应用:
- 打开 Visual Studio。
- 转到文件|新建|项目或按Ctrl+Shift+F5:

- 选择 ASP.NET Core Web 应用。
- 从模板窗口中,选择 ASP.NET Core api——确保你选择了。NET Core 2.0。
- 命名项目,选择解决方案的路径,然后单击 OK。
- 在 Solution Explore 中添加
Core文件夹,右键单击并选择 Add New Folder,并将其命名为Model。 - 在
Core文件夹下添加Interfaces和Model文件夹。 - 在
Model文件夹下添加一个新类-在解决方案资源管理器中右键单击Model文件夹并选择 Add new Item。 然后,选择Class或按Shift+Alt+c
请注意,快捷键根据您对 Visual Studio 的设置而不同。
- 将其命名为
Product.cs,并将以下代码添加到该类中:
namespace Chap06_01.Core.Model
{
public class Product
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Image { get; set; }
public decimal Price { get; set; }
public Guid CategoryId { get; set; }
public virtual Category Category { get; set; }
}
}
- 重复步骤 7和8加入
Category.cs和ProductViewModel.cs。 - 重复步骤 6,添加
Infrastructure文件夹。 - 下添加一个新类
Infrastructure的Infrastructurefolder-right-click 文件夹在解决方案资源管理器中,选择添加新项目,和,选择类或单击转变+Alt + c
** 命名为ProductContext.cs。*
*在这个演示项目中,我们没有遵循测试驱动的开发方法; 为了演示目的,我们将对应用进行单元测试。
- 现在,打开
appsettings.json文件并添加以下代码片段:
"ConnectionStrings":
{
"ProductConnection": "Data Source=.;Initial
Catalog=ProductsDB;Integrated
Security=True;MultipleActiveResultSets=True"
}
- 在解决方案资源管理器中右键单击项目并选择管理 Nuget 包。
- 在 Nuget Package Manager 屏幕下,搜索
Swashbuckle.AspNetCore并安装它。
Swagger is open source and adheres to open specifications (https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md). Swagger allows you to describe an API's structure. Swagger provides documentation to users (devs who are going to use APIs). There are a lot of open source and commercial tools available that can integrate with Swagger.
Swagger CodeGen (https://swagger.io/swagger-codegen/) helps to generate client libraries for an API.
Swagger UI (https://swagger.io/swagger-ui/) helps to generate an API's documentation.
Swashbuckle.AspNetCore (https://github.com/domaindrivendev/Swashbuckle.AspNetCore) is a tool that helps document APIs built on ASP.NET Core.
- 在
Core/Interfaces下面加上interface IProductRepository。 - 在
IProductRepository接口中添加以下代码:
namespace Chap06_01.Core.Interfaces
{
public interface IProductRepository
{
void Add(Product product);
IEnumerable<Product> GetAll();
Product GetBy(Guid id);
void Remove(Guid id);
void Update(Product product);
}
}
Please note that for the complete source code, refer to the GitHub repository at https://github.com/PacktPublishing/Building-RESTful-Web-Services-with-DotNET-Core.
- 在
Infrastructure文件夹下添加ProductRepository类。 - 将以下代码添加到
ProductRepository:
namespace Chap06_01.Infrastructure
{
public class ProductRepository : IProductRepository
{
private readonly ProductContext _context;
public ProductRepository(ProductContext context)
=> _context = context;
public IEnumerable<Product> GetAll() =>
_context.Products.Include(c =>
c.Category).ToList();
public Product GetBy(Guid id) => _context.Products.
Include(c => c.Category).FirstOrDefault(x => x.Id == id);
public void Add(Product product)
{
_context.Products.Add(product);
_context.SaveChanges();
}
public void Update(Product product)
{
_context.Update(product);
_context.SaveChanges();
}
public void Remove(Guid id)
{
var product = GetBy(id);
_context.Remove(product);
_context.SaveChanges();
}
}
}
- 打开
Startup.cs文件,添加以下代码:
services.AddScoped<IProductRepository, ProductRepository>();
services.AddDbContext<ProductContext>
(
o => o.UseSqlServer(Configuration.GetConnectionString
("ProductConnection"))
);
services.AddSwaggerGen
(
swagger =>
{
swagger.SwaggerDoc("v1", new Info { Title = "Product
APIs", Version = "v1" });
}
);
你的项目层次结构现在看起来应该像下面的解决方案资源管理器的截图:

现在,您可以开始使用该应用了! 从菜单中运行应用或按F5。 在浏览器中,在 URL 地址栏添加后缀/swagger,如下图所示:

这个 URL 应该显示 swagger API 文档,如下面的截图所示:

如果你点击 GET /api/Product/productlist,它应该会返回一个产品列表,如下图所示:

编写单元测试
在本节中,我们将使用 ASP 添加一个测试项目.NET Core 2.0 和使用 xUnit 编写单元测试。 在开始编写测试之前,我们应该在现有的应用中设置一个测试项目。
下面是我们的测试项目设置所需的几个简单步骤:
- 从 Visual Studio 的解决方案资源管理器,右键单击解决方案'Chap06_01'(1 项目),并单击添加|新项目… ,如下图所示:

- 从 Add New Project 模板中,选择. net Core 和 xUnit Test Project(.NET Core),并提供一个有意义的名称,例如
Chap06_01_Test:

- 添加名为
Fake和Services的文件夹。 (请参阅前一节,了解如何从解决方案资源管理器中添加新文件夹。) 你的项目结构现在看起来应该像下面的截图:

ProductData.cs类应该如下所示:
namespace Chap06_01_Test.Fake
{
public class ProductData
{
public IEnumerable<ProductViewModel> GetProducts()
{
var productVm = new List<ProductViewModel>
{
new ProductViewModel
{
CategoryId = Guid.NewGuid(),
CategoryDescription = "Category Description",
CategoryName = "Category Name",
ProductDescription = "Product Description",
ProductId = Guid.NewGuid(),
ProductImage = "Image full path",
ProductName = "Product Name",
ProductPrice = 112M
},
new ProductViewModel
{
CategoryId = Guid.NewGuid(),
CategoryDescription = "Category Description-01",
CategoryName = "Category Name-01",
ProductDescription = "Product Description-01",
ProductId = Guid.NewGuid(),
ProductImage = "Image full path",
ProductName = "Product Name-01",
ProductPrice = 12M
}
};
return productVm;
}
public IEnumerable<Product> GetProductList()
{
return new List<Product>
{
new Product
{
Category = new Category(),
CategoryId = Guid.NewGuid(),
Description = "Product Description-01",
Id = Guid.NewGuid(),
Image = "image full path",
Name = "Product Name-01",
Price = 12M
},
new Product
{
Category = new Category(),
CategoryId = Guid.NewGuid(),
Description = "Product Description-02",
Id = Guid.NewGuid(),
Image = "image full path",
Name = "Product Name-02",
Price = 125M
}
};
}
}
}
在前面的代码片段中,我们为Products和ProductsViewModel创建了假数据。
The full code is available to download from https://github.com/PacktPublishing/Building-RESTful-Web-Services-with-DotNET-Core.
- 我们的单元测试类
ProductTest.cs看起来如下所示:
Important terms for xUnit:
- Fact是一个属性,用于不带参数的正常测试方法
- 理论是一个属性,用于参数化测试方法
namespace Chap06_01_Test.Services
{
public class ProductTests
{
[Fact]
public void Get_Returns_ActionResults()
{
// Arrange
var mockRepo = new Mock<IProductRepository>();
mockRepo.Setup(repo => repo.GetAll()).Returns(new
ProductData().GetProductList());
var controller = new ProductController(mockRepo.Object);
// Act
var result = controller.GetList();
// Assert
var viewResult = Assert.IsType<OkObjectResult>(result);
var model =
Assert.IsAssignableFrom<IEnumerable<ProductViewModel>>
(viewResult.Value);
Assert.NotNull(model);
Assert.Equal(2, model.Count());
}
}
}
在前面的代码片段中,我们只是测试我们的ProductController,它是一个Get资源GetList。 在这段代码中,我们对列表进行了模拟; 我们实际上并没有攻击数据库,而是使用假数据测试我们的Controller方法。
- 从测试资源管理器运行测试; 如果你的测试通过了,你会看到如下截图:
![]()
存根和嘲笑
存根是对测试期间进行的调用的响应,而 mock 旨在设置期望。 它们可以进一步解释如下:
- 存根:在
stubs对象中,我们总是得到一个有效的存根响应。 响应并不关心您提供了什么输入。 在任何情况下,输出都是相同的。 - mock:在
mock对象中,我们可以测试或验证可以在被 mock 对象上调用的方法。 这是一个伪对象,用于验证单元测试是失败还是通过。 换句话说,我们可以说模拟对象只是实际对象的副本。
在前一节编写单元测试中,我们使用最小起订量框架来实现模拟对象。
安全性测试
安全性是一个非常广泛的术语,不能用几行字来解释。 一般来说,安全测试是一种测试应用是否安全或是否有可能泄露某人的数据的方法。
安全性和安全系统将在第八章、安全 RESTful Web 服务中讨论。
安全测试非常重要,尤其是当我们在基于 web 的应用中工作时。 Web 应用是公开的,容易受到攻击,因此身份验证和授权是这里最重要的因素。
FxCop (https://en.wikipedia.org/wiki/FxCop), which is shipped with Visual Studio and VeraCode (https://www.veracode.com/), is one of the most popular tools used in security testing.
集成测试
在单元测试中,我们测试单个代码单元,而在 Web API 的集成测试中,我们测试所有协同工作的服务(内部和外部,包括第三方组件)。 应该进行服务调用以确保与外部服务的集成。
运行测试
让我们以我们在前一节中创建的单元测试应用为例:
- 添加一个用于集成测试的新项目,并确保项目结构如下截图所示:

- 在
ProductTest.cs的构造函数中编写如下代码:
var server = new TestServer
(
new WebHostBuilder()
.UseStartup<TestStartup>()
);
_client = server.CreateClient();
在前面的代码块中,我们初始化了TestServer,其中我们使用TestStartup作为我们的启动条目文件。 最后,我们创建了WebHostBuilder()中的private readonly HttpClient _client;。
- 然后,编写一个调用 productlist 资源的简单方法:
[Fact]
public async Task ReturnProductList()
{
// Act
var response = await _client.GetAsync("api/Product
/productlist");
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
// Assert
Assert.NotEmpty(responseString);
}
在前面的代码中,我们正在使用我们的资源GET api/product/productlist并测试它,看它是否返回预期的输出。
为了平稳地运行代码,需要在代码中添加Microsoft.AspNetCore.Hosting;和Microsoft.AspNetCore.TestHost;名称空间。
该测试还确保内部组件(或此方法进行的任何外部服务调用)按预期工作。
- 完成
ProductTes.cs的代码如下:
namespace Chap06_02_Test.Services
{
public class ProductTest
{
public ProductTest()
{
// Arrange
var server = new TestServer(new WebHostBuilder()
.UseStartup<TestStartup>());
_client = server.CreateClient();
}
private readonly HttpClient _client;
[Fact]
public async Task ReturnProductList()
{
// Act
var response = await
_client.GetAsync("api/Product/productlist");
response.EnsureSuccessStatusCode();
var responseString = await
response.Content.ReadAsStringAsync();
// Assert
Assert.NotEmpty(responseString);
}
}
}
- 为
TestStartup文件编写如下代码:
namespace Chap06_02_Test
{
public class TestStartup : Startup
{
public TestStartup(IConfiguration configuration) :
base(configuration)
{ }
public static IConfiguration InitConfiguration()
{
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
return config;
}
public override void ConfigureServices(
IServiceCollection services)
{
//mock context
services.AddDbContext<ProductContext>
(
o => o.UseSqlServer
(
InitConfiguration().GetConnectionString
(
"ProductConnection"
)
)
);
services.AddMvc();
services.AddScoped<IProductRepository,
ProductRepository>();
}
public override void Configure
(
IApplicationBuilder app, IHostingEnvironment env
)
{
app.UseStaticFiles();
app.UseMvc();
}
}
}
在前面的代码中,我们的TestStartup类继承了Startup类,这意味着我们现在使用它的成员和方法。
您需要将方法ConfigureServices和Configure设置为虚拟,以便在TestStartup类中重写这些方法。
看看我们的InitConfiguration()方法; 此方法将添加您的测试配置文件,以便您可以在任何其他环境中使用测试配置值。
在我们的TestStartup类中,我们覆盖了ConfigureServices和Configure方法,这样我们就可以配置测试服务或为测试目的而专门创建的任何实用程序类。
现在,我们已经设置好运行测试、打开测试资源管理器并运行选定的测试。 您还可以从ProductTest.cs文件运行测试(只需右键单击并选择 run tests)。
如果您需要调试代码,您也可以调试测试。 如果你这样做,你会得到以下结果:

您可以编写任意多的测试。 测试还取决于要测试的代码。
假的物品
顾名思义,假物品是指不真实的物品。 伪对象用于测试目的,包含实际的代码,但不具有真正的所有功能。 例如,我们可以使用 Entity Framework Core 创建一个假对象来获取数据记录; 在本例中,我们更喜欢使用 InMemory(https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/in-memory)而不是直接的 DB 连接。
运行测试
让我们以在前一节单元测试中开发的应用为例。 按照上一节中提到的步骤添加一个新的 xUnit 测试项目。
我们正在寻找用于测试目的的假对象或数据,因此我们不会攻击实际的数据库服务器。 相反,这里我们将使用 InMemory 数据库。
需要添加Microsoft.EntityFrameworkCore.InMemoryNuGet 包启动 InMemory 数据库。
我们不打算在这里改变任何东西,但我们将创建假数据和记录来测试。 要继续,请在ConfigureServices方法中向TestStartup.cs文件中添加以下代码:
//for tests use InMemory db
services.AddDbContext<ProductContext>
(
o => o.UseInMemoryDatabase
(
InitConfiguration().GetConnectionString
(
"ProductConnection"
)
)
);
这里,我们使用以下方法:
- 通过将
.UseInMemoryDatabase添加到TestStartup类,只用于测试目的的 InMemory 数据库 - 对于我们的实际代码,我们的数据库服务器将在
Startup.cs类中保持不变,即.UseSqlServer
现在我们需要伪造数据和记录,所以在TestStartup类中添加如下方法:
private static void FakeData(DbContext context)
{
var category = new Category
{
Id = ToGuid("A5DBF00D-2E29-4993-A0CA-7E861272C6DC"),
Description = "Technical Videos",
Name = "Videos"
};
context.Add(category);
var product = new Product
{
Id = ToGuid("02341321-C20B-48B1-A2BE-47E67F548F0F"),
CategoryId = category.Id,
Description = "Microservices for .NET Core",
Image = "microservices.jpeg",
Name = "Microservices for .NET",
Price = 651,
InStock = 5
};
context.Add(product);
context.SaveChanges();
}
然后从Configure(IApplicationBuilder app, IHostingEnvironment env)方法中调用FakeData(context)方法,如下代码所示:
public override void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
var context = app.ApplicationServices.GetService<ProductContext>();
FakeData(context);
app.UseStaticFiles();
app.UseMvc();
}
现在我们已经准备好运行测试,因此打开测试资源管理器并点击 run All。 如果测试通过,您应该看到如下截图:

为了再次检查测试是否没有击中实际的数据库,让我们调试测试代码。 打开ProductTest.cs类并为以下测试设置断点:
[Fact]
public async Task ReturnProductList()
{
// Act
var response = await _client.GetAsync("api/Product/productlist");
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
// Assert
Assert.NotEmpty(responseString);
}
现在右键单击 Debug Test,使用 step-into(F11键)进入控制器和存储库,并检查产品的实际列表。 您可以看到我们的测试返回了假数据,这意味着它们没有击中实际的数据库。 下面是调试代码的屏幕截图:

前面的截图来自一个小应用,我们用它演示了使用假对象进行测试。 使用这种测试方法,我们的假对象总是会被击中,而不是任何实际的代码。
使用 Postman、Advanced REST Client 等测试服务调用
有很多工具可用来测试 RESTful web 服务和 api。 这些工具提供实际的输出。
当您只有 API 资源,并且希望在不同的场景中测试预期的输出,但没有实际的源代码时,Web 服务测试工具非常有用。
我们将使用以下两个工具测试我们的产品 api。
邮递员
Postman(https://www.getpostman.com/)是测试 web 服务输出时最流行的工具之一。 它还带有一个谷歌 Chrome 扩展:
- 启动邮差。 如果你没有它,从前面的链接安装它。
- 选择 Resource 类型为 GET 并输入 API 的 URL; 在我们的例子中,它是
http://localhost:60431/api/Product/productlist。 - 点击发送(或者,你可以点击发送和下载,如果你需要文件中的数据)。
- 如果测试通过,你会看到如下截图:

先进的 Rest 客户端
高级休息客户端(ARC)是另一个流行的工具,也作为一个 Chrome 扩展。 您可以安装它从 Chrome 扩展商店或直接从https://install.advancedrestclient.com/:
- 安装 Chrome 扩展为 ARC,如果还没有安装。
- 发射弧。
- 传递 GET 资源。
- 如果测试通过,你会看到如下截图:

用户验收测试
顾名思义,用户验收测试(UAT)是由用户完成或被用户接受的测试。 在这种测试方法中,可能是应用最终用户的用户直接参与测试。 可能存在用户在生产环境中测试的场景,或者用户可以访问他们可以接受或拒绝的预测试结果。
这种类型的测试取决于将在生产环境中使用应用的实际用户。 这种测试通常在 UAT 或预生产环境中进行。
行业中的典型环境被称为开发、阶段、QA、UAT、预生产和生产。 在您的组织中,您可能没有符合项目需求的所有环境; 如果是,请参考https://www.guru99.com/user-acceptance-testing.html。
UAT 测试也被视为最终测试,它的接受或拒绝告诉我们当前的版本是否将被部署到生产环境中。 此测试的主要焦点是与业务相关的。 此测试不处理测试代码或各种模式的实现; 它只是确保实现了所有的业务规则和需求。
性能或负载测试
对于 web 应用的性能来说,可伸缩性是非常重要的。 一个应用可以是非常安全的、经过良好测试的、用好的代码创建的,但如果它不可伸缩,用户仍然会避免使用它。
我们将在第 9 章、*伸缩 RESTful web 服务(web 服务性能)*中详细讨论 RESTful web 服务的伸缩。
性能对于一个好的 API 来说是非常重要的,所以我们需要测试并确保我们的应用能够加载或处理大的请求。 负载测试是一种非功能类型的测试(https://www.guru99.com/non-functional-testing.html),负载测试的主要目的不是验证代码或测试代码的运行状况。
这个测试的主要目的是确保 web API 在可扩展性、可靠性等方面表现良好。
以下是性能测试的技术或类型:
- 负载测试:测试系统在特定负载的各种情况下的行为。 这还包括关键事务、数据库负载、应用服务器等等。
- 压力测试:这是一种系统进行回归测试并找到系统容量上限的方法。 它还取决于系统在当前负载超过预期最大负载的危急情况下的行为。
- 浸泡试验:这也称为耐力试验。 在这个测试中,主要目的是监视内存利用率、内存泄漏或影响系统性能的各种因素。
- 峰值测试:这是一种确保系统能够维持工作负载的方法。 确定性能的最佳任务之一是突然增加用户负载。
在 ASP.NET Core,我们可以使用以下方法来执行负载测试:
- Visual Studio:如果你有 Visual Studio 企业版,你可以轻松地创建一个负载测试项目; 更多信息请访问以下链接:https://docs.microsoft.com/en-us/vsts/load-test。
- WebSurge:这是一个用于 api 的负载测试。 您可以在云中使用它,也可以免费用于学习目的。 更多信息,请访问http://websurge.west-wind.com/。
- BenchmarkDotNet:这个工具告诉我们有多少代码是性能的。 它测试不同的代码块,得到相同的结果,看看哪个性能最好。 更多信息,请访问https://github.com/dotnet/BenchmarkDotNet。
- Netling:这是一个用于 web 应用的负载测试工具。 使用 Netling,您可以更改并重新测试您的代码,以满足您的性能尺度。 更多信息,请访问https://github.com/hallatore/Netling。
Explanations, along with working examples, of these tools and Visual Studio Load Testing is beyond the scope of this book.
在本节中,我们将简单地测试我们的产品 api,以检查它们列出我们所请求的产品所花费的时间。
您还可以使用一个简单的 web 客户端测试 api 的请求时间。 在第 10 章、*构建 Web 客户端(消费 Web 服务)*中,我们将详细讨论如何构建 Web 客户端。
看看我们的ProductTest类的代码,如下:
public class ProductTest
{
public ProductTest(ITestOutputHelper output)
{
_output = output;
}
private const double ExpectedRequestTime = 1000;
private const int ApiLoad = 100;
private const string RequestUri =
"http://localhost:60431/api/product/productlist";
private readonly ITestOutputHelper _output;
private static double RequestCallTime()
{
DateTime start;
DateTime end;
using (var client = new HttpClient())
{
start = DateTime.Now;
var response = client.GetAsync(RequestUri).Result;
end = DateTime.Now
}
var actual = (end - start).TotalMilliseconds;
return actual;
}
[Fact]
public void SingleCallRequestTime()
{
var actual = RequestCallTime();
_output.WriteLine($"Actual time: {ExpectedRequestTime}
millisecond.
Expected time: {actual} millisecond.");
Assert.True(actual <= ExpectedRequestTime);
}
//code truncated
}
前面的代码是不言自明的。 我们只是计算单个和多个请求所花费的时间,并检查它是否达到我们的基准。
The complete code is available to download from https://github.com/PacktPublishing/Building-RESTful-Web-Services-with-DotNET-Core.
运行测试
要运行测试,您需要确保您的 api 正在运行,并使用 URL 进行访问。 请使用 CLI 完成以下步骤:
- 打开 Visual Studio 命令提示符
- 找到 API 项目的文件夹
- 启动命令
dotnet run
你现在应该看到一个类似于下面截图的屏幕:

按照以下步骤使用 Visual Studio 测试资源管理器运行测试:
- 打开
ProductTest.cs文件 - 开启测试资源管理器
- 单击“运行”
这将运行所有测试; 你应该看到类似下面截图的输出:

我们还可以检查各个 api 完成请求所花费的确切时间。 要做到这一点,单击特定TestCase结果的测试资源管理器中的Output,您应该会看到以下屏幕:

还可以使用命令行运行这些测试,如下所示:
- 打开 Visual Studio 命令提示符
- 找到 API 项目的文件夹
- 启动命令
dotnet test
前面的命令将运行所有测试; 如果它们通过,你应该看到以下屏幕:

Visit https://docs.microsoft.com/en-us/dotnet/core/tools/?tabs=netcore2x to check all the available CLI commands.
在本节中,我们尝试了一个基于请求时间的简单负载测试。 我们试过一个电话和多个电话。
总结
测试有助于确保我们的代码没有错误。 测试也是所有想要使他们的代码干净和可维护的开发人员的实践。 在本章中,我们介绍了开发团队日常活动中的测试范例,了解了存根和模拟,以及理解集成、安全性和性能测试的重要性。
在接下来的章节中,我们将讨论安全性,包括遵循 OWASP 安全标准和 JWT 身份验证。 我们将介绍使用自定义过滤器和输入验证的更复杂的场景。 对于任何 web 应用来说,数据保护总是一个高度优先级的问题,因此我们还将关注敏感数据的持久性和存储。*****
七、持续集成和持续部署
大多数项目都是团队努力的结果。 团队可以位于不同的地方,也可以位于相同的地方,来自不同位置的成员必须以同步模式工作,这样他们的更改就不会与其他团队成员发生冲突。 一个系统只有在各种场景中使用时才会成熟; 这些场景可以基于领域专家的经验,也可以来自生产环境。 即使系统被认为是一个完美的系统,系统也有可能在生产环境中崩溃。 对于 web 应用来说,由于性能故障、糟糕的用户体验等原因,这些条件更加关键。 一个系统应该经历这样一个过程:如果团队成员进行了更改,那么代码在单元测试之后被集成,然后构建被部署到相关的环境中。
当我们说到部署时,我们立即想到 Xcopy 部署。 在这种类型的部署中,您只需构建和复制相关文件,并将其部署/粘贴到相关环境中。
在这一章,我们将讨论的基本部署和新兴实践的影响,如持续集成(CI)和持续部署**(【T6 CD】**)。 我们将重点关注以下议题:
- Azure 环境
- 出版社/托管
- 在线使用 TFS 的 CI 和 CD
- CI 和 CD 之间的区别
简介—部署术语
在进一步讨论之前,我们应该首先讨论为什么要讨论部署。 部署周期具有特定的流程,我们应该理解部署术语。 部署术语简单地包括从代码更改到发布的步骤。 在本节中,我们将讨论所有这些部署步骤。
构建阶段
在构建阶段,随着所有相应单元测试的通过,服务源在没有任何错误的情况下被编译。 这个阶段生成构建工件。
持续集成
每次开发人员提交任何更改时,CI 强制重新构建整个应用——应用代码将被编译,并针对它运行一组全面的自动化测试。 这种实践产生于大型团队中频繁集成代码的问题。 其基本思想是保持增量或对软件的更改较小。 这为软件处于可操作状态提供了信心。 即使开发人员的签入破坏了系统,使用这个过程也很容易修复它。
部署
硬件供应、安装基本操作系统和. net 框架的正确版本是部署的先决条件。 下一部分是通过各个阶段将这些构建工件推进到生产环境中。 这两个部分的组合称为部署阶段。 在大多数应用中,部署和发布阶段之间没有区别。
持续部署
在 CD 中,每个成功的构建都被部署到一个首选环境中,例如生产环境。 环境因组织而异。 因此,CD 并不适用于生产环境,但您也可以将其用于其他环境,如开发、登台等。 从技术团队的角度来看,CD 更重要。 在 CD 之下,还有一些其他的实践,例如自动化单元测试、标记、构建号的版本控制,以及变更的可追溯性。 通过持续交付,技术团队确保通过各种较低环境推动到生产中的变更在生产中按预期工作。 通常,它们很小,部署得很快。
持续交付
持续交付与 CD 不同,CD 是从技术团队的角度出发的,而持续交付更关注于尽早向客户提供已部署的代码。 为了确保客户得到正确的无缺陷产品,在连续交付中,每个构建都必须通过所有的质量保证检查。 一旦产品通过了令人满意的质量验证,何时发布就由业务涉众决定了。
构建和部署管道
构建和部署管道是通过自动化实现持续交付的一部分。 它是一个步骤工作流,代码通过这些步骤提交到源存储库中。 在部署管道的另一端,生成用于发布的构件。 构建和部署管道的一些步骤如下:
- 单元测试
- 集成测试
- 代码覆盖和静态分析
- 回归测试
- 部署到临时环境
- 负载/压力测试
- 部署到发布存储库
释放
最终用户可用的业务特性被称为特性的发布版本。 要发布一个特性或服务,应该事先部署相关的构建构件。 通常,功能开关管理一个功能的发布。 如果特性标志(也称为特性切换)在生产环境中没有被打开,它被称为指定特性的暗版本。
成功的 RESTful 服务部署的先决条件
任何系统部署的成功都取决于团队所遵循的体系结构风格和实践。 采用以下实践,我们的 RESTful 服务获得成功的机会更大:
- 自给自足的团队:作为 SOA 和微服务架构的先驱,Amazon 遵循了两个披萨团队的范例。 这意味着一个微服务团队的成员通常不超过 7 - 10 人。 这些团队成员将拥有所有必要的技能和角色; 例如,开发、运营和业务分析师。 这样的服务团队处理微服务的开发、运营和管理。
- CI 和 CD:CI 和 CD 是实现基于微服务体系结构风格的系统的 RESTful 服务的先决条件。 小型自给自足的团队,能够经常整合他们的工作,是微服务成功的先行者。 这个建筑不像巨石那么简单。 然而,自动化和定期推动代码升级的能力使团队能够处理复杂性。 诸如Team Foundation Online Services(TFS)、TeamCity 和 Jenkins 等工具是这个领域中非常流行的工具链。
- 基础设施即代码:用代码表示硬件和基础设施组件(如网络)的想法是新的。 它可以帮助您使部署环境(如集成、测试和生产)看起来完全相同。 这意味着开发人员和测试工程师将能够很容易地在较低的环境中重现产品缺陷。 使用 CFEngine、Chef、Puppet、Ansible 和 Powershell DSC 等工具,您可以将整个基础设施编写为代码。 通过这种范式转换,您还可以将基础设施置于版本控制系统下,并将其作为部署中的工件发布。
- 云计算的利用:云计算是采用微服务的一大催化剂。 然而,对于微服务部署来说,它并不是强制性的。 云计算具有近乎无限的规模、弹性和快速供应能力。 云是微服务的天然盟友,这一点是显而易见的。 因此,Azure 云的知识和经验将帮助您采用微服务。
Azure 环境
Azure 是微软提供各种云计算服务的服务。 Azure 是一个云平台,可以帮助您在全球范围内构建、部署和管理应用。
在讨论 Azure 环境之前,我们应该先了解云计算。
云计算
简而言之,云计算是一种存储/场所,通过互联网提供各种基于计算机的服务,即存储、数据库、服务器和软件(这里的互联网被称为云)。
There are various terms in existence related to cloud-computing, you can refer to this link for these terms: https://azure.microsoft.com/en-in/overview/cloud-computing-dictionary/.
任何人都可以出售这些服务,提供云计算服务的供应商/公司被称为云提供商。
云计算并不是一个新术语,它已经存在一段时间了,只是现在它变得流行起来。 如果你正在使用任何在线服务来帮助你向他人发送或接收电子邮件,那么这就是云计算。 在云的帮助下,您几乎可以做任何您想做的事情。 这些服务包括:
- 创建新应用
- 存储数据
- 托管、部署应用
还有更多的活动,这取决于你的云提供商提供的服务或你的订阅类型。
云计算的好处
目前,云计算在 IT 资源相关业务的增长中扮演着重要的角色。 如今,每个人的想法都与传统系统不同; 云对所有人都有好处,正如这里所讨论的:
- Pick and start:如果你有任何类型的云计算订阅,你不需要思考,只要选择你的服务,从任何地方开始。 你只是需要一个互联网开始。
- 成本:当你使用云计算时,没有必要考虑花钱购买昂贵的硬件或相关基础设施。 你可以得到你需要的硬件,这些都是划算的。
- 速度:可快速委托新资源; 这些服务的性能非常好。
- 可用性:云计算最重要的好处是,您不需要考虑服务的可用性,因为它们是全球可用的。 例如,如果您从印度委托了一台虚拟机,那么您不必担心使用这台机器,即使您可能在世界的另一个地方。
要决定适合您的云提供商,请参考https://azure.microsoft.com/en-in/overview/choosing-a-cloud-service-provider/。
云计算服务模型
云计算服务有很多,但最好的云计算服务类型定义如下(其他类型仅基于这些服务类型):
- 基础设施即服务(IaaS):提供基础设施,即存储、虚拟机等。 更多信息请访问https://azure.microsoft.com/en-in/overview/what-is-iaas/。
- 平台即服务(PaaS):这为开发、测试或管理应用等活动提供了随需应变的环境。 更多信息请访问https://azure.microsoft.com/en-in/overview/what-is-paas/。
- 软件即服务(SaaS):按需提供软件应用。 云计算提供商可能提供各种订阅模型,您可以在这些模型下订阅特定的软件应用。 更多信息请访问https://azure.microsoft.com/en-in/overview/what-is-saas/。
讨论 Azure 环境
Azure 环境提供了一种通过互联网获取各种服务的方法。 以下截图代表了所有云计算服务模型的典型概述:

它展示了 IaaS 是提供服务器和存储的基本模型,SaaS 是提供几乎所有云计算服务的高级模型。
从 Azure
要开始使用 Azure,您需要访问 Azure 门户。 遵循以下步骤:
- 使用以下链接登录 Azure 门户:https://portal.azure.com。
如果你没有 Azure 账户,可以在这里免费创建一个:https://azure.microsoft.com/en-in/free/。
- 登录后,会看到如下截图所示的仪表盘:

Azure portal dashboard
门户仪表板可能与您在前面的屏幕截图中看到的不同。 如果您是第一次登录,那么您可能需要创建资源(根据您的需求)。 在这里,您可以委托您的虚拟机(参见 IaaS),选择一个特定的环境,比如 Windows 机器或 Linux(参见 PaaS),或者部署您的应用(参见 SaaS)。
- 现在你可以做任何你想做的按照你的订阅。
出版社/托管
发布/托管是一种使您的应用公开可用的服务。 应用可以存储在托管提供商提供的服务器上。 在本节中,我们将使用 TFS(现在是 VSTS):参见https://www.visualstudio.com/tfs/。
如果现有项目托管在 TFS 上,则需要迁移它。 详见链接:https://www.visualstudio.com/team-services/migrate-tfs-vsts/。
项目托管
您需要访问 Visual Studio Online/TFS Online(现在是 VSTS)来托管一个项目。 为此,你需要遵循以下步骤:
- 使用您喜欢的浏览器转到https://www.visualstudio.com/vso/。
- 点击 Sign in,如图所示:

VSTS home screen
- 输入您的微软帐户; 如果你没有,你可以创建一个。
- 按照步骤创建您的帐户。
- 你会被重定向到你的 Visual Studio 团队服务帐户页面:

VSTS my profile page
- 单击 Create new project。
- 您将被重定向到一个新页面,在那里您将被要求提供一些与您的项目相关的信息。
- 添加您的项目信息,如下图所示:

Creating a new project
- 选择您的版本控制-您将被给予选择 Git 或团队基础版本控制(TFVC)。
如果你对这些选项感到困惑,请参考这个链接来比较 Git 和 TFVC:https://docs.microsoft.com/en-us/vsts/tfvc/comparison-git-tfvc?view=vsts。
在我们的例子中,选择 Team Foundation Version Control:

- 现在,选择 Work Item Process,参考https://docs.microsoft.com/en-us/vsts/work/work-items/guidance/choose-process?view=vsts以了解更多可用的各种选项。 在我们的例子中,选择 Scrum:

- 现在单击 Create。
- 你会被重定向到一个新创建的项目页面,看起来像下面的截图:

FlixOneProductServices project main screen
- 项目主屏幕是一个快速显示页面,在这里您可以快速地看到所有的活动。
您的项目已经创建,现在可以开始您的项目了。
仪表板
仪表板是一个包含项目活动快照的屏幕。 它告诉你分配给你的任务,显示一个 sprint burndown 图表,项目进度,或任何你在仪表板上配置的东西:

Dashboard of FlixOneProductServices project
在项目仪表板中,您可以通过添加新的或删除现有的小部件来编辑小部件。 前面的屏幕截图显示了我们项目的默认视图。
代码
下面是您可以管理当前项目的实际代码的屏幕:

Code screen of FlixOneProductServices project
您可以查看以下内容:
- Files:存储库中的所有文件。
- 变更集变更集:代码变更带有变更集编号,您还可以获得关于什么变更集针对什么任务/bug 的信息。
- 搁置集:可用于审查或与当前项目相关的任何其他用途的任何搁置更改。
- Pull Requests:这是一种协作方式。 你可以在任何时候发起一个 Pull Request,点击 Code 并选择 New Pull Request,项目的所有者或维护者将被告知这个 Pull Request。
工作
默认情况下,它显示 Work Items 屏幕,向您显示分配给您的项目,或者您正在处理或已经处理的任务/bug。 我们已经创建了一个新的项目,所以你会得到一个空白页; 要从工作项开始,您需要创建一些待定项,然后在团队中分配它们。
单击 Backlog,然后从屏幕上出现的新模板中为 Product Backlog 添加一个标题。 见下图:

New product backlog
你可以在前面的截图中看到,默认情况下,你有六个 sprint。 现在,打开一个新创建的产品待办事项列表项,并提供一个完整的描述—详细信息,例如该工作的工作内容、谁将处理该工作项,或者谁将是该工作项的所有者。 请看下面的截图,它显示了一切:

Product backlog item
现在,进入 Sprint 1 并设置日期——你应该设置开始当前迭代的日期,如下面的截图所示:

类似地,您可以添加更多产品待办事项列表项。 不要将项目移动到 Sprint 1。 Backlog 条目屏幕的视图如下图所示:

Board backlog items
向存储库中添加代码
我们没有在存储库中添加任何代码; 现在,是时候这么做了。 要添加代码,你可以点击 code 并直接从 web 界面上传文件,或者从 Visual Studio IDE 进入源代码控制,在添加存储库后签入代码。 在本节中,我们将使用 Visual Studio 添加代码。 要做到这一点,请遵循以下步骤:
- 打开 Visual Studio
- 创建或打开现有项目
- 开放的团队资源管理器
- 单击连接 TFS 服务器
- 如果找不到服务器,就添加一个 TFS 服务器,然后为它提供一个有效的地址
- 点击连接
下面的截图显示了与 FlixOneProductServices 的连接:

Connecting TFS server
- 您需要将 TFS 存储库映射到本地驱动器。 映射源代码并获得代码:

Mapping and getting source code
- 现在单击“源代码管理资源管理器”,将打开“源代码管理资源管理器”选项卡。 你会看到空的项目节点,如下图所示:

Source Control Explorer
- 在“解决方案控制资源管理器”中,右键单击“解决方案”,然后单击“将解决方案添加到源代码控制”。 参考以下截图:

Add solution to Source Control
- 做出你的选择并选择它:

Add solution to FlixOneProductServices
- 现在你可以看到一个新的文件夹和文件已经被添加到源代码控制中——请看下面的截图:

Newly added project
转到 Team Explorer 并单击 Pending Changes。 您将发现签出的各种文件。
添加一个工作项并添加一条注释,然后单击 Check In:

Check In pending changes
您已经成功地将解决方案添加到源代码控制中。
现在回到您的 TFS Online 并单击 Code。 您将发现最近添加到源代码控制中的所有代码文件/文件夹。 参考以下截图:

Viewing Code
您已经成功地将项目托管到 VSTS。 在下面的部分中,我们将讨论构建和部署。
测试
VSTS 的这个屏幕帮助您创建各种测试计划,以便您可以跟踪一个 sprint 的手动测试。 它有助于监控当前 sprint 的手动测试何时完成。 我们在第 6 章、测试 rest 式 Web 服务中讨论了测试的各种术语。
在接下来的章节中,我们将看到这如何帮助我们通过创建一个测试计划和测试用例来测试我们的冲刺。
创建测试计划
在“测试计划”选项卡上单击+,然后单击“测试计划”,如下图所示:

Creating a test plan
在下一个屏幕中,命名您的测试计划,确保您为测试计划选择了正确的冲刺。 参考以下截图:

Naming your test plan
由于测试计划是针对手动测试的,我们现在必须添加需要测试的待定项。 在您的案例中,我们只是添加了 Sprint 1 待办事项列表项。 在这样做的过程中,我们为 Sprint 1 的所有待定项添加了测试套件。
创建测试用例
在前面的部分中,我们已经创建了迭代 1 测试计划,并向其添加了一个测试套件。 现在,我们需要创建一个测试用例,点击 New 并选择 New test case:

Creating a new test case
通过添加一个有效的名称来完成您的测试用例,并将具有预期输出的步骤添加到测试用例中。 参考以下截图:

Writing a test case
您现在可以将测试人员分配给这些测试用例或测试套件,以便测试人员可以运行这些测试。
运行手动测试
在前面的部分中,我们已经创建了手动运行的测试用例。 单击测试套件并单击 Run 以运行可用的测试。 参考以下截图:

Running manual tests
这些测试将在浏览器窗口中运行,所以请确保您的浏览器不会阻止弹出窗口,如下截图所示:

Verifying manual test results
由于这些测试是手动的,因此您需要手动测试这些测试。 执行这些测试之后,您必须验证预期的输出,并相应地将测试标记为通过或失败。 测试用例的总体结果将显示在测试套件窗口中。
在这个部分中,您已经创建了一个测试计划,一个针对当前迭代的测试套件,以及用于测试特定迭代、代码更改、构建或发布的手动测试用例。
维基
Wiki 页面帮助团队成员一起工作。 这些页面可以包含项目文档或处理项目的指示,比如编码指南,等等。 最初,通过点击 Create Wiki 按钮,你会得到一个空白模板,如下截图所示:

Creating Wiki
从 Create Wiki 页面模板中,您可以添加任意多的页面。 Wiki 页面支持 markdown:https://docs.microsoft.com/en-us/vsts/collaborate/markdown-guidance?view=vsts。
构建和发布选项卡
Build and Release 选项卡提供了为项目创建构建和发布的工具。 在本节中,我们将讨论使用 VSTS 的 CI 和 CD。 最初,不会有任何构建定义。
CI 和光盘
我们已经在前面的小节中讨论了这两种方法。 在本节中,我们将简要讨论 CI 和 CD 之间的区别。
CI 是所有团队成员(开发人员)集成代码的实践。 这发生在每次签入时,每当开发人员推送更改时,或者在代码配置时,CI 触发。 这样做最重要的好处是,它可以在开发周期中节省时间,因为它可以识别冲突(如果存在冲突的话)。 这从设置汽车测试的初始步骤开始。 一旦有人将更改推送到存储库,就会触发构建和测试。
持续部署解决了在部署到生产环境时代码的部署问题。
在线使用 TFS 的 CI 和 CD
在本节中,我们将讨论项目的持续集成。 转到构建选项卡,然后构建定义,然后点击+新建:

Creating a new definition
在下一步中,系统将要求您选择存储库。 选择 Repository Source Control 并映射分支,然后单击 Continue:

Selecting a repository source
在下一个屏幕中,选择你的模板; 在我们的例子中,选择 ASP.NET Core:

Selecting ASP.NET Core template
按照模板的说明,提供所需的值,如下图所示:

Creating build tasks
以下是与 VSTS 有关的基本但重要的概念; 理解这些也很重要,这样你就可以成功地配置构建步骤:
- 任务:这些构建步骤指导 VSTS 执行特定的任务
- 变量:这些构建配置值告诉构建代理关于系统或自定义值
- 触发器:这些触发器根据您是否启用 CI 来启用各种任务
- 选项:这些是构建属性,您可以为此提供构建描述、构建作业超时时间等
- 保留:保留策略可根据需要构建; 典型的策略是您希望将好的或坏的构建保留多久
为了使我们的例子简单,我选择了变更#5 来保存构建定义:

Saving build definition and queue
现在,您可以在 build and Release 选项卡下看到构建结果——您的构建可能没有运行(请重新访问所有步骤); 下面的截图显示了验证的步骤:

Build steps
启动 CD 发布过程
你已经设置好了 CI 流程,现在是时候使用 CD 了。转到 Release 选项卡,点击 New definition 按钮:

Adding release definition
根据您的应用选择一个模板。 参考以下截图:

Selecting Azure App Service Deployment
通过选择您的存储库或构建,将工件添加到发布定义中,请参考以下截图:

Adding an artifact
为您的部署环境、将要使用的 Azure 服务或应用类型设置值,等等。 参考以下截图:

Adding task values
去 Release,你可以看到你的 Release 的状态; 我们只添加了一个版本定义,所以你会看到如下截图:

Release deployment status
我们还没有部署我们的版本,所以它的状态是未部署。 您可以手动触发部署。 这个版本是针对开发环境的——您可以设置任意多的环境。 在本节中,我们看到了如何使用 VSTS 启动 CI 和 CD。
总结
部署术语使团队与其工作保持一致,即使团队工作在不同的地理区域。 在 CI 的帮助下,CD 团队在项目中的任何团队签入后都会立即收到最近的更改,从而保持同步。
在下一章中,我们将讨论日常开发活动中的测试范例。 我们将讨论与测试范例相关的重要术语,包括围绕这些术语的理论,然后我们将讨论具有存根、mock 知识的代码示例,并理解集成和安全性,以及性能测试。
八、保护 RESTful Web 服务
在 web 应用的世界中,有许多通过 HTTP 进行的请求和响应交换,安全性是最重要的横切关注点之一。 任何不安全的服务或 web 应用都可能面临数据篡改问题。
当数据被未经授权的渠道修改(破坏、操纵或编辑)时,这通常被称为数据篡改。
数据在传输过程中或在其他地方时可能被篡改。 数据被篡改可能有几个原因——未受保护的数据是行业中最常见的原因。 为了防止此类问题,您可以保护环境和应用系统。 通常,防火墙是保护环境(服务器)的最佳方法。 可以通过实现授权机制来保护应用。
不幸的是,知名网站的数据泄露如今是司空见惯的事。 考虑到这一点,信息和应用的安全性对于 web 应用来说已经变得至关重要。 出于同样的原因,安全应用不应该再是事后考虑。 安全是组织中每个人的责任。
在本章中,我们将主要关注安全性以及 REST 和 OWASP 安全标准。 在本章结束时,您将了解身份验证、单点登录(SSO)、基于令牌的身份验证和使用代理服务器(如 Azure API Management)的身份验证的概念。 我们将涵盖以下议题:
- web 安全的 OWASP 标准
- 保护 RESTful web 服务
- 身份验证和授权
- 验证
- 数据加密和存储敏感数据
OWASP 安全标准
开放 Web 应用安全项目(OWASP)是一个在线社区,主要通过创建各种研究和标准来解决 Web 应用安全问题。 在本章中,我们将遵循 2017 年发布的 OWASP 安全标准(https://www.owasp.org/index.php/Top_10-2017_Top_10):

Application security risks
上面的图是应用安全风险的图示概述。 它描述了攻击者可能如何攻击较弱的应用。 攻击者通过注入脚本(主要是 JavaScript)并影响系统来攻击应用组件。 在此图像中,您将注意到 web 应用中只有一个不安全的部分受到了攻击。 一个安全的系统是安全的,即使它被攻击了。
以下是 OWASP 定义的应用安全风险:
- 注射
- 破碎的身份验证
- 敏感数据暴露
- XML 外部实体(XXE)
- 破碎的访问控制
- 安全错误配置
- 跨站点脚本(XSS)
- 不安全的反序列化
这些都是高度警惕的安全风险,应该在每个 web 应用中处理。
保护 RESTful web 服务
在开始学习如何保护 RESTful web 服务之前,我想告诉您关于 web 世界中的安全性。 总的来说,短语安全性描述了为确保一切安全而采取的措施。 但是所有都包括什么呢? 让我们来详细说明一下:安全性是一种方法或过程,它可以阻止未经身份验证和未经授权的 web 应用对机密数据的访问。
机密数据的类型取决于 web 应用的性质——例如,如果 web 应用用于医疗和临床服务,那么机密信息包括与他们的测试、医疗历史等相关的所有患者数据。
创建安全流程的第一步是对 web 应用的访问进行身份验证和授权。 如果请求没有经过身份验证,那么系统就不应该接受它。 如果请求已经过身份验证,但未被授权访问 web 应用的数据,则也不应该接受该请求。
下图展示了使用 Auth 服务的认证过程概述:

在前面的图中,您可以考虑一个典型的 ASP.NET Core API 系统,使用 Auth 服务作为中间件服务器。 可能有多个客户机或消费者正在使用这些服务,并可以请求访问数据。 在这里,Auth 服务在验证来自客户机的传入请求时扮演着重要的角色。 如果 Auth 服务将请求标识为经过身份验证的,它将生成一个令牌并将其发送给 API 服务器进行进一步处理。 如果请求不是经过身份验证的请求,则 Auth 服务将身份验证失败通知客户机。 上面的图像只是一个典型场景的概述。 实际的场景可能更复杂,需要使用一个或多个中间件后端服务器(典型的 API 管理服务器)。
在以下几节中,您将更好地了解以下两个重要的安全概念:
- 身份验证:身份验证只是系统通过某种凭证(通常是用户 ID 和密码)验证或标识传入请求的过程。 如果系统发现提供的凭证是错误的,那么它会通知用户(通常通过 GUI 屏幕上的消息)并终止授权过程。
- Authorization:授权总是在认证之后。 它是一个过程,允许提出请求的经过身份验证的用户在验证他们对特定资源或数据的访问之后访问资源或数据。
由此,您可以得出结论:RESTful 服务的安全性是应用最重要的特性。
如何在 RESTful web 服务中维护会话? 基于 rest 的 web 服务使用 HTTP 协议,这是一种无状态协议(https://stackoverflow.com/questions/13200152/why-say-that-http-is-a-stateless-protocol),并将每个请求视为一个新请求。 http 协议无法帮助在 RESTful web 服务中维护会话。 但是,我们可以在经过身份验证的标记的帮助下通过编程实现这一点。 这种技术称为基于令牌的授权(我们将在接下来的章节中详细讨论它)。 在此技术的帮助下,您可以授权经过身份验证的用户在预定义的一段时间内允许使用数据或资源。
每个通过服务或任何其他模式来的请求都应该在系统响应用户或发出调用的客户端之前进行身份验证和授权。 这个过程主要包括以下几个方面:
- 保密性:安全的系统确保任何敏感数据不会暴露给未经身份验证和未经授权的访问请求
- 可用性:系统中的安全措施,通过系统的认证授权机制,确保系统对真实用户的可用性
- 完整性:在一个安全的系统中,数据是不可能被篡改的,因此数据是安全的
不安全的 web 应用的脆弱部分
在当今的 web 应用中,防止未经授权访问的主要脆弱资产是资源和数据。 如果一个站点是不安全的,那么脆弱性的可能性就很高。 根据官方网站https://docs.microsoft.com/en-us/aspnet/core/security/,以下区域是任何不安全的 web 应用的主要威胁。
跨站点脚本攻击
跨站点脚本攻击——或 XSS 攻击——通常是由于错误输入而发生的,攻击者将客户机脚本(大多数情况下是 JavaScript)注入到 web 页面中。 官网(https://docs.microsoft.com/en-us/aspnet/core/security/cross-site-scripting):
"Cross-Site Scripting (XSS) is a security vulnerability which enables an attacker to place client-side scripts (usually JavaScript) into web pages."
在这里,我使用一个使用 web 服务的 web 客户机的示例。 您将在第 10 章、*构建 web 客户端(消费 web 服务)*中了解更多关于 web 客户端的内容。
下面的截图显示了 Create 屏幕受到攻击的场景:

上面的截图表示一个不安全的 web 应用。 在这里,用户可以注入脚本标记,当用户单击 Create 时,脚本标记会被发送回服务器。
下面的截图显示了代码的调试模式,我们可以看到我们的系统正在接受脚本数据:

前面的屏幕截图显示了如何将其发布到服务器,并最终保存到数据库或任何持久存储库中。
受影响页面如下截图所示:

当任何人访问带有受影响数据的页面时,它将显示一个警告,如上面的截图所示。
您可以构建一个系统,通过应用一些代码更改来阻止此类攻击。 我将在验证一节中介绍这一点。
SQL 注入攻击
SQL 注入攻击是直接针对数据库的最严重的攻击之一。 这是 OWASP 应用安全风险列表中的第一个。 攻击者可以利用 SQL 注入窃取系统的安全数据。
下图展示了 SQL 注入的过程:

在上面的图中,您可以看到一个典型的 SQL 注入场景,其中攻击者注入了一个or子句来获取特定表的所有数据。 实际的代码指令是根据雇员表中的EmpId返回一条记录。 但是由于它被注入了一个额外的短语,它返回雇员表的完整记录。 这是不安全系统的最大问题。 这里,攻击者在语句中注入了一个简单子句。
这里在做什么?
在前一节中,您看到了一个假想的场景,并实际经历了一个 SQL 注入。 让我们看一个实际的例子,通过使用 ASP 创建 RESTful 产品 API。 2.0 NET Core。 在你开始构建这个应用之前,记住这个应用的以下先决条件:
- Visual Studio 2017 更新 3 或更高版本
- ASP.NET Core 2.0 或更高版本
- c# 7.0 或更高版本
- 微软实体框架核心 2.0.0
按照以下步骤创建我们的应用:
- 打开 Visual Studio。
- 选择文件|新|项目或按Ctrl+Shift+F5。
- 选择 ASP.NET Core Web 应用。
- 从模板窗口中,选择 ASP.NET Core API。 确保你选择了。net Core 2.0。
- 命名项目,选择解决方案的路径,然后单击 OK。
- 添加
Models文件夹。 在解决方案探索中,右击,从下拉菜单中选择 Add New Folder,并将其命名为Models。 - 在
Models文件夹下添加一个新类。 右键单击“解决方案资源管理器”中的Models文件夹,从下拉菜单中选择“添加新项目|类”,或者使用Shift+Alt+C。
请注意,快捷键根据您对 Visual Studio 的设置而不同。
- 将其命名为
Product.cs,并将以下代码添加到该类中:
namespace Chap08_02.Models
{
public class Product
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Image { get; set; }
public decimal Price { get; set; }
public Guid CategoryId { get; set; }
public virtual Category Category { get; set; }
}
}
- 重复步骤 7 和步骤 8,加入
Category.cs和ProductViewModel.cs。 - 重复步骤 6,添加
Contexts文件夹。 - 在
Contexts文件夹下添加一个新类。 右键单击“解决方案资源管理器”中的Contexts文件夹,选择“添加新项目”,然后在对话框中选择“类”,或者使用Shift+Alt+C - 命名为
ProductContext.cs。 - 现在,打开
appsettings.json文件并添加以下代码:
"ConnectionStrings":
{
"ProductConnection": "Data Source=.;Initial
Catalog=ProductsDB;Integrated
Security=True;MultipleActiveResultSets=True"
}
在解决方案资源管理器中右键单击项目并选择管理 NuGet 包。
在 NuGet Package Manager 界面下,搜索
Swashbuckle.ASPNETCore并安装。添加名为
Persistence的新文件夹。在
Persistence文件夹下添加IProductRepository接口。在
IProductRepository接口中添加以下代码:
namespace Chap08_02.Persistence
{
public interface IProductRepository
{
void Add(Product product);
IEnumerable<Product> GetAll();
IEnumerable<Product> GetByProduct(string id);
IEnumerable<Product> GetBy(string productName);
void Remove(string id);
void Update(Product product);
}
}
参考 GitHub 库链接https://github.com/PacktPublishing/Building-RESTful-Web-Services-with-DotNET-Core获取完整的源代码。
- 在
Persistence文件夹下添加ProductRepository.cs类。 - 将以下代码添加到
ProductRepository.cs:
public IEnumerable<Product> GetByProduct(string id) => _context.Products.FromSql("SELECT * FROM dbo.Products WHERE id="+ id).Include(p => p.Category)
.ToList();
- 打开
Startup.cs文件,添加以下代码:
services.AddScoped<IProductRepository, ProductRepository>();
services.AddDbContext<ProductContext>
(
o => o.UseSqlServer
(
Configuration.GetConnectionString("ProductConnection")
)
);
services.AddSwaggerGen
(
swagger =>
{
swagger.SwaggerDoc("v1", new Info { Title = "Product APIs",
Version = "v1"
});
});
- 现在,您可以开始使用应用了。 从菜单中运行应用或点击F5。 在 web 浏览器中,在地址栏的 URL 中添加
/swagger后缀,如下图所示:

它将显示 Swagger API 文档,如下面的截图所示:

Swagger documentation for the Product APIs I used Swagger for the documentation and to test API calls. You can use other API test clients, such as Advanced Rest Client and PostMan.
- 要测试我们的不安全代码,点击
GET``/api/product/{productid}资源,通过产品 ID,如下截图所示:

- 单击执行。 您应该看到以下预期输出:

现在,让我们试着添加OR子句,看看会发生什么。 这里,我将productid值输入为4D261E4A-A657-4ADD-A0F6-DDE6E1464D55或1=1。 执行它并查看结果:

这里,我们可以看到我们的应用受到了 SQL 注入的影响。 您将看到表中的所有记录。 这是因为我们正在使用的原始 SQL 查询(参见https://docs.microsoft.com/en-us/ef/core/querying/raw-sql以获得更多信息)。 仔细查看代码后,您可以找到导致早期结果的原因。 下面的截图会提醒你请求的 URL 是什么:

这是注入的 URL。 当我们的存储库的GetByProduct(string id)方法执行时,它创建了以下原始 SQL 查询:
SELECT [p].[Id], [p].[CategoryId], [p].[Description], [p].[Image], [p].[Name],
[p].[Price],[p.Category].[Id],[p.Category].[Description], [p.Category].[Name]
FROM (SELECT * FROM dbo.Products WHERE id='4D261E4A-A657-4ADD-A0F6-DDE6E1464D55' or 1=1) AS [p]
INNER JOIN [Categories] AS [p.Category] ON [p].[CategoryId] = [p.Category].[Id]
这表明攻击者非常聪明和安静地完成了这项工作。 我们的不安全代码释放并返回目标表的全部数据。 我使用 SQL 分析器(https://docs.microsoft.com/en-us/sql/tools/sql-server-profiler/sql-server-profiler)来跟踪查询。
修复 SQL 注入攻击
没有什么比不安全的代码更危险了。 使用不安全的代码,应用总是处于危险之中。 攻击者可以在任何时候窃取数据,通过篡改请求来强行操纵数据。
Saineshwar Bageri 写了 10 个创建安全 ASP 的技巧。 净的 web 应用。 你可以在https://www.codeproject.com/Articles/1116318/Points-to-Secure-Your-ASP-NET-MVC-Applications上阅读。
你可以用以下两种技术来阻止 SQL 注入攻击:
- 验证:我们将在本章的后面讨论这些。
- 在原始 SQL 查询中使用参数:除了通过连接值直接使用原始 SQL 查询外(请参考我们的不安全代码示例)。 这样,可以将
GetByProduct(string id)方法重写如下:
public IEnumerable<Product> GetByProduct(string id) => _context.Products
.FromSql("SELECT * FROM dbo.Products WHERE id={0}", id)
.Include(p => p.Category)
.ToList();
前面的代码仍然包含一个原始 SQL 查询,但是它足够安全,可以处理任何注入的代码。 如果您尝试我们前面使用的相同参数值,那么修改后的代码将不会接受它。 它会抛出一个异常,如下面的截图所示:

如果你的 EF Core 版本是 2.0.0 或更高版本,你也可以在原始 SQL 查询中使用字符串插值语法。 使用字符串插值,代码如下所示:
public IEnumerable<Product> GetByProduct(string id) => _context.Products
.FromSql($"SELECT * FROM dbo.Products WHERE id={id}")
.Include(p => p.Category)
.ToList();
- 数据加密:我们将在本章后面讨论这个问题。
跨站点请求伪造
跨站请求伪造(CRSF)也可以缩短为XSRF。 这是一种常见的攻击,攻击者在客户机与宿主应用交互(请求/响应)时注入不需要的动作。 通常,攻击者使用恶意代码来影响交互。
恶意代码是将下载到 web 浏览器并执行的脚本代码,即使不知道经过身份验证的用户。 详情请参见https://www.techopedia.com/definition/4013/malicious-active-content。
攻击者非常聪明,他们使用不同的平台提供指向恶意代码的虚假链接。 这些链接与受到攻击的域名(网站)非常相似。 金融网站是主要目标。
下图描述了 XSRF 攻击:

攻击者可以通过电子邮件、社交媒体或任何其他媒体发送链接。 当用户单击该链接时,他或她将处于攻击者的世界中,而不知道这是一个虚假的站点,而不是他们的目标站点。
官方网站:https://docs.microsoft.com/en-us/aspnet/core/security/anti-request-forgery。
CSRF 漏洞基本上是 web 应用的问题,而不是终端用户的问题。
要处理这类攻击,您需要构建一个安全的、经过正确身份验证的系统。 在下一节中,我将重点介绍身份验证的细节。
正在进行的身份验证和授权
到目前为止,您已经了解了身份验证和授权的基础知识。 在本节中,您将看到保护应用的这两种最重要的方法。
基本身份验证、基于令牌的授权和其他身份验证
无论何时谈论安全的 web 服务/web 应用,您都应该考虑我在前面几节中提到的关于身份验证和授权的所有要点。
在本节中,我将讨论实现阶段的身份验证和授权。
基本身份验证
是显而易见的从基本这个词,基本身份验证系统包括一种机制,要求简单的凭据(用户名和密码)进行身份验证或验证用户通过传入的请求从客户端 web 通过 rest 风格的 web 服务或应用服务器(在我们的例子中,ASP.NET Core web api)。
*考虑下面的图,它展示了基本的身份验证:

上图展示了我将在代码中实现的基本 HTTP 身份验证。 在这里,来自客户机的请求访问受保护的资源(不允许公共访问的资源)。 请求在服务端头中包含用户名和密码,服务通过验证其存储库(通常是数据库存储库)中的用户名和密码来检查它是否是一个验证请求。 如果验证了用户,服务将在其响应中返回数据给客户端; 否则,它将返回 HTTP 状态码 401 的无效凭据。
您可以在https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html找到 HTTP 状态码及其定义的完整列表。
基本身份验证的安全问题
基本身份验证,顾名思义,是一种非常基本的身份验证机制,在阻止攻击者时并不太安全。 在此,我简要记下了以下认证过程中的安全漏洞:
- 凭据:所需的凭据是最重要的安全问题,可能导致安全漏洞,进而进一步利用系统的薄弱安全性。
- 请求:请求可能被篡改,并可能导致严重的安全漏洞; 通过基本身份验证,每个请求都携带凭证(用户名和密码),可以对其进行篡改并用于进一步利用系统。
- 关闭浏览器会话:这里应该有一个高优先级的问题——没有能力使用基本身份验证方法从应用注销,除非用户关闭浏览器以销毁自己的浏览器会话。
更多信息请访问官方网站https://docs.microsoft.com/en-us/aspnet/web-api/overview/security/basic-authentication。
"Basic authentication is also vulnerable to CSRF attacks. After the user enters credentials, the browser automatically sends them on subsequent requests to the same domain, for the duration of the session."
在 web 应用安全性方面,可能存在更多的安全问题,使基本的身份验证机制成为最弱的。
基本身份验证导致各种安全问题。 我不会展示这些代码示例使用基本身份验证,但是如果你仍然想要测试基本身份验证机制,那么我建议你从分叉的 GitHub 库中提取的代码在 https://github.com/garora/Bazinga.AspNetCore.Authentication.Basic。
基于符号的授权
我在本章的前几节中解释了授权,在那里您看到授权是身份验证之后访问受限资源的下一步。
让我们考虑下面的图,它描述了基于令牌的认证:

上图显示了基于令牌的身份验证。 如果验证了请求(取决于凭据的标识),那么客户机将发送一个带有凭据和返回的令牌的请求。 然后客户端存储此令牌。 然后,它在每个请求中发送这些带有头的令牌,直到令牌有效为止。 如果它被授权访问安全资源,服务器将验证请求检查并使用数据进行响应。 在某些情况下,客户端可能请求一个新令牌,或者在现有令牌过期时调用一个刷新令牌。
让我们添加一个AuthRequet模型,就像在前面创建的 API 项目中所示:
public class AuthRequest
{
public string UserName { get; set; }
public string Password { get; set; }
}
在Controller文件夹中添加一个新的GenerateTokenController.cs控制器。 以下是我们的GetTokenPOST 资源:
[AllowAnonymous]
[HttpPost]
public IActionResult RequestToken([FromBody] AuthRequest request)
{
//Kept it simple for demo purpose
var user = _loginRepository.GetBy(request.UserName,
request.Password);
if (user == null) return BadRequest("Invalid credentials.");
var token = new TokenUtility().GenerateToken
(
user.UserName,
user.Id.ToString());
return Ok(new
{
token = new JwtSecurityTokenHandler().WriteToken(token)
}
);
}
您注意到前面代码中的[AllowAnonymous]属性了吗? 您将在后面的小节中看到它。 在前面的代码中,我只是验证凭证,如果凭证有效,则TokenUtility 中间件生成令牌。
下面是TokenUtility代码:
public JwtSecurityToken GenerateToken(string userName, string userId)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, userName),
new Claim(JwtRegisteredClaimNames.Jti, userId)
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtKey));
var creds = new SigningCredentials(key,
SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(TokenIssuer,
TokenIssuer,
claims,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: creds);
return token;
}
前面的代码是不言自明的:它生成标记。 在这段代码中,我将JwtKey作为常数(仅为演示目的)。
在生产环境中,应该将JwtKey保存在环境变量中(出于安全原因),并且可以很容易地访问,例如string jwtKey = Environment.GetEnvironmentVariable("JwtKey");。
我不打算讨论存储库模型和其他类似的方法,因为它们是不言自明的。 您可以从 GitHub 存储库https://github.com/PacktPublishing/Building-RESTful-Web-Services-with-DotNET-Core中提取整个源代码。
要启用基于令牌的身份验证,需要在startup.cs文件中做一些更改,在app.UseMvc();之前的Configure方法中插入Add app.UseAuthentication();。
在ConfigureService方法中,添加以下代码:
services.AddAuthentication()
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = "gaurav-arora.com",
ValidAudience = "gaurav-arora.com",
IssuerSigningKey = new
SymmetricSecurityKey
(
Encoding.UTF8.GetBytes("abcdefghijklmnopqrstuvwxyz")
)
};
});
构建并运行项目。 让我们使用 Swagger 文档进行一个简单的测试,如下面的截图所示:

这将在一个有效的请求上给出令牌:

在接下来的部分中,我们将根据用户的角色和访问级别制定一个复杂的授权过程来访问资源。
其他身份验证方法
除了基本的和基于令牌的身份验证之外,您还可以使用其他可用的身份验证机制(我们不会详细讨论这些机制,因为它们超出了本书的范围)。 IdentityServer4 是最著名的身份验证服务器之一,它支持将身份验证作为服务、单点登录/签出以及许多其他选项(有关更多信息,请参阅https://identityserver4.readthedocs.io/en/release/)。
使用注释保护服务
ASP.NET Core 提供了各种方法来创建安全的应用注释(数据注释)。 这个选项是我们为 web 应用保护模型的方法之一。 数据注释提供了一种在客户端或服务器端验证输入的方法。
验证
从名称验证可以很容易地看出,它们只是用户/客户端输入的验证器。 用户输入可以在客户端或 API 端(服务器端)进行验证。 在 RESTful 服务中,您可以在数据注释的帮助下使用模型验证来验证输入。
如果验证了模型,这并不能保证随请求而来的数据是安全的。
在本节中,我们将重写在前一节的代码示例中使用的模型。
下面是修改后的ProductViewModel代码:
public class ProductViewModel
{
public Guid ProductId { get; set; }
[Required]
public string ProductName { get; set; }
[Required]
public string ProductDescription { get; set; }
public string ProductImage { get; set; }
[Required]
public decimal ProductPrice { get; set; }
[Required]
public Guid CategoryId { get; set; }
public string CategoryName { get; set; }
public string CategoryDescription { get; set; }
}
在使用注释时不要忘记包含System.ComponentModel.DataAnnotations名称空间。
在前面的代码中,我使用了一个非常简单的注释required属性。 这确保了我们的模型具有所需的属性。
以下是我们添加新产品项目的Post资源:
[HttpPost]
[Route("addproduct")]
public IActionResult Post([FromBody] ProductViewModel productvm)
{
if (productvm == null)
return BadRequest();
var productModel = ToProductModel(productvm);
_productRepository.Add(productModel);
return StatusCode(201, Json(true));
}
编译应用并运行它,以测试数据注释对模型的影响。 这一次,您可以使用 PostMan(https://getpostman.com/)来测试 API。
下面的屏幕截图显示了addproduct POST资源; /api/product/addproductAPI 用于保存产品:

POST request using Swagger
前面的输入是有效的; 您已经提供了所有需要的值。 让我们删除Price和ProductName。 您将看到,在执行请求时没有任何更改,即使没有提供所需的值。 这里,验证失败。 这些数据注释不影响过程的原因是您没有指示系统验证输入。 为了验证输入,您应该明确地告诉系统您想要验证什么,例如,模型状态。 我们将在下一节中通过过滤器的帮助来解决这个问题。
保护环境
在filter属性的帮助下,您可以在上下文级别实现安全性。 在本节中,我们将重写模型和 API 资源以实现过滤器/属性。
在前一节中,我们在Product模型中使用了Required属性,但这并不适用于我们。 在本节中,我们将在过滤器的帮助下解决这个问题(有关过滤器的更多信息,请访问https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters)。
让我们添加一个自定义过滤器来验证输入,检测是否缺少任何必需的字段。 如果是,它只会抛出一个异常。 您需要修改之前的代码,将Product模型更改为如下:
[Required(ErrorMessage = "Product Name shoud not be empty.")]
public string ProductName { get; set; }
只需在所需的ProductName 字段中添加ErrorMessage; 模型的其他属性保持不变。
现在,从解决方案资源管理器中在项目中添加一个新的Filters 文件夹。 要做到这一点,请执行前面关于SQL 注入的步骤,并使用以下代码在这个文件夹中添加一个名为ValidateInputAttribute.cs的新类:
namespace Chap08_04.Filters
{
public class ValidateInputAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext
context)
{
if (!context.ModelState.IsValid)
context.Result = new BadRequestObjectResult
(context.ModelState);
}
}
}
将此筛选器作为属性应用于Post资源以添加产品。 我们的代码应该如下所示:
[HttpPost]
[Route("addproduct")]
[ValidateInput]
public IActionResult Post([FromBody] ProductViewModel productvm)
{
if (productvm == null)
return BadRequest();
var productModel = ToProductModel(productvm);
_productRepository.Add(productModel);
return StatusCode(201, Json(true));
}
运行应用,输入不带产品名称的新产品值,如下截图所示:

处理请求并查看来自服务器的响应,如上面的截图所示。 请求将不被处理,服务器将发出响应,通知您有一个错误的请求和相关的错误消息。
下面的屏幕截图显示了 Bad Request 响应(HttpStatus Code 400):

要使任何过滤器在任何地方都可用,你应该在startup.csconfigure 方法中添加以下代码,像这样:
services.AddMvc(option =>
{
option.Filters.Add(typeof(ValidateInputAttribute));
});
现在,让我们回到基于令牌的授权方法。 在前面的应用中,您看到了如何构建一个 API 来验证凭证。 现在,让我们制定一个限制资源的策略。 对这个问题的详细解释超出了本书的范围; 建议您参考官方文档https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies。
数据加密和存储敏感数据
在任何应用中,数据安全性都是一个大问题,在编写或设计应用时,它是一个高优先级。 您可以使用任何散列算法通过加密和解密来保护数据,但这会导致性能下降。 ASP.NET Core 提供了一种利用 asp.net 实现数据保护的方法.NET DataProtection(https://www.nuget.org/packages/Microsoft.AspNetCore.DataProtection.Abstractions/)NuGet 包。
A complete explanation of this topic is beyond the scope of this book. You can refer to https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/ for further information.
敏感数据
在使用 API 时,必须存储敏感数据:API 密钥、秘密密钥、用户名、密码等等。 下面是一些建议,当你在 ASP 中处理这些数据时,你需要考虑.NET Core 应用:
- 您应该将配置文件与代码分开。
- 您应该避免将这些数据存储在纯文本文件中。
- 您可以使用单独的类文件,其中可以以常量的形式存储这些数据值。
- 您应该将机密数据存储在环境变量中。 更多信息请参考http://www.dowdandassociates.com/blog/content/howto-set-an-environment-variable-in-windows-command-line-and-registry/。
- 您还可以使用一个秘密管理器来存储您的机密数据(https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?tabs=visual-studio#secret-manager)。
敏感数据因应用和需求的不同而不同。 详情请参考https://stormpath.com/blog/store-protect-sensitive-data-dotnet-core。
总结
在本章中,我们讨论了数据安全性,遵循了 OWASP 安全标准,并了解了 JWT 身份验证。 我们还使用一个代码示例讨论了自定义过滤器和输入验证。 对于任何 web 应用来说,数据保护总是一个高度优先级。 我们讨论了在 ASP 中存储敏感数据时的数据保护方法.NET Core 应用。
在下一章中,我们将讨论 web 服务的性能,介绍向内扩展、向外扩展的方法以及一些缓存机制的实现。*
七、扩展 RESTful 服务——Web 服务的性能
在网络世界中,每个人都在编写或寻找一个网络应用。 随着需求的增加,每个 web 应用都需要能够服务更多的请求——有时一天数千个请求。 因此,应该编写应用来处理这种巨大的请求。
例如,假设您是负责开发公司旗舰产品 FlixOne Store 的开发和支持团队的一员。 该产品很受欢迎,并获得了吸引力,导致您的电子商务网站(FlixOne)被消费者流量淹没。 你系统里的支付服务很慢,几乎导致整个系统瘫痪,导致你失去客户。 虽然这是一个想象的场景,但它可以发生在现实生活中,并可能导致业务损失。 为了避免这种情况,您应该考虑 FlixOne 应用的可伸缩性。
可伸缩性是关键系统最重要的非功能需求之一。 用几百个事务服务几个用户与用几百万个事务服务几百万个用户是不一样的。
在本章中,我们将一般地讨论可伸缩性。 我们还将讨论如何扩展 RESTful 服务,在设计它们时要考虑什么,以及如何使用不同的模式(包括对常规应用也有帮助的技术、库和工具)来避免级联故障。
读完本章,你将会学到:
- 聚类
- 负载平衡
- 可伸缩性简介
聚类
集群是在多个服务器上提供相同服务的一种方法。 通过添加更多的服务器,您可以避免不受控制的情况,例如故障转移、系统崩溃等等。 在数据库上下文中,集群指的是多个服务器实例连接到单个服务器的能力。 容错和负载均衡是集群的两个主要优点。
负载平衡
在集群时,负载平衡器是一个有用的工具。 您可以将负载平衡定义为一种设备,它帮助在集群服务器内或跨集群服务器分发网络或应用流量,并提高应用的响应能力。
在实现中,负载均衡器被放置在客户机和服务器之间。 它有助于在多个服务器上平衡多个应用请求。 换句话说,负载均衡器减少了单个服务器的时间,并防止了应用服务器故障。
它是如何工作的?
负载平衡器的工作是确保应用的服务器可用。 如果一个应用的服务器不可用,负载均衡器会将所有新请求重定向到可用的服务器,如下图所示:

在上面的图中,您可以看到负载均衡器在其典型环境中,其中一个系统接受来自互联网上不同来源的多个请求,然后由负载均衡器从多个服务器管理这些请求。
In .NET, this arrangement is also known as a web farm (https://www.codeproject.com/Articles/114910/What-is-the-difference-between-Web-Farm-and-Web-Ga).
负载均衡器使用各种算法,也称为负载均衡器方法:最小连接法、轮询法、最小响应时间法、最小带宽法、最小数据包法、自定义负载法等等。
负载均衡器在应用的可伸缩性中扮演着重要的角色,因为它确保应用的服务器对服务器请求可用。 请注意,您将需要在不更改代码的情况下安排硬件基础设施,以满足负载平衡器的需求(然而,有些场景需要更改代码)。 市场上有很多负载平衡器,如 Incapsula (https://www.incapsula.com/),F5 (https://www.f5.com/),Citrix Netscaler (https://www.citrix.com/),直流发电机(https://dyn.com/),亚马逊弹性负载平衡,和亚马逊 ELB(【显示】https://aws.amazon.com/)。
在接下来的章节中,我们将研究扩展系统的不同方法。
介绍了可伸缩性
每个应用都有自己的服务请求的能力。 应用的能力是指它的性能以及在负载增加时它如何满足其目标。
许多 web 应用将此称为在规定时间内的请求数。
在设计 web 应用时,做出正确的设计决策是非常重要的; 设计决策会影响服务的可伸缩性。 一定要达到适当的平衡,以便您的方法考虑服务及其基础设施以及任何伸缩需求。
性能和可伸缩性是系统的两个不同特征。 性能处理的是系统的吞吐量,而可伸缩性处理的是为更多的用户或更多的事务提供所需的吞吐量。
缩放(垂直缩放)
向或向上扩展(也称为垂直扩展)是通过向同一台机器添加更多资源(如内存或更快的处理器)来实现可伸缩性的一种方法。 这并不总是适用于所有应用,因为成本也是考虑垂直扩展时的一个因素。
您还可以升级资源或硬件,而不是向您的机器添加新资源。 例如,如果您有 8gb 的 RAM,您可以将其升级到 16gb,同样的事情也适用于处理器和其他资源。 不幸的是,随着硬件的升级,您可以对机器进行多大的扩展是有限制的。 这可能导致简单地转移瓶颈,而不是解决提高可伸缩性的真正问题。
您还可以将应用迁移到完全不同的机器上,例如简单地将应用迁移到更强大的 MacOS 上。
垂直扩展不涉及任何代码更改,所以它是一项简单的任务,但它涉及额外的成本,因为它是一项相当昂贵的技术。 Stack Overflow 是那些基于. net 的垂直伸缩系统中罕见的例子之一。
向外缩放(水平缩放)
向上扩展、向外扩展或水平扩展会向服务请求添加更多的服务器或节点,而不是资源。 如果不想扩展应用,总有办法向外扩展。
当应用代码不依赖于它所运行的服务器时,向外扩展是一种成功的策略。 但是,如果请求需要在特定的服务器上执行,也就是说,如果应用代码具有服务器亲和性,则很难向外扩展。 对于无状态代码,它更容易在任何服务器上执行。 因此,当无状态代码在水平伸缩的机器或集群上运行时,可伸缩性得到了提高。由于横向缩放的性质,它是整个行业常用的方法。 有许多用这种方式管理大型可伸缩系统的例子,如谷歌、Amazon 和 Microsoft。
线性可伸缩性
线性可伸缩性是指应用 Amdahl 定律(https://en.wikipedia.org/wiki/Amdahl%27s_law)垂直扩展应用。 在这里,您还可以考虑并行计算。
并行计算是一种计算体系结构,它表示多个处理器同时执行处理。
在你的应用中线性可伸缩性的好处包括:
- 不需要更改代码
- 可以很容易地添加额外的资源
- 有物理可用性
分布式缓存
在分布式缓存技术的帮助下,我们可以提高 RESTful web 服务(web API)的可伸缩性。 分布式缓存可以存储在集群的多个节点上。 分布式缓存提高了 web 服务的吞吐量,因为缓存不再需要对任何外部资源进行 I/O 访问。
这种方法有以下优点:
- 客户得到相同的结果
- 分布式缓存由持久性存储进行备份,并作为不同的远程进程运行; 即使应用服务器重新启动或出现任何问题,也不会影响缓存
- 源数据存储所发出的请求较少
缓存持久化数据(数据层缓存)
与应用性能类似,您还应该考虑数据库性能。 通过缓存持久化数据,在向数据库添加缓存层后,您将获得更好的性能。 当应用中大量使用读请求时,这一点也很重要。 现在我们将以 EF Core 的缓存级别为例。
一级缓存
这是 EF Core 启用的内建会话缓存。 从服务的第一个请求开始,从数据库中检索对象并存储在 EF Core 会话中。 换句话说,EF Object Context 和 DbContext 维护它们所管理的实体的状态信息。 一旦上下文不再可用,它的状态信息也就消失了。
第二级缓存
对于以分布式方式开发的应用,或者需要持久化数据的长时间运行的请求,如 web 应用,二级缓存是很重要的。 二级缓存存在于事务或应用的范围之外,并且这些缓存可用于任何上下文或实例。 您可以使用应用可用的缓存机制,而不用编写自己的代码,比如 Memcached。
应用缓存
应用缓存或应用层缓存有助于缓存应用中的任何对象。 这进一步提高了应用的可伸缩性。 在下一节中,我们将讨论各种可用的缓存机制。
CacheCow
当你想在客户端和服务器上都实现 HTTP 缓存时,就会用到 CacheCow。 这是一个轻量级的库.NET web API 支持目前是可用的。 CacheCow 是开源的,并带有一个 MIT 许可,可以在 GitHub 上获得(https://github.com/aliostad/CacheCow)。
要开始使用 CacheCow,你需要通过以下步骤为服务器和客户端做好准备:
在你的 ASP 中安装
Install-Package CacheCow.ServerNuGet 包.NET Web API 项目; 这将是您的服务器。在客户端项目中安装
Install-Package CacheCow.ClientNuGet 包; 客户端应用将是 WPF、Windows 窗体、控制台或任何其他 web 应用。创建一个缓存存储。 您需要创建一个缓存
store
在服务器端,需要一个数据库来存储缓存元数据(https://github.com/aliostad/CacheCow/wiki/Getting-started#cache-store)。
Memcached
Memcached 是一个可定制的开源项目; 您可以使用源代码,并根据您的需求添加和更新它。 Memcached 的官方页面(https://memcached.org/)定义为:
"An in-memory key-value store for small chunks of arbitrary data (strings, objects) from results of database calls, API calls, or page rendering." Refer to https://www.deanhume.com/memcached-for-c-a-walkthrough/ for a complete walkthrough.
Azure 复述,缓存
Azure Redis Cache 是建立在一个叫做 Redis(https://github.com/antirez/redis)的开源存储之上的,它是一个内存中的数据库,并在磁盘上持久化。 根据微软的描述(https://azure.microsoft.com/en-in/services/cache/):
"Azure Redis Cache is based on the popular open source Redis cache. It gives you access to a secure, dedicated Redis cache, managed by Microsoft and accessible from any application within Azure."
开始 Azure Redis 缓存是非常简单的,如果你采取以下步骤:
创建一个 web API 项目。 参考前面章节中的代码示例。
实现复述。 转诊点请使用https://github.com/StackExchange/StackExchange.Redis。 另外,安装
Install-Package StackExchange.RedisNuGet 包。更新 CacheConnection 的配置文件(https://docs.microsoft.com/en-us/azure/redis-cache/cache-dotnet-how-to-use-azure-redis-cache#NuGet)。
然后,在 Azure(https://docs.microsoft.com/en-us/azure/redis-cache/cache-web-app-howto#publish-and-run-in-azure)上发布。
通信(异步)
沟通这个术语是不言自明的; 它是服务之间的交互行为。 这方面的例子包括如下:
- 在同一应用中与另一个服务通信的服务
- 与应用外部的另一个服务通信的服务(外部服务)
- 与组件(内部或外部)通信的服务
当消息或数据通过线路时,该通信通过 HTTP 协议进行。
应用的性能会影响服务之间的通信方式。
异步通信是帮助扩展应用的方法之一。 在 ASP。 . NET Core,我们可以通过使用异步 HTTP 调用(异步编程):https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/
在处理异步通信时,您应该小心操作,例如,在添加带有映像的新产品时。 设计了一个系统,可以创建不同大小的图像的缩略图。 这是一项耗时的任务,如果处理不当,可能会导致性能下降。 从设计的角度来看,异步操作在此场景中不起作用。 在这里,您应该实现类似于带有回调的任务之类的东西,它告诉系统作业何时完成。 有时,您可能还需要中间件来处理请求。
实现异步通信的最佳方法是使用异步 RESTful API。
在创建可伸缩系统时,必须始终考虑异步通信。
总结
在本章中,我们讨论了可伸缩性,包括可用的库、工具等等。 然后,我们讨论了如何扩展 RESTful 服务,在设计它们时要考虑什么,以及如何使用不同的模式避免级联失败。
在接下来的章节中,我们将讨论并构建一个 web 客户机来调用和使用 RESTful 服务。
十、构建 Web 客户机——使用 Web 服务
到目前为止,在本书中,我们已经创建了 RESTful 服务,以便我们可以在项目内部或外部调用或使用这些服务。 在本章中,我们将讨论这些服务的一些用例,以及使用 RESTful web 服务的技术和方法。
在本章中,我们将涵盖以下主题:
- 使用 RESTful web 服务
- 构建一个 REST web 客户机
使用 RESTful web 服务
到目前为止,我们已经创建了 RESTful 服务,并在代码示例的帮助下讨论了服务器端代码。 我们使用了外部第三方工具,如 Postman 和 Advanced RESTClient,来使用这些服务。 我们还使用了假对象并在单元测试期间使用了这些服务。 虽然这些使用示例很有帮助,但它们并没有显示 RESTful 服务的真正实力,因为它们要么测试了其功能,要么验证了其输出。
在某些情况下,您可能需要在类似于控制器的另一个应用中使用或使用这些服务,甚至需要在您自己的应用中使用这些服务。 这些应用可以是以下任何一种:
- 基于控制台的
- 基于 Web 的
- 移动或任何其他设备
让我们来看看我们已经讨论过的应用之一:假设在集成在线支付系统时,您需要某种机制来实现或使用外部 API(在本例中是 PayPal)。 在这种情况下,我们已经讨论过的外部工具,比如 Postman 和 Advanced RESTClient,就无能为力了; 为了满足您的需求,您需要一个 REST 客户机。
下图说明了如何在 HTTP 客户机的帮助下使用 REST 客户机使用服务。 在下面的图中,REST 客户机正在与用 ASP 开发的外部服务和网络服务交互(请求、响应).NET Core 或位于相同或不同的服务器上。

Web、Console、Mobile等是在 REST 客户端帮助下使用这些服务的客户端。
现在,我们将讨论如何构建一个 REST 客户机,以便使用应用中的其他 RESTful web 服务(即 api)。
构建一个 REST web 客户机
基于 rest 的服务可能是 web 应用的一部分,也可能不是。 web 应用可以调用或使用来自同一应用的外部 api 或服务。 在服务和使用这些服务的应用之间实现交互或通信(请求、响应)的程序称为客户机。
客户机帮助应用与 api 进行通信(请求、响应)。
在本节中,我们将创建一个 web 客户机。 web 客户端是用 ASP 编写的应用或程序。 净的核心。
在构建测试 web 客户端之前,我们需要讨论必须调用什么。
继续我们的 FlixOne 书店示例,下表列出了我们将调用和消费的产品和服务:
| API 资源 | 描述 |
| GET /api/product | 获取产品列表。 |
| GET /api/product{id} | 得到一个产品。 |
| PUT /api/product{id} | 更新现有产品。 |
| DELETE /api/product{id} | 删除现有产品。 |
| POST /api/product | 添加新产品。 |
我们的 FlixOne 产品服务旨在完成以下任务:
- 添加新产品
- 更新现有产品
- 删除现有产品
- 检索一个产品
我们已经确保 Swagger 支持我们的产品 api(更多信息请参阅前面的章节),所以让我们开始吧。 要开始这个项目,请遵循以下步骤:
- 首先,运行 Visual Studio 2017
- 选择文件|打开
- 选择项目 flixon.com . bookstore . productservice
- 通过按F5或直接从菜单中单击来运行项目
- 输入以下 URL:
http://localhost:10065/swagger/
现在,您应该看到产品 api 的 Swagger 文档,如下面的截图所示:

Product APIs documentation
烹饪 web 客户端
我们已经讨论了需要使用哪些 api 以及哪些资源返回什么,所以现在是时候烹饪我们的 web 客户端了,这样我们就可以使用和调用我们的产品 api。 要做到这一点,请遵循以下步骤:
- 要为我们的新项目创建一个全新的解决方案,转到文件|新|项目(或按Ctrl+Shift+N),如下截图所示。

- 从新项目中,选择 ASP.NET Core Web 应用。
- 将项目命名为
FlixOne.BookStore.WebClient,然后单击 OK,如下图所示:

- 从 ASP。 选择“Web Application”,单击“OK”,如下图所示:

- 现在运行项目调试|开始调试或按F5键。
- 你现在应该看到一个默认的网站模板。
- 现在我们将使用RestSharp创建一个 web 客户端。 我们需要添加对 RestSharp 的支持,以获得通过 HTTP 协议调用 API 资源的功能。
RestSharp 是一个轻量级的 HTTP 客户端库。 您可以根据需要进行更改,因为它是一个开源库。 您可以在https://github.com/restsharp/RestSharp找到完整的存储库。
- 使用 Open package Manager(右键单击 Solution Explorer 中的 Solution)添加一个 NuGet 包,如下图所示:

- 搜索 RestSharp 并勾选包含预发布的复选框,然后单击安装,如下截图所示:

Selecting the RestSharp NuGet package
- 所需的软件包现在将安装,如下截图所示:

Installing the RestSharp package
在继续之前,让我们首先确保我们的产品 api 正常工作。 运行产品 API 项目,打开 Swagger,点击GET /api/product/productlist资源如下:

执行了前面的资源后,你应该看到一个完整的产品列表,如下面的截图所示:

Try all available resources to make sure that your product APIs are working correctly.
编写代码
到目前为止,我们已经准备好了为 REST web 客户端编写代码所需的东西; 在本节中,我们将编写实际的代码:
- 添加一个简单的代码来调用或使用您的产品 api。
如果你创建了一个新的项目在同一个解决方案(参见第一步**一节烹饪的 web 客户端),请确保该项目产品 API 是运行在开始之前您的 web 客户端项目。
- 在
Client文件夹中添加一个新类(Ctrl+Shift+C),命名为RestSharpWebClient。

- 现在打开
RestSharpWebClient类并添加以下代码:
private readonly RestClient _client = new RestClient("http://localhost:10065/api/");
前面的代码初始化 RestSharp 的 RestClient,并将基本 URL 作为字符串或 URI 接受。
URI 代表统一资源标识符,是用于标识资源的字符串的表示形式。
您可能遇到有多个环境的场景; 在这种情况下,您应该根据您的环境将 URI 存储在指向它的位置。 例如,您可以为您的开发环境使用 URIhttp://devserver:10065/api/,也可以为您的 QA 环境使用 URIhttp://testenv:10068/api/。 您应该将这些键存储在config文件或类似的地方,以便可以方便地访问这些值。 我们推荐使用new RestClient(somevariableforURI);。
In our application, product APIs are running on localhost and the listening port 10065. This may be different in your case.
让我们讨论下面的代码片段来调用或使用GET /api/product /productlist资源并填充完整的产品列表,如下所示:
public List<ProductViewModel> GetProducts()
{
var request = new RestRequest("product/productlist", Method.GET);
var response = _client.Execute<List<ProductViewModel>>(request);
return response.Data ?? new List<ProductViewModel> {new
ProductViewModel()};
}
在这里,我们使用RestRequest创建了一个GET请求,其中传递了一个资源和方法。
要使用productid获得特定的产品,请输入以下代码:
public ProductViewModel GetProductDetails(string productId)
{
var request = new RestRequest("product/{productid}", Method.GET);
request.AddParameter("productid", productId);
var response = _client.Execute<ProductViewModel>(request);
return response.Data ?? new ProductViewModel();
}
在前面的代码块中,GetProductDetails方法做了与GetProducts方法相似的事情。 不同之处在于它接受参数productId。
下面是我们 REST 客户端的完整代码:
public class RestSharpWebClient
{
private readonly RestClient _client = new
RestClient("http://localhost:10065/api/");
public List<ProductViewModel> GetProducts()
{
var request = new RestRequest("product/productlist",
Method.GET);
var response = _client.Execute<List<ProductViewModel>>
(request);
//To avoid any exception lets return an empty view model
//On production environment return exact exception or your
custom code
return response.Data ?? new List<ProductViewModel> {new
ProductViewModel()};
}
public ProductViewModel GetProductDetails(string productId)
{
var request = new RestRequest("product/{productid}",
Method.GET);
request.AddParameter("productid", productId);
var response = _client.Execute<ProductViewModel>(request);
//To avoid any exception lets return an empty view model
//On production environment return exact exception or your
custom code
return response.Data ?? new ProductViewModel();
}
public bool AddProduct(ProductViewModel product)
{
var request = new RestRequest("product/addproduct",
Method.POST);
request.AddBody(product);
var response = _client.Execute(request);
return response.StatusCode == HttpStatusCode.OK;
}
public bool UpdateProduct(string id, ProductViewModel product)
{
var request = new RestRequest("updateproduct", Method.PUT);
request.AddQueryParameter("productid", id);
request.AddBody(product);
var response = _client.Execute(request);
return response.StatusCode == HttpStatusCode.NoContent;
}
public bool DeleteProduct(string id, ProductViewModel product)
{
var request = new RestRequest("deleteproduct", Method.DELETE);
request.AddQueryParameter("productid", id);
request.AddBody(product);
var response = _client.Execute(request);
return response.StatusCode == HttpStatusCode.NoContent;
}
}
通过前面的代码片段,您现在已经添加了调用和使用产品 api 的功能。
实现 REST web 客户端
基于 rest 的服务可能是你的 web 应用的一部分,也可能不是,但我们仍然需要理解如何实现它们。
所以,现在是时候做些真正的工作了。 将ProductController添加到项目中,并执行以下操作:
public ActionResult Index()
{
var webClient = new RestSharpWebClient();
var products = webClient.GetProducts();
return View("ProductList", products);
}
看一下前面的代码片段。 我们调用了RestSharpWebClient的GetProducts方法,并在Index.cshtml视图中填充了完整的产品列表。
要添加另一个操作方法,请输入以下我们的ProductController的完整代码。 下面的代码片段包含了Index操作方法,并给了我们一个产品列表:
public class ProductController : Controller
{
public ActionResult Index()
{
var webClient = new RestSharpWebClient();
var products = webClient.GetProducts();
return View("ProductList", products);
}
public ActionResult Details(string id)
{
var webClient = new RestSharpWebClient();
var products = webClient.GetProductDetails(id);
return View(products);
}
现在让我们看看两种Create动作方法:HttpGet和HttpPost。 第一个为我们提供一个输入屏幕,第二个使用HttpPost方法发布所有数据(输入值)。 在服务器端,您可以在IFormCollection参数中接收所有数据,还可以轻松地编写逻辑来获取ProductViewModel中的所有值。
public IActionResult Create()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(IFormCollection collection)
{
try
{
var product = new ProductViewModel
{
ProductId = Guid.NewGuid(),
ProductName = collection["ProductName"],
ProductDescription = collection["ProductDescription"],
ProductImage = collection["ProductImage"],
ProductPrice = Convert.ToDecimal(collection["ProductPrice"]),
CategoryId = new Guid("77DD5B53-8439-49D5-9CBC-DC5314D6F190"),
CategoryName = collection["CategoryName"],
CategoryDescription = collection["CategoryDescription"]
};
var webClient = new RestSharpWebClient();
var producresponse = webClient.AddProduct(product);
if (producresponse)
return RedirectToAction(nameof(Index));
throw new Exception();
}
catch
{
return View();
}
}
You can also write a HttpPost method that accepts a parameter of the type ProductViewModel.
下面的代码片段向我们展示了Edit动作方法的代码,它与Create动作方法相似,但它更新了现有数据,而不是插入新的数据:
public ActionResult Edit(string id)
{
var webClient = new RestSharpWebClient();
var product = webClient.GetProductDetails(id);
return View(product);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(string id, IFormCollection collection)
{
try
{
var product = new ProductViewModel
{
ProductId = new Guid(collection["ProductId"]),
ProductName = collection["ProductName"],
ProductDescription = collection["ProductDescription"],
ProductImage = collection["ProductImage"],
ProductPrice = Convert.ToDecimal(collection["ProductPrice"]),
CategoryId = new Guid(collection["CategoryId"]),
CategoryName = collection["CategoryName"],
CategoryDescription = collection["CategoryDescription"]
};
var webClient = new RestSharpWebClient();
var producresponse = webClient.UpdateProduct(id, product);
if (producresponse)
return RedirectToAction(nameof(Index));
throw new Exception();
}
catch
{
return View();
}
}
动作方法用于从数据库或集合中删除特定的记录或数据。 HttpGet的操作方法Delete根据给定的 ID 获取一条记录,并显示准备修改的数据。 HttpPost的另一个Delete操作将修改后的数据发送到服务器进行进一步处理。 这意味着系统可以删除数据和记录。
public ActionResult Delete(string id)
{
var webClient = new RestSharpWebClient();
var product = webClient.GetProductDetails(id);
return View(product);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(string id, IFormCollection collection)
{
try
{
var product = new ProductViewModel
{
ProductId = new Guid(collection["ProductId"]),
ProductName = collection["ProductName"],
ProductDescription = collection["ProductDescription"],
ProductImage = collection["ProductImage"],
ProductPrice = Convert.ToDecimal(collection["ProductPrice"]),
CategoryId = new Guid(collection["CategoryId"]),
CategoryName = collection["CategoryName"],
CategoryDescription = collection["CategoryDescription"]
};
var webClient = new RestSharpWebClient();
var producresponse = webClient.DeleteProduct(id, product);
if (producresponse)
return RedirectToAction(nameof(Index));
throw new Exception();
}
catch
{
return View();
}
}
}
现在让我们从Shared文件夹中打开_Layout.cshtml,添加以下一行,添加一个链接到我们新添加的ProductController:
<li><a asp-area="" asp-controller="Product" asp-action="Index">Web Client</a></li>
当你运行项目时,你应该看到一个名为 Web Client 的新菜单,如下面的截图所示:

我们现在准备看到一些结果。 单击 Web Client 菜单,您将看到以下屏幕:

在前面的屏幕中,您可以执行其他操作,以及调用和使用您的产品 api—即创建、编辑和删除。
总结
基于 rest 的服务的创建对于任何项目都是重要的,但是如果没有办法使用这些服务,那么它们就毫无用处。 在本章中,我们了解了如何将 RestSharp 支持添加到我们的 web 项目中,并使用我们预先开发的产品 api。 我们还创建了一个 web 客户端,它可以通过使用 ASP 在网页上呈现输出来使用 web 服务。 净的核心。
在下一章中,我们将讨论微服务的热门话题,下一个层次的服务分离。 我们将讨论微服务如何通信,它们的优势是什么,以及我们为什么需要它们。
十一、微服务简介
到目前为止,我们已经通过实践示例介绍了 RESTful api,并创建了小型应用。 在前一章中,我们开发了一个应用,并讨论了 RESTful API、安全性、测试、性能和部署。
本章简要介绍了微服务,它是我们 RESTful 服务之旅的下一站。 在本章中,我们将介绍微服务的基本组件,并使用一个正在转换为微服务的单一应用的例子。
我们将涵盖以下议题:
- microservices 是什么?
- 交流 microservices
- Microservices 测试策略
- 可伸缩性
- ASP 中的微服务生态系统.NET Core
概述 microservices
简单地说,当将应用或模块划分为更小的、独立的服务时,其结果也称为微服务。 这些小部件也可以独立部署。
如果我们回顾历史,我们会发现微服务这个术语第一次被使用是在 2011 年的软件架构师研讨会上。 2012 年 3 月,詹姆斯·刘易斯(James Lewis)提出了他对“微服务”一词的一些看法。 到 2013 年底,IT 行业的各种团体已经开始讨论微服务,到 2014 年,微服务已经变得足够流行,被认为是大型企业的有力竞争者。
那么,到底什么是微服务呢? 有太多的定义,所以你可以根据自己对这个术语的理解或者你可能有什么样的用例和讨论来定义微服务。 让我们来看看官方网站对微服务的定义:(来源:https://docs.microsoft.com/en-us/azure/service-fabric/service-fabric-overview-microservices)
"Microservice applications are composed of small, independently versioned, and scalable customer-focused services that communicate with each other over standard protocols with well-defined interfaces."
Microservice 属性
在前一节中,我们看到微服务完全独立于系统中的其他服务,并在它们自己的进程中运行。 根据这个定义,有一些属性定义了微服务完全独立于其他组件。 让我们先看看核心属性是什么:
- 独立功能:不要试图在单个微服务中实现太多。 相反,设计它只有一个原因,并做好它。 这意味着设计应该尽量避免依赖于功能的任何其他部分。 在我看来,这部分非常重要,因为它为其他属性奠定了基础。
- 隔离的数据和状态:每个服务拥有自己的数据和状态。 它不与任何其他应用或部分共享所有权。
- 独立部署:上述各点的累积效应。 这有助于您进行连续部署。
- 技术采用:考虑了前两点后,这就容易多了,因为这对任何现有模块都不再有影响。 这里的美妙之处在于,你可以在两种不同的技术中拥有两种不同版本的微服务。 非常有益的。
- 一致性和弹性:它必须是完美的。 如果您不能指望某个服务在预期的时间内返回,或者指望它总是可用,那么它的全部目的就不复存在了。
理解 microservice 架构
我们已经讨论了微服务体系结构是如何开发包含一组较小服务的单个应用的。 这些服务是独立的,运行在它们自己的进程中。
换句话说,我们可以说微服务是隔离我们的服务的一种方式,这样它们就可以为设计、开发、部署和升级的目的而彼此独立地处理。
微服务有很多好处,具体如下:
- 较小的代码库:每个服务都较小,因此更容易作为一个单元进行开发和部署
- 独立环境的易用性:随着服务的分离,所有的开发人员都独立工作,独立部署,没有人担心任何模块依赖
交流 microservices
在处理微服务体系结构时,仔细考虑消息传递机制的选择非常重要。 如果忽略了这一方面,那么就会破坏使用微服务体系结构进行设计的整个目的。
让我们继续讨论同步和异步消息传递以及不同的消息传递格式的选择。
同步消息传递
当系统期望从服务中得到及时的响应,并且系统等待该响应,直到从服务接收到响应为止,它被称为同步消息。 REST 是微服务体系结构中最受欢迎的选择之一。 它很简单,并且支持 HTTP 请求-响应,因此不需要寻找替代方案。 这也是大多数微服务实现使用 HTTP(基于 api 的样式)的原因之一。
异步消息传递
当系统没有立即期待来自服务的响应时,它可以在不阻塞调用的情况下继续处理,它被调用为异步消息。
消息格式
在过去的几年里,使用 MVC 之类的东西让我迷上了 JSON 格式。 您还可以考虑 XML。 这两种格式都可以很好地处理带有 API 样式的 HTTP 资源。 如果需要使用二进制消息格式,也可以使用二进制消息格式。 我们在这里不推荐任何格式,您可以选择任何消息格式。
为什么我们应该使用微服务
人们已经探索了大量的模式和架构,其中一些受到欢迎,而另一些则在互联网流量之战中失利。 每个解决方案都有自己的优点和缺点,所以对于公司来说,快速响应可伸缩性、高性能和易于部署等基本需求变得越来越重要。 任何不具成本效益的单一方面都很容易对大型企业产生负面影响,从而决定企业是否盈利。 下图显示了选择微服务的优势:

这就是我们看到微服务拯救企业系统架构师的地方。 在这种架构风格的帮助下,他们可以确保自己的设计没有任何问题。 同样重要的是要考虑到这一目标是以具有成本效益和及时的方式实现的。
微服务架构是如何工作的
在前面的部分中,我们讨论了微服务体系结构,并试图对这个术语进行更深入的阐述。 现在,您可以看到微服务架构可能如何工作; 您可以根据自己的设计方法使用任何组合。 在使用微服务架构时,需要记住以下几点:
- 它是为现代时代编写的程序,在这个时代我们应该遵循所有的 SOLID 原则。 它是面向对象编程(OOP)。
- 这是向其他或外部组件公开功能的最佳方式,以便任何编程语言都可以使用这些功能,而不依赖于任何用户界面或服务(web 服务、API、REST 服务等)。
- 整个系统是协同工作的,而不是相互联系和依赖的。
- 每个组件负责自己的功能。
- 它将代码。 分离的代码是可重用的。
microservices 的优点
以下是微服务的一些优势:
- 您不需要投资来使整个应用具有可伸缩性。 对于购物车,我们可以简单地对产品搜索模块和订单处理模块进行负载平衡,而留下库存管理、订单取消和发货确认等不常用的操作服务。
- 我们可以很容易地匹配一个组织的部门层次。 由于大型企业中有不同的部门赞助产品开发,这可能是一个巨大的优势。
- 由于代码已经以一种不依赖于具有独立功能的其他模块的代码的方式完成,如果做得好,那么一个微服务中的更改影响另一个微服务的机会是非常小的。
- 由于整个应用更像是一组相互隔离的生态系统——如果需要,我们可以一次部署一个微服务。 任何一个服务的故障都不需要导致整个系统瘫痪。
- 你可以在一夜之间将一个微服务或一大堆微服务移植到不同的技术上,而你的用户甚至都不知道这一点。 不用说,你需要维护这些服务合同。
- 这是暗含的,但这里有必要提醒一下。 确保异步调用被很好地使用,并且同步调用没有真正阻塞整个信息流。 良好地使用数据分区。 我们稍后会讲到这个,所以现在不用担心。
- 在竞争激烈的环境中,这无疑是一个优势,因为如果您对新特性请求或系统中对新技术的采用反应缓慢,用户可能很快就会失去兴趣。
微服务体系结构的先决条件
在采用微服务架构达成一致后,明智的做法是具备以下先决条件:
- 随着开发的快速周转,需求变得更加苛刻。 它要求您尽可能快地部署和测试。 如果只是少量的服务,那就不是问题。 然而,随着服务数量的增加,这可能很快就会挑战现有的基础设施和实践。 例如,您的 Q/A 和登台环境可能不再足以测试从开发团队返回的构建的数量。
- 随着应用进入公共领域,用不了多久,开发与 Q/A 的古老脚本就会再次上演。 这次的不同之处在于,生意岌岌可危。 因此,您需要准备以自动方式快速响应,以便在需要时识别根本原因。
- 随着微服务数量的增加,您将很快需要一种方法来监控整个系统的运行和健康状况,以防止任何可能的瓶颈或问题。 如果没有监视已部署微服务的状态和由此产生的业务功能的方法,任何团队都不可能采取主动的部署方法。
扩展
扩展是任何企业在试图迎合日益增长的用户基础时所面临的最大挑战之一。
可伸缩性就是系统/程序处理不断增长的工作的能力。 换句话说,可伸缩性是指系统/程序可伸缩的能力。
系统的可伸缩性是它处理不断增加的工作负载的能力。 我们可以使用两种主要的可伸缩性策略或类型来扩展应用。
垂直扩展
在垂直扩展中,我们分析现有的应用,以找出由于执行时间较长而导致应用变慢的模块的哪些部分。 提高代码的效率可能是一种策略,这样可以减少内存的消耗。 这种减少内存消耗的做法可以适用于特定模块,也可以适用于整个应用。 另一方面,由于此策略中涉及的明显挑战,我们可以向现有 IT 基础设施添加更多资源,比如升级 RAM、添加更多磁盘驱动器,等等,而不是更改应用。 这两种垂直扩展路径的收益都有一定的限制,即在特定的时间点之后,所产生的收益将趋于稳定。 记住这一点很重要; 这种扩展需要停机时间。
水平扩展
在水平扩展中,我们将深入研究那些对整体性能影响较大的模块。 我们考虑诸如高并发性等因素,以使我们的应用能够服务于增加的用户基础。 我们还将实现负载平衡来处理更大量的工作。 向集群中添加更多服务器的选项不需要停机时间,这无疑是一个优势。 具体情况可能会有所不同,所以我们需要检查在电力、许可证和冷却方面的额外成本是否值得。
DevOps 文化
在 DevOps 的帮助下,一个团队应该强调开发团队和另一个运营团队的协作。 我们应该建立一个系统,让开发、问答和基础设施团队协作。
自动化
基础设施设置可能是一项非常耗时的工作。 在基础设施准备就绪时,开发人员可以空闲。 在加入团队并做出贡献之前,他或她将等待一段时间。 基础设施设置的过程不应该阻止开发人员变得高效,因为它会降低整体生产力。 这应该是一个自动化的过程。 通过使用 Chef 或 PowerShell,我们可以轻松地创建虚拟机,并在需要时快速增加开发人员数量。 这样,我们的开发人员就可以从加入团队的第一天就开始工作。
测试
测试是任何应用的关键任务,而在使用微服务时则更为复杂。 我们必须将测试方法划分如下:
- 采用 TDD 时,开发人员需要测试他或她自己的代码。 测试只是验证功能是否按预期工作的另一段代码。 如果发现任何功能不能满足测试代码,相应的单元测试将失败。 该功能可以很容易地修复,因为它知道问题在哪里。 为了实现这一点,我们可以利用 MSTest 或单元测试等框架。
- Q/A 团队可以使用脚本来自动化他们的任务。 他们可以利用 QTP 或 Selenium 框架来创建脚本。
部署
部署是一个巨大的挑战。 为了克服这个问题,我们可以引入 CI。 在这个过程中,我们需要设置一个 CI 服务器。 随着 CI 的引入,整个过程现在是自动化的。 任何团队成员只要使用版本控制 TFS 或 Git 检入代码,CI 过程就会开始工作。 它确保生成新代码,并在集成测试的同时运行单元测试。 在这两个场景中,不管是成功的构建还是其他情况,团队都会收到关于结果的警告。 这使得团队能够快速响应问题。
接下来,我们有连续部署。 在这里,我们介绍各种环境,即开发环境、登台环境、Q/ a 环境,等等。 现在,只要任何团队成员检入代码,持续集成就开始发挥作用。 它调用单元/集成测试套件,构建系统,并将其推出到我们已经设置的各种环境中。 这样,开发团队为 Q/ a 提供合适的构建的周转时间就减少了。
ASP 中的微服务生态系统.NET Core
每当我想到 ASP 中的微服务生态系统.NET Core 系统我考虑了各种小 api、异步编程、回调、事件触发等等。 事实上,生态系统要大得多,而且不知何故也更复杂。
我们已经讨论过,微服务体系结构风格是一种为大型应用创建小型和独立单元的方式。 如果不使用各种工具和实用程序,这是不可能的。
下图是一个典型的微服务架构风格的示意图,描述了不同的客户端对不同服务的请求以及这个请求是如何验证的:

一个典型的微服务生态系统由以下组件组成,您将在 ASP 中了解这些组件.NET Core。
Azure 服务架构——微服务平台
平台是任何生态系统的必备组件。 它支持系统,工作顺利,并产生预期的结果。 Azure Service Fabric 只是微软提供的一个平台,它在微服务生态系统中非常受欢迎。 它提供容器部署和业务流程。
官方文件请参见:https://docs.microsoft.com/en-us/azure/service-fabric/service-fabric-overview
"Azure Service Fabric is a distributed systems platform that makes it easy to package, deploy, and manage scalable and reliable microservices and containers."
无状态和有状态服务——一种服务编程模型
健壮的服务编程模型是微服务生态系统的支柱。 一个人应该知道他/她应该使用什么样的服务模型根据他/她的需求:
- Stateless:服务在客户端请求之间不保持任何状态。 也就是说,服务不知道,也不关心后续的请求是否来自已经发出/没有发出前一个请求的客户端。 当我们有外部数据存储时,这是最好的服务编程模型。 我们的服务可以基于与外部数据库存储上的数据进行交互和持久化的无状态服务编程模型。
- 有状态:服务保持一种可变状态,主动处理或保留特定于服务所要处理的任务的状态数据。
通信——服务之间交换数据的一种方式
如果微服务都是关于服务的,那么服务之间的通信应该是健壮的。 通信是服务之间交换数据的一种方式。 服务使用 Rest API(即 HTTP 调用请求/响应)进行通信,它们本质上是同步的。
当服务彼此通信时,它们实际上是在交换数据,这也称为服务之间的消息传递。 在处理微服务体系结构时,仔细考虑消息传递机制的选择是非常重要的。 如果忽略了这一方面,那么就会危及使用微服务体系结构进行设计的整个目的。 在单片应用中,这不是一个问题,因为组件的业务功能是通过函数调用调用的。 另一方面,这是通过松散耦合的 web 服务级消息传递实现的,其中服务主要基于 SOAP。 微服务消息传递机制应该是简单和轻量级的。
在微服务体系结构的各种框架或协议之间进行选择,没有固定的规则。 然而,这里有几点值得考虑。 首先,它的实现应该足够简单,而不会给系统增加任何复杂性。 其次,它应该足够轻,记住微服务体系结构可能严重依赖于服务间消息传递这一事实。 让我们继续讨论同步和异步消息传递以及不同的消息传递格式的选择。
总结
微服务体系结构风格提供了某些好处。 它使开发变得又快又容易。 它允许 DevOps (CI 和 CD)团队在地理位置上分离,平稳地工作并同步。 应用被划分为小的服务组件或部件,因此维护很容易。 这允许开发团队让业务发起人选择首先响应什么行业趋势。 这将带来成本效益、更好的业务响应、及时的技术采用、有效的扩展和消除对人类的依赖。
在本章中,你已经了解了 ASP.NET 中典型的微服务体系结构风格和微服务生态系统。
现在,我建议你阅读以下关于微服务的内容,以提高你的技能:
- 使用。net Core 2.0 构建微服务-第二版by PACKT(https://www.packtpub.com/application-development/building-microservices-net-core-20-second-edition
- 微服务模式和最佳实践by PACKT(https://www.packtpub.com/application-development/microservice-patterns-and-best-practices)



浙公网安备 33010602011771号