使用 ASP.NET Core 构建 Web API:1 网页 API 概览

本章涵盖

  • Web API 概述和实际案例示例
  • Web API 的类型及其优缺点
  • ASP.NET 核心概述
  • 主要 ASP.NET 核心架构原则

几乎所有应用程序都需要数据,尤其是基于 Web 的应用程序,其中大量客户端与聚焦式实体(一般是基于 HTTP 的服务)交互,以访问信息并可能更新或操作它们。在本书中,我们将学习如何设计和开发一种特定类型的基于 HTTP 的服务,其唯一目的是向这些客户端提供数据,允许它们以统一、结构化和标准化的方式与所需的信息进行交互:Web API

在本章的第一部分中,我们将了解 Web API 的独特特征,并了解如何将其应用于多个实际场景。在第二部分中,我们将熟悉 ASP.NET Core,我们将在本书中用于创建Web API的Web框架。

1.1 网页接口

应用程序编程接口API) 是一种软件接口,它公开计算机程序用于相互交互和交换信息的工具和服务。执行此类交换所需的连接是通过通用通信标准(协议)、给定的可用操作集(规范)和数据交换格式(JSON、XML 等)建立的。

从这个定义中,我们可以很容易地看到,API 的主要目的是允许各方使用通用语法(或抽象)进行通信,该语法(或抽象)简化和标准化每个引擎盖下发生的事情。整体概念类似于现实世界的接口,后者也提供了一个通用的“语法”,允许不同的各方进行操作。一个完美的例子是插头插座,这是所有国家电气系统使用的抽象概念,允许家用电器和电子设备通过给定的电压、频率和插头类型标准与电源交互。

图1.1显示了形成电网的主要组成部分:一个互连的网络,用于从生产者向住宅消费者发电、传输和输送电力。正如我们所看到的,每个组件都以不同的方式处理电力,并使用各种“协议”和“适配器”(电缆线、变压器等)与其他组件进行通信,最终目标是将其带到人们的家中。当住宅单元连接到电网时,家用电器(电视机、烤箱、冰箱等)可以通过安全、受保护、易于使用的接口(交流电源插头)使用电力。如果我们思考这些插座的工作原理,我们可以很容易地理解它们在多大程度上简化了底层电网的技术方面。家用电器不必知道这样的系统是如何工作的,只要它们能够处理接口。

使用 ASP.NET Core 构建 Web API:1 网页 API 概览

图1.1 接口的常见示例:交流电源插头插座

Web API 与引入万维网的概念一样:可通过 Web 访问的接口,该接口公开一个或多个插头(端点),其他各方(客户端)可以使用这些接口通过通用通信协议 (HTTP) 和标准(JSON/XML 格式)与电源(数据)进行交互。

注意在本书中,术语Web API将互换使用,以表明接口和实际的Web应用程序。

1.1.1 概述

图 1.2 说明了典型面向服务的体系结构 (SOA) 环境中 Web API 的用途。SOA 是一种围绕通过网络进行通信的各种独立服务的责任分离而构建的架构风格。

使用 ASP.NET Core 构建 Web API:1 网页 API 概览

图 1.2 Web API 在典型的基于 SOA 的环境中的角色

如我们所见,Web API 起着关键作用,由于它负责从底层数据库管理系统 (DBMS) 中检索数据并使其可用于服务:

  • Web 应用程序 1(例如 React 信息网站),它从 Web API 获取数据,通过 HTML 页面和组件向最终用户显示数据
  • 移动应用 1(例如 Android 应用)和移动应用 2(可以是同一应用的 iOS 端口),它们还会提取数据并通过其本机用户界面 (UI) 向最终用户显示数据
  • Web 应用程序 2(例如 PHP 管理网站),它访问 Web API 以允许管理员与数据交互并可能修改数据
  • 云服务 1(例如数据仓库),它定期从 Web API 中提取数据以馈送其内部存储库(例如,保留一些访问日志)
  • 云服务 2(例如机器学习软件),它定期从 Web API 检索一些测试数据,并使用它来执行预测并提供有用的见解

我们刚刚描述的是一个有趣的场景,可以协助我们理解 Web API 在一个相当常见的基于服务的生态系统中的角色。但我们仍在讨论一种非个人的理论方法。让我们看看如何使一样的体系结构适应特定的现实场景。

1.1.2 真实世界的例子

在本节中,我们将图 1.1 中描述的抽象概念实例化为具体、可信和现实的场景。每个人都知道什么是棋盘游戏,对吧?我们谈论的是带有骰子、纸牌、棋子和类似东西的桌面游戏。假设我们在棋盘游戏俱乐部的 IT 部门工作。俱乐部有一个棋盘游戏数据库,其中包括游戏信息,例如姓名、出版年份、最小最大玩家、游戏时间、最低年龄、复杂性和机制,以及俱乐部成员、客人和其他玩家随时间给出的一些排名统计数据(评级数量和平均评级)。我们还假设俱乐部希望使用此数据库来提供一些基于 Web 的服务和应用程序,例如:

  • 最终用户网站 – 每个人都可以访问,以展示棋盘游戏及其评级,并为注册用户提供一些附加功能(例如创建列表的功能)
  • 移动应用程序 – 也可公开访问,具有与网站一样的功能,以及适用于智能手机和平板电脑的优化 UI/UX(用户体验)界面
  • 管理门户 – 可用于受限制的授权用户列表,允许这些用户在数据库中执行创建、读取、更新和删除 (CRUD) 操作,并执行其他基于管理的任务
  • 数据分析服务 – 托管在第三方平台上

正如我们很容易猜到的那样,满足这些要求的一个好方法是实现一个专用的 Web API。这种方法允许我们向所有这些服务提供,而无需将数据库服务器及其底层数据模型暴露给其中任何一个。图 1.3 显示了体系结构。

使用 ASP.NET Core 构建 Web API:1 网页 API 概览

图 1.3 由单个 MyBGList Web API 提供的多个与棋盘游戏相关的 Web 应用程序和服务

同样,Web API 是组织者,获取包含所有相关数据的数据源(MyBGList DBMS)并将其提供给其他服务:

  • MyBGList网站 – 完全托管在内容交付网络上的ReactJS网站(遵循Jamstack方法),用户可以在其中浏览棋盘游戏目录并将游戏添加到一组预定义的列表(拥有,想要尝试,想要购买等),以及添加自己的自定义列表
  • MyBGList移动应用程序 – 适用于Android和iOS的React Native应用程序,允许用户执行与MyBGList网站上一样的操作(浏览并添加到列表)
  • MyBGList 管理门户 – 托管在安全私有云中的专用虚拟机 (VM) 服务器上的 ASP.NET Web 应用程序,系统管理员可以访问该应用程序以添加、更新和删除棋盘游戏,以及执行基于维护的任务
  • MyBGList 见解 — 一种软件即服务 (SaaS) 服务,可定期从 Web API 中提取数据,以馈送其内部存储库并执行日志记录、监控、性能分析和商业智能任务

此 Web API 是我们将在以下章节中使用的 API。

什么是 Jamstack?

Jamstack(JavaScript,API和Markup)是一种现代架构模式,基于将网站预呈现为静态HTML页面并使用JavaScript和Web API加载内容。有关 Jamstack 方法的更多信息,请参阅 https://jamstack.org。有关使用 Jamstack 方法开发基于标准的静态网站的综合指南,请查看 Raymond Camden 和 Brian Rinaldi (
https://www.manning.com/books/the-jamstack-book) 的
The Jamstack Book: Beyond Static Sites with JavaScript, API and Markup

1.1.3 网页接口的类型

目前我们已经了解了大致情况,我们可以花一些时间探索当今 Web API 开发人员可用的各种体系结构和消息传递协议。正如我们将能够看到的,每种类型都有特点、优缺点,使其适用于不同的应用程序和业务。

你在这本书中找不到的东西

出于篇幅的缘由,我有意将本书的主题限制在HTTP Web API,跳过其他应用层协议,如高级消息队列协议(AMQP)。如果您有兴趣通过AMQP了解有关基于消息的应用程序的更多信息,我提议您阅读Gavin M. Roy(
https://www.manning.com/books/rabbitmq-in-depth)的
RabbitMQ in Depth

在查看各种体系结构之前,让我们简要总结每个 Web API 一般属于的四个主要使用范围:

  • 公共 API – 公共 API 也称为开放 API(不要与 OpenAPI 规范混淆)。顾名思义,该术语是指可供任何第三方使用的 API,一般没有访问限制。这些 API 一般不涉及身份验证和授权(如果它们可以免费使用),或者如果出于各种缘由(例如应用每次调用成本)需要识别调用方,则它们采用密钥或令牌身份验证机制。此外,由于它们的端点一般可以从万维网访问,因此公共 API 一般使用各种限制、队列和安全技术来避免被大量并发请求以及拒绝服务 (DoS) 攻击所削弱。
  • 合作伙伴 API – 属于此类别的 API 仅供专门选择和授权的合作伙伴使用,例如外部开发人员、系统集成商公司、列入白名单的外部互联网协议 (IP) 等。合作伙伴 API 主要用于促进企业对企业 (B2B) 活动。例如,一个电子商务网站希望与需要为其自己的客户关系管理 (CRM) 和营销自动化系统提供服务的第三方业务合作伙伴共享其客户数据。这些 API 一般实现强身份验证机制和 IP 限制技术,以防止未经授权的用户访问它们;它们绝对不适合最终用户或“公共”客户端(如标准网站)使用。
  • 内部 API – 也称为私有 API,这些 API 仅供内部使用,例如连接同一组织拥有的不同服务,并且一般托管在同一虚拟专用网络、Web 场或私有云中。例如,内部企业资源规划软件可以使用内部 API 从各种内部业务源(工资单、资产管理、项目管理、采购等)检索数据,以及创建高级报告和数据可视化。由于这些 API 不向外部公开,因此它们一般实施温和的身份验证技术,具体取决于组织的内部安全模型及其性能和负载平衡调整。
  • 复合 API — 有时称为 API 网关,这些 API 组合多个 API,通过单个调用执行一系列相关或相互依赖的操作。简而言之,它们允许开发人员同时访问多个端点。这种方法主要用于微服务架构模式,其中执行复杂任务可能需要以同步或异步方式完成由多个服务处理的子任务链。复合 API 主要充当 API 业务流程协调程序,确保执行主调用所需的各种子任务成功并能够返回有效结果(如果不是,则使整个过程无效)。常见的使用场景可确保多个第三方服务完成各自的工作,例如,当我们需要永久删除内部数据库多个外部数据源中的用户记录(及其所有个人信息)时,而不会产生原子性问题,例如将潜在的敏感数据留在某处。由于编排调用可以是多种类型(公共、合作伙伴和/或内部),因此复合 API 一般最终是混合的,这就是为什么它们一般被视为代表不同的 API 类型。

1.1.4 架构和消息协议

为了履行其在各方之间实现数据交换的角色,Web API 需要定义一组清晰、明确的规则、约束和格式。本书介绍了四种最常用的Web API架构风格和消息协议:REST、RPC、SOAP 和 GraphQL。每种方法都有独特的特征、权衡和支持的数据交换格式,使其可用于不同的目的。

休憩

具象状态传输REST) 是一种体系结构样式,专为基于网络的应用程序设计,这些应用程序使用标准 HTTP GET、POST、PATCH、PUT 和 DELETE 请求方法(最初在现已取代的 RFC 2616
https://www.w3.org/Protocols/rfc2616/rfc2616.xhtml 中定义)来访问和操作数据。更具体地说,GET 用于读取数据,POST 用于创建新资源或执行状态更改,PATCH 用于更新现有资源,PUT 用于用新资源替换现有资源(如果不存在则创建它),DELETE 用于永久擦除资源。这种办法不需要额外的公约来允许当事人进行通信,这使得它易于采用和快速实施。

不过,REST远不止于此。它的体系结构范例依赖于六个指导约束,如果正的确 现,可以极大地使多个 Web API 属性受益:

  • 客户端-服务器方法 – RESTful API 应通过将 UI 和数据存储问题分开来强制实施关注点分离原则。此方法特别适用于万维网,其中客户端(如浏览器)具有与 Web 应用程序不同的角色,并且不知道它们是如何实现的。重大的是要清楚,分离这些问题不仅可以提高它们的可移植性,还可以使它们更易于实现和维护,从而提高整个系统的简单性、可伸缩性和可修改性
  • 无状态 – 服务器应处理客户端之间的所有通信,而不保留、存储或保留以前调用的数据(如会话信息)。此外,客户端应在每次调用中包含任何与上下文相关的信息,例如身份验证密钥。此方法可减少服务器上每个请求的开销,这可以显著提高 Web API 的性能可伸缩性属性,尤其是在负载较重的情况下。
  • 可缓存性 – 客户端和服务器之间交换的数据应使用 HTTP 协议提供的缓存功能。更具体地说,所有 HTTP 响应都必须在其标头中包含适当的缓存(或非缓存)信息,以优化客户端工作负载,同时防止它们错误地提供过时或过时的内容。良好的缓存策略会对大多数 Web API 的可伸缩性和性能属性产生巨大影响。
  • 分层系统 – 将服务器置于一个或多个中间 HTTP 服务或筛选器(如转发器、反向代理和负载均衡器)后面,可以极大地改善 Web API 的整体安全性方面,以及其性能和可伸缩性属性。
  • 按需代码COD) – COD 是唯一可选的 REST 约束。它允许服务器提供客户端可用于采用自定义行为的可执行代码或脚本。COD 的示例包括分布式计算和远程评估技术,其中服务器将其部分作业委托给客户端,或者需要它们使用复杂或自定义任务在本地执行某些检查,例如验证是否安装了某些应用程序或驱动程序。从更广泛的意义上讲,这种约束还指从服务器获取构建和加载应用程序所需的源代码的能力(在REST的早期很少见,但在最近的JavaScript驱动的Web应用程序中很常见)以及进一步的REST调用问题。COD 可以提高 Web API 的性能和可伸缩性,但同时,它会降低整体可见性并带来不平凡的安全风险,这就是它被标记为可选的缘由。
  • 统一接口 – 最后一个 REST 约束是最重大的一个。它定义了 RESTful 接口必须具备的四个基本功能,以使客户端能够在服务器不知道它们如何工作的情况下与服务器通信,从而使它们与 Web API 的基础实现分离。这些功能是
    • 资源标识 – 每个资源必须通过其专用的唯一通用资源标识符 (URI) 统一标识。例如,URI https://mybglist.com/api/games/11 标识单个棋盘游戏。
    • 通过表明操作资源 – 客户端必须能够使用资源 URI 和相应的 HTTP 方法对资源执行基本操作,而无需其他信息。例如,要读取棋盘游戏 11 的数据,客户端应该向 https://mybglist.com/api/games/11 发出 GET 请求。如果客户端想要删除该数据,它应该向同一 URI 发出 DELETE 请求,依此类推。
    • 自描述性邮件 – 每个发件人的邮件必须包含收件人正确理解和处理邮件所需的所有信息。此要求对客户端和服务器有效,并且很容易通过 HTTP 协议实现。请求和响应都旨在包含一组描述协议版本、方法、内容类型、默认语言和其他相关元数据的 HTTP 标头。Web API 和连接客户端只需确保正确设置标头。
    • HATEOAS超媒体作为应用程序状态的引擎)- 服务器应向客户端提供有用的信息,但只能通过超链接和 URI(或 URI 模板)。此要求将服务器与其客户端分离,由于除了对超媒体的一般理解之外,客户端几乎不需要知道如何与服务器交互。此外,服务器功能可以独立发展,而不会产生向后兼容性问题。

实现所有这些约束的 Web API 可以描述为 RESTful

介绍 Roy Fielding,REST 无可争议的父亲

Roy Fielding描述了六个REST约束,他是HTTP规范的主要作者之一,被广泛认为是REST之父,在他的论文Architectural Styles and the Design of Network-based Software Architectures中,他在2000年获得了计算机科学博士学位。菲尔丁论文的原始文本可在 http://mng.bz/19ln 的加利福尼亚大学出版物档案中找到。

广泛接受的规则和指南,HTTP协议经过验证的可靠性以及实现和开发的整体简单性使REST成为当今最流行的Web API架构。出于这个缘由,我们将在本书中开发的大多数 Web API 项目都使用 REST 架构风格,并遵循 RESTful 方法以及本节中介绍的六个约束。

SOAP

简单对象访问协议SOAP) 是一种消息传递协议规范,用于通过可扩展标记语言 (XML) 跨网络交换结构化信息。大多数 SOAP Web 服务都是通过使用 HTTP 进行消息协商和传输来实现的,但也可以使用其他应用层协议,例如简单邮件传输协议 (SMTP)。

SOAP规范由Microsoft于1999年24月发布,但直到2003年3月1日才成为Web标准,当时它最终获得了W2C推荐状态(SOAP v3.12,https://www.w<>.org/TR/soap<>)。接受的规范定义了 SOAP 消息传递框架,该框架由以下内容组成:

  • SOAP 处理模型 – 定义处理 SOAP 消息的规则
  • SOAP 扩展性模型 — 介绍 SOAP 功能和 SOAP 模块
  • SOAP 基础协议绑定框架 – 描述使用基础协议创建绑定的规则,该协议可用于在 SOAP 节点之间交换 SOAP 消息

SOAP 的主要优点是其可扩展性模型,它允许开发人员使用其他官方消息级标准(WS-Policy、WS-Security、WS-Federation 等)扩展基本协议,以执行特定任务,以及创建自己的扩展。生成的 Web 服务可以用 Web 服务描述语言 (WSDL) 进行记录,这是一种描述各种端点支持的操作和消息的 XML 文件。

但是,此方法也是一个缺陷,由于用于发出请求和接收响应的 XML 可能会变得极其复杂,并且难以阅读、理解和维护。多年来,许多开发框架和 IDE 都缓解了此问题,包括 ASP.NET 和 Visual Studio,它们开始提供快捷方式和抽象来简化开发体验并自动生成所需的 XML 代码。即便如此,与REST相比,该协议也有几个缺点:

  • 更少的数据格式 – SOAP 仅支持 XML,而 REST 支持更多种类的格式,包括 JavaScript 对象表明法 (JSON),它提供更快的解析速度并且绝对更易于使用,以及逗号分隔值 (CSV),对于带宽优化是主要关注点的大型数据集来说,这是一种很好的 JSON 替代方案。
  • 更差的客户端支持 – 现代浏览器和前端框架都经过优化,可以使用 REST Web 服务,这一般提供更好的兼容性。
  • 性能和缓存问题 — SOAP 消息一般通过 HTTP POST 请求发送。由于HTTP POST方法是非幂等的,因此它不会在HTTP级别缓存,这使得其请求比RESTful对应项的请求更难缓存。
  • 更慢、更难实现 — SOAP Web 服务一般更难开发,尤其是在必须与现有网站或服务集成时。相反,REST 一般可以作为现有代码的插入功能实现,而无需重构整个客户端-服务器基础结构。

由于所有这些缘由,今天在REST与SOAP的辩论中,普遍的共识是,除非有特定的缘由使用SOAP,否则REST Web API是首选的方式。这些缘由可能包括硬件、软件或基础结构的限制,以及现有实现的要求,这就是为什么我们不会在本书中使用 SOAP 的缘由。

GraphQL

2015年,随着GraphQL的公开发布,REST的至高无上地位受到质疑,GraphQL是Facebook开发的API的开源数据查询和操作语言。这两种方法之间的主要区别不在于体系结构样式,而在于它们发送和检索数据的不同方式。

实际上,GraphQL 遵循大多数 RESTful 约束,依赖于一样的应用层协议 (HTTP),并采用一样的数据格式 (JSON)。但是,它不是使用不同的端点来获取不同的数据对象,而是允许客户端执行动态查询并询问单个端点的具体数据要求。

我将尝试使用从棋盘游戏 Web API 场景中获取的实际示例来解释此概念。假设我们要检索对 Citadels 棋盘游戏给予积极反馈的所有用户的名称和唯一 ID。当我们处理典型的 REST API 时,完成这样的任务需要以下操作:

  1. 对全文搜索终结点的 Web API 请求 – 检索名称等于“Citadels”的棋盘游戏列表。为了简单起见,让我们假设这样的调用返回单个结果,允许我们获取目标棋盘游戏的信息,包括它的唯一 ID,这是我们正在寻找的。
  2. 向反馈终结点发出的另一个 web API 请求 – 检索从该用户的唯一 ID 收到的所有反馈。同样,让我们假设反馈范围从 1(“我经历过的最糟糕的游戏体验”)到 10(“有史以来最好的游戏”)。
  3. 客户端迭代例如 foreach 循环)- 循环遍历所有检索到的反馈并检索分级等于或大于 6 的用户 ID。
  4. 向用户终端节点发出的第三个 Web API 请求 – 检索与这些唯一 ID 对应的用户。

完整的请求/响应周期如图1.4所示。

使用 ASP.NET Core 构建 Web API:1 网页 API 概览

图 1.4 REST 中的 HTTP 请求-响应周期

该计划是可行的,但不可否认的是,它涉及大量工作。具体来说,我们必须执行多次往返(三个HTTP请求)和大量的过度获取 – Citadels游戏信息,所有反馈的数据以及所有给予正面评价的用户的数据 – 以获得一些名字。唯一的解决方法是实现额外的 API 端点以返回我们需要的内容或简化一些中间工作。我们可以添加一个端点来获取给定棋盘游戏 ID 甚至给定棋盘游戏名称的正面反馈,包括基础实现中的全文查询。如果我们愿意,我们甚至可以实现一个专用的端点,例如
api/positiveFeedbacksByName,来执行整个任务。

不过,不可否认的是,这种方法会影响后端开发时间,并增加我们 API 的复杂性,并且不够通用,无法在类似的、不一样的情况下提供协助。如果我们想检索负面反馈或多个游戏或给定作者创建的所有游戏的正面反馈怎么办?正如我们很容易理解的那样,克服这些问题可能并不简单,特别是当我们在客户端数据获取要求方面需要高水平的多功能性时。

目前让我们看看 GraphQL 会发生什么。当我们采用这种方法时,我们不是从现有(或附加)端点的角度来思考,而是专注于我们需要发送到服务器的查询以请求我们需要的内容,就像我们对 DBMS 所做的那样。完成后,我们将该查询发送到(单个)GraphQL 端点,并在单个 HTTP 调用中准确地返回我们请求的数据,而无需获取任何我们不需要的字段。图 1.5 显示了 GraphQL 客户端到服务器的往返行程。

使用 ASP.NET Core 构建 Web API:1 网页 API 概览

图 1.5 GraphQL 中的 HTTP 请求-响应周期

正如我们所看到的,性能优化不仅限于 Web API。GraphQL 方法不需要客户端迭代,由于服务器已经返回了我们正在寻找的准确结果。

GraphQL 规范可在 https://spec.graphql.org 上找到。我将在第 10 章中广泛讨论 GraphQL,向您展示如何将其与 REST 一起使用以实现特定的面向查询的目标。

1.2 ASP.NET Core

目前我们已经了解了什么是 Web API,以及如何使用它们在网络中的 Web 应用程序和服务中交换数据,目前是时候介绍我们将在本书中创建它们的框架了。该框架 ASP.NET Core,这是一个高性能,跨平台,开源Web开发框架,由Microsoft于2016年推出,作为 ASP.NET 的继任者。

在以下各节中,我将简要介绍其最独特的方面:整体体系结构、请求/响应管道管理、异步编程模式、路由系统等。

注意在本书中,我们将使用 .NET 6.0 和 ASP.NET Core Runtime 6.0.11,这是本文撰写时最新的正式发布版本,也是继 .NET Core 1.0、1.1、2.0、2.1、2.2、3.0、3.1 和 .NET 5.0 之后发布的第九部分。每个版本都引入了一些更新、改善和其他功能,但为了简单起见,我将回顾最新版本附带的结果特征。此外,从 .NET 5 开始,所有偶数 .NET 版本(包括 .NET 6)都授予长期支持 (LTS) 状态,这意味着它们将在未来许多年内得到支持,而不是具有较短时间范围的奇数版本。目前,偶数版本的支持期为三年,奇数版本的支持期为 18 个月 (http://mng.bz/JVpa)。

1.2.1 体系结构

我选择将 ASP.NET Core 用于我们的 Web API 项目,由于新的 Microsoft 框架强制执行了几个现代架构原则和最佳实践,使我们能够构建轻量级、高度模块化的应用程序,这些应用程序具有高水平的可测试性和源代码可维护性。这种方法自然会指导开发人员构建(或采用)由离散和可重用组件组成的应用程序,这些组件执行特定任务并通过框架提供的一系列接口进行通信。这些组件称为服务和中间件,它们在 Web 应用程序的 Program.cs 文件中注册和配置,该文件是应用的入口点。

注意如果您来自较旧的 ASP.NET Core 版本(如 3.1 或 5.0),您可能想知道 Startup 类(及其相应的 Startup.cs 文件)发生了什么变化,该文件用于包含服务和中间件配置设置。此类已从 .NET 6 引入的最小宿主模型中删除,该模型将程序类和启动类合并到单个 Program.cs 文件中。

服务业

服务是应用程序提供其功能所需的组件。我们可以将它们视为应用依赖项,由于我们的应用依赖于它们的可用性才能按预期工作。我在这里使用术语依赖关系是有缘由的:ASP.NET Core 支持依赖注入 (DI) 软件设计模式,这是一种架构技术,允许我们在类与其依赖关系之间实现控制反转。以下是整个服务实现、注册/配置和注入过程在 ASP.NET Core 中的工作方式:

  • 实现 – 每个服务都通过专用接口(或基类)来实现,以抽象实现。接口和实现都可以由框架提供、由开发人员创建或从第三方(GitHub、NuGet 包等)获取。
  • 注册和配置 – 应用程序使用的所有服务都在内置的 IServiceProvider 类(服务容器)中进行配置和注册(使用其接口)。实际的注册和配置过程发生在 Program.cs 文件中,开发人员还可以在其中选择合适的生存期(瞬态、作用域或单一实例)。
  • 依赖关系注入 – 可以将每个服务注入到要使用它的类的构造函数中。框架通过 IServiceProvider 容器类自动提供依赖项的实例,创建新的实例或可能重用现有实例,具体取决于配置的服务生存期,并在不再需要时释放它。

框架提供的服务接口的典型示例包括可用于实现基于策略的权限的 IAuthorizationService,以及可用于发送电子邮件的 IEmailService。

中间件

中间件是一组在 HTTP 级别运行的组件,可用于处理整个 HTTP 请求处理管道。如果您还记得 ASP.NET 在 ASP.NET Core 出现之前使用的 HttpModules 和 HttpHandler,您可以很容易地看到中间件如何扮演类似的角色并执行一样的任务。ASP.NET 核心 Web 应用程序中使用的中间件的典型示例包括
HttpsRedirectionMiddleware,它将非 HTTPS 请求重定向到 HTTPS URL,以及 AuthorizationMiddleware,它在内部使用授权服务来处理 HTTP 级别的所有授权任务。

每种类型的中间件都可以将 HTTP 请求传递给管道中的下一个组件或提供 HTTP 响应,从而缩短管道本身并阻止其他中间件处理请求。这种类型的请求阻止中间件称为终端中间件,一般负责处理各种应用端点的主要业务逻辑任务。

警告值得注意的是,中间件是按注册顺序(先进先出)处理的。始终在程序.cs文件中的非终端中间件之后添加终端中间件;否则,该文件将不起作用。

终端中间件的一个很好的例子是 StaticFileMiddleware,它最终处理指向静态文件的端点 URL,只要它们是可访问的。发生这种情况时,它会使用适当的 HTTP 响应将请求的文件发送给调用方,从而终止请求管道。我之前简要提到的
HttpsRedirectionMiddleware 也是终端中间件,由于它最终会响应所有非 HTTPS 请求的 HTTP-to-HTTPS 重定向。

注意StaticFileMiddleware 和
HttpsRedirectionMiddleware 只有在满足某些情况时才会终止 HTTP 请求。如果请求的终结点与其激活规则不匹配(例如,对于前者,指向不存在的静态文件的 URL,或者对于后者,指向已在 HTTPS 中),则会将其传递给管道中存在的下一个中间件,而无需执行任何操作。出于这个缘由,我们可以说它们可能是终端中间件,以区别于总是在 HTTP 请求到达请求管道时结束请求管道的中间件。

1.2.2 Program.cs

目前我已经介绍了服务和中间件,我们终于可以看一下在应用程序开始时执行的 Program.cs 文件:

var builder = WebApplication.CreateBuilder(args);    ❶
 
// Add services to the container.
 
builder.Services.AddControllers();                   ❷
// Learn more about configuring Swagger/OpenAPI
// at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();          ❷
builder.Services.AddSwaggerGen();                    ❷
 
var app = builder.Build();                           ❸
 
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();                                ❹
    app.UseSwaggerUI();                              ❹
}
 
app.UseHttpsRedirection();                           ❹
 
app.UseAuthorization();                              ❹
 
app.MapControllers();                                ❹
 
app.Run();                                           ❺

创建 WebApplicationBuilder 工厂类

注册和配置服务

构建 Web 应用程序对象

注册和配置非终端和潜在的终端中间件

注册和配置终端中间件

通过分析这段代码,我们可以很容易地看到该文件负责以下初始化任务:

  • 实例化 Web 应用程序
  • 注册和配置服务
  • 注册和配置中间件

更准确地说,Web 应用程序由 WebApplicationBuilder 工厂类实例化,从而创建一个 WebApplication 对象。此实例存储在应用局部变量中,用于注册和配置所需的服务和中间件。

注意服务和中间件都通过专用的扩展方法注册和配置,这是一种简化设置过程并保持程序.cs文件尽可能简洁的便捷方法。在 ASP.NET 核心命名约定下,服务主要通过使用带有 Add 前缀的扩展方法进行配置;中间件前缀为“使用”、“映射”和“运行”。至少目前,我们关心的唯一区别是 Run 委托始终是 100% 终端和最后一个要处理的。稍后,我们将在试验最小 API 时详细讨论这些约定及其含义。

为了简单起见,我将中间件分为两类:潜在终端和终端。在这一点上,区别应该很明显。潜在的终端中间件只有在与某些规则匹配时才结束HTTP请求管道,而终端中间件总是这样做(所以难怪它是最后一个)。

1.2.3 控制器

在 ASP.NET Core 中,控制器是一个类,用于对一组处理类似 HTTP 请求的操作方法(也称为操作)进行分组。从这个角度来看,我们可以说控制器是可用于聚合具有共同点的操作方法的容器:路由规则和前缀、服务、实例、授权要求、缓存策略、HTTP 级过滤器等。这些常见要求可以直接在控制器类上定义,从而避免了为每个操作指定这些要求的需要。此方法由一些强劲的内置命名和编码约定强制执行,有助于保持代码干燥并简化应用的体系结构。

注意 DRYDon't Repeat Yourself 的首字母缩写词,这是一个众所周知的软件开发原则,可协助我们记住避免冗余、模式重复以及可能导致代码异味的任何其他内容。它与 WET 相反(将所有内容写入两次每次写入)。

从 ASP.NET Core 开始,控制器可以从两个内置基类继承:

  • ControllerBase,不支持视图的最小实现
  • 控制器,一个更强劲的实现,它继承自 ControllerBase 并添加了对视图的完全支持

正如我们很容易理解的那样,控制器基类旨在用于采用模型-视图-控制器 (MVC) 模式的 Web 应用程序,其中它们旨在返回来自业务逻辑(一般由依赖注入的服务处理)的数据。在典型的 ASP.NET Core Web 应用程序中,生成的数据通过视图返回到客户端,这些视图使用客户端标记和脚本语言(如 HTML、CSS、JavaScript 等)处理应用程序的数据表明层(或服务器端呈现的语法,如 Razor)。在处理 Web API 时,我们一般不使用视图,由于我们希望直接从控制器返回 JSON 或 XML 数据(无 HTML)。在这种情况下,从 ControllerBase 基类继承是在特定情况下的推荐方法。下面是处理两种类型的 HTTP 请求的示例 Web API 控制器:

  • 对 /api/Sample/ 的 GET 请求以接收项目列表
  • 对 /api/Sample/{id} 的 GET 请求,用于接收具有指定 {id} 的单个项目,假设它是一个充当主键的唯一整数值
  • 对 /api/Sample/{id} 的 DELETE 请求,以删除具有指定 {id} 的单个项
[ApiController]                                ❶
[Route("api/[controller]")]                    ❷
public class SampleController : ControllerBase
{
    public SampleController()
    {
    }
 
    [HttpGet]                                  ❸
    public string Get()
    {
        return "TODO: return all items";
    }
 
    [HttpGet("{id}")]                          ❹
    public string Get(int id)
    {
        return $"TODO: return the item with id #{id}";
    }
 
    [HttpDelete("{id}")]                       ❺
    public string Delete(int id)
    {
        return $"TODO: delete the item with id #{id}";
    }
}

添加特定于 API 的行为

默认路由规则

处理 HTTP GET 到 /api/Sample/ 的操作

处理 HTTP GET 到 /api/Sample/{id} 的操作

处理 HTTP DELETE 到 /api/Sample/{id} 的操作

正如我们通过查看此代码所看到的,通过利用一些有用的内置约定,我们能够用几行代码完成给定的任务:

  • 一个聚焦式的 /api/Sample/ 路由前缀,对所有操作方法都有效,这要归功于在控制器级别应用的 [Route(“api/[controller]”)] 属性
  • 所有已实现的 HTTP 谓词(包括必需参数)的自动路由规则,这要归功于应用于相应操作方法的 [HttpGet] 和 [HttpDelete] 属性
  • 使用控制器中间件的默认规则进行自动路由映射,由于我们将控制器后缀应用于类名,只要应用。MapControllers() 扩展方法存在于程序.cs文件中

注意此外,由于我们使用了 [ApiController] 属性,因此我们还需要一些特定于 Web API 的其他约定,这些约定会自动返回某些 HTTP 状态代码,具体取决于操作类型和结果。我们将在第6章中详细讨论它们,届时我们将深入研究错误处理。

1.2.4 最小接口

具有内置约定的控制器是使用几行代码实现 Web API 业务逻辑的好方法。不过,在 ASP.NET Core 6中,该框架引入了一个最小的范式,允许我们以更少的仪式来构建Web API。这个特性被称为最小API,尽管它很年轻,但它得到了新的和经验丰富的 ASP.NET 开发人员的大量关注,由于它一般允许更小,更易读的代码库。

解释最小 API 的最好方法是在基于控制器的方法和块上的新孩子之间执行快速代码比较。以下是我们使用 SampleController 处理的一样 HTTP 请求如何由最小 API 处理,并对程序.cs文件进行一些小的更新:

// app.MapControllers();                                  ❶
app.MapGet("/api/Sample",                                 ❷
    () => "TODO: return all items");                      ❷
app.MapGet("/api/Sample/{id}",                            ❷
    (int id) => $"TODO: return the item with id #{id}");  ❷
app.MapDelete("/api/Sample/{id}",                         ❷
    (int id) => $"TODO: delete the item with id #{id}");  ❷

删除此行(和 SampleController 类)

改为添加这些行

如我们所见,所有实现都传输到 Program.cs 文件中,而无需单独的控制器类。此外,代码可以进一步简化(或DRYfied),例如通过添加前缀变量,无需多次重复“/api/Sample”。在代码可读性、简单性和减少开销方面,其优势显而易见。

提示任何最小 API 方法的业务逻辑都可以放在程序.cs文件之外;只需要 Map 方法才能到达那里。实际上,将实际实现移动到其他类几乎总是很好的做法,除非我们处理的是单行代码。

最小 API 范例不太可能取代控制器,但它肯定会简化大多数小型 Web API 项目(如微服务)的编码体验,并吸引大多数寻求时尚方法和浅学习曲线的开发人员。出于所有这些缘由,我们将在本书中常常将其与控制器一起使用。

1.2.5 基于任务的异步模式

影响 Web 应用程序的大多数性能问题都是由于有限数量的线程需要处理可能无限量的并发 HTTP 请求。在响应这些请求之前,这些线程必须执行一些资源密集型(一般是不可缓存的)任务,在最终收到结果之前被阻止时,尤其如此。典型的示例包括通过 SQL 查询从 DBMS 读取或写入数据、访问第三方服务(如外部网站或 Web API)以及执行任何其他需要大量执行时间的任务。

当 Web 应用程序被迫同时处理大量这些请求时,可用线程的数量会迅速减少,从而导致响应时间变慢,并最终导致服务不可用(HTTP 503 等)。

注意这种情况是大多数基于 HTTP 的拒绝服务 (DoS) 攻击的主要目标,在这种攻击中,Web 应用程序被请求淹没,使整个服务器不可用。线程阻塞、非缓存的 HTTP 请求是 DoS 攻击者的金矿,由于他们很有可能迅速耗尽线程池。

处理资源密集型任务的一般经验法则是积极缓存生成的 HTTP 响应,如 REST 约束 3(可缓存性)所述。不过,在某些情况下,缓存不是一种选择,例如当我们需要写入DBMS(HTTP POST)或读取可变或高度可定制的数据(带有大量参数的HTTP GET)时。在这种情况下,我们该怎么办?

ASP.NET Core 框架允许开发人员通过实现 C# 语言级异步模型(一般称为基于任务的异步模式TAP))来有效地处理此问题。了解TAP如何工作的最好方法是看到它的实际效果。请看一下以下源代码:

[HttpGet]
public async Task<string> Get()           ❶
{
    return await Task.Run(() => {         ❷
        return "TODO: return all items";
    });
}

异步

等待

如我们所见,我们对之前用于实现 TAP 的 SampleController 的第一个操作方法应用了一些小的更新。新模式依赖于使用 async 和 await 关键字以非阻塞方式处理任务:

  • async 关键字定义返回任务的方法。
  • await 关键字允许调用线程以非阻塞方式启动新实现的任务。任务完成后,线程继续执行代码,从而返回生成的逗号分隔字符串。

值得注意的是,由于我们在 Get() 操作方法中使用了 await 关键字,所以我们也必须将该方法标记为异步。这完全没问题;这意味着该方法将由调用线程等待而不是阻塞它,这是我们想要的。

我们的最小实目前性能方面没有任何好处。我们绝对不需要等待像字面 TODO 字符串这样微不足道的事情。但是它应该让我们了解当示例 Task.Run 被需要服务器执行一些实际工作(例如从 DBMS 检索实际数据)的内容所取代时,异步/等待模式将为 Web 应用程序带来的好处。我将在第 4 章中广泛讨论异步数据检索任务,其中介绍了 Microsoft 最流行的 .NET 数据访问技术:实体框架核心。

总结

  • Web API 是基于 HTTP 的服务,任何软件应用程序都可以使用它来访问和可能操作数据。
  • Web API 旨在通过使用通用通信标准(协议)、给定的可用操作集(规范)和数据交换格式(JSON、XML 等)来工作。
  • Web API 一般分为四个可能的使用范围:
    • 公开开放 – 当任何人拥有(或可以获得)访问权限时
    • 合作伙伴 – 当访问权限仅限于业务伙伴时
    • 内部专用 – 当访问权限仅限于组织的网络时
    • 复合 – 当他们编排多个公共、合作伙伴和/或内部 API 调用时
  • 为了履行其职责,Web API 需要一组统一的规则、约束和格式,具体取决于所选的体系结构样式或消息传递协议。目前最常用的标准是 REST、SOAP、gRPC 和 GraphQL:
    • 每种方法都有独特的特征、权衡和支持的格式。
    • 本书主要关注 REST,但第 10 章专门介绍 GraphQL。
  • ASP.NET Core 是一个高性能、跨平台、开源的 Web 开发框架,我们将在本书中使用它来设计和开发 Web API:
    • ASP.NET Core 的现代架构原则允许开发人员构建具有高度可测试性和可维护性的轻量级、高度模块化的应用程序。
  • ASP.NET Core 体系结构允许开发人员通过使用离散和可重用的组件来执行特定任务来自定义应用的功能和工作负载。这些组件分为两个主要类别,这两个类别都在应用的入口点(程序.cs文件中注册和配置:
    • 服务 – 需要提供功能并通过依赖关系注入进行实例化
    • 中间件 — 负责处理 HTTP 请求管道
  • ASP.NET Core 支持两种构建 Web API 的范式:控制器,它提供了极大的多功能性和一整套受支持的功能,以及最小的 API,这是一种简化的方法,允许我们编写更少的代码,同时减少开销:
    • .NET 6 中引入的最小 API 方法可能是以省时的方式构建简单 Web API 的好方法,超级适合正在寻找时尚、现代编程方法的开发人员。
  • ASP.NET Core Web 应用可以通过使用 TAP 来克服由于资源密集型线程阻塞调用而导致的大多数性能问题,TAP 是一种异步编程模型,允许主线程以非阻塞方式启动任务并在任务完成后恢复执行。
© 版权声明

相关文章

暂无评论

none
暂无评论...