-NET-Core-REST-Web-服务构建指南-全-
.NET Core REST Web 服务构建指南(全)
原文:
zh.annas-archive.org/md5/1e7ad7205a3cebbe0ef61345f17029dc译者:飞龙
前言
本书将引导读者通过 RESTful 网络服务的设计,并利用 ASP.NET Core 框架来实现这些服务。从 REST 背后哲学的基本原理开始,读者将经历设计和实现企业级 RESTful 网络服务的步骤。采用实用方法,每一章都提供了他们可以应用于自己情况的代码示例。它展示了最新 .NET Core 版本的威力,与 MVC 一起工作。然后它超越了框架的使用,探索解决弹性、安全和可伸缩性问题的方法。读者将学习处理 Web API 安全的技术,并了解如何实施单元和集成测试策略。最后,本书通过指导你构建 RESTful 网络服务的 .NET 客户端以及一些扩展技术来结束。
本书面向对象
这本书旨在为那些想学习使用最新的 .NET Core 框架构建 RESTful 网络服务的人编写。为了最好地利用书中包含的代码示例,你应该具备基本的 C# 和 .NET Core 知识。
本书涵盖内容
第一章,入门,将涵盖规划阶段,解释如何根据我们的需求或问题陈述确定完美的技术堆栈,以及 RESTful 和 RESTless 服务的根本方面。
第二章,构建初始框架 – 应用程序布局基础,将使你熟悉各种方法的概念,例如 GET、POST、PUT、DELETE 等。
第三章,用户注册和管理,将使你熟悉使用 ASP.NET Core 2.0、Entity Framework Core、基本身份验证和 OAuth 2.0 进行身份验证。
第四章,商品目录、购物车和结账,将帮助你理解在构建电子商务应用程序的不同部分时 ASP.NET Core 的复杂组件,包括 .NET Standard 2.0。
第五章,集成外部组件和处理,将帮助你了解中间件、使用中间件实现日志记录、身份验证和资源限制。
第六章,测试 RESTful 网络服务,将使你熟悉测试范式、测试概念、存根和模拟、安全测试和集成测试。
第七章,持续集成和持续部署,将使你熟悉使用 VSTS 和 Azure 的 CI 和 CD 概念。
第八章,保护 RESTful 网络服务,将帮助你了解各种安全技术,包括基本身份验证、XSS 攻击和数据加密。
第九章,扩展 RESTful 服务(Web 服务的性能),将解释扩展内、扩展外以及各种扩展模式。
第十章,构建 Web 客户端(消费 Web 服务),将教授读者使用 ASP.NET Core 和 Rest Client with RestSharp。
第十一章,微服务简介,通过涵盖 ASP.NET Core 中的微服务生态系统来概述微服务。
为了充分利用本书
读者应具备先前的.NET Core 和.NET Standard 知识,以及 C#、RESTful 服务、Visual Studio 2017(作为 IDE)、Postman、高级 Rest 客户端和 Swagger 的基本知识。
为了设置系统,读者应在他们的机器上具备以下内容:
Visual Studio 2017 更新 3 或更高版本(有关下载和安装说明,请参阅
docs.microsoft.com/en-us/visualstudio/install/install-visual-studio)SQL Server 2008 R2 或更高版本(有关下载和安装说明,请参阅
blogs.msdn.microsoft.com/bethmassi/2011/02/18/step-by-step-installing-sql-server-management-studio-2008-express-after-visual-studio-2010/)).NET Core 2.0
下载示例代码文件
您可以从www.packtpub.com的账户下载本书的示例代码文件。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
在www.packtpub.com上登录或注册。
选择 SUPPORT 选项卡。
点击代码下载与勘误表。
在搜索框中输入书籍名称,并遵循屏幕上的说明。
文件下载后,请确保您使用最新版本的以下软件解压缩或提取文件夹:
Windows 上的 WinRAR/7-Zip
Mac 上的 Zipeg/iZip/UnRarX
Linux 上的 7-Zip/PeaZip
本书代码包也托管在 GitHub 上,地址为github.com/PacktPublishing/Building-RESTful-Web-services-with-DOTNET-Core。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有其他来自我们丰富的书籍和视频目录的代码包可供选择,请访问github.com/PacktPublishing/。查看它们!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表的彩色图像的 PDF 文件。您可以从www.packtpub.com/sites/default/files/downloads/BuildingRESTfulWebServiceswithDOTNETCore_ColorImages.pdf下载它。
使用的约定
本书使用了多种文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“Header必须作为信封的第一个子元素出现,在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 的消息协议,用于计算机之间交换信息。”
警告或重要注意事项看起来像这样。
小贴士和技巧看起来像这样。
联系我们
我们始终欢迎读者的反馈。
一般反馈:请通过feedback@packtpub.com发送电子邮件,并在邮件主题中提及书籍标题。如果您对本书的任何方面有疑问,请通过questions@packtpub.com与我们联系。
勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告这一点。请访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版:如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packtpub.com与我们联系,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
评论
请留下评论。一旦您阅读并使用了这本书,为什么不在您购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 公司可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
如需了解 Packt 的更多信息,请访问 packtpub.com.
第一章:入门
现代 Web 开发要求与服务器交互时没有麻烦。这意味着随着不同 UI 和后端框架的演变,开发者需要找到一种方法,在不依赖任何框架的情况下与任何可用的框架共享数据。这意味着应该有一种方法从服务器向客户端共享数据,无论它们的语言和框架如何。为了使数据共享统一,首先想到的是.xml和.json格式。这些格式被每个框架所支持。
在本章中,我们将探讨一种架构风格,通过它可以从任何语言使用任何框架编写的任何程序中获取或发送数据。使用 REST,我们将讨论的架构可以引入客户端易于消费的数据操作方法。
本章将涵盖以下主题:
RESTful 服务
为什么我们应该使用 RESTful 服务?RESTful 和 RESTless 服务之间的区别
客户端-服务器架构
ASP.NET Core 和 RESTful 服务
讨论 RESTful 服务
REST代表表征状态转移。它是一种定义构建 Web 服务的一组指南的架构风格。
什么是架构风格?它不过是一个具有预定义原则的概念。我们稍后将讨论这些原则。当你遵循 REST 时,你实际上是在你的应用程序中实现 REST 的构建块原则。
然而,REST 的实现方式肯定会有所不同,每个开发者都有自己的实现风格。没有固定的实现方式。不要与架构模式混淆,架构模式不是概念,而是实际的实现。MVC 就是一个架构模式,因为它有一个固定的结构,定义了组件之间如何交互,以及它们不能有不同的实现方式。
以下是一个基于 REST 的服务非常简单的图示:

为了简化问题,考虑前面的图示,它展示了具有某些方法的服务,例如GET、POST、PUT和DELETE。这就是这种风格的核心所在。当你设计你的服务时,它将包含所有这些方法——以及它们内部预期的操作——我们可以将其视为基于 REST 的服务,也称为 RESTful 服务。更重要的是,服务可以从任何平台和任何语言构建的应用程序中调用,因为服务具有标准化的架构。
如前所述,RESTful 服务是一种支持 REST 的服务。让我们来谈谈 REST 的特性,以便我们了解对 RESTful 服务的期望。
REST 特性
网络服务的主要构建块是客户端和服务器架构。从服务器发送的响应实际上是针对客户端请求的回复。这就像你提出一个问题,服务器在找到答案时会做出回应。从服务器返回的响应实际上是以某种格式或表示的资源。常见的格式有.json、.xml、.pdf、.doc等等。
REST 是无状态的。无状态意味着系统的状态始终不同。因此,当请求到达服务器时,它会被服务并遗忘。因此,下一个请求不依赖于前一个请求的状态。每个请求都由服务器独立处理。
请求是在 HTTP 连接中执行的。它们各自都采取统一资源标识符(URI)的形式。这个标识符帮助我们定位互联网服务器上的所需资源。
Roy Fielding 的博士论文,题为《架构风格和网络软件架构设计》,定义了 REST。以下是他研究中的几个关键点:
与许多分布式架构一样,REST 强加了层、无状态和缓存。
REST 提高了效率、互操作性和整体性能。
REST 通过遵循一系列关于如何标识和操作资源以及通过元数据简化其操作描述的过程来引入一致性。我们将更多地讨论这种一致性,这被称为统一接口。
由于 REST 是一种架构风格,只要支持 HTTP,就可以使用任何语言或平台来开发服务。
你可以在www.ics.uci.edu/~fielding/pubs/dissertation/top.htm阅读完整的论文。
资源导向架构
网上的每个资源都已被赋予一个唯一的标识符,也称为 URI。统一资源定位符(URL)是今天在互联网上使用最广泛的 URI 类型。URL www.packtpub.com/ 识别并定位 Packt Publishing 网站。
让我们快速看一下架构的图片。在下面的图中,客户端正在尝试通过一个标识符(URL)访问资源。资源存在于服务器上,并且有一个可以在请求时返回给客户端的表示:

如其名所示,URL 是与单一资源相关联的;因此,如果我想指向一个资源,我可以在电子邮件、聊天中等轻松地共享该标识符。
如果用公司或资源名称命名,这些标识符很容易记住。最好的例子是 www.google.com,因为 Google 这个名字很容易记住。因此,我们可以通过口碑传播资源链接,你可以在几秒钟内将其输入到网络浏览器中,例如 Chrome 或 Firefox。
你可能会在某个网页上找到超链接,链接到另一个网站上的另一个资源。这意味着由于超链接的存在,资源现在是相互关联的。
这些相互关联的资源构成了面向资源的架构。超链接使得通过目标资源 URI 从一个资源导航到另一个资源变得容易。
例如,在 HTML 中,你通过锚元素链接到另一个资源。以下是一个链接到 Packt 的物联网图书目录页面的锚元素:
<a href="https://www.packtpub.com/tech/Internet%20of%20%20Things">Packt IoT Books</a>
默认情况下,锚元素被渲染为带下划线的文本。当你悬停在它上面时,你可以在下面的截图看到附加的 URI:

你可以点击锚文本(Packt IoT Books),然后对目标资源 URI 发起一个 GET 请求。
注意,当你点击超链接时,你会跳转到实际上是一个资源表示的网页。你将遇到的最常见的表示形式是 HTML 格式。其他一些常见的格式包括(X)HTML、JPEG、GIF、WMV、SWF、RSS、ATOM、CSS、JavaScript/JSON 等等。当浏览器收到这些表示之一时,它会尝试解析它,并在解析成功后将其渲染供查看。
URI
我们已经谈了很多关于资源的内容。实际上,它们是我们在一个特定网站上看到的页面。然而,HTTP 中的资源不仅仅是简单的 HTML 网页形式的文件。通常,资源被定义为任何可以通过 URI 唯一识别的信息,例如 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 约束
REST 由六个约束定义,如下所示图所示。其中一个是可选的:

这些约束中的每一个都强制服务遵循一个设计决策。如果不遵循,则该服务不能被视为 RESTful。让我们逐一讨论这些约束。
客户端-服务器架构
客户端或服务的消费者不需要担心服务器如何处理数据并将其存储在数据库中。同样,服务器也不需要依赖于客户端的实现,尤其是用户界面。
想象一个没有太多用户界面的物联网设备或传感器。然而,它通过与服务器交互使用 API 来存储数据,这些 API 被编程在特定事件上触发。假设你正在使用一个物联网设备,当你的车耗尽汽油时它会提醒你。当物联网设备中的传感器检测到汽油短缺时,它会调用配置好的 API,然后最终向车主发送警报。
这意味着客户端和服务器不是同一个实体,它们可以独立存在。它们可以独立设计和演进。现在你可能想知道,客户端如何在没有了解服务器架构的情况下工作,反之亦然?好吧,这正是这些约束的目的所在。当服务被客户端交互时,提供了足够的信息关于其本质:如何消费它,以及你可以使用它执行哪些操作。
随着我们进入本节内容,你会发现客户端和服务器之间完全没有关系,并且如果它们完美地遵守所有这些约束,它们可以完全解耦。
无状态
无状态这个术语意味着应用程序在特定时间内保持的状态可能不会持续到下一个时刻。RESTful 服务不维护应用程序的状态,因此它是无状态的。
RESTful 服务中的请求不依赖于过去的请求。服务独立处理每个请求。另一方面,有状态服务需要在请求执行时记录应用程序的当前状态,以便它可以针对下一个请求采取必要的行动。
此外,由于没有这些复杂性,无状态服务变得非常容易托管。因为我们不需要担心应用程序的状态,所以实现起来变得容易,维护也变得顺畅。
缓存
为了避免每次请求都生成相同的数据,存在一种称为缓存的技术,用于在客户端或服务器端存储数据。这些缓存数据可以在需要时用于进一步参考。
在使用缓存时,正确管理它非常重要。原因很简单。我们正在存储不会被服务器的新数据替换的数据。虽然这增加了服务的性能,但同时,如果我们不小心缓存什么以及在其生命周期内如何配置,我们可能会看到过时的数据。例如,假设我们在网站上显示黄金的实时价格,并且我们缓存了这个数字。下次价格变化时,除非我们过期之前存储的缓存,否则它不会反映出来。
让我们看看不同类型的 HTTP 头以及如何配置缓存:
| 头 | 应用 |
|---|---|
| 日期 | 表示生成的日期和时间。 |
| 最后修改 | 服务器最后修改此表示的时间戳。 |
| Cache-control | 这是用于控制缓存的 HTTP 1.1 头。我们将在表格之后更详细地讨论这一点。 |
| Expires | 此头帮助为表示标记一个过期日期和时间。 |
| Age | 表示自表示从服务器获取以来经过的秒数。 |
前五个头的配置取决于服务的性质。以提供实时黄金价格的服务的例子来说,理想情况下,它应该将缓存年龄限制尽可能低,或者甚至关闭缓存,因为用户每次访问网站时都应该看到最新的结果。
然而,包含许多图像的网站几乎不会更改或更新它们。在这种情况下,缓存可以配置为存储更长时间。
这些头值将根据 cache-control 头进行咨询,以检查缓存的结果是否仍然有效。
以下是最常见的 cache-control 头值:
| 指令 | 应用 |
|---|---|
| 公共 | 这是默认指令。这允许每个组件缓存表示。 |
| 私有 | 只有客户端或服务器可以缓存表示。但是,中间组件受到限制。 |
| no-cache/no-store | 使用此值,我们可以关闭缓存。 |
| max-age | 此值表示自日期头中提到的日期和时间起,经过的秒数,该头表示表示的有效性。 |
| s-maxage | 这与 max-age 的功能相同,但仅针对中间缓存。 |
| must-revalidate | 这表示如果 max-age 已过,则必须重新验证表示。 |
| proxy-validate | 这与 max-revalidate 的功能相同,但仅针对中间缓存。 |
需要时按需提供代码(可选)
如短语按需代码所暗示的,服务可能会尝试在客户端执行代码以扩展功能。然而,这是可选的,并不是每个服务都这样做。
考虑一个网络应用的例子,该应用调用票务服务以获取所有可用的票。该服务总是希望在警报中显示此信息。为此,服务可以返回与数据一起的 JavaScript 代码,其中包含带有可用票数警报的消息。因此,一旦客户端从服务接收到响应,就会执行警报并显示数据。
统一接口
当我们遇到“接口”这个词时,首先想到的是解耦。我们创建接口以实现松散耦合的架构,在 RESTful 的案例中也可以看到同样的架构。
在实现 REST 时,我们使用相同的概念来解耦客户端和 REST 服务的实现。然而,为了在客户端和服务之间实现这种解耦,定义了标准,每个 RESTful 服务都支持这些标准。
注意上一行中的“标准”一词。世界上有如此多的服务,显然,消费者数量超过了服务。因此,在设计服务时,我们必须遵循一些规则,因为每个客户端都应该能够轻松地理解服务,而无需任何麻烦。
REST 由四个接口约束定义:
资源的标识:URI 用于标识资源。资源是一个网络文档。
通过表示操作资源:当客户端拥有一个给定的资源——以及任何元数据——他们应该有足够的信息来修改或删除资源。例如,
GET意味着你想要检索关于 URI 标识的资源的数据。你可以使用 HTTP 方法和 URI 来描述一个操作。自描述消息:传递的消息应包含足够的数据信息,以便理解和处理以进行后续操作。MIME 类型用于此目的。
超媒体作为应用程序状态引擎(HATEOAS):服务返回的表示应包含所有未来操作作为链接。这就像访问一个网站,你可以在其中找到不同的超链接,为你提供不同类型的可用操作。
HTTP 1.1 提供了一套称为动词的方法。在我们的服务中实现这些动词将使它们成为标准化的。重要的动词如下:
| 方法 | 在服务器上执行的操作 | 方法类型 |
|---|---|---|
GET |
读取/检索一个资源。 | 安全 |
PUT |
要么插入一个新资源,要么如果它已经存在,则更新该资源。 | 幂等 |
POST |
插入一个新资源。也可以用来更新现有资源。 | 非幂等 |
DELETE |
删除一个资源。 | 幂等 |
OPTIONS |
获取一个资源的所有允许的操作列表。 | 安全 |
HEAD |
仅返回响应头,不返回响应体。 | 安全 |
上述表格相当直观,除了“方法类型”列。让我澄清这一点。
当在服务上执行时,一个安全的操作不会对资源的原始值产生影响。由于GET、OPTIONS和HEAD动词仅检索或读取与资源相关的信息,并不更新它,因此它们是安全的。
当执行幂等(可重复)操作时,无论我们执行多少次,都会得到相同的结果。例如,当你进行DELETE或PUT操作时,你实际上是在操作一个特定的资源,并且操作可以重复执行而不会出现任何问题。
POST与PUT:这是互联网上非常常见的讨论话题,也是很容易理解的一个话题。POST和PUT都可以用来插入或更新资源。然而,POST是非幂等的,这意味着它不可重复。原因是每次你使用POST调用时,如果你没有提供资源的精确 URI,它将创建一个新的资源。下次你使用POST时,它将再次创建一个新的资源。但在PUT中,它将首先验证资源是否存在。如果存在,它将更新它;否则,它将创建它。
更多解释
在所有可用方法中,GET是最受欢迎的,因为它用于获取资源。
HEAD方法只会返回带有空体的响应头。这通常只在不需要整个资源表示的情况下才需要。
OPTIONS方法用于获取资源上允许或可用的操作列表。
考虑以下请求:
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的作者将被处理,但如果该作者不存在,它将首先被创建。随后的对此 URI 的请求将被视为修改具有 ID 19的作者资源的更新请求。
另一方面,如果我们使用以下类似的POST请求,它将创建一个新的作者资源,并使用发布的数据:
POST http://packtservice.com/Authors/
有趣的是,如果你重复这样做,你将负责具有相同数据的重复记录。这就是为什么它在本质上是非幂等的。
注意以下带有 ID 的POST请求。与PUT不同,如果该资源不存在,POST不会将其视为新资源的请求。它始终被视为更新请求:
POST http://packtservice.com/Authors/19
updated.
在本节中,以下是一些需要关注的主要点:
PUT在调用相同的 URI 时创建或更新一个资源如果资源已经存在,
PUT和POST的行为相同POST,如果没有 ID,每次触发时都会创建一个资源
分层系统
大多数现代应用程序都是使用多层设计的,RESTful 服务也期望如此。在分层系统中,每一层都限制在只能看到或知道层次结构中的下一层。
拥有分层架构有助于提高代码的可读性,隐藏复杂性,并提高代码的可维护性。想象一下,你只有一个层,从身份验证到数据库操作的一切都在其中进行。这绝对是不推荐的,因为主要组件,如身份验证、业务逻辑和数据库操作,并没有分离出来。
因此,从 RESTful 服务中期望这种约束,并且没有任何客户端实际上可以说它连接到了最终层。
RESTful 服务的优缺点
以下是一些 RESTful 服务的优缺点:
优势
使用 RESTful 服务的优势如下:
不依赖于平台或任何编程语言
通过 HTTP 标准化的方法
它不会在服务器上存储客户端的状态
支持缓存
可供任何类型的客户端访问,例如移动、Web 或桌面
缺点
虽然有优势,但肯定也有一些缺点。让我们来看看 RESTful 服务的缺点:
如果不正确遵循标准,它们对客户端来说很难理解
由于没有提供此类元数据,文档变得有问题
如果没有遵循任何过程来限制资源的访问,安全性是一个关注点
ASP.NET Core 和 RESTful 服务
.NET Core 被定义为用于创建现代 Web 应用程序、微服务、库和控制台应用程序的跨平台、开源、云就绪和模块化 .NET 平台,这些应用程序可以在任何地方(Windows、Linux 和 macOS)运行。
ASP.NET Core 是一个免费的开源 Web 框架,是 ASP.NET 的下一代。它是一个模块化框架,由运行在完整 .NET Framework、Windows 和跨平台 .NET Core 上的小型框架组件包组成。
该框架是从头开始完全重写的。它将之前分开的 ASP.NET MVC 和 ASP.NET Web API 统一成一个单一的编程模型。
ASP.NET Web API 是为了将 Web/HTTP 编程模型映射到 .NET Framework 编程模型而构建的。它使用熟悉的结构,如 Controller、Action、Filter 等,这些在 ASP.NET MVC 中使用。
ASP.NET Web API 是在 ASP.NET MVC 运行时之上设计的,包括一些简化 HTTP 编程的组件。我们可以利用 Web API 技术在 .NET Framework 上执行服务器端操作;然而,为了遵循 RESTful,我们应该遵守我们在本章前面讨论的标准。幸运的是,Web API 自动管理所有 HTTP 的底层传输细节,同时保持所有必需的约束。
由于 Web API 提供的统一性,强制执行 RESTful 原则,客户端(如移动设备、Web 应用程序、云等)可以轻松访问它而不会出现任何问题:

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

以下是在我点击 File | New | Project | 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 定义了如何通过额外的约束使用统一接口,如何识别资源,如何通过表示来操作资源,以及如何包含使消息自我描述的元数据。
互联网建立在 HTTP 的统一接口之上,重点是交互资源和它们的表示。REST 与任何特定的平台或技术无关;互联网是唯一一个完全体现 REST 的主要平台。RESTful Web 服务的架构风格的基本风格是客户端-服务器。
在这里,客户端请求一个资源,服务器处理并响应所请求的资源。服务器的响应是基于用户和平台无关的。关注点的分离是客户端-服务器约束背后的原则。因为在客户端-服务器架构中,存储和用户界面是由服务器和客户端分别承担的角色,这提高了用户界面在多个平台上的可移植性。
我们应该为客户端开发者记录下每一个资源和 URI。我们可以使用任何格式来结构化我们的文档,但它应该包含足够关于资源、URI、可用方法以及访问服务所需的其他信息的描述。
Swagger 是一个可以用于文档的工具,它在一个屏幕上提供了关于 API 端点的所有信息,你可以通过发送参数来可视化 API 并测试它。开发者可以使用另一个名为 Postman 的工具来测试 API。这两种工具将在本书的后续章节中通过示例进行解释。
ASP.NET Web API 是一个开发环境,用于开发 RESTful Web 服务,允许应用程序轻松地发送和接收 HTTP 请求(Web 请求),并根据对它提出的请求类型(如提供用户信息等)执行操作。
ASP.NET Core 中的 Web API 设计遵循与 MVC 相同的编程模型而简化。
在下一章中,我们将通过设置环境和查看 Web API 中 HTTP 动词的各种基本原理来开始编码。
第二章:构建初始框架 – 为应用程序奠定基础
在上一章中,我们讨论了 REST,它的特性以及如何在 ASP.NET Core 中实现它。我们将在此基础上继续,并在本章中设置环境以开发应用程序。
我们将开始构建应用程序的基本框架。我们将了解每个 HTTP 动词,它们是如何工作的,以及它们在 ASP.NET Core Web API 中的实现范例。在所有这些之前,我们将快速查看 SOAP 以及它与 REST 的不同之处。
当我们遍历动词时,我们将探索一个非常易于使用的工具来分析 HTTP 请求和响应。
我们将涵盖以下主题:
关于所有网络服务(REST 和 SOAP)的内容
运行开发服务器
REST 动词和状态码
在 ASP.NET Core Web API 中实现动词
使用 Postman 的示例
SOAP 与 REST
基于 REST API 的单页应用程序模型
基于 REST 的面向服务架构 (SOA) 概述
SOAP
简单对象访问协议(SOAP)是一种基于 XML 的消息协议,用于在计算机之间交换信息。SOAP 依赖于应用层协议,通常是 超文本传输协议(HTTP)或 简单邮件传输协议(SMTP),用于消息协商和传输。由于我们正在讨论 HTTP,它是安装在每台操作系统上并运行的,因此实现 SOAP 的网络服务可以从任何平台使用任何语言调用。
SOAP 结构
我们已经知道 SOAP 消息是一个 XML 文档,但让我们通过图表更好地了解它:

以下是对前面图表中组件的描述:
信封:SOAP 消息结构的必需元素。定义消息的开始和结束。
头部:SOAP 消息的可选元素。它包含有关 SOAP 消息的信息,可用于处理数据。
正文:这是主要内容,它包含实际的 XML 结构中的消息。显然,这是一个必需的元素。
错误:如果在处理 SOAP 消息时发生任何错误,可以使用可选的 Fault 元素来提供有关这些错误的信息。
您可能想知道是谁确切地告诉我们遵循这种结构。好吧,有一个名为 W3 的组织为特定技术提出标准。他们为 SOAP 结构做了同样的事情。
您可以轻松地在 www.w3.org/2001/12/soap-envelope 上找到有关 SOAP 封装的详细信息。同样,您可以在 www.w3.org/2001/12/soap-encoding 上查看有关 SOAP 编码和数据类型的详细信息。
我们关于 SOAP 消息结构的讨论是由 W3 组织定义的。然而,这个组织不断研究优化结构,并时不时地引入更稳健的规范。因此,我们必须根据他们提供的最新规范进行更新并相应地实施。
下面的块描述了 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 封包内部应该只有一个主体元素。
Header元素是一个可选元素。然而,如果存在,那么应该只有一个Header元素。Header必须作为封包的第一个子元素出现,在主体元素之前。使用
ENV命名空间前缀和Envelope元素来构建一个 SOAP 封包。(参考以下示例。)encodingStyle属性定义了文档中使用的数据类型。这为消息中出现的所有数据类型提供了一个概括。如果此属性出现在任何 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>
SOAP 通过 HTTP POST
在 HTTP 头部中提到的 Authors 实际上是包含一个 POST 动作方法的控制器或程序的 URL,所有内容都托管在 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
REST 是一种网络计算机系统之间的架构风格,以便系统可以轻松地相互通信。符合 REST 风格的服务通常被称为 RESTful 服务。
让我们讨论一下当 Web 服务被标记为 RESTful 时的一些重要约束。
服务器和客户端是独立的
在 REST 中,服务器和客户端之间没有限制或依赖。两者都可以相互独立。这只是客户端理解服务的 URL。服务器上 Web 服务的代码可以修改,而不必关心与之关联的客户端,反之亦然。
这种分离有助于客户端/服务器架构在没有障碍的情况下自由呼吸。因此,设计应用程序和分离其核心业务逻辑变得容易。我的意思是,可以使用客户端技术来设计应用程序,而 RESTful 网络服务则在需要数据库中的业务相关操作的地方被调用。
然而,保持服务器和客户端模块化和分离取决于一个条件,那就是他们发送和接收的消息格式。他们都应该知道发送和接收的消息格式。
由于用户界面与业务和数据存储相关的操作分离,可以通过简化服务器组件来提高灵活性和可扩展性。此外,这种分离允许每个组件独立发展。
REST 端点通过特定的 URL 暴露。不同的客户端可以使用 URL 进行连接,然后执行预期的操作并获取响应。
在这本书中,我们将构建一个具有最小操作的简单电子商务 Web 服务,用户可以使用购物车并下订单。这些操作将通过端点公开。正如我们讨论的那样,端点可以很容易地从不同类型的客户端中消费,包括移动应用、Web 应用、服务器端代码等。
无状态
这个概念非常容易理解。在服务器/客户端架构中,服务器需要知道哪个客户端正在请求它的数据,相应地,它决定发送什么以及不发送什么。
然而,REST 系统是无状态的。这意味着服务器不需要知道任何关于客户端状态的信息,反之亦然。这最终会减少服务器在每次请求到来时识别客户端的开销。
但现在的问题是,客户端和服务器是如何交互的?答案是通过对适当的消息进行交互。假设一个用户想查看一个订单的详细信息。它只需通过发送订单 ID 来向服务器请求,服务器就会以.json或.xml格式返回订单详情,这些格式可以很容易地被客户端解析。每条消息都有处理该消息所需的所有信息。
这些约束(以及一些其他约束,如caching、layered system、uniform interface和code on demand)在 Web 服务上实现时,有助于 RESTful 应用程序实现可靠性、优化性能和可扩展性。原因在于组件可以独立管理、完美更新且无需影响整个系统即可重用。
让我们在下一节中具体看看服务器和客户端之间是如何进行通信的。
设置环境
在我们探索通信机制之前,让我们首先设置开发环境。我们将使用 Visual Studio 2017 作为我们的示例。
打开 Visual Studio 并执行我们最喜欢的步骤,文件 | 新建 | 项目,这将打开一个包含可用模板的对话框窗口,如以下截图所示:

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

项目已创建。美丽的是,它已经为我们构建了所有必要的组件,并创建了一个名为ValuesController的示例控制器,如下面的截图所示:

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

让我们在以下部分讨论刚才发生的事情。
这里在做什么?
注意 URL,localhost: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

结论
从前面的观察中可以得出以下结论。请注意,我们正在讨论 URL 为 api/values 的 GET 请求,这意味着我们正在讨论控制器中所有非参数化动作方法。在阅读以下要点时,请忽略带有参数或其他属性的方法:
当我们不带任何参数访问特定的 Web API 控制器(例如,
api/values)时,控制器中首先会搜索带有[HttpGet]属性的动作方法。如果在非参数化方法中没有提到属性,那么当 .NET Core 运行时在选择请求的一个动作方法时会感到困惑。
动作方法的命名规范没有限制。只要它是唯一一个没有
[HttpGet]属性的方法,或者唯一一个具有[HttpGet]属性的方法,当GET 请求到达 API 时,它就会被完美执行。
请求和响应
现在我们已经快速浏览了 ValuesController 的演示,让我们确切地看看客户端是如何发送请求以及它是如何接收响应的。
一个 REST 请求通常由以下内容组成:
HTTP 动词: 这表示请求想要在服务器上执行的操作类型。
Header: REST 请求的这个元素允许客户端传递更多关于请求的信息。
URL: REST 请求要操作的资源的实际路径。
Body: 主体可以包含与资源相关的额外数据,用于标识或更新资源。这是可选的。
HTTP 动词
以下是在请求 REST 系统进行资源交互时使用的基本 HTTP 动词:
GET: 用于通过其标识符或资源集合检索特定资源
POST: 用于创建/插入新资源
PUT: 用于通过其标识符更新特定资源
DELETE: 用于通过其标识符删除特定资源
让我们逐一探索 REST 中这些动词的请求/响应机制。我们将尝试设计一个具有基本操作的商业应用程序。在第一阶段,我们将处理产品,这是这些类型应用程序的核心。
Postman
要测试 API,我们可以使用一个非常易于使用的工具,名为 Postman。它可以从:www.getpostman.com/ 下载。请下载并打开它。我们将在下一节中看到如何通过 Postman 发送请求,并分析我们从 Web API 收到的响应。
GET
我将添加另一个名为 ProductsController 的控制器。目前,让我们有一个简单的动作方法 GET,它将返回一些产品。目前,这些产品在动作方法中是硬编码的。该方法看起来如下:
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))
};
}
}
}
[路由] 属性提供了一个定义良好的模板 "api/[控制器]"。在这里,控制器名称是 ProductsController。当我们使用 URL api/Products 进行请求时,框架将搜索具有该路由定义的控制器。[控制器] 占位符是一个特殊的命名约定,将在运行时替换为文本(控制器名称)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; }
}
我们完成了。让我们通过 Postman 进行 GET 请求 来分析 REST 中的请求和响应机制。对于 GET 请求,很简单。只需打开 Postman。然后按照以下截图中的步骤操作:

在 Postman 中执行 GET 请求
在 步骤-1 中,只需粘贴 URL,即我们示例中的 http://localhost:57571/api/products。其他所有内容都已为 GET 请求 设置好。您可以看到 URL 框左边的请求类型,它是 GET。这意味着当前请求将是一个 GET 请求。按如下所示的 步骤-2 中的发送按钮。
响应是在底部部分显示的产品列表。它以 .json 格式呈现。请参考以下截图,它显示了 GET 请求的响应:

现在您已经愉快地了解了 GET 的工作原理,让我们分析幕后发生了什么。客户端(在这里是 Postman),发送 HTTP 请求并得到响应。在发送请求时,它还指定了请求头部,而服务器作为回报,发送响应头部。
HTTP 头部允许客户端和服务器分别在与请求和响应交互时发送和接收额外的信息。这决定了 HTTP 事务的确切行为。您可以参考以下资源来了解有关头部的更多信息。我们将在下一节中快速查看头部:
在 Postman 中,您可以点击 代码,如下面的截图所示:

点击此链接将打开一个模态窗口,显示发送到服务器的 HTTP 请求头以处理请求。查看以下模态窗口的截图,其中清楚地说明了请求类型为 GET,Host 为 API 的 URL,然后是其他头信息,如 Cache-Control 和 Postman-Token:

想要知道此 GET 调用的 jQuery 代码片段是什么样子吗?使用 Postman 非常简单。在主屏幕上点击代码,然后从包含语言的下拉菜单中选择 jQuery。(见以下截图。)此外,您还可以通过从下拉列表中选择来获取不同语言的代码。复制愉快!

响应头 清楚地显示在主页面中,如下截图所示。注意,提到了状态码,在这种情况下是 200 OK。那么,这个代码代表什么意思呢?
我们将在下一节中讨论这个问题。

Postman 响应头
状态码
当服务器返回响应时,它包括状态码。这些状态码通知客户端请求在服务器上的执行情况。作为开发者,您不需要知道每个状态码(有很多),但您应该了解最常见的状态码及其用法:
| 状态码 | 说明 |
|---|---|
| 200 OK | 对成功 HTTP 请求的标准响应。 |
| 201 CREATED | 当成功创建项目时,对 HTTP 请求的标准响应。 |
| 204 NO CONTENT | 如果响应体中没有返回任何内容,对成功 HTTP 请求的标准响应。 |
| 400 BAD REQUEST | 由于请求语法错误、大小过大或其他客户端错误,请求无法处理。 |
| 403 FORBIDDEN | 客户端没有权限访问请求的资源。 |
| 404 NOT FOUND | 资源当前无法找到。它可能已被删除,或者尚不存在。 |
| 500 INTERNAL SERVER ERROR | 当在处理服务器端代码时发生失败或异常时,此响应出现。 |
对于以下 HTTP 动词,服务器默认期望从服务器返回一些状态码:
GET: 返回 200 OKPOST: 返回 201 CREATEDPUT: 返回 200 OKDELETE: 如果操作失败,返回 204 NO CONTENT
我们已经看到了 API 如何对 GET 请求返回 200 OK。随着我们继续使用其他动词,我们将探索之前提到的代码返回的响应。
ASP.NET Core HTTP 属性
根据 互联网工程任务组 (IETF) RFC-7231 文档 (tools.ietf.org/html/rfc7231),ASP.NET Core 实现了八种 HTTP 动词中的七种 HTTP 属性。框架中从动词列表中排除的唯一一个是 HTTP TRACE 动词。
以下是在 ASP.NET Core 中提供的完整 HTTP 动词属性列表:
HttpGetAttributeHttpPostAttributeHttpPutAttributeHttpDeleteAttributeHttpHeadAttributeHttpPatchAttributeHttpOptionsAttribute
由于动词名称与属性相关联,很明显它们将用于各自的动词。这些属性帮助框架理解哪个动作方法与哪个动词相关联。考虑到这一点,当控制器收到请求时,它可以决定执行哪个方法。
框架还提供了一个对路由也很重要的属性,名为RouteAttribute。
动作方法的参数还有一些其他属性,用于帮助识别从请求的不同位置(如 URL、Body 等)传递给 API 动作的参数。以下是一些框架中用于动作参数的属性:
FromServicesAttributeFromRouteAttributeFromQueryAttributeFromBodyAttributeFromFormAttribute
POST
使用POST来创建资源。在我们的例子中,我们将尝试使用 POST 请求向服务器创建产品。在这样做之前,我们将对我们的项目做一些修改。你可以在 GitHub 上找到所有相关代码(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,否则返回500。请注意,为了返回正确的状态码,我们正在利用IActionResult接口。这个接口有一组大量的子类,可以通过Controller类访问。由于我们继承了Controller基类,我们可以轻松地使用如StatusCode等方法,根据我们对资源的操作返回我们期望的状态。
在上一节中,我们提到,在POST请求成功时,我们应该收到 201 CREATED 状态码,而在失败时,应该发送一个通用的 500 内部服务器错误响应。这正是代码所做的事情。
另一个有趣的事情是:Created("api/products/{product.Id}", product)。这是Controller类中的一个方法,它将 URL 分配给位置,并将 201 分配给响应的状态码。你不信吗?好吧,让我立即通过Postman来证明这一点。
看看以下是从Postman请求屏幕中捕获的截图:

注意,我们以 JSON 格式传递了产品的数据,创建产品后,API 返回了 201 Created 状态码和创建的新产品的 URL,即 api/products/1。这意味着,当你以GET请求运行此 URL 时,你会收到新创建的产品详情。简单,不是吗?
如您所见,传递的产品详情的数据类型是 JSON,但问题是,谁告诉服务器它是以那种格式存储的?嗯,那是请求头 content-type 设置的值为 application/json。您可以在最后一张截图中看到。默认编码为 charset=utf-8,由 Postman 添加。
然而,有趣的是,Postman 如何知道我们想要的数据类型内容是 JSON?它不能自动设置。我告诉它这样做。
在 URL 文本框下方有设置任何类型请求头的选项。参看以下截图,它显示了我已经设置了 content-type 头:

对于之前提到的 GET 请求,该请求通过 ID 返回产品详情,我们可以设计如下操作方法:
// GET: api/Products/1
[HttpGet("{id}")]
public Task<Product> Get(int id)
=> _productService.GetOrderAsync(id);
在这里,我们为 [HttpGet] 提供了一个模板参数 "{id}"。这将确保有一个 HTTP Get 路由,例如 api/orders/1 可用——其中 ID 是传递给 GET 请求的变量。
我们有一个名为 ProductService 的服务,它实现了 IProductService 接口,并且通过控制器的构造函数,服务(依赖项)被注入,这被称为 依赖注入。在 .NET Core 中,使用内置的 控制反转容器 处理依赖项非常容易。如果您不明白我在说什么,那么我强烈建议您阅读我关于这个主题的另一本书,Dependency Injection in .NET Core (www.packtpub.com/application-development/dependency-injection-net-core-20)。
PUT
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] 属性提供了一个类似于 [HttpGet] 中的模板 {id}。在 PUT 的情况下,它将从 URL 中获取 ID,并从请求体中获取 Product 对象,这由 [FromBody] 属性指定,正如我们在上一节中 POST 的情况所做的那样。
当 ID 和产品对象与参数绑定时,方法体开始执行,这反过来又调用服务方法 UpdateProductAsync 并传递相同的参数。该方法将根据更新是否成功返回一个布尔值。如果一切顺利,我们将通过调用 OK() 方法返回 200 OK,否则如果发生错误,将给出 500 内部服务器错误。
让我向您展示 Postman 的截图:

如果 PUT 请求附带一个已过期的 ID,可以返回另一个状态码,301 Moved Permanently,这意味着请求体中的产品与 ID 不相关。为了识别这种条件,我们需要相应地添加业务逻辑,并且如果我们能够验证 ID 是否与产品相关,如果不相关,我们可以简单地返回 301 Moved Permanently,并附带产品实际当前存在的新的 URL。
DELETE
理想情况下,一个 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。这意味着服务器无法找到请求的资源,因为它已经不存在了。
看一下 Postman 截图。看到状态码是 200 OK 表示删除成功:

SOAP 与 REST 对比
以下是 SOAP 和 REST 之间的一些关键区别:
| SOAP | REST |
|---|---|
| 它是一个基于 XML 的消息协议。 | 它是一种架构风格。 |
| WSDL 用于客户端和服务器之间的通信。 | XML 或 JSON 用于客户端和服务器之间发送和接收数据。 |
| 服务通过调用 RPC 方法被调用。 | 服务通过 URL 暴露端点。 |
| 响应易于人类阅读。 | 响应以纯 XML 或 JSON 的形式可读。 |
| 数据传输通过 HTTP 进行。它利用 SMTP、FTP 等协议。 | REST 数据传输仅通过 HTTP 进行。 |
| 从 JavaScript 调用 SOAP 服务比较困难。 | 从 JavaScript 调用 REST 服务非常容易。 |
单页应用程序模型
传统上,在 Web 应用程序中,客户端请求服务器提供网页。然后,服务器在必要时验证和认证请求后,向客户端响应请求的 HTML 页面。下一个请求可能发生在用户点击页面上的某个链接、提交表单等情况。服务器再次处理请求,并返回另一个 HTML 页面的响应。
你难道不认为我们不应该获取整个 HTML 页面(这将会与最后加载的页面外观和感觉基本相同),而应该只获取我们所需的数据,并更新当前加载的页面本身,而不需要向服务器发送回帖?是的,现代 Web 开发在这方面就是这样工作的。今天,我们只需要根据需要从服务器获取数据使用 Ajax。在收到数据后,我们只需使用 JavaScript 或客户端框架(如 Angular)更新 UI。
这就是我们所说的单页面应用(SPA)。在第一次向服务器请求时,服务器会响应整个应用页面。与传统 Web 应用不同,后续请求不会要求获取 HTML 页面,而是会使用 Ajax 请求获取数据,其中内容类型通常是 JSON。在获取数据后,浏览器只需更新页面中已更改的部分,而不是重新加载整个页面。SPA 通过快速响应用户在相同页面上的操作,无疑提高了用户体验,因为重新加载页面会暂时分散用户的注意力。
然而,实现单页面应用(SPA)并不像我们想象的那么简单,我们必须确保在需要时页面上显示的是最新数据。在这里,当设计 SPA 时,新兴技术,如 ASP.NET Web API,以及 JavaScript 框架,如 AngularJS 和 CSS3,都派上了用场。
您的应用程序可以调用 REST API 的不同端点来完成特定任务,并在收到响应后更新 UI,而无需重新加载页面。
面向服务架构
与 SPA 一样,Web API 在面向服务架构(SOA)中扮演着重要的角色。正如其名所示,它是一种从业务导向的角度处理责任分离的架构方法,将其分解为独立的服务。通常,这些独立的服务或组件可以使用 RESTful Web API 进行设计。
考虑一个电子商务应用,它可能包含不同的组件,如订单、账单、支付处理、客户资料管理等。这些组件各自有自己的业务逻辑,并且可以独立实现。
以下图表示了这样一个具有独立组件的应用程序的图示:

为了使它们独立,可以为这些组件公开 RESTful API,这些 API 可以很容易地被任何客户端/应用程序消费,包括其他组件,只要它们满足认证和授权要求。
以下是一个单体或传统应用架构与面向服务架构(SOA)的图示。它清楚地说明了 SOA 如何为同一业务应用引入可重用组件。此外,通过 Web API 实现它们,可以使其对外暴露,供任何应用程序(包括其他组件)消费,只要它们满足认证和授权要求:

由于本书接下来几章需要探索许多关于 Web API 的内容,因此 SOA 的实现超出了本书的范围。无论我们在本书的应用程序中做什么,都会使用一个 Web API,但你可以将它们分离出来以构建一个更可扩展的架构。
摘要
我们从一些基本的 SOAP 知识开始,然后逐渐转向 REST。
本章简要介绍了 REST 的基本构建块及其实际工作原理。我们探讨了广泛使用的 HTTP 动词返回的不同状态码。
为了探索 Web API 的请求和响应周期,你可以使用 Postman,在这里你不仅能够控制发送和接收的内容,还能获取不同语言的代码以便消费 API。
ASP.NET Core 属性可以绑定到控制器操作方法,使它们在路由和参数方面更具表达性和可管理性。
单页应用程序 可以通过使用客户端技术并消费 ASP.NET Core Web API 来轻松设计,以便在接收到响应后立即更新页面,从而提供流畅的用户体验。
Web API 可以集成到面向服务的架构中,以实现模块化设计,从而提高可扩展性。通过使用 API 将整个架构的不同关键组件进行分离,我们能够更好地在不同的应用程序中重用这些组件,并将它们暴露给世界,以便任何人都可以消费。
在下一章中,我们将探讨与身份验证相关的 Web API 架构的重要部分。
第三章:用户注册和管理
在上一章中,我们构建了应用程序的基础,同时我们也详细探讨了在 ASP.NET Core Web API 内部创建控制器时使用的 HTTP 动词。
现在,我们正逐渐转向 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 中,这样我们就可以创建一个对象并在数据库中保存数据。我们将使用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:这定义了将被提取为模型类的表。我们现在将处理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; }
}
}
配置 DbContext
context类可以在包含OnConfiguring和OnModelCreating方法以及一个名为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启动程序中,我们将使用连接字符串将上下文添加到服务集合中:
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文件夹,然后点击添加,接着点击控制器。您将结束在一个模态窗口中,您将看到创建不同类型控制器选项:

- 选择使用 Entity Framework 的 API 控制器和操作,然后点击添加按钮。以下截图显示了接下来会发生什么:

- 点击添加。*哇!*它完成了所有艰苦的工作,并使用 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。您可以在附带的文件中查看整个代码,或者参考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/Customers URL 和POST HTTP 方法。这最终会调用名为PostCustomers的 API 中的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方法完成后对从action方法接收到的数据进行任何操作,因为它返回整个customer对象。如果你不相信我,请参考以下调试工具源窗口的截图:

在 jQuery Ajax 成功方法中创建新客户后的 POST 请求的响应
那么,是谁做的这件事?简单,以下这个返回语句,它位于POST方法内部,完成了所有的魔法:
return CreatedAtAction("GetCustomers", new { id = customers.Id }, customers);
这一行做了几件事情:
发送状态码:201 已创建,因为
POST操作成功创建了资源。设置一个带有资源实际 URL 的位置头。如果你记得 RESTful 特性,在
POST操作之后,服务器应该发送资源的 URL。这正是它所做的事情。
让我通过展示开发者工具的网络标签来证明我的观点。你也可以使用Postman并分析它。以下截图显示了响应细节:

接收到的带有状态码和位置头的 POST 成功请求的响应
Guid实际上是Customer ID,正如我们在数据库中的列类型中定义的那样,我在Customer模型类的构造函数中为其赋值。
现在,如果你复制这个 URL 并在浏览器或 Postman 中打开它,你将得到客户的详细信息,如下面的截图所示:

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

记住,我们添加了一行来检查电子邮件 ID 的存在,并添加了一个ModelState错误。现在它正在执行。
为了简化本书中的演示,我只是保存了纯文本密码。你实际上不应该这样做。实现密码的正确加密是必须的。
使用这种方式,我将结束注册过程。然而,在客户端和服务器端实施验证都有空间。你可以在Model类的属性中添加属性,使其更加稳固,这样你就不会从客户端收到不良数据。当ModelState验证失败时,发送一个BadRequest()响应。可以给Model类添加必需的电子邮件格式和密码比较属性。
CORS
如果你调用 API 操作时看到以下错误,那么你需要启用跨源资源共享(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 的信息,请访问 docs.microsoft.com/en-us/aspnet/core/security/cors?view=aspnetcore-2.1。
将基本认证添加到我们的 REST API 中
现在我们已经注册了 客户,我们可以转向认证过程。认证是为了验证 客户 是否是我们站点的有效用户。由于他们使用我们的注册表单进行了注册,我们已经有了他们的凭据。当他们尝试使用这些凭据从我们的网站访问任何资源时,我们将首先进行验证,然后允许访问。
注册将允许所有人进行,且不需要认证。然而,当 客户 想要 读取他们的个人资料详情 或 删除他们的账户 等操作时,我们需要设置认证,以确保数据返回给实际信任的应用程序用户。
对于 基本认证:
我们将获取 用户名,它将是客户端请求资源时的 电子邮件 ID 和 密码。这将通过 HTTP 头部发送。当设计客户端时,我们将看到它。
然后,将从数据库验证这些数据。
如果找到,将允许操作,否则将发送一个
401 未授权响应。
第 1 步 – 添加 (授权) 属性
让我们限制返回 客户 个人资料详情的动作方法,即 CustomersController 的 GET 方法,命名为 GetCustomers([FromRoute] Guid id)。
当 客户 尝试访问个人资料时,我们将验证以下两点:
请求来自应用程序的信任用户。这意味着请求来自拥有有效 电子邮件 和 密码 的 客户。
客户只能访问他们的个人资料。为了检查这一点,我们将验证 客户 的凭据(存在于请求中)与 URL 上请求的 客户 ID。
让我们开始吧。记住,我们的目标是实现以下内容:
[Authorize(AuthenticationSchemes = "Basic")]
public async Task<IActionResult> GetCustomers([FromRoute] Guid id)
现在,我们将专注于这个动作方法来理解这个概念。您可以看到这里定义了 AuthenticationScheme 为 Basic 的 Authorize 属性。这意味着我们必须告诉运行时什么是 基本认证,这样它就会在进入动作方法之前先执行。
如果认证成功,动作方法将被执行,否则将向客户端发送一个 401 未授权响应。
第 2 步 – 设计 BasicAuthenticationOptions 和 BasicAuthenticationHandler
首先,我们需要一个类,该类将继承 Microsoft.AspNetCore.Authentication 中存在的 AuthenticationSchemeOptions 类,如下面的代码块所示:
using Microsoft.AspNetCore.Authentication;
namespace DemoECommerceApp.Security.Authentication
{
public class BasicAuthenticationOptions : AuthenticationSchemeOptions {}
}
为了简单起见,这里留空,但可以加载不同的属性。我们不会深入探讨这一点。
接下来,我们需要一个用于基本认证的处理程序,我们将在这里实现实际的逻辑:
public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOptions>
我们可以在其构造函数-like 后面添加一个额外的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 挑战问题,这意味着每当决定客户无效时,可以在该方法中编写代码来处理这种情况。
我们假设我们将通过名为Authorization的 HTTP 头接收电子邮件和密码,它们由分隔符冒号(:)分隔。以下是从头中提取数据并验证其是否正确的代码:
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()。
让我们尝试将这个认证附加到我们的操作方法上,使用以下类似的方法:
[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 获取详细信息来显示用户的个人资料。你可以使用对 API 的jQuery Ajax调用并操作接收到的结果:
$.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 加密。在你得到结果后,你可以做许多事情。以下截图显示了使用一些 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);
}
如果客户端尝试访问受限制的资源或需要认证的资源,那么通知客户端关于认证类型和相关信息的责任在于服务器。WWW-Authenticate HTTP 响应头由定义了应使用何种认证方法来获取请求的受限制资源的服务器设置。
因此,很明显,WWW-Authenticate头会与 401 未授权响应一起发送。该字符串包含三个东西:认证类型、域和字符集。域是认证有效的域名或区域。
在我们的例子中,方案是Basic,域是http://localhost:57571,字符集是UTF-8。因此,如果客户端提供了基本认证参数作为用户名和密码,那么这些在localhost:57571域中将是有效的。
这就是它的含义。所以,只需移除分配代码的标题或将其注释掉以进行测试。以下是从 Chrome 的开发者工具网络标签页的截图:

看看以下截图中的警告消息,这是我们在 Ajax 调用的错误方法中包含的。当 API 操作在没有凭证的情况下被调用时,会发生这种情况:

将 OAuth 2.0 认证添加到我们的服务中
OAuth 是一个开放标准,由 API 用于控制客户端(如网站、桌面应用程序甚至其他 API)对资源的访问。然而,实现 OAuth 的 API 可以在不与第三方应用共享密码的情况下提供用户信息。
你可能见过允许使用不同服务(如 Facebook、Twitter 或 Google)登录的网站,比如(对于 Facebook)使用 Facebook 登录。这意味着 Facebook 有一个 OAuth 服务器,它会通过你之前提供给 Facebook 的某个身份验证你的应用程序,并给你一个有效的访问令牌。使用该令牌,你可以读取所需用户的个人资料。
以下是一些基本的 OAuth2.0 术语:
资源: 我们已经在之前的章节中定义了这一点。资源是我们需要保护的东西。这可能与我们系统相关的任何信息。
资源服务器: 这是保护资源的服务器,通常是用来访问我们电子商务数据库的 API。
资源所有者: 将授予我们访问特定资源的人。大多数情况下,用户是所有者,正如你点击使用 Facebook 登录时所见,它会要求你的登录和同意。
客户端: 希望访问我们资源的应用程序。在我们的例子中,它是当在设计的 HTML 页面上执行jQuery代码时尝试访问资源的浏览器。
访问令牌:这实际上是这个架构的基石。我们将要设计的 OAuth 服务器应该使用用户的凭证来提供令牌,以便后续访问我们的资源,正如我们所知,OAuth 标准告诉我们不要向客户端提供密码。
Bearer 令牌:这是一种特定的访问令牌,允许任何人轻松地使用该令牌,这意味着,为了使用令牌进行资源访问,客户端不需要加密密钥或其他秘密密钥。由于这比其他类型的令牌安全性较低,Bearer 令牌应仅在 HTTPS 上使用,并且应在短时间内过期。
授权服务器:这是向客户端提供访问令牌的服务器。
让我们开始将 OAuth 添加到我们的 Web API 中。我们将使用 IdentityServer4,这是一个免费的、开源的 OpenID Connect 和 OAuth 2.0 框架,适用于 ASP.NET Core。项目可以在以下位置找到:github.com/IdentityServer。
IdentityServer (identityserver.io/) 基于 OWIN/Katana,但据我们所知,它作为分布式软件包以 NuGet 包的形式提供。为了开始使用 IdentityServer,请安装以下两个 NuGet 包:

在生产场景中,理想情况下,授权服务器应与主 Web API 分离。但为了这本书的简单起见,我们将直接将其放入同一个 Web API 项目中。我们不使用默认的 ASP.NET Core Identity。我们将使用我们自己的表格集。例如,我们将使用我们的 客户 表的详细信息进行验证。
第一步 – 设计 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" }
}
};
}
您可以根据需要添加多个客户端。您可以在该方法中设置客户端 ID、客户端密钥和授权类型,根据 OAuth 标准。注意,密码设置为secretpassword。您可以在这里设置任何字符串;它可以是Guid。在这里,GrantType.ResourceOwnerPassword定义了我们验证传入请求以生成令牌的方式。
它告诉授权服务器,“嘿,在请求体中查找username和password。”还有其他类型的授权可用。您可以在官方文档链接中了解更多信息。
您现在可能有一个问题!我们将如何处理username和password?当然,我们将验证它们,但用什么呢?答案是来自Customers表的Email和Password字段。我们还没有做任何与将授权服务器与客户端表连接相关的事情。这就是我们接下来要做的。但在那之前,让我们在Startup中注册这些设置。
只是为了确保我们处于同一页面上,我们已经到达了这样一个点,即我们正在尝试从授权服务器生成令牌以访问我们的 API。
第 2 步 – 在启动时注册配置
对于注册,我们必须在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实际上是一个包含Bearer值的字符串常量。Bearer 身份验证也称为令牌身份验证。这意味着我们的客户端需要发送一个令牌才能访问 API 的资源。而要获取令牌,他们需要调用我们的授权服务器,该服务器位于/connect/token。
注意我们已将Audience设置为FlixOneStore.ReadAccess,这是我们已在配置中为客户端指定的。简单来说,我们正在设置 Bearer 类型的身份验证。
第 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/token URL 上执行了一个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 的过程:

在授权头中使用令牌调用 API 端点
哇!这成功了。我只是复制了我得到的令牌,并以Bearer [访问令牌]的格式添加到授权头中,并发送了请求。现在一切完美了。
第 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 帮助我们构建所有声明。我们将在稍后看到这些声明的实际用途。
我们在配置中添加了多个声明,包括 ApiResources 的列表,例如 Id、Name、Email、GivenName 和 FamilyName。这意味着服务器可以返回关于 客户 的这些详细信息。
让我们跳转到 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 令牌,你可以使用 jwt.io/ 来解码它并查看里面有什么:

基于客户端的 API 消费架构
我们已经讨论了 RESTful 服务、Web API 以及如何注册、认证和授权用户。此外,我们还稍微关注了服务的消费方面。服务不仅设计用于在 Postman 上进行测试,而且实际上是为了被不同类型的应用程序(桌面、Web、移动、智能手表和物联网应用程序)消费。
当大多数现代应用程序基于 MVC 架构时,在那些应用程序的控制器中消费 Web 服务有一定的需求。基本上,我需要找到一种方法,从我的控制器中调用服务而没有任何麻烦。
为了实现这一点,我不能调用 Postman 或任何其他第三方工具。我需要的是一个客户端或组件,它可以与我交互 RESTful Web API。我只需要告诉这个客户端我需要通过传递 id 或某些标识符来获取 客户 的详细信息,其余的由客户端处理,包括调用 API、传递值和获取响应。响应最终返回到控制器,然后我可以对其进行操作。
我们将在第十章 构建 Web 客户端(消费 Web 服务) 中探讨如何通过简单、快捷、简单的步骤构建 REST 客户端。
摘要
注册是应用程序中非常常见但非常重要的一个部分。我们通过 API 处理了 客户 的注册。在此之前,我们学习了如何使用 EF Core 引导 API 控制器操作和模型类。在我们做所有这些的同时,我们还遇到了 CORS 并学习了如何处理它。
然后,我们逐渐转向认证部分,其中我们详细讨论了 基本认证。这是一种通过 客户(即我们 API 的用户)的凭证(用户名 和 密码)来验证客户端的机制,这些凭证随请求一起传递。
Bearer 或 基于令牌 认证是我们接下来探索的主题,我们使用了 IdentityServer4 实现了 OAuth 模式。在这种情况下,客户端不能像基本认证那样直接通过 用户名 和 密码 访问资源。它首先需要一个令牌,这个令牌是由一个授权服务器在客户端请求时,根据客户端的详细信息(如 客户端 ID 和 客户端密钥)生成的。然后,这个令牌可以被发送到 API,用于后续请求以访问受限制的资源。
在下一章中,我们将把这些知识应用到构建我们 API 的其他组件中,例如 购物车、运输、订单项 和 结账。
第四章:项目目录、购物车和结账
本章将探讨编码电子商务应用程序的主要部分及其相关的 API 端点。
我们已经在上一章讨论了用户注册和身份验证,我们将继续使用这些知识来帮助我们在本章构建的不同控制器中实现安全性。
为了高效地显示产品和搜索它们,我们还将设计ProductsController。
之后,我们还将探讨如何将您的产品添加到购物车中,讨论如何在购物车中添加、更新和删除项目。
最后,但同样重要的是,我们还将查看订单管理和处理。
在本章中,我们将涵盖以下主题:
实现不同的控制器
产品列表和产品搜索
添加、更新和删除购物车项目
在控制器上实施安全性
订单处理和发货信息
实现控制器
由于我们将学习我们应用程序的核心功能,我们需要设计其控制器,以便我们有 REST 端点来执行来自客户端的任务。例如,产品列表、产品搜索、添加到购物车、下订单和处理发货可以通过为每个功能分配一个专门的控制器来完成。这些控制器将负责对数据库执行操作,因此我们需要为相关表建模类。让我们开始工作!
生成模型
以下行可以在包管理控制台中执行以生成数据库中所有表的模型类:
Scaffold-DbContext "Server=.;Database=FlixOneStore;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -Force
上述命令将为Models文件夹中的每个表生成类文件,如下面的截图所示:

如果您还没有这样做,请参考github.com/PacktPublishing/Building-RESTful-Web-services-with-DOTNET-Core中的数据库脚本以生成您应用程序的数据库表。
生成控制器
要为模型生成控制器,右键单击Controllers文件夹 | 添加 | 控制器 | 使用 Entity Framework 的 API 控制器(带操作)。
首先,让我们从ProductsdetailsController开始,因为我们最初想要向客户展示产品列表。
通过生成器生成的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 格式显示产品详情数组。请注意,我们通过在请求的“头部”选项卡中设置contentType头部为application/json来发送此请求。
产品列表
现在,让我们设计所需的 jQuery 代码来消费此端点,以便我们可以在网页上显示这些记录并列出可供购买的产品。它应该看起来如下:
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>');
});
}
});
}
要获取不同语言的调用 API 的代码,您可以在 Postman 中点击代码链接,然后选择所需的语言。我们已经在之前的章节中讨论过这一点。
前面的方法调用端点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。
调用此端点实际上不会工作,这是因为存在循环引用。如果您仔细观察Products和Productdetail模型,您应该会看到它们都相互包含引用。这在解析到 JSON 时会产生问题。为了避免这种情况,我们需要在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现在接受一个searchText参数,该参数作为 URL 参数传递给 API。现在,只需在调用此方法时发送参数值即可。
以下代码展示了搜索功能,它获取文本并使用输入的值执行LoadProducts:
$('#btnSearch').click(function ()
{
var searchText = $('#txtSearch').val().trim();
if (searchText)
{
$('#tblProducts').empty();
LoadProducts(searchText);
}
});
以下截图显示了此功能在实际操作中的样子:

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

客户端添加到购物车函数
当客户点击“添加到购物车”时,信息将被添加到另一个名为“我的购物车”的 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作为参数,因为这些信息在购物车 HTML 表中显示。
注意,在前面的图像中,每一行都有一个删除图标。这是通过在锚点内添加glyphicon并在其周围包裹一个 span 来实现的,该锚点的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 调用
在本节中,我们将查看客户端实际执行的 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);
}
});
}
直接了当,不是吗?现在我们可以构建一个购物车对象并发送一个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可以存储在任何请求的会话存储中——尽管这被认为是一种风险行为。
您可以发送电子邮件 ID 到 POST 操作,而不是发送Customerid。然后使用电子邮件 ID,您可以获取Customerid,这可以用来插入购物车记录。
让我们现在运行我们的应用程序并点击特定产品的“添加到购物车”。哎呀!开发工具中出现了以下错误:

为什么会发生这种情况?
这种错误的真正原因实际上非常明显。由于控制器已应用了 [Authorize] 属性,现在对 CartsController 的每次调用都期望由 OAuth2.0 授权服务器生成的令牌,该令牌是通过带有 Email Id 和 Password 的请求生成的。
我们已经详细探讨了 OAuth2.0 认证。
为了继续我们的实现,我们将从 Postman 调用令牌服务器,并在我们的应用程序中使用它。理想情况下,当你收到未经授权的错误时,你应该打开登录屏幕,以便用户可以登录。如果验证了 Email Id 和 Password,则会返回一个令牌。此令牌可用于进一步的请求,例如添加到购物车。
为了节省时间和空间,我们将直接使用 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);
},
});
注意,为了简洁起见,我们在前面的片段中删除了令牌字符串。使用前面的代码,将创建一个购物车记录,并从 API 返回的所有数据都将提供有关该记录的所有详细信息。您可以在 HTML 行中存储购物车的 Id(如前述代码块中所示),以便在更新或删除购物车记录时进行进一步处理。
下面的截图显示了 Chrome 开发者工具中元素标签页的购物车记录 ID,存储为 data-cart-id 属性:

PUT – api/Carts/
现在我们已经添加了一个购物车记录,让我们转到更新记录,每当客户反复点击“添加到购物车”按钮时。我们已经有更新客户端表格中数量和价格的代码,所以我们只需要编写调用 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] 属性的路由中读取,而购物车对象则从请求体中读取,因为它带有 [FromBody] 标记。如果没有与路由一起发送 ID,客户端将收到 400 BadRequest 错误。
API 操作现在已更新了带有必要详细信息的记录,如下面的截图所示:

如您所见,我们已经点击了四次“添加到购物车”。finalPrice 根据以下方式计算:49.99 * 4。
DELETE – api/Carts/
路由 /api/Carts/{id} 告诉我们我们只需要将购物车 Id 发送到 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。
以下两个表负责订单过程:
订单:这存储了送货地址详情、客户详情、订单状态等
OrdersProducts:这存储了添加到购物车的产品、它们的价格和数量
Orders类是我们最初生成的脚手架,其中包含所有必要的信息。让我们使用这个类生成控制器。按照我们为ProductsController、ProductsdetailsController和CartsController生成控制器的方式,遵循相同的流程生成控制器。
模型和控制类可以在 GitHub 仓库中找到。
现在是时候调用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 设计
在我们继续之前,我们需要向用户显示一个模态框,让他们可以输入他们的送货地址。一旦点击“下订单”按钮,模态框就会打开,如下面的截图所示。

以下代码片段展示了“下订单”的点击事件(如果购物车中有项目,则会打开模态框):
$('#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.");
});
通过点击“提交”使用POST进行 Ajax 调用,将订单记录插入数据库。以下代码片段是“提交”的点击事件:
$('#btnConfirmOrder').click(function () {
PostOrders();
});
客户端 PostOrder 函数
让我们继续到PostOrders所需的步骤。
构建与模型类 Orders.cs 匹配的订单对象
在这里,我们必须从与送货信息相关的文本框中读取值,并将它们与Orders.cs的字段匹配,以构建一个对象。OrdersProducts是一个表示模型类OrdersProducts.cs的数组。每个订单可以与多个产品相关联。
以下代码实现了订单对象:
// 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()
};
将购物车项目作为数组推入订单对象
填充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/Orders
太好了,现在我们有了我们的对象!现在是时候使用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 方法
订单表看起来与我们发送给客户端的略有不同。在以下截图中,请注意标记在框中的字段。这些是我们没有发送而是在动作方法内部操作的字段:

像姓名、电子邮件和电话号码这样的字段可以从客户表中获取。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。
这些可以在订单构造函数内部完成,如下所示:
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。
本书演示的应用程序尚未准备好投入生产。通常,应该有一个与OAuth2.0 认证一起工作的登录页面。在 API 端以及客户端进行基本验证也是必要的。在这本书中,我们的应用程序是为了展示我们正在探索的概念而构建的,但你绝对可以优化我们的示例,甚至在此基础上构建。
暴露运输详情
使用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 文档的引用(docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/):
“中间件是组装到应用程序管道中以处理请求和响应的软件。”
查看以下图表,展示了一个简单的中间件组件的示例:

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

使用 Visual Studio 2017 创建新项目
从“新建项目”屏幕中选择 ASP.NET Core Web 应用程序。
命名您的新项目(例如
Chap05_01),选择位置,然后单击“确定”,如下截图所示:

选择新项目模板
从新的 ASP.NET Core Web 应用程序模板屏幕中选择 API 模板。确保您选择了 .NET Core 和 ASP.NET Core 2.0。
单击“确定”,如下截图所示:

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

显示 Chap05_01 项目的文件/文件夹结构
从我们刚刚创建的模拟项目中打开 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 添加到请求管道中。
你可以在 docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.iapplicationbuilder?view=aspnetcore-2.0 获取有关 IApplicationBuilder 的更多信息。
它还指示系统在开发环境中使用特定的异常页面。前面的方法配置了应用程序。
在下一节中,我们将详细讨论四个重要的 IApplicationBuilder 方法。
使用
Use 方法向应用程序请求管道中添加一个委托。请查看以下截图以了解此方法的签名:

Use 方法的签名
正如我们在上一节中讨论的,中间件方法可以短路请求管道或将请求传递给下一个委托。
短路请求不过是结束请求。
请查看以下 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);
}
在前面的代码中,我试图通过使用局部函数 RequestDelegate 来展示 Run 终止请求管道。在这里,我使用了局部函数。
你可以看到我在此之前添加了一个控制台记录器,并且有空间添加更多的代码片段,但不是那些将响应发送回客户端的代码片段。在这里,Run 通过返回一个字符串来终止。运行 Visual Studio 或按 F5 键,你将得到类似于以下截图的输出:

映射
Map 方法有助于当你想要连接多个中间件实例时。为此,Map 调用另一个请求委托。请查看以下截图以了解此方法的签名:
^(Map 方法的签名)
看看下面的代码:
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);
}
在此代码中,我添加了一个Map,它只是映射<url>/testroute。接下来是之前讨论过的相同的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。
参考以下 请求委托 部分,以刷新你对如何创建一个新的 ASP.NET Core 项目的记忆。
以下截图显示了我们的产品 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();
}
}
ProductRepository类实现了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.ASPNETCore NuGet 包。
现在,打开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。在网页浏览器中,你可以在地址的 URL 后使用后缀/swagger。
完整的源代码,请参考 GitHub 仓库链接: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接口,它来自依赖注入容器(更多详情请见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资源:

点击发送按钮,你期望返回产品列表,但情况并非如此,如下所示:

上述异常发生是因为我们在ProductController中使用了一个非泛型类型,该类型不可注入。
因此,我们需要对我们的ProductController进行一些小的修改。看看以下代码片段:
private readonly IProductRepository _productRepository;
private readonly ILogger<ProductController> _logger;
public ProductController(IProductRepository productRepository, ILogger<ProductController> logger)
{
_productRepository = productRepository;
_logger = logger;
}
在上述代码中,我添加了一个泛型ILogger<ProductController>类型。由于它是可注入的,它将自动解析。
与其早期版本相比,.NET Core 2.0 中的日志记录略有不同。默认情况下,非泛型 ILogger 的实现不可用,但 ILogger<T> 可用。如果您想使用非泛型实现,请使用 ILoggerFactory 而不是 ILogger。
在这种情况下,我们的 ProductController 构造函数将如下所示:
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 命令。它将构建项目,您将收到类似于以下截图的消息:

从这一点开始,如果您运行 Postman,它将给出以下截图所示的结果:

上述代码添加了记录操作的能力。您将收到类似于以下截图所示的类似日志操作:

在这里,我们编写了一些使用默认 ILogger 的代码。我们使用了默认方法来调用记录器;然而,在某些场景中,我们需要一个定制的记录器。在下一节中,我们将讨论如何编写用于自定义记录器的中间件。
通过构建自己的中间件来拦截 HTTP 请求和响应
在本节中,我们将为现有应用程序创建自己的中间件。在这个中间件中,我们将记录所有请求和响应。让我们按以下步骤进行:
打开 Visual Studio。
通过单击文件 | 打开 | 项目/解决方案(或按 Ctrl + Shift + O)打开 Product APIs 的现有项目,如图以下截图所示:

- 定位到您的解决方案文件夹并单击打开,如图以下截图所示:

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

右键单击
Middleware文件夹并选择添加 | 新项。从网络模板中,选择中间件类并将新文件命名为
FlixOneStoreLoggerMiddleware。然后单击添加,如图以下截图所示:

您的文件夹层次结构应类似于以下截图所示:

感谢 Justin Williams 提供的 POST 资源的解决方案;他的解决方案可在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 规范(见www.jsonrpc.org/specification获取更多详细信息))定义了各种数据结构和它们的处理规则。
主要对象按规范在以下部分中展示。
请求对象
Request对象代表发送到服务器的任何调用/请求。Request对象具有以下成员:
jsonrpc:一个表示 JSON-RPC 协议版本的字符串。它必须准确(在这种情况下,版本 2.0)。
method:一个字符串,包含要调用的方法名称。以单词
rpc开头并后跟一个点字符(U+002E 或 ASCII 46)的方法名称被限制为 rpc-内部方法和扩展,不得用于其他任何目的。params:一个结构化值,主导参数值。在整个方法调用过程中都要使用此成员。此成员可能被删除。
id:客户端固定的一个标识符,如果构成,必须有字符串、数字或null值。
响应对象
根据规范,每当对服务器进行调用时,服务器必须有一个响应。Response以一个包含以下成员的单个 JSON 对象表示:
jsonrpc:一个表示 JSON-RPC 协议版本的字符串
result:一个必需的成员,如果请求成功
error:如果发生错误,一个必需的成员
id:一个必需的成员
在本节中,我们概述了 JSON-RPC 规范 2.0。
摘要
在本章中,我们讨论了与支付网关、订单跟踪、通知服务等相关的外部 API/组件的集成。我们还通过实际代码实现了它们的功能。
测试是我们帮助代码无错误的唯一过程。它也是所有希望使代码整洁和可维护的开发者的实践。在下一章中,我们将涵盖日常开发活动中的测试范式。我们将讨论与测试范式相关的一些重要术语。我们还将涵盖这些术语的理论,然后我们将涵盖代码示例,查看存根和模拟,并了解集成、安全和性能测试。
第六章:测试 RESTful Web 服务
一个系统在经过各种场景的测试之前无法成熟。这些场景通常基于领域专家的经验或现有的生产环境。即使在系统被称为完美系统的情况下,系统在生产环境中崩溃的可能性总是存在的。对于 Web 应用程序,由于性能问题、糟糕的用户体验等原因,条件更为关键。系统应该通过一系列开发原则的过程来应对这些问题。简单来说,我们必须测试系统。测试是一个确保系统质量的过程。
换句话说,质量保证或测试是从不同方面评估系统的一种方式。当系统需要测试以识别错误代码,或者我们想要评估其业务合规性时,这个过程也非常有用。
质量保证是一个评估系统并确保其质量的过程。
测试完全依赖于系统的架构风格,并且因系统而异;一切取决于我们如何策略性地规划测试方法或计划。
在本章中,我们将主要关注测试 RESTful 服务,并通过遵循测试驱动开发方法来使我们的代码更佳。在本章结束时,你将能够使用测试范式在日常开发活动中进行测试,了解存根、模拟以及了解集成和安全性和性能测试。
在本章中,我们将涵盖以下主题:
测试范式(包括测试用例创建在内的质量保证基础)
测试 ASP.NET 核心控制器(单元测试)
存根和模拟
安全测试
集成测试
模拟对象
使用 Postman、Advanced RESTClient 等工具进行服务调用测试
用户验收测试
性能或负载测试
测试范式
在上一节中,我们了解到测试和质量保证是软件开发周期中最重要的部分之一。我们应该采取措施设计一个测试软件的框架,这被称为测试范式。
测试范式是一种测试框架。它基于一个人计划实施测试的方式。简而言之,测试范式是一种测试方法。
测试方法是你决定如何创建测试用例的地方,包括它的语言将是什么,你将如何记录测试用例,等等。这也告诉你你将如何执行测试方法(例如,使用黑盒测试)。
测试方法是一种基于特定输入测试或验证特定输出的方法,而不了解系统的内部功能。
在我们创建测试用例或开发测试范式或测试框架之前,我们需要掌握一些重要的术语。
测试覆盖率和代码覆盖率
从一般意义上讲,覆盖率是指覆盖的内容以及如何衡量这种覆盖率。从开发者的角度来看,在测试驱动开发中编写单元测试告诉我们如何以及覆盖了代码的哪个区域。
测试期间执行的代码测量称为代码覆盖率。
测试期间执行的测试用例的测量称为测试覆盖率。
代码经过单元测试,并已证明被覆盖的代码已通过测试。在这个代码覆盖率中,会有许多被覆盖的内容,包括代码行、函数、条件、表达式、API 资源等。
对于软件测试术语,请参阅castb.org/wp-content/uploads/2014/05/istqb_glossary_of_testing_terms_v2.3.pdf。
测试覆盖率和代码覆盖率也可以涵盖以下任何测试类型:
单元测试
安全测试
集成测试
在接下来的章节中,我们将通过代码示例详细查看这些测试。
任务、场景和用例
当某人使用测试范式工作时,他们应该了解任务、场景和用例等术语。在本节中,我们将详细讨论这些术语:
- 任务:任务是一个通用词汇,不仅与软件行业相关,还与许多其他行业相关。这是一项需要完成的行动或工作。完成任务会有不同的方法,但任务的整体意图是应该完成。在不同的领域,任务有不同的目的。在敏捷开发中(
whatis.techtarget.com/definition/storyboard),故事板或任务板帮助开发者理解需要完成的工作。
以下图表说明了我们所说的任务:

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

在前面的图中,很明显,“测试用例”是“测试场景”的子集,而“用例”是“测试场景”的超集。每次创建测试用例时,它都是从测试场景中下来的。
清单
通常,清单不过是一系列需要采取行动以实现目标的项目列表。清单可以是待办事项列表,日常活动列表,或开发者的任务列表。
在测试的世界里,列表可能包含要验证的测试用例,需要执行的测试列表等。清单因人而异,从开发者到开发者,甚至从组织到组织,但清单的目的是始终限制人类忘记某些事情的行为。
缺陷和错误
缺陷和错误是行业中用得最多的术语之一。在一些组织中,这些术语可以互换使用。然而,一般来说,缺陷是指执行正确但产生意外输出的东西,例如,2 + 3 = 6。另一方面,缺陷是指在规划过程中被遗漏的东西。
关于缺陷和错误的一些注意事项:
缺陷几乎总是由于需求的纯实现不当,例如,错误地满足基本需求的代码
缺陷通常在开发阶段或测试阶段被识别
缺陷与设计或需求差距有关,这种差距在生产过程中被客户或客户遗漏。
缺陷通常表明人为错误
在测试过程中发现缺陷时可以修复
缺陷可能导致系统出现故障,进而导致设计问题
测试方法
通常,测试方法是一个执行路径,说明了测试将如何进行。这些方法因系统而异;如果一个系统需要咨询方法,并不意味着另一个系统也是如此。不同的系统需要不同的测试方法。
测试方法是一种测试策略,它只是系统或项目的实现。
测试策略应该对每个人都很清晰,以便创建的测试可以帮助非技术团队成员(如利益相关者)了解系统是如何工作的。这些测试可以是自动化的,例如测试业务流程,也可以是用户验收测试系统上的用户可以执行的手动测试。
测试策略或方法有以下技术:
主动式:这是一种早期方法,试图在从初始测试设计中创建构建之前修复缺陷
反应式:在这种方法中,一旦编码完成就开始测试
测试金字塔
测试金字塔是一种策略或定义在 RESTful 服务中应该测试什么的途径。换句话说,我们可以这样说,测试金字塔帮助我们定义 RESTful 服务的测试范围。
测试金字塔的概念是由 Mike Cohn 在 2009 年开发的 (www.mountaingoatsoftware.com/blog/the-forgotten-layer-of-the-test-automation-pyramid)。
测试金字塔有各种版本;不同的作者通过说明他们如何放置或优先考虑他们的测试范围来描述这一点。
下面的图表示意了 Mike Cohn 定义的相同概念:

让我们详细讨论这些层。
单元测试:这些是测试在 ASP.NET Core 中开发的 RESTful 服务应用的单元中的小功能性的测试
RESTful 服务测试(验收测试):这些是测试独立服务或与另一个服务(通常是外部服务)通信的服务的测试
GUI 测试(REST 客户端测试):这些测试属于将消费 RESTful 服务的客户端或消费者;它们有助于测试整个系统,并具有用户界面方面的特性,是端到端测试
我们将讨论与在 ASP.NET Core 中开发的 RESTful 服务应用相关的测试。
测试类型
在上一节中,我们讨论了测试方法或测试策略。这些策略决定了我们将如何进行系统的测试。在本节中,我们将讨论我们应用中使用的各种测试类型。
测试 ASP.NET Core 控制器(单元测试)
单元测试通常是测试单个函数调用,以确保程序的最小部分得到测试。因此,这些测试旨在验证特定功能,而不考虑其他组件。在这里,测试策略派上用场,并确保系统将执行最佳的质量保证。当它与 测试驱动开发(TDD)方法结合使用时,它增加了更多的力量。
您可以通过在 github.com/garora/TDD-Katas 上的 Katas 学习和实践 TDD。
我们将借助代码示例来讨论这一点。在继续之前,请查看以下先决条件:
Visual Studio 2017 更新 3 或更高版本
.NET Core 2.0 或更高版本
C# 7.0 或更高版本
ASP.NET Core 2.0 或更高版本
Entity Framework 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。
为项目命名,选择解决方案的路径,然后点击确定。
添加
Core文件夹—在 Solution Explore 中,右键单击并选择添加新文件夹,并将其命名为Model。在
Core文件夹下添加Interfaces和Model文件夹。在
Model文件夹下添加一个新类——在解决方案资源管理器中右键单击Model文件夹并选择“添加新项”。然后,选择类或点击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文件夹下添加一个新类——在解决方案资源管理器中右键单击Infrastructure文件夹,选择“添加新项”,然后选择类或点击Shift + Alt + C。将其命名为
ProductContext.cs。
在这个演示项目中,我们不是遵循测试驱动开发方法;我们将仅为了演示目的对应用程序进行单元测试。
- 现在,打开
appsettings.json文件并添加以下代码片段:
"ConnectionStrings":
{
"ProductConnection": "Data Source=.;Initial
Catalog=ProductsDB;Integrated
Security=True;MultipleActiveResultSets=True"
}
在解决方案资源管理器中右键单击项目,然后选择“管理 Nuget 包”。
在 Nuget 包管理器屏幕下,搜索
Swashbuckle.AspNetCore并安装它。
Swagger是开源的,遵循开放规范(github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md)。Swagger 允许您描述 API 的结构。Swagger 为用户提供文档(将要使用 API 的开发者)。有许多开源和商业工具可以与 Swagger 集成。
Swagger CodeGen(swagger.io/swagger-codegen/)有助于为 API 生成客户端库。
Swagger UI(swagger.io/swagger-ui/)有助于生成 API 的文档。
Swashbuckle.AspNetCore(github.com/domaindrivendev/Swashbuckle.AspNetCore)是一个帮助文档基于 ASP.NET Core 的 API 的工具。
在
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);
}
}
请注意,对于完整的源代码,请参阅 GitHub 仓库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。在网页浏览器中,将后缀/swagger添加到地址栏中的 URL,如下面的截图所示:

此 URL 应显示 Swagger API 文档,如下所示截图:

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

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

- 从“添加新项目”模板中选择 .NET Core 和 xUnit 测试项目 (.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 创建了假数据。
完整代码可以从以下链接下载:github.com/PacktPublishing/Building-RESTful-Web-Services-with-DotNET-Core。
ProductTest.cs,我们的单元测试类,看起来如下所示:
xUnit 的重要术语:
事实(Fact) 是一个属性,用于没有参数的正常测试方法
理论(Theory) 是一个属性,用于参数化测试方法
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 方法。
从测试资源管理器运行测试;如果你的测试通过,你应该会看到以下截图:
![截图]()
存根和模拟
存根是对测试期间发出的调用的预定义响应,而模拟是为了设置期望。它们可以进一步解释如下:
存根(Stubs):在
stubs对象中,我们总是得到一个有效的存根响应。响应不关心你提供了什么输入。在任何情况下,输出都将相同。模拟(Mocks):在
mock对象中,我们可以测试或验证可以调用模拟对象的函数。这是一个验证单元测试是否失败或通过的假对象。换句话说,我们可以这样说,模拟对象只是我们实际对象的复制品。
在前面的“编写单元测试”部分中,我们使用了 moq 框架来实现模拟对象。
安全测试
安全是一个非常广泛的概念,不能在几行文字中解释清楚。一般来说,安全测试是一种测试应用程序是否安全或是否存在泄露他人数据的任何可能性的方法。
安全和安全的系统将在第八章 Securing RESTful Web Services 中讨论。
安全测试非常重要,尤其是在我们处理基于 Web 的应用程序时。Web 应用程序是公开可用的,容易受到攻击,因此认证和授权是这里最重要的因素。
FxCop (en.wikipedia.org/wiki/FxCop),它是与 Visual Studio 和 VeraCode (www.veracode.com/) 一起提供的,是安全测试中最受欢迎的工具之一。
集成测试
在单元测试中,我们测试单个代码单元,而在 Web API 的集成测试中,我们测试所有协同工作的服务(包括内部和外部服务,以及第三方组件)。应确保服务调用与外部服务集成。
运行测试
让我们以上一节中创建的相同应用程序进行单元测试:
- 添加一个新的集成测试项目,并确保项目结构看起来如下截图所示:

- 在
ProductTest.cs的构造函数中编写以下代码:
var server = new TestServer
(
new WebHostBuilder()
.UseStartup<TestStartup>()
);
_client = server.CreateClient();
在前面的代码块中,我们初始化了 TestServer,其中我们使用 TestStartup 作为我们的启动入口文件。最后,我们创建了一个 WebHostBuilder() 的 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);
}
在前面的代码中,我们正在消费我们的资源 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文件中运行测试(只需右键单击并选择运行测试)。
如果您需要调试代码,您也可以调试测试。如果您这样做,您应该得到以下结果:

您可以编写尽可能多的测试。测试也取决于您想测试的代码。
模拟对象
如其名所示,模拟对象是非真实对象。模拟对象用于测试目的,包含实际代码,但并非所有真实功能。例如,我们可以创建一个模拟对象来使用 Entity Framework Core 获取数据记录;在这种情况下,我们更倾向于使用内存数据库(docs.microsoft.com/en-us/ef/core/miscellaneous/testing/in-memory)而不是直接数据库连接。
运行测试
让我们参考上一节中开发的单元测试应用程序。按照前面章节中提到的步骤添加一个新的 xUnit 测试项目。
我们正在寻找用于测试目的的模拟对象或数据,所以我们将不会连接到实际的数据库服务器。相反,这里我们将使用内存数据库。
您需要添加Microsoft.EntityFrameworkCore.InMemory NuGet 包以启动内存数据库。
我们在这里不会做任何更改,但我们将创建模拟数据和记录以进行测试。要继续,请将以下代码添加到TestStartup.cs文件中的ConfigureServices方法:
//for tests use InMemory db
services.AddDbContext<ProductContext>
(
o => o.UseInMemoryDatabase
(
InitConfiguration().GetConnectionString
(
"ProductConnection"
)
)
);
在这里,我们使用以下内容:
仅用于测试目的的 InMemory 数据库,通过在
TestStartup类中添加.UseInMemoryDatabase来实现。对于我们的实际代码,数据库服务器在
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();
}
现在我们已经准备好运行测试,所以打开测试资源管理器并运行所有测试。如果测试通过,你应该会看到以下截图:

为了确保测试没有实际连接到数据库,让我们调试测试代码。打开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);
}
现在右键单击“调试测试”,使用单步进入(F11键)进入控制器和仓储,并检查实际的产品列表。您可以看到我们的测试返回的是模拟数据,这意味着它们没有连接到实际的数据库。以下是被调试代码的截图:

以下截图来自我们用来演示使用模拟对象进行测试的小型应用程序。在这种测试方法中,我们的模拟对象总是被击中,而不是任何实际代码。
使用 Postman、高级 REST 客户端等测试服务调用
有很多工具可用于测试 RESTful 网络服务和 API。这些工具提供实际输出。
当您只有 API 资源并想在不同场景中测试预期的输出,但没有实际源代码时,网络服务测试工具非常有用。
我们将使用以下两个工具测试我们的产品 API。
Postman
Postman([www.getpostman.com/](https://www.getpostman.com/))是测试网络服务输出时最受欢迎的工具之一。它还附带一个 Google Chrome 扩展程序:
启动 Postman。如果您还没有它,可以从前面的链接安装它。
选择资源类型为 GET 并输入 API 的 URL;在我们的例子中,它是
http://localhost:60431/api/Product/productlist。点击发送(或者,如果您需要将数据保存在文件中,您也可以点击发送并下载)。
如果测试通过,你应该会看到以下截图类似的内容:

高级 Rest 客户端
高级 rest 客户端(ARC)是另一个流行的工具,也作为 Chrome 扩展程序提供。您可以从 Chrome 扩展程序商店安装它,或者直接从 install.advancedrestclient.com/ 安装:
如果尚未安装,请安装 ARC 的 Chrome 扩展程序。
启动 ARC。
通过 GET 资源。
如果测试通过,你应该会看到以下截图类似的内容:

用户验收测试
正如其名所示,用户验收测试(UAT)是由用户执行或用户接受的测试。在这种测试方法中,可能成为应用程序最终用户的用户将直接参与测试。可能会有用户在生产环境中进行的测试场景,或者他们可能有权访问他们可以接受或拒绝的预测试结果。
这种测试取决于实际将在生产环境中使用应用程序的用户。这种测试通常在 UAT 或预生产环境中进行。
行业中典型的环境被称为开发、预发布、QA、UAT、预生产和生产。在您的组织中,您可能不会根据项目需求拥有所有这些环境;如果是这样,请参阅 www.guru99.com/user-acceptance-testing.html。
UAT 测试也被视为最终测试,其接受或拒绝决定告诉我们当前版本是否将被部署到生产环境中。这种测试的主要重点是业务相关的。这种测试不涉及测试代码或各种模式的实现;它只是确保所有业务规则和需求都已实现。
性能或负载测试
对于 Web 应用程序的性能,可伸缩性非常重要。一个应用程序可以非常安全、经过良好测试,并且使用良好的代码创建,但如果它不可伸缩,用户仍然可能会避免使用它。
我们将在第九章中详细讨论 RESTful Web 服务的扩展,扩展 RESTful 服务(Web 服务的性能)。
对于一个好的 API 来说,性能非常重要,因此我们需要测试并确保我们的应用程序能够处理或承受大量请求。负载测试是一种非功能性测试(www.guru99.com/non-functional-testing.html),负载测试的主要目的不是验证代码或测试代码的健康状况。
这种测试的主要目的是确保基于各种指标(如可伸缩性、可靠性等)的 Web API 表现良好。
以下是一些性能测试的技术或类型:
负载测试:这种测试在特定负载的各种情况下测试系统的行为。这也包括关键交易、数据库负载、应用服务器等。
压力测试:这是一种方法,系统在回归测试中进行,以找到系统容量的上限。它还取决于系统在当前负载超过预期最大负载的关键情况下的行为。
浸泡测试:这也被称为耐久测试。在这个测试中,主要目的是监控内存利用率、内存泄漏或影响系统性能的各种因素。
峰值测试:这是一种确保系统能够承受工作负载的方法。确定性能的最佳任务之一是突然增加用户负载。
在 ASP.NET Core 中,我们可以使用以下方法进行负载测试:
Visual Studio:如果你有 Visual Studio Enterprise Edition,你可以轻松创建一个负载测试项目;更多信息,请访问以下链接:
docs.microsoft.com/en-us/vsts/load-test。WebSurge:这是一种用于 API 的负载测试工具。你可以用于云服务或免费用于学习目的。更多信息,请访问
websurge.west-wind.com/。BenchmarkDotNet:这个工具告诉我们代码中有多少是高效的。它测试不同代码块,这些代码块给出相同的结果,以查看哪个性能最好。更多信息,请访问
github.com/dotnet/BenchmarkDotNet。Netling:这是一个针对网络应用的负载测试工具。使用 Netling,你可以修改代码并重新测试以符合你的性能规模。更多信息,请访问
github.com/hallatore/Netling。
这些工具和 Visual Studio 负载测试的解释,以及工作示例,超出了本书的范围。
在本节中,我们将简单地测试我们的产品 API,以检查它们列出我们请求的产品所需的时间。
你也可以使用简单的网络客户端测试 API 的请求时间。在第十章 Building a Web Client (Consuming Web Services) 中,我们将详细讨论如何构建网络客户端。
查看我们的 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
}
上述代码是自我解释的。我们只是在计算单个和多次请求所需的时间,并检查是否达到我们的基准。
完整的代码可以从 github.com/PacktPublishing/Building-RESTful-Web-Services-with-DotNET-Core 下载。
运行测试
要运行测试,你需要确保你的 API 正在运行,并且可以通过 URL 访问。为此,使用 CLI 完成以下步骤:
打开 Visual Studio 命令提示符
定位到你的 API 项目文件夹
执行命令
dotnet run
现在,你应该会看到一个类似于以下屏幕截图的屏幕:

按照以下步骤使用 Visual Studio 测试资源管理器运行测试:
打开
ProductTest.cs文件打开测试资源管理器
点击运行
这将运行所有测试;你应该会看到一个类似于以下屏幕截图的输出:

我们还可以检查单个 API 完成请求所花费的确切时间。为此,在特定 TestCase 结果的测试资源管理器中点击 输出,你应该会看到以下屏幕:

你也可以使用 CLI 运行这些测试,如下所示:
打开 Visual Studio 命令提示符
定位到你的 API 项目文件夹
执行命令
dotnet test
前面的命令将运行所有测试;如果它们通过,你应该会看到一个类似于以下屏幕截图的输出:

访问 docs.microsoft.com/en-us/dotnet/core/tools/?tabs=netcore2x 检查所有可用的 CLI 命令。
在本节中,我们尝试了一个基于请求时间的简单负载测试。我们尝试了单个调用和多次调用。
摘要
测试有助于确保我们的代码无错误。测试也是所有希望使代码整洁和易于维护的开发者的实践。在本章中,我们涵盖了开发团队日常活动中测试范式,包括对存根和模拟的了解,以及理解集成、安全性和性能测试的重要性。
在接下来的章节中,我们将讨论安全性,包括遵循 OWASP 安全标准和 JWT 认证。我们将使用自定义过滤器和输入验证来涵盖更复杂的场景。数据保护对于任何 Web 应用来说始终是最高优先级,因此我们还将探讨敏感数据的持久化和存储。
第七章:持续集成和持续部署
大多数项目都是团队合作的结果。团队可能位于不同的地方,也可能位于同一个地方,来自不同地点的成员必须同步工作,以确保他们的更改不会与其他团队成员发生冲突。一个系统只有在各种场景中使用后才会成熟;这些场景可能基于领域专家的经验,也可能来自生产环境。即使系统被视为完美的系统,也有可能在生产环境中崩溃。对于 Web 应用来说,由于性能问题、用户体验不佳等因素,条件更为关键。系统应该经过一个过程,在这个过程中,如果团队成员进行更改,代码在单元测试后进行集成,然后部署到相关环境中。
当我们提到部署时,Xcopy 部署立刻浮现在我们的脑海中。在这种部署类型中,你只需构建并复制相关文件,然后部署/粘贴到相关环境中。
在本章中,我们将讨论部署的基础知识以及新兴实践(如持续集成(CI)和持续部署(CD))的影响。我们将重点关注以下主题:
Azure 环境
发布/托管
使用 TFS online 进行 CI 和 CD
CI 和 CD 的区别
简介——部署术语
在进一步讨论之前,我们首先应该讨论为什么我们要讨论部署。部署周期有一个特定的流程,我们应该了解部署术语。部署术语简单地说就是从代码更改到发布的步骤。在本节中,我们将讨论所有这些部署步骤。
构建阶段
在构建阶段,服务源代码编译无误,并且所有相应的单元测试都通过。这一阶段产生构建工件。
持续集成
每当开发者提交任何更改时,CI 都会强制整个应用程序重新构建——应用程序代码被编译,并对其运行一系列全面的自动化测试。这种做法源于大型团队中频繁集成代码的问题。基本思想是保持软件更改的增量或变化尽可能小。这提供了软件处于可工作状态的信心。即使开发者的提交破坏了系统,使用此过程也容易修复。
部署
硬件配置、安装基础操作系统和正确的.NET 框架版本是部署的先决条件。接下来,通过各个阶段将构建工件推进到生产环境中。这两部分的组合被称为部署阶段。在大多数应用程序中,部署阶段和发布阶段之间没有区别。
持续部署
在持续交付(CD)中,每个成功的构建都会部署到首选环境,例如生产环境。环境因组织而异。因此,持续交付不仅仅是为了生产环境,也可以用于其他环境,如开发、预发布等。从技术团队的角度来看,持续交付更为重要。在持续交付下,还有其他一些实践,例如自动单元测试、标记、构建号的版本化和变更的可追溯性。通过持续交付,技术团队确保通过各种低级环境推送到生产环境中的变更能够按预期工作。通常,这些变更很小,部署得很快。
持续交付
持续交付与持续部署(CD)不同。持续部署来自技术团队的角度,而持续交付更侧重于尽可能早地将部署的代码提供给客户。为了确保客户获得无缺陷的正确产品,在持续交付中,每个构建都必须通过所有质量保证检查。一旦产品通过满意的质量验证,何时发布就是业务利益相关者的决定。
构建和部署管道
构建和部署管道是自动化实现持续交付的一部分。它是一系列步骤的工作流程,代码在源代码库中被提交。在部署管道的另一端,生成发布工件。构建和部署管道可能包含的步骤如下:
单元测试
集成测试
代码覆盖率与静态分析
回归测试
部署到预发布环境
压力/负载测试
部署到发布仓库
发布
将业务功能提供给最终用户称为功能的发布。为了发布功能或服务,相关的构建工件应提前部署。通常,功能开关(也称为功能切换)管理功能的发布。如果功能标志(也称为功能切换)在生产环境中未开启,则称为指定功能的暗色发布。
成功部署 RESTful 服务的前提条件
任何系统部署的成功都取决于团队遵循的架构风格和实践。我们采用以下实践后,RESTful 服务成功的可能性更大:
自给自足的团队:作为 SOA 和微服务架构的先驱,亚马逊遵循“两个披萨团队”模式。这意味着一个微服务团队通常不会超过 7-10 名成员。这些团队成员将拥有所有必要的技能和角色;例如,开发、运维和业务分析师。这样的服务团队负责微服务的开发、运维和管理。
持续集成和持续部署:CI 和 CD 是实现基于微服务架构风格的系统中的 RESTful 服务的先决条件。能够频繁集成其工作的较小、自给自足的团队是微服务成功的前提。这种架构并不像单体架构那样简单。然而,自动化和定期推送代码升级的能力使团队能够处理复杂性。Team Foundation Online Services(TFS)、TeamCity 和 Jenkins 等工具在这个领域非常受欢迎。
基础设施即代码:用代码表示硬件和基础设施组件,例如网络,这一想法是新的。它可以帮助你使部署环境,如集成、测试和生产,看起来完全相同。这意味着开发人员和测试工程师将能够在较低的环境中轻松地重现生产缺陷。使用 CFEngine、Chef、Puppet、Ansible 和 PowerShell DSC 等工具,你可以将整个基础设施写成代码。随着这一范式转变,你还可以将基础设施置于版本控制系统之下,并以部署工件的形式进行分发。
云计算的利用:云计算是采用微服务的一个大催化剂。尽管如此,微服务部署并不强制使用云计算。云计算具有近乎无限的扩展性、弹性和快速部署能力。云是微服务的天然盟友,这是不言而喻的。因此,了解和使用 Azure 云将有助于你采用微服务。
Azure 环境
Azure 是微软提供各种云计算服务的一项服务。Azure 是一个云平台,可以帮助你全球范围内构建、部署和管理应用程序。
在我们讨论 Azure 环境之前,我们应该了解云计算。
云计算
简而言之,云计算是一个提供各种基于计算机服务的存储/场所,包括存储、数据库、服务器和软件,通过互联网(在这里,互联网被称为云)。
与云计算相关的术语有很多,你可以参考以下链接了解这些术语:azure.microsoft.com/en-in/overview/cloud-computing-dictionary/。
这些服务可以由任何人销售,提供这些云计算服务的供应商/公司被称为云服务提供商。
云计算不是一个新术语,它已经存在了一段时间,只是现在它变得流行了。如果你使用任何在线服务来帮助你发送或接收电子邮件给其他人,那么这就是云计算。借助云的帮助,你可以做几乎任何事情。这些服务包括:
创建新应用程序
存储数据
托管、部署应用程序
根据你的云提供商提供的服务或你拥有的订阅类型,还有许多其他活动。
云的优势
现在,云计算在 IT 资源相关业务增长中扮演着重要角色。如今,每个人都在从传统系统出发思考不同;正如这里所讨论的,云具有其优势,对所有用户都有益处:
选择并开始:如果你有任何类型的云计算订阅,你无需思考,只需选择你的服务并从任何地方开始。你只需要互联网即可开始。
成本:当你选择云计算时,你无需考虑购买昂贵的硬件或相关基础设施的费用。你可以获得所需的硬件,并且这些硬件是经济实惠的。
速度:你可以快速部署新资源;这些服务性能卓越。
可用性:云计算最重要的好处之一是你无需担心服务的可用性,因为这些服务是全球可用的。例如,如果你从印度租用了虚拟机,那么即使你身处世界的另一端,你也不必担心使用这台机器。
要决定哪个云提供商适合你,请参考 azure.microsoft.com/en-in/overview/choosing-a-cloud-service-provider/。
云计算服务模型
云计算服务种类繁多,但最佳类型的云计算服务被定义为以下几种(其他类型仅基于这些服务类型):
基础设施即服务(IaaS):这提供基础设施,即存储、虚拟机等。更多信息,请访问
azure.microsoft.com/en-in/overview/what-is-iaas/。平台即服务(PaaS):这提供了一种按需环境,用于开发、测试或管理应用程序等活动。更多信息,请访问
azure.microsoft.com/en-in/overview/what-is-paas/。软件即服务(SaaS):这提供按需的软件应用程序。云计算提供商可能提供各种订阅模式,你可以在这些模式下订阅特定的软件应用程序。更多信息,请访问
azure.microsoft.com/en-in/overview/what-is-saas/。
讨论 Azure 环境
Azure 环境提供了一种通过互联网获取其各种服务的方式。以下截图代表所有云计算服务模型的典型概述:

它显示了 IaaS 作为一个非常基础的模型,提供服务器和存储,以及 SaaS 作为一个高级模型,几乎提供所有云计算服务。
从 Azure 开始
要开始使用 Azure,您需要访问 Azure 门户。按照以下步骤操作:
- 使用此链接登录 Azure 门户:
portal.azure.com。
如果您没有 Azure 账户,可以在此免费创建一个:azure.microsoft.com/en-in/free/。
- 登录后,您将看到如下截图所示的仪表板:

Azure 门户仪表板
门户仪表板可能与您之前看到的截图不同。如果您是首次登录,您可能需要创建资源(根据您的需求)。这是一个您可以部署虚拟机的地方(请参阅 IaaS),选择特定的环境,例如 Windows 机器或 Linux(请参阅 PaaS),或者您可以部署您的应用程序(请参阅 SaaS)。
- 现在,您可以根据您的订阅进行任何您想做的事情。
发布/托管
发布/托管是一个使您的应用程序公开可用的服务。应用程序可以存储在托管提供商提供的服务器上。在本节中,我们将使用 TFS(现在为 VSTS):请参阅 https://www.visualstudio.com/tfs/。
如果您的现有项目托管在 TFS 上,您需要将其迁移。有关更多详细信息,请参阅此链接:www.visualstudio.com/team-services/migrate-tfs-vsts/。
项目托管
您需要访问 Visual Studio Online/TFS Online(现在为 VSTS)以托管项目。为此,您需要按照以下步骤操作:
使用您首选的浏览器访问
www.visualstudio.com/vso/。点击登录,如下所示:

VSTS 主页
输入您的 Microsoft 账户;如果您没有,可以创建一个。
按照步骤操作并创建您的账户。
您将被重定向到您的 Visual Studio Team Services 账户页面:

VSTS 我的个人资料页面
点击创建新项目。
您将被重定向到一个新页面,您将需要提供一些与您的项目相关的信息。
添加您的项目信息,如下截图所示:

创建新项目
- 选择您的版本控制 – 您将可以选择 Git 或 团队基础版版本控制(TFVC)。
如果您对选项感到困惑,请参考此链接以比较 Git 和 TFVC:docs.microsoft.com/en-us/vsts/tfvc/comparison-git-tfvc?view=vsts。
在我们的情况下,选择团队基础版版本控制:

- 现在,选择工作项流程,参考
docs.microsoft.com/en-us/vsts/work/work-items/guidance/choose-process?view=vsts了解可用的各种选项。在我们的情况下,选择 Scrum:

现在点击创建。
您将被重定向到一个新创建的项目页面,其外观如下所示截图:

FlixOneProductServices 项目主屏幕
- 项目主屏幕是一个快速显示页面,您可以快速查看所有活动。
您的项目已创建,现在您已准备好开始您的项目。
仪表板
仪表板是一个包含您项目活动快照的屏幕。它告诉您分配给您的任务,显示冲刺燃尽图,项目进度,或您为仪表板配置的任何内容:

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

FlixOneProductServices 项目的代码屏幕
您可以查看以下内容:
文件:存储库中的所有文件。
变更集:带有变更集编号的代码更改,您还可以获取有关哪些变更集推送到哪些任务/错误的详细信息。
货架集:任何可用于审查或与当前项目相关的任何其他目的的货架更改。
拉取请求:这是一种协作工作的方式。您可以在任何时候发起拉取请求,点击代码并选择新建拉取请求,项目所有者或维护者将收到此拉取请求的通知。
工作
默认情况下,它显示工作项屏幕,显示分配给您的项目或您正在处理或已处理的任务/错误。我们已创建了一个新项目,因此您将获得一个空白页面;要开始工作项,您需要创建一些待办事项,然后在团队内分配它们。
点击待办事项,然后从屏幕上出现的新模板中为您的产品待办事项添加标题。参见以下截图:

新产品待办事项
您可以在前面的截图中看到,默认情况下,您被分配了六个冲刺。现在,打开一个新创建的产品待办事项,并提供完整的描述——例如这项工作的努力程度、谁将处理这个项目,或谁将是这个工作项的所有者。参见以下截图,它显示了所有内容:

产品待办事项
现在,转到 Sprint 1 并设置日期——您应该设置日期以开始当前迭代,如下截图所示:

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

看板待办事项
将代码添加到仓库
我们没有向我们的仓库添加任何代码;现在是时候做这件事了。要添加代码,您可以从 Web 界面直接点击“代码”并上传文件,或者从 Visual Studio IDE 的源代码控制中添加仓库后提交代码。在本节中,我们将使用 Visual Studio 添加我们的代码。要这样做,请按照以下步骤操作:
打开 Visual Studio
创建或打开您现有的项目
打开团队资源管理器
点击“连接 TFS 服务器”
如果找不到服务器,请添加 TFS 服务器,然后提供其有效地址
点击“连接”
以下截图显示了与 FlixOneProductServices 的连接:

连接 TFS 服务器
- 您需要将 TFS 仓库映射到您的本地驱动器。映射源并获取代码:

映射和获取源代码
- 现在点击“源代码控制资源管理器”,源代码控制资源管理器选项卡将打开。您将看到空的项目节点,如下截图所示:

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

将解决方案添加到源代码控制
- 进行选择并选择它:

将解决方案添加到 FlixOneProductServices
- 现在,您可以看到已添加到源代码控制的新文件夹和文件——如下截图所示:

新增项目
前往“团队资源管理器”,然后单击“挂起的更改”。您将找到各种已签出的文件。
添加工作项并添加注释,然后点击“提交”:

提交挂起的更改
您已成功将解决方案添加到源代码控制。
现在返回到您的 TFS Online,然后点击“代码”。您将找到最近添加到源代码控制的所有代码文件/文件夹。参考以下截图:

查看代码
您已成功将项目托管到 VSTS。在以下部分,我们将讨论构建和部署。
测试
VSTS 的这个屏幕可以帮助您创建各种测试计划,以便您可以跟踪冲刺的手动测试。它有助于监控当前冲刺的手动测试何时完成。我们在第六章测试 RESTful Web 服务中讨论了测试的各个术语。
在以下部分,我们将看到通过创建测试计划和测试用例,这是如何帮助我们测试冲刺的。
创建测试计划
从“测试计划”选项卡,点击+,然后点击“测试计划”,如图所示以下截图:

创建测试计划
在下一屏中,为您的测试计划命名,确保您已为测试计划选择了正确的冲刺。请参考以下截图:

命名测试计划
由于测试计划是针对手动测试的,我们现在必须添加需要测试的待办事项。在您的案例中,我们只是添加了冲刺 1 的待办事项。在这个过程中,我们已经为冲刺 1 的所有待办事项添加了测试套件。
创建测试用例
在上一节中,我们已经创建了迭代 1 测试计划并为其添加了一个测试套件。现在,我们需要创建一个测试用例,点击“新建”并选择“新建测试用例”:

创建新的测试用例
通过添加有效的名称和测试用例中的步骤以及预期输出来完成您的测试用例。请参考以下截图:

编写测试用例
您现在可以将测试人员分配到这些测试用例或测试套件,以便测试人员可以运行这些测试。
运行手动测试
在前面的章节中,我们已经创建了手动运行的测试用例。点击测试套件,然后点击“运行”以运行可用的测试。请参考以下截图:

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

验证手动测试结果
由于这些测试是手动的,您需要手动进行测试。执行这些测试后,您必须验证预期的输出,并根据结果将测试标记为通过或失败。测试用例的整体结果将在测试套件窗口中显示。
在本节中,您已创建了一个测试计划、当前迭代的测试套件以及用于测试特定迭代、代码更改、构建或发布的手动测试用例。
Wiki
Wiki 页面帮助团队成员共同工作。这些页面可以包括项目文档或有关在项目上工作的说明,例如编码指南等。最初,您将通过点击“创建 Wiki”按钮获得一个空白模板,如图所示以下截图:

创建 Wiki
您可以从“创建 Wiki 页面模板”中添加任意数量的页面。Wiki 页面支持 Markdown:docs.microsoft.com/en-us/vsts/collaborate/markdown-guidance?view=vsts。
“构建和发布”选项卡
“构建和发布”选项卡提供了为项目创建构建和发布的设施。在本节中,我们将讨论使用 VSTS 的持续集成和持续部署。最初,可能没有任何构建定义。
CI 与 CD 的比较
我们已经在前面的部分讨论了这两种方法。在本节中,我们将简要讨论 CI 和 CD 之间的区别。
持续集成是一种实践,其中所有团队成员(开发者)将他们的代码集成在一起。这发生在每次提交时,无论开发者何时推送更改,或者代码配置时,持续集成都会触发。这种做法最重要的优点是,它可以在您的开发周期中节省时间,因为它可以识别冲突(如果有的话)。这始于设置自动化测试的初始步骤。一旦有人将更改推送到存储库,就会触发构建和测试。
持续部署解决了在生产环境中部署代码时的部署问题。
使用 TFS Online 进行 CI 和 CD
在本节中,我们将讨论我们项目的持续集成。转到“构建”选项卡,然后选择“构建定义”,点击+“新建”:

创建新的定义
在下一步中,系统将要求您选择您的存储库。选择“存储库源控制”,映射分支,然后点击“继续”:

选择存储库源
在下一屏幕上,选择您的模板;在我们的例子中,选择 ASP.NET Core:

选择 ASP.NET Core 模板
按照模板说明提供所需值,如下截图所示:

创建构建任务
以下是与 VSTS 相关的基本但重要的概念;了解这些概念对于成功配置构建步骤也很重要:
任务:这些是构建步骤,指示 VSTS 执行特定任务
变量:这些是构建配置值,它们告诉构建代理有关系统或自定义值
触发器:这些基于您是否启用了持续集成来启用各种任务
选项:这些是构建属性,您可以提供构建描述、构建作业超时等。
保留:保留策略可以按需构建;典型的策略是您希望保留良好或不良构建多长时间。
为了使我们的例子简单,我选择了更改集 #5 以保存构建定义:

保存构建定义和队列
现在,你可以在“构建和发布”选项卡下看到构建结果——你的构建可能没有运行(请重新检查所有步骤);以下截图显示了已验证的步骤:

构建步骤
启动 CD 发布流程
你已经设置了 CI 流程,现在是时候进行 CD 了。转到“发布”选项卡,然后点击“新建定义”按钮:

添加发布定义
根据你的应用程序选择一个模板。参考以下截图:

选择 Azure App Service 部署
通过选择你的仓库或构建来将工件添加到发布定义中,参考以下截图:

添加工件
为你的部署环境设置值,你将使用什么 Azure 服务或应用类型,等等。参考以下截图:

添加任务值
前往“发布”页面,你可以查看你发布的状态;我们只添加了一个发布定义,所以你会看到如下截图:

发布部署状态
我们尚未部署我们的发布,所以其状态为“未部署”。你可以手动触发部署。这个发布是为了开发环境——你可以设置你想要的任何数量的环境。在本节中,我们介绍了如何使用 VSTS 启动 CI 和 CD。
摘要
部署术语使团队在其工作中保持一致,即使团队在不同的地理区域工作。借助 CI/CD,当项目中的任何团队在提交后立即收到更改时,CI/CD 团队保持同步。
在下一章中,我们将讨论开发日常活动中的一种测试范式。我们将讨论与测试范式相关的重要术语,包括这些术语的理论,然后我们将讨论具有对存根、模拟和了解集成、安全性和性能测试的了解的代码示例。
第八章:保护 RESTful Web 服务
在 Web 应用程序的世界中,在 HTTP 上存在大量的请求和响应交换,安全是其中一个最重要的横切关注点。任何不安全的服务或 Web 应用程序都可能面临数据篡改问题。
“当数据被未经授权的渠道修改(销毁、篡改或编辑)时,通常被称为数据篡改。”
数据在传输或存储在其他地方时可能会被篡改。数据被篡改可能有几个原因——在行业中,未受保护的数据是最常见的原因。为了防止这些问题,您可以保护您的环境和应用程序系统。通常,防火墙是保护环境(服务器)的最佳方式。您可以通过实施授权机制来保护应用程序。
不幸的是,知名网站的数据泄露事件如今司空见惯。考虑到这一点,信息和应用程序安全对 Web 应用程序变得至关重要。因此,安全的应用程序不应再是事后考虑的事情。在组织中,安全是每个人的责任。
在本章中,我们将主要关注安全和 REST 以及 OWASP 安全标准。到本章结束时,您将了解认证、单点登录(SSO)、基于令牌的认证以及使用代理服务器(如 Azure API Management)进行认证的概念。我们将涵盖以下主题:
OWASP 网络安全标准
保护 RESTful web 服务
认证和授权
验证
数据加密和存储敏感数据
OWASP 安全标准
开放网络应用安全项目(OWASP)是一个在线社区,主要通过创建各种研究和标准来解决 Web 应用程序安全问题。在本章中,我们将遵循 2017 年发布的 OWASP 安全标准(www.owasp.org/index.php/Top_10-2017_Top_10):

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

在前面的图表中,你可以考虑一个典型的使用 Auth 服务作为中间件服务器的 ASP.NET Core API 系统。可能有多个客户端或消费者正在使用这些服务并请求访问数据。在这里,Auth 服务在验证来自客户端的传入请求方面发挥着重要作用。如果 Auth 服务识别请求为已验证,则生成一个令牌并将其发送到 API 服务器以进行进一步处理。如果请求未经验证,则 Auth 服务会通知客户端身份验证失败。前面的图像只是一个典型场景的概述。实际场景可能更复杂,可能使用一个或多个中间件后端服务器(典型的 API 管理服务器)。
在以下章节中,你将更好地了解以下两个重要的安全概念:
身份验证:身份验证不过是一个通过某种凭证(通常是一个用户 ID 和密码)来验证或识别传入请求的过程。如果系统发现提供的凭证错误,则它会通知用户(通常通过 GUI 屏幕上的消息),并终止授权过程。
授权:授权总是在身份验证之后进行。这是一个允许已验证用户在验证他们有权访问特定资源或数据后访问资源或数据的过程。
从这里,你可以得出结论,RESTful 服务的安全性是应用程序最重要的功能。
你如何在 RESTful Web 服务中维护会话?
RESTful Web 服务使用 HTTP 协议,这是一个无状态协议(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)注入到网页中。根据官方网页(docs.microsoft.com/en-us/aspnet/core/security/cross-site-scripting):
"跨站脚本(XSS)是一种安全漏洞,它允许攻击者将客户端脚本(通常是 JavaScript)放入网页中。"
在这里,我使用了一个消费 Web 服务的 Web 客户端的例子。你将在第十章[9fcac4d2-710a-48a2-98be-ed0034525cee.xhtml],*构建 Web 客户端(消费 Web 服务)*中了解更多关于 Web 客户端的内容。
以下截图显示了一个攻击中创建屏幕的场景:

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

前面的截图显示了它如何被发送到服务器,并最终保存在数据库或任何持久存储库中。
以下截图显示了受影响的页面:

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

在前面的图中,你可以看到一个典型的 SQL 注入场景,攻击者注入了一个or子句来获取特定表的全部数据。实际的代码指令是根据员工表中的EmpId返回单个记录。但由于注入了额外的短语,它返回了员工表的完整记录。这是不安全系统最大的问题。在这里,攻击者将一个简单的子句注入到语句中。
这里在做什么?
在前面的部分,你看到了一个假设的场景,并进行了 SQL 注入的实际操作。现在,让我们通过创建一个使用 ASP.NET Core 2.0 的 RESTful 产品 API 来查看一个实际示例。在你开始构建这个应用程序之前,请记住以下应用程序的先决条件:
Visual Studio 2017 更新 3 或更高版本
ASP.NET Core 2.0 或更高版本
C#7.0 或更高版本
Microsoft Entity Framework Core 2.0.0
按照以下步骤创建我们的应用程序:
打开 Visual Studio。
选择文件 | 新建 | 项目或按Ctrl + Shift + F5。
选择 ASP.NET Core Web 应用程序。
从模板窗口中选择 ASP.NET Core API。确保你选择了.NET Core 2.0。
命名项目,选择解决方案的路径,然后单击确定。
添加
Models文件夹。在解决方案资源管理器中右键单击,从下拉菜单中选择添加新文件夹,并将其命名为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 包管理器界面下,搜索
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 仓库链接 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 键。在网页浏览器中,将
/swagger后缀添加到地址栏中的 URL,如下面的截图所示:

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

产品 API 的 Swagger 文档
我使用 Swagger 进行文档编写和测试 API 调用。您可以使用其他 API 测试客户端,例如 Advanced Rest Client 和 PostMan。
- 要测试我们的未加密代码,请点击
GET/api/product/{productid}资源,并传递产品 ID,如下面的截图所示:

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

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

在这里,我们可以看到我们的应用程序受到了 SQL 注入的影响。您看到了表中的所有记录。这是由于我们使用的原始 SQL 查询(有关更多信息,请参阅 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 分析器(docs.microsoft.com/en-us/sql/tools/sql-server-profiler/sql-server-profiler)来追踪查询。
解决 SQL 注入攻击
没有什么比不安全的代码更危险了。使用不安全的代码,应用程序始终处于危险之中。攻击者可以随时窃取数据,通过篡改请求强制性地操纵事物。
Saineshwar Bageri 写了 10 个创建安全 ASP.NET Web 应用程序的技巧。您可以在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。这是一种常见的攻击方式,攻击者在客户端与托管应用程序交互(请求/响应)时注入不受欢迎的操作。通常,攻击者使用恶意代码来影响交互。
恶意代码是下载到网页浏览器并执行的脚本代码,即使未经认证用户的知识,也会执行。有关详细信息,请参阅www.techopedia.com/definition/4013/malicious-active-content。
攻击者非常聪明,他们使用不同的平台提供虚假链接到恶意代码。这些链接与受攻击的域(网站)非常相似。金融网站是主要的目标。
下图描述了一个 XSRF 攻击:

攻击者可以通过电子邮件、社交媒体或其他任何媒介发送链接。当用户点击链接时,他们将会进入攻击者的世界,而不知道这是一个虚假的网站,而不是他们想要访问的网站。
你可以在docs.microsoft.com/en-us/aspnet/core/security/anti-request-forgery找到官方网页。
CSRF 漏洞本质上是 Web 应用程序的问题,而不是终端用户的问题。
为了处理这种攻击,你需要构建一个既安全又正确认证的系统。在接下来的章节中,我将详细介绍认证的细节。
实际操作中的身份验证和授权
到目前为止,你已经学习了身份验证和授权的基础知识。在本节中,你将看到这两种最重要的保护应用程序的方法在实际中的应用。
基本身份验证、基于令牌的授权以及其他授权方式
每当你谈论受保护的 Web 服务/应用程序时,你应该考虑我在前几节中提到的关于身份验证和授权的所有要点。
在本节中,我将讨论实施阶段的身份验证和授权。
基本身份验证
如“基本”一词所示,基本身份验证涉及一种机制,其中系统要求用户提供简单的凭证(用户名和密码),通过来自客户端到 Web 或应用程序服务器的请求(在我们的案例中,是 ASP.NET Core Web API)来验证或验证用户。
考虑以下图表,它展示了基本身份验证:

以下图表是我将要在我们的代码中实现的基本 HTTP 身份验证。在这里,请求来自客户端,以访问受保护的资源(从公共访问中保留的资源)。请求在其头部包含用户名和密码,服务通过验证其存储库(通常是数据库存储)中的用户名和密码来检查它是否是一个有效的请求。如果用户被验证,服务将在其响应中返回数据给客户端;否则,它将返回带有 HTTP 状态码 401 的无效凭证。
你可以在www.w3.org/Protocols/rfc2616/rfc2616-sec10.html找到完整的 HTTP 状态码列表及其定义。
基本身份验证的安全问题
基本身份验证,正如其名所示,是一种非常基本的身份验证机制,在阻止攻击者方面并不太安全。在这里,我已经记录了以下身份验证过程中的安全漏洞:
凭证:所需的凭证是可能导致安全漏洞的最重要安全问题,这反过来又可能进一步利用系统薄弱的安全。
请求:请求可能会被篡改,可能导致重大安全漏洞;使用基本身份验证时,每个请求都携带凭证(用户名和密码),这些凭证可能会被篡改并用于进一步利用系统。
关闭浏览器会话:这是一个应该优先考虑的问题——使用基本身份验证方法无法从应用程序中注销,除非用户自己关闭浏览器以终止浏览器会话。
您可以查看官方网页docs.microsoft.com/en-us/aspnet/web-api/overview/security/basic-authentication以获取更多信息。
“基本身份验证也容易受到 CSRF 攻击。用户输入凭证后,浏览器会自动在会话期间将它们发送到同一域的后续请求中。”
可能还有更多安全问题,使得基本身份验证机制在 Web 应用安全方面变得最弱。
基本身份验证导致各种安全问题。我无意使用基本身份验证的代码示例来展示这些问题,但如果您仍然想测试基本身份验证机制,我建议您从 GitHub 仓库github.com/garora/Bazinga.AspNetCore.Authentication.Basic中提取代码。
基于令牌的授权
在本章前面的部分,我解释了授权,您可以看到授权是认证之后访问受限资源的下一步。
让我们考虑以下图示,它描述了基于令牌的认证:

上述图示展示了基于令牌的认证。如果请求被验证(取决于凭证的识别),则客户端发送带有凭证和返回令牌的请求。然后客户端存储此令牌。它随后将这些令牌与每个请求的头部一起发送,直到令牌有效。如果它被授权访问受保护资源,服务器将验证请求检查并返回数据。在某些情况下,客户端可能会请求新的令牌或调用刷新令牌,如果现有令牌过期。
让我们在上一节创建的 API 项目中添加一个AuthRequet模型:
public class AuthRequest
{
public string UserName { get; set; }
public string Password { get; set; }
}
在Controller文件夹中添加一个新的GenerateTokenController.cs控制器。以下是我们的GetToken POST 资源:
[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 仓库github.com/PacktPublishing/Building-RESTful-Web-Services-with-DotNET-Core中提取整个源代码。
要启用基于令牌的认证,您需要在 startup.cs 文件中进行一些更改,在 Configure 方法中 app.UseMvc(); 之前插入 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 是最著名的认证服务器之一,它使认证成为一种服务,单点登录/注销,以及更多选项(更多信息请参阅identityserver4.readthedocs.io/en/release/)。
使用注解保护服务
ASP.NET Core 提供了多种方式来创建安全的应用程序注解(数据注解)。这是我们为 Web 应用程序安全模型的一种方式。数据注解提供了一种在客户端或服务器端验证输入的方法。
验证
从名称 validations 可以直观地看出,它们不过是用户/客户端输入的验证器。用户输入可以在客户端或 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 (getpostman.com/)来测试 API。
下面的截图显示了addproduct POST资源;使用/api/product/addproduct API 保存产品:

使用 Swagger 进行 POST 请求
前面的输入是有效的;您已提供了所有必填的值。让我们移除Price和ProductName。您会发现,即使在未提供必填值的情况下执行请求,也不会有任何变化。在这里,验证失败了。这些数据注解没有影响处理过程的原因是您没有指导系统验证输入。要验证输入,您应该明确告诉系统您想要验证的内容,例如模型状态。我们将通过下一节中的过滤器来解决这个问题。
保护上下文
通过使用filter属性,您可以在上下文级别实现安全性。在本节中,我们将重写我们的模型和 API 资源以实现过滤器/属性。
在上一节中,我们使用了Required属性与我们的Product模型一起,但这并没有为我们解决问题。在本节中,我们将借助过滤器(有关过滤器的更多信息,请访问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));
}
运行应用程序并输入新的产品值,但不包括产品名称,如下面的截图所示:

处理请求并查看服务器返回的响应,如前面的截图所示。请求将不会被处理,服务器将返回一个通知您有错误请求的响应,并包含相关错误信息。
以下截图显示了错误的请求响应(HttpStatus Code 400):

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

在前面的图中,您可以看到负载均衡器在其典型环境中,系统通过互联网从不同来源接受多个请求,然后由负载均衡器从多个服务器进行管理。
在.NET 中,这种配置也被称为 Web 农场 (www.codeproject.com/Articles/114910/What-is-the-difference-between-Web-Farm-and-Web-Ga)。
负载均衡器使用各种算法,也称为负载均衡方法:最少连接方法、轮询方法、最少响应时间方法、最少带宽方法、最少数据包方法、自定义负载方法等。
负载均衡器在应用程序的可扩展性中扮演着重要角色,因为它确保应用程序的服务器能够处理服务器请求。请注意,您可能需要在代码不变的情况下安排您的硬件基础设施以适应负载均衡器(尽管,有些情况下可能需要代码更改)。市场上有很多负载均衡器,例如 Incapsula (www.incapsula.com/)、F5 (www.f5.com/)、Citrix Netscaler (www.citrix.com/)、Dyn (dyn.com/))、Amazon Elastic Load Balancing 和 Amazon ELB (aws.amazon.com/)。
在接下来的部分中,我们将探讨您可以扩展系统的不同方法。
可扩展性简介
每个应用程序都有其自己的服务请求能力。应用程序的能力指的是其性能以及当负载增加时如何满足其目标。
许多 Web 应用程序将此称为在规定时间内的请求数量。
在设计您的 Web 应用程序时做出正确的设计决策非常重要;设计决策会影响您服务的可扩展性。确保找到正确的平衡点,以便您的方案不仅考虑服务,还要考虑其基础设施,以及任何扩展需求。
性能和可扩展性是系统的两个不同特性。性能涉及系统的吞吐量,而可扩展性涉及为更多用户或更多交易处理所需吞吐量。
向内扩展(垂直扩展)
向内扩展或向上扩展(也称为垂直扩展)是通过向同一机器添加更多资源(如内存或更快的处理器)来实现可扩展性的方法。这并不总是适用于所有应用程序,因为成本也是考虑垂直扩展时的一个因素。
您也可以升级资源或硬件,而不是向机器添加新资源。例如,如果您有 8 GB 的 RAM,您可以将其升级到 16 GB,同样的情况也适用于处理器和其他资源。不幸的是,随着硬件的升级,机器的可扩展性有一定的限制。这可能会导致仅仅是将瓶颈转移,而不是解决提高可扩展性的真正问题。
您也可以将应用程序迁移到完全不同的机器上,例如简单地迁移到更强大的 MacOS,例如。
垂直扩展不涉及任何代码更改,因此这是一个简单的任务,但它涉及额外的成本,因为这是一种相当昂贵的技巧。Stack Overflow 是那些罕见的基于.NET 的系统之一,它进行了垂直扩展。
扩展(横向扩展)
扩展(向上扩展)、扩展(向外扩展)或横向扩展是通过添加更多服务器或节点来服务请求,而不是资源。如果您不想扩展应用程序,总有一种方法可以将其扩展。
当应用程序代码不依赖于其运行的服务器时,横向扩展是一种成功的策略。然而,如果需要在一个特定的服务器上执行请求,即如果应用程序代码具有服务器亲和性,那么横向扩展将会很困难。在无状态代码的情况下,在任何服务器上执行都更容易。因此,当无状态代码在横向扩展的机器或集群上运行时,可扩展性得到了提高。由于横向扩展的性质,这在整个行业中是一种常用的方法。有许多大型可扩展系统以这种方式管理,例如 Google、Amazon 和 Microsoft。
线性可扩展性
线性可扩展性指的是应用阿姆达尔定律(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 就派上用场。这是一个轻量级库,目前支持 ASP.NET Web API。CacheCow 是开源的,并附带 MIT 许可证,可在 GitHub 上找到(github.com/aliostad/CacheCow)。
要开始使用 CacheCow,你需要通过以下步骤为服务器和客户端做好准备:
在你的 ASP.NET Web API 项目中安装
Install-Package CacheCow.ServerNuGet 包;这将是你服务器。在你的客户端项目中安装
Install-Package CacheCow.ClientNuGet 包;客户端应用将是 WPF、Windows Form、控制台或任何其他 Web 应用。创建一个缓存存储。你需要创建一个
存储
在服务器端存储缓存元数据的缓存存储(
github.com/aliostad/CacheCow/wiki/Getting-started#cache-store)。
Memcached
Memcached 是一个可定制的开源项目;你可以使用源代码,并根据你的需求对其进行添加和更新。Memcached 由其官方页面([memcached.org/](https://memcached.org/))定义为:
"一个用于存储来自数据库调用、API 调用或页面渲染结果的任意数据小块(字符串、对象)的内存中键值存储。"
参考以下链接获取完整教程https://www.deanhume.com/memcached-for-c-a-walkthrough/。
Azure Redis Cache
Azure Redis Cache 建立在名为 Redis 的开源存储之上(github.com/antirez/redis),这是一个内存数据库,数据持久化在磁盘上。根据 Microsoft 的描述(azure.microsoft.com/en-in/services/cache/):
“Azure Redis Cache 基于流行的开源 Redis 缓存。它为您提供了访问由 Microsoft 管理的安全、专用的 Redis 缓存,并且可以从 Azure 中的任何应用程序访问。”
如果您按照以下步骤操作,开始使用 Azure Redis Cache 非常简单:
创建一个 Web API 项目。参考我们之前章节中的代码示例。
实现 Redis。参考点为
github.com/StackExchange/StackExchange.Redis。同时,安装Install-Package StackExchange.RedisNuGet 包。更新您的 CacheConnection 配置文件(
docs.microsoft.com/en-us/azure/redis-cache/cache-dotnet-how-to-use-azure-redis-cache#NuGet)。然后,在 Azure 上发布(
docs.microsoft.com/en-us/azure/redis-cache/cache-web-app-howto#publish-and-run-in-azure)。
通信(异步)
术语“通信”是自解释的;它是服务之间交互的行为。以下是一些例子:
在同一应用程序内部与另一个服务通信的服务
与应用程序外部(外部服务)的其他服务通信的服务
一个与组件(内部或外部)通信的服务
这种通信通过 HTTP 协议进行,消息或数据通过网络传输。
您应用程序的性能会影响服务之间如何通信。
异步通信是帮助扩展应用程序的方法之一。在 ASP.NET Core 中,我们可以通过使用异步 HTTP 调用(异步编程)来实现这一点:docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/
在处理异步通信时,你应该小心操作,例如,在添加带有图片的新产品时。系统被设计成可以创建不同尺寸的图片缩略图。这是一个耗时任务,如果处理不当可能会导致性能下降。从设计角度来看,异步操作在这种情况下是不可行的。在这里,你应该实现一个带有回调的任务,告诉系统何时完成工作。有时,你可能还需要中间件来处理请求。
实现异步通信的最佳方式是使用异步 RESTful API。
在创建可扩展的系统时,你必须始终考虑异步通信。
摘要
在本章中,我们讨论了可扩展性,包括帮助实现它的库、工具等等。然后我们讨论了如何扩展 RESTful 服务,设计它们时需要考虑什么,以及如何使用不同的模式避免级联故障。
在接下来的章节中,我们将讨论并构建一个网络客户端来调用和消费 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、控制台、移动等,都是通过 REST 客户端帮助消费这些服务的客户端。
我们现在将讨论如何构建一个 REST 客户端,我们可以使用它来消费我们应用程序中的其他 RESTful Web 服务(即 API)。
构建 REST Web 客户端
RESTful 服务可能是也可能不是 Web 应用程序的一部分。Web 应用程序可以调用或消费来自同一应用程序的外部 API 或服务。使服务与应用程序消费这些服务的交互或通信(请求、响应)成为可能的程序称为客户端。
客户端帮助应用程序通过 API 进行(请求、响应)通信。
在本节中,我们将创建一个 Web 客户端。Web 客户端是用 ASP.NET Core 编写的应用程序或程序。
在我们构建测试 Web 客户端之前,我们需要讨论我们需要调用什么。
继续我们的 FlixOne BookStore 示例,以下表格列出了我们将调用和消费的生产者和服务:
| API 资源 | 描述 |
|---|---|
GET /api/product |
获取产品列表。 |
GET /api/product{id} |
获取一个产品。 |
PUT /api/product{id} |
更新现有产品。 |
DELETE /api/product{id} |
删除现有产品。 |
POST /api/product |
添加新产品。 |
我们的 FlixOne 产品服务是为以下任务设计的:
添加新产品
更新现有产品
删除现有产品
获取产品
我们已经确保了我们的产品 API 支持 Swagger(请参阅前面的章节以获取更多信息),让我们开始吧。要开始此项目,请按照以下步骤操作:
首先,运行 Visual Studio 2017
选择“文件 | 打开”
选择项目 FlixOne.BookStore.ProductService
按下 F5 键或直接从菜单中点击来运行项目
输入以下 URL:
http://localhost:10065/swagger/
你现在应该能看到你的产品 API 的 Swagger 文档,如下截图所示:

产品 API 文档
构建 Web 客户端
我们已经讨论了哪些 API 需要消费以及哪些资源返回什么,现在是时候构建我们的 Web 客户端,以便我们可以消费和调用我们的产品 API。为此,请按照以下步骤操作:
- 要为我们的新项目创建一个全新的解决方案,请转到“文件 | 新建 | 项目”(或按 Ctrl + Shift + N),如下截图所示。

从“新建项目”中选择 ASP.NET Core Web 应用程序。
命名项目
FlixOne.BookStore.WebClient并然后点击如下截图所示的“确定”:

- 从如下截图所示的 ASP.NET Core 模板窗口中选择 Web Application 并点击“确定”:

现在运行项目 Debug | 开始调试或按 F5 键。
你现在应该看到一个默认的网站模板。
我们现在将使用RestSharp创建一个 Web 客户端。我们需要添加对 RestSharp 的支持,以便能够对我们的 API 资源进行 HTTP 协议调用。
RestSharp 是一个轻量级的 HTTP 客户端库。你可以根据需要对其进行更改,因为它是一个开源库。你可以在github.com/restsharp/RestSharp找到完整的仓库。
- 使用如下截图所示的“打开包管理器”(在解决方案资源管理器中右键单击解决方案),添加一个 NuGet 包:

- 搜索“RestSharp”,勾选包含预发布版本的复选框,然后点击“安装”,如下截图所示:

选择 RestSharp NuGet 包
- 现在将安装所需的包,如下截图所示:

安装 RestSharp 包
在继续前进之前,让我们首先确保我们的产品 API 能够正确工作。运行产品 API 项目,打开 Swagger,然后按照以下方式点击GET /api/product/productlist资源:

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

尝试所有可用的资源以确保你的产品 API 能够正确工作。
编写代码
到目前为止,我们已经为编写 REST Web 客户端的代码准备好了所需的东西;在本节中,我们将编写实际的代码:
- 添加简单的代码来调用或消费你的产品 API。
如果你在同一解决方案中创建了一个新项目(参考Cooking the web client部分的步骤 1),请在开始你的 web 客户端项目之前确保 Product API 项目正在运行。
- 在
Client文件夹中添加一个新的类(Ctrl + Shift + C)并命名为RestSharpWebClient。

- 现在打开
RestSharpWebClient类并添加以下代码:
private readonly RestClient _client = new RestClient("http://localhost:10065/api/");
前面的代码初始化 RestSharp 的 RestClient 并接受一个字符串或 URI 作为基础 URL。
URI 代表统一资源标识符,它是一个用于标识资源的字符串表示。
你可能会遇到存在多个环境的情况;在这种情况下,你应该存储一个 URI,根据你的环境将其指向相应的位置。例如,你可以为你的开发环境使用 URI http://devserver:10065/api/,或者为你的 QA 环境使用 URI http://testenv:10068/api/。你应该将这些键存储在config文件或类似的位置,以便值易于访问。我们建议使用new RestClient(somevariableforURI);。
在我们的应用程序中,产品 API 在本地主机上运行,监听端口为10065。在你的情况下可能会有所不同。
让我们讨论以下代码片段,以调用或消费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 客户端
RESTful 服务可能是也可能不是你 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();
}
}
你也可以编写一个接受ProductViewModel类型参数的HttpPost方法。
以下代码片段显示了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();
}
}
Delete操作方法旨在从数据库或集合中删除特定的记录或数据。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——即创建、编辑和删除。
摘要
创建 RESTful 服务对任何项目都很重要,但如果无法使用这些服务,这些服务就毫无用处。在本章中,我们探讨了如何将 RestSharp 支持添加到我们的 Web 项目中,并消费我们预先开发的产品 API。我们还创建了一个可以通过在 ASP.NET Core 网页上渲染输出来消费 Web 服务的 Web 客户端。
在下一章中,我们将讨论微服务这一热门话题,这是服务分离的下一级。我们将讨论微服务如何通信,它们的优点是什么,以及为什么我们需要它们。
第十一章:微服务简介
到目前为止,我们已经通过实际操作示例学习了 RESTful API,并创建了小型应用程序。在上一章中,我们开发了一个应用程序,并讨论了 RESTful API、安全、测试、性能和部署。
本章简要介绍了微服务,这是我们在 RESTful 服务旅程中的下一站。在本章中,我们将介绍微服务的基本组件,并使用一个正在转换为微服务的单体应用程序的示例。
我们将涵盖以下主题:
什么是微服务?
微服务中的通信
微服务测试策略
可伸缩性
ASP.NET Core 中的微服务生态系统
微服务概述
简而言之,当将应用程序或模块划分为更小、独立的独立服务时,结果也被称为微服务。这些小部分也可以独立部署。
如果我们回顾历史,我们会发现微服务这个术语最早在 2011 年的软件架构师研讨会上被使用。2012 年 3 月,詹姆斯·刘易斯(James Lewis)提出了一些关于微服务术语的想法。到 2013 年底,IT 行业的各个群体开始讨论微服务,到 2014 年,微服务已经足够流行,被认为是大型企业的一个严肃的竞争者。
那么,微服务到底是什么呢?有各种各样的定义,因此您可以按照自己对术语的理解或可能遇到的使用案例和讨论来定义微服务。让我们看看一个官方网站上的微服务定义:(来源:docs.microsoft.com/en-us/azure/service-fabric/service-fabric-overview-microservices)
"微服务应用程序由小型、独立版本、可伸缩且以客户为中心的服务组成,这些服务通过标准协议和定义良好的接口相互通信。"
微服务属性
在上一节中,我们了解到微服务在系统中与其他服务完全独立,并在它们自己的进程中运行。根据这个定义,有一些属性定义了微服务与其他组件完全独立。让我们首先看看核心属性是什么:
独立的功能性:不要试图在单个微服务中实现太多功能。相反,只为一个原因设计它,并且做好这一点。这意味着设计应该尽量避免对功能其他部分的任何依赖。在我看来,这部分非常重要,因为它为其他属性奠定了基础。
独立的数据和状态:每个服务拥有自己的数据和状态。它不与其他应用程序或部分共享所有权。
独立部署:前述点的累积效应。这有助于你进行持续部署。
技术采用:当前两点得到妥善处理时,这会更容易,因为不再对任何现有模块产生影响。这里的美丽之处在于,你可以在两种不同的技术中拥有两个不同版本的微服务。这非常有益。
一致性和弹性:这必须完美无缺。如果你不能依赖服务在预期的期限内返回,或者依赖它始终可用,那么它的整个目的就失去了。
理解微服务架构
我们已经讨论了微服务架构是如何通过包含一组较小服务的单个应用程序来开发的一种方式。这些服务是独立的,并且在自己的进程中运行。
换句话说,我们可以这样说,微服务是一种将我们的服务分离出来的方式,这样它们就可以在设计和开发、部署和升级方面独立于彼此处理。
微服务有很多好处,如下所示:
更小的代码库:每个服务都很小,因此更容易作为一个单元进行开发和部署。
独立环境的便利性:随着服务的分离,所有开发者都可以独立工作,独立部署,没有人会担心任何模块的依赖性。
微服务中的通信
在处理微服务架构时,仔细考虑消息机制的选择非常重要。如果忽略了这个方面,那么它可能会损害使用微服务架构设计的整个目的。
让我们继续前进,考虑我们同步和异步消息的选择,以及不同的消息格式。
同步消息
当系统期望从服务中获得及时响应,并且系统会等待直到收到服务的响应时,这被称为同步消息。REST 是微服务架构中最受欢迎的选择之一。它简单且支持 HTTP 请求-响应,因此不需要寻找替代方案。这也是大多数微服务实现使用 HTTP(基于 API 的风格)的原因之一。
异步消息
当系统不需要立即从服务中获取响应,并且可以在不阻塞调用的情况下继续处理时,这被称为异步消息。
消息格式
在过去的几年里,使用 MVC 和类似技术让我对 JSON 格式产生了依赖。你也可以考虑 XML。这两种格式在 HTTP 上使用 API 风格资源时都表现良好。如果你需要使用二进制消息格式,也是可用的。我们在这里不推荐任何格式,你可以选择任何你喜欢的消息格式。
为什么我们应该使用微服务
已经探索了大量的模式和架构,其中一些获得了流行,而另一些则在争夺互联网流量的战斗中败下阵来。每个解决方案都有其自身的优缺点,因此对于公司来说,快速响应诸如可扩展性、高性能和易于部署等基本需求变得越来越重要。任何被发现不具备成本效益的单一方面都可能对大型企业产生负面影响,从而在盈利和非盈利企业之间造成差异。以下图表突出了选择微服务架构的优势:

正是在这里,我们看到微服务成为企业系统架构师的救星。他们可以利用这种架构风格确保他们的设计没有问题。同时,考虑这样一个事实也很重要,即这个目标是以成本效益和及时的方式实现的。
微服务架构是如何工作的
在前面的章节中,我们讨论了微服务架构,并试图对这个术语进行更深入的探讨。现在,您可以看到微服务架构可能的工作方式;您可以根据自己的设计方法使用任何组合。以下是一些在微服务架构工作中需要记住的要点:
这是面向现代时代的编程,我们应该遵循所有 SOLID 原则。它是面向对象编程(OOP)。
这是向其他或外部组件公开功能的最有效方式,因此任何编程语言都可以使用这些功能,而无需遵循任何用户界面或服务(如 Web 服务、API、REST 服务等)。
整个系统协同工作,而不是相互连接和依赖。
每个组件都负责其自身的功能。
它将代码分离。分离的代码是可重用的。
微服务的优势
以下是一些微服务的优势:
您不必投资使整个应用程序可扩展。以购物车为例,我们可以简单地负载均衡产品搜索模块和订单处理模块,同时保留使用频率较低的运营服务,如库存管理、订单取消和交货确认。
我们可以轻松地匹配组织的部门层级。在大企业中,不同的部门赞助产品开发,这可以是一个巨大的优势。
由于代码已经以不依赖于其他具有独立功能模块的代码的方式进行编写,如果做得正确,那么一个微服务中的更改影响另一个微服务的可能性非常小。
由于整个应用程序更像是一组相互隔离的生态系统——如果需要,我们可以一次部署一个微服务。任何单个服务的故障都不必导致整个系统崩溃。
你可以在一夜之间将单个微服务或一大堆微服务迁移到不同的技术,而用户甚至可能都不知道。不用说,你需要维护这些服务合同。
虽然不言而喻,但在此处仍需提醒注意。确保你的异步调用被正确使用,而同步调用不会真正阻塞整个信息流。合理使用数据分区。我们稍后会详细讨论这一点,所以现在不必担心。
在竞争激烈的世界中,如果你对新的功能请求或系统内新技术的采用反应迟缓,用户很容易迅速失去兴趣,这无疑是一个明显的优势。
微服务架构的先决条件
在同意采用微服务架构之后,明智的做法是确保以下先决条件已经到位:
随着开发周期的缩短,需求变得更加紧迫。这要求你尽可能快地部署和测试。如果只是少量服务,那么这不成问题。然而,随着服务数量的增加,这可能会很快对现有的基础设施和实践构成挑战。例如——你的质量保证和预发布环境可能不再足以测试从开发团队返回的构建数量。
当应用程序进入公共领域时,很快就会再次上演开发与质量保证之间的古老剧本。这次的不同之处在于,业务处于风险之中。因此,你需要准备好以自动化的方式快速响应,在需要时识别根本原因。
随着微服务数量的增加,你很快就需要一种方法来监控整个系统的运行状况和健康状态,以发现任何可能的瓶颈或问题。如果没有监控已部署微服务和由此产生的业务功能状态的手段,任何团队都无法采取主动部署的方法。
可扩展性
可扩展性是任何企业在试图满足不断增长的用户基础时面临的最大挑战之一。
可扩展性简单来说就是系统/程序处理不断增长工作的能力。换句话说,可扩展性是系统/程序扩展的能力。
系统的可扩展性是指其处理不断增加/增加的工作负载的能力。我们可以采用两种主要策略或类型来扩展我们的应用程序。
垂直扩展
在垂直扩展中,我们分析现有应用程序,找出由于执行时间较长而使应用程序变慢的模块部分。使代码更高效可能是一种策略,这样可以减少内存消耗。这种减少内存消耗的练习可以是针对特定模块或整个应用程序。另一方面,由于这种策略涉及明显的挑战,我们可以在不改变应用程序的情况下,向现有的 IT 基础设施添加更多资源,例如升级 RAM、添加更多磁盘驱动器等。在垂直扩展的这两条路径中,都有其有益性的极限,因为经过一段时间后,产生的效益将趋于平稳。在这里,重要的是要记住这个事实;这种扩展需要停机时间。
水平扩展
在水平扩展中,我们深入挖掘对整体性能影响较大的模块。我们考虑诸如高并发等因素,以使我们的应用程序能够服务更多的用户基础。我们还会实施负载均衡以处理更多的任务。向集群添加更多服务器的选项不需要停机时间,这无疑是一个优势。它可能因情况而异,因此我们需要检查额外的电力、许可证和冷却成本是否值得。
DevOps 文化
在 DevOps 的帮助下,团队应该强调开发团队和另一个运营团队的协作。我们应该建立一个系统,让开发、Q/A 和基础设施团队协作工作。
自动化
基础设施设置可能是一项非常耗时的工作。在基础设施为开发者准备期间,开发者可能会闲置。在加入团队并贡献之前,他们需要等待一段时间。基础设施设置的过程不应该阻止开发者变得高效,因为这会降低整体生产力。这应该是一个自动化的过程。使用 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 Service Fabric – 微服务平台
对于任何生态系统来说,平台是一个必备的组件。它支持系统,运行顺畅,并产生预期的结果。Azure Service Fabric 是微软提供的一个平台,在微服务生态系统中非常受欢迎。它提供容器部署和编排。
官方文档可以在以下位置找到:docs.microsoft.com/en-us/azure/service-fabric/service-fabric-overview
"Azure Service Fabric 是一个分布式系统平台,它使得打包、部署和管理可扩展且可靠的微服务和容器变得容易。"
无状态和有状态服务 – 一种服务编程模型
一个健壮的服务编程模型是微服务生态系统的支柱。一个人应该知道根据他的需求应该使用哪种类型的服务模型:
无状态:服务在客户端请求之间不保留任何状态。也就是说,服务不知道,也不关心后续请求是否来自之前已经或未发起请求的客户端。当我们有外部数据存储时,这是最好的服务编程模型。我们的服务可以基于无状态服务编程模型,该模型与外部数据库存储交互并持久化数据。
有状态:服务维护一个可变的状态,积极处理或保留特定于服务任务的州数据。
通信 – 服务之间交换数据的方式
如果微服务都是关于服务的话,那么服务之间的通信应该是健壮的。通信是服务之间交换数据的方式。服务通过 Rest API(即 HTTP 请求/响应调用)进行通信,这些通信本质上是同步的。
当服务相互通信时,它们实际上是在交换数据,也称为服务间的消息传递。在处理微服务架构时,仔细考虑消息机制的选择非常重要。如果忽略了这个方面,那么它可能会损害使用微服务架构设计的整个目的。在单体应用程序中,这不是一个问题,因为组件的业务功能是通过函数调用调用的。另一方面,这是通过松散耦合的基于 SOAP 的 Web 服务级别消息传递发生的,其中服务主要基于 SOAP。微服务消息机制应该是简单和轻量的。
在微服务架构中,没有固定的规则来选择各种框架或协议。然而,这里有一些值得考虑的点。首先,它应该足够简单,以便在不增加系统复杂性的情况下实现。其次,它应该足够轻量,考虑到微服务架构可能会严重依赖服务间消息传递。让我们继续前进,考虑我们同步和异步消息传递的选择,以及不同的消息格式。
摘要
微服务架构风格提供了一些好处。它使开发变得快速且简单。它允许 DevOps(CI 和 CD)团队在地理上分离,工作顺利,同步。应用程序被划分为小的服务组件或部分,因此维护变得容易。这使得开发团队能够让业务赞助商首先选择响应哪些行业趋势。这导致了成本效益、更好的业务响应、及时的技术采用、有效的扩展和减少对人类的依赖。
在本章中,你已对典型的微服务架构风格和 ASP.NET 中的微服务生态系统有了了解。
现在,我建议你阅读以下关于微服务的文章,以提升你的技能:
使用 .NET Core 2.0 构建微服务 – 第二版 由 PACKT 出版 (
www.packtpub.com/application-development/building-microservices-net-core-20-second-edition)微服务模式和最佳实践 由 PACKT 出版 (
www.packtpub.com/application-development/microservice-patterns-and-best-practices)



浙公网安备 33010602011771号