精通-ASP-NET-Web-API-全-

精通 ASP.NET Web API(全)

原文:Mastering ASP.NET Web API

协议:CC BY-NC-SA 4.0

零、前言

编写 web API 是最受欢迎的编程技能之一,因为它提供了轻量级 HTTP 服务,可以覆盖广泛的客户机。设计良好的 web API 可用于各种客户端,如桌面、web 和移动应用;作为其 HTTP 服务,它可以跨平台使用。
ASP.NET Web API 2 是构建基于 REST 的 API 的理想平台,被广泛采用,并取得了很大成功。微软通过引入跨平台的.NET Core 和跨平台的 ASP.NET Core 技术,进入了开源世界。
ASP.NET Core 为开发 web 应用开辟了一个激动人心、功能丰富且轻量级的新天地。有了这项新技术,我们不再局限于 Windows 操作系统的世界来构建应用。它确实是跨平台的,因为我们不再需要使用 VisualStudioIDE 来开发应用。
ASP.NET Core 为构建 web API 提供了一种非常创新的方法。在本书中,您将了解 ASP.NET Core 剖析,通过探索中间件的概念创建 web API,与数据库集成,应用各种安全机制,并在流行的 web UI 框架中使用它们。
本书考虑了经验丰富的开发人员和新开发人员。有开发 web API 的经验将是一个额外的优势,但这不是一个先决条件。它将帮助您构建一个真正跨平台的 ASP.NET Core Web API 并掌握它。在编写本书时,我们正在使用.NET Core 2.0 Preview 2 和 ASP.NET Core 2.0 Preview 2,以及 Visual Studio 2017 Preview 3,我们确实计划在 ASP.NET Core 2.0 的最终版本中更新本书。

这本书涵盖的内容

第一章微服务和面向服务架构简介,讨论了行业中面向服务架构的发展趋势,以及微服务架构带来了什么。

第 2 章理解 HTTP 和 REST更新了 web 架构的概念,描述了 HTTP 背后的核心技术和概念及其方法,并向您介绍 REST 架构风格。

第 3 章ASP.NET Core Web API 剖析带您踏上了解 Web API 被接受的旅程,并让您开始创建 ASP.NET Core Web API 并了解其剖析。

第 4 章控制器、动作和模型涵盖了请求如何与控制器交互、如何与控制器调度流程协同工作、如何定制控制器调度流程以及如何与动作方法结果协同工作的核心概念。

第 5 章实现路由,帮助您了解路由如何将传入 HTTP 请求映射到其相应控制器的动作方法。

第 6 章中间件和过滤器深入探讨了 ASP.NET Core 的一个显著特征——中间件和过滤器。

第 7 章执行单元和集成测试介绍了如何为 web API 编写单元测试和执行集成测试。

第 8 章Web API 安全探讨了 Web API 的标识、身份验证和授权的概念。

第 9 章与数据库的集成,通过 EF 6、EF Core、Dapper 等 ORM 与各种数据库进行集成。

第 10 章错误处理、跟踪和日志记录探讨了 ASP.NET Core 内置的日志记录功能,并向您展示了如何编写高效的错误处理代码。

第 11 章优化与性能解释了编写 web API 的异步方式,以及如何应用缓存技术来提高 web API 的性能。

第 12 章托管部署在 IIS、单机版、Docker、Azure、Linux 等多种平台上部署 ASP.NET Core Web API。它展示了其真正的跨平台性质。

第 13 章现代 Web 前端在 UI 框架中使用从前面章节开发的 Web API,如 Angular、Ionic、React 等。

这本书你需要什么

完成本书中给出的练习练习需要以下软件:

  • Windows 7 或更高版本、任何 Linux 风格的计算机或 macOS
  • .NET Core 2.0 预览版 2 SDK
  • Visual Studio 2017 预览版 3(任何版本)
  • 非 Windows 计算机的 Visual Studio 代码
  • 用于 VisualStudio 代码的 OmniSharp
  • NodeJS 构建现代 UI 框架
  • SQLServerExpress 版
  • 码头工人工具箱
  • 邮递员:跨平台 REST 客户端
  • 您最喜欢的浏览器

这本书是给谁的

本书面向希望掌握 ASP.NET Core(Web API)的.NET 开发人员,他们对以前的 ASP.NET Web API 略知一二,但对它没有深入的了解。您需要了解 VisualStudio 和 C#,并具备一些 HTML、CSS 和 JavaScript 知识。

习俗

在本书中,您将发现许多文本样式可以区分不同类型的信息。下面是这些风格的一些例子,并解释了它们的含义。

文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄如下所示:“运行dotnet build命令将执行例程构建并生成binobj文件夹。”

代码块设置如下:

    public class Program 
    {
      public static void Main(string[] args)
      {
         BuildWebHost(args).Run();
      } 
    } 

任何命令行输入或输出的编写方式如下:

docker run -it -d -p 85:80 packtcontantsAPI

新术语重要词语以粗体显示。您在屏幕上(例如,在菜单或对话框中)看到的文字将显示如下文本:“打开 Visual Studio 2017 IDE,单击“新建项目”以打开“项目模板”对话框。”

Warnings or important notes appear like this. Tips and tricks appear like this.

读者反馈

我们欢迎读者的反馈。让我们知道你对这本书的看法你喜欢还是不喜欢。读者反馈对我们来说很重要,因为它可以帮助我们开发出您将真正从中获得最大收益的标题。要向我们发送总体反馈,只需发送电子邮件feedback@packtpub.com,并在邮件主题中提及该书的标题。如果您对某个主题有专业知识,并且您有兴趣撰写或贡献一本书,请参阅我们的作者指南www.packtpub.com/authors

客户支持

既然您是一本 Packt 图书的骄傲拥有者,我们有很多东西可以帮助您从购买中获得最大收益。

下载示例代码

您可以从您的帐户下载本书的示例代码文件 http://www.packtpub.com 。如果您在其他地方购买了本书,您可以访问http://www.packtpub.com/support 并注册,将文件直接通过电子邮件发送给您。您可以通过以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。
  2. 将鼠标指针悬停在顶部的“支持”选项卡上。
  3. 点击代码下载和勘误表。
  4. 在搜索框中输入图书的名称。
  5. 选择要下载代码文件的书籍。
  6. 从您购买本书的下拉菜单中选择。
  7. 点击代码下载。

下载文件后,请确保使用以下最新版本解压或解压缩文件夹:

  • WinRAR/7-Zip for Windows
  • 适用于 Mac 的 Zipeg/iZip/UnRarX
  • 适用于 Linux 的 7-Zip/PeaZip

该书的代码包也托管在 GitHub 上的https://github.com/PacktPublishing/Mastering-ASP.NET-Web-API 。我们在上还提供了丰富的书籍和视频目录中的其他代码包 https://github.com/PacktPublishing/ 。看看他们!

勘误表

虽然我们已尽一切努力确保内容的准确性,但错误确实会发生。如果您在我们的一本书中发现错误,可能是文本或代码中的错误,如果您能向我们报告,我们将不胜感激。通过这样做,您可以使其他读者免于沮丧,并帮助我们改进本书的后续版本。如果您发现任何错误,请访问进行报告 http://www.packtpub.com/submit-errata ,选择您的书籍,点击勘误表提交表单链接,然后输入勘误表的详细信息。一旦您的勘误表得到验证,您的提交将被接受,勘误表将上载到我们的网站或添加到该标题勘误表部分下的任何现有勘误表列表中。要查看之前提交的勘误表,请转至https://www.packtpub.com/books/content/support 并在搜索字段中输入图书名称。所需信息将出现在勘误表部分下。

盗版行为

在互联网上盗版版权材料是所有媒体都面临的一个持续问题。在 Packt,我们非常重视版权和许可证的保护。如果您在互联网上发现任何形式的非法复制品,请立即向我们提供地址或网站名称,以便我们采取补救措施。请致电copyright@packtpub.com与我们联系,并提供可疑盗版材料的链接。我们感谢您在保护我们的作者方面提供的帮助以及我们为您带来有价值内容的能力。

问题

如果您对本书的任何方面有任何问题,可以通过questions@packtpub.com与我们联系,我们将尽力解决该问题。

一、微服务和面向服务的架构简介

随着互联网可用性的增加,数据通信技术正在不断发展。架构的改进非常具有创新性、可扩展性,并且可以跨环境采用。需要在互联网上提供具有通用接口的软件组件,以便在不同平台和编程语言之间进行通信。

这导致了创建易于部署且具有可伸缩性的服务的概念,并通过 internet 公开这些服务。

服务功能设计被广泛采用;以服务的形式向异构客户机提供特性是一个好主意。这种使用服务的概念导致了SOA面向服务的架构

在本章中,我们将研究以下主题:

  • SOA 中的服务
  • 单片建筑
  • 微服务简介

SOA 中的服务

服务是向系统内或系统外的其他软件提供功能的软件。

其他软件(客户端)可以是任何东西,从 web 应用(网站)到移动应用(本机或混合),或桌面应用,甚至是使用其他服务来执行特定类型功能的其他服务。

在电子商务网站上下文中,当用户下订单时,web 应用与服务通信,对数据库执行创建、读取、更新和删除CRUD操作。

软件组件(客户端)和服务之间的通信通常通过具有某种通信协议的网络进行,例如,通过互联网与服务通信的移动应用。

以这种方式使用一个或多个服务的系统具有面向服务的架构。

这种架构背后的主要思想是,它不在每个客户端应用中使用模块,而是让我们使用一个或多个服务来为它们提供功能。这允许我们有许多使用相同功能的客户端应用。

SOA 之所以成功,是因为它具有以下特点:

  • 它允许我们在需求增加时扩展软件,使其能够在多台服务器上拥有服务的副本,因此当流量进来时,负载平衡器将该请求重定向到服务的特定实例,我们可以拥有服务的多个实例。因此,当需求增加时,增加服务器上的实例数量可以帮助我们扩展它。
  • SOA 拥有标准化的契约或接口。当客户端应用调用服务时,它通过调用方法调用服务。该方法的签名通常不会在服务更改时更改,因此只要合同和接口不变,我们就可以升级服务,而无需升级客户机。
  • 事实上,服务是无状态的,因此当从网站向我们的服务发出请求时,该服务实例不必记住来自该特定客户的前一个请求。它基本上拥有请求中的所有信息,以便检索与服务中以前的请求相关联的所有数据,因此,服务不必记住客户端以前对该服务的特定实例进行的调用。

服务实现

SOA 因其服务的实现而广受欢迎,这些服务可以通过独立于操作系统平台和编程语言的标准 internet 协议访问。

来自开发人员 POV 的服务只不过是托管在 web 服务器上的 web 服务,使用SOAP简单对象访问协议或 JSON 进行通信。很有意思的是,web 服务可以用作遗留系统的包装器,使它们能够实现网络功能。

实现服务(SOA)的一些流行技术如下:

  • 基于WSDLWeb 服务描述语言和 SOAP 的 Web 服务
  • 消息传递,例如,使用 ActiveMQ、JMS 和 RabbitMQ
  • WCF(微软的 Web 服务实现)
  • 阿帕奇节俭
  • 巫师
  • RESTful HTTP

当单片架构方法的经验证明比之前想象的更痛苦时,面向服务的架构开始获得动力。让我们简要了解什么是单片系统,以及它们导致采用 SOA 的缺点。

单片建筑

基于单片架构的系统在 SOA 或微服务运动之前就存在了。这些类型的系统与 SOA 试图实现的恰恰相反。

典型的单片系统是基于企业的应用,该应用可能是一个大型网站的形式,所有工作模块都打包在一个包中,也可能是一个与网站对话的服务的形式。它可以打包为部署在计算机上的大型可执行文件。

在这些系统中,我们向应用添加了不同的组件以保持增长;没有大小限制,也没有划分。总有一个包包含所有内容,因此,我们最终得到了一个庞大的代码库。

单片系统的高级架构图如下所示:

Typical Monolithic architecture

单片建筑的管理费用

从长远来看,企业在将单片架构应用于其系统时面临以下缺点:

  • 由于代码库太大,团队花了更长的时间在应用中开发新功能。
  • 大型系统的部署也可能具有挑战性,因为即使对于一个小的 bug 修复,我们也必须部署整个系统的新版本,因此,这会产生更大的风险。
  • 这是一个庞大的代码库,因此,我们也只能使用一个技术堆栈。
  • 这会降低整个系统的竞争力,因为我们不能轻易地采用可能给我们带来竞争优势的新技术。
  • 由于代码在一个大的包中,我们可能还具有高度的耦合,这意味着如果在系统的一个部分中进行更改,它可能会影响系统的另一个部分,因为代码是相互交织的。这种耦合可能存在于模块之间,也可能存在于不同的服务之间。
  • 扩大这项服务以满足需求效率很低。例如,如果系统的 Orders 模块有需求,我们必须创建整个包、整个服务的副本,以便仅扩展 Orders 部分。
  • 需要购买功能更强大的服务器,才能高效地运行大量单片应用。
  • 对如此庞大的代码库进行单元测试需要时间,而 QA 的回归测试也是一个耗时的过程。

The only one advantage that a Monolithic system has is the fact that we can run the entire code base on one machine, so, when developing and testing, we could probably replicate the entire environment on a machine.

单片系统的一个示例可以是 ASP.NET MVC 站点,其中站点本身是 UI 层,然后在业务层中,您拥有业务逻辑和数据访问层。多年来,如果我们继续采用同样的方法,那么它将成为一个整体系统。

引入微服务

微服务架构基本上是面向服务的架构。在使用面向服务的架构多年之后,软件开发人员已经意识到面向服务的架构应该是什么样子,这基本上就是微服务架构——它是面向服务架构的演变。

微服务是小型的、自治的服务,它可以很好地执行一项功能,同时也可以与其他服务协同工作。

Microservices 引入了一组新的附加设计原则,它们教会我们如何正确地调整服务的大小。以前,没有关于如何确定服务大小以及服务中包含什么的指导。传统的面向服务的架构导致了单一的大型服务,并且由于服务的大小,这些服务的扩展变得效率低下。

让我们看看使用微服务的优势。

轻量级但可扩展

微服务提供的服务具有更高的可扩展性和灵活性,并且可以在需要性能的领域提供高性能。

基于微服务架构的应用通常是由多个微服务驱动的应用,其中每一个都为应用的特定部分提供一组功能或一组相关功能。微服务架构通常为应用、客户端应用和客户端服务提供一组相关功能。

微服务架构还使用客户端和服务之间或两个或多个服务之间的轻量级通信机制。通信机制必须是轻量级和快速的,因为当一个微服务架构的系统执行一个事务时,它是一个由多个服务完成的分布式事务。因此,服务需要通过网络以快速有效的方式相互通信。

技术不可知论

微服务的应用接口,或者我们与微服务的通信方式,也需要与技术无关。这意味着服务需要使用开放式通信协议,这样它就不会指定客户端应用需要使用的技术。通过使用开放通信协议,例如 HTTP REST(基于 JSON),我们可以很容易地拥有一个与基于 Java 的微服务对话的.NET 客户端应用。

独立可变

微服务的另一个关键特征是它是独立可变的。我们可以升级、增强或修复特定的微服务,而无需更改任何客户端或系统内的任何其他服务。

在微服务架构中,每个微服务都有自己的数据存储。通过修改一个微服务,我们应该能够在系统中独立地部署该更改,而无需部署任何其他内容。

Sample Microservices architecture app

上图描绘了微服务系统的高级架构图。这是一个典型的电子商务系统示例,正如您在左侧看到的,客户的浏览器中运行着一个购物网站,或者它可能是一个使用 API 网关的移动应用。

浏览器通过 internet 连接到演示购物网站——演示购物网站可能是在 IIS 上运行的 ASP.NET MVC 网站。与网站的所有交互所需的所有处理实际上都是由大量后台运行的微服务执行的。

每个微服务都有一个焦点,或一组相关功能,有自己的数据存储,并且可以独立地更改和部署。例如,我们可以升级 Orders 服务,而无需升级此系统的任何其他部分。

每种类型的微服务也可能有多个实例。例如,如果 Orders 服务有需求,我们可能有几个 Orders 服务实例来满足需求。为了将来自购物网站的请求定向到订单服务的正确实例,我们有一个 API 网关,用于管理请求并将其路由到系统中正确的微服务。

因此,在本例中,当客户下订单时,购物网站可能会在这些服务中使用多个服务和多个功能来满足该交易。这就是为什么在微服务架构中,一个事务通常是一个分布式事务,因为该事务实际上由多个软件(即微服务)来满足。

微服务的好处

以下是微服务的好处:

  • 微服务架构满足了快速响应变化的需要。当今软件市场竞争激烈。如果您的产品不能提供所需的功能,它将很快失去市场份额。
  • 它满足了业务领域驱动设计的需要。应用的架构需要与组织结构或组织内业务功能的结构相匹配。
  • 微服务架构使用自动化测试工具。我们已经看到,在微服务架构中,事务是分布式的,因此,一个事务在完成之前将由多个服务处理。这些服务之间的集成需要测试,手动测试这些微服务可能是一项相当复杂的任务。自动化测试工具帮助我们执行此集成测试,减少了手动负担。
  • 云兼容的微服务可以减轻部署和发布管理的负担。
  • 微服务架构提供了一个采用新技术的平台。由于系统由多个运动部件组成,因此我们可以轻松地将一个部件(即微服务)从一个技术堆栈更改为另一个技术堆栈,以获得竞争优势。
  • 通过使用异步通信,分布式事务不必等待单个服务完成任务后才能完成。
  • 微服务的开发时间更短。因为系统被分成更小的活动部分,我们可以单独处理一个活动部分,可以让团队同时处理不同的部分,而且因为微服务的规模较小,而且它们只有一个焦点,所以团队在范围方面不必太担心。
  • 微服务架构还为我们提供了更多的正常运行时间,因为在升级系统时,我们可能会一次部署一个微服务,而不会影响系统的其余部分。

Netflix adopted the Microservices architecture; the lessons learnt on architectural designs are summarized in this link along with a video: https://www.nginx.com/blog/microservices-at-netflix-architectural-best-practices/.

总结

在过去的十年中,随着互联网带宽、机器处理能力、更好的框架等方面的改进,建筑服务的发展经历了许多变化。

从开发人员的角度来看,微服务是使用 ASP.NET、Java、PHP 或其他工具的基于 REST 的 Web API。在接下来的章节中,我们将学习开发基于 ASP.NET Core 的 Web API 应用的各个方面。

二、理解 HTTP 和 REST

REST 表示代表性状态转移。REST 架构风格是 Roy T.Fielding 的一篇博士论文,题为架构风格和基于网络的软件设计。这篇论文在经过 6 年的研究后于 2000 年首次发表。我们可以感谢菲尔丁先生的研究工作和发现。

现代的 API 是以 REST 为模型的,你会听到人们提到,它不是 RESTful 或被质疑的,你的 API 是 RESTful 吗?

要创建定义良好的 API 并对其建模,您需要对 REST 有充分的了解。出于这个原因,我们将深入研究 Roy T.Fielding 的研究。

罗伊·T·菲尔丁着手解决 1993 年出现的几个问题。许多作者在网络上发表他们的作品,他们希望合作。网络成为了一个分享和讨论研究工作的好地方。然而,它一流行就变得麻烦了。

就文件的发布方式和编辑方式而言,似乎缺少标准。还有一些与基础设施和速度有关的问题,编辑和访问文档的速度很慢。

在本章中,我们将探讨以下主题:

  • 软件架构
  • 休息原则
  • REST 建筑元素
  • 超文本传输协议
  • HTTP/2
  • 理查森成熟度模型

软件架构

软件架构(architecture)是软件系统在运行阶段的运行时元素的抽象。一个系统可能由多个抽象层次和多个操作阶段组成,每个阶段都有自己的软件架构。

软件架构由架构元素、组件、连接器和数据的配置定义,这些元素、组件、连接器和数据在它们的关系中受到约束,以实现所需的架构属性集:

  • 组件:是软件指令和内部状态的抽象单元,通过接口提供数据转换
  • 连接器:这是一种抽象机制,用于调解组件之间的通信、协调或协作
  • 数据:这是一个信息元素,通过其连接器从组件传输或由组件接收

REST 架构样式是几种网络架构的组合:

  • 数据流样式:

    • 管道和过滤器
    • 均匀管和过滤器
  • 复制样式:

    • 复制存储库
    • 隐藏物
  • 分层样式:

    • 客户端服务器
    • 分层系统和分层客户机服务器
    • 客户端无状态服务器
    • 客户端缓存无状态服务器
    • 分层客户端缓存无状态服务器
    • 远程会话
    • 远程数据访问
  • 移动代码样式:

    • 虚拟机
    • 远程评估
    • 按需编码
    • 分层按需代码客户端缓存无状态服务器
    • 移动代理
  • 点对点样式:

    • 基于事件的集成
    • C2
    • 分布式对象
    • 代理分布式对象

休息原则

REST 的建模方法是从零开始,然后添加约束。我们将对软件架构应用约束,您的架构将变得 RESTful。

客户机-服务器

注意,在 Roy T.Fielding 的整个工作中,他没有提到 REST 必须应用于 HTTP 协议。在我们的例子中,客户端服务器将作为浏览器作为客户端,IIS 作为服务器。

注意,客户机和服务器的分离允许抽象。这两个组件可以独立构建,也可以独立部署。

无国籍

下一个要添加的约束是无状态的。服务器不应包含任何工作流状态。这样,客户机就是其所需信息的驱动程序。当客户端向服务器请求数据时,客户端需要将所有相关信息传递给服务器。这种设计软件的方法创建了一个抽象,其中服务器不知道客户机;它创造了一个松散的耦合设计,这有利于改变。在本章后面,我们将通过扩展幂等概念来进一步研究无状态。

客户端必须跟踪其状态。缺点是客户机必须在每次请求时向服务器发送更多数据。

拥有无状态服务器允许扩展,因为服务器不存储任何特定于客户端的数据。

隐藏物

缓存是下一个约束。每当服务器传输不会更改的数据时,我们将这些数据称为静态数据。服务器可以缓存数据。

当发出第一个请求时,服务器将访问数据库以获取数据。然后,应将该数据缓存为应用层。对该数据的每个后续请求都将从缓存中提取,从而将服务器的请求保存到数据库中,从而更快地将响应返回到客户端。

统一界面

这是使 REST 不同于其他网络架构模式的约束。组件公开的接口是通用的。服务器不了解其消费者。它以相同的方式处理来自客户端的所有请求。您得到的是粗粒度的数据,因为并非所有的消费者都需要这样数量的数据。

要获得统一的界面,必须应用四个约束:

  • 确定资源
  • 操纵资源
  • 自描述性消息
  • 作为应用状态引擎的超媒体

我们稍后将研究这些问题。

分层系统

通过对组件进行分层,我们确保每个组件不知道其邻居连接到的层。这促进了良好的安全性,以便有良好的边界墙。它还允许在您的架构中使用旧系统时对其进行保护,并允许您保护新系统:

With the layered approach, it leads to many hops between systems, but you have the security boundaries, and components can be updated individually.

按需编码

这可能是 REST 最不受欢迎的特性。它允许服务器通过客户端可以执行的小程序或脚本向客户端提供代码。这允许服务器在部署后向客户端提供更多功能。约束是可选的,我们将不详细探讨它。

REST 建筑元素

如前所述,REST 不是一个协议,可以在没有实现的情况下讨论它。REST 的关键元素是向组件、连接器和数据添加约束的能力。

数据元素

选择需要将数据从服务器传输到客户端的超链接时,客户端需要解释数据并将其呈现为用户所需的格式。REST 原则是如何做到这一点的?REST 组件将数据和元数据传输到客户机,并提供帮助客户机组合其请求的资源的说明:

| 数据元 | 现代网络示例 |
| 资源 | 超文本引用的预期概念目标 |
| 资源标识符 | URL,URN |
| 代表 | HTML 文档,JPEG 图像 |
| 表示元数据 | 媒体类型,上次修改时间 |
| 资源元数据 | 源链接、替换、更改 |
| 控制数据 | 如果自之后进行了修改,则缓存控制 |

资源和资源标识符

资源是对您希望共享的任何信息的引用。它可以是您希望与朋友共享的图片或文档。罗伊·T·菲尔丁非常准确地总结了一个资源。资源是到一组实体的概念映射,而不是在任何特定时间点对应于映射的实体。更准确地说,资源 R 是一个随时间变化的隶属函数Mr(r),对于时间 t,它映射到一组等价的实体或值。集合中的值可以是资源表示和/或资源标识符。

当在组件之间使用资源时,REST 使用资源标识符来知道它是哪个资源。

在组件之间使用资源时,您的资源应该有一个资源标识符,REST 使用该标识符来标识您的资源。

陈述

表示是要共享的数据和与其关联的元数据的组合。表示的格式称为媒体类型。本章后面将用一些具体的例子更详细地讨论媒体。当服务器发送一些数据供客户端渲染时,媒体类型很重要;理想情况下,服务器将首先发送媒体类型,该类型将向客户端描述应如何呈现数据。当客户机接收到数据时,它可以开始呈现表示,从而获得更好的用户体验。这与客户端接收所有数据,然后接收有关如何呈现表示的指示进行比较。

连接器

连接器的类型有客户端、服务器、缓存、解析器和隧道。您可以将连接器视为接口。它们抽象了组件的通信方式。在 REST 架构中,连接器的工作是支持检索资源表示以及公开资源。其余的是无国籍的;每个请求都必须包含服务器处理来自客户端的请求所需的所有信息。

让我们看看 REST 用来处理请求的模型。该请求可以与存储过程进行比较:

Control Data defines the purpose of a message between components, such as the action being requested or the meaning of a response.

-Roy T.Fielding 的《架构风格和基于网络的软件架构设计》,第 5.2.1.2 节,第 109 页

组件

REST 架构中的一个组件是客户端上的 web 浏览器和服务器上的 IIS。

超文本传输协议

HTTP 代表超文本传输协议。第一个版本是 0.9;然后是 1.0 版。

1.0 和 1.1 之间的关键区别在于客户端与服务器建立连接,并且该连接被重用,而在 HTTP 1.0 中,该连接被丢弃,对于每个请求,都会创建一个新的连接。HTTP 1.1 也是通过将 REST 约束应用于 1.0 而派生的。

基本 HTTP 消息由头和正文组成。

当客户端与服务器通信时,它通过 HTTP 进行通信。服务器用消息和代码响应客户机。

HTTP/1.1 状态代码

有一系列广泛的状态代码,向客户端指示服务器处理的请求发生了什么:

  • 2xx:成功

  • 200:好的

  • 201:已创建

  • 3xx:重定向

  • 4xx:客户端错误

  • 400:请求错误

  • 401:未经授权

  • 403:禁止

  • 404:未找到

  • 409:冲突

  • 5xx:服务器错误

  • 500:内部服务器错误

我们将集中讨论最常见的代码,以及在本书后面实现 API 时将使用的代码。

API 示例

我已经使用 GithubAPI 来显示基本的 HTTP 方法。如果您希望探索 API,可以注册到 GitHub 并获得身份验证令牌。在下一章中,我们将创建自己的 API。在这些示例中,我们充当 API 的使用者。在这些示例中,我使用 Fiddler 发出请求。你可以使用任何你喜欢的工具;其他常用的工具是 Postman,它内置在 Chrome 浏览器或高级 Rest 客户端中。幂等元是 RESTAPI 使用的术语;简单地说,当您调用一个方法时,无论您调用它多少次,它都将返回相同的数据。在下面的示例中,我将列出哪些方法是幂等的。

HTTP POST 示例

我们的 HTTP 方法是 POST 和https://api.github.com/gists 是我们的资源。我们在请求中也有一个头值。User-Agent为表头键,取值为Awesome-Octocat-App。这是文档中指定的内容。

您可以在以下屏幕截图中记录请求正文:

这是我们对POST方法的要求。

我们对这一请求的回应如下所示。服务器已响应201,表示我们的请求有效,服务器已成功执行操作:

服务器还向我们发回一个资源。一个新的资源已经诞生,我们可以从中获取数据。

POST不是幂等的。REST 世界中的幂等式意味着,作为客户机,当我多次调用端点时,我希望收到相同的行为,或者希望返回相同的数据。考虑一个示例,您必须创建一个与唯一电子邮件地址的联系人。当您第一次使用此电子邮件地址和其他联系方式呼叫POST时,服务器将以201进行响应,这意味着已经创建了联系人并发布了唯一的资源,您可以在其中获取该数据。

如果您使用相同的电子邮件地址调用POST方法,会发生什么?服务器应返回冲突,409。该电子邮件存在于数据存储中。所以POST不是幂等的:

HTTP 获取示例

使用来自服务器的资源,我们对资源执行GET

服务器以200状态响应,状态为 OK:

服务器返回我们请求的数据:

GET是幂等的,当我们发出第一次请求时,我们得到一个响应。发出相同的请求将返回相同的响应。这也与无国籍的 REST 原则有关。为了让这个GET请求返回相同的数据,服务器应该是无状态的。

HTTP PUT 示例

我们可以使用以下 URL 对我们的表示进行更新。注意 HTTP 动词PUT

文档中说,我们可以在下面的资源中调用PUT方法,并将/star作为 URI 的一部分。PUT用于修改我们的表述。通常,PUT会有一个主体。在 GitHub 的 GistAPI 中,他们简化了它。一般来说,PUT的概念与POST相似,只是 URI 包含调用POST方法时收到的标识符:

当资源不存在时,PUT的行为类似于POST。在我们来自POST的示例中,如果您必须在第一次呼叫PUT时使用POST请求来创建联系人,那么您将收到201,通知您资源已创建。然后,如果您必须再次在PUT上调用请求,您将返回200并获得相同的数据。这样,PUT就是幂等的。

HTTP 删除示例

DELETEGET非常相似。我们的 HTTP 方法是DELETE,我们想撤销我们用PUT创建的星 put。DELETE通常没有主体:

DELETE是幂等的;当您呼叫DELETE时,您将返回200,表示该资源已被删除。再次发出此请求将导致404,未找到资源,因为数据已被删除。再次拨打此电话将导致404。虽然响应已从200更改为404,但您仍然可以恢复相同的行为,服务器也没有做任何不同的事情。

HTTP 的第 2 版

HTTP/2 是对 HTTP 1.1 的优化。许多浏览器已经支持 HTTP/2;你的 Chrome 浏览器已经做到了。

HTTP/2 是两个规范的组合:超文本传输协议版本 2(RFC7540)和 HTTP2 的 HPACK-Header 压缩(RFC7541)。

传输层安全TLS上使用 HTTP/2 时,使用“h2”表示协议。

当通过明文 TCP 使用 HTTP/2 或 HTTP.1.1 升级时,使用“h2c”字符串。

GET请求的示例如下:

GET / HTTP/1.1
Host: server.example.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>

-RCF 7540 第 3.2 节

此请求来自不知道是否支持 HTTP/2 的客户端。它发出 HTTP 1.1 请求,但在标头“h2c”中包含一个升级字段,以及至少一个 HTTP2 设置标头字段:

A server that does not support HTTP/2 will respond as follows:

HTTP/1.1 200 OK
Content-Length: 243
Content-Type: text/html

-RCF 7540 第 3.2 节

这看起来像一个常规的 HTTP/1.1 响应:

A server that does support HTTP/2 will respond as follows:

HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: h2c
[ HTTP/2 connection ...

-RCF 7540 第 3.2 节

在 HTTP2 中引入了一个框架作为基本单元。您可以将帧视为通过导线传输的数据包。对于请求和响应,使用HEADERDATA帧作为构建块,对于 HTTP/2 特性,使用SETTINGSWINDOWS_UPDATEPUSH_PROMISE帧。

单连接

服务器和客户端都可以使用服务器和客户端之间的单个连接来传输多个请求。假设您有一个包含多个组件的页面,所有这些组件都向服务器发出独立请求,比如说,一个将获得今天的天气,一个将获得最新的股票价格,还有一个将获得最新的标题。它们都可以通过一个连接而不是三个单独的连接来实现。这也适用于服务器。你最终得到的是更少的连接被创建。

服务器推送

服务器可以将数据推送到客户端。当客户机从服务器请求数据时,服务器可以确定客户机还需要一些其他数据。服务器可以将此数据推送到客户端。客户端总是可以通过向服务器发送应禁用推送数据的信号来拒绝数据。服务器发送给客户端的数据称为PUSH_PROMISE帧。如果客户端实现了 HTTP 缓存,则数据存储在客户端缓存中。

多路复用和流

流就像一个隧道,有许多汽车在两个方向上通过,汽车被帧取代,流在客户端和服务器之间是独立的。在 HTTP/2 中,一个连接可以有多个流,来自一个请求的帧分布在多个流上,尽管帧的顺序很重要。

这是对 HTTP 1.1 的重大改进,HTTP 1.1 使用多个连接来呈现单个页面。

流优先级

拥有多个流是很好的,但是有时候,您希望一个流在另一个流之前被寻址。在 HTPP/2 中,客户端可以在HEADERS帧中指定流的优先级。客户端可以使用Priority帧更改流的优先级。通过这种方式,客户机可以向其对等方指示它希望如何处理其请求。

二进制消息

与文本相比,采用二进制格式的消息处理速度更快。由于它们是有线传输的本机二进制格式,因此不需要通过 TCP 协议将文本转换为二进制。

头压缩

随着 web 的发展,更多的数据从服务器发送到客户端,从客户端发送到服务器。HTTP 1.1 不压缩头字段。HTTP 在 TCP 上工作,并通过此连接发送请求,其中头文件较大且包含冗余数据。TCP 采用网络拥塞避免算法实现的慢启动,该算法将数据包放置在网络上。如果报头被压缩,更多的数据包可以通过网络发送。HTTP/2 通过报头压缩修复了这个问题,报头压缩利用了 TCP,从而提高了数据传输速度。

媒体类型

通常称为MIME多用途互联网邮件扩展类型),媒体类型用于标识 HTTP 消息体的格式。媒体类型为{type/subtype}格式;举例如下:

  • 文本/html
  • 图像/png
  • 音频/mpeg
  • 视频/视频

请求可以如下所示:

    GET: 
    Host: 
    Accept:application/json, text/javascript 

客户端正在指定它可以接收数据的格式。

理查森成熟度模型

理查森成熟度模型RMM由 Leonard Richardson 开发。通常称为 RMM,用于升级 API 的标准。

0 级

这是传统的基于 soap 的 web 服务或 XML-RPC 服务。它使用 HTTP,但有一个方法和一个 URI。此方法通常为POST,将返回大量数据集。我相信我们所有人都曾经使用过这种类型的 web 服务,或者在某个时候可能会遇到它。整个数据库作为数据集包装在此输出中。

一级

资源是公开的,但您仍然有一个 HTTP 方法。如果您处于级别 0,那么更改 web 服务以返回资源将使您从级别 0 转到级别 1。您仍然有一个 HTTP 方法,但当调用您的方法时,您的服务将传回一个资源:

    Request: 
    POST 
    diet/ate 
    Response 
    diet/ate/12789 
    Request: 
    POST 
    diet/ate 
    Response: 
    diet/ate/99000 

仍然有一个端点diet/ate,它返回许多资源。

二级

级别 2 用于使用 HTTP 谓词。因此,在级别 1 中,我们介绍了资源,级别 2 介绍了动词。

使用前面的例子,当你在上午 10 点发布你吃的东西时,服务器会给你一个资源。使用此资源,您可以在该资源上执行GET,并查看您在上午 10 点吃了什么的详细信息:

    GET: diet/ate/12789 
    Response  
    { 
      'time':'10:00', 
      'apple':'1', 
      'water':'2' 
    } 

然后您可以使用PUT更新这些详细信息;请注意,我们使用的是同一资源。

请求如下:

    PUT: diet/ate/12789 
    { 
      'time':'10:00', 
      'tea':'1', 
      'muffin':'3' 
    } 

如果您稍后意识到您在上午 10 点没有吃饭,您也可以删除此资源:

    DEL : diet/ate/12789 

我们使用相同的资源,但使用了不同的动词。

当我们在 1 级创建资源时,我们将POST更改为在创建资源时返回 201,如果资源存在,则返回409冲突。

第 2 级部分使用响应代码,不会在每次操作中返回200

三级

在第三级,超媒体被引入我们的响应中,通常称为HATEOAS超文本作为应用状态的引擎)。

让我们回到POST示例:

    POST : diet/ate  
    Response: 
    { 
      "id":"12789", 
      "links":[{ 
        "rel":"self", 
        "href":"http://yoursitename/diet/12789 
      }, 
      { 
        "Rel":"self", 
        "href":"http://yoursitename/diet/12789" 
      }, 
      "rel":"rating", 
      "href":"http://yoursitename/diet/12789/rating/" 
      ] 
    } 

链接的要点是,它让消费者知道它可以执行哪些操作。

虽然两个端点看起来相同,但消费者会发现一个是DELETE,另一个是PUT

最后一个链接是对您添加的膳食进行评分的资源。

总结

我们研究了 REST 的定义以及 REST 是如何派生出来的。当您查看 REST 架构时,您应该能够将其分为三类,正如 Roy T.Fielding 所解释的那样。一个是流程视图,它描述了数据如何从客户机流向多个组件。第二,连接器视图专门用于在特定于资源和资源标识的组件之间交换消息。第三,我们称之为表示的数据如何从服务器传输到客户端的数据视图。对 REST 原则有一个很好的理解是非常重要的,REST 被应用于 HTTP1.0,以便派生 HTTP1.1。

HTTP 是 REST 原则的一个活生生的例子。像GETPOST这样的动作是无状态的,这是休息的原则。这些示例展示了如何构造 HTTP 请求以及服务器作为响应返回的内容。有了 HTTP/2,我们就有了新的特性,这使得我们的传输速度更快,应用响应更快。

Richardson 成熟度模型解释了 API 是如何分类的;作为一名开发人员,您应该以 3 级模型为目标。如果您是 API 的消费者,可能需要在几个选项中进行选择。RMM 将帮助您做出明智的决定。

在本章中,我们没有关注具体的技术;在下一章中,我们将深入研究 ASP.NET Core 及其作为框架提供的内容,以构建 web API。

三、ASP.NET Core Web API 剖析

本章首先简要回顾一下 MVC。当我们使用 web API 时,您可能会惊讶于为什么我们需要 MVC。这是因为 ASP.NET Web API 是基于控制器、模型和视图的 MVC 原则设计的(在 Web API 的情况下,返回的响应可以被视为一个无面视图)。

本章的重点是了解为什么我们需要 Web API 形式的基于 HTTP 的轻量级服务技术,它的发展以满足不断变化的行业需求,以及 Microsoft 以.NET Core 和 ASP.NET Core 应用的形式进入开源世界,并且不局限于开发 ASP.NET web 应用的 Windows 操作系统世界。

在本章中,我们将研究以下主题:

  • 快速回顾 MVC 框架
  • web API 的诞生及其发展
  • NET Core 简介
  • ASP.NET Core 架构概述
  • 使用 Visual Studio IDE 创建 ASP.NET Core 项目
  • 在 Linux/macOS 中创建 ASP.NET Core 项目
  • 检查 ASP.NET Core 项目文件和结构
  • 理解请求处理
  • MVC 与 webapi 的统一
  • 运行 ASP.NET Core Web API

快速回顾 MVC 框架

模型视图控制器MVC)是一种功能强大且优雅的分离应用中关注点的方法,它非常适合于 web 应用。

对于 ASP.NET MVC,MVC 代表以下内容:

  • 模型(M):这些是表示域模型的类。其中大多数表示存储在数据库中的数据,例如,员工、客户等。
  • 视图(V):这是一个动态生成的 HTML 页面作为模板。
  • 控制器(C):这是一个管理视图和模型之间交互的类。视图上的任何操作都应该在控制器中具有相应的处理,如用户输入、呈现适当的 UI、身份验证、日志记录等。

Web API 的诞生及其发展

回顾基于 ASP.NET ASMX 的 XML web 服务被广泛用于构建面向服务的应用的日子,创建基于SOAP简单对象访问协议)的服务是最简单的方法,该服务可供.NET 应用和非.NET 应用使用。它只在 HTTP 上可用。

在 2007 年底,微软发布了 Tyt T0. Windows 通信基金会 T1 T1(Po.T2。WCF 过去和现在都是构建基于 SOA 的应用的强大技术。这是微软.NET 世界的一次巨大飞跃。

WCF 足够灵活,可以配置为 HTTP 服务、远程处理服务、TCP 服务等。使用 WCF 的契约,我们将保持整个业务逻辑代码基础不变,并通过 SOAP/非 SOAP 将服务公开为基于 HTTP 或非基于 HTTP。

直到 2010 年,基于 ASMX 的 XML web 服务(或 WCF 服务)被广泛应用于基于客户机-服务器的应用中;事实上,一切都很顺利。

但是.NET 和非.NET 社区的开发人员开始感到有必要为客户机-服务器应用开发一种全新的 SOA 技术。这背后的一些原因如下:

  • 随着应用投入生产,通信时使用的数据量开始爆炸式增长,通过网络传输这些数据会消耗带宽。
  • SOAP 在某种程度上是轻量级的,开始显示出有效负载增加的迹象。几 KB 的 SOAP 数据包将变成几 MB 的数据传输。
  • 由于 WSDL 和代理生成,在应用中使用 SOAP 服务导致了巨大的应用规模。在 web 应用中使用时,情况更糟。
  • 对 SOAP 服务的任何更改都会导致更新服务代理以反映更改。对于任何开发人员来说,这都不是一项容易的任务。
  • 基于 JavaScript 的 web 框架已经发布,并为更简单的 web 开发方式奠定了基础。使用基于 SOAP 的服务并不是那么理想。
  • 平板电脑和智能手机等手持设备开始流行。他们有更专注的应用,需要一种非常轻量级的面向服务的方法。
  • 基于浏览器的单页应用SPA)发展非常迅速。对这些 SPA 来说,使用基于 SOAP 的服务是相当繁重的。
  • 微软发布了基于 REST 的 WCF 组件,这些组件可以配置为以 JSON 或 XML 响应,但它仍然构建在 WCF 的重型技术之上。
  • 应用不再只是大型企业服务,需要一个更加专注、轻量级和易于使用的服务,它可以在几天内启动并运行。

任何看过基于 SOA 的技术(如 ASMX、WCF 或任何基于 SOAP 的技术)不断发展的开发人员都觉得需要更轻的、基于 HTTP 的服务。

基于 HTTP 的、与 JSON 兼容的POCO普通的旧 CLR 对象)轻量级服务是当下的需要,Web API 的概念开始获得发展势头。

介绍 webapi

使用 HTTP 谓词通过 web 访问的任何方法都称为 web API。它是一种通过 HTTP 传输数据的轻量级方式,很容易被各种客户端(如浏览器、桌面应用、手持设备,甚至其他 SOA 应用)使用。

要使 web API 成为一个成功的基于 HTTP 的服务,它需要一个强大的 web 基础设施,如托管、缓存、并发、日志记录、安全性等。最好的 web 基础设施之一就是 ASP.NET。

ASP.NET 以 Web 表单或 MVC 的形式被广泛采用,因此 Web 基础设施的坚实基础已经足够成熟,可以扩展为 Web API。

微软通过创建 ASP.NETWebAPI 来回应社区的需求——这是一个超级简单但功能强大的框架,用于构建仅限 HTTP 的默认 JSON Web 服务,而无需 WCF 的繁琐工作。

ASP.NET Web API 可用于在几分钟内构建基于 REST 的服务,并且可以轻松使用任何前端技术。

它于 2012 年推出,具有基于 HTTP 的服务的最基本需求,如基于约定的路由、HTTP 请求和响应消息。

后来,微软在 Visual Studio 2013 中发布了更大更好的 ASP.NET Web API 2 和 ASP.NET MVC 5。

ASP.NET Web API 2 通过以下功能以更快的速度发展:

  • 通过使用 NuGet,WebAPI2 的安装变得更加简单;您可以创建空的 ASP.NET 或 MVC 项目,然后在 NuGet Package Manager 控制台上运行以下命令:
        Install-Package Microsoft.AspNet.WebApi

  • WebAPI 的初始版本基于基于约定的路由,这意味着我们定义了一个或多个路由模板,并围绕它展开工作。它很简单,没有太多麻烦,因为路由逻辑在一个地方,并且它适用于所有控制器。
  • 现实世界的应用更为复杂,因为资源(控制器/操作)具有子资源,例如,客户有订单,书籍有作者,等等。在这种情况下,基于约定的路由是不可伸缩的。
  • WebAPI2 引入了属性路由的新概念,它使用编程语言中的属性来定义路由。一个直接的优点是,开发人员可以完全控制 web API 的 URI 的形成方式。
  • 下面是属性路由的一个快速片段:
        Route("customers/{customerId}/orders")] 
        public IEnumerable<Order>GetOrdersByCustomer(int customerId) { ... } 

For more details on this, read Attribute Routing in ASP.NET Web API 2 at https://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2.

  • ASP.NET Web API 位于 ASP.NET framework 上,这可能会让您认为它只能托管在 IIS 上。但是,使用 OWIN self-host,也可以在没有 IIS 的情况下托管它。
  • 如果任何 web API 都是使用.NET 或非.NET 技术开发的,并且要跨不同的 web 框架使用,那么启用 CORS 是必须的。

A must read on CORS and ASP.NET Web API 2 can be found at this link: https://www.asp.net/web-api/overview/security/enabling-cross-origin-requests-in-web-api.

  • IHTTPActionResult 和 WebAPI OData 改进是帮助 WebAPI 2 发展成为开发基于 HTTP 的服务的强大技术的其他几个显著特性。
  • 随着 C 语言的改进,ASP.NET Web API 2 在过去几年中变得更加强大,如使用 Async/Await、LINQ、实体框架集成、依赖注入 DI 框架等进行异步编程。

ASP.NET 进入开源世界

每种技术都必须随着硬件、网络和软件行业的需求和进步而不断发展,ASP.NET Web API 也不例外。

从开发人员社区、企业和最终用户的角度来看,ASP.NET Web API 应该进行的一些更改如下:

  • 虽然 ASP.NET MVC 和 web API 是 ASP.NET 堆栈的一部分,但它们的实现和代码库不同。统一的代码库减少了维护它们的负担。
  • 众所周知,web API 由各种客户端使用,例如 web 应用、本机应用、混合应用和使用不同技术的桌面应用(.NET 或非.NET)。但是,如何以跨平台的方式开发 web API,开发人员不必总是依赖 Windows OS/Visual Studio IDE。
  • ASP.NET 堆栈应该是开源的,以便在更大范围内采用。
  • 最终用户受益于开源创新。

我们看到了 web API 被接受的原因,它们是如何演变成一个强大的基于 HTTP 的服务的,以及一些必要的演变。有了这些想法,微软通过发布.NETCore 和 ASP.NETCore 进入了开源世界。

NET Core 简介

.NETCore 是一个跨平台的开源托管软件框架。它构建在 CORECRL 之上,CORECRL 是 CLR 的一个完整的跨平台运行时实现。

.NET Core 应用可以在 Windows、Linux 和 macOS 系统等跨平台上开发、测试和部署。

.NET Core 具有以下重要组件:

  • CORECRL:这是一个.NET Core 执行引擎,执行 GC、编译机器代码等基本任务。
  • CoreFX:它包含用于.NETCore 的集合、文件系统、XML、异步等的类库。
  • SDK 工具:这是一套用于日常开发体验的 SDK 工具。创建项目、构建、运行和测试是开发人员的常见需求,也是这些 SDK 工具的一部分。

.NET Core 共享原始.NET Framework 的一个子集,另外它还附带了自己的一组 API,这些 API 不是.NET Framework 的一部分。这就产生了一些可供.NET Core 和.NET 框架使用的共享 API。

.NET Core 应用可以轻松地在现有的.Net 框架上工作,但反之亦然。

.NET Core 为操作系统的执行入口点提供CLI命令行界面,并提供编译、包管理等开发人员服务。

以下是有关.NET Core 的一些有趣的知识点:

  • .NET Core 可以安装在 Windows、Linux 和 macOS 等跨平台上。它可用于设备、云和嵌入式/物联网场景。
  • Visual Studio IDE 不是与.NET Core 一起工作所必需的,但在 Windows 操作系统上工作时,我们可以利用现有的 IDE 知识。
  • .NET Core 是模块化的,这意味着开发人员处理的不是程序集,而是 NuGet 包。
  • .NETCore 依靠其软件包管理器接收更新,因为跨平台技术不能依赖 Windows 更新。
  • 要学习.NETCore,我们只需要一个 shell、文本编辑器,并在运行时安装它。
  • .NET Core 具有灵活的部署。它可以包含在您的应用中,也可以在用户或机器范围内并排安装。
  • .NET Core 应用也可以作为独立应用自托管/运行。

.NET Core 支持四种跨平台方案:ASP.NET Core web 应用、命令行应用、库和通用 Windows 平台应用。

它不支持 Windows 窗体或 WPF,这些窗体或 WPF 在 Windows 上呈现桌面软件的标准 GUI。

目前,只有 C#编程语言可以用来编写.NET Core 应用。F#和 VB 支持正在进行中。

我们将主要关注 ASP.NET Core web 应用,其中包括 MVC 和 web API。将简要介绍 CLI 应用和库。由于其跨平台性,必须安装 VisualStudioIDE 才能创建应用不是强制性的。在本节中,我们将安装.NET Core,构建一个非常基本的.NET Core 应用,并了解.NET Core 的不同命令。

安装.NET Core SDK

打开.NET Core(https://www.microsoft.com/net/core/preview 网站,根据您选择的平台下载 SDK。在撰写本文时,.NET Core 2 预览版 2 已可用。

对于 Windows 环境,.NET Core 2.0 SDK 可以通过两种方式安装:

  • .NET Core 2.0 和 Visual Studio 工具:在 Visual Studio 2017 安装期间,提供了安装所需工具的选项,或者.NET Core SDK 也可以安装它们。CLI 与此一起安装。
  • .NET Core 2.0 Windows SDK:这是使用.NET Core 应用的 CLI 模式。

If you're using Windows OS, and prefer Visual Studio 2017 IDE, then it's better to leverage your IDE experience..

要使用代码,还可以安装一个文本编辑器,如 VisualStudio 代码。它是微软为 Windows、Linux 和 macOS 开发的轻量级代码编辑器。可从下载 https://code.visualstudio.com/ 。也可以使用其他文本编辑器,如 Vim、Atom 和 Sublime。

对于非 Windows 计算机,请提供适当的.NET Core SDK(请参阅此链接了解您选择的操作系统:https://www.microsoft.com/net/core/preview 应安装用于处理代码的和 Visual Studio 代码(推荐)。

Visual Studio for Mac is an exclusive IDE for macOS users, and can be used for .NET Core and ASP.NET Core apps. Download it from https://www.visualstudio.com/vs/visual-studio-mac/.

创建和运行基本的.NET Core 应用

我们将重点学习.NETCore 的一些基本概念,以及如何使用命令行。以下步骤是跨平台学习.NET Core 的步骤。有关更多详细信息,请参阅中的文档链接 https://docs.microsoft.com/en-us/dotnet/core/tools/.

首先,让我们确保一切都已正确安装。打开 Console/Shell(根据您选择的操作系统),输入以下命令以查看 CLI 命令和工具版本、产品信息以及运行时环境:

> dotnet -info  

.NET Core CLI 提供了以下要使用的命令:

| new | 初始化基本.NET 项目 |
| restore | 恢复.NET 项目中指定的依赖项(大多数情况下自动运行) |
| build | 构建一个.NET 项目 |
| publish | 发布用于部署的.NET 项目(包括运行时) |
| run | 编译并立即执行.NET 项目 |
| test | 使用项目中指定的测试运行程序运行单元测试 |
| pack | 创建一个 NuGet 包 |

还有一些命令,也可以去寻找它们。

在命令行中,键入以下命令:

> dotnet new console --name DemoCoreApp  

.NET Core Command in action

让我们了解在前面的屏幕截图中发生了什么:

  • dotnet new;在目录上下文中创建.NET Core C#控制台项目。它有两个文件:program.cs包含 C#代码,它的项目文件csproj
  • DemoCoreApp.csproj是一个常见的.NET 项目文件,以 XML 格式包含项目的所有详细信息。然而,在.NETCore 中,由于使用 netcoreapp2.0 作为目标框架,项目被高度精简。
  • 从.NETCore2.0 开始,无论何时创建、构建或发布项目,dotnet restore都会自动运行。

如前面的屏幕截图所示,演示项目在 VS 代码中打开;查看program.cs查看控制台上输出文本的 C#代码。

就像在传统的.NET 项目中一样,我们构建一个 C#项目,同样的方式,运行dotnet build命令,将执行例行构建并生成binobj文件夹。

现在dotnet run将运行 C#console 应用,并在控制台上显示结果Hello World

此 C#项目可以通过运行dotnet publish发布并用于部署。这将在bin目录下创建publish文件夹。这个publish文件夹可以移植到任何安装了.NET Core SDK 的机器上。

我们看到了一个控制台应用的构建;我们可以使用相同的dotnet new命令创建库、web 和 xunittest 项目,如下所示:

dotnet new [--type] 

--type选项指定要创建的项目的模板类型,即 console、web、lib 和 xunittest。

使用.NET CLI 命令dotnet new web,您可以创建一个使用.NET Core 的 web 应用,该应用称为 ASP.NET Core。

介绍 ASP.NET Core

ASP.NET Core 是一个新的开源跨平台框架,用于使用.NET 构建基于云的现代 web 应用。

ASP.NET Core 是完全开源的,您可以从 GitHub(下载 https://github.com/aspnet/Mvc )。它是跨平台的,这意味着您可以在 Linux/macOS 上开发 ASP.NET Core 应用,当然也可以在 Windows 操作系统上开发。

ASP.NET 最早是在大约 15 年前与.NET framework 一起发布的。从那时起,它已被数百万开发人员用于大型和小型应用。

由于.NET Core 是跨平台的,ASP.NET 在开发和部署 web 应用方面跨越了 Windows 操作系统环境的界限。让我们深入了解跨平台 ASP.NET 的更多细节。

ASP.NET Core 概述

ASP.NET Core Architecture overview

ASP.NET Core 的高级概述提供了以下见解:

  • ASP.NET Core 在完整的.NET framework 和.NET Core 上运行。
  • 具有完整.NET framework 的 ASP.NET Core 应用只能在 Windows 计算机上开发和部署。
  • 当使用.NETCore 时,可以在所选的平台上开发和部署它。Windows、Linux 和 macOS 的徽标表明您可以在这些系统上使用 ASP.NET Core。
  • ASP.NET Core 在非 Windows 计算机上运行时,使用.NET Core 库运行应用。很明显,您不会拥有所有的.NET 库,但大多数都是可用的。
  • 在 ASP.NET Core 上工作的开发人员可以轻松地切换到不限于 Visual Studio IDE 的任何机器上工作。
  • ASP.NET Core 可以与不同版本的.NET Core 一起运行。

除了跨平台之外,ASP.NET Core 还有许多其他基础性改进。以下是使用 ASP.NET Core 的优点:

  • ASP.NET Core 采用完全模块化的方法进行应用开发——构建应用所需的每个组件都被很好地分解到 NuGet 包中。我们只需要通过 NuGet 添加所需的包,以保持整个应用的轻量级。
  • ASP.NET Core 不再基于System.Web.dll
  • Visual Studio IDE 用于在 Windows OS box 上开发 ASP.NET 应用。现在,由于我们已经超越了 Windows 世界,我们需要在 Linux/macOS 上开发 ASP.NET 应用所需的 IDE/编辑器/工具。Microsoft 为几乎所有类型的 web 应用开发了功能强大的轻量级代码编辑器,称为 Visual Studio 代码。
  • NET Core 就是这样一个框架,我们不需要 Visual Studio IDE/代码来开发应用。我们也可以使用 Sublime 和 Vim 等代码编辑器。要在编辑器中使用 C 代码,请安装 OmniSharp 插件。
  • ASP.NET Core 与 Angular、Ember、NodeJS 和 Bootstrap 等现代 web 框架进行了强大的无缝集成。
  • 使用 bower 和 NPM,我们可以使用现代 web 框架。
  • ASP.NET Core 应用通过配置系统实现了云端准备——它只是无缝地从内部部署过渡到云端。
  • 内置依赖注入。
  • 可以托管在 IIS 上,也可以在您自己的进程或 Nginx 上自托管(它是一个免费、开源、高性能的 HTTP 服务器和 LINUX 环境的反向代理)。
  • 新的轻量级和模块化 HTTP 请求管道。
  • web UI 和 web API 的统一代码库。当我们探索 ASP.NET Core 应用的剖析时,我们将看到更多关于这方面的内容。

使用 Visual Studio IDE 创建 ASP.NET Core 项目

现在,我们将使用 Visual Studio 2017 IDE 创建一个 ASP.NET Core Web API 应用。在开始之前,请确保此先决条件:

  • 安装 Visual Studio 2017(安装时请选择.NET Core SDK 选项)。我们将一直使用社区版。本书通篇使用 ASP.NET Core 2.0 预览版 2

让我们开始逐步构建 ASP.NET Core Web API:

  1. 打开 Visual Studio 2017 IDE,单击新建项目以打开“项目模板”对话框。
  2. 在 Visual C#Templates 下,单击.NET Core,然后选择 ASP.NET Core Web 应用,如以下屏幕截图所示:

Create ASP.NET Core project in Visual Studio 2017 IDE We can also create an ASP.NET Core web application targeting the full .NET framework by web template under the Visual C# section

  1. 提供适当的项目名称,如MyFirstCoreApi,单击“确定”。

选择应用类型

ASP.NET Core 为我们提供了不同的应用模板来开始开发应用。这些模板为我们提供了一个最佳的项目结构,以保持一切井然有序。我们有以下几种:

  • Empty:这是项目模板的最简单形式,只包含Program.csStartup.cs类。由于 ASP.NET Core 的完全模块化特性,我们可以将这个空项目升级到任何类型的 web 应用。
  • Web API:这将创建带有控制器、web.config等的 Web API 项目。我们的重点将放在这个应用模板上。
  • Web 应用:这将创建一个 ASP.NET Core MVC 类型的项目,其中包含控制器、视图、客户端配置、Startup.csweb.config
  • Web 应用(Razor 页面):这将使用 Razor 页面创建 ASP.NET Core Web 应用。
  • Angular、React.js 和 React.js 与 Redux:这将创建基于 JavaScript 框架的 ASP.NET Core web 应用。

ASP.NET Core Project Templates

遵循 ASP.NET Core 提供的模板项目结构不是强制性的。在处理大型项目时,最佳做法是将它们拆分为单独的项目以实现可维护性。默认项目结构足以理解各个组件之间的交互。

选择身份验证类型

每个应用都需要某种类型的身份验证,以防止未经授权访问该应用。在前面的屏幕截图中,更改身份验证将提供以下身份验证选项:

  • 无身份验证:选择此选项不会向应用添加任何身份验证包。但是,我们可以在需要时添加这样的包来完全保护我们的应用数据。
  • 个人用户帐户:连接到 Azure AD B2C 应用将为我们提供所有身份验证和授权数据。
  • 工作或学校帐户:使用 Office 365、Active Directory 或 Azure Directory 服务对用户进行身份验证的企业、组织和学校可以使用此选项。
  • Windows 身份验证:在 Intranet 环境中使用的应用可以使用此选项。

在更改身份验证选项中,选择无身份验证,如此屏幕截图所示:

Select Authentication Type for Web API

单击 OK 创建一个 ASP.NET Core Web API 项目;VisualStudio 工具将立即开始恢复所需的包。

执行dotnet restore命令以恢复所有 NuGet 包。

我们了解了 Visual Studio IDE 工具如何帮助我们在 Windows 操作系统上创建 ASP.NET Core 应用。这与创建 ASP.NET(MVC4/5 和 ASPX)应用时的操作类似。

在 Linux/macOS 上创建 ASP.NET Core web 应用

ASP.NET Core 是一种跨平台技术,在 Linux/macOS 上创建 web 应用时,我们需要类似的用户体验。众所周知,Visual Studio IDE 不能安装在 Linux/macOS 上,因此,在非 Windows 操作系统上使用 ASP.NET Core 应用有一种不同的方法。

以下是 Linux/macOS 机器的软件要求:

On Windows machines too, we can use NodeJS, Visual Studio Code, and .NET Core SDK for working with ASP.NET Core and avoid Visual Studio IDE.

使用 Yeoman 创建 ASP.NET Core web 应用

Yeoman 是现代网络应用的网络脚手架工具。它是一个开源工具,使用命令行选项与 VisualStudio 模板类似。Yeoman 命令行工具 yo 与 Yeoman 生成器一起工作。

Yeoman 生成一个完整的项目,其中包含运行应用所需的所有文件,就像 VS IDE 一样。阅读链接http://yeoman.io/ 了解更多。

要安装 Yeoman,请确保从前面给出的软件先决条件中的链接安装 NodeJS 和 NPM。

打开命令行运行命令安装Yeomanyo)。选项-g全局安装 npm 包,以便可以从任何路径使用。

npm install -g yo 

一旦 Yeoman 安装成功,我们需要为 yo 安装 ASP.NET Core 生成器。它将有助于项目创建和构建 web 应用的不同组件。在命令行中,运行以下命令:

npm install -g generator-aspnet

Yeoman 脚手架只能与 ASP.NET Core web 应用用于.NET Core。

使用 Yeoman 创建 ASP.NET Core Web API

确保所有内容都已正确安装,打开命令行,键入yo aspnet以查看与 Visual Studio IDE 类似的不同项目模板。我们将创建一个 web API 应用,提供一个合适的名称,如yowebapidemo,然后点击输入来创建项目。

Create ASP.NET Core apps using Yeoman

一旦 Yeoman 生成 web API 项目,它将显示创建的项目文件列表和要执行的指令。

We can even use the .NET Core CLI commands to create an ASP.NET Core Web API project by referring to the link https://docs.microsoft.com/en-us/dotnet/core/tools/.

ASP.NET Core Web API 应用结构

我们已经使用 Windows 上的 Visual Studio IDE、Yeoman generator 或 Linux/macOS 上的.NET Core CLI 创建了一个 web API 项目——应用结构将是相同的,包括文件、文件夹和配置设置。让我们详细了解应用结构:

| 文件和文件夹 | 目的 |
| /Controllers文件夹 | 这就是我们放置处理请求的Controller类的地方 |
| Program.cs文件 | 这是使用Main方法执行应用的入口点。 |
| Startup.cs文件 | 这是设置配置和连接应用将使用的服务所必需的 |
| .csproj文件 | 这是一个 C#项目文件(.csproj,它更轻量级、更健壮、更易于使用。 |
| Appsettings.json文件 | 这是基于键/值的配置设置文件 |
| Web.config文件 | 这严格用于 IIS 配置,并在 IIS 上运行时调用应用 |

What about Model classes?
Domain models or POCO classes can be added in the Models folder, or we can create a separate class library for models. The preceding project structure is a good starting point; on larger projects, we can split the controllers, repositories, and domain models into separate class libraries for maintainability.

Asp.Net Core Web API project structure

让我们详细了解每个文件的内容。

Program.cs

从根本上说,ASP.NET Core web 应用是控制台应用——知道这一点不奇怪吗?就像每个控制台应用都需要Main()才能执行一样,.NET Core 应用也有包含Main()方法的Program.cs文件。

ASP.NET Core 是建立在.NET Core 之上的,这就是我们刚才创建的应用结构中有program.cs的原因。签出此文件:

    using Microsoft.AspNetCore; 
    using Microsoft.AspNetCore.Hosting; 

    namespace MyFirstCoreAPI 
    { 
      public class Program 
      { 
        public static void Main(string[] args) 
        { 
            BuildWebHost(args).Run(); 
        } 

        public static IWebHost BuildWebHost(string[] args) => 
          WebHost.CreateDefaultBuilder(args) 
          .UseStartup<Startup>() 
          .Build(); 
      } 
    } 

您可以按如下方式分解前面的代码:

  • 就像任何.NET 控制台应用都需要Main()方法来开始执行一样,.NET Core 运行时将通过调用Main()方法来启动应用。
  • 需要使用IWebHost接口将这些控制台应用作为 web 应用(MVC 或 web API)运行。
  • WebHost类有CreateDefaultBuilder,它预先配置了运行应用的默认值。这意味着 web 应用需要一个 web 服务器(Kestrel)、一个 IIS 集成设置、用于读取各种值的配置文件以及Content Root文件夹。所有这些需求都在该方法中预先配置。
  • 虽然我们可以实现一个自定义方法,但是提供的默认实现足以启动。通过此链接了解 ASP.NET Core GitHub 回购协议中的CreateDefaultBuilder代码https://github.com/aspnet/MetaPackages/blob/rel/2.0.0-preview1/src/Microsoft.AspNetCore/WebHost.cs 。让我们从 GitHub repo 中了解这些方法:
    • UseKestrel ()是一种扩展方法,其作用类似于运行 ASP.NET Core 应用的 web 服务器。它基于 libuv。Kestrel 通常被称为负责运行应用的内部 web 服务器。它重量轻、速度快、跨平台。在有更多关于这方面的内容 https://github.com/aspnet/KestrelHttpServer.
    • UseContentRoot()是一种扩展方法,用于指定 web 主机要使用的内容根目录。这通常是当前的工作目录;可以将其配置为指向另一个文件夹,其中包含运行应用所需的所有文件。
    • 当使用 ASP.NET Core 应用时,IIS 被视为一个外部 web 服务器,暴露于 internet 以接收请求。UseIISIntegration()配置服务器在AspNetCoreModule之后运行时应监听的端口和基本路径。
    • ConfigureAppConfiguration读取配置文件,并添加UserSecrets和环境变量。
    • ConfigureLogging设置控制台日志记录和调试窗口。
    • UseStartup(StartUp)设置类来配置各种服务,如点击请求-响应管道等。Startup 是一个没有任何基类的简单类,有两个方法,ConfigurationConfigure
    • Build()构建并准备WebHostBuilder运行 web 应用。
    • Run();在 Kestrel web 服务器下运行 ASP.NET Core web 应用。

Startup.cs

在 ASP.NET Core 中,webHostBuilderMain()运行时调用 Startup 类,所有应用都需要该类。

它是任何请求或响应返回时的第一行执行。它执行一系列操作,如提供依赖项注入、添加和使用各种中间件组件等。

Startup.cs类必须定义ConfigureConfigureServices方法;当主机开始运行时,将调用它们。

配置方法

ASP.NET Core 是完全模块化的,也就是说,如果您真的需要组件,您可以添加它们。通过这种方法,web 应用在部署和性能方面变得轻量级。

Startup.cs类的Configure方法的主要目的是配置 HTTP 请求管道。在下面的Startup.cs代码示例中,您可以看到IApplicationBuilder使用扩展方法来配置管道。

UseMvc扩展方法将路由中间件添加到请求管道中,并将 MVC 配置为默认处理程序,如下所示:

    app.UseMvc(); 

我们已经创建了一个简单的 web API,为此,我们使用 MVC。您可能想知道,当我们使用 web API 时,为什么要使用 MVC?原因是 ASP.NET Core 具有统一的 MVC 和 web API。

IApplicationBuilder定义一个类,该类提供配置应用请求管道的机制。您可以将自定义管道配置构建为中间件,并使用扩展方法将其添加到IApplicationBuilder第 6 章中间件和过滤器专门针对此。

默认情况下,IApplicationBuilderIHostingEnvironmentILoggerFactory由服务器在 web API 或 MVC 项目中注入:

    using Microsoft.AspNetCore.Builder; 
    using Microsoft.AspNetCore.Hosting; 
    using Microsoft.Extensions.DependencyInjection; 

    namespace MyFirstCoreAPI 
    { 
      public class Startup 
      { 
        // This method gets called by the runtime. Use this method to add services to
          the container. 
        public void ConfigureServices(IServiceCollection services) 
        { 
          services.AddMvc(); 
        } 

        // This method gets called by the runtime. Use this method to configure the
          HTTP request pipeline. 
        public void Configure(IApplicationBuilder app, IHostingEnvironment env) 
        { 
          app.UseMvc(); 
        } 
      } 
    } 

在下一节中,我们将详细讨论 ASP.NET Core 请求管道处理以及中间件的角色。

ConfigureServices 方法

ConfigureServices方法使用依赖项注入DI设置应用运行的所有可用服务。将服务列表添加到IServiceCollection实例中,在Configure之前调用。

运行时首先调用此方法的原因是,在准备好请求管道处理之前,需要添加一些特性,如 MVC、标识、实体框架等。因此,在前面的代码中,我们看到了services.AddMvc()

ConfigureServices方法有助于在 ASP.NET Core 应用中实现依赖项注入模式。让我们看一个简单的例子,假设我们通过实现INewsOfDay接口来编写NewsOfDay类。我想使用 DI 模式,以便任何其他类都可以轻松地注入此接口以获得当天的报价。

    public void ConfigureServices(IServiceCollection services) 
    { 
      // using DI to inject the interface 
      services.AddSingleton<INewsOfDay, NewsOfDay>(); 
    } 

ASP.NET Core 默认支持依赖注入;我们不需要使用任何第三方 DI 容器,如 Unity、StructureMap、Autofac 等。但是,如果开发人员觉得需要使用其他 DI 容器,他们可以覆盖默认实现。

*csproj 先生

任何.Net 开发人员都会熟悉.Net 项目中的*.csproj文件;在 ASP.NET Core 应用中,我们确实可以找到此文件。与传统的.NET 应用相比,它是一个非常精简的版本。

在 ASP.NET Core 的初始版本中,基于 JSON 的project.json文件用于包管理,但为了与其他.NET 应用保持同步,并与 MSBUILD 系统配合良好,该文件被删除。

The .csproj file can be now edited in Visual Studio 2017 IDE without reloading the entire project. Right-click on the project file, click on Edit to make changes.

我们来看看 ASP.NET Core 项目的*.csproj文件的内容:

    <Project Sdk="Microsoft.NET.Sdk.Web"> 
     <PropertyGroup> 
       <TargetFramework>netcoreapp2.0</TargetFramework> 
       <UserSecretsId>aspnet-MyFirstCoreAPI-D0B356AB-BC35-4D73-9576-
         997BC358BEE9</UserSecretsId> 
     </PropertyGroup> 

     <ItemGroup> 
       <Folder Include="wwwroot\" /> 
     </ItemGroup> 

     <ItemGroup> 
       <PackageReference Include="Microsoft.AspNetCore.All"
         Version="2.0.0-preview2-final" /> 
     </ItemGroup> 

     <ItemGroup> 
       <DotNetCliToolReference
         Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools"
         Version="2.0.0-preview2-final" /> 
     </ItemGroup> 
    </Project> 

您可以将*.csproj文件分解如下:

  • TargetFramework标签指向netcoreapp2.0。这是.NET 标准 2.0 的 ASP.NET Core 名称。我建议您访问此链接以了解有关.NET 标准 2.0 的更多信息:https://blogs.msdn.microsoft.com/dotnet/2016/09/26/introducing-net-standard/
  • 指示Folder在构建过程中包含wwwroot目录。
  • PackageReference是将包含在项目中的 NuGet 软件包或任何自定义库。Microsoft.AspNetCore.All元包引用了所有 ASP.NET Core 包,只有一个版本号。只有更改此版本,才能更新任何新版本。虽然您可以将它们作为单独的包添加,但建议使用Microsoft.AspNetCore.All

为什么选择 Microsoft.AspNetCore.All 元软件包?

.NETCore2.0 带来了一个称为运行时存储的新功能。从本质上说,这让我们可以在中央位置的机器上预安装软件包,这样我们就不必将它们包含在单个应用的发布输出中。

ASP.NET Core 请求处理

ASP.NET(MVC 和 ASPX)的所有请求处理都依赖于system.web.dll。它曾经完成所有繁重的浏览器-服务器通信工作,并且与 IIS 紧密耦合。

NET Core 是通过完全删除system.web.dll来实现跨平台设计的;这导致了一种完全可插拔的不同请求处理技术。这种删除也有助于统一 ASP.NET 中的 MVC 和 web API 堆栈。

ASP.NET Core 没有区分 MVC 和 web API,因此请求处理现在将很常见。在下一节中,我们将了解有关统一的更多信息。

下图显示了 ASP.NET Core 请求处理的概述:

ASP.NET Core request processing

让我们一步一步地了解 ASP.NET Core 请求处理。

各种客户端,如 web 应用(浏览器)、本机应用和桌面应用,都会访问托管在外部 web 服务器(如 IIS/Nginx)上的 web API。有趣的是,IIS 现在是外部 web 服务器的一部分,因为它不运行 ASP.NET Core 应用。它只充当通过 internet 公开的托管 web 服务器。Nginx 是 Linux 机器上 IIS 的对应物。

IIS/Nginx 调用计算机上安装的 dotnet 运行时来启动处理请求。现在,该处理属于.NET Core。为此,web.config文件仍存在于 ASP.NET Core 应用中。

Dotnet 运行时调用 kestrelweb 服务器(内部 web 服务器)来运行应用。Kestrel 是一个基于 libuv 的开源轻量级跨平台 web 服务器;这是使 ASP.NET Core 应用真正跨平台的重要步骤之一。

Kestrel 然后通过应用中的Main ()方法启动应用。请记住,ASP.NET Core 应用是控制台应用。Program.cs中的Main ()方法是.NET Core 应用的起点。

然后,Main ()方法构建并运行webHostBuilder。然后,请求被推送到Startup.cs类的Configure方法;HTTP 请求管道在此处配置。我们之前创建的默认 web API 模板项目只在管道中添加了app.UseMvc ()。我们可以以中间件的形式定制 HTTP 请求管道处理逻辑(详见第 6 章中间件和过滤器

  1. MVC 中间件构建在通用路由中间件之上,用于处理请求。此时,请求被处理,并发送到相应的控制器进行处理。
  2. 当请求处理完成时,响应将以相反的顺序通过相同的管道发送。如果请求无效,定制中间件可以帮助我们返回响应。

ASP.NET Core 请求管道处理完全可插拔;Configure方法应该只包括所需的中间件,而不是 ASP.NET web 堆栈中存在的繁重的system.web.dll

ASP.NET Core 中统一的 MVC 和 Web API

主要的架构演进之一是在 ASP.NET Core 中统一 MVC 和 web API。ASP.NET Core 中的 MVC 和 web API 控制器之间没有区别。

在以前的 ASP.NET 堆栈中,MVC 和 web API 中的控制器从各自的基本控制器派生,如下所示:

    // ASP.NET MVC 5 Controller 
    public class HomeController : Controller 
    {  
      // Action Methods 
    } 
    // ASP.NET MVC 5 Controller  
    public class ValuesController : ApiController 
    {  
      // API Action Methods 
    } 

MVC and Web API unification

ASP.NET MVC 4/5 和 web API 2 都具有控制器、操作、过滤器、模型绑定、路由和属性,但由于以下原因,它们具有不同的代码基:

  • ASP.NET MVC(4/5)依赖于system.web.dll,该system.web.dll绑定到 IIS 进行托管。如果没有 IIS,您将托管一个 MVC 应用。
  • ASP.NET Web API 设计为自托管;它不依赖 IIS 托管。

ASP.NET Core 的设计思想之一是使其自托管,并独立于 IIS 进行托管。通过这种方式,system.web.dll被删除,因此它可以在没有 IIS 的情况下托管。这导致了 MVC 和 WebAPI 的合并,形成了一个单一的代码库。

在 ASP.NET Core 中,MVC 和 web API 共享同一个基本控制器,因此,MVC 和 web API 之间没有实现差异。

运行 ASP.NET Core Web API 项目

我们的 ASP.NET Core Web API 是由 Visual Studio 2017 IDE 为 Windows 环境和 Yeoman generator 为 Linux/macOS 创建的。我们将使用 IIS Express 或 Kestrel 服务器运行该应用。

在运行应用之前,让我们进一步了解Values控制器(默认创建)。在Controllers文件夹下有一个名为ValuesController.cs的 C#类文件:

    using System.Collections.Generic; 
    using Microsoft.AspNetCore.Mvc; 

    namespace MyFirstCoreAPI.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) 
        { 
        } 
      } 
    } 

您可以按如下方式分解前面的代码:

  • Web API 基于模型、值和控制器概念。ValuesController.cs是客户端通过 HTTP 访问的 C#类。
  • 它派生自控制器基类,使任何类都成为 MVC 或 WebAPI 控制器
  • [Route("api/[controller]")]定义路由策略。基于此配置可以访问控制器
  • ValuesController提供了以下可通过 HTTP 访问的方法:

| 方法名称 | HTTP 动词 | 备注 |
| Get() | HttpGet | 返回字符串的 IEnumerable |
| Get(int id) | HttpGet | 返回基于值的字符串 |
| Post([FromBody]string value) | httpost | 使用 POST 插入字符串 |
| public void Put(int id, [FromBody]string value) | HttpPut | 使用 PUT 根据 Id 更新字符串 |
| public void Delete(int id) | HttpDelete | 使用 DELETE 删除字符串记录 |

在 VisualStudioIDE 中运行应用

在使用 Visual Studio 2017 IDE 时,我们有两种运行 ASP.NET Core 应用的方法:

  • 使用 IIS Express:如果您想使用 IIS Express。按F5开始运行应用;它将打开选定的 web 浏览器。
  • 使用红隼服务器:如果您想运行红隼服务器,请从运行选项中选择MyFirstCoreAPI。按F5开始运行应用;它将打开所选的 web 浏览器以及控制台窗口。

在这些环境中,我们将使用命令行选项来运行应用。他们使用 Kestrel 服务器来运行应用。

在 Linux/macOS 上运行 ASP.NET Core Web API

从 Linux/macOS 机器的项目根目录中打开 console/shell,如下所示:

dotnet run 

上述命令编译、构建和运行应用;它还开始监听http://localhost:5000上的请求。打开任何浏览器,粘贴 URLhttp://localhost:5000/api/values以查看 web API 从值控制器返回响应。

您将看到应用正在运行,并在浏览器中显示响应,如以下屏幕截图所示:

Response after Web API on browser

我们可以使用 Postman 发送请求并接收来自所示 web API 的响应。端口是自动生成的,并且会根据机器的不同而有所不同。

Postman 是一个用于使用 API 各种活动的工具——我们将使用它进行 API 测试。您可以从下载 https://www.getpostman.com/

在 ASP.NET Core Web API 项目中,响应格式化程序默认设置为 JSON。但是,我们可以根据需要定制。

How about debugging using Visual Studio Code? If you need a similar experience for debugging .NET Core (C#) in Visual Studio Code,
Omnisharp--C# extension for .NET core--should be installed. Refer to this link to set up debugging: https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger.md

进一步阅读

有关的详细信息,请参阅以下链接。净核心:

有关 ASP.NET Core 的更多信息,请参阅以下内容:

总结

在本章中,我们已经介绍了很多方面。我们首先介绍了 MVC,然后介绍了 ASP.NET Web API 在过去十年中是如何成熟的。有了背景知识,我们学会了如何使用.NET Core 和跨平台框架的强大功能。

我们了解 ASP.NET Core 及其架构,并使用.NET Core SDK 设置系统。我们还使用 VisualStudio 工具和 Yeoman 生成器创建了一个 web API。我们详细了解了 ASP.NET Core 请求处理以及将 web API 和 MVC 统一到单个代码库中的过程。

在下一章中,您将了解有关模型、视图和控制器的更多信息。

四、控制器、动作和模型

服务的入口点和实体是控制器。虽然处理程序是 ASP.NET Core 管道中的初始类之一,但一旦请求通过 ASP.NET 并找到合适的路由,它将被定向到控制器。

现在,您可以控制要作为响应发送的数据。控制器可以包含许多方法。尽管这些可能是公共方法,但并非所有方法都可用。在这些方法上启用 HTTP 操作将使这些方法变成操作。

在本章中,您将更好地了解控制器以及它们如何与 ASP.NET 管道相结合。我们将创建一些控制器以及这些控制器的操作。

在本章中,我们将介绍以下主题:

  • 控制器简介
  • 行动
  • 使用模型创建控制器

控制器简介

当您向项目中添加新控制器时,ASP.NET 会自动为您加载该控制器并使其准备好使用。以下是一些您可能想知道的指针,以便您不会陷入困境,或者如果您想创建一个新项目并将所有控制器都放在其中:

  • 您的控制器需要以单词Controller结尾。
  • 确保你的班级是public;不用说,接口和抽象类将无法工作。从 Microsoft 控制器类继承它们。
  • 不同名称空间中不能有相同的控制器名称。ASP.NET 允许同一控制器使用多个名称空间,但不会解析两个控制器。最佳做法是为控制器指定唯一的名称。

行动

web API 有很多动作,其中一些动作在第 2 章理解 HTTP 和 REST中介绍,并附有示例。作为复习,我们将再次讨论它们,因为我们希望在创建控制器时使用这些操作。Action属性将用于装饰一个方法。

每一个行动都应该从消费者的角度来考虑;例如,对于Post,客户机正在发布一些内容。

如果我们已经创建了ShoesController,那么其路径如下:

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

邮递

当我们想要创建一些东西时,使用此操作。邮件正文将包含需要保存到数据存储中的数据:

    [Route("")] 
    [HttpPost]  
    public IActionResult CreateShoes([FromBody] ShoeModel model) 

第一行是路由,应该始终声明路由。它们让正在阅读代码或调试代码的人更好地理解正在发生的事情以及流程是如何进行的。

在第二行中,我们陈述了Action属性;在这种情况下,它是Post,您不需要一直设置它。宣布行动是一种良好的做法。

收到

Get用于检索数据。在大多数情况下,Get未明确说明:

    [Route("")] 
    [HttpGet]  
    public IHttpActionResult GetShoes() 

这可以声明如下:

    [Route("")] 
    public IHttpActionResult Get() 

注意动作的省略。

Put用于更新数据或创建一些不存在的数据:

    [Route("{id}")] 
    [HttpPut]  
    public IHttpActionResult Update(ShowModel model) 

您会注意到 ID 是路由的一部分,它意味着调用者知道他们想要更新哪个实体。我们所做的就是创建名为Update的方法;名称可以是您想要的名称。

色斑

PatchPut类似,区别在于您发送的只是改变了增量的数据,而不是整个模型:

    [Route("{id}")] 
    [HttpPatch]  
    public IHttpActionResult PatchUpdate(ShowModel model) 

删去

Delete用于删除数据。您所需要的只是 ID:

    [Route("{id}")] 
    [HttpDelete]  
    public IHttpActionResult Delete(string id) 

这些是我们将在控制器中使用的操作。我已经将Actions属性与控制器分开进行了研究,因此我们可以在不担心实现的情况下对其进行一些关注。

控制器

我启动了 VS2017 社区版并创建了一个新的 web 项目。请注意,您可以选择要针对哪个模板。我选择了 ASP.NET Core Web 应用。创建一个名为Puhoi的新项目,这是新西兰的一个小镇,生产一些乳制品。我的目标是为他们的一些产品创建一个控制器。创建一个有形的、真实的工作示例是很好的。我倾向于远离书籍控制器或产品控制器之类的东西。

创建项目后,系统将提示您选择模板;通过选择 ASP.NET Core 2.0,选择以下屏幕截图中突出显示的模板:

我创建了一个新的控制器,保存在Controllers文件夹中:

另外,记下控制器的路线;它不包含控制器的名称:

让我们谈谈我们想要创造的东西和一点关于 Puhoi 奶酪的知识。他们有一些产品,如牛奶、奶酪和酸奶。

像任何公司一样,您希望列出所有产品、添加新产品和删除产品。因此,如果有一个前端,一个网站,或一个应用,那么他们会将自己连接到这个 API 中以获取相关信息。让我们开始构建一些逻辑。我们不会为此创建后端,因为它超出了本章的范围。

让我们构建 stores controller,它将列出一些顶级产品,例如百货商店。然后,让我们深入并创建一个子管道控制器,例如列出各种奶酪的东西。如果你不喜欢奶酪,你就不会喜欢这一章;我事先道歉。

模型

我们在一个单独的项目中创建模型,因为我们不想用一切污染 API 项目,它不是一个垃圾场。当我们创建类时,一个类有一个职责,当它们有多个职责时,我们就创建一个新类。项目应该包含具有并共享相同职责的类。我们还创建了一个BaseModel类,该类具有一些我们希望所有模型都具有并且应该具有的公共属性,因为它们是相关的。

    public  class BaseModel  
    { 
      public Guid Id { get; set; } 
    } 

创建模型项目后,我们有一个包含模型类和我们的BaseModel类的文件夹:

StoreModel类继承BaseModel类,属性表示模型属性:

现在我们有了我们的模型,让我们将其添加到控制器并开始。别忘了将 web API 中的引用添加到Models项目中。

稍后,您还将看到我们如何分别添加模型。我已经重构了我们的控制器以使用存储模型,现在看起来是这样的:

总之,我们在Get上返回一个或多个StoreModel,并且PostPut方法将我们的模型作为输入参数。

现在我们已经准备好了,剩下的是我们应该如何返回Get的数据并传输PutPost要存储的数据。

我们可以创建内存中的数据存储并将其用作持久存储。但这是有点黑客的,你并不是真的在生产系统中这样做。也许我们应该创建一个数据库,并将数据从 API 一直传递到数据库。这是一个很大的努力,这一章是关于控制器。因此,我们可能会创建一个到数据存储的接口,并将数据从 API 传递到接口,当您准备实现 API 时,这应该是一个很好的模式。

为了创建一个分层良好的架构并隐藏一些将 API 连接到数据层的代码,我们将创建一个新的类库项目并向该库添加一些类。这些类将引导数据进出数据存储接口。

Minimum-layered web API architecture

我已经用最少数量的组件制作了通用图。如果需要,可以创建更多组件。在应用中考虑边界点很重要。您的 API 项目不应该引用数据项目,数据项目也不应该引用 API 项目。

商业

使用应用的名称创建一个新的类库项目,然后创建.Business,如下所示:

还有它的文件夹结构。

这是IDataStoreManager的接口:

    namespace Puhoi.Business.Interfaces 
    { 
      public interface IStoreManager 
      { 
        HttpModelResult Add(BaseModel model); 
        HttpModelResult Update(BaseModel model, Guid id); 
        HttpModelResult Get(Guid id); 
        HttpModelResult Delete(Guid id); 
        HttpModelResult GetAll(); 
      } 
    } 

您需要对模型项目的引用,并添加对System.Web的引用。

请注意,我们正在返回HttpStatusCode。有人可能会争论为什么业务引用System.Web,在过去,这些可能是一个有效的论点。与我们的设计图一样,Business是 web API 和数据层之间的一层。它必须了解这两个方面,它是一个业务层,但它是一个 WebAPI 业务层。

HttpModelResult类如下所示:

我们现在创建一个实现IStoreManager的具体类。但是,我们需要对其进行注册。许多人会使用依赖项注入来创建对象,并使用像 Autofac 这样的库。

有了 ASP.NET Core,这是内置的。

依赖注入

以下是我注册StoreManager课程的方式:

我已经导航回我们的 PuhoiAPI 项目,并在Startup.cs类中添加了以下代码:

可用的选项如下所示:

  • AddTransient
  • AddScoped
  • AddInstance
  • AddSingleton

如果您是依赖注入DI的新手,以下内容应该能让您更好地理解:

  • 瞬态:每次需要时都会创建一个新对象。这最适合于无状态对象。
  • 范围:为每个请求创建一个新对象。
  • 单例:与单例模式类似。第一次需要该对象时,将创建一个新对象,并且该对象的每个后续依赖项都将使用该对象。
  • 实例:最好的描述方式是它的行为类似于单例,只是单例是延迟加载的。

既然管理器已经包含在我们的 DI 中,那么让我们将其合并到控制器中。

通过身份证

StoreController类被注入IStoreManager接口,作为其构造函数中的依赖项:

对于控制器上的Get,我们将对其进行重构,因此不要过于关注实现:

注意路线;我们使用的是 ID,我们的 ID 是Guid,这是我们的唯一标识符。然后,我们有storemanager,我们将 ID 传递给我们的商店经理,并从控制器返回一个模型。

相当容易;这在实际通话中是什么样子的,我们如何称呼它?

我创建了一个简单的实现,它将返回一个包含所请求内容的模型。

小提琴手的要求如下:

小提琴手的反应如下:

我们的状态代码是 200,这是成功的。结果非常好,因为我们有数据流。考虑没有找到结果的场景。返回一个空模型并不理想,因为我们将返回一个带有 200 的空模型。我们希望返回更直观的结果。

如果我们将实现更改为以下内容,从管理器检查模型并返回 null,我们的响应为 204,这不会告诉使用者特定资源不存在:

使用IAction结果类型更灵活,并提供所需的结果:

我们可以从经理那里测试我们的模型,并注意我们的回报。有三种不同的回报。一个是模型的OK。第二个是NotFound;如果经理没有找到我们查询的 ID,我们可以返回未找到状态。这对消费者来说更直观,并且检查结果代码比解析数据便宜得多。

最后,我们假设我们收到的请求是错误的,并返回一个错误的请求。

需要注意的是,这只是一个示例和一种模式,说明了如何构造控制器;您可以在 switch 语句中添加更多事例。

您也可以将BadRequest更改为更智能一点,而不仅仅是返回BadRequest;这里的重点是从消费者的角度展示IActionResult的用途以及它是如何凝胶的。

现在,您可以在 Fiddler 中看到所需的结果:

这是在StoreManager类的Get方法中实现逻辑的方式:

我们通过 ID 向DataStore请求对象,如果结果为空,则返回Not Found。如果有一个对象从数据存储返回,那么我们使用映射器在数据库中的对象和 API 公开的对象之间进行映射。本质上,我们在dtomodel之间映射。然后,我们将状态设置为OK并等待model结果。

映射

我们使用 AutoMapper 在模型Dto之间进行映射,反之亦然。下图显示了AutoMapper如何融入我们的解决方案:

为 API 项目和将进行映射的项目添加对AutoMapper的 NuGet 引用。

在 API 项目中,我创建了一个新类来设置映射。此类继承自AutoMappers概要文件类。

这是最简单的设置:

它说你应该在dto作为源和model作为目标之间创建一个映射。然而,我们知道这并不是那么简单。结果是这样的:

对于dto,我们忽略StoreIdUId,对于model,我们忽略Id。然后,映射完成后,将UIddto映射到Idmodel映射。

Startup.cs中通过以下方式完成管道内的设置:

为映射器配置创建成员变量。然后,在Startup构造函数中,我们设置了一个新的MapperConfiguration变量,并添加包含映射的概要文件。

我们快搞定了。在ConfigureService中,我们需要将其添加到服务中:

IMapper被创建为单例,因为完成的映射不会更改,并且它们不包含任何状态。

邮递

从控制器开始,实现 Post,如果对象被创建,或者如果它返回内部管理器发送给我们的内容,则返回 201。

我们没有特殊的路由,但是我们声明默认路由是清除的。用HttpPost装饰方法。与Get一样,我们返回IActionResult。从请求主体检索模型。如果您查看此方法中的代码,我们会将所有工作委托给经理。然后,我们得到一个结果;如果管理器发送给我们HttpStatusCode,我们知道对象已经创建,我们返回 201,创建时带有新创建资源的位置。任何其他结果被翻译回HttpStatusCodeResult。这是我对 Fiddler 的研究结果:

请求主体 JSON 如下所示:

服务器的响应如下所示:

经理的工作是从控制器获取模型并将其传递到此数据存储:

然而,它的责任比这多一点。它需要告诉管理器对象是否已创建,如果未创建,则需要指定问题所在。在数据存储可以插入对象之前,管理器会检查存储是否具有此对象。请注意,检查现有对象是否由管理器负责,而不是由数据存储负责。顾名思义,这是一家商店。如果对象存在,则管理器会将冲突返回给控制器。这是管理器中的 else 块:

我们利用AutoMappermodeldto之间切换并返回到模型。您必须在AutoMapperProfileConfiguration中进行更改,才能使其正常工作:

我们使用Put更新商店或创建新商店。Put的签名与Post不同:

我们在控制器类的签名中有 ID 和模型。路由具有 ID,并且模型位于请求主体中。您会注意到Post没有 ID,我们要求店长更新我们的型号;如果我们从商店管理器中获得一个已创建的资源,那么我们将发布新资源的这个位置。任何其他状态,包括OK,返回为HttpStatusCodeResult

我已经对我们的商店经理做了一些重构,就像人们通常做的那样。如果找不到提供给我们的 ID,那么我们将模型添加到数据存储中。在正常流程中,更新模型并返回 200。

我把这件事告诉了费德勒;注意动作Put和主体中的模型。

在此之前的步骤是使用Post创建模型,使用Put更新模型,然后在资源上调用 get 以检查是否已执行更新。

删去

Delete非常直截了当。我们将继续执行第 1 章微服务和面向服务架构简介中设定的原则,围绕其余删除原则。当我们第一次删除一个表示时,我们可以返回 200,但是当我们发出相同的请求时,表示不再存在,所以我们应该返回 404。让我们看看这个代码:

要求商店经理删除具有特定 ID 的对象,然后我们返回从经理那里得到的任何东西。让我们看看经理:

这也很简单;根据我们从数据存储中得到的信息,我们返回 200 或未找到。

我将不显示 Fiddler 请求和对不同流的响应,因为我觉得这对于我们在本章中介绍的内容非常基本,直到现在。这只是删除请求:

请注意,我们的操作是Delete

盖特尔

我们的GetAll操作看起来比Delete简单:

我们向我们的经理索要所有门店,并将其连同 200:

我们在 manager 中所做的就是,向数据存储请求所有 DTO,然后将它们映射到一个模型,并将它们作为IEnumerable返回。就在那之前,我们把状态设为 200。

路线是什么样的?它看起来类似于GetById,只是不需要设置 ID。

总结

在本章中,我们开发了一个完整的 CRUD 端点,并研究了 ASP.NETCore2.0 的一些新特性,如内置依赖项注入。

我们探索了HttpGetHttpPostHttpPutHttpDelete动作,以及一些基本的路由。

我们建立了一个干净的模式来分解类的职责,并使其更容易扩展给定的功能。

对象是松散耦合的,这使得它们更容易测试;这是通过内置依赖项注入实现的。我们还使用 Fiddler 来演示我们的 API 是如何工作的。

在下一章中,我们将深入讨论路由机制、路由生成器、属性路由、约束等等。

五、实现路由

我们使用URL统一资源定位器)访问 web 上的代码资源。例如,当您看到对www.dummysite.com/pages/profile.aspx的请求时,很容易推断profile.aspx实际存在于网站 dummysite.com 上的 pages 文件夹中。

请注意,在我们的示例中,URL 和物理文件有直接的关系——当 web 服务器接收到此文件的请求时,执行代码,并返回响应以在浏览器上显示。

当使用基于 MVC 的框架(如 ASP.NETCore)时,URL 会使用一种称为路由的方法映射到控制器类及其操作方法。

在本章中,我们将研究以下主题:

  • 引入路由
  • 路由中间件
  • 路线生成器
  • 基于约定和基于模板的路由
  • 基于属性的路由
  • 路线约束
  • 链接生成
  • 路由的最佳实践

引入路由

在第四章控制器、动作和模型中我们学习了很多关于控制器和动作的知识。任何 ASP.NET Web API 项目都将有一个或多个控制器,这些控制器具有许多基于 HTTP 谓词(如 GET、POST、PUT 和 DELETE)的操作方法。

当我们在第 3 章ASP.NET Core Web API 剖析中创建一个基本的 ASP.NET Core Web API 并运行该应用时,我们在浏览器中看到 URL 为http://localhost:5000/api/values——它显示了来自值控制器的 JSON 响应。

这里出现了如下几个问题:

  • 项目如何知道应该加载特定的控制器和操作方法?
  • 如果我在真实场景中有许多控制器和动作方法会怎么样?我们如何指向特定的控制器?
  • 正确服务 HTTP 请求的机制是什么?

将传入 HTTP 请求映射到其相应控制器的操作方法的机制称为路由。它是 ASP.NET Core MVC/Web API 的关键组件之一。如果没有路由机制,ASP.NET(MVC/Web API)将无法运行应用和服务请求。路由解决了上述所有问题。

路由中间件

在 ASP.NET Core 世界中,每个 HTTP 请求和响应都必须通过各种中间件。Startup类的Configure方法配置处理请求的管道,并对请求采取适当的操作。

ASP.NET Core 提供路由中间件来执行将请求映射到相应控制器和操作方法的任务。让我们了解一下这个中间件。

创建一个空的 ASP.NET Core 项目,通过手动编辑*.csproj或 NuGet 将Microsoft.AspNetCore.All添加到其中。在依赖项中添加以下包详细信息,以便恢复并准备使用:

    > " Microsoft.AspNetCore.All ": "2.0.0-preview2-final" 

打开Startup类添加以下代码,查看路由中间件的运行情况:

    using Microsoft.AspNetCore.Builder; 
    using Microsoft.AspNetCore.Hosting; 
    using Microsoft.AspNetCore.Http; 
    using Microsoft.Extensions.DependencyInjection; 
    using Microsoft.AspNetCore.Routing; 

    namespace BasicRoutes  
    { 
      public class Startup 
      {         
        public void ConfigureServices(IServiceCollection services) 
        { 
           // Adding Router Middleware 
           services.AddRouting(); 
        }  

        public void Configure(IApplicationBuilder app, 
          IHostingEnvironment env, ILoggerFactory loggerFactory) 
        { 
          //HTTP pipeline now handles routing 
          app.UseRouter(new RouteHandler( 
            context => context.Response.WriteAsync("Mastering Web API!!") 
          )); 
        } 
      } 
    } 

您可以按如下方式分解前面的代码:

  • Services.AddRouting()增加要使用的路由中间件。
  • app.UseRouter处理 HTTP 请求的路由。需要RouteHandler来处理请求。这里我们只是用一个字符串来写响应。

对于传入的任何请求,管道都需要有人来处理。这是由RouteHandler完成的;每个路由都应该有一个处理程序。

运行应用时(按F5,浏览器显示字符串 Mastering Web API,如本屏幕截图所示:

Basic example of routing middleware

这是 ASP.NET Core 中最基本的路由示例。由于管道仅对其进行路由,因此使用任何 URL 访问应用都将给出相同的响应。

RouteBuilder 类

RouteBuilder类帮助我们构建定制路线,并在请求到达时处理它们。MVC 还使用这个类来构建控制器的默认路由机制 actions。

在上一个示例中,我们创建了一个服务于任何路由的基本路由。现在,我们将使用不同的 HTTP 谓词(如 GET 和 POST)创建自定义路由。

Startup.csConfigure方法中为自定义路由生成器复制以下代码:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env,
    ILoggerFactory loggerFactory) 
    { 
      var routes = new RouteBuilder(app) 
      .MapGet("greeting", context => context.Response.WriteAsync("Good morning!!
        Packt readers.")) 
      .MapGet("review/{msg}", context => context.Response.WriteAsync(
        $"This book is , {context.GetRouteValue("msg")}")) 
      .MapPost("packtpost", context => context.Response.WriteAsync(
        "Glad you did Post !")) 
      .Build(); 
      app.UseRouter(routes); 
    } 

您可以按如下方式分解前面的代码:

  • 实例化一个新的RouteBuilder类。
  • 我们使用MapGet设置问候路径,以使用 HTTP 动词 GET 处理客户端请求。
  • 我们使用MapGet设置审核路径,用RouteDataRouteValue处理客户请求。请求处理程序委托读取消息。
  • 使用MapPost,我们得到处理客户端 POST 请求的packtpost路径。

Route Builder using MapGet

地图路线

MapRoute是一种扩展方法,使用指定的名称和模板向IRouteBuilder添加路由。需要增加一个DefaultHandler进行进路办理。

下面的代码显示了如何定义要与MapRoute一起使用的defaultHandler

    var routeHandler = new RouteHandler(context =>  
    { 
      var data = context.GetRouteData().Values; 
      return context.Response.WriteAsync("Controller Name is " +
        data["controller"].ToString()); 
    }); 

    var routes = new RouteBuilder(app, routeHandler) 
    .MapRoute("packt", "{controller}/{action}") 
    .Build(); 
    app.UseRouter(routes); 

ASP.NET Core Web API 和路由

到目前为止,我们还看到了路由的基础知识,没有 MVC 或 web API,而是以中间件路由、RouteBuilder 和 MapRoute 的形式出现。必须理解这些概念是如何协同工作的。

当我们创建一个 ASP.NET Core 应用作为 web API 时,需要了解一些与路由相关的功能。

第 3 章ASP.NET Core Web API 剖析中,我们创建了一个简单的 Web API 项目;查看Startup类的ConfigureConfigureServices方法,只添加了 MVC 中间件和服务。没有提到路由中间件。

现在出现的问题是 WebAPI 项目如何完成所有需要的路由。答案在于在Configure方法中添加的 MVC 中间件app.UseMvc()

UseMvc()是微软 ASP.NET Core 团队编写的 MVC 和 web API 项目的中间件。该中间件通过相同的代码库确认 MVC 和 WebAPI 的工作。

以下代码是 GitHub 上的 ASP.NET MVC 开源项目(的一部分 https://github.com/aspnet/Mvc

    public static IApplicationBuilder UseMvc( 
      this IApplicationBuilder app, 
      Action<IRouteBuilder> configureRoutes) 
      { 
        if (app == null) 
        { 
          throw new ArgumentNullException(nameof(app)); 
        } 
        if (configureRoutes == null) 
        { 
          throw new ArgumentNullException(nameof(configureRoutes)); 
        } 
        if (app.ApplicationServices.GetService(typeof(MvcMarkerService))
          == null) 
        { 
          throw new InvalidOperationException(
            Resources.FormatUnableToFindServices( 
              nameof(IServiceCollection), 
            "AddMvc", 
            "ConfigureServices(...)")); 
        } 
        var middlewarePipelineBuilder =
          app.ApplicationServices.GetRequiredService
          <MiddlewareFilterBuilder>(); 
        middlewarePipelineBuilder.ApplicationBuilder = app.New(); 
        var routes = new RouteBuilder(app) 
        { 
          DefaultHandler =
            app.ApplicationServices.GetRequiredService<MvcRouteHandler>(), 
        }; 
        configureRoutes(routes); 
        routes.Routes.Insert(0,
          AttributeRouting.CreateAttributeMegaRoute(
            app.ApplicationServices)); 
        return app.UseRouter(routes.Build());} 

您可以按如下方式分解前面的代码:

  • UseMvc方法创建 RouteBuilder 的一个实例。
  • RouteBuilder需要DefaultHandler处理路由;这是由MvcRouteHandler类提供的。
  • MvcRouteHandler实现IRouter,完成 URL 模式匹配和生成 URL 的工作。
  • configureRoutes是配置路由的动作方式。在前面的示例中,我们完成了相同的任务。
  • AttributeRouting作为IRouter集合的第一个条目添加。
  • CreateAttributeMegaRoute方法扫描所有控制器动作并自动建立路由。这是UseMvc的重要行之一。
  • 最后,使用UseRouter构建路由并添加到路由中间件。这与本章开头执行的任务类似。

UseMvc中的AttributeRouting.CreateAttributeMegaRoute在 ASP.NET Core MVC 和 web API 应用中默认提供属性路由。

基于约定的路由

在 ASP.NET Web API 的第一个版本中,路由机制是基于约定的。这种类型的路由具有一个或多个路由模板的参数化字符串定义。

ASP.NET Core 仍然支持这种类型的路由;下面的代码片段显示了如何实现这一点:

    public void Configure(IApplicationBuilder app) 
    { 
      //Rest of code removed for brevity 
      app.UseMvc(routes => 
      { 
        // route1 
        routes.MapRoute( 
          name: "packtroute1", 
          template: "api/{controller}/{id}" 
        ); 
        // route2 
        routes.MapRoute( 
          name: "packtroute1", 
          template: "testpackt", 
          defaults: new { controller = "Books", action = "Index" } 
        ); 
      }); 
    } 

基于约定的路由方式不受欢迎,原因如下:

  • 它不支持 web API 世界中常见的某些类型的 URL 模式
  • 具有子资源的资源很难创建。例如,/products/1/orders/2/reviews
  • 当 web API 有许多控制器和操作时,它是不可伸缩的

基于模板的路由

在使用 web API 时,您可能会遇到多种 URI,如/product/12/product/12/orders/departments//books等等。

在 WebAPI 世界中,它们被称为路由——一个描述 URI 模板的字符串。例如,可以在此 URI 模式上形成一个示例路由:/products/{id}/orders

这里有几点需要注意:

  • URI 模板由文本和参数组成
  • 在前面的示例中,产品和订单是文本
  • 大括号{}中的任何内容都称为参数--{id}就是这样一个例子
  • 路径分隔符(/必须是路由模板的一部分——URI 将/理解为路径分隔符
  • 文本、路径分隔符和参数的组合应与 URI 模式匹配

使用 web API 时,文本将是控制器或方法。路由参数在使路由模板具有多用途方面起着重要作用。大括号中的参数可以发挥多用途作用,如下所示:

  • 即使管线模板具有参数,也可以通过在模板中放置“?”使其成为可选参数。例如,/books/chapters/{numb?}——这里,如果我们不提供numb,那么它将加载所有章节,如果我们提供 numb,那么将加载相关章节。
  • 一个管线模板可以有多个管线参数。
  • 路由参数可以使用*作为前缀,以便绑定到 URI 的其余部分。这种参数称为catch-all参数。
  • 可以为管线参数提供默认值。此默认值将在未为其提供 route 参数时生效。例如,"{controller=Home}/{action=Index}"将在Home控制器中加载Index动作方法。
  • 管线参数可以具有约束,以确保以正确的方式生成管线。例如,/customers/{id:int}/services/{id:int}参数表示id必须是整数,否则 web API 响应 404 响应类型。

以下是在Startup classConfigure方法中定义的几个示例路由模板:

    public void Configure(IApplicationBuilder app) 
    { 
      app.UseMvc(routes => 
      { 
        // Route Template with default values and optional parameter 
        routes.MapRoute( 
          name: "default", 
          template: "{controller=Home}/{action=Index}/{id?}"); 

        //Route Template with default value, parameter constrainst 
        routes.MapRoute( 
          name: "alternate_route", 
          template: "{controller}/{action}/{id:int}/{guid:string}", 
          defaults: new { controller = "Dashboard" }); 

        // Route Template with no default values or parameters 
        routes.MapRoute( 
          name: "simple_route", 
          template: "{controller}/{action}");                     
      }); 
    } 

基于属性的路由

在.NET 编程世界中,将各种元素(如类、方法和枚举)的声明信息添加到程序中的标记称为属性。

ASP.NET Web API 2 引入了基于属性的路由的概念,为 Web API 中的 URI 提供了更多控制。这有助于我们轻松构建具有资源层次结构的 URI。

在 ASP.NET Core 应用中,默认情况下提供基于属性的路由。在Startup类的Configure方法中,app.UseMvc()行表示在请求处理管道中包含 MVC 中间件。

ASP.NET Core Web API 和路由一节中,我们解释了默认情况下如何实现基于属性的路由,这与 ASP.NET Web API 2 不同,后者必须在配置中显式启用。

AttributeRouting.CreateAttributeMegaRoute重复执行所有 MVC 控制器操作并自动构建路由的繁重工作。

当我们在第 3 章ASP.NET Core Web API 剖析中创建演示项目时,它是使用 ASP.NET Core 工具附带的 Web API 模板创建的。

让我们了解演示项目中ValuesController.cs的属性路由:

    using System.Collections.Generic; 
    using Microsoft.AspNetCore.Mvc; 

    namespace MyFirstCoreAPI.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 is " + id; 
        } 

        // 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) 
        { 
        } 
      }  
    } 

ValuesController类用Route属性修饰为[Route("api/[controller]")]

在运行应用时,当我们导航到http://localhost:5000/api/values/时,您可以看到结果,如以下屏幕截图所示:

Attribute Route in action

在控制器上定义属性路由;我们需要访问 URL,如前面的屏幕截图所示。/values为控制器名称,测试时根据HttpGet动词执行动作GetGet(int id)

RESTful 应用的属性路由

在语法上,属性路由定义如下:

[HttpMethod("Template URI", Name?, Order?)]

  • 基于 HttpVerbs,ASP.NET Core 提供了这些用于属性路由的 HttpMethods--HttpDeleteAttributeHttpGetAttributeHttpHeadAttributeHttpOptionsAttributeHttpPatchAttributeHttpPostAttributeHttpPutAttribute
  • Template URI是描述路由的字符串。
  • Name——属性路由的可选名称。它通常在操作似乎是重载方法时使用。
  • 命令有助于在控制器中执行重载 HTTP 方法时优先。顺序取决于文字、参数、约束以及顺序。这是一个可选参数。

考虑到这些语法,让我们创建一个新的 web API 控制器PacktController,添加操作方法,并为其添加属性路由:

    using Microsoft.AspNetCore.Mvc;  
    using System.Collections.Generic; 

    namespace MyFirstCoreAPI.Controllers 
    { 
      [Route("api/[controller]")] 
      public class PacktController : Controller 
      { 
        // Get: api/packt/show 
        [HttpGet("Show")] 
        public string Show() 
        { 
          return "I am Packt Show !!"; 
        } 

        // GET api/packt 
        [HttpGet] 
        public IEnumerable<string> Get() 
        { 
          return new string[] { "Packt 1", "Packt 2" }; 
        } 

        // GET: /api/packt/13 
        [HttpGet("{id:int}", Name = "GetPacktById", Order = 0)] 
        public string Get(int id) 
        { 
          return "Response from GetPacktById" + id; 
        }         

        // POST: /api/packt 
        [HttpPost] 
        public IActionResult Post() 
        { 
          return Content("Created Post !!"); 
        } 

        // POST: /api/packt/packtpost 
        [HttpPost("packtpost")] 
        public IActionResult Post([FromBody]string chapterName) 
        { 
          return Content("You invoked packt post"); 
        } 

        // PUT api/packt/5 
        [HttpPut("{id}")] 
        public void Put(int id, [FromBody]string value) 
        { 
        } 

        // DELETE api/packt/15 
        [HttpDelete("{id}")] 
        public void Delete(int id) 
        { 
        } 
      } 
    }  

您可以按如下方式分解前面的代码:

PacktController类有以下七种动作方法:

  • Show():使用路由/api/packt/show上的HttpGet请求调用。
  • Get():使用路由/api/packt上的HttpGet请求调用。
  • Get(int id):使用路由/api/packt/13上的HttpGet请求调用。此处提供了路由名称。
  • Post():使用路由/api/packt/上的HttpPost请求调用。
  • Post([FromBody]string chapterName):使用路由/api/packt/packtpost上的HttpPost请求调用。
  • Put():使用路由api/packt/12上的HttpPut请求调用。提供路由参数 ID,因为PUT对应更新功能,对现有记录进行更新。
  • Delete():使用路由api/packt/12上的HttpDelete请求调用。要删除任何记录,我们需要传递其唯一标识;因此,需要传递 ID route 参数。

多路线

有时,我们可能会得到路由需求,例如应用于同一控制器或操作方法的不同路由。起初,这似乎非常令人惊讶,但在大型项目中,我们可能需要这种路由。

通过在控制器上放置多个路由属性,可以实现多个路由,如以下代码段所示:

    [Route("Stocks")] 
    [Route("[controller]")] 
    public class PacktsController : Controller 
    { 
      [HttpGet("Check")]      
      [HttpGet("Available")] 
      public string GetStock() 
      { 
         return "Its multiple route"; 
      } 
    } 

运行应用以查看正在运行的多个路由。您可以通过访问浏览器上的端点/api/stocks/check or /api/packts/available来验证多条路由是否正常工作。

路由约束

ASP.NET Core 作为 MVC 或 web API 应用,支持属性和集中式路由机制。可以使用 Route 属性直接在控制器和操作上设置路由,也可以通过在一个位置创建和映射所有路由来设置路由。

我们已经看到,可以使用或不使用管线参数创建不同的管线模板。当路由模板中包含参数时,有助于构建优秀的路由模式。但是路线参数的存在也会引起问题;我们来看一个例子:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env,
      ILoggerFactory loggerFactory) 
    { 
      var routeBuilder = new RouteBuilder(app); 
      routeBuilder.MapGet("employee/{id}", context => 
      { 
        var idValue = context.GetRouteValue("id");                 
        return context.Response.WriteAsync($"The number is - {idValue}"); 
      }); 

      var routes = routeBuilder.Build(); 
      app.UseRouter(routes); 
    } 

您可以按如下方式分解前面的代码:

  • 提供模板为employee/{id}的基本路由,其中{id}为路由参数
  • routes 模板用于通过传递 ID 来获取员工列表
  • GetRouteValue读取{id}参数,并返回响应

Route Parameters without constraints

运行应用以查看提供路由参数的不同方式。看起来一切都在运转——事实上,它运转得很好,但问题仍然存在。

这里,我们假设员工将其 ID 存储为整数,因为大多数组织使用整数作为员工 ID,例如 John 的员工 ID 为 23,Sarah 的员工 ID 为 45。当路由参数 ID 作为整数传递时,一切正常,但是如果{id}作为字符串传递,会怎么样,如上图所示?

应用确实接受参数,并且响应良好。但是,在处理数据库源的实际项目时,作为字符串提供的 ID 将破坏应用,并导致异常或错误。

一个明显的问题是,如果在路由参数中传递了错误的数据,我们如何限制对此类路由参数的请求处理。答案是路线限制。

如果参数未能满足应用于它们的约束条件,路由约束可以帮助我们限制请求处理。

可通过在参数上使用:添加参数约束。例如,"employee/{id:int}"

当带有路由参数约束的请求发送无效类型时,返回的 HTTP 响应为 404(未找到)。是的,由于路由限制,ASP.NET Core 将失败的 HTTP 请求响应为 404(未找到)。请求处理不会影响处理请求的控制器。

让我们对前面代码中的{id}参数应用整数约束,并运行应用,如下所示:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env,
      ILoggerFactory loggerFactory) 
    { 
      var routeBuilder = new RouteBuilder(app); 

      // Id parameter should be Integer now 
      routeBuilder.MapGet("employee/{id:int}", context => 
      { 
        var idValue = context.GetRouteValue("id");                 
        return context.Response.WriteAsync($"The number is - {idValue}"); 
      }); 

      // Name parameter should be length 8 
      routeBuilder.MapGet("product/{name:length(8)}", context => 
      { 
        var nameValue = context.GetRouteValue("id"); 
        return context.Response.WriteAsync($"The Name is - {nameValue}"); 
      }); 

      var routes = routeBuilder.Build(); 
      app.UseRouter(routes); 
    } 

您可以按如下方式分解前面的代码:

  • 路由模板employee/{id:int}只取参数 ID 为整数
  • 路由模板product/{name:length(8)}参数仅取长度 8

运行应用以查看路由现在在请求处理中是否得到了规范。路线限制是第一道防线。

下图显示,当路由参数名称的长度为 8 时,它正确地返回了响应。但当名称长度不是 8 时,它返回 404 错误:

Route Constraints in action

管线约束也可以应用于属性管线。下面是一个例子:

    using Microsoft.AspNetCore.Mvc; 
    namespace MyFirstCoreAPI.Controllers 
    { 
      [Route("api/[controller]")] 
      public class PacktController : Controller 
      { 
        // GET: /api/packt/13 
        [HttpGet("{id:int}")] 
        public string Get(int id) 
        { 
          return "Response from " + id; 
        } 
      } 
    }  

路线限制的类型

ASP.NET Core 团队基于不同的数据类型创建了一系列广泛使用的路由约束场景。下表列出了不同的管线约束:

| 约束名称 | 用法 | 备注 |
| Int | {id:int} | 参数应为整数 |
| 布尔 | {isExists:bool} | 参数应为 TRUE 或 FALSE |
| 日期时间 | {eventdate:datetime} | 仅接受日期时间作为参数 |
| 十进制的 | {amount:decimal} | 仅接受十进制作为参数 |
| 双重的 | {weight:double} | 仅接受 double 作为参数 |
| 浮动 | {distance:float} | 仅将 float 作为参数匹配 |
| 指南 | {id:guid} | 仅接受 GUID 作为参数 |
| 长的 | {ticks:long} | 参数应为长类型 |
| 最小长度 | {username:minlength(8)} | 参数的最小长度应为 8 |
| 最大长度 | {filename:maxlength(5)} | 参数的最大长度应为 5 |
| 长度(最小值、最大值) | {name:length(4,16)} | 参数的最小长度可以是 4,最大长度可以是 16 |
| 最小值(值) | {age:min(18)} | 参数年龄的最小值应为 18 |
| 最大值 | {weight:max(90)} | 参数权重的最大值应为 90 |
| 范围(最小值、最大值) | {age:range(18,100)} | 年龄参数应介于 18 到 100 之间 |
| 阿尔法 | {name:alpha} | 仅允许字母作为参数 |
| 正则表达式(表达式) | {email:regex(/\S+@\S+\.\S+/} | 使用正则表达式匹配参数;电子邮件就是这样一个例子 |
| 要求的 | {productId:required} | 必须提供参数 |

我们可以根据需求将不同的路由约束组合到参数中。例如,如果tableno是一个整数,并且在 18 到 30 之间可用,那么我们可以定义如下路由:

    [Route("api/[controller]")] 
      public class HotelController : Controller 
    { 
      [HttpGet("{tableno:int}/{tableno:range(18, 30)}")] 
      public string Get() 
      { 
        return "Table Range is 18 - 30"; 
      } 
    } 

编写自定义路由约束

到目前为止,我们使用了内置的路由约束;它们服务于大量用例,但是不同的业务需求可能导致编写定制约束的需要。使用Microsoft.AspNetCore.Routing命名空间中的IRouteConstraint接口编写自定义路由约束。

接口有Match方式,采用HttpContextIRouterouteKeyvaluesRouteDirection。当Match方法实现时,如果约束条件与参数值匹配,则返回TRUE,否则返回FALSE

例如,让我们使用自定义路由约束的业务用例,该参数指定该参数应该包含域名为 OutT0},否则它应该以 404 的错误响应。

  1. 通过复制以下代码创建一个 C#类DomainConstraint。在此代码中我们实现了IRouteConstraint接口和Match方法:
        public class DomainConstraint : IRouteConstraint 
        { 
          public bool Match(HttpContext httpContext, IRouter route,
          string routeKey, RouteValueDictionary values,
            RouteDirection routeDirection) 
          {  
            var isMatch = false; 
            if (values["domName"].ToString().Contains("@packt.com")) 
            { 
              isMatch = true; 
            } 
            return isMatch; 
          } 
        } 

  1. 打开Startup.cs,在ConfigureServices方法中将该约束添加为RouteOption
  2. Startup.cs中,使用Configure方法中路由模板中的 DomainConstraint 域,如下所示:
        using Microsoft.AspNetCore.Builder; 
        using Microsoft.AspNetCore.Hosting; 
        using Microsoft.AspNetCore.Http; 
        using Microsoft.Extensions.DependencyInjection; 
        using Microsoft.Extensions.Logging; 
        using Microsoft.AspNetCore.Routing; 

        namespace BasicRoutes  
        { 
          public class Startup 
          {         
            public void ConfigureServices(IServiceCollection services) 
            { 
              // Adding Router Middleware 
              services.AddRouting(); 

              services.Configure<RouteOptions>(options => 
              options.ConstraintMap.Add("domain",
                typeof(DomainConstraint))); 
            } 

            public void Configure(IApplicationBuilder app,
              IHostingEnvironment env, ILoggerFactory loggerFactory) 
            { 
              var routeBuilder = new RouteBuilder(app);             

              // domName parameter should have @packt.com 
              routeBuilder.MapGet("api/employee/{domName:domain}",
                context => 
              { 
                var domName = context.GetRouteValue("domName"); 
                return context.Response.WriteAsync(
                  $"Domain Name Constraint Passed - {domName}"); 
              }); 

              var routes = routeBuilder.Build(); 
              app.UseRouter(routes); 
            } 
          } 
        } 

运行前面的应用将显示如下响应。

由于 route 参数包含@packt.com域,根据自定义约束,这可以很好地工作,如此屏幕截图所示:

Domain Constraint returns response

现在让我们在不使用@packt.com的情况下传递参数;这将抛出如下的404 Not Found错误:

Domain Constraint returns 404 error

链接生成

路由机制提供了足够的链接到应用中的路由,但是,有时,生成指向特定路由的链接变得至关重要。我们可以使用链接生成概念实现自定义链接。

ASP.NET Core 提供了UrlHelper类,这是IUrlHelper接口的一个实现,用于为 ASP.NET Core 应用(MVC 或 web API)构建 URL。方法CreatedAtRouteCreatedAtAction是链接生成的两种内置方法。让我们在一个例子中使用它们:

    using Microsoft.AspNetCore.Mvc; 
    using System.Collections.Generic; 
    using System.Linq; 

    namespace BasicRoute.Controllers 
    { 
      public class TodoController : Controller 
      { 
        // List containing Todo Items 
        List<TodoTasks> TodoList = new List<TodoTasks>(); 

        // Gets Todo item details based on Id 
        [Route("api/todos/{id}", Name = "GetTodoById")] 
        public IActionResult GetTodo(int id) 
        { 
          var taskitem = TodoList.FirstOrDefault(x => x.Id == id); 
          if (taskitem == null) 
          { 
             return NotFound(); 
          } 
          return Ok(taskitem); 
        } 

        // Adds or POST a todo item to list 
        [Route("api/todos")] 
        public IActionResult PostTodo([FromBody]TodoTasks todoItems) 
        { 
          TodoList.Add(todoItems); 
          // CreatedAtRoute generators link 
          return CreatedAtRoute("GetTodoById", new { 
            Controller = "Todo", id = todoItems.Id }, todoItems); 
        } 
      } 

      public class TodoTasks 
      { 
        public int Id { get; set; } 
        public string Name { get; set; } 
      } 
    } 

您可以按如下方式分解前面的代码:

  • TodoTasks类是基本的 C#类,具有用于 Todo 任务的idName
  • TodoController定义了GetTodoPostTodo两种动作方式,具有适当的属性路由。
  • CreatedAtRoute方法生成一个链接,并将其添加到生成链接的响应头中,以便客户端可以访问该链接。

可以编写一个类似的CreatedAtRoute,以生成更集中的链接。当我们使用 Postman 客户端运行应用时,位置头被显式添加,URL 指向带有idGetTodo方法,如下面的屏幕截图所示:

Link generation using CreatedAtRoute method

路由最佳实践

NET Core 以中间件、基于约定的路由、直接或属性路由、约束等形式提供了一种轻量级、完全可配置的路由机制。正确使用这些路由功能对于应用的最佳性能是必要的。

下面总结一些需要考虑的最佳实践;随着使用各种用例构建更多的应用,这些实践将不断发展。根据您的应用的需要,这些列表可能适用于您的应用,也可能不适用于您的应用:

  • 使用位于路由中间件之上的内置UseMvc中间件实现路由策略。
  • 传统的路由已经足够了,CRUD 风格的 web API 正在开发中。它减少了为各种控制器中的所有操作编写直接路由的开销。
  • 尽量避免使用 catch-all 路由参数,即{*empId}。在传统(集中式)路由中使用时,不需要的路由可能会与此“一网打尽”匹配,从而导致意外行为。
  • 在使用 ASP.NET Core Web API 时,最好使用基于 HTTP 谓词的路由,即使用 HTTP 谓词(如 GET、POST、PUT 和 DELETE)的属性路由。客户端可以轻松地使用这些 web API。
  • 应该避免在属性路由上排序,而是为不同的 API 方法使用路由名称。
  • 多个路由可以指向相同的控制器/操作,但大规模使用此路由可能会导致将操作的实现与许多条件情况相结合。
  • 管线参数约束是一项功能强大的功能,但不应将其用于模型验证。
  • 应尽量减少使用传统路由和属性路由设计 web API。

总结

ASP.NET Core 路由功能强大且高度可配置。在本章中,您了解了路由中间件如何与 HTTP 管道一起工作。我们使用 RouteBuilder 创建路由表并在它们之间导航。您还了解了在内部实现路由中间件的UseMvc中间件。

路由可以是传统的,也可以是基于属性的,我们使用自定义约束讨论并实现了路由参数。链接生成功能可用于生成到特定路由的链接。您还学习了一些 web API 路由的最佳实践。

下一章将重点介绍 ASP.NET Core 的中间件概念——您将学习中间件的基础知识、编写一些自定义中间件以及 ASP.NET Core 应用中的过滤器。

六、中间件和过滤器

任何 web 应用开发框架的成功都取决于其高效处理 HTTP 请求和响应的能力。任何 HTTP 请求都必须经过一系列的验证和确认,才能访问请求的资源。

当大量的同时请求到达 web 服务器时,快速地为它们提供服务而不会在负载下崩溃是设计良好的 web 应用的一个关键因素。这涉及到基于模块化方法设计框架,只有在需要时,我们才能使用功能。

NET Core 是开发现代 web 应用的完全模块化方式;它的设计理念是包含您所需的内容,而不是包含所有功能,这使得处理请求变得非常繁重。

中间件和过滤器是 ASP.NET Core 的这些特性,它们在 HTTP 请求的处理中起着重要作用。在本章中,您将深入了解这两个特性。

在本章中,我们将介绍以下主题:

  • 引入中间件
  • HTTP 请求管道和中间件
  • 中间件的顺序
  • 内置中间件
  • 创建自定义中间件
  • 将 HTTP 模块迁移到中间件
  • 引入过滤器
  • 动作过滤器
  • 身份验证和授权筛选器
  • 异常过滤器

引入中间件

假设您创建了 ASP.NET Core 应用(MVC 或 web API)、控制器和连接到数据库的操作方法来获取记录,然后添加了身份验证;一切顺利。

为了好玩,对Startup类的Configure方法中的代码进行注释并运行应用。令人惊讶的是,不会出现任何构建错误,并且会显示一个空白的浏览器窗口。

导航到任何路线、页面或资源时,始终会返回一个空白屏幕。深入研究这一点,我们可以得出以下结论:

  • Configure方法是 HTTP 请求到达时的起点之一
  • 当请求到达时,必须有人处理请求并返回响应
  • HTTP 请求在访问资源之前必须经历各种操作,如身份验证、CORS 等

Startup类的Configure方法是 HTTP 请求管道的中心;此管道中的任何软件组件在 ASP.NET Core 中都称为中间件。他们根据在管道中的放置顺序负责请求和响应处理。

每个中间件都可以设计为将请求传递到下一个中间件或结束请求处理管道。请求管道是使用请求委托构建的。请求委托处理每个 HTTP 请求。

Without middleware components, ASP.NET Core does nothing.

Configure方法将其中一个参数作为IApplicationBuilder。此接口提供了配置应用请求管道的机制。使用IApplicationBuilder接口的以下四种扩展方法配置请求委托:

  • Use()方法将中间件请求委托添加到应用管道中。请求委托处理程序可以是匿名方法或可重用类。
  • Run()方法在应用管道中也称为终端中间件委托。在此之后将不处理任何内容。
  • Map()方法根据请求路径的匹配对请求管道进行分支。
  • Mapwhen()方法根据给定谓词或条件的结果分支请求管道。

让我们了解各种中间件组件如何与IApplicationBuilder协同工作。

HTTP 请求管道和中间件

ASP.NET Core 请求管道处理完全重写了传统 ASP.NET 请求处理。每个请求都通过一系列请求委托进行处理,以返回响应。

ASP.NET Core 文档描述了 HTTP 请求处理,如以下屏幕截图所示:

HTTP request processing in ASP.NET Core

蓝色条表示,一旦 HTTP 请求到达管道(即Configure方法),中间件组件(内置或定制)就会遇到中间件 1组件。请求处理在//逻辑中进行,然后使用next()将请求按顺序传递给下一个中间件。

请求处理到达中间件 2组件,进行处理,并使用next()传递到下一个中间件 3。这里,在处理请求之后,它没有遇到next(),这表示序列在此结束并开始返回响应。

响应按相反顺序处理--中间件 3中间件 2中间件 1,因为最终响应返回给客户端。

IApplicationBuilder按顺序跟踪请求处理管道;这样,很容易顺利地处理请求和响应。

请求代理的设计应具有特定的重点,而不是将多个代理组合成一个组件。中间件应尽可能精简,以使其可重用。

中间件可以传递请求以进行进一步处理,也可以自行停止传递以处理请求。例如身份验证、CORS、开发人员异常页面等。

运行中的中间件

在上一节中,我们研究了中间件请求管道处理的图形表示。在本节中,您将通过编写代码来了解中间件在请求管道中的角色。

创建一个空的 ASP.NET Core 应用,并注释掉/删除Startup类的Configure方法中的默认代码。

我们将编写一段代码来理解上一节中的四种扩展方法:Use()Run()Map()IApplicationBuilderMapWhen()

中间件组件接受RequestDelegate方法。它们是处理带有签名的 HTTP 请求的函数,如下所示:

    public delegate Task RequestDelegate(HttpContext context); 

它接受一个参数HttpContext并返回Task,异步工作。

用法()

当中间件被写为app.Use(...)时,一个中间件委托被添加到应用的请求管道中。它可以作为内联委托或在类中编写。

它可以将处理传递到下一个中间件或结束处理它的管道。Use扩展方法让代理接受两个参数,第一个参数是HttpContext,第二个参数是RequestDelegate

可以编写非常基本的中间件,如以下代码段所示:

    app.Use(async (context, next) => 
   { 
     await context.Response.WriteAsync("Middleware 1"); 
   }); 

运行此代码将在浏览器上显示Middleware 1

该中间件组件确实具有在响应时写入内容的逻辑,但它不会调用任何其他中间件,因为缺少请求委托的第二个参数。

要调用下一个中间件,我们需要调用请求委托nextInvoke()方法,如下代码所示:

    public void Configure(IApplicationBuilder app, IHostingEnvironment 
     env, ILoggerFactory loggerFactory) 
    { 
      app.Use(async (context, next) => 
      { 
        await context.Response.WriteAsync("Middleware 1<br/>"); 
        // Calls next middleware in pipeline 
        await next.Invoke(); 
      }); 

      app.Use(async (context, next) => 
      { 
        await context.Response.WriteAsync("Middleware 2<br/>"); 
        // Calls next middleware in pipeline 
        await next.Invoke(); 
      }); 

      app.Use(async (context, next) => 
      { 
        await context.Response.WriteAsync("Middleware 3<br/>"); 
        // No invoke method to pass next middleware 
      }); 

      app.Use(async (context, next) => 
      { 
        await context.Response.WriteAsync("Middleware 4<br/>"); 
        // Calls next middleware in pipeline 
        await next.Invoke(); 
     }); 
   } 

我们可以将前面的代码分解如下:

  • Middleware 1Middleware 2Middleware 3Middleware 4是简单的内联中间件示例。
  • Middleware 1Middleware 2具有调用管道中next中间件的请求委托。
  • Middleware 3没有next调用代码,管道将推断这是最后一个请求委托并停止进一步处理。这就是所谓的短路
  • 尽管Middleware 4是用next调用编写的,但它并没有被称为Middleware 3,因为它还并没有通过请求处理。

运行应用以在浏览器中查看中间件响应。Middleware 4的响应没有写入,因为管道已经处理过Middleware 3是最后一个,并且进一步结束处理,即管道结束处理:

Middleware using app.Use (...)

运行()

Run()方法在管道上增加了一个RequestDelegate终端。在Run之后编写的任何中间件组件都不会被处理,因为 ASP.NET Core 将其视为管道处理的结束。

在使用Use()编写中间件之前,复制Configure方法开头的以下代码段:

    app.Run(async context => { 
      await context.Response.WriteAsync("Run() - a Terminal Middleware 
      <br/>"); 
    }); 

运行应用时,您只会看到执行了Run()中间件,而其余的中间件(1,2,3,4)根本没有执行。

一些中间件确实公开了这种方法,并将其视为终端中间件。不应该在代码中使用它来终止请求处理。

Adopt the Use() method for request processing because it can either pass or short circuit requests.

地图()

Map()方法提供了基于请求路径匹配的中间件管道处理分支能力。Map()方法有两个参数:PathString和名为Configuration的委托人。

下面的代码片段显示了Map()正在运行;在Configure方法中复制并运行应用:

    public void Configure(IApplicationBuilder app, 
     IHostingEnvironment env, ILoggerFactory loggerFactory) 
    { 
      app.Use(async (context, next) => 
      { 
        await context.Response.WriteAsync("Middleware 1 without Map 
        <br/>"); 
        await next(); 
      }); 

      // Get executed only on "/packtchap2" 
      app.Map(new PathString("/packtchap2"), branch => 
      { 
        branch.Run(async context => { await 
        context.Response.WriteAsync("packtchap2 - Middleware 1 
        <br/>"); }); 
      }); 

      // Get executed only on "/packtchap5" 
      app.Map(new PathString("/packtchap5"), branch => 
      { 
        branch.Run(async context => { await context.Response.WriteAsync(
          "packtchap5 - Middleware 2 <br/>"); }); 
      }); 

      app.Run(async context => { await context.Response.WriteAsync("
        Middleware 2 without Map <br/>"); }); 
    } 

运行应用以查看其运行情况。导航至http://localhost:60966/packtChap3http://localhost:60966/packtChap5

The port number 60966 will be different when it is run on your machine.

我们可以将代码分解如下:

  • packtchap2packtchap5中间件只有在分支时才能执行。
  • 无论映射分支如何,都会执行Middleware 1 without Map
  • 应用上述任何分支时,Middleware 2 without Map将不会执行。这很明显,因为管道处理已经改变了方向。

分支中间件可以包含任意数量的中间件组件。下面的屏幕截图显示了Map()正在运行:

Map (...) in action

MapWhen()

Map()方法类似,但对 URL、请求头、查询字符串等有更多的控制。MapWhen()方法在检查HttpContext作为参数的任何条件后返回布尔值。

Startup类的Configure方法中复制以下代码段:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env,
      ILoggerFactory loggerFactory) 
    { 
      app.Use(async (context, next) => 
      { 
        await context.Response.WriteAsync("Middleware 1 - Map When <br/>"); 
        await next(); 
      }); 

      app.MapWhen(context => 
        context.Request.Query.ContainsKey("packtquery"), (appbuilder) => 
      { 
        appbuilder.Run(async (context) => 
        { 
          await context.Response.WriteAsync("In side Map When <br/>"); 
        }); 
      }); 
    } 

运行此示例将显示以下结果:

MapWhen in action

当 URL 将查询字符串映射到packtquery时,请求管道处理分支并执行基于MapWhen()的中间件。

中间件的顺序

ASP.NET Core 请求管道处理通过按照Startup类的Configure方法中放置的顺序运行中间件组件来工作。

请求处理的图形表示描述了中间件的执行顺序。序列顺序是通过放置UseRunMapMapWhen扩展方法代码来创建的。调用下一个中间件决定了中间件的执行顺序。

以下是中间件顺序扮演重要角色的一些现实场景:

  • 身份验证中间件应该首先处理请求。只有经过身份验证的请求才允许访问应用。
  • 在 WebAPI 项目中,CORS 中间件与初始中间件和身份验证中间件非常适合。
  • 自定义中间件可以在访问资源之前执行一些自定义处理,即使请求已通过身份验证。
  • 在将 web API 响应返回到客户端之前,需要对其进行修改。

以下代码片段显示了中间件顺序的重要性:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env,
      ILoggerFactory loggerFactory) 
    { 
      app.Use(async (context, next) => 
      { 
        await context.Response.WriteAsync("Middleware 1<br/>"); 
        // Calls next middleware in pipeline 
        await next.Invoke(); 
        await context.Response.WriteAsync("Middleware 1
          while return<br/>"); 
      }); 

      app.Map(new PathString("/packtchap2"), branch => 
      { 
        branch.Run(async context => { await context.Response.WriteAsync("
          packtchap2 - Middleware 1<br/>"); }); 
      }); 

      app.Run(async context => { 
        await context.Response.WriteAsync("Run() - a Terminal 
          Middleware <br/>"); 
      }); 

      app.Use(async (context, next) => 
      { 
        await context.Response.WriteAsync("Middleware 2<br/>"); 
        // Calls next middleware in pipeline 
        await next.Invoke();                 
      }); 
    } 

运行应用以在浏览器中查看结果,如以下屏幕截图所示:

Middleware order in action

我们可以将代码分解如下:

  • 使用Use(...)编写的中间件在使用Invoke()方法传递到下一个中间件之前进行处理
  • 执行Middleware 1并返回响应
  • 使用Run编写的中间件得到处理并开始中间件的反向执行,因为Run()是终端(end)
  • Middleware 2因为前面有Run()而从未接到电话
  • 只有当packtchap2路径字符串有效时,才会执行Middleware using Map()

内置中间件

ASP.NET Core 团队已经编写了许多内置中间件来满足各种各样的需求。一些内置中间件如下所示:

  • 认证:用于认证支持,如登录、注销等。
  • CORS:配置用于 web API 项目的跨源资源共享。
  • 路由:定义并约束请求路由。第 5 章实施路由的,是专门针对路由的。
  • 会话:提供对用户会话管理的支持。
  • 静态文件:支持作为wwwroot的一部分提供静态文件和目录浏览服务。

使用静态文件中间件

静态文件(HTML、CSS、图像、JS 文件)由 ASP.NET Core 应用中的wwwroot文件夹提供。在本节中,让我们添加一个内置的静态文件服务中间件,用于加载wwwroot文件夹中的所有资产。

创建一个空的 ASP.NET Core 项目,也可以从前面的中间件代码示例继续。用于提供静态内容的Microsoft.AspNetCore.StaticFilesNuGet 包已经是Microsoft.AspNetCore.All的一部分。

打开Startup类,在Configure方法中添加以下代码使用此StaticFiles中间件:

    app.UseStaticFiles(); 

wwwroot中创建一个 HTML 文件index.html;这是应用运行时将提供的静态文件:

    <!DOCTYPE html> 
    <html> 
      <head> 
        <meta charset="utf-8" /> 
        <title>Mastering Web API</title> 
      </head> 
      <body> 
        <h3>Served by StaticFile middleware.</h3>  
      </body> 
    </html> 

运行应用时,您会看到index.html正在浏览器上服务,如下图所示:

Index.html served by StaticFile middleware

编写自定义中间件

对于请求管道处理,每个项目都有自己的特定于业务或领域的需求。内置中间件已经足够好了,但是编写自定义中间件可以让我们更好地控制请求处理策略。

在本节中,我们将编写有关 web API 项目的自定义中间件组件。通过考虑以下几点,可以编写自定义中间件:

  • 具有中间件名称的 C#类
  • 一个RequestDelegate变量--_next--用于调用管道中的下一个中间件
  • 接受使用 DI 注入的请求委托对象的类构造函数
  • 一种以HttpContext为参数异步返回TaskInvoke方法

我们将编写一个带有业务场景的定制中间件,因为每个请求都应该包含packt-book头,其值应该是Mastering Web API。如果标头值丢失或不正确,则 HTTP 响应应该是错误的请求,如果匹配正确,则 web API 应该返回请求资源。

创建一个名为PacktHeaderValidator的 C#类,并在其中复制以下代码:

    using Microsoft.AspNetCore.Builder; 
    using Microsoft.AspNetCore.Http; 
    using System.Threading.Tasks; 

    namespace custom_middleware 
    { 
      // Custom ASP.NET Core Middleware 
      public class PacktHeaderValidator 
      {     
        private RequestDelegate _next; 

        public PacktHeaderValidator(RequestDelegate next) 
        { 
           _next = next; 
        } 

        public async Task Invoke(HttpContext context) 
        { 
          //If matches pipeline processing continues 
          if (context.Request.Headers["packt-book"].Equals(
            "Mastering Web API")) 
          {    
             await _next.Invoke(context); 
          } 
          else 
          { 
             // Pipeline processing ends 
             context.Response.StatusCode = 400; //Bad request 
          }             
        } 
      } 

      #region ExtensionMethod 
      //Extension Method to give friendly name for custom middleware 
      public static class PacktHeaderValidatorExtension 
      { 
        public static IApplicationBuilder 
          UsePacktHeaderValidator(this IApplicationBuilder app) 
        { 
          app.UseMiddleware<PacktHeaderValidator>(); 
          return app; 
        } 
      } 
      #endregion 
    } 

Startup类的Configure方法中使用这个新创建的自定义中间件,如下所示:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env,
      ILoggerFactory loggerFactory) 
    { 
      //Custom middleware added to pipeline 
      app.UsePacktHeaderValidator(); 
      app.UseMvc(); 
    } 

我们可以将代码分解如下:

  • PacktHeaderValidator类查找packt-book头值。
  • 如果packt-book值匹配,管道过程继续。它返回 web API 控制器数据。
  • 如果packt-book值不匹配,则管道处理结束。
  • Configure方法中增加了中间件PacktHeaderValidatorUseMvc()中间件操作仅在标头值匹配时执行。

为了测试这个代码示例,我们将使用 Postman 工具进行测试。我们可以传递头值、查看状态代码以及 API 响应:

  • 运行(F5)在编写自定义中间件的地方投影的 web API
  • 打开邮递员工具,使用GET访问 URLhttp://localhost:55643/api/values
  • 使用有效和无效的标题值进行测试以查看结果,如以下屏幕截图所示:

Custom middleware in action

将 HTTP 模块迁移到中间件

在 ASP.NET Core 之前,ASP.NET 世界有 HTTP 处理程序和 HTTP 模块的概念。它们是请求管道的一部分,可以在整个请求过程中访问生命周期事件。

它们类似于 ASP.NET Core 中的中间件概念。在本节中,我们将把 HTTP 模块迁移到 ASP.NET Core 中间件。

编写 HTTP 模块并在web.config中注册它们超出了本书的范围;我们将参考这篇 MSDN 文章,演练:创建和注册自定义 HTTP 模块https://msdn.microsoft.com/en-us/library/ms227673.aspx ),并迁移到 ASP.NET Core 中间件。

总结本文——当文件扩展名请求为.aspx时,HTTP 模块在响应的开始和结束处追加一个字符串。

创建 C#类AspxMiddleware,该类会查找包含路径.aspx的请求。如果存在,那么它将以适当的文本作为代码的一部分进行响应。还编写了一个扩展方法,用于Startup类中的Configure方法,如下所示:

    using Microsoft.AspNetCore.Builder; 
    using Microsoft.AspNetCore.Http; 
    using System.Threading.Tasks; 

    namespace httpmodule_to_middleware 
    { 
      public class AspxMiddleware 
      { 
        private RequestDelegate _next; 
        public AspxMiddleware(RequestDelegate next) 
        { 
          _next = next; 
        } 

        public async Task Invoke(HttpContext context) 
        { 
          if (context.Request.Path.ToString().EndsWith(".aspx")) 
          { 
            await context.Response.WriteAsync("<h2><font color=red>" + 
             "AspxMiddleware: Beginning of Request" + 
             "</font></h2><hr>"); 
          } 

          await _next.Invoke(context); 

          if (context.Request.Path.ToString().EndsWith(".aspx")) 
          { 
            await context.Response.WriteAsync("<hr><h2><font color=red>" + 
              "AspxMiddleware: End of Request</font></h2>"); 
          } 
        } 
      } 

      #region ExtensionMethod 
      //Extension Method to give friendly name for custom middleware 
      public static class AspxMiddlewareExtension 
      { 
        public static IApplicationBuilder UseAspxMiddleware(this
          IApplicationBuilder app) 
        { 
          app.UseMiddleware<AspxMiddleware>(); 
          return app; 
        } 
      } 
      #endregion 
    } 

Startup类的Configure方法中调用中间件,如下所示:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env,
      ILoggerFactory loggerFactory) 
    { 
      // Calling HTTP module migrated to middleware 
      app.UseAspxMiddleware(); 
      app.Run(async (context) => 
      { 
        await context.Response.WriteAsync("Hello World!"); 
      }); 
    } 

运行应用查看中间件响应,我们将 HTTP 模块的BeingRequestEndRequest转换为中间件类,如下图所示:

HTTP modules into Middleware in action

引入过滤器

中间件是 ASP.NET Core 中一个强大的概念;适当设计的中间件可以减少应用中请求处理的负担。NET Core 应用,无论是 MVC 还是 web API,都在 MVC 中间件上工作,处理身份验证、授权、路由、本地化、模型绑定等。

ASP.NET MVC 应用包含控制器和操作,如果需要,控制它们的执行将非常有用。

过滤器帮助我们在执行管道中的特定阶段之前或之后运行代码。它们可以配置为全局运行、每个控制器或每个操作。

过滤管道

过滤器在 MVC 上下文中运行;它与 MVC 操作调用管道一起运行,该管道也称为过滤器管道

过滤器管道以以下方式执行:

  • 仅当 MVC 中间件接管时启动。这就是过滤器不属于其他中间件的原因。
  • 授权过滤器是第一个运行的;如果未经授权,则会立即使管道短路。
  • 资源筛选器对授权请求生效。它们可以在请求的开始和结束时运行,然后再离开 MVC 管道。
  • 动作是 MVC 的重要组成部分。操作过滤器在控制器的操作执行之前或之后运行。他们可以访问模型绑定参数。
  • 每个动作都会返回结果;结果过滤器与结果(之前或之后)一起使用。
  • 未捕获的异常必然会发生在应用中,处理它们至关重要。对于这些类型的异常,可以全局应用自定义编写的过滤器来跟踪它们。

MVC Filter pipeline

过滤器作用域

过滤器能够将其划分为三个级别,这使它成为 ASP.NET MVC 应用中的一个强大功能。

过滤器可以全局应用,也可以每个控制器或每个操作级别应用:

  • 过滤器的写入属性可以应用于任何级别。如果不是真的需要,则在每个控制器或每个操作上应用全局筛选器。
  • 控制器属性过滤器应用于控制器及其所有动作方法。
  • 操作属性筛选器仅应用于特定操作。这为应用过滤器提供了更大的灵活性。
  • 操作执行时的多个筛选器由 order 属性确定。如果出现相同的命令,则执行从全局级别开始,然后是控制器,最后是操作级别。
  • 当操作运行时,过滤器的顺序颠倒,即从操作到控制器再到全局过滤器。

动作过滤器

动作筛选器是可应用于控制器或特定动作方法的属性。

动作过滤器实现IActionFilterIAsyncActionFilter接口。他们可以查看和直接修改操作方法的结果。

让我们使用IActionFilter接口创建一个简单的操作过滤器。检查表头录入publiser-name;如果其值与Packt不匹配,则返回动作结果BadRequestObjectResult

在 ASP.NET Core Web API 项目中添加CheckPubliserNameAttribute类。复制以下代码段:

    using Microsoft.AspNetCore.Mvc; 
    using Microsoft.AspNetCore.Mvc.Filters; 

    namespace filters_demo  
    { 
      //Action Filter example 
      public class CheckPubliserNameAttribute : TypeFilterAttribute 
      { 
        public CheckPubliserNameAttribute() : base(typeof 
          (CheckPubliserName)) 
        { 
        } 
      } 
      public class CheckPubliserName : IActionFilter 
      { 
        public void OnActionExecuted(ActionExecutedContext context) 
        { 
          // You can work with Action Result 
        } 

        public void OnActionExecuting(ActionExecutingContext context) 
        { 
          var headerValue = context.HttpContext.Request.Headers[
            "publiser-name"];             
          if (!headerValue.Equals("Packt")) 
          { 
             context.Result = new BadRequestObjectResult("Invalid Header");                
          } 
        } 
      } 
    } 

打开ValuesController并将前面代码中创建的Action过滤器添加到Get()方法中,如下代码所示:

    [HttpGet] 
    [CheckPubliserName] 
    public IEnumerable<string> Get() 
    { 
      return new string[] { "value1", "value2" }; 
    } 

我们可以将代码分解如下:

  • CheckPubliserName类实现IActionFilter并检查OnActionExecuting方法中的头值。
  • ValuesController类中,Get方法增加了动作过滤器CheckPubliserName。此方法仅在传递有效标头时返回数据。

使用有效标头运行应用以查看响应。

In this example, we can see the difference between middleware and filter, as filter can be applied to the Action method or Controller.

Action Filter in returns response

授权过滤器

这些过滤器控制对操作方法或控制器的访问。它们是第一个在过滤器管道中执行的。一旦Authorization过滤器被授权,其他过滤器将被执行。

创建ProductsController在 web API 项目中,下面的代码片段在控制器级别添加了Authorize属性。表示未经授权不能访问任何操作方法。

Get()方法用AllowAnonymous属性修饰;它允许访问操作方法:

    using Microsoft.AspNetCore.Authorization; 
    using Microsoft.AspNetCore.Mvc; 
    using System; 

    namespace filters_demo.Controllers 
    { 
      [Route("api/[controller]")] 
      [Authorize] 
      public class ProductsController : Controller 
      { 
        // GET: api/values 
        [HttpGet] 
        [AllowAnonymous] 
        public string Get() 
        { 
          return "Year is " + DateTime.Now.Year.ToString(); 
        } 

        // GET api/values/5 
        [HttpGet("{id}")] 
        public string Get(int id) 
        {    
          return "value is " + id; 
        } 
      } 
    } 

运行应用并使用 Postman 访问产品 API。

异常过滤器

在 ASP.NET Core 中,异常可以通过两种方式处理:通过UseExceptionHandler中间件或使用IExceptionFilter接口编写我们自己的异常处理程序。

我们将使用IExceptionFilter接口编写一个自定义异常处理程序,在 MVC 服务中全局注册它,并对其进行测试。

创建PacktExceptionFilter类,实现IExceptionFilter接口和OnException方法。此筛选器读取异常并准备将其发送到客户端,如下所示:

    using Microsoft.AspNetCore.Http; 
    using Microsoft.AspNetCore.Mvc.Filters; 
    using System; 
    using System.Net; 

    namespace filters_demo 
    { 
      // Exception Filter example 
      public class PacktExceptionFilter : IExceptionFilter 
      {  
        public void OnException(ExceptionContext context) 
        { 
          HttpStatusCode status = HttpStatusCode.InternalServerError; 
          String message = String.Empty; 

          var exceptionType = context.Exception.GetType(); 

          if (exceptionType == typeof(ZeroValueException)) 
          { 
             message = context.Exception.Message; 
             status = HttpStatusCode.InternalServerError; 
          } 
          HttpResponse response = context.HttpContext.Response; 
          response.StatusCode = (int)status;             
          var err = message; 
          response.WriteAsync(err); 
        } 
      } 
    } 

创建ZeroValueException类作为Exception类:

    using System; 
    namespace filters_demo 
    { 
      public class ZeroValueException : Exception 
      { 
        public ZeroValueException(){} 

        public ZeroValueException(string message) 
        : base(message) 
        { } 

        public ZeroValueException(string message, Exception innerException) 
        : base(message, innerException) 
        { } 
      } 
    } 

打开Startup类,修改ConfigureServices方法,将 MVC 服务包含在异常过滤选项中:

    public void ConfigureServices(IServiceCollection services) 
    {             
      // Exception global filter added 
      services.AddMvc(config => { 
        config.Filters.Add(typeof(PacktExceptionFilter)); 
      }); 
    } 

创建EmployeeControllerweb API 控制器。下面的代码片段显示,如果id0,则Get()方法抛出异常:

    using System; 
    using Microsoft.AspNetCore.Mvc; 

    namespace filters_demo.Controllers 
    { 
      [Route("api/[controller]")] 

      public class EmployeeController : Controller 
      { 
        // GET api/values/5 
        [HttpGet("{id}")] 
        public string Get(int id) 
        { 
          if (id == 0) 
          { 
             throw new ZeroValueException("Employee Id Cannot be Zero"); 
          } 
          return "value is " + id; 
        } 
      }
    } 

运行应用,通过将 id 传递为零(0)来访问EmployeeAPI。应用以异常消息响应我们:

Exception filter in action

总结

在本章中,您学习了大量关于中间件、请求管道的知识,并了解了中间件及其顺序。我们编写了定制中间件,并将 HttpModule 迁移到中间件。中间件的概念是请求处理的核心。

您深入了解了过滤器、其管道、排序,并创建了操作过滤器和异常过滤器,还了解了授权过滤器。

在下一章中,我们将着重于编写单元测试和集成测试。

七、执行单元和集成测试

没有一个系统是 100%正确的。每个系统或过程都有缺陷。要知道你写的每件事都是不正确的——它可能会发生变化,需要纠正。世界上一些最好的系统就是围绕这一事实建模的。

特别是航空业,我们知道除了最近发生的事故外,事故很少。他们的风险是围绕系统性故障或瑞士奶酪模型建模的,如下图所示:

系统的每个组件都可能有 bug,这是意料之中的。当孔排成一条直线并暴露出缺陷时,问题就产生了。一路上有一组足够的测试可以帮助解决这个问题。

在本章中,我们将介绍以下主题:

  • 测试驱动开发
  • 集成 API 测试
  • 单元测试

Bob 叔叔关于测试驱动开发的三条规则

以下是关于测试驱动开发TDD的一些指南:

  • 仅编写代码以使测试通过
  • 编写测试时,编写最小值以使测试失败;这包括未编译的代码
  • 编写最少的代码以通过测试

话虽如此,另一条经验法则是红绿重构。

红绿重构

写一个测试;如果没有编译,这是红色的。让它过去,那是你的绿色。然后将代码重构,而不是单元测试,重构到你的核心内容,这就是你的重构。

所以红色,绿色,重构,这应该是你的口头禅。

我知道我已经开始了前面章节中的生产代码。如果这是一本关于 TDD 的书,那么我会从测试开始。我们的目标是引入 ASP.NET Core 2.0。

我们将回到我们开始的例子,Puhoi 奶酪,我将重述我们正在存储并可以检索我们拥有的一些存储。商店有一个描述和它拥有的产品数量,以及其他数据。

假设我们想在此基础上进行扩展,并提供一些有关产品的信息。

产品将有名称、一些描述、价格、库存数量和尺寸(目前,我们将保持尺寸简单)。我们开始吧。

首先创建一个测试项目——我们将从我们的模型开始,然后逐步进行:

我为我们的ProductModel测试创建了一个新类,如下所示:

让我们编写第一个测试,考虑红绿重构和 TDD 的三条规则。

通过创建一个不存在的新ProductModel类,我们有编译错误,这是我们的红色,如下所示:

    [TestMethod] 
    public void BeABaseModel() 
    { 
      // arrange 
      ProductModel model = new ProductModel(); 

要使此测试通过,我们需要创建一个ProductModel类:

添加对测试项目的引用,现在我们可以构建我们的测试项目。让我们完成测试:

    [TestMethod] 
    public void BeABaseModel() 
    { 
      // arrange 
      ProductModel model = new ProductModel(); 

      // act 
      BaseModel baseModel = (BaseModel) model; 

      // assert 
      baseModel.Should().NotBeNull(); 
    } 

我使用FluentAssertions来表示更受行为驱动的语法;添加对FluentAssertions的 NuGet 引用。

ProductModel类固定如下:

    public class ProductModel : BaseModel 

弹回到测试类,并运行它。它应该通过;我们有绿色:

没有什么需要重构的,所以我们将继续进行下一个测试。希望您喜欢第一个测试单元。

让我们开始为模型属性添加测试;我将以Description开头,如下所示:

    [TestMethod] 
    public void HaveADescriptionProperty() 
    { 
      //arrange  
      const string testDescription = "Test Description"; 

      // act 
      ProductModel model = new ProductModel { Description = 
        testDescription };

我们有红色的。

Description添加到ProductModel类中,如下所示:

    public class ProductModel : BaseModel  
    { 
      public string Description { get; set;} 

然后我通过添加如下断言来完成对Description的测试:

    [TestMethod] 
    public void HaveADescriptionProperty() 
    { 
      //arrange 
      const string testDescription = "Test Description"; 

      // act 
      ProductModel model = new ProductModel { Description = 
      testDescription }; 

      // assert 
      model.Description.Should().Be(testDescription); 
    } 

这是一种模式,可以应用于我们需要使用 TDD 的一整套需求的模型。

以下屏幕截图显示了一整套通过测试:

结果模型如下所示:

    public class ProductModel : BaseModel 
    { 
       public string Description { get; set; } 
       public string Name { get; set; } 
       public int Count { get; set; } 
       public string Size { get; set; } 
    } 

接下来,我们继续讨论验证器;与之前一样,我们将使用 Fluent 验证来帮助我们的验证案例。

我创建了一个Valid模型,并将其称为Green模型,如下所示:

    public static class GreenProductModel 
    { 
      public static ProductModel Model() 
      { 
        return new ProductModel 
        { 
          Id = Guid.NewGuid(), 
          Name = "Gold Strong Blue Cheese", 
          Description = "Award winning Blue Cheese from Puhoi Valley", 
          Count = 10000, 
          Size = "Small" 
        }; 
      } 
    }  

然后我们将创建一个测试,说明当我们有一个绿色模型时,我希望验证计数为零:

    [TestMethod] 
    public void ReturnNoValidationErrorsForAGreenModel() 
    { 
      // arrange 
      ProductModel model = GreenProductModel.Model(); 

      // act 
      IEnumerable<ValidationResult> validationResult = 
        model.Validate(new ValidationContext(this)); 

很好,我们有红色的。现在让我们让我们的模型有一个验证器:

    public class ProductModel : BaseModel, IValidatableObject 
    { 
      public string Description { get; set; } 
      public string Name { get; set; } 
      public int Count { get; set; } 
      public string Size { get; set; } 

      public IEnumerable<ValidationResult> Validate(ValidationContext
       validationContext) 
      { 
        yield return new ValidationResult(string.Empty); 
      } 
    }  
    [TestMethod] 
    public void ReturnNoValidationErrorsForAGreenModel() 
    { 
      // arrange 
      ProductModel model = GreenProductModel.Model(); 

      // act 
      IEnumerable<ValidationResult> validationResult = 
        model.Validate(new ValidationContext(this)); 

      // assert 
      validationResult.Should().HaveCount(0); 
    } 

我的测试结果是红色的,如以下屏幕截图所示:

现在我们必须通过这个测试。

我在ProductModel类上使用了StoreModel类的 validate 实现,这使我为ProductModel类创建了一个验证器,如下所示:

    public class ProductModelValidator : BaseValidator<ProductModel> 
    { 
      public ProductModelValidator() 
      {  
      } 
    } 

由于没有真正的验证器(这很好),这是我通过测试所需的最少代码量:

现在我将采用绿色模型,对其进行一些更改,使其成为红色模型,然后测试验证以获得验证结果。在这种情况下,我们想要不等于零的东西,我们知道我们正在测试我们的模型:

     [TestMethod] 
     public void ReturnInValidWhenNameIsEmpty() 
     { 
       // arrange  
       ProductModel model = GreenProductModel.Model(); 
       model.Name = string.Empty; 

       //act  
       IEnumerable<ValidationResult> validationResult = 
         model.Validate(new ValidationContext(this)); 

       //assert  
       validationResult.Should().HaveCount(1); 
     } 

前面的代码是我们的红色测试。我使用了绿色模型,并将Name属性设置为空字符串。调用 validate,我希望验证器返回一个无效。

现在,使测试通过的代码如下所示:

    public class ProductModelValidator : BaseValidator<ProductModel> 
    { 
      public ProductModelValidator() 
      { 
        RuleFor(p => p.Name).NotEmpty(); 
      } 
    }  

这将使我们的测试通过,并为属性的其余验证建立模式。

我鼓励您完成其余的验证。

在编写测试代码之后,我最终得到了以下验证器:

    public ProductModelValidator() 
    { 
      RuleFor(p => p.Name).NotEmpty(); 
      RuleFor(p => p.Description).NotEmpty(); 
      RuleFor(p => p.Size).NotEmpty(); 
    } 

这次所有测试都通过了,如此屏幕截图所示:

运行 API 测试

打开 API 项目的命令行,并使用.NET core 工具运行以下命令:

dotnet run

所以我们的 API 在端口5000上运行,或者你可以让 API 在 IIS 上运行——我选择这个,因为它更简单,允许我快速让服务器运行 API,并针对它运行一些测试。第一项测试如下:

    [TestMethod] 
    public async Task ReturnOkForHealthCheck() 
    { 
      // arrange  
      using (HttpClient client = new HttpClient()) 
      { 
        client.BaseAddress = new Uri(_baseUri); 

        //act  
        HttpResponseMessage response = await client.GetAsync("stores/healthcheck"); 

        // assert 
        response.Should().NotBeNull(); 
        response.StatusCode.Should().Be(HttpStatusCode.OK); 
      } 
    }  

在前面的测试代码中,我们调用控制器上的healthcheck端点,并检查它是否为 null,然后我们可以返回一个OK(200)响应。

没什么特别的,但至少有一些测试可以开始。

为了设置基址,我在app.config中设置了这个,并在测试初始化时得到这个值:

    private string _baseUri; 
    private const string _configurationBaseUri = "baseUri"; 

    [TestInitialize] 
    public void TestInit() 
    { 
       _baseUri = ConfigurationManager.AppSettings.Get(_configurationBaseUri); 
    } 

最后,在app.config文件中设置该值,如下所示:

    <appSettings> 
      <add key="baseUri" value="http://localhost:5000/api/"/> 
    </appSettings>  

后创建的测试

    [TestMethod] 
    public async Task ReturnCreatedForAPost() 
    { 
      // arrange  
      using (HttpClient client = new HttpClient()) 
      { 
        client.BaseAddress = new Uri(_baseUri); 
        //act  
        StoreModel storeModel = GreenStoreModel(); 
        HttpResponseMessage response = await client.PostAsJsonAsync(
          "stores", storeModel); 

        // assert 
        response.Should().NotBeNull(); 
        response.StatusCode.Should().Be(HttpStatusCode.Created); 

        // clean up 
        await DeleteStore(client, storeModel.Id); 
      } 
    } 

我们使用标准HttpClient将其包装在 using 语句中,这样当我们退出测试时,客户机就可以被清理干净。这里的通用模式也用于其他操作。

设置基本 URI

我们使用我们的绿色模型,然后使用客户端的PostAsJsonAsync注意,我们不需要对我们的模型做任何事情,只需指向客户端请求 URI,这是我们在测试中建立的。

我们断言响应不应该是空的。此外,我们希望应该创建状态代码。

冲突后的考验

通过这个测试,我们检查我们的服务是否以Conflict的状态响应。我们怎样才能做到这一点?

发送相同的数据两次,这就是我们要做的。

我们像以前一样得到了我们的绿色模型。使用我们的HttpClient调用post,虽然这是一个动作,但在这个特定测试的上下文中,它是我们设置的一部分。现在再次呼叫post,这就是我们的行动。然后,我们断言我们得到了一个冲突:

    [TestMethod] 
    public async Task ReturnConflictForAPost() 
    { 
      // arrange  
      using (HttpClient client = new HttpClient()) 
      { 
         client.BaseAddress = new Uri(_baseUri); 

         //arrange  
         StoreModel storeModel = GreenStoreModel(); 
         storeModel.Name = "ConflictStore"; 
         HttpResponseMessage response = await client.PostAsJsonAsync("
           stores", storeModel); 
         response = await client.PostAsJsonAsync("stores", storeModel); 

         // act  
         response = await client.PostAsJsonAsync("stores", storeModel); 

         // assert 
         response.StatusCode.Should().Be(HttpStatusCode.Conflict); 
      } 
    } 

Put 测试

Put测试中,我们执行 post 请求以创建资源。然后使用相同的模型,这是我们的绿色模型,并更改模型的名称。这就是我们要做的:

    storeModel.Name = "Creamy Cheese"; 

现在我们准备使用我们的HttpClient调用put方法,然后断言我们的响应是 OK:

    [TestMethod] 
    public async Task ReturnOkForAPut() 
    { 
      // arrange  
      using (HttpClient client = new HttpClient()) 
      { 
        client.BaseAddress = new Uri(_baseUri); 

        //arrange 
        StoreModel storeModel = GreenStoreModel(); 
        storeModel.Id = Guid.NewGuid(); 
        HttpResponseMessage response = await client.PostAsJsonAsync("stores",
          storeModel); 
        storeModel.Name = "Creamy Cheese"; 

         // act  
         string putUri = string.Format("stores/{0}", storeModel.Id); 
         response = await client.PutAsJsonAsync(putUri, storeModel); 

         // assert 
         response.StatusCode.Should().Be(HttpStatusCode.OK); 
      } 
    } 

删除测试

Delete测试中,我们使用与前面测试相同的模式。我们像以前一样创建资源。然后使用HttpClient调用Delete,并断言我们得到的响应是 OK:

    [TestMethod] 
    public async Task ReturnOkForDelete() 
    { 
      // arrange  
      using (HttpClient client = new HttpClient()) 
      { 
        client.BaseAddress = new Uri(_baseUri); 

        //arrange  
        StoreModel storeModel = GreenStoreModel(); 
        storeModel.Id = Guid.NewGuid(); 
        HttpResponseMessage response = await
           client.PostAsJsonAsync("stores", storeModel); 

        // act  
        string deleteUri = string.Format("stores/{0}", storeModel.Id); 
        response = await client.DeleteAsync(deleteUri); 

        // assert 
        response.StatusCode.Should().Be(HttpStatusCode.OK); 
      } 
    } 

xUnit 测试

MS 测试的另一种选择是 xUnit,一个开源单元测试框架。熟悉 nUnit 和 xUnit 的人都会喜欢在.NETCore 和 VisualStudio 中使用它。Microsoft 已将 xUnit 与.NET core SDK 打包,因此您不必将 xUnit 作为单独的软件包安装。

模型测试

创建新项目;这将是.NET Core 的类库。

我把它命名为Puhoi.Models.xUnit.Tests。我将编写本章前面为模型创建的相同测试,但作为 xUnit 测试,然后我们将查看差异:

我将GreenProductModel复制到项目中;可以执行此操作,也可以将文件添加为快捷方式。

为了能够使用 models 项目和公共库(即.NET 程序集),我必须将它们重新创建为.NET Core 程序集。您将在存储库中找到这些文件的副本。

现在,您应该在解决方案资源管理器中看到:

因此,添加对Puhoi.Models.Core的引用。

现在我们有了内部核心组件,我们现在需要外部组件。如果您还记得,我们在测试中使用了FluentAssertions作为我们的断言。

FluentAssertions是预释放,所以您必须记住勾选预释放框:

创建一个新类ProductModelShould,并添加以下测试:

    [Fact] 
    public void BeABaseModel() 
    { 
      // arrange  
      ProductModel model = new ProductModel(); 

      // act  
      BaseModel baseModel = (BaseModel)model; 

      // assert 
      baseModel.Should().NotBeNull(); 
    } 

注意我们的测试方法上面的Fact属性。此外,该类没有使用TestClass装饰。

打开命令窗口,转到测试项目所在的位置,即..\Puhoi.Models.xUnit.Tests,运行以下命令:

  • dotnet restore
  • dotnet build
  • dotnet test

我们从命令行恢复project.json中的包,编译测试项目,最后运行测试。您可以看到这样做的好处,它可以是对您的环境进行批处理,并最终在您的连续构建和部署上运行。

测试结果将输出到控制台:

    [TestMethod]
    public void BeABaseModel() 
    { 
      // arrange  
      ProductModel model = new ProductModel(); 

      // act  
      BaseModel baseModel = (BaseModel) model; 

      // assert 
      baseModel.Should().NotBeNull(); 
    } 

如果我们比较我们在 MS 测试中编写的测试,除了将TestMethod属性替换为Fact之外,其他测试都是相同的。

这并不是说您可以进行 MS 测试并交换属性,您还可以进行 xUnit 测试。让我们进一步研究我们编写的一些测试,以及如何将它们转换为 xUnit,并演示 xUnit 可以与.NET Core 程序集一起使用。

验证器类

此类验证我们在项目中使用的model对象:

    [Fact]
    public void ReturnNoValidationErrorsForAGreenModel() 
    { 
       // arrange  
       ProductModel model = GreenProductModel.Model(); 

       // act  
       IEnumerable<ValidationResult> validationResult = 
         model.Validate(new ValidationContext(this)); 

       // assert  
       validationResult.Should().HaveCount(0); 
    } 

前面的测试看起来非常类似于我们的 MS 测试,没有什么太令人兴奋的。

API 测试

让我们尝试复制我们的 API 测试。创建一个名为PuhoiAPI.xUnit.Tests的新.NET Core 项目,并将project.json文件更改如下。

添加具有以下条目的appsettings.json文件:

    { 
      "baseUri": "http://localhost:5000/api/" 
    } 

如前所述,添加一个名为StoresControllerShould的类。如前所述复制RedStoreModelGreenStoreModel

这是构造器:

    public StoresControllerShould() 
    { 
       var builder = new ConfigurationBuilder().AddJsonFile("appsettings.json"); 
       var config = builder.Build(); 
       _baseUri = config[_configurationBaseUri]; 
       _jsonSerializer = new JsonSerializer(); 
    } 

我们使用ConfigurationBuilder类来读取我们的appsettings文件,它是 JSON 格式的。

这与我们在 MS 测试中得到的不同。当我们编写第一个测试时,很明显我们为什么需要JsonSerializer方法。

我们的healthcheck测试如下:

    [Fact]     
    public async void ReturnOkForHealthCheck() 
    { 
      // arrange  
      using (HttpClient client = new HttpClient()) 
      { 
        client.BaseAddress = new Uri(_baseUri); 

        //act  
        HttpResponseMessage response = await client.GetAsync("stores/healthcheck"); 

        // assert 
        response.Should().NotBeNull(); 
        response.StatusCode.Should().Be(HttpStatusCode.OK); 
      } 
    } 

打开 API 项目的命令行,键入dotnet run。这将启动托管 API 的 web 服务器。从 Visual Studio 中运行测试,或使用以下命令从命令行运行测试:

  • dotnet restore
  • dotnet build
  • dotnet test

考试会通过的。接下来,让我们编写一个测试,将一些数据发布到 API:

    [Fact] 
    public async Task ReturnCreatedForAPost() 
    { 
      // arrange  
      using (HttpClient client = new HttpClient()) 
      { 
         client.BaseAddress = new Uri(_baseUri); 
         StoreModel storeModel = GreenStoreModel(); 
         HttpContent httpContent = SerializeModelToHttpContent(
           storeModel); 

         //act  
         HttpResponseMessage response = await client.PostAsync(
           "stores", httpContent); 

         // assert 
         response.Should().NotBeNull(); 
         response.StatusCode.Should().Be(HttpStatusCode.Created); 
         await DeleteStore(client,storeModel.Id); 
      } 
    } 

因此,与我们的 MS 测试相比,前面的测试有一些不同之处。我们得到了我们需要的模型,但是我们必须将模型序列化到HttpContent。为什么?我们的HttpClientin.NET Core 与普通的.NET 库不同。PostSync方法采用 URI 和HttpContent。在此之后,我们的断言是相同的:

    private HttpContent SerializeModelToHttpContent(object obj) 
    { 
      string storeModelJSon = _jsonSerializer.Serialize(obj); 

      HttpContent stringContent = new StringContent( 
        storeModelJSon, 
        Encoding.UTF8, 
        "application/json"); 

      return stringContent; 
    } 

我们使用JsonSerializer将对象序列化为字符串。这是转换的一个示例:

    { 
      "NumberOfProducts":5, 
      "DisplayName":"API ", 
      "Description":"Green Model", 
      "Id":"036c4610-c9c2-47ea-9aff-39e2341916e1", 
      "Name":"Test" 
    } 

与 MS 测试相比,这是唯一的区别,对PUT也是如此。

    string putUri = string.Format("stores/{0}", storeModel.Id); 
    response = await client.PutAsync(putUri, httpContent); 

Get 保持不变,因为我们不发送任何数据作为有效负载的一部分。

总结

在本章中,我们介绍了使用 Bob 叔叔的三条定律进行测试驱动的开发,同时还介绍了红绿重构来帮助我们进行测试驱动的开发。

我们使用更多的行为驱动断言创建了一些断言。然后,我们将所有这些应用于测试我们的 API,就好像我们正在运行集成测试一样。如果您已经设置了自动部署,那么所有这些都是在上下文中进行的,并且您需要确定您的 API 是完全功能的。

然后,我们介绍了 xUnit 测试,以确定何时需要将一些程序集创建为纯.NET Core 程序集。我们验证了我们用 MS 测试创建的东西也可以用 xUnit 创建的理论。

在下一章中,我们将重点介绍如何为 web API 实现不同的安全机制。

八、Web API 安全

Web API 使用数据服务请求,并通过 HTTP(即 internet)使用处理后的数据进行响应。WebAPI 以 CRUD 操作的形式处理机密、个人或业务相关的数据。任何外行都会明白,对数据的 CRUD 操作不应该由每个人执行。

无论是出于积极还是消极的意图,web API 设计都将暴露在外部世界中,以便在未经许可的情况下潜入。web API 的安全性应该是我们的首要任务,重点应该放在谁将访问它,他们将访问什么,以及传输的数据有多安全。

在这个由 web 应用、移动应用、服务器-服务器通信、桌面应用等组成的异构世界中,web API 的安全性应该是无缝的,以避免在切换客户端时遇到麻烦。它应该按照身份验证和授权概念进行设计。

在本章中,我们将介绍以下主题:

  • 理解威胁模型和 OWASP
  • 应用 SSL
  • 科尔斯
  • 数据保护 API
  • 保护 web API
  • 实现 JWT
  • 基于索赔的授权
  • webapi 中的身份管理

理解威胁模型和 OWASP

从一开始,直到应用在生产中使用,它都会受到各种各样的威胁。这些不同类型的威胁可能会使应用无法成功使用。因此,应对这些威胁非常重要。

威胁模型

识别和分类的方法以及应对威胁的过程称为威胁建模。这个过程的结果是一个威胁模型。这个过程不仅仅与代码审查、遵循编码标准或部署过程有关。

威胁建模更多地包括分析应用的安全性,在 SDLC 的早期阶段开始时更注重结果。这些威胁来自于编写的代码、部署策略、环境、其他应用和硬件故障。

大体上,威胁可以根据其性质分为三类:分解应用、对威胁进行排序,以及策略、应对措施和缓解措施。

分解应用是最基本的部分,因为它有助于更好地理解应用。这涉及到创建使用应用的用例,以及它与外部实体(如软件、补丁、其他应用、服务等)的交互。

丰富的文档帮助我们保持更新,并帮助任何新加入者很快了解应用。可以记录各种类型的信息,例如应用名称、版本、所有者、外部依赖项列表、用户数据输入的入口点、使用的各种资产以及对直接影响应用的各种依赖项的不同信任级别。

威胁排名将为我们提供优先威胁列表。该清单将确保持续关注最主要的威胁类别,这可能会减少威胁一再受到影响的可能性。

可以计算威胁的恐惧(损害+再现性+可利用性+受影响用户+可发现性)分数,以确定其优先级,Microsoft 恐惧威胁风险排名模型是对威胁进行分类的最佳方法之一。

当威胁袭来时,我们不能坐视不管,思考该怎么办。为此,需要建立相应的应对措施和缓解措施,以便在最短的停机时间内启动并运行应用。

构成通用基础的一些应对措施包括身份验证、授权、配置管理、应用数据备份、错误处理、数据验证和日志记录。还可以根据应用敏感性考虑拒绝服务和提升权限。

有关威胁建模的更多信息,请参见https://www.owasp.org/index.php/Application_Threat_Modelin

OWASP

在互联网快速发展的几年中,它的使用既有积极的一面,也有消极的一面。负面方面更具挑战性,因为几乎所有类型的行业都使用 web 应用。

Web 应用经常成为漏洞的目标,提供了太多的信息,无法浏览,并且没有安全连接。

OTASP OLD T1,也称为 OutT2A.开放 Web 应用安全项目 AutoT3E.Fund,整理了许多步骤,以维护健康、安全和高效的 Web 应用而不受威胁。

在我们探索 web API(基于 REST 的服务)时,我们将简要探讨其中的几个威胁:

  • 密码、会话令牌和 API 密钥(任何敏感信息)不应出现在 URL 中。由于 URL 可以在服务器日志中捕获,这就像是自愿提供信息。

  • 使用 OAuth 2.0 或 OpenID Connect 作为身份验证和授权协议。

  • 为适当的方法保护 HTTP 方法。有时,保护每个 HTTP(GetPostPutDelete是不可取的。RESTful API 需要端点(如产品)在无需身份验证的情况下获取;但是,要添加、更新或删除,我们需要保护这些方法。

  • 正确使用授权。在任何给定的应用中,仅仅作为经过身份验证的用户不应该有删除资源的自由。检查用户是否具有适当的角色,否则返回 403 禁止。

  • 输入验证起着重要的作用,因为 WebAPI 没有自己的 UI。请求模型验证应该严格,以确保存储正确的数据。

  • 应设置 API 速率限制以限制请求数。在暴力攻击期间,这一点尤其重要,以摧毁服务。

  • 响应数据应视需要而定。一个产品类可能有 30 个属性,但它不会将所有属性返回给客户机。

  • 应验证内容类型,以便不处理不需要的请求。

  • 参数化值应传递给数据访问层,以避免 SQL 注入。使用 ORMs 可以解决这个问题。

在设计 web API 时,还可以考虑许多其他因素。通读https://www.owasp.org/index.php/REST_Security_Cheat_Sheet 了解更多信息。

应用 SSL

当 web API 请求和响应通过 internet(HTTP)传输时,我们可能会加密密码,但其余的应用数据会暴露在 internet 上。中间的人可以在客户端应用和 Web API 应用之间读取这些值。

当连接不安全时,很可能会看到数据被传输;为了克服这个问题,我们可以使用 HTTPS 使用SSL安全套接字层)对连接进行加密。应用此方法时,与 web API 的通信应使用 HTTPS 而不是 HTTP 进行。

在 ASP.NET Core 中,可以使用RequireHttps属性强制 SSL,或者通过对IServiceCollection应用过滤器全局启用 SSL。由于我们的目的是使整个应用安全,因此我们应该应用全局过滤器来使用 HTTPS。

打开Startup.cs类,如果使用非开发环境,则将服务配置为使用 HTTPS。我们可以在非开发环境中启用 HTTPS,如以下代码段所示:

    public void ConfigureServices(IServiceCollection services) 
    { 
      // Add framework services. 
      services.AddMvc(); 

      //Checks if non Development environment, then enables HTTPS attribute 
      if (!env.IsDevelopment()) 
      { 
        services.Configure<MvcOptions>(o => o.Filters.Add(new
          RequireHttpsAttribute())); 
      } 
    } 

您可以将 ASP.NET Core 环境变量ASPNETCORE_ENVIRONMENT更改为生产或登台,以查看 HTTPS 的运行情况。

First-time users need to at least enable a self-signed certificate to work with HTTPS. Creating, procuring, and enabling SSL on ASP.NET Core on IIS or Nginx is beyond the scope of this book, but plenty of resources exist on internet. Check out the following links for your reference: https://www.digicert.com/ssl-certificate-installation-microsoft-iis-7.htm and https://www.digicert.com/ssl-certificate-installation-nginx.htm

科尔斯

跨源资源共享CORS允许跨源应用访问该应用。对于 WebAPI,它是一个匿名应用,接收请求并返回响应;但是,当在另一个 web 应用中使用此 web API 时(使用 JavaScript 中的 AJAX 调用 API),客户机将位于不同的域上。

考虑一个例子,Web API 被托管为 AutoT0. www. PoptDeMo.COM/API Oracle T1。当 web 应用调用时,API 响应为请求的资源上不存在访问控制允许来源标头。这意味着不允许您的域访问 API 资源。

此 CORS 概念还可用于限制任何不需要的 web 应用访问 web API。这背后的想法是在 ASP.NET Core 启动处理中添加 CORS 策略,并全局或按控制器应用它们。

在本章中,我们将构建一个演示 ASP.NET Core Web API 项目PersonalBudget,以跟踪个人支出。添加名称为BudgetCategoryController的 web API 控制器。

使用 index.html 页面创建两个简单的 web 应用,并使用XMLHttpRequest调用前面的 web API 控制器BudgetCategoryController,使用GET方法。您将在浏览器控制台中看到类似错误,如以下屏幕截图所示:

CORS error on browser console

现在,让我们将 CORS 策略添加到 web API 项目中。打开Startup类并用以下代码更新:

    public void ConfigureServices(IServiceCollection services) 
    { 
       // Add framework services. 
       services.AddMvc(); 

       //Code removed for brevity 
       services.AddCors(options => 
       { 
          options.AddPolicy("DemoCorsPolicy", 
          c => c.WithOrigins("http://localhost:3000/")); 
       }); 
    }         
    public void Configure(IApplicationBuilder app, IHostingEnvironment env,
       ILoggerFactory loggerFactory) 
    { 
        //Code removed for brevity 
        app.UseMvc(); 
        app.UseCors("DemoCorsPolicy"); 
    } 

您可以按如下方式分解前面的代码:

  • 添加名称为DemoCorsPolicy的 CORS 策略

  • 此策略将只允许来源为localhost:3000的请求

  • 然后将策略放置在Configure方法中的 HTTP 管道处理中

  • 要允许每个 web 应用访问 web API,请使用*而不是域名

运行 web API 和 web 应用;演示 web API 1 可以成功接收响应,演示 web API 2 将接收错误,如前面与 CORS 相关的代码所示。

The CORS policy can include varieties of combinations, such as Origins, Methods, Headers, and so on, making them more flexible.

数据保护 API

ASP.NET Core 使用Microsoft.AspNetCore.DataProtection处理加密密钥,用于保护在应用和客户端之间发布的状态值。

Machine.config密钥不再用于 ASP.NET Core 中的数据保护。数据保护是一个相当广泛的话题;您可以参考 Microsoft 文档(https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/ )了解更多信息。

The Cookie generation takes places using Data Protection APIs.

我们将以加密给定实体的 ID 值为例。

考虑具有由 ID 唯一标识的各种属性的 AuthT0.类。当我们检索预算类别或单个对象的列表时,也应该包含所传递的 ID。由于此 ID 对业务非常敏感,因此我们不必传递保存在数据库中的真实 ID。

对于这类需求,我们可以在响应时加密,在接收请求时解密。我们可以通过数据保护 API 实现这种行为。

继续前面的项目,创建两个类BudgetCategoryBudgetCategoryDTO,后一个类以EncryptId作为字符串,这是 ID 的加密版本。BudgetCategoryDTO用于请求和响应操作,对象映射使用AutoMapper完成。

数据保护 API 通过ConfigureServices方法中Startup类中的services.AddDataProtection()添加到服务中。创建一个具有属性的StringConstants类来保存用于加密和解密的密钥;这称为目的键:

public class StringConstants 
    { 
        public string IdQryStr => "AppIdString";  
//Name can be anything. 
    } 

当我们使用AutoMapper进行对象映射时,我们需要借助数据保护 API、IDataProtectionProvider接口和目的键将整数 ID 转换为字符串 ID。

使用自定义的IdProtectorConverter类型转换器将整数 ID 映射到字符串 ID,这需要在Configure方法的 HTTP 管道处理中配置,如下代码所示:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env,
      ILoggerFactory loggerFactory, 
      IDataProtectionProvider dataprovider, StringConstants strconsts) 
    { 
      loggerFactory.AddConsole(); 
      AutoMapper.Mapper.Initialize(cfg => 
      { 
        cfg.ConstructServicesUsing(type => new
          IdProtectorConverter(dataprovider, strconsts)); 
        cfg.CreateMap<BudgetCategoryDTO, BudgetCategory>(); 
        cfg.CreateMap<BudgetCategory, BudgetCategoryDTO> 
         ().ConvertUsing<IdProtectorConverter>(); 
      }); 
    } 

自定义类型转换器IdProtectorConverterAutoMapper使用,编写如下:

    namespace PersonalBudget 
    { 
      public class IdProtectorConverter : ITypeConverter<BudgetCategory,
        BudgetCategoryDTO> 
      { 
        private readonly IDataProtector protector;        
        public IdProtectorConverter(IDataProtectionProvider
          protectionprovider, StringConstants strconsts) 
        { 
          this.protector = protectionprovider.CreateProtector
            (strconsts.IdQryStr); 
        } 
        public BudgetCategoryDTO Convert(BudgetCategory source,
          BudgetCategoryDTO destination, ResolutionContext context) 
        { 
          return new BudgetCategoryDTO 
          { 
             Name = source.Name, 
             Amount = source.Amount, 
             EncryptId = this.protector.Protect(source.Id.ToString()) 
          }; 
        } 
      } 
    } 

您可以按如下方式分解前面的代码:

  • BudgetCategory是源,BudgetCategoryDTO是传递给调用 web API 的客户端的目标
  • IDataProtectionProviderStringConstants被 DI 到构造函数中创建IDataProtector
  • Convert方法中,使用this.protector.Protect(source.Id.ToString())对 ID 值进行加密

现在,客户机接收到一个加密的 ID,他们将发送该 ID 以获取特定记录。此时,我们需要解密此 ID 以传递数据源以获取记录。

我们需要调用IDataProtectorUnprotect方法将其转换回整数 ID;以下代码段执行此任务:

    [Route("api/[controller]")] 
    public class BudgetCategoryController : Controller 
    { 
       private readonly PersonalBudgetContext _context; 
       private readonly IDataProtector protector; 

       public BudgetCategoryController(PersonalBudgetContext context, 
          IDataProtectionProvider protectionprovider,
          StringConstants strconsts) 
       { 
          this.protector = protectionprovider.CreateProtector(
            strconsts.IdQryStr); 
          _context = context; 
       } 

       [HttpGet("{id}")] 
       public IActionResult Get(string id) 
       { 
          var decryptId = int.Parse(protector.Unprotect(id)); 
          var item = _context.BudgetCategories.Find(decryptId); 
          if (item == null) 
          { 
              return NotFound(); 
          } 
          var results = Mapper.Map<BudgetCategoryDTO>(item); 
          return Ok(results); 
        } 
    } 

运行应用添加几个BudgetCategory元素,并使用 Postman 检索其列表。以下屏幕截图显示了加密的 ID:

Data Protection API in action

保护 web API

API 控制器类是执行满足业务需求的最基本工作的 web API 应用的核心。我们需要保护它们不被未经授权的访问。

web API(控制器)使用应用于控制器的Authorize属性进行保护。除非识别了客户端调用,否则不会提供对控制器操作方法的访问。

以下代码片段显示应用于BudgetCategoryControllerAuthorize属性访问此 API 端点,并将导致未经授权的响应(HTTP 401 状态代码):

    using Microsoft.AspNetCore.Authorization; 

    namespace PersonalBudget.Controllers 
    {  
      [Authorize] 
      [Route("api/[controller]")] 
      public class BudgetCategoryController : Controller 
      { 
        ... 
      } 
    } 

尝试在浏览器或邮递员上访问此端点将导致未经授权的访问,如以下屏幕截图所示:

Shows Unauthorized response on protected API endpoint

实现 JWT

JWT也称为JSON 网络令牌;它们是安全令牌的行业标准,用于作为 JSON 对象在客户端和服务器之间安全地传输信息。

它们因其独立、小型和完整而被广泛使用。由于尺寸较小,它们可以通过 URL、POST 参数或 HTTP 头内部发送。

JSON Web 令牌包含凭证、声明和其他信息。要了解更多关于 JWT 的信息,我建议阅读https://jwt.io/introduction/

JWT 如此流行的原因之一是,当它与 web API 一起使用时,使用它们的客户端可以轻松工作,无论是移动应用、混合应用、web 应用,还是任何基于桌面应用或服务的编程语言。

示例 JWT 是一个加密字符串,包含用于安全通信的信息,如以下屏幕截图所示:

JSON web token sample

使用 JWT on web API 进行安全通信的工作流程可以在 JWT.io 网站上的下图中进行说明。这些步骤不言自明。

JWT and web API in action (Image Credit - jwt.io)

生成 JWTs

第一步是在 ASP.NET Core web API 项目中生成 JSON web 令牌。为此,让我们创建一个 web API 控制器--AuthController

在此AuthController中,我们将使用包含用户名和密码的CredentialsModelAppUsers进行核对。如果用户存在,那么我们将生成一个 JWT 令牌,作为响应传递。

我们将使用实体框架核心内存提供程序生成数据库上下文和表,并为其种子数据。请参阅第 9 章与数据库的集成、中关于使用 EF 6、EF Core 和 Dapper ORM 处理不同数据库的内容。

AuthController代码将使用PersonalBudgetContext,这是 EF 核心数据库上下文,以及IConfigurationRoot,它读取appsettings.json文件并包含生成 JWT 的config条目:

    namespace PersonalBudget.Controllers  
    {     
      public class AuthController : Controller 
      { 
        private readonly PersonalBudgetContext _context; 
        private readonly IConfigurationRoot _config; 
        public AuthController(PersonalBudgetContext context,
          IConfigurationRoot config) 
        { 
            _context = context; 
            _config = config; 
        } 

        [HttpPost("api/auth/token")] 
        public IActionResult CreateToken([FromBody] CredentialsModel model) 
        { 
            if (model == null) 
            { 
                return BadRequest("Request is Null"); 
            } 
            var findusr = _context.AppUsers.First(m => m.UserName.Equals(
              model.Username) && m.Password.Equals(model.Password)); 
            if(findusr != null) 
            { 
              var claims = new[] 
              { 
                new Claim(JwtRegisteredClaimNames.Sub, findusr.UserName), 
                new Claim(JwtRegisteredClaimNames.Jti, 
                  Guid.NewGuid().ToString()),                                     
              }; 

              var key = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(_config["Tokens:Key"])); 
              var creds = new SigningCredentials(key,
                SecurityAlgorithms.HmacSha256); 

              var token = new JwtSecurityToken( 
                issuer: _config["Tokens:Issuer"], 
                audience: _config["Tokens:Audience"], 
                claims: claims, 
                expires: DateTime.UtcNow.AddMinutes(12), 
                signingCredentials: creds 
              ); 

              return Ok(new 
              { 
                 token = new JwtSecurityTokenHandler().WriteToken(token),                     
                 expiration = token.ValidTo 
              }); 
            } 
            return BadRequest("Failed to generate Token"); 
          } 
       } 
    } 

您可以按如下方式分解前面的代码:

  • 构造函数使用 DI 获取数据库上下文和配置条目。上下文和配置应该注册在Startup类--ConfigureServices中。
  • CreateToken是生成 JWT 令牌的 HTTP POST 操作方法,取CredentialsModel
  • 检查AppUsers表中是否存在用户名和密码;如果是,则返回对象。
  • JWT 令牌在生成颁发者、受众、声明、过期和签名凭据之前必须具有这些详细信息。
  • 发行人是产生 JWT 的人;在我们的例子中,是 web API。最佳做法是将这些条目保存在配置文件中。
  • 声明将帮助我们生成具有适当声明的 JWT,即用户名、唯一密钥或包含的任何其他信息。
  • Expires 是生成的 JWT 的时间有效性。
  • 签名凭据是最重要的方面,因为它们包含一个强密钥(将它们放在配置文件中)并使用了安全算法。建议使用非常强的键来创建更好的 JWT。
  • 最后,它使用前面的信息生成 JWT 并将其作为响应返回。

运行应用并使用正确的凭据调用 API 端点将产生如下所示的响应:

API endpoint generates JWT

验证 JWT

从图JSON web 令牌示例中,我们实现了 JSON web 令牌的生成,并将其作为响应返回。现在,当任何客户端(web、移动或桌面)使用前面生成的令牌调用 web API 端点时,我们需要验证这个有效的 JWT 是否由我们的应用生成。

如果验证成功,则允许它访问请求的资源,也就是说,用户现在已通过身份验证。如果我们不验证它,那么我们肯定会得到未经授权的响应。

由于这将是 API 请求到达 HTTP 管道的第一步,我们需要使用UseJwtBearerAuthentication中间件在Startup类的Configure方法中添加验证功能。

在 HTTP 管道处理中添加以下代码,以便对 JWT 进行验证:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env,
      ILoggerFactory loggerFactory) 
    { 
      //Rest of code for brevity 
      //JWT Validation  
      app.AddJwtBearerAuthentication( "PacktAuthentication",new JwtBearerOptions() 
      { 
         options.ClaimsIssuer = Configuration["Tokens:Issuer"]; 
         options.Audience = Configuration["Tokens:Audience"]; 
         TokenValidationParameters = new TokenValidationParameters() 
         { 
           ValidIssuer = Configuration["Tokens:Issuer"], 
           ValidAudience = Configuration["Tokens:Audience"], 
           ValidateIssuerSigningKey = true, 
           IssuerSigningKey = new SymmetricSecurityKey(
              Encoding.UTF8.GetBytes(Configuration["Tokens:Key"])), 
           ValidateLifetime = true 
         } 
      }); 
    } 

您可以按如下方式分解前面的代码:

  • AddJwtBearerAuthentication服务接受选项参数JwtBearerOptions来验证 JWT。
  • 在生成 JWT 令牌时,我们使用了一些参数;现在,需要相同的参数来检查请求中令牌的有效性。
  • 参数数据从配置文件中读取。一些参数是ValidIssuerValidAudienceIssuerSigningKey
  • 上述参数中的任何更改都将无法验证 JWT,也就是说,不会验证篡改的令牌。
  • 一旦成功,请求资源就可以访问。

运行应用,并使用 Postman 在标头中使用令牌调用 API 端点,如以下屏幕截图所示:

JWT validation in action

非统组织

OAuth 是用于授权的行业标准协议;它通过为 web 应用、桌面、移动设备等提供特定的授权流来关注客户机开发。

OAuth 的使用需要一个用于基于流的身份验证的 UI。将其与 ASP.NET Core Web API 集成的步骤比想象的要简单得多。参见https://auth0.com/authenticate/aspnet-core-webapi/oauth2 ,并按照此处显示的步骤进行操作。

基于索赔的授权

在前面的部分中,我们看到了如何使用 JWT 实现身份验证,即根据存储的数据识别用户并允许他们访问 web API 资源。

在大多数应用中,我们只需要允许某些经过身份验证的用户执行任务。这也称为授权。

在 ASP.NET Core 中,可以使用授权技术来实现声明。与用于授权的传统角色不同,我们使用 JWT 声明来执行授权。

修改AppUsers以包含IsSuperUser属性。此属性将指示登录用户是否为超级用户。AppUsers类现在包括IsSuperUser属性:

    namespace PersonalBudget.Models   
    { 
      public class AppUser 
      { 
        public int Id { get; set; } 
        public string UserName { get; set; } 
        public string Password { get; set; } 
        public bool IsSuperUser { get; set; } 
      } 
    } 

修改AuthController CreateToken动作方法,将IsSuperUser纳入索赔。这将添加到生成令牌中:

    var claims = new[] 
    { 
       new Claim(JwtRegisteredClaimNames.Sub, findusr.UserName), 
       new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), 
       new Claim("SuperUser", findusr.IsSuperUser.ToString()) 
    }; 

现在,运行应用并调用令牌生成端点,如前面的代码所示。将添加具有SuperUser声明的新令牌。您可以在 jwt.io 网站上验证令牌内容。

使用索赔策略的授权

我们成功地生成了一个带有声明的令牌;现在,是添加索赔政策的时候了。

索赔策略模型由三个主要概念组成:策略名称、需求和处理程序。

  • 保单名称用于标识索赔要求
  • 需求包含策略名称用于评估用户标识的数据参数列表
  • 处理程序评估需求的属性,以检查用户是否有权访问 API 资源

第一步是使用AddAuthorizationStartup类的ConfigureServices方法中注册SuperUser策略。为此,要求检查SuperUser策略是否设置为TRUE

    public void ConfigureServices(IServiceCollection services) 
    { 
      //Claim Authorization Policy 
      services.AddAuthorization(cfg => 
      { 
         cfg.AddPolicy("SuperUsers", p => p.RequireClaim(
           "SuperUser", "True")); 
      }); 

      //Code removed for brevity 
    } 

索赔策略应与控制器级别或操作方法级别的Authorize属性一起使用。这里,我们只希望在 JWT 中具有SuperUser声明的经过身份验证的用户访问POST方法来创建BudgetCategory

    [HttpPost] 
    [Authorize(Policy = "SuperUsers")] 
    public IActionResult Post([FromBody]BudgetCategoryDTO value) 
    { 
       if (value == null) 
       {                 
          return BadRequest(); 
       } 
       if (!ModelState.IsValid) 
       { 
          return BadRequest(ModelState); 
       } 
       var mappeditem = Mapper.Map<BudgetCategory>(value); 
       var newItem = _context.BudgetCategories.Add(mappeditem); 
       Save();             
       var dtomapped = Mapper.Map<BudgetCategoryDTO>(mappeditem); 
       return Ok(dtomapped); 
    } 

运行应用并使用 Postman 或 Fiddler 调用令牌生成 API 端点,如前面的代码所示。

使用非超级用户凭证,然后使用生成的令牌调用BudgetCategoryPOST方法

这注定要失败;然而,有趣的是,它没有给出一个401 unauthorized响应,但给出了一个更清晰的403 Forbidden响应。

The 403 Forbidden response says that you're authenticated but not allowed to perform this operation; that is, you're authenticated but not authorized.

现在,使用SuperUserTRUE尝试相同的场景,并使用新生成的令牌调用POST方法。一切正常,它添加了一个OK响应记录。以下屏幕截图显示了基于索赔的授权的实际情况:

Claims fail and return Forbidden response

具有有效身份验证和授权令牌的超级用户能够访问资源,如下所示:

Claims work and return Ok response

webapi 中的身份管理

ASP.NET Core Identity 是一个成员系统,用于添加登录功能以及创建、更新和删除应用用户。身份识别系统还提供角色管理,将特定角色分配给用户以提供授权。

身份管理是 web 安全的主要任务之一。ASP.NET Core Identity 软件包提供了完成工作所需的所有功能。

It also provides external authentication with Google, Facebook, Twitter, and so on. It's highly recommended that you refer to the following link for a better understanding of Identity:
https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity

我们将为 ASP.NET Core Web API 项目添加身份框架,使用应用用户对其进行身份验证,并研究2FA双因素身份验证)。

添加标识包

使用 web API 模板创建新的 ASP.NET Core 应用,或使用任何现有应用。以下软件包将成为.NET Core SDK 的一部分:

Microsoft.AspNetCore.Identity
Microsoft.AspNetCore.Identity.EntityFrameworkCore  

配置启动类

恢复包后,在Startup类的ConfigureServicesConfigure方法中包含 Identity,如下代码所示:

    public void ConfigureServices(IServiceCollection services) 
    { 
       services.AddTransient<IdentityDbSeeder>(); 
       services.AddDbContext<IdentityDbContext>(options => 
       options.UseSqlServer(Configuration.GetConnectionString("BudgetConnStr"),
         sqlopt => sqlopt.MigrationsAssembly("BudegetIdentityDemo"))); 

       services.AddIdentity<IdentityUser, IdentityRole>() 
         .AddEntityFrameworkStores<IdentityDbContext>();             

       // Add framework services. 
       services.AddMvc(); 
    } 

    public void Configure(IApplicationBuilder app, IHostingEnvironment env,
       ILoggerFactory loggerFactory, IdentityDbSeeder identitySeeder) 
    {            
        app.UseIdentity(); 
        app.UseMvc(); 
        identitySeeder.Seed().Wait(); 
    } 

ASP.NET Core Identity is undergoing breaking changes, and with the final changes of ASP.NET Core 2.0, most of this becomes obsolete. However, a modified code, post release, will be available in the code bundles. Watch out!!

您可以按如下方式分解前面的代码:

  • IdentityDbContext是一个默认的数据库上下文,用于标识使用 EF Core 处理数据库。设置连接字符串,从appsettings.json读取。
  • AddIdentity方法被添加到服务中。IdentityUserIdentityRole方法也被添加到服务中。他们是实际用户及其角色。
  • HTTP 管道中的app.UseIdentity方法表示处理应该通过标识。
  • IdentityDbSeeder类帮助在应用运行时创建示例用户。在真实场景中,我们将有一个单独的 API 端点来添加用户。

创建与身份相关的数据库

使用 EF 命令工具,我们将运行迁移并创建一个标识数据库,然后准备好与标识相关的类和数据库。要使用 EF 命令工具创建数据库,请参阅第 9 章与数据库集成中的 EF 核心部分。

基于 Cookie 的身份验证

在数据库中创建标识表后,运行应用一次以为默认用户种子。(这是一个可选步骤,出于演示目的,我们为数据库种子)。

创建AuthController,复制以下代码读取用户名和密码,对照身份数据库进行验证,返回 cookie:

    using Microsoft.AspNetCore.Identity; 
    using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 
    using Microsoft.AspNetCore.Mvc;  
    using System.Threading.Tasks; 

    namespace BudegetIdentityDemo.Controllers 
    {     
      public class AuthController : Controller 
      { 
        private readonly SignInManager<IdentityUser> _signInMgr;        

        public AuthController(SignInManager<IdentityUser> signInMgr) 
        { 
          _signInMgr = signInMgr;             
        } 

        [HttpPost("api/auth/login")] 
        public async Task<IActionResult> Login([FromBody]
          CredentialModel model) 
        { 
          var result = await _signInMgr.PasswordSignInAsync(model.UserName,
            model.Password, false, false); 
          if (result.Succeeded) 
          { 
             return Ok(); 
          } 
          return BadRequest("Failed to login"); 
        } 
      } 
    } 

您可以按如下方式分解前面的代码:

  • SignInManager包含必要的登录方式,因此其 DI 进入**AuthController**
  • Login操作方法采用CredentialModel(用户名和密码)对Identity数据库进行身份验证
  • PassSignInAsync方法使用凭据模型属性验证帐户并返回OK响应,但反过来,它会在浏览器中设置 cookie 以用于进一步的 API 调用

运行应用,并在凭据匹配后使用 Postman 进行端点调用。返回带有 cookie 的OK响应,如以下屏幕截图所示:

Cookie authentication using Identity

现在,保持 Postman 窗口不变,并再次调用BudgetCategoryController(参考代码示例),即使它具有Authorize属性。cookie 确实会得到验证以返回响应,如以下屏幕截图所示:

API response with Cookie authentication

双因素认证

通常的身份验证方式,即用户名加密码,在大多数情况下有效,但当需要额外的安全性时,如电话号码或电子邮件验证,则需要使用双因素身份验证2FA)。

我们仍然会使用好的旧用户名和密码进行身份验证;但是,同时,如果为用户启用 2FA,那么我们将通过 SMS 或电子邮件发送安全代码,并且只有在输入代码时,才会进行实际登录。

Note that either the SMS or email needs to be confirmed along with 2FA enabled.

我们将使用以下代码创建TwoFactorAuthController

    namespace BudegetIdentityDemo.Controllers 
    { 
      public class TwoFactorAuthController : Controller 
      { 
        private readonly SignInManager<IdentityUser> _signInMgr; 
        private readonly UserManager<IdentityUser> _usrMgr; 
        private IPasswordHasher<IdentityUser> _hasher; 

        public TwoFactorAuthController(SignInManager<IdentityUser>
          signInMgr,
          IPasswordHasher<IdentityUser> hasher,  
        UserManager<IdentityUser> usrMgr) 
        { 
          _signInMgr = signInMgr; 
          _usrMgr = usrMgr; 
          _hasher = hasher; 
        } 

        [HttpGet("api/twofactorauth/login")] 
        public async Task<IActionResult> Login([FromBody] 
          CredentialModel model) 
        { 
          var user = await _usrMgr.FindByNameAsync(model.UserName); 
          if (user != null) 
          { 
             if (_hasher.VerifyHashedPassword(user, user.PasswordHash,
               model.Password) == PasswordVerificationResult.Success) 
             { 
               if (user.TwoFactorEnabled && user.PhoneNumberConfirmed) 
               { 
                 var code = await 
                   _usrMgr.GenerateChangePhoneNumberTokenAsync(
                   user, user.PhoneNumber); 
                 await SendSmsAsync(user.PhoneNumber, "Use OTP " + code); 
               } 
             } 
           } 
           return BadRequest("Failed to login"); 
         } 

         [HttpPost]                 
         public async Task<IActionResult> VerifyCode(
            VerifyCodeViewModel model) 
         { 
           if (!ModelState.IsValid) 
           { 
              return BadRequest(); 
           }             
           var result = await _signInMgr.TwoFactorSignInAsync(
             model.Provider, model.Code, model.RememberMe,
             model.RememberBrowser); 
           if (result.Succeeded) 
           { 
             return Ok(); 
           } 
           return BadRequest("Failed to Login"); 
         } 

         private Task SendSmsAsync(string number, string message) 
         { 
           // Plug in your SMS service here to send a text message. 
           // Use Twilio or clickatell for related docs             
           return Task.FromResult(0); 
         } 
       } 
    } 

您可以按如下方式分解前面的代码:

  • 登录、获取用户详细信息和密码匹配分别需要SiginManagerUserManagerIPasswordHasher类。它们被直接插入控制器。
  • Login动作方法检查用户是否存在,2FA 是否启用,电话号码是否确认。
  • 如果是这样,则通过第三方 SMS 提供商(如 Twilio 或 Clickatell)发送 SMS。
  • VerifyCode检查短信代码是否匹配。如果是,则实际登录发生,返回 cookie。

这个例子涉及多个端点;使用此端点的客户端可以使用单个用户帐户引用 ASP.NET Core(MVC 应用),以检查 UI 流是如何完成的。

ASP.NET Core with Identity and JWT can work.

总结

Web API 安全从一开始就应该是一个优先事项。即使业务需要或不需要,开发人员也应该专注于以 SSL 和 CORS 的形式为 web API 提供正确的安全性,并使用令牌进行身份验证。

授权在 API 使用中也起着重要作用,因为应用用户可能具有不同级别的凭据;我们通过基于索赔的授权对此进行了调查。ASP.NET Core 安全性可以通过使用 Identity Server 4、OpenId 连接机制等进行身份管理来扩展。

在下一章中,我们将学习如何使用市场上流行的 ORM 与数据库集成。

九、与数据库集成

数据是软件应用中的王者,无论是以数据库、文件、流等形式。很难找到不与数据库交互的应用。在上一章中,您学习了大量关于 ASP.NET Core 以路由、中间件和过滤器的形式处理请求及其安全机制的知识;它们构成了应用的匿名前端。

到目前为止,我们还没有讨论过 ASP.NET Core 处理后端(数据存储的流行术语),也就是数据库。任何 ASP.NET Core(Web API)都必须在应用开发期间或从一开始就与数据库集成。

Microsoft SQL Server 通常被认为是 ASP.NET 应用的登录数据库。现在有了 ASP.NET Core,与不同类型数据库的集成比以往任何时候都更有趣。在不同的数据库系统中,如 Oracle、MySQL、PostgreSQL、SQLite 等,集成本质上是跨平台的。

在 ORMs 的帮助下,与数据库的集成是快速、可扩展和高效的。我们将探索微软的 ORM,即现有和新数据库的实体框架(EF 6.x 和 EF Core)和Dapper(Micro ORM)。

在本章中,我们将介绍以下主题:

  • 对象关系映射器简介
  • 使用实体框架 6.x 进行集成
  • 用简洁的语言进行集成
  • 利用 EF 核进行积分

对象关系映射器简介

与数据库集成需要做大量的基础工作来执行简单的CRUD创建读取更新删除等操作操作。一些基础工作包括连接到数据库、释放连接、池、查询数据库、处理单个或多个记录、连接弹性、批量更新等。

为这项基础工作编写代码是一项艰巨的任务,通常以大量手写代码、代码重复、错误结果和维护问题而告终。

对象关系映射器(ORM)以类对象映射到关系数据库表的形式提供了更好的与数据库集成的方法。

ORM 提供了上述必要的基础工作,还使用面向对象的概念将类对象映射到关系数据库表。

例如,广泛使用的 Microsoft SQL Server 学习数据库是 Adventure Works。在基于.NET 的应用中,关系表Production.Product映射到Product类。ORM 不局限于表格;它们处理存储过程、视图、模式迁移等。

一些流行的 ORM 有实体框架(6.x 和 core)、NHibernate、Dapper 等等。

在本章中,我们将重点介绍如何使用实体框架和简洁的 ORM 将数据库与 ASP.NET Core Web API 集成。

使用 Entity Framework 6.x 集成 ASP.NET Core Web API 和现有数据库

实体框架EF是针对.NET world 的 ORM。EF 和其他 ORM 一样,可以用于创建新的数据库和表,或者对现有数据库使用 EF。

我们将使用 EntityFramework6.1 构建 ASP.NET Core Web API,与现有数据库集成。我们将使用 Microsoft SQL Server 学习数据库AdventureWorks2014

EF 6.1 是一个使用完整的.NET 框架构建的 ORM,这意味着它只适用于完整的.NET 应用。为了实现这一点,我们需要为完整的.NET 框架而不是.NET Core 创建 ASP.NET Core。

通过以下步骤,我们将使用 EF 6.1 创建与 AdventureWorks2014 数据库集成的AdvWorksAPI(ASP.NET Core Web API)。

恢复 AdventureWorks2014 数据库

下载并恢复 AdventureWorks2014 数据库备份(https://msftdbprodsamples.codeplex.com/releases/view/125550 )。本例中使用的是 Microsoft SQL Server 2014。

这将作为我们使用 EF 6.1 的现有数据库。您可以使用任何现有数据库。

EF6 数据访问库

如前一节所述,EF6 只在完整的.NET 框架上工作;因此,我们不能在 ASP.NET Core 应用中直接使用它们。为此,我们需要一个类库,并对现有数据库执行反向工程,AdventureWorks2014 就是我们的示例。

逆向工程是实体框架(ORM)中的一个过程,用于生成类/模型(它们对应于表)并构建数据库上下文以使用数据库。

创建一个空白的 Visual Studio 解决方案AdvWorksAPI,并向其添加类库AdvWorksAPI.DataAccess。这将作为数据访问层,并将在 ASP.NET Core 应用中引用。

要使用反向工程生成类/模型,请右键单击项目名称以添加新的 ADO.NET 实体数据模型,并按照步骤连接到数据库,使用实体数据模型向导选择适当的表、存储过程和其他数据库项。

更多可视步骤请参考https://msdn.microsoft.com/en-us/data/jj200620

在此过程中,我们只选择了Production.Product表;这将导致Product类、AdvWorksContext类包含Product中的DbSet进行处理。在 EF 术语中,我们执行了逆向工程代码,这是 AdventureWorks2014 数据库的第一个过程。

In the real world, existing database schema will surely contain many tables, SPs, and so on. Web API's are usually built targeting only relevant tables. By reverse engineering only the relevant tables, we are reducing the in-memory database snapshot generated when EF runs in the application.

It's one of the recommended approaches to use required tables when working with the existing database for EF.

Connect to the existing database using the Entity Data Model wizard

为完整的.NET Framework 创建 ASP.NET Core 应用

正如本章前面提到的,EF6 是在完整的.NET 框架上构建的,因此我们不能在.NET Core 下创建 ASP.NET Core 应用,相反,我们应该针对完整的.NET 框架创建它。

每个 ASP.NET Core 功能都可以使用,但不能部署在非 Windows 计算机上。大多数现有企业仍然在部署的机器上使用完整的.NET 框架,因此利用它不会成为问题。

在空白解决方案中,创建带有 ASP.NET API 模板的 ASP.NET Core Web 应用(.NETFramework)。这将是我们与 AdventureWorks2014 数据库集成的 ASP.NET Core Web API。

由于我们已经准备好了数据访问类库,请使用 Add 引用将其包含在 webapi 项目中。确保您添加的 EntityFramework(6.1.3)库使用 NuGet 或 Package Manager 控制台。

On the target project, right-click the project name and click Add Reference to open a folder dialog window, navigate to the bin folder and select AdvWorksAPI.DataAccess to add it web API project.

使用 IPProductRepository 访问数据库

通常,在使用 ORM 访问数据库时,使用存储库模式访问 EF DataContext。使用它有很多目的,其中最突出的是:

  • 它分离了检索数据的逻辑
  • 实体到业务模型的映射,与数据源无关
  • 它有助于单元测试和集成测试

Service文件夹中创建接口IProductRepository及其实现ProductRepository,如下所示:

    namespace AdvWorks.WebAPI.Services 
    { 
      public class ProductRepository : IProductRepository 
      { 
        private AdvWorksContext _context; 
        public ProductRepository(AdvWorksContext context) 
        { 
          _context = context; 
        } 
        public void AddProduct(Product proddetails) 
        { 
          _context.Products.Add(proddetails); 
        } 
        public void DeleteProduct(Product proddetails) 
        { 
          _context.Products.Remove(proddetails); 
        } 
        public Product GetProduct(int productId) 
        { 
          return _context.Products.Where(c => c.ProductID == 
            productId).FirstOrDefault(); 
        } 
        public IEnumerable<Product> GetProducts() 
        { 
          return _context.Products.Take(10).ToList(); 
        } 
        public bool ProductExists(int productId) 
        { 
          return _context.Products.Any(c => c.ProductID == productId); 
        } 
        public bool Save() 
        { 
          return (_context.SaveChanges() >= 0); 
        } 
      } 
    }

您可以按如下方式分解前面的代码:

  • 它是IProductRepository接口的一个实现。
  • AdvWorksContext是从Startup类注入的依赖项,这有助于单元测试。
  • AddProduct接收Product对象并将其添加到AdvWorksContext中。它还没有保存在数据库中。
  • DeleteProduct接收Product对象并从AdvWorksContext中移除。它还没有保存在数据库中。
  • GetProduct接收ProductId以检索产品详细信息。
  • GetProducts返回数据库中存储的前 10 个产品。由于该表有许多记录,因此它将使用前 10 条记录。
  • ProductExists返回基于现有产品的布尔值。
  • Save方法保留所有AdvWorksContext更改。

启动中的连接字符串和 IPProductRepository

使用数据库时,连接字符串是必须的;它包含数据库服务器的位置、数据库名称、访问数据库的凭据以及其他信息。

ASP.NET Core 将所有配置/连接数据存储在 JSON 文件中,称为appsettings.json。在appsettings.json文件中复制以下连接字符串:

    { 
      //Others removed for brevity 
      "connectionStrings": { 
        "AdvWorksDbConnection": "Server=.\\sqlexpress;initial 
        catalog=AdventureWorks2014;Trusted_Connection=True;
        MultipleActiveResultSets=true" 
      } 
    } 

既然我们已经提供了连接字符串(AdvWorksDbConnection并编写了IProductRepository来访问数据库,那么我们需要在应用中配置(参与依赖项注入)它们。

为此,我们需要在ConfigureServices方法的服务中添加它们,如下代码所示:

    public void ConfigureServices(IServiceCollection services) 
    { 
      services.AddMvc(); 
      services.AddScoped<AdvWorksContext>(_ => new 
        AdvWorksContext(Configuration.GetConnectionString(
        "AdvWorksDbConnection"))); 
      services.AddScoped<IProductRepository, ProductRepository>(); 
    } 

使用自动映射器

任何现有数据库的表中都会有许多列。有时,web API 响应或请求对象不需要与表列一致的所有属性。

我们可以在数据访问层编写一个精简版的Product类,作为Models文件夹中的ProductDTO类。对于许多列,手动映射变得很难维护。Product类应转换为ProductDTO类,反之亦然;使用AutoMapper可以简化此转换。

AutoMapper是一个基于约定的.NET 对象映射器。使用 NuGet 安装此程序。

首先,我们在Models文件夹中创建ProductDTO,这是Product类的精简版本,如下所示:

    using System.ComponentModel.DataAnnotations; 
    namespace AdvWorks.WebAPI.Models 
    { 
      public class ProductDTO 
      { 
         public int ProductID { get; set; } 
         [Required] 
         [StringLength(50)] 
         public string Name { get; set; } 
         [Required] 
         [StringLength(25)] 
         public string ProductNumber { get; set; } 
         [StringLength(15)] 
         public string Color { get; set; }         
         public short ReorderPoint { get; set; } 
         public decimal StandardCost { get; set; } 
         public decimal ListPrice { get; set; } 
         public decimal? Weight { get; set; } 
         public int DaysToManufacture { get; set; } 
      } 
    } 

Using AutoMapper is optional, using it would help keep objects lean.

我们需要在管道处理中初始化Mapper,以便相应地映射请求和响应:

    public void Configure(IApplicationBuilder app, 
      IHostingEnvironment env, ILoggerFactory loggerFactory) 
    { 
      AutoMapper.Mapper.Initialize(cfg => 
      { 
        cfg.CreateMap<Product, ProductDTO>(); 
        cfg.CreateMap<ProductDetailsDTO, Product>(); 
      }); 
      app.UseMvc();  
    } 

现在我们有了一个使用DbContext与数据库对话的接口,这是一个初始化的对象转换映射器,现在是编写 web API 控制器的时候了。

写入 ProductController 以访问数据库

右键点击Controllers文件夹添加Web API Controller类,命名为ProductController。复制以下代码在Product表上执行 CRUD 操作:

    public class ProductController : Controller 
    { 
      private readonly IProductRepository _productRepository; 

      public ProductController(IProductRepository productRepository) 
      { 
        _productRepository = productRepository; 
      } 
      // GET: api/values 
      [HttpGet] 
      public IActionResult Get() 
      { 
         var prodlist = _productRepository.GetProducts(); 
         var results = Mapper.Map<IEnumerable<ProductDTO>>(prodlist); 
         return Ok(results); 
      } 
      // GET api/values/5 
      [HttpGet("{id}")] 
      public IActionResult Get(int id) 
      { 
        if (!_productRepository.ProductExists(id)) 
        { 
          return NotFound(); 
        } 
        var prod = _productRepository.GetProduct(id); 
        var results = Mapper.Map<ProductDTO>(prod); 
        return Ok(results); 
      } 

      //Complete code part of source code bundle 
    } 

Complete source code is available in the code bundle.

我们可以将前面的代码分解如下:

  • IProductRepository在构造函数中注入依赖项;我们在Startup课上注册了这个。
  • Get()方法通过IProductRepository接口从数据库返回产品列表。AutoMapperProductProductDTO的对象转换生效。
  • Get(int id)方法根据ProductID返回匹配产品,否则返回NotFound(404)HTTP 响应。AutoMapper也会转换为ProductDTO
  • Post()方法使用FromBody请求接收ProductDetailsDTO(类似于ProductDTO的对象)。它检查空值和模型验证,如果有,返回BadRequest。它还映射回Product对象,并将其添加到 EF6 的DbContext。调用Save方法来持久化AdventureWorks2014数据库的Production.Product表中的条目。
  • Put()方法也执行与 post 类似的操作,唯一的区别是更新,而不是创建。
  • Delete()方法检查产品是否存在,然后通过调用Save()方法将其从数据库中删除。

建设和运行项目;使用PostManProductController进行积垢操作,如下图所示:

Get product from the database using EF 6

用简洁的语言进行集成

Dapper是一款开源的简单对象映射器,适用于基于.NET 的应用。与实体框架或 NHibernate 相比,也称为微 ORM

扩展了IDbConnection接口,不依赖任何具体的 DB 实现;这使得它可以与几乎所有的关系数据库一起工作,如 SQLite、SQLCE、Firebird、Oracle、MySQL、PostgreSQL 和 SQLServer。

它被认为是 ORM 之王,因为它在其他 ORM 中重量轻、性能高。我建议在上阅读他们的 GitHub 回购协议 https://github.com/StackExchange/dapper-dot-net

由于 Dapper 与现有数据库一起使用,因此我们将对其使用相同的 AdventureWorks2014 数据库。在本节中,我们将使用HumanResources.Department表。

让我们使用 Dapper ORM 创建一个与 AdventureWorks2014 数据库集成的 ASP.NET Core Web API 应用。

创建 advwrksdapperWebAPI 并添加 Dapper 库

Dapper 可以与完整的.NET Framework 以及.NET Core Framework 一起使用,因此让我们使用 web API 创建 ASP.NET Core(.NET Core)应用,并将其命名为AdvWrksDapper。使用 NuGet 管理器添加 Dapper 库。

使用 IDepartmentRepository 和 department 模型访问数据库

Models文件夹中创建Department类参与数据库访问。请记住,属性名称应与表列名一致:

    using System.ComponentModel.DataAnnotations; 

    namespace AdvWrksDapper.Models 
    { 
      /// <summary> 
      /// HR.Department Table of Adventure Works Database  
      /// </summary> 
      public class Department 
      { 
        [Key] 
        public int DepartmentID { get; set; } 
        [Required] 
        [StringLength(50)] 
        public string Name { get; set; } 
        [Required] 
        [StringLength(50)] 
        public string GroupName { get; set; } 
      } 
    } 

正如我们在前面的示例中所做的,我们将创建用于执行 CRUD 操作的IDepartmentRepository,如下所示:

    public class DepartmentRepository : IDepartmentRepository 
    { 
      private readonly AdvWorksConfig _advConfig; 
      public DepartmentRepository(IOptions<AdvWorksConfig> advconfig) 
      { 
         _advConfig = advconfig.Value; 
      } 

      public IDbConnection Connection 
      { 
        get 
        { 
           return new SqlConnection(_advConfig.DbConnectionString); 
        } 
      } 

      public bool AddDepartment(Department deptdetails) 
      { 
        bool isSuccess = false; 
        using (IDbConnection dbConnection = Connection) 
        { 
          dbConnection.Open(); 
          var rows = dbConnection.Execute("INSERT INTO
            HumanResources.Department (name,groupname)
            VALUES(@Name,@GroupName)", deptdetails); 
          if (rows == 1) 
          { 
            isSuccess = true; 
          } 
        } 
        return isSuccess; 
      }       

      public IEnumerable<Department> GetDepartments() 
      { 
        using (IDbConnection dbConnection = Connection) 
        { 
          dbConnection.Open(); 
          return dbConnection.Query<Department>("SELECT * FROM
            HumanResources.Department"); 
        } 
      } 
      //Complete code part of source code bundle 
    }

我们可以将前面的代码分解如下:

  • 使用 ASP.NET Core 选项模式读取Startup类中配置的连接字符串appsettings.json
  • Connection属性用于使用连接字符串访问 SQL 数据库。
  • AddDepartment方法通过打开连接并执行INSERT SQL语句来添加部门,并在成功时返回TRUE。这与上一节中看到的 EF6 示例有很大不同。
  • DeleteDepartment方法基于DepartmentId从数据库表中删除记录,成功返回TRUE
  • DepartmentExists方法检查记录是否存在。
  • GetDepartment方法根据DepartmentId返回部门记录。
  • GetDepartments方法返回表中存在的部门列表。
  • UpdateDepartment方法执行更新 SQL 操作,成功返回TRUE

ASP.NET Core 中的连接字符串和 IOption

任何数据库都可以通过包含其位置、数据库名称、访问凭据等的连接字符串进行访问。此信息可以放在appsettings.json中,如下代码所示:

    {   
      "ApiConfig": { 
        "DbConnectionString": "Server=.\\sqlexpress;initial
          catalog=AdventureWorks2014;Trusted_Connection=True;
          MultipleActiveResultSets=true" 
      } 
    } 

我们可以在应用中访问此配置或连接字符串详细信息,并将其作为跨应用的强类型配置类使用,从而消除魔术字符串。

为此,我们在Models文件夹中创建AdvWorksConfig类,如下代码行所示:

    namespace AdvWrksDapper.Models  
    { 
      public class AdvWorksConfig 
      { 
        public string DbConnectionString { get; set; } 
      } 
    } 

Startup类中,ConfigureServices方法修改代码以读取配置节,并将IDepartmentRepository注册为应用中注入的依赖项:

    public void ConfigureServices(IServiceCollection services) 
    { 
      // Add framework services. 
      services.AddMvc(); 
      services.Configure<AdvWorksConfig>(Configuration.GetSection(
       "ApiConfig"));  services.AddScoped<IDepartmentRepository, 
       DepartmentRepository>(); 
    } 

添加 DeparmentController Web API

Controllers文件夹中添加一个新的 web API 控制器类,命名为DepartmentController,并添加以下代码以使用 HTTP 谓词执行 CRUD 操作:

    [Route("api/[controller]")] 
    public class DepartmentController : Controller 
    { 
      private readonly IDepartmentRepository _deptrepo; 
      public DepartmentController(IDepartmentRepository deptrepo) 
      { 
         _deptrepo = deptrepo; 
      } 
      // GET: api/values 
      [HttpGet] 
      public IActionResult Get() 
      { 
        var results = _deptrepo.GetDepartments(); 
        return Ok(results); 
      } 

      // GET api/values/5 
      [HttpGet("{id}")] 
      public IActionResult Get(int id) 
      {             
        if (!_deptrepo.DepartmentExists(id)) 
        { 
           return NotFound(); 
        } 
        var dept = _deptrepo.GetDepartment(id); 
        return Ok(dept); 
      } 

      // POST api/values 
      [HttpPost] 
      public IActionResult Post([FromBody]Department dept) 
      { 
        if (dept == null) 
        { 
          return BadRequest(); 
        } 
        if (!ModelState.IsValid) 
        { 
          return BadRequest(ModelState); 
        } 

        if (!_deptrepo.AddDepartment(dept)) 
        { 
          return StatusCode(500, "A problem happened while handling
            your request."); 
        } 
        else 
        { 
          return StatusCode(201, "Created Successfully"); 
        } 
        //Complete code part of source code bundle 
      } 
    }

您可以按如下方式分解前面的代码:

  • 构造函数中的依赖项注入IDepartmentRepository
  • Get()方法获取所有部门并作为列表返回。
  • Get(int id)方法根据 ID 获取部门记录。
  • Post()方法检查请求是否为空且有效,否则返回BadRequest响应。如果一切正常,它会将记录保存到数据库中。
  • Put()方法更新单个部门的记录。
  • Delete()方法检查部门是否存在,如果存在则删除,或者返回NotFound响应。

构建并运行应用;使用 Postman(或 Fiddler)测试 web API。在本场景中,我们将通过传递Department对象对应的JSON对象来测试POST方法:

    { 
      "name":"Cafeteria", 
      "groupName": "Housekeeping Department" 
    }  

在 HTTP 请求主体中传递此 JSON,并将内容类型设置为 application/JSON。

发送请求后,通过POST方法,再到IDepartmentRepository,进行INSERT操作,增加新部门。

同样,使用邮递员执行其他操作,如GETPUTDELETE

Note that we didn't use AutoMapper here; you can use it by referring to the EF6 demo.

Post department data to web API using Dapper

与 EF 核集成

实体框架核心EF 核心)是微软针对.NET Core 框架的最新 ORM,符合 ASP.NET Core 路线图。现在,ASP.NET Core 和 EF Core 为构建跨平台 web 应用提供了一个很好的平台。

EF Core 是将 EF 6 完全重写为更集中的包,以使其更精简。EF 团队计划同时支持关系数据库和非关系数据库。在写这本书的时候,EF1.1 已经发布,作为一个成熟的 ORM 开发还需要一段时间。欲了解更多关于 EF Core 的信息,请访问https://docs.microsoft.com/en-us/ef/

在本节中,我们将创建PacktContactsCoreweb API 项目,以使用 EF Core 与数据库集成。我们将使用 MS SQL Server 2014 Express Edition 作为数据库服务器;但是,目前,您还可以使用 SQLite、MySQL 和 PostgreSQL。

创建 PacktContactsCore ASP.NET Core 项目

我们正在完全处理 ASP.NET 和 EFORM 的.NET Core 框架,因此让我们使用名为PacktContactsCore的 web API 模板创建一个 ASP.NET Core 项目。

您可以使用 Yeoman 生成器或.NET CLI 创建项目。

添加 EF 核心包和工具

这一步非常重要,因为我们正在使用 NuGet 包和 EF Core 工具添加 EF Core。EF 工具提供 CLI 支持,以使用 EF 迁移和执行数据库更新。

使用 NuGet 软件包管理器或 PMC(CLI)添加 EF Core SQL Server 软件包:

"Microsoft.EntityFrameworkCore.SqlServer": "2.0.0-preview2-final" 
"Microsoft.EntityFrameworkCore.Tools": "2.0.0-preview2-final", 

To work with other databases, install appropriate NuGet packages by referring to the provider list in this link: https://docs.efproject.net/en/latest/providers/index.html.

联系人模型类和 DbContext

Models文件夹中创建一个Contacts类,该类对应于数据库中的Contacts表,如下所示:

    using System; 
    using System.ComponentModel.DataAnnotations; 
    namespace PacktContactsCore.Model 
    { 
      public class Contacts 
      { 
        [Key] 
        public int Id { get; set; } 
        [Required] 
        [MinLength(4)] 
        public string FirstName { get; set; } 
        public string LastName { get; set; } 
        [Required] 
        public string Email { get; set; } 
        public DateTime DateOfBirth { get; set; } 
      } 
    } 

有了 EF,我们需要ContactsContext-一个DbContext类作为数据库和应用之间的桥梁来执行与数据库相关的操作:

    using Microsoft.EntityFrameworkCore; 
    using PacktContactsCore.Model; 
    namespace PacktContactsCore.Context 
    { 
      public class ContactsContext : DbContext 
      { 
        public ContactsContext(DbContextOptions<ContactsContext> 
         options) 
        : base(options) { } 
        public ContactsContext() { 
        } 
        public DbSet<Contacts> Contacts { get; set; } 
      } 
    } 

配置服务以使用 SQL Server 数据库

要连接到数据库应用,需要一个连接字符串;与前面的 EF6 和 Dapper 示例一样,在appsettings.json文件中添加连接字符串详细信息,如下所示:

    {   
      "ConnectionStrings": { 
        "SqlDbConnStr": "Server=.\\sqlexpress;initial 
        catalog=PacktContactsDB;Trusted_Connection=True;" 
      } 
    } 

ContactsContext方法需要通过读取数据库连接字符串添加到Startup类中的服务集合中:

    public void ConfigureServices(IServiceCollection services) 
    { 
      // Add framework services. 
      services.AddDbContext<ContactsContext>(options => 
        options.UseSqlServer(Configuration.GetConnectionString(
        "SqlDbConnStr"))); 
      services.AddMvc(); 
    } 

数据库迁移和更新的 EF 工具

现在我们已经准备好了ContactsContactsContextDbContext类),并且已经使用连接字符串将 SQL Server 注册到服务集合,现在是添加 EF 迁移和更新数据库的时候了。(在 EF 术语中,它意味着创建或更新数据库模式。)

在使用 EF(6.x 或 Core)时,第一步是基于数据模型生成一个migration类。这一步在C#代码中生成 SQL 脚本的副本,如创建表、添加列约束、种子设定等。

从项目的root文件夹中运行以下命令创建 EF migrations 类:

dotnet ef migrations add init 

成功执行迁移命令后,将在项目中创建一个包含第一个 SQL 脚本(代码格式)的Migrations文件夹,并联系上下文快照(其代码模式副本)。

在前面的命令中,init是添加迁移的步骤,即初始化(第一步)。假设进行了任何更改,那么我们需要提供一个合适的名称。例如,添加了一个新的表地址,因此,通过添加addressAdded而不是init来使migration类唯一。

现在,运行以下命令,在连接字符串中提供的数据库位置生成数据库及其架构:

dotnet ef database update  

此命令将连接到 SQL server 数据库服务器并创建(更新,如果已经存在)数据库。数据库名称出现在连接字符串中,如以下屏幕截图所示:

PacktContactsDB created by running EF Core commands

用于积垢操作的接触器控制器

添加新的 web API 控制器类ContactsController;它将在PacktContactsDB上执行积垢操作。复制以下代码:

    [Route("api/[controller]")] 
    public class ContactsController : Controller 
    { 
      private readonly ContactsContext _context; 
      public ContactsController(ContactsContext contactContext) 
      { 
         _context = contactContext; 
      } 

      // GET api/values/5 
      [HttpGet("{id}", Name ="GetContactById")] 
      public IActionResult Get(int id) 
      { 
        var result = _context.Contacts.Any(c => c.Id == id); 
        if (!result) 
        { 
          return NotFound(); 
        } 
        return Ok(_context.Contacts.Where(c => c.Id == id)
          .FirstOrDefault()); 
      } 

      // POST api/values 
      [HttpPost] 
      public IActionResult Post([FromBody]Contacts reqObj) 
      { 
        if (reqObj == null) 
        { 
          return BadRequest(); 
        } 
        if (!ModelState.IsValid) 
        { 
          return BadRequest(ModelState); 
        } 

        var contextObj = _context.Contacts.Add(reqObj); 
        var count = _context.SaveChanges(); 
        if (count == 1) 
        { 
          return Ok(); 
        } 
        else 
        { 
          return StatusCode(500, "A problem happened while handling
            your request."); 
        } 
      } 
      //Complete code part of source code bundle 
    }  

Controller code does not use repository pattern and AutoMapper, reader can explore EF 6 example to implement repository pattern and AutoMapper.

我们可以将前面的代码分解如下:

  • 构造函数通过 DI 获取ContactsContext
  • Get()方法从数据库中检索联系人列表。
  • Get(int id)方法获取联系人详细信息以匹配传递的 ID,否则返回NotFound
  • Post()方法在空检查和模型验证之后将Contacts对象插入数据库。
  • Put()根据 ID 更新Contacts对象并更新所有属性。此处可以使用AutoMapper进行对象映射。
  • Delete()方法根据 ID 从数据库中删除。

构建并运行应用,并使用 Postman 测试 web API。下面的屏幕截图显示了一个PUT请求正在运行,并有相应的响应:

The PUT method in action for EF Core Exercise for reader to perform other operations either using Postman or Fiddler.

总结

在本章中,您学习了大量有关使用 ORM(如 EF 6.x、Dapper 和 EF Core)将 ASP.NET Core 应用与数据库集成的知识。由于有许多使用数据库提供者的选项,它当然提供了很大的灵活性。

在不使用 ORMs 的情况下,我们仍然可以使用经典的 ADO.NET 与数据库通信。

在下一章中,我们的重点将是处理错误和异常,优雅地通知客户,并设计跟踪和日志机制。

十、错误处理、跟踪和日志记录

任何软件应用中都必然会出现错误或异常,即使在各种环境中进行了大量测试之后也是如此。一旦应用投入生产,软件将面临更高的负载、正确或错误的用户输入、系统或网络崩溃,以及如果处理不当将导致应用崩溃的其他事件。

异常管理的概念是,系统应在发生灾难性故障时继续工作,并应以错误或未处理异常的形式记录故障的详细信息,以供进一步调查。

在本书中,我们正在构建一个 ASP.NET Core Web API 应用,该应用将主要由 Web 或移动客户端使用,任何异常都应该由 Web API 优雅地处理,以便客户端能够继续工作。

在本章中,我们将学习在 ASP.NET Core 中登录、向各种日志提供程序写入错误或异常以及构建异常处理程序以优雅地向客户端返回适当响应的基础知识。

在本章中,我们将研究以下主题:

  • ASP.NET Core 中日志记录的基础知识
  • MyWallet--演示 ASP.NET Core 项目
  • 使用 NLog 将错误记录到文件
  • 使用 Serilog 将错误记录到数据库
  • MyWallet 项目中的异常管理

ASP.NET Core 中日志记录的基础知识

ASP.NET Core 的一个功能是使用ILoggerFactory内置日志记录。当您立即创建 ASP.NET Core 应用(基于空、MVC 或 web API)时,您将看到 program 类的IWebHost's CreateDefaultBuilder方法对日志功能进行底层工作,以使其读取appsettings.json日志部分的文件,以提供将信息记录到调试或控制台窗口所需的所有基础结构。

ILoggerFactory分为AddProviderCreateLogger两部分,大大简化了测井的使用。

AddProvider方法采用ILoggerProvider写入/存储应用生成的日志信息。提供程序可以是控制台、调试窗口、文件、数据库、基于云的存储,也可以是第三方日志分析服务(Splunk、Raygun、Loggly 等)。

CreateLogger方法采用将通过上述可能的提供程序写入日志信息的类或方法的名称。

简而言之,要将所有日志信息记录到一个文件中,我们需要使用文件提供程序(NLog、Serilog 或任何其他提供程序)并创建一个CreateLogger实例来发送要存储在文件中的日志信息。

日志记录级别

根据所写入信息的严重性,所写入的日志信息将具有不同的级别。下表按升序描述了 ASP.NET Core 中的日志级别:

| 日志级别 | 写为 | 备注 |
| 跟踪=0 | _logger.LogTrace (...) | 开发人员调试的一部分。可能包括敏感信息。 |
| 调试=1 | _logger.LogDebug (...) | 开发人员调试的一部分。大部分时间都在使用。 |
| 信息=2 | _logger.LogInformation(...) | 可以在此处记录应用流。不用于调试。 |
| 警告=3 | _logger.LogWarning (...) | 对于意外事件。例如,数据与业务规则不匹配。 |
| 误差=4 | _logger.LogError (...) | 未处理的申请事件;可能是错误。 |
| 临界值=5 | _logger.LogCritical (...) | 需要立即采取行动解决的事件。 |

登录操作

在这里,我们将了解如何使用 web API 模板创建 ASP.NET Core 应用。已经添加了Microsoft.Extensions.Logging扩展以及DebugConsole扩展。

打开Startup类并进行以下更改以查看内置操作:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env,
      ILoggerFactory loggerFactory) 
    {             
      var strtpLogger = loggerFactory.CreateLogger<Startup>(); 
      strtpLogger.LogTrace("Looking at Trace level "); 
      strtpLogger.LogDebug("This is Debug level"); 
      strtpLogger.LogInformation("You are Startup class - FY Information"); 
      strtpLogger.LogWarning("Warning - Entered Startup so soon"); 
      strtpLogger.LogError("This result in Null reference exception"); 
      strtpLogger.LogCritical("Critical - No Disk space"); 
      app.UseMvc();             
    } 

您可以按如下方式分解前面的代码:

  • 日志级别和类别从appsettings.json文件的日志部分读取
  • 记录器工厂同时添加控制台和调试窗口提供程序
  • 我们将Startup类记录器的一个实例创建为strtpLogger
  • 此记录器工厂实例根据日志级别记录到控制台和调试窗口

将应用作为控制台应用运行(使用 Kestrel 并显示控制台窗口)。console 窗口显示如下日志(这是 console 窗口的一部分):

Logs displayed according to level

查看上图,我们可以看到它提供了日志来源的信息,即Startup类,以及不同的日志级别。

尽管我们在Startup类中编写了Trace日志级别,但它并没有在控制台窗口中编写。这是因为appsettings.json中存在日志类别。

日志类别

Startup类代码片段中,您可以看到AddConsole日志提供程序正在读取appsettings.json的日志部分。本节包含日志类别详细信息,如默认、系统和 Microsoft。

日志类别有助于编写特定于应用、框架或整个系统的日志。理想情况下,在开发或生产期间,特定于应用级别的日志记录就足够了。

让我们添加特定于应用级别的日志记录,而不是当前存在的默认设置。打开appsettings.json,删除已有的LogLevel细节,进行如下更改:

    { 
      "Logging": { 
        "IncludeScopes": false, 
        "LogLevel": { 
          "basic_logging_demo": "Warning" 
        }     
      } 
    }

basic_logging_demo是项目名称(可以是任何字符串),我们将日志级别设置为Warning。任何低于警告级别的日志(请参阅日志级别表)都不会显示在控制台上。再次运行应用以仅查看控制台窗口上显示的WarningErrorCritical日志。

依赖注入中的 iLogger 工厂

默认情况下,依赖注入被烘焙到 ASP.NET Core 中,我们可以通过将ILogger注入控制器、中间件、过滤器或任何其他类来利用这一点。

打开ValuesControllerweb API 类(默认使用 web API 模板创建),如下编辑代码,运行应用在控制台窗口查看日志:

    namespace basic_logging_demo.Controllers 
    { 
      [Route("api/[controller]")] 
      public class ValuesController : Controller 
      {   
        private ILogger<ValuesController> _logger; 

        public ValuesController(ILogger<ValuesController> logger) 
        { 
          _logger = logger; 
        } 
        // GET api/values 
        [HttpGet] 
        public IEnumerable<string> Get() 
        { 
          _logger.LogWarning("Warning from Values Controller ");             
          return new string[] { "value1", "value2" }; 
        }         

        // POST api/values 
        [HttpPost] 
        public void Post([FromBody]string value) 
        { 
          try 
          { 
            if(value.Length > 0) 
            { 
               _logger.LogInformation($"String length is {value.Length}"); 
            } 
          } 
          catch (Exception ex) 
          { 
            _logger.LogError("Error Occurred while POST ", ex.Message); 
            throw; 
          } 
        }  //Removed code for brevity 
      } 
    } 

您可以按如下方式分解前面的代码:

  • 与我们在上一个示例中使用CreateLogger的方式相同,我们有另一种方法来创建ILogger并注入它。这里我们正在创建一个ValuesControllerILogger实例,并使用构造函数将其注入。
  • 当我们点击这些 API 端点时,GET方法记录警告,POST方法将信息和错误记录到控制台。

Inject the ILogger<T> instance in the same way for middleware, filter, or any other classes to log information.

MyWallet-演示 ASP.NET Core 项目

要了解有关使用不同提供商进行日志记录的更多信息,我们将创建一个演示 ASP.NET Core 应用 MyWallet,该应用具有以下功能:

  • 它应该能够列出所有的日常开支。
  • 它应该能够通过传递 ID 获得特定的费用。
  • 它应该能够在列表中添加/发布单个费用。如果电影费用超过 300 美元,则该请求数据无效。
  • 它应该能够编辑和删除特定的费用。

在这个演示应用中,我们将使用 EF Core InMemory 提供程序。这个 EF 核心包在内存中运行应用,而不是持久化到数据库中。它非常适合于对数据访问层进行单元测试,但这里使用它可以使示例更简单。

You can use any database provider (Sql Server, MySQL, SQLite) by reading Chapter 09, Integration with Databases.

使用 NuGet 软件包管理器,将Microsoft.EntityFrameworkCore.InMemory添加到最新的软件包中。将 web API 控制器创建为WalletController, DailyExpense作为模型类,ExpenseContext作为数据上下文。

不要忘记在Startup类和ConfigureServices方法中注入ExpenseContext数据上下文。

根据所选场景,WalletControllerweb API 控制器代码如下所示:

    [Route("api/[controller]")] 
    public class WalletController : Controller 
    { 
       private readonly ExpenseContext _context;         
       private readonly ILogger<WalletController> _logger; 

       public WalletController(ExpenseContext context,
         ILogger<WalletController>
         logger) 
       { 
          _context = context;             
          _logger = logger; 
       } 

       // GET api/values/5 
       [HttpGet("{id}")] 
       public IActionResult Get(int id) 
       {             
          var spentItem = _context.DailyExpenses.Find(id); 
          if (spentItem == null) 
          { 
             _logger.LogInformation($"Daily Expense for {id} does
                not exists!!"); 
             return NotFound();                 
          } 
          return Ok(spentItem); 
       } 

       // POST api/values 
       [HttpPost] 
       public IActionResult Post([FromBody]DailyExpense value) 
       { 
         if (value == null) 
         { 
            _logger.LogError("Request Object was NULL"); 
            return BadRequest(); 
         } 
         CheckMovieBudget(value); 

         if (!ModelState.IsValid) 
         {                 
            return BadRequest(ModelState); 
         } 
         var newSpentItem = _context.DailyExpenses.Add(value); 
         Save(); 
         return Ok(newSpentItem.Entity.Id); 
       } 
    } 

您可以按如下方式分解前面的代码:

  • 使用 DI 注入ILogger<WalletController>ExpenseContext(EF Core 需要)。
  • Get(int id)通过搜索并返回DailyExpense项来查找该项。如果没有找到,我们会记录信息,说明没有找到。
  • Post()检查请求是否为空。如果是,则返回BadRequest并记录错误。CheckMovieBudget方法检查电影费用是否不超过 300 美元,并返回一个Model Validation错误。如果一切顺利,就可以节省日常开支。
  • Put()Delete()方法检查费用项目。如果找不到,他们会记录信息。如果找到,则根据找到的内容更新或删除它。

将应用作为控制台应用运行,通过使用 Postman 或 Fiddler 点击控制台窗口来查看控制台窗口上的各种日志信息。

到目前为止,我们一直使用 console 窗口查看代码中的日志信息,因为 ASP.NET Core 提供了 console 或 Debug 窗口作为默认日志提供程序。在现实世界的应用中,我们需要将信息记录到文件、数据库或云中,为此我们需要使用第三方日志提供商。

使用 NLog 将错误记录到文件

NLog是我们可以用于在 ASP.NET Core 中记录信息的第三方提供商之一。在这种情况下,我们将登录到一个文件。但是,我们也可以使用 NLog 登录到数据库或云。

欲了解更多关于 NLog 的信息,请参考https://github.com/NLog/NLog.Extensions.Logging

使用 NuGet 软件包管理器,添加与 NLog 相关的软件包:

"NLog.Extensions.Logging": "1.0.0-rtm-beta5" 
"NLog.Web.AspNetCore": "4.4.1"  

At the time of writing this book, NLog is still in beta. Refer to the preceding link for updates.

Startup类的Configure方法中,修改该方法,将记录信息的 NLog 包含到文件中:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env,
      ILoggerFactory loggerFactory) 
    { 
       loggerFactory.AddNLog(); 
       app.AddNLogWeb(); 
       // Rest of code removed for brevity  
       app.UseMvc(); 
    } 

对于 NLog,有一个配置文件可用于不同的配置设置,如日志级别、文件名、日志文件位置等。在根目录中创建nlog.config文件,并复制以下最基本配置代码:

    <?xml version="1.0" encoding="utf-8" ?> 
    <nlog  
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 

      <extensions> 
        <add assembly="NLog.Web.AspNetCore"/> 
      </extensions> 

      <!-- define various log targets --> 
      <targets> 
        <target xsi:type="File" name="ownFile-web" fileName="D:\PacktLogs\
          nlog-own-${shortdate}.log" layout="${longdate}|${
          event-properties:item=EventId.Id}|${logger}|${uppercase:${level}}|
          ${message} ${exception}|url: ${aspnet-request-url}|action: $
          {aspnet-mvc-action}" /> 

        <target xsi:type="Null" name="blackhole" /> 
      </targets> 
      <rules> 
        <!--Skip Microsoft logs and so log only own logs--> 
        <logger name="Microsoft.*" minlevel="Trace" writeTo="blackhole"
          final="true" /> 
        <logger name="*" minlevel="Trace" writeTo="ownFile-web" /> 
      </rules> 
    </nlog> 

一旦一切都启动并运行,访问WalletController(通过邮递员或 Fiddler)将导致在配置文件中提供的位置创建一个带有nlog-own-(date).log的日志文件。

使用 Serilog 将错误记录到数据库

Serilog是针对.NET 应用的第三方日志记录包。它关注于完全结构化的事件。它以简单的 API、简单的设置和许多可以登录到各种源(如文件、数据库、ElasticSearch、Raygun 等)的包而闻名。

我们将使用Serilog.Sinks.MSSqlServer将日志信息写入数据库(即 MS SQL Server 数据库)。我们将使用 NuGet 软件包管理器添加以下软件包:

Serilog: 2.5.1-dev-00873
Serilog.Sinks.MSSqlServerCore: 1.1.0 

对于数据库日志记录,我们需要数据库服务器位置、名称和表。打开appsettings.json添加这些配置设置:

    "Serilog": { 
      "ConnectionString":"Server=.\\sqlexpress;
      Database=MyWalletLogDB;trusted_connection=true",
      "TableName": "Logs" 
    } 

MyWalletLogDB上运行在此 URL 处找到的 SQL 脚本以创建日志表。参见https://github.com/serilog/serilog-sinks-mssqlserver 了解更多信息。

Startup类的ConfigureServices方法中,将 Serilog 添加到要在整个应用中使用的 DI 中:

    services.AddSingleton<Serilog.ILogger>(x => 
    { 
      return new   LoggerConfiguration().WriteTo.MSSqlServer(Configuration[
      "Serilog:ConnectionString"],
      Configuration["Serilog:TableName"]).CreateLogger(); 
    }); 

打开WalletControllerweb API,将Serilog.ILogger包含到 DI 并写入日志:

    public WalletController(ExpenseContext context, Serilog.ILogger logger) 
    { 
       _context = context; 
       _logger = logger; 
    } 

附加的源代码包含整个控制器类。现在运行应用并使用邮递员或小提琴手点击WalletController。打开 SQL Server 以查看表中的日志。结果与此类似:

MyWallet logs in the SQL Server database

MyWallet 项目中的异常管理

任何 web API 项目本身都不是应用。它的构建使得流行的前端,无论是 web 还是桌面,都可以使用它。web API 必须优雅地处理任何意外事件(未处理的异常)或任何业务异常。

如果发生意外事件(未经处理或与业务相关),则应记录这些事件,并将相应的响应发送回客户,以便客户能够知道。

为此,我们需要以下课程:

  • WebAPIError:作为响应对象,供客户端使用。
  • WebAPIException:业务异常或未处理异常的自定义异常类。它包含异常详细信息以及状态代码。
  • WebAPIExceptionFilter:包含一个异常属性,用作控制器或动作的属性。

创建一个ErrorHandlers文件夹,并使用前面的名称创建类文件。为每个类复制以下内容:

  • WebAPIError:作为响应对象:
        namespace MyWallet.ErrorHandlers   
        { 
          public class WebAPIError 
          { 
             public string message { get; set; } 
             public bool isError { get; set; } 
             public string detail { get; set; }         

             public WebAPIError(string message) 
             { 
                this.message = message; 
                isError = true; 
             }         
          } 
        } 

  • WebAPIException:自定义异常类,携带异常详细信息和状态码:
        using System; 
        using System.Net; 

        namespace MyWallet.ErrorHandlers 
        { 
          public class WebApiException : Exception 
          { 
             public HttpStatusCode StatusCode { get; set; }         

             public WebApiException(string message, 
               HttpStatusCode statusCode = 
                 HttpStatusCode.InternalServerError) :
               base(message) 
             { 
               StatusCode = statusCode; 
             } 
             public WebApiException(Exception ex, HttpStatusCode statusCode
               =  HttpStatusCode.InternalServerError) : base(ex.Message) 
             { 
                StatusCode = statusCode; 
             } 
          } 
        } 

  • WebAPIExceptionFilter:作为过滤器处理异常:
        namespace MyWallet.ErrorHandlers 
        { 
           public class WebApiExceptionFilter : ExceptionFilterAttribute 
           { 
              private ILogger<WebApiExceptionFilter> _Logger; 

              public WebApiExceptionFilter(ILogger<WebApiExceptionFilter>
                logger) 
              { 
                  _Logger = logger; 
              } 

              public override void OnException(ExceptionContext context) 
              { 
                 WebAPIError apiError = null; 
                 if (context.Exception is WebApiException) 
                 { 
                   // Here we handle known MyWallet API errors 
                   var ex = context.Exception as WebApiException; 
                   context.Exception = null; 
                   apiError = new WebAPIError(ex.Message);                 

                  context.HttpContext.Response.StatusCode =
                     (int)ex.StatusCode;   
                  _Logger.LogWarning($"MyWallet API thrown error:
                     {ex.Message}", ex); 
                } 
                else if (context.Exception is UnauthorizedAccessException) 
                { 
                  apiError = new WebAPIError("Unauthorized Access"); 
                  context.HttpContext.Response.StatusCode = 401;                 
                } 
                else 
                { 
                  // Unhandled errors 
                  #if !DEBUG 
                    var msg = "An unhandled error occurred."; 
                    string stack = null; 
                  #else 
                    var msg = context.Exception.GetBaseException().Message; 
                    string stack = context.Exception.StackTrace; 
                  #endif 
                    apiError = new WebAPIError(msg); 
                    apiError.detail = stack; 

                  context.HttpContext.Response.StatusCode =
                   (int)HttpStatusCode.InternalServerError; 

                  // handle logging here 
                  _Logger.LogError(new EventId(0), context.Exception, msg); 
                } 

                // always return a JSON result 
                context.Result = new JsonResult(apiError); 
                base.OnException(context); 
              }          
           } 
        } 

您可以按如下方式分解前面的代码:

  • 采用ILogger写入日志。
  • OnException方法检查这是否是自定义WebApiException类型。如果是,它将准备一个带有状态代码的WebApiError响应。如果是未经授权的访问,它会做出适当的响应。
  • 对于未处理的异常,它获取堆栈跟踪详细信息,并以WebApiError响应,其中包含释放模式的完整详细信息。
  • ILogger实例在需要的地方写入日志。

我们的异常处理已经在MyWalletweb API 项目中准备就绪。让我们将过滤器合并到控制器中。您可以使用现有控制器或创建新控制器。

首先,用我们创建的WebApiExceptionFilter装饰控制器:

    [ServiceFilter(typeof(WebApiExceptionFilter))] 
    [Route("api/[controller]")] 
    public class NewWalletController : Controller 
    { 
       private readonly ExpenseContext _context; 
       private readonly ILogger<WalletController> _logger; 

       public NewWalletController(ExpenseContext context,
         ILogger<WalletController> logger) 
       { 
          _context = context; 
          _logger = logger; 
       } 
       //Code removed for brevity 
    }  

我们使用ServiceFilter来使用过滤器,因为我们使用依赖注入将ILogger包含在过滤器中,以便它写入日志。

Get()方法可以编写如下来处理业务逻辑故障,例如,如果请求的记录不存在:

    // GET api/values/5 
    [HttpGet("{id}")] 
    public IActionResult Get(int id) 
    { 
      var spentItem = _context.DailyExpenses.Find(id); 
      if (spentItem == null) 
      {                 
        throw new WebApiException($"Daily Expense for {id} 
          does not exists!!", HttpStatusCode.NotFound); 
      } 
      return Ok(spentItem); 
    } 

下面是另一个示例,说明了当发生未处理的异常时,过滤器将优雅地处理它们,并记录错误:

    [Route("ThrowExceptionMethod")] 
    public IActionResult ThrowExceptionMethod() 
    { 
      try 
      { 
        string emailId = null; 
        if (emailId.Length > 0) 
        { 
           return Ok(); 
        } 
        throw new WebApiException("Email is empty",
          HttpStatusCode.BadRequest); 
      } 
      catch (Exception ex) 
      { 
        throw ex; 
      } 
    } 

您可以将信息记录到文件或数据库中。配置完成后,运行应用并使用 Postman 或 Fiddler 发送请求并接收响应。

以下是使用 NLog 写入文件的示例日志,然后是邮递员回复的屏幕截图:

2017-02-13 17:13:59.9597|0|MyWallet.ErrorHandlers.WebApiExceptionFilter|WARN| MyWallet API thrown error: Max movie expense limit is 300 and your amount is 2081 |url: http://localhost/api/newwallet|action: Post

Web API graceful exception Custom middleware can be written for gracefully handling the web API errors or exception.

日志管理服务的链接

我们甚至可以将这些日志移动到第三方日志管理服务(存在试用版和付费版),以收集有关 web API 异常的指标:

总结

ASP.NET Core 大大改进了对任何持久性源的日志记录功能。使用第三方工具编写日志变得简单。NLog 或 Serilog 是管理写入日志最广泛使用的工具。

我们还学习了如何优雅地处理 web API 错误,以及如何将它们记录到日志存储中以进行分析。

在下一章中,我们将学习如何使用缓存、异步编程和其他方法优化和改进 ASP.NET Core 性能。

十一、优化与性能

任何 web API 的真正测试都是在各种客户端(大多数是前端应用)使用它时开始的,它会随着负载的变化而增加 HTTP 流量。当我们开始意识到 web API 的性能受到了冲击时,就需要进行优化和性能改进。

对性能的关注主要是特定于应用的,但建议在构建 web API 应用时遵循最佳实践和技术。性能和优化是一个连续的过程,需要定期监控以检查瓶颈。

由于 web API 是通过 HTTP 公开和使用的,探索各种最佳实践以保持应用在轻负载或重负载情况下的良好性能应该是头等大事。

在本章中,我们将学习如何度量应用性能,以异步方式编写控制器操作方法,压缩 HTTP 响应,以及实现缓存策略以优化资源使用。

在本章中,我们将研究以下主题:

  • 测量应用性能
  • 异步控制器动作方法
  • HTTP 压缩
  • 内存缓存的实现
  • 使用分布式缓存
  • 响应缓存

测量应用性能

web API 应用性能可以通过使用各种技术来衡量。最重要的测量参数之一是在 WebAPI 上运行负载测试。

我们将使用Apache HTTP 服务器基准测试工具,也称为ab.exe。它是在端点上发送数百个并发请求的工具。

我们要瞄准的终点是/api/contacttype,动作方式是GetAllContactTypesGetAllContactTypeAsync

这两种操作方法都使用同步和异步方式,使用 Dapper ORM 从数据库调用存储过程。在下一节中,我们将更详细地了解使用async await关键字的异步 web API。

请参阅链接https://httpd.apache.org/docs/2.4/programs/ab.html ,用于使用 ab.exe 工具,然后运行应用并执行负载测试。运行该命令后,我们将看到类似的测试结果(它们根据系统配置而不同):

ab.exe for synchronous API endpoint

通过检查Requests per second参数,我们可以看到async在负载测试中确实表现出了改进的性能:

ab.exe for asynchronous API endpoint

其他测量应用的方法:

异步控制器动作方法

ASP.NET 使用TAP基于任务的异步模式)支持异步操作,该模式首次在.NET 4.0 框架中发布,并在.NET 4.5 及更高版本中使用asyncawait关键字进行了重大改进。

通常,.NET 中的异步编程有助于实现响应性应用,提高可伸缩性,并在 web 应用中处理大量请求。

.NET Core 还支持以asyncawait模式的形式进行异步编程。当使用 I/O 或 CPU 绑定或数据库访问时,应使用此模式。

由于异步意味着不在同一时间发生,因此以异步方式调用的任何方法稍后都将返回结果。为了协调返回的结果,我们使用了Task(无返回值,即Void)或Task<T>(返回值)。await关键字允许我们执行其他有用的工作,直到Task返回结果。

要了解更多关于async await模式的信息,请阅读此链接https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

在 ASP.NET Web API(Core 或 Web API 2)中,action方法执行异步工作,并返回结果。web API 控制器不应分配有async关键字。

从上一章的MyWalletweb API 演示应用示例(第 10 章错误处理、跟踪和日志记录中),我们将重构WalletController动作方法以异步工作。

MyWallet演示应用使用 EF Core In Memory provider,我们将按照第 9 章中的与 EF Core部分、与数据库的集成对其进行扩展,以与 Microsoft SQL Server 数据库配合使用。

创建新的 web API 控制器或修改现有控制器。WalletController动作方法现在被重构,以异步方式工作。检查以下控制器代码:

    [Route("api/[controller]")] 
    public class WalletController : Controller 
    { 
       private readonly WalletContext _context;        
       private readonly ILogger<WalletController> _logger;         
       public WalletController(WalletContext context,
         ILogger<WalletController> logger) 
       { 
          _context = context; 
          _logger = logger; 
       }         

       // GET api/values/5 
       [HttpGet("{id}")] 
       public async Task<IActionResult> Get(int id) 
       {             
          var spentItem = await _context.Wallet.FindAsync(id); 
          if (spentItem == null) 
          { 
             _logger.LogInformation($"Daily Expense for {id} 
               does not exists!!");                 
             return NotFound(); 
          } 
          return Ok(spentItem); 
       } 

       // POST api/values 
       [HttpPost] 
       public async Task<IActionResult> Post([FromBody]DailyExpense value) 
       { 
         if (value == null) 
         { 
            _logger.LogError("Request Object was NULL"); 
            return BadRequest(); 
         } 
         CheckMovieBudget(value); 
         if (!ModelState.IsValid) 
         {                 
            return BadRequest(ModelState); 
         } 
         var newSpentItem = _context.Wallet.AddAsync(value); 
         await SaveAsync(); 
         return Ok(newSpentItem.Result.Entity.Id); 
       } 

       //Complete code part of source code bundle 
    } 

现在让我们通过分解代码来理解代码:

  • 所有动作(GETPOSTPUTDELETE方法现在都有一个async关键字,表示它们是异步调用的一部分。
  • 所有的async方法返回包含方法操作返回值的Task<IActionResult>,通常值为状态码和响应数据。
  • await关键字用于实现了async的方法。我们使用 efcore,因为它为异步操作提供了几乎所有的函数。

Best practice is to make methods asynchronous from top to bottom, that is, don't mix synchronous and asynchronous code.

运行应用并使用 Postman 对其进行测试。对于简单的测试,我们感觉不到异步方法的优势。当我们有大量的负载时,它表现得很好,但是仍然编写async方法将使应用负载就绪。

HTTP 压缩

Web API 请求和响应通过 internet 传输(基于 HTTP 的数据传输)。网络带宽是宝贵的,它因地区而异。Web API 响应大多是 JSON 形式(一个轻量级的字符串集合)。在这些情况下,即使我们发送了大量数据,这也非常重要。

为了通过 HTTP 快速传输响应数据,最好在返回到客户端之前压缩响应。ASP.NET Core 提供了响应压缩中间件,在将响应发送给客户端之前对其进行压缩。

让我们看看它的操作,在GET请求上创建PersonController,返回列表为Person(您仍然可以继续使用书中的任何 Web API 项目)。我正在使用GenFu——NuGet 软件包生成真实的原型数据,安装这个软件包,或者我们甚至可以连接到数据库并返回任何表的响应。

GenFu 会给我一个Person类的集合,我在调用PersonController时返回。以下是PersonController的代码:

    using GenFu;
    using Microsoft.AspNetCore.Mvc;
    using System;
    namespace compression_cache_demo.Controllers 
    {
      [Route("api/[controller]")]
      public class PersonController : Controller  
      {
        // GET: api/values
        [HttpGet]
        public IActionResult Get()
        {
          //Generate demo list using GenFu package 
          //Returns 200 counts of Person object
          var personlist = A.ListOf<Person>(200);
          return Ok(personlist);
        }
      }
      public class Person
      {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
        public DateTime DoB { get; set; }
      }
    }

运行应用,浏览 Google Chrome 中的Person控制器(首选),您将看到 200 个Person对象在 HTTP 上的 JSON 格式响应大小(大小可能在您的机器上有所不同)。

添加响应压缩中间件

ASP.NET Core 提供了一个中间件,在将响应发送回之前对其进行压缩。该中间件提供程序压缩不同的 MIME 类型,在本例中,我们对 JSON 数据感兴趣。

这个包Microsoft.AspNetCore.ResponseCompression是作为.NET SDK 的一部分包含的,有趣的是,我们不需要在控制器或操作级别工作,只需要在 HTTP 管道处理中包含这个中间件。

默认情况下,使用 GZIP 压缩提供程序;我们可以使用其他压缩提供程序或编写自己的压缩提供程序。

打开Startup类,在ConfigureServicesConfigure方法中进行如下更改:

    public void ConfigureServices(IServiceCollection services) 
    {             
       services.AddResponseCompression();             
       services.AddMvc(); 
    }         
    public void Configure(IApplicationBuilder app, IHostingEnvironment env,
       ILoggerFactory loggerFactory) 
    {             
       app.UseResponseCompression(); //Before logging 
       app.UseMvc(); //code removed for brevity 
    } 

运行应用以查看压缩后的响应大小。请参阅以下屏幕截图(您的系统可能会有所不同):

Response compression middleware in action

内存缓存的实现

访问资源是一项昂贵的操作,如果频繁地请求资源,并且几乎不更新资源,那么访问资源的成本甚至更高。为了获得性能更好的 Web API,必须通过实现缓存机制来减少访问更新最少的资源的负担。

缓存概念通过减少生成内容所需的工作,有助于提高应用的性能和可伸缩性。

NET Core 提供了一种基于 web 服务器的内存缓存技术,称为内存缓存。内容缓存通过使用IMemoryCache接口在 web 服务器内存上进行。

内存缓存是有限使用的好选择;不在 web 场上承载的应用。它速度快,但使用简单。只需将IMemoryCache接口注入Controller/类即可。下面的代码片段说明了这一点。

在本例中,Dapper(Micro-ORM)用于从数据库中获取值,并将其作为响应发送。要在 ASP.NET Core 中使用 Dapper,请参考第 9 章与数据库集成:

    [Route("api/[controller]")] 
    public class ContactTypeController : Controller 
    { 
       private string connectionString; 
       private readonly IMemoryCache _cache; 

       public ContactTypeController(IMemoryCache memoryCache) 
       { 
          _cache = memoryCache; 
          connectionString = "Data Source=..\\SQLEXPRESS;
          Initial Catalog=AdventureWorks2014;Integrated Security=True"; 
       } 
       public IDbConnection Connection 
       { 
          get 
          { 
             return new SqlConnection(connectionString); 
          } 
       } 

       // GET: api/values 
       [HttpGet] 
       public async Task<IActionResult> Get() 
       { 
         // Look for cache key. 
         if (!_cache.TryGetValue("ContentTypeKey", out IList<ContactType>
           contentList)) 
         { 
            // Setting the cache options 
            var cacheEntryOptions = new MemoryCacheEntryOptions() 
            // Keep in cache for this time, reset time if accessed. 
            .SetSlidingExpiration(TimeSpan.FromSeconds(60)); 
            contentList = await GetAllContactTypesAsync(); 
            // Save data in cache. 
            _cache.Set("ContentTypeKey", contentList, cacheEntryOptions); 
         } 
         return Ok(contentList); 
       } 
       //Complete code part of source code bundle 
    } 

现在让我们通过分解来理解整个代码:

  • 使用 DI,我们将IMemoryCache注入到与工作缓存相关的方法中,如TryGetValueSet
  • 我们正在使用Connection属性使用 Dapper ORM 连接到数据库。我们正在使用 AdventureWorks2014 数据库。您也可以使用任何示例或真实世界的数据库。
  • Get方法中,首先我们要检查缓存键条目是否存在,如果存在,它将返回它的响应。
  • 如果缓存密钥不存在,则使用GetAllContactTypesAsync方法获取记录,然后使用ContentTypeKey方法SET添加到内存缓存中。

运行应用。当第一次访问ContactTypeController时,从数据库中提取数据,在任何后续访问中,web API 都会从缓存中返回数据。

使用分布式缓存

大多数真实世界的企业应用从各种数据源(如第三方数据库、web 服务)获取数据,最重要的是,web API 部署在云或服务器场环境中。

在前面的例子中,内存不能用于缓存,因为它是基于 web 服务器内存的。为了在部署的环境中提供更健壮的缓存策略,建议使用分布式缓存。

分布式缓存将数据存储在持久性存储中,而不是 web 服务器内存中,这样缓存数据就可以跨部署的环境使用。

实际数据存储获得的请求少于内存中的请求,因此分布式缓存在 web 服务器重新启动、部署甚至失败时仍能生存。

分布式缓存既可以通过Sql Server实现,也可以通过IDistributedCache接口通过Redis实现。

使用 SQL Server 分布式缓存

我们将使用 SQL Server 进行分布式缓存;甚至 Redis 也可以使用。要使用 SQL Server,请使用 NuGet 安装以下软件包:

Microsoft.Extensions.Caching.SqlServer: 2.0.0-preview2-final 

要使用 sql 缓存工具,请将SqlConfig.Tools添加到.csproj文件的<ItemGroup>元素并运行dotnet restore(可选):

<DotNetCliToolReference Include="Microsoft.Extensions.Caching.SqlConfig.Tools"
 Version=" 2.0.0-preview2-final" /> 

完成此操作后,通过从项目的根文件夹运行以下命令,验证 SQL tools for cache 工作正常:

dotnet sql-cache create -help 

之后,运行以下命令在PacktDistCache数据库中创建一个Democache表,该表存储所有缓存项。

在运行以下命令之前,确保创建了一个PacktDistCache数据库:

dotnet sql-cache create "Data Source=..\SQLEXPRESS;Initial Catalog=PacktDistCache;Integrated Security=True;" dbo DemoCache 

您可以验证该表是使用 SQLServerManagementStudio 创建的。

现在,我们已经准备好在 MS SQL Server 中使用分布式缓存存储,请更新Startup类以通知它使用此位置进行分布式缓存:

    public void ConfigureServices(IServiceCollection services) 
    { 
      services.AddDistributedSqlServerCache(options => 
      { 
        options.ConnectionString = @"Data Source=..\SQLEXPRESS;
        Initial Catalog=PacktDistCache;Integrated Security=True;"; 
        options.SchemaName = "dbo"; 
        options.TableName = "DemoCache"; 
      }); 

      // Add framework services. 
      services.AddMvc(); 
    } 

我们将创建CurrencyConverterController,它从公共 web API 获取每日货币汇率,并将其存储在缓存数据库中。对于对CurrencyConverterController的任何进一步访问,数据将从缓存返回,而不是从公共 web API 返回。这减少了服务器获取每个请求的速率的负担:

    namespace distributed_cache_demo.Controllers 
    { 
      [Route("api/[controller]")] 
      public class CurrencyConverterController : Controller 
      { 
        private readonly IDistributedCache _cache; 
        public CurrencyConverterController(IDistributedCache cache) 
        { 
          _cache = cache; 
        } 
        // GET: api/values 
        [HttpGet] 
        public async Task<IActionResult> Get() 
        { 
          var rate = await GetExchangeRatesFromCache(); 
          if (rate != null) 
          { 
             return Ok(rate); 
          } 

          await SetExchangeRatesCache(); 
          return Ok(await GetExchangeRatesFromCache()); 
        }        

        private async Task SetExchangeRatesCache() 
        { 
          var ratesObj = await DownloadCurrentRates(); 
          byte[] ratesObjval = Encoding.UTF8.GetBytes(ratesObj); 

          await _cache.SetAsync("ExchangeRates", ratesObjval, 
            new DistributedCacheEntryOptions()                     
            .SetSlidingExpiration(TimeSpan.FromMinutes(60))                     
            .SetAbsoluteExpiration(TimeSpan.FromMinutes(240)) 
          ); 
        } 
        private async Task<RatesRoot> GetExchangeRatesFromCache() 
        { 
          var rate = await _cache.GetAsync("ExchangeRates"); 
          if (rate != null) 
          { 
            var ratestr = Encoding.UTF8.GetString(rate); 
            var ratesobj = JsonConvert.DeserializeObject<RatesRoot>(ratestr); 
            return ratesobj; 
          } 
          return null; 
        } 
      }  
    } 

这就是代码的工作原理:

  1. IDistributedCacheSetGet值被 DI 到 web API 控制器类中。
  2. SetExchangeRatesCache方法调用DownloadCurrentRates(),然后使用ExchangeRates键将它们设置为缓存。
  3. GetExchangeRatesFromCache方法读取缓存数据库,从ExchangeRates键获取值。
  4. Get()方法获取数据并返回响应(如果存在),否则首先设置数据并返回缓存值。

运行应用时,访问CurrencyConverterController会将值设置到缓存数据库中,任何后续访问都会从数据库返回数据。缓存数据存储在数据库中,如图所示:

Distributed Cache store

响应缓存

在 ASP.NET Core 中,响应缓存中间件允许响应缓存。它将缓存相关的头添加到响应中。这些头指定您希望客户端、代理和中间件如何缓存响应。

要使用此功能,我们需要使用 NuGet 包含其软件包,根据.NET Core 2.0 最新版本,预安装了相关软件包:

"Microsoft.AspNetCore.ResponseCaching": "2.0.0-preview2-final " 

安装包后,更新Startup类,将ResponseCaching添加到服务集合中:

    public void ConfigureServices(IServiceCollection services) 
    {    
       //Response Caching Middleware 
       // Code removed for brevity 
       services.AddResponseCaching(); 
       services.AddMvc(); 
    } 

还包括 HTTP 管道处理中的ResponseCaching中间件:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env,
      ILoggerFactory loggerFactory) 
    { 
       app.UseResponseCompression(); 
       app.UseResponseCaching();             
       app.UseMvc(); 
    } 

现在中间件Startup类被更新为使用响应缓存,是时候将它们添加到控制器操作方法中了。

应避免对经过身份验证的客户端或数据进行响应缓存,因此,我们正在使用ResponseCache属性更新控制器操作方法:

    public class PersonController : Controller 
    { 
      // GET: api/values 
      [HttpGet] 
      [ResponseCache(VaryByHeader = "User-Agent", Duration = 30)] 
      public IActionResult Get() 
      {        
         //Generate demo list using GenFu package 
         //Returns 200 counts of Person object 
         var personlist = A.ListOf<Person>(200); 
         return Ok(personlist); 
      }         
    } 

此属性将设置缓存控制标头,并将最大使用时间设置为 30 秒。运行应用时,响应头显示缓存控制头(在 Fiddler 或 Chrome 工具中)。

总结

在本章中,我们学习了大量关于如何编写异步 Web API 控制器、响应压缩以及通过连接缓存机制来提高响应时间的知识。

我们还学习了如何度量 web API 应用的性能。

在下一章中,我们将研究将应用发布和部署到不同环境和托管提供商上的不同方式。

十二、托管和部署

这本书快结束了。在本书的学习过程中,我们通过创建 ASP.NET Core web API 项目、编写控制器和操作、添加路由、编写自定义中间件、单元测试代码、处理异常以及执行一些优化,了解了许多有关 web API 的概念。现在是托管和部署 WebAPI 应用的时候了。

由于 ASP.NET Core 是跨平台的,托管和部署不局限于 Windows 环境(IIS 和 Azure)本身;AWS、Docker、Linux 等环境都是不错的选择。

在本章中,我们将重点介绍以真正跨平台的方式托管和部署一个示例 ASP.NET Core Web API 项目。

在本章中,我们将研究以下主题:

  • 创建演示 ASP.NET Core Web API 项目
  • 发布 web API 项目
  • 独立 web API
  • 部署策略
  • 将 web API 部署到 IIS
  • 将 web API 部署到 Azure 应用服务
  • 将 web API 发布到 Azure 上的 Windows 虚拟机
  • 向 Docker 发布 web API
  • 在 AWS 上将 web API 发布到 EC2
  • 将 web API 发布到 Linux

创建演示 ASP.NET Core Web API 项目

PacktContacts将是在我们的各种环境中托管和部署的演示项目。该项目是使用本书中学习的一些功能构建的,例如属性路由、自定义中间件、链接生成和路由约束。

创建一个名为PacktContacts的 ASP.NET Core Web API 项目,在 controller 文件夹中创建一个 Web API 控制器类ContactsController,并复制以下代码:

    namespace PacktContacts.Controllers 
    { 
      [Route("api/[controller]")] 
      public class ContactsController : Controller 
      {         
        static List<Contact> ContactList = new List<Contact>(); 

        // GET: api/Contacts 
        [HttpGet] 
        public IActionResult Get() 
        { 
          return Ok(ContactList); 
        } 

        // GET api/Contacts/5 
        [HttpGet("{id:int}", Name = "GetContacts")] 
        public IActionResult Get(int id) 
        { 
          var findContact = ContactList.Where(e => e.Id == id); 
          if(findContact != null) 
          { 
             return Ok(findContact); 
          } 
          else 
          { 
            return NotFound(); 
          }             
        } 

        // POST api/Contacts 
        [HttpPost] 
        public IActionResult Post([FromBody]Contact contactsInfo) 
        { 
          if (contactsInfo == null) 
          { 
            return BadRequest(); 
          } 
          ContactList.Add(contactsInfo); 
          return CreatedAtRoute("GetContacts", 
            new { Controller = "Contacts",
            id = contactsInfo.Id }, contactsInfo); 
        } 

        // Complete code part of source code bundle         
      } 
    }  

The complete source code is available in the code bundle.

Model文件夹中创建Contact类文件并复制以下代码。它用作复杂对象,但用作模型:

    public class Contact 
    { 
      public int Id { get; set; } 
      public string FirstName { get; set; } 
      public string LastName { get; set; } 
      public string Email { get; set; }         
    } 

您可以按如下方式分解前面的代码:

  • ContactsController是对Contact类执行 CRUD 操作的 web API 控制器。
  • 静态列表属性保存Contact的记录。在本例中,它的作用类似于数据库。
  • GetPostPutHTTP 方法使用IActionResult返回响应。我们可以使用 HTTP 代码获得各种结果。
  • Post方法响应为CreatedAtRoute。这将在响应头中生成一个链接。
  • PutDelete方法仅在存在接触时有效;否则,他们会做出适当的回应。
  • Contact是一个包含基本细节的 POCO 类。

PacktHeaderValidator--a custom middleware, mentioned in Chapter 6, Middleware and Filters, can be used to check whether the request contains a custom header entry for the web API to respond to.

发布 ASP.NET Core Web API 项目

我们创建了一个演示 web API 项目PacktContacts,对其进行了测试,然后在本地开发环境中运行。对于生产,应发布应用。

.NET Core 或 ASP.NET Core 项目(MVC 或 Web API)可以使用 CLI 或 Visual Studio 工具发布。我们将学习出版它的两种方法。

通过 CLI 发布

第 3 章对 ASP.NET Core Web API的剖析中,我们学习了各种.NET Core 命令。要发布应用,.NET Core CLI 为我们提供了dotnet publish命令。它生成运行应用所需的工件。

从项目文件夹中打开命令提示符,然后运行以下命令进行发布:

dotnet publish --output "<output-path>" --configuration release  

分解发布命令:

  • dotnet publish命令通过引用*.csproj编译应用。它收集所有依赖项并将它们发布到一个目录中。
  • -output选项指定发布项目的目录路径。
  • -configuration选项指定以RELEASE模式发布哪个应用。默认情况下,它始终处于DEBUG模式。

导航到文件资源管理器中的输出文件夹,以查看包含已编译 DLL、appsettings.jsonweb.config和运行应用所需的其他库的已发布应用。文件夹屏幕截图显示了其中一些文件。

dotnet publish命令在大多数情况下都已足够,但.NET CLI 提供了更多用于发布命令的选项。

Read through this excellent documentation on dotnet publish: https://docs.microsoft.com/en-us/dotnet/articles/core/tools/dotnet-publish.

dotnet-publish output folder The output folder containing the portable PacktContacts application can run on any OS with the .NET Core runtime already installed.

要运行已发布的应用,请从命令行导航到输出文件夹并运行以下命令,如以下屏幕截图所示:

dotnet PacktContacts.dll

Running the published application

PacktsContacts应用运行http://localhost:5000。这可以配置到任何端口。使用 Fiddler 或 Postman 工具测试 API。

通过 VisualStudio 工具发布

VisualStudioIDE 为发布应用提供了出色的工具支持。正如旧版本的 ASP.NET 在 Visual Studio 中执行发布步骤一样,在 ASP.NET Core 中也没有什么不同。

在 Visual Studio 2017 中打开PacktContacts应用,右键单击项目名称,然后单击发布,打开发布项目的对话框窗口。

它为我们提供了三个选择发布目标的选项:

  • Microsoft Azure 应用服务:在 Azure 上发布
  • 导入:导入现有配置文件以进行发布
  • 自定义:使用 web 部署、包、FTP 或文件系统发布

在本节中,我们将使用自定义选项将项目发布到文件系统,类似于 CLI 方法。单击 Custom 并为其提供配置文件名称PacktPublish

在连接下,选择发布方法作为文件系统,并选择目标位置作为文件系统,如下所示:

Select the Publish method and target location

在“设置”下,将配置选项选择为“发布”,将目标框架选项选择为.NETCoreApp。如果安装了不同版本的.NET Core,则 Target Framework 字段可能会显示更多选项。

我们这里没有针对任何特定的运行时。在下一节中,我们将探讨构建独立应用时的运行时。

Setting the configuration and target framework

单击“发布”按钮开始发布过程。完成后,提供的目标位置将具有运行应用所需的所有文件。将在解决方案结构中创建配置文件文件夹,其中包含使用发布向导时提供的所有设置。

要运行应用,只需按照 CLI 部分中所示的命令进行操作。

独立 web API

在上一节中,我们将 ASP.NET Core 应用发布为可移植的.NET Core 应用。在任何操作系统(Windows、macOS 或 Linux)上,如果安装了.NET Core 运行时,前面的可移植应用都将运行。

可移植的.NET Core 应用本机运行,即使它们发布在任何操作系统上。它是通过本机运行libuv(ASP.NET Core 应用的 web 服务器)实现的。

ASP.NET Core 可以构建为独立(自托管)应用,即包含运行时(.NET Core 运行时)的已发布应用。由于.NET Core(ASP.NET Core)应用本质上是控制台应用,当它们作为独立应用发布时,将生成一个可执行文件,运行此文件将启动应用。

我们将PacktContacts作为一个独立的应用发布,编辑*.csproj文件,在PropertyGroup部分添加RuntimeIdentifiers,如下图:

    <PropertyGroup> 
      <TargetFramework>netcoreapp2.0</TargetFramework> 
      <RuntimeIdentifiers>win7-x64</RuntimeIdentifiers> 
    </PropertyGroup>  

运行时也称为RID,.NET Core 运行时标识符RID)。我们需要提到.NET Core 应用将作为独立应用构建的目标操作系统。

我使用 Windows 7 x64 计算机作为独立应用构建 ASP.NET Core Web API。可以同时针对多个 RID。应该运行dotnet restore命令来恢复所有包(如果在编辑器中编辑*.csproj文件,则应该显式调用它)。

For a different OS runtime identifier, read through the documentation of the .NET Core runtime identifier at https://docs.microsoft.com/en-us/dotnet/articles/core/rid-catalog.

PacktContacts文件夹运行以下命令以创建独立应用:

dotnet publish --output "<output-path>" --configuration release  

提供适当的输出路径以保存已发布的应用。成功完成后,您将注意到许多文件被复制,并且还创建了一个PacktContacts.exe文件。

它包含已发布的 web API 以及运行应用的.NET Core 运行时。

现在PacktContacts ASP.NET Core Web API应用可以作为 EXE 运行,如下所示:

Running as a standalone application It's recommended that you use the appropriate runtime identifier. As it's built for Windows 7 x64, it might work on a higher version of the Windows OS, but it won't work on Linux or macOS.

部署策略

ASP.NET Core 运行在基于 libuv 的全新 web 服务器上,该服务器名为Kestrel

Microsoft recommends that Kestrel should be treated as an internal web server-excellent for development, it but shouldn't be exposed to the internet.

那么,一个显而易见的问题就是如何托管 ASP.NET Core 应用以将其暴露于互联网。下图简要说明了部署策略:

ASP.NET Core apps deployment strategy

该图描述了以 IIS、Nginx 等形式提供代理(也称为反向代理)的部署策略。

这些反向代理允许我们通过从 HTTP 服务器提供静态内容、缓存请求、压缩请求和 SSL 终止服务来减轻工作负担。

来自 internet 的任何请求都将通过反向代理(IIS 或 Nginx)进行处理。请求被传递,然后 ASP.NET Core 应用调用 Kestrel 服务器对此采取操作。

在接下来的部分中,我们将使用此策略部署PacktContactsweb API。

将 web API 部署到 IIS

在不同的 Windows 操作系统(计算机或服务器)上托管时,将 ASP.NET Core 应用部署到 IIS 是首选。

了解 IIS 如何与 ASP.NET Core 应用配合使用非常重要。在上一节中,我们将应用发布到输出文件夹,其中包含运行它的所有人工制品。

著名的 web.config 也存在于发布的文件夹中。检查内容以了解 IIS 和 ASP.NET Core 如何协同工作:

    <?xml version="1.0" encoding="utf-8"?> 
    <configuration> 
      <!-- Configure your application settings in appsettings.json. 
       Learn more at http://go.microsoft.com/fwlink/?LinkId=786380 --> 
      <system.webServer> 
        <handlers> 
          <add name="aspNetCore" path="*" verb="*"
           modules="AspNetCoreModule" resourceType="Unspecified" /> 
        </handlers> 
        <aspNetCore processPath="dotnet" arguments=".\PacktContacts.dll" 
          stdoutLogEnabled="false" 
          stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" /> 
      </system.webServer> 
    </configuration> 
    <!--ProjectGuid: d8d6c16d-f42a-4d87-a244-6484d6bffb5e--> 

分解web.config文件:

  • 需要安装AspNetCoreModule才能将请求传输到 Kestrel。
  • aspNetCore告知processPathdotnetargumentPacktContacts.dll。日志记录现在已禁用。这与dotnet packtcontacts.dllCLI 命令相同,但通过web.config执行。

web.config文件包含在 ASP.NET Core 中,以便 IIS 可以调用应用并让 Kestrel 处理请求。请注意,IIS 充当反向代理。

To know more about ASP.NET Core module configuration, read https://docs.microsoft.com/en-us/aspnet/core/hosting/aspnet-core-module.

在 IIS 上配置网站

假设您的计算机上已启用 IIS,请打开 IIS 管理器并执行以下步骤:

  1. 在“应用池”下,添加专门用于 ASP.NET Core 应用的新应用池。我们正在将.NET Framework 版本设置为无托管代码。
  2. 在“站点”下,右键单击“添加网站”,提供适当的站点名称,分配先前创建的应用池,并分配已发布文件夹的物理路径(执行dotnet publish步骤时参考输出路径)

在本章末尾,我们将了解如何测试PacktContacts应用:

Creating a new application pool and adding a website

在本章的最后,我们将了解如何测试PacktContactsweb API。

将 web API 部署到 Azure 应用服务

在本节中,我们将使用 Azure 应用服务部署应用。此示例使用免费试用帐户。如果您有权访问任何其他订阅,可以在此处使用。

按照以下步骤进行部署:

  1. 在解决方案资源管理器中右键单击该项目,然后选择“发布”

  2. 在发布对话框中,单击 Microsoft Azure 应用服务。

  3. 单击“新建”从 Visual Studio 创建新的资源组。您也可以使用现有的。在本例中,使用了资源组下已经存在的 Azure 应用。

  4. 单击“确定”将应用 web 部署到 Azure 应用。

  5. Web Deploy 将执行安装 dotnet 运行时、还原包以及将已发布的 Web API 应用复制到 Azure 应用的操作。

  6. 完成后,浏览器会自动打开链接。您现在可以测试它了:

Web deploy to Azure App Service

将 web API 发布到 Azure 上的 Windows 虚拟机

在本节中,我们将把发布的PacktContactsweb API 部署到在 Windows Azure 上创建的虚拟机上。我们将创建一个 Windows Server 2012 R2 数据中心作为虚拟机。

Azure 免费试用帐户就足够了。通过以下步骤部署PacktContactsweb API:

  1. 要创建 Azure 虚拟机,请按照中提到的步骤操作 https://docs.microsoft.com/en-us/azure/virtual-machines/virtual-machines-windows-hero-tutorial 创建 Windows Server 2012 R2 数据中心。
  2. 创建 VM 后,建立远程桌面连接以部署应用。
  3. 由于它是 Windows 服务器,我们将在此 VM 上的 IIS 上部署PacktContactsweb API。因为它是新创建的机器,所以不会配置 IIS;要配置它,请阅读https://docs.microsoft.com/en-us/azure/virtual-machines/virtual-machines-windows-hero-role?toc=%2fazure%2fvirtual-计算机%2fwindows%2ftoc.json
  4. 配置 IIS 后,安装.NET Core Windows Server Hostinghttps://aka.ms/dotnetcore_windowshosting_1_1_0 服务器上的包。这将安装.NET Core 运行时、.NET Core 库和 ASP.NET Core 模块。
  5. 手动或通过 FTP 将已发布的 web API 项目复制到 Windows VM,并按照节中描述的步骤在 IIS上配置网站。

Ensure that the deployed application on the VM is configured for access using a public IP address.

向 Docker 发布 web API

Docker 是一种通过使用容器来创建、部署和运行应用的工具。它们的工作方式与虚拟机类似,但更轻量级,并使用主机提供更好的性能。

To understand more about Docker, read the article What is Docker? found at https://www.docker.com/what-docker. Docker can be installed on your machine by following the appropriate steps for your machine, found at https://www.docker.com/products/docker.
To use Docker, your machine should support hardware virtualization.

在机器上安装 Docker 后,通过以下步骤构建PacktContactsAPIDocker 映像并在 Docker 容器上运行:

  1. 右键单击项目名称并转到添加| Docker 支持以创建 Docker 文件。
  2. PacktContacts项目中,创建Dockerfile并复制以下代码:
        FROM microsoft/aspnetcore: 2.0 
        ENTRYPOINT ["dotnet", "PacktContacts.dll"] 
        ARG source=.
        WORKDIR /app 
        EXPOSE 80 
        COPY $source . 

它从 ASP.NET Core 2 运行,其ENTRYPOINTPacktContacts.dll(与在 CLI 下运行时相同),没有参数。将当前目录内容复制到映像。

  1. 再次运行dotnet publish命令;这将发布包含我们创建的 Docker 文件的文件夹。
  2. 现在,通过从 Docker 终端运行以下命令来构建 Docker 映像:
 docker build D:\publishOutput -t packtcontantsAPI 

  1. 运行以下命令在 Docker 容器上运行映像:
 docker run -it -d -p 85:80 packtcontantsAPI 

一切正常运行后,使用 Docker 机器默认 IP 访问PacktContactsweb API。

在 AWS 上将 web API 发布到 EC2

在上一节中,我们为我们的 ASP.NET Core Web API 项目构建 Docker 映像packtcontactsapi。在本节中,我们将在 AWS EC2 容器服务上运行此 Docker 映像。

Docker 映像是使用所有必需的运行时预构建的,这减少了已发布应用设置环境的工作量。

创建 AWS 帐户并转到 EC2 容器服务部分。按照这些步骤操作(非常简单),您将看到将 Docker 映像推送到 AWS EC2 的命令。命令显示在以下屏幕截图中:

AWS push commands to Docker images For more in-depth explanation of the steps involved in running Docker images on AWS EC2, refer to https://aws.amazon.com/getting-started/tutorials/deploy-docker-containers/.

创建任务定义。在这里,您将指定要使用的 Docker 映像。以下屏幕截图中带下划线的文本表示所使用的推送图像:

Creating the task definition in AWS

配置服务以启动和维护先前创建的任务定义:

Configure the service to run the task

配置 AWS EC2 群集,以便在其上运行服务和任务。点击图中突出显示的行将显示访问PacktContactsweb API 所需的公共 DNS 名称:

Configure and run the cluster on EC2

将 web API 发布到 Linux

ASP.NET Core 可以托管在 Linux 操作系统上。有许多使用 Linux 操作系统的低成本主机提供商。在本节中,我们将在作为虚拟机运行的 Linux 机器上部署PacktContactsweb API。

我们将使用 Ubuntu 服务器 14.04 LTS Linux。有很多关于如何在虚拟机上运行 Ubuntu 的文章。

要安装.NET Core for Linux,请转至https://www.microsoft.com/net/core#linuxubuntu 。如果需要,安装 VisualStudio 代码将帮助您编写代码。

有两种方法可以在此 Linux 机器上部署演示 web API 项目:传输已发布的文件或从源代码运行 publish 命令。

我发现从源代码运行 publish 要容易得多。有了源代码,您也可以在 Linux 机器上工作。将源代码推送到 Git 存储库并克隆 Linux(需要安装 Git)。

克隆源代码后,运行dotnet publish命令,如本章开头所示。运行应用;它将在端口 5000 上开始侦听。

测试 packtwebapi

ASP.NET Core 应用可以通过多种方式托管和部署,包括本地 IIS、Windows Azure、Docker、独立、廉价的 Linux 托管提供商、虚拟机、AWS 等选项。

到目前为止,我们只看到 web API 应用的托管和部署,但从未测试过。在本节中,我们将测试所有 CRUD 场景和自定义中间件功能。我们将使用邮递员工具,不过,也可以使用 Fiddler。

下表显示了访问应用所需的部署位置和 URL:

| 部署位置 | 访问 URL |
| 本地 IIS | http://localhost:85 |
| 独立的 | http://localhost:5000 |
| Azure 应用服务 | http://packtcontacts.azurewebsites.net |
| 码头工人 | http://192.168.99.100:85/ |
| AWS 上的 EC2 | http://ec2-35-164-207-251.us-west-2.compute.amazonaws.com |
| Linux 主机 | http://localhost:5000 |

These URLs will vary according to your setup.

测试用例-不使用标头访问 web API

来自第 6 章中间件和过滤器的定制中间件示例希望每个请求都有一个名为packt-book的定制头,其值为Mastering Web API

如果没有报头,或者报头无效,web API 响应错误请求;否则,它将相应地作出响应:

Test web API without header

测试用例-使用标头访问 web API

在这种情况下,我们将传递值为Mastering Web APIpackt-book。web API 以 OK 响应:

Web API responds with OK

测试用例-向 web API 添加联系人

在本例中,我们将 JSON 请求主体传递给ContactsControllerPOST方法。如前一屏幕截图所示,它“必须通过自定义标题packt-book,以确保POST请求得到处理。这个例子说明了中间件的概念。

以下是 JSON 请求:

    { 
      "id": 20, 
      "firstName": "Mithun", 
      "lastName": "Pattankar", 
      "email": "mithu@abc.com", 
      "dateOfBirth": "1916-11-15" 
    } 

一旦请求得到处理,web API 将在响应正文中以201 Created状态进行响应。在观察响应头时,我们可以看到位置头以及访问所创建资源的 URL(在本例中为 contact)。

位置报头是第 5 章实现路由中学习的链路生成概念的一个示例。

Post in action

测试用例-从 web API 获取联系人

我们将使用生成的链接获取联系人详细信息:

Get the contact details by passing the ID

测试用例-从 web API 获取所有联系人

这将获得联系人列表中的所有联系人,再加上一个联系人。web API 将返回两个联系人详细信息:

Get all contacts

测试用例-编辑 web API 的联系人

在本例中,我们使用ContactsControllerPUT方法传递 JSON 请求主体。

以下是 JSON 请求:

    { 
      "id": 30, 
      "firstName": "Steve", 
      "lastName": "Jobs", 
      "email": "steve@abc.com", 
      "dateOfBirth": "1966-10-15" 
    } 

测试用例-从 web API 中删除联系人

在这种情况下,我们将通过使用DELETE方法传递 ID 来删除联系人。然后我们可以打电话Get all Contacts检查它是否被移除。如果 ID 不存在,则返回Not Found响应:

Delete a contact

总结

在本章中,我们学习了 ASP.NET Core 的各种托管和部署选项,从在 IIS 上部署已发布应用的传统方法,到将其作为独立应用托管。这一转变确实令人鼓舞。

我们了解到,通过使用 Azure 应用服务发布,ASP.NET Core 和 Azure 可以无缝集成。有很多低成本的 Linux 托管选项,我们也探讨了这些选项。诚然,ASP.NET Core 自始至终都是一种跨平台技术。

在下一章中,我们将在现代前端使用这些 web API,如 JavaScript、JQuery、Angular、React 和混合移动应用。

十三、现代网络前端

我们在这本书的最后一章。在前面的章节中,我们学习了很多概念,例如如何创建 ASP.NET Core Web API、编写控制器和操作、添加路由、中间件、单元测试和处理优化错误,以及在各种环境中部署和托管。

托管的 web API 使用 Postman 或 Fiddler 工具进行了测试,效果良好。但 ASP.NET Core Web API(以及通常使用任何框架构建的 Web API)的真正用途在于它能够被前端应用(如 Web、移动或桌面应用)使用。

正如 ASP.NET Core 是跨平台的一样,我们有各种各样的 web 前端,可以使用开源技术跨平台开发。

在本章中,我们将重点介绍如何使用现代 web 框架构建 web 应用,如 Angular 4(也称 Angular)、ReactJS、TypeScript(JavaScript 的超集)、JQuery 和 JavaScript、Bootstrap 以及 Ionic 3 framework(混合移动应用框架)。我们还将了解他们如何使用我们在前几章中开发的 web API。

在本章中,我们将研究以下主题:

  • PacktContacts-概述演示 ASP.NET Core Web API
  • web 框架的软件先决条件
  • 使用 Angular 4 使用 web API
  • 使用 Ionic 3 使用 web API
  • 使用 ReactJS 使用 web API
  • 使用 JavaScript 使用 web API
  • 使用 JQuery 使用 web API

PacktContacts-演示 web API 项目概述

第 12 章托管和部署中,我们了解了名为PacktContacts的演示 ASP.NET Core Web API 项目,并将其托管和部署到各种环境中。

web API 在Contact模型上执行基本的 CRUD 操作。我们将使用这个托管在 IIS 上的 web API 作为访问它的终点。

模型文件夹中的Contact类文件充当复杂对象,用作网络数据传输的模型:

    namespace PacktContacts.Model 
    { 
      public class Contact 
      { 
        public int Id { get; set; } 
        public string FirstName { get; set; } 
        public string LastName { get; set; } 
        public string Email { get; set; }                 
      }   
    } 

处理跨来源问题

当我们使用 Postman 或 Fiddler 测试PacktContactsweb API 时,他们通过调用适当的响应进行响应。当他们托管在现代 web 框架中使用的 web API 的端点 URL 时,它无法工作,导致了跨源问题。

任何使用 AJAX 请求调用 web API 的 web 框架都会因浏览器的安全性而无法调用另一个域。在 web 应用世界中,它通常被称为同源策略。

更简单地说,web API 应用托管在 web 服务器上,访问 URL 为http://www.abcd.com/api/packtcontacts 和 web 应用托管在单独的域上(http://www.xyz.com )。例如,WeatherAPI 公开 URL,我们在应用中使用这些 URL,但很明显它们托管在不同的域上。

当我们尝试访问 web API URL 时,会发生跨源错误。为了避免这种情况,我们需要在PacktContacts项目中启用 CORS 特性。

CORS 中间件在这里开始运行。应将其添加到Startup类的ConfigureConfigureServices方法中:

    public void ConfigureServices(IServiceCollection services) 
    { 
       // Add framework services. 
       services.AddMvc(); 

       // Add service and create Policy with options 
       services.AddCors(options => 
       { 
          options.AddPolicy("CorsPolicy", 
          builder => builder.AllowAnyOrigin() 
          .AllowAnyMethod() 
          .AllowAnyHeader() 
          .AllowCredentials()); 
       }); 
    } 
    public void Configure(IApplicationBuilder app, IHostingEnvironment env,
       ILoggerFactory loggerFactory) 
    {             
        app.UseCors("CorsPolicy");             
        app.UseMvc();             
    } 

现在让我们浏览一下代码,以便更好地理解它:

  • ConfigureServices方法中,我们使用AddCors将 CORS 添加到 web API 中。
  • 我们正在创建CorsPolicy,它允许任何源调用、任何方法和任何头访问 web API。
  • 前面的策略对于这个演示项目来说是非常自由的,但是在一个真实的例子中,我们可以有一个更严格的策略。它在全球一级发挥作用。
  • Configure方法中,我们使用 CORS 中间件并传入我们创建的CorsPolicy

其余代码与前一章(第 12 章托管和部署中的代码相同,测试 web API 的步骤也相同。

PacktHeaderValidator - a custom middleware, mentioned in Chapter 6, Middleware and Filters, can be used to check that the request contains the custom header entry for the web API to respond to.

web 框架的软件先决条件

我们从一开始就致力于开源技术,因此我们将以使用开源技术构建现代 web 前端作为结束。

需要根据所使用的操作系统安装以下软件-即,根据 Windows、Linux 或 macOS:

  • Visual Studio 代码:这是一个轻量级的跨平台代码编辑器。如果您正在非 Windows 计算机上构建 ASP.NET Core Web API,则可能已经安装了它。如果尚未安装,则可以从安装 https://code.visualstudio.com/
  • NodeJS:这是一个使用开源库的包生态系统。我们正在使用它来安装软件包。从开始安装 https://nodejs.org/en/

您可以自由使用自己选择的代码编辑器,例如 Sublime、Atom,甚至 VisualStudioIDE。我们将使用Visual Studio 代码VS 代码作为本章的代码编辑器。

使用 Angular 4 使用 web API

Angular(又名 Angular 4)是谷歌为开发高性能单页应用(SPA)而构建的开源 web 框架。2016 年,谷歌正式发布了 Angular 的最新版本,即 Angular 4。它被完全重写了,和以前的版本不一样。

Angular 4 文档内容广泛且深入;您可以在处阅读 https://angular.io/docs/ts/latest/

Angular 4 框架是用 TypeScript 编写的,TypeScript 是为 Angular 编写 web 组件的推荐语言。那么什么是打字脚本呢?

TypeScript 是生成 JavaScript 的强类型、面向对象的脚本。TypeScript 设计用于编写使用 JavaScript 的企业级 web 应用。它就像其他编程语言一样,比如 C#或 Java。

TypeScript 语言的一些好处是:

  • 编译为 JavaScript
  • 强类型或静态类型如果是松散类型的代码,您将在代码编辑器中看到红色下划线
  • 流行的 JavaScript 库是提供代码完成的类型定义的一部分
  • 封装

TypeScript 可以单独用于开发 web 应用,因为在 JavaScript 的情况下,它可以使用 npm(节点包管理器)或 MSI 安装程序安装。它可以与任何代码编辑器或 IDE 一起使用,如 VS 代码、Sublime、Atom 或 Visual Studio IDE。有关该语言的更多信息,请参阅www.typescriptlang.org

It's recommended that you learn the basics of Angular 4 and TypeScript before moving ahead in this chapter.

角型 CLI

Angular CLI 是一个命令行界面CLI)工具,用于创建 Angular 4 应用,从创建应用到创建服务、组件、路由、目标构建和运行单元测试。

创建 Angular 4 应用有两种不同的方法:通过 GitHub 上的启动包或从 CDN 运行应用。坦率地说,Angular CLI 是创建 Angular 4 应用的最佳工具,我建议您在本章中使用它。

安装最新的 NodeJ 和 npm 以与 Angular CLI 配合使用。从 NPM 安装 CLI 工具,并打开命令提示符以运行以下命令:

npm install -g angular-cli 

安装后,运行以下命令创建一个名为ngPacktContacts的 Angular 应用,用于为我们的PacktContactsAPI 创建 web 前端:

ng new ngPacktContacts 

此命令创建端到端的 Angular 4 应用。构建不同的应用组件有许多选项。使用在中找到的信息 https://github.com/angular/angular-cli 适当生成这些选项。

在构建 Angular 4 应用时,我们需要以下元素来使用PacktContactsweb API:

  • 用于使用 HTTP 谓词调用 web API 的服务提供程序
  • 用于调用前面的服务提供者并与 HTML 交互的角度组件
  • 用于显示 UI 的角度组件模板,如 HTML

我们正在 ASP.NET Core 项目中处理一个Contacts数据模型。在我们的 Angular 4 应用中有一个类似的数据模型将是非常棒的。因为我们使用的是 TypeScript,所以我们可以使用 CLI 脚手架选项创建一个名为contacts.class.ts的类,并复制以下代码:

    export class Contacts { 
      id:number; 
      firstName:string; 
      lastName:string; 
      email:string; 
    } 

Note that TypeScript allows you to define the class and types to its properties, just like an object-oriented language.

PacktServices-角度服务提供商

让我们通过从 CLI 工具构建服务并从项目文件夹运行以下命令来创建 Angular service provider:

ng generate service packt 

这将在app文件夹中创建一个名为PacktServices的服务类。复制以下代码以使用不同的 HTTP 谓词调用 web API:

    import { Contacts } from './contacts.class'; 
    import { Http, Response, Headers, HttpModule } from '@angular/http'; 
    import 'rxjs/add/operator/map' 
    import { Observable } from 'rxjs/Observable'; 
    import { Injectable } from '@angular/core'; 

    @Injectable() 
    export class PacktServices { 
      private actionUrl: string; 
      constructor(private _http:Http) {  
        this.actionUrl = 'http://domain-name/api/contacts/'; 
      } 

      public GetAll = (): Observable<any> => { 
        let headers = new Headers({'Content-Type': 'application/json',
          'packt-book' : 'Mastering Web API', 
          'Authorization': 'Bearer ' + this.token}); 
        let options = new RequestOptions({ headers: headers }); 
        return this._http.get(this.actionUrl, options) 
        .map((response: Response) => <any>response.json()); 
      }  

      public addContacts(ContactsObj: Contacts){ 
        let headers = new Headers({'Content-Type': 'application/json',
          'packt-book' : 'Mastering Web API', 
          'Authorization': 'Bearer ' + this.token}); 

        let options = new RequestOptions({ headers: headers }); 
        return this._http.post(this.actionUrl, JSON.stringify(ContactsObj),
          options).map((response: Response) => <any>response.json()); 
      }  
      //Complete code part of source code bundle 
    } 

现在让我们了解一下我们刚刚开发的代码:

  • 导入我们创建的Contacts类模型和 Angular HTTP 模块以调用基于 REST 的 API(在我们的场景中是 web API)。
  • actionUrl参数指向 ASP.NET Core Web API 端点。这将是来自 IIS、Azure 应用服务、AWS 或 Docker 的托管 URL,甚至是运行在 localhost 上的应用。
  • 创建headers对象以将内容设置为 JSON,创建自定义头packt-book以与创建的自定义中间件协同工作。还添加了授权头以传递 JWT 令牌。
  • GetAll方法使用 HTTP GET 动词调用 web API 来获取所有记录。我们传递headers对象,否则我们会得到错误的请求作为响应。
  • addContacts方法从 UI 接收Contacts对象,并使用 HTTPPOST方法传递给 web API。
  • updateContacts方法从 UI 接收Contacts对象和ContactsId并使用 HTTPPUT方法传递给 web API。
  • deleteContacts方法从 UI 接收ContactsId对象,并使用 HTTPDELETE方法传递给 web API。

AppComponent-角度分量

组件是在页面上构建和指定元素和逻辑的主要方式。它们创建要在 HTML 页面中使用的自定义 HTML 元素(标记),并开始使用其中的角度特性。

从 Angular CLI 生成的应用中,打开 index.html 页面以查看<app-root>Loading...</app-root>html 元素。这不是一个常规的 HTML 元素,它是一个角度组件,选择器名称为app-root

在检查app.component.ts文件时,我们会看到组件声明中也提到了选择器名称、模板文件(HTML 文件)和样式表文件路径:

    @Component({ 
      selector: 'app-root', 
      templateUrl: './app.component.html', 
      styleUrls: ['./app.component.css'] 
    }) 

角度文档将深入解释Components概念;在阅读这些内容以了解更多内容 https://angular.io/docs/ts/latest/api/core/index/Component-decorator.html

我们将使用这个文件,而不是创建新的组件,以保持简单。在app.component.ts中复制以下代码:

    import { Contacts } from './contacts.class'; 
    import { PacktServices} from './packt-services.service'; 
    import { Component, OnInit } from '@angular/core'; 
    import {  FormGroup,  FormBuilder,  Validators} from '@angular/forms'; 

    @Component({ 
      selector: 'app-root', 
      templateUrl: './app.component.html', 
      styleUrls: ['./app.component.css'] 
    })   
    export class AppComponent implements OnInit { 
      title = 'Angular 2 with ASP.NET Core Web API'; 
      recordsExists: boolean = false; 
      formAddEdit:boolean = false; 
      contactModel:Contacts; 
      contactForm: FormGroup; 

      public values: any[]; 
      constructor(private _dataService: PacktServices, 
        private fb: FormBuilder) {} 
      ngOnInit() { 
        this.LoadAllContacts(); 
      } 

      LoadAllContacts(){ 
        this._dataService 
        .GetAll() 
        .subscribe(data => { 
          if (data.length > 0) { 
            this.values = data; 
            this.recordsExists = true; 
            //console.log(data); 
          } 
        }, 
        error => { 
          console.log(error.status); 
        }, 
        () => console.log('complete')); 
      } 

      onSubmit({ value, valid }: { value: any, valid: boolean }) { 
        console.log(value, valid); 
        let values = value as Contacts; 

        if(this.contactModel.id === 0){ 
        //Insert 
        values.id =  Math.floor((Math.random() * 100) + 1); 
        this.AddContacts(values); 
      } 
      else{ 
        values.id = this.contactModel.id; 
        this.UpdateContacts(values); 
      } 
      //Complete code part of source code bundle 
    } 

您可以按如下方式分解前面的代码:

  • Contacts类和PacktServices是导入的。

  • AppComponentselectortemplateUrlstyleUrls声明。还有很多其他的选择。

  • 定义了AppComponent类,该类继承自OnInit页面循环事件。还声明了其他新属性。

  • AppComponent构造函数通过依赖注入接收PacktServicesFormBuilder

  • 组件初始化时调用ngOnInit方法并调用LoadAllContacts方法。

  • LoadAllContacts方法调用PacktServices,后者依次调用 web API 并加载 UI 上的所有联系人。仅当记录存在时,才会显示联系人列表。

  • createContact方法使用 Angular 2 中的反应式表单技术在 UI 上设置联系人表单。

  • OnSubmit方法通过为POST(添加)和PUT(更新)调用PacktServices来保存在 UI 上输入的联系人详细信息。

AppComponent 模板-角度组件的 HTML

每个角度组件都伴随着 UI 模板及其选择器。在我们的示例中,aap.component.html文件是显示 UI 的模板。

要打开app.component.html文件,请复制以下代码:

    <nav class="navbar navbar-default"> 
      <div class="container-fluid"> 
        <div class="navbar-header"> 
          <a class="navbar-brand" href="#">ngPacktContacts - {{title}}</a> 
        </div> 
      </div> 
    </nav>  
    <div class="container"> 
      <div> 
        <button type="button" class="btn btn-primary" 
          (click)="createContact()">Create Contact</button> 
      </div> 
      <br> 
      <div id="contactlist" *ngIf="recordsExists"> 
      <br> 
      <table class="table table-hover"> 
        <thead> 
          <tr> 
            <th>First Name</th> 
            <th>Last Name</th> 
            <th>Actions</th> 
          </tr> 
        </thead> 
        <tbody> 
          <tr *ngFor="let item of values"> 
            <td>{{item.firstName}}</td> 
            <td>{{item.lastName}}</td> 
            <td><button type="button" (click)="editContact(item)" 
               class="btn btn-primary btn-sm">Edit</button> 
              <button type="button" (click)="deleteContact(item)"
                class="btn btn-danger btn-sm">Delete</button> 
             </td> 
           </tr> 
         </tbody> 
       </table> 
     </div> 
     <div id="contactlistform" *ngIf="formAddEdit"> 
       <form novalidate (ngSubmit)="onSubmit(contactForm)"
         [formGroup]="contactForm">       
         <div class="form-group"> 
            <label>First Name</label> 
            <input type="text" class="form-control" 
              formControlName="firstname"> 
         </div> 
         <div class="form-group"> 
            <label>Last Name</label> 
            <input type="text" class="form-control"
              formControlName="lastname"> 
         </div> 
         <div class="form-group"> 
           <label>Email Address</label> 
           <input type="email" class="form-control"
             formControlName="email"> 
         </div> 
         <div class="form-group"> 
           <label>Date Of Birth</label> 
           <ng2-datepicker formControlName="date"></ng2-datepicker> 
         </div> 
         <button class="btn btn-success" type="submit"
           [disabled]="contactForm.invalid">Save</button> 
         <button class="btn btn-danger" type="button" 
           (click)="cancelForm()">Cancel</button> 
       </form> 
     </div> 
    </div> 

让我们深入了解我们的代码,以便更好地理解:

  • Create按钮显示输入Packt触点的表格。
  • 显示联系人列表的表格与 Angular For 语句(*ngFor一起保存。
  • 每个记录都有一个EditDelete按钮用于各自的操作。
  • 有一个表格可以输入/更新Packt联系人的记录。这种形式属于角反应形式的范畴。

现在使用命令行ng serve运行应用,并打开浏览器查看正在运行的应用。

该演示项目实现了基于 JWT 的身份验证,这使我们能够包括登录屏幕来执行身份验证。成功验证登录凭据后,将生成 JSON web 令牌。此令牌保存在本地存储器中,与后续 HTTP 调用一起作为Authorization头传递:

Login form in Angular 4 Note that Angular CLI runs the application on localhost:4200 port. This can also be changed.

Contact表单帮助输入联系人详细信息并保存,如下所示:

Contact form in Angular 4

在表单上填写所有有效条目时,“保存”按钮将启用,从而使您能够将有效数据保存到 web API。

添加联系人后,我们可以看到 UI 上的列表,以及每行的编辑和删除按钮的操作。

使用 Ionic 3 构建混合移动应用

Ionic 3 是一个开源框架,用于构建混合移动应用,可以内置到 Android、iOS 和 Windows 手机的移动应用中。

爱奥尼亚 3 是建立在 Angular 和 ApacheCordova 之上的,带有 HTML5、CSS 和 SASS。在本节中,我们将构建一个使用PacktContactsweb API 的混合本机应用。

按照中的步骤进行操作 https://ionicframework.com/docs/v2/setup/installation/ 安装离子 3。确保已安装最新版本的 NodeJS 和 NPM。

通过运行以下命令,使用 TypeScript 为 Ionic 3 创建一个空白应用:

ionic start packtcontactapp blank 

这将创建一个构建在 Angular 框架之上的 Ionic 3 项目,因此我们可以利用上一节中编写的代码。

PacktService是使用 HTTP 与 web API 对话的服务提供商,可通过运行此处显示的 IONAL CLI 命令创建:

ionic g provider PacktService 

Ionic CLI 命令可在中找到 https://ionicframework.com/docs/v2/cli/

由于提供程序与 Angular 4 服务提供程序相同,因此我们可以重用在上一节中创建的相同的PacktServiceweb API。

主页-爱奥尼亚 3 页

Ionic 3 通过创建类似于 Angular 组件和模板 URL 概念的页面来工作,但在本例中,它们是通过推送和弹出概念来导航的。

我们可以重用上一节中的大部分app.component.ts代码;在home.ts中复制以下代码:

    import { PacktService } from './../../providers/packt-service'; 
    import { Contacts } from './../../providers/contacts.class'; 
    import { Component } from '@angular/core'; 
    import {  FormGroup,  FormBuilder,  Validators} from '@angular/forms'; 
    import { NavController } from 'ionic-angular'; 

    @Component({ 
      selector: 'page-home', 
      templateUrl: 'home.html' 
    }) 
    export class HomePage { 
      title = 'Ionic 2 with ASP.NET Core Web API'; 
      recordsExists: boolean = false; 
      formAddEdit:boolean = false; 
      contactModel:Contacts; 
      contactForm: FormGroup; 
      public values: any[]; 

      constructor(public navCtrl: NavController, 
        private _dataService: PacktService,
        private fb: FormBuilder) { 
      } 
      ionViewDidLoad() { 
        this.LoadAllContacts(); 
      } 

      LoadAllContacts(){ 
        this._dataService 
        .GetAll() 
        .subscribe(data => { 
          if (data.length > 0) { 
            this.values = data; 
            this.recordsExists = true; 
            console.log(data); 
          } 
        }, 
        error => { 
          console.log(error.status); 
        }, 
        () => console.log('complete')); 
      } 

      // Code removed for brevity 
      // Similar to App.component.ts 
    } 

这就是我们的代码将如何工作:

  • 它将导入PacktService提供程序和 Contacts 类
  • ionViewDidLoadngOnInit类似,我们称之为LoadAllContacts方法
  • 代码的其余部分类似于app.component.ts

主页-爱奥尼亚 3 HTML 页面

Ionic 3 页面有一个组件和 HTML 来完成页面。我们为主页创建了组件;现在让我们对home.html文件进行更改。

打开home.html并从中复制以下代码:

    <ion-content padding> 
      <div *ngIf="formAddEdit"> 
        <form novalidate (ngSubmit)="onSubmit(contactForm)"
          [formGroup]="contactForm"> 
          <ion-list noline> 
            <ion-item> 
              <ion-label floating danger>First Name</ion-label> 
              <ion-input formControlName="firstname" type="text"></ion-input> 
            </ion-item> 
            <ion-item> 
              <ion-label floating danger>Last Name</ion-label> 
              <ion-input formControlName="lastname" type="text"></ion-input> 
            </ion-item> 
            <ion-item> 
              <ion-label floating danger>Email</ion-label> 
              <ion-input formControlName="email" type="text"></ion-input> 
            </ion-item>         
          </ion-list> 
          <button ion-button color="secondary" type="submit"
             [disabled]="contactForm.invalid">Save</button> 
          <button ion-button color="danger" type="button"
            (click)="cancelForm()">Cancel</button> 
        </form> 
      </div> 
      <ion-list *ngIf="recordsExists"> 
        <ion-item-sliding *ngFor="let item of values"> 
          <button ion-item (click)="editContact(item)">         
            <h2>{{item.firstName}} {{item.lastName}}</h2> 
          </button> 
          <ion-item-options> 
            <button ion-button color="danger" 
              (click)="deleteContact(item)">Delete</button> 
          </ion-item-options> 
        </ion-item-sliding> 
      </ion-list> 
      <ion-fab right bottom> 
        <button ion-fab color="secondary" (click)="createContact()">
          <ion-icon name="add"></ion-icon> 
        </button> 
      </ion-fab> 
    </ion-content> 

The complete source code is available in the code bundle.

现在,让我们了解一下该代码的具体工作原理:

  • ion-header标记将标题信息显示为静态或动态
  • 只有在使用*ngIf进行添加或编辑时,才会显示“联系人表单”部分
  • ion-list标记显示从 web API 获取的已保存联系人
  • ion-list标签可以执行编辑和删除联系人任务
  • 底部显示Create按钮,用于创建新联系人

在命令行中,运行ionic serve命令以查看正在运行的应用。通过遵循中的步骤,还可以进一步构建 Android 或 iOS 原生应用 https://ionicframework.com/docs/cli/

Just as with Angular apps, don't forget to include PacktServices as Providers in the app.module.ts file of the Ionic app.

Ionic 3 app shows Packt contacts

使用 ReactJS 构建 web 应用

ReactJs 或 React 是一个 JavaScript 库,用于构建用户界面用户界面。它由 Facebook 构建,后来作为开源库发布。

它的重点更多地是一种声明性的、基于组件的 UI 开发方式。它与构建现代 web 前端的角度框架一样流行。

学习 React 的一个很好的起点是浏览上的文档、教程和博客 https://facebook.github.io/react/

ASP.NET web 应用中的 ReactJS

您可以找到许多方法来开始或创建在 internet 上使用 React 的基本应用流,而选择一种方法是相当困难的。在前面的部分中,我们使用非 ASP.NET 技术创建了 web 应用;但是,在本节中,我们将使用带有 React 的 ASP.NET web 应用来构建现代 web 前端。

使用 Visual Studio IDE 创建 ASP.NET 空网站或 MVC 5 web 应用,并在其中安装 ReactJS NuGet 包。它在Scripts文件夹部分创建一个React文件夹。

Scripts文件夹中,转到添加JSX file新文件。

打开PacktContacts.jsx并复制以下代码:

    var ContactsRow = React.createClass({ 
      render: function () { 
        return ( 
          <tr>                 
             <td>{this.props.item.firstName}</td> 
             <td>{this.props.item.lastName}</td> 
             <td>{this.props.item.email}</td> 
             <td><button class='btn btn-primary'>Edit</button>&nbsp;
               <button class='btn btn-danger'>Delete</button></td> 
          </tr> 
        ); 
      } 
    }); 

    var ContactsTable = React.createClass({ 
      getInitialState: function () { 
        return { 
          result: [] 
        } 
      }, 
      componentWillMount: function () { 
        var xhr = new XMLHttpRequest(); 
        xhr.open('get', this.props.url, true); 
        xhr.onload = function () { 
          var response = JSON.parse(xhr.responseText); 
          this.setState({ result: response }); 
        }.bind(this); 
        xhr.send(); 
      }, 
      render: function () { 
        var rows = []; 
        this.state.result.forEach(function (item) { 
          rows.push(<ContactsRow key={item.Id} item={item} />); 
        }); 
        return (<table className="table table-hover"> 
          <thead> 
            <tr>                     
              <th>FirstName</th> 
              <th>LastName</th> 
              <th>Email</th> 
              <td></td> 
            </tr> 
          </thead> 

          <tbody> 
            {rows} 
          </tbody> 
        </table>); 
      } 
    }); 

    ReactDOM.render(<ContactsTable
      url="http://localhost:50461/api/contacts" />, 
      document.getElementById('grid'))} 

创建要与 ReactJS 一起使用的 HTML 文件,以呈现Contacts网格,名称yReactUI.html

    <!DOCTYPE html>
     <html>
       <head>
         <meta charset="utf-8" />
           <title>Consuming Web API using ReactJS</title> 
             <!--CSS--> 
             <link href="Content/bootstrap.min.css" rel="stylesheet" />
             <!-- JS --> 
             <script src="Scripts/jquery-3.1.1.min.js"></script>
             <script src="Scripts/bootstrap.min.js"></script>
             <script src="Scripts/react/react.js"></script>
             <script src="Scripts/react/react-dom.js"></script>
       </head>
       <body class="container">
         <div>
           <h1>Consuming ASP.NET Core Web API using ReactJS</h1>
         </div>
         <div id="grid" class="container">
         </div> 
       </body>
    </html>
    <script src="Scripts/PacktContacts.jsx"></script>

现在,让我们深入了解前面代码的功能:

  • React 的CreateClass呈现一个动态表
  • componentWillMount使用XMLHttpRequest调用 web API
  • 通过传递返回正确响应所需的自定义头,使用 fetch 调用PacktContactsweb API
  • 联系人列表呈现在网格div标记上的 UI 上

F5启动应用;您将看到Contacts列表:

ReactJs fetching the PacktContact web API

使用 JavaScript 使用 web API

JavaScript 是 HTML 和 web 的编程语言。每个 UI 框架都使用 JavaScript。我们不会把重点放在学习 JavaScript 上,但如果您是新手,那么我建议您从 W3Schools 网站学习它 https://www.w3schools.com/js/

在本节中,我们将使用 JavaScript 使用(调用)PacktContacts(ASP.NET Core Web API),并执行身份验证和 CRUD 操作。

创建任何 web 应用(ASP.NET、MVC5 或任何非.NET web 应用);代码包将使用 ASP.NET 空应用。

创建 HTML 文件以复制以下代码以显示联系人列表,并添加联系人:

    <body class="container"> 
      <div > 
        <h1>Consuming ASP.NET Core Web API using JavaScript</h1>        
      </div> 
      <div class="container" id="divlogin" style="display:block;"> 
        <label><b>Username</b></label> 
        <input type="text" placeholder="Enter Username"
          id="uname" required> 
        <label><b>Password</b></label> 
        <input type="password" placeholder="Enter Password"
          id="psw" required> 
        <button onclick="doLogin()" class="btn btn-primary">Login</button>         
      </div>     
      <div id="contactsgrid" style="display:none;"> 
        <button onclick="AddContacts()"
          class="btn btn-success">Add</button> 
        <div id="gridContent">             
        </div> 
      </div> 
      <div id="contactform" style="display:none;"> 
        <div> 
          <div class="form-group"> 
            <label for="usr">First Name:</label> 
            <input type="text" class="form-control" id="firstName"> 
          </div> 
          <div class="form-group"> 
            <label for="usr">Last Name:</label> 
            <input type="text" class="form-control" id="lastName"> 
          </div> 
          <div class="form-group"> 
            <label for="usr">Email:</label> 
            <input type="email" class="form-control" id="emailid"> 
          </div> 
          <button onclick="saveContact()" 
            class="btn btn-primary">Save</button>         
          <button onclick="doCancel()"
            class="btn btn-primary">Cancel</button>         
        </div> 
      </div> 
    </body> 
    </html> 

在运行应用时,我们将看到带有登录页面的 UI,如下所示:

Login UI using JavaScript

根据代码包输入您的凭证(用户名,mithunvp和密码,abcd123;您也可以更改它们)。

使用XMLHttpRequest,我们将调用 web API 方法对Contact模型执行登录和 CRUD 操作。JavaScript 代码如下所示:

    function doLogin() {     
      var contactform = document.getElementById('contactform'); 
      var divlogin = document.getElementById('divlogin'); 
      var contactsgrid = document.getElementById('contactsgrid'); 

      var loginUrl = endpoint + "/api/auth/token"; 
      var xhr = new XMLHttpRequest(); 
      var userElement = document.getElementById('uname'); 
      var passwordElement = document.getElementById('psw');     
      var username = userElement.value; 
      var password = passwordElement.value; 

      xhr.open('POST', loginUrl, true); 
      xhr.setRequestHeader('Content-Type',
         'application/json; charset=UTF-8'); 
      xhr.addEventListener('load', function () { 
        if (this.status == 200) { 
          var responseObject = JSON.parse(this.response); 
          if (responseObject.token) { 
            //console.log(responseObject.token); 
            localStorage.setItem("AuthToken", responseObject.token); 
            getContacts();                 
            divlogin.style.display = 'none'; 
          } 
        } 
        else { 
          bootbox.alert("Authentication Failed", function () {                 
          }); 
        } 
      }); 

      var sendObject = JSON.stringify({ username: username,
        password: password }); 
      xhr.send(sendObject); 
    } 

    function getContacts() { 
      var contactform = document.getElementById('contactform'); 
      var divlogin = document.getElementById('divlogin'); 
      var contactsgrid = document.getElementById('contactsgrid'); 
      var Url = endpoint + "/api/contacts"; 
      var xhr = new XMLHttpRequest();     
      var authtoken = localStorage.getItem("AuthToken"); 
      xhr.open('GET', Url, true); 
      xhr.setRequestHeader("Authorization", "Bearer " + authtoken); 
      xhr.addEventListener('load', function () { 
        var responseObject = JSON.parse(this.response);         
        if (this.status == 200) { 
          var responseObject = JSON.parse(this.response); 
          console.log(responseObject + ' ' + responseObject.length);  
          if (responseObject.length > 0) {                 
             contactsgrid.style.display = 'block'; 
             DisplayContactsGrid(responseObject); 
          } else { 
             bootbox.confirm("No Contacts exists, Click OK to create",
               function (result) {                     
               if (result) { 
                 contactform.style.display = 'block'; 
                 contactsgrid.style.display = 'none'; 
                 clearFormValues(); 
               } 
               else { 
                 contactform.style.display = 'none'; 
                 contactsgrid.style.display = 'block';                         
               } 
             });                 
           }             
         } 
         else { 
           bootbox.alert("Operation Failed", function () { 
           }); 
         } 
      }); 
      xhr.send(null); 
    } 

您可以按如下方式分解前面的代码:

  • doLogin函数使用XMLHttpRequest使用loginUrl调用 web API。认证成功后,我们将auth令牌保存在本地存储器中,以便与授权头一起使用。
  • XMLHttpRequest POST方法采用用户名和密码。然后将其传递给 web API。
  • getContacts()函数从 web API 获取所有联系人,并使用auth令牌传递,以便 web API 将其验证为正确的请求。

运行应用、登录并添加联系人以查看列表:

Contacts list using JavaScript

使用 JQuery 使用 web API

JQuery 是一个 JavaScript 库,它极大地简化了 JavaScript 编程。

要学习 JQuery,请阅读上的文档 https://www.w3schools.com/jquery/

我们将使用与上一节中工作的项目相同的项目。向其中添加 HTML 和 JavaScript 文件。

JQuery 用于使用 web API 的 JavaScript 文件如下所示:

    function doLogin() { 
      var loginUrl = endpoint + "/api/auth/token"; 
      var sendObject = JSON.stringify({ username: $("#uname").val(),
        password: $("#psw").val() }); 

      $.ajax({ 
        url: loginUrl, 
        contentType: 'application/json; charset=UTF-8', 
        data: sendObject, 
        method: "POST" 
      }).done(function (data, status) { 
        if (status == "success") { 
          if (data.token) { 
            localStorage.setItem("AuthToken", data.token); 
            getContacts(); 
            $('#divlogin').hide(); 
          } 
        } 
        else { 
          bootbox.alert("Authentication Failed", function () { 
          }); 
        } 
      }).fail(function () { 
         bootbox.alert("Authentication Error", function () { 
         }); 
      });  
    } 

    function getContacts() { 
      var getUrl = endpoint + "/api/contacts"; 
      var authtoken = localStorage.getItem("AuthToken"); 
      $.ajax({ 
        url: getUrl, 
        contentType: 'application/json; charset=UTF-8',         
        beforeSend: function (xhr) { 
          xhr.setRequestHeader('Authorization', 'BEARER ' + authtoken); 
        }, 
        method: "GET" 
      }).done(function (data, status) { 
        if (status == "success") {             
          if (data.length > 0) { 
            $("#contactsgrid").show(); 
             DisplayContactsGrid(data); 
          } 
          else { 
            bootbox.confirm("No Contacts exists, Click OK to create",
              function (result) { 
              if (result) { 
                $("#contactsgrid").hide(); 
                $("#contactform").show(); 
                clearFormValues(); 
              } 
              else { 
                $("#contactsgrid").show(); 
                $("#gridContent").html(""); 
                $("#contactform").hide(); 
              } 
            }) 
          }} 
        }).fail(function () { 
          bootbox.alert("Authentication Error", function () { 
          }); 
      });    
    } 

您可以按如下方式分解前面的代码:

  • doLogin()函数使用 JQueryAjax GET方法通过传递用户名和密码调用 web API,并将 JWT 令牌保存在本地存储器中
  • getLogin()函数使用相同的 JQueryAjax POST方法获取联系人列表

运行应用并添加联系人后,我们将看到一个类似的 UI,但使用 JQuery 构建:

Contacts list consumed with JQuery

总结

在本章中,我们着重于以 Angular 4、Ionic 3、ReactJS、JavaScript 和 Jquery 的形式构建现代 web 前端,使用在前几章中构建的 web API。对每个 web 前端框架的深入解释是一本书本身。我们更加关注 web API 的使用;但是,源代码将帮助您更好地理解所有概念。

从理解 HTTP 和 REST 的概念,到开始使用 ASP.NET Core Web API 及其剖析,以及详细了解控制器和操作、单元测试 Web API 应用、构建路由和中间件,这是一段美妙的旅程。

我们学习了 ASP.NET Core 如何使用 ORM 和各种数据库集成,然后我们对 web API 进行了优化。我们还研究了异常处理。我们以 JWT、身份和 cookie 身份验证的形式为 web API 应用应用了各种安全措施。

借助 ASP.NET Core 跨平台概念,我们研究了在各种异构环境(如 IIS、Azure App Service、NGINX、Linux 甚至独立应用)上部署 web API。

最后,在本章中,我们在流行的 web 前端(UI 框架)中使用了这些 web API。写这本书是一段令人惊叹的旅程,我希望每个人都能从中受益。

posted @ 2025-10-26 08:52  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报