C--和--NET-API-编程-全-
C# 和 .NET API 编程(全)
原文:
zh.annas-archive.org/md5/8a4b0a65f03719c969837d3cfd7b579b译者:飞龙
前言
构建一个优秀的应用程序程序接口(API)对于创建显示来自数据源(通常是数据库)数据的实际应用至关重要。正如你在本书中看到的,API 的工作是在大规模上分离关注点;具体来说,是将前端和后端的关注点分离。这允许你更改一个(例如,更换新的数据库)而不会破坏另一个(例如,网站)或反之亦然。
一个典型的企业可能有多个团队在同一个整体产品上工作。例如,你可能有一个团队在数据的前端展示上工作,另一个在 iOS 版本上工作,第三个在 Android 上工作。
在后端,你可能会有多个数据源。在本书中我们使用的简单示例中,我们考察了一个汽车经销商。它可能来自销售,也可能来自库存,来自提供平均价格信息的服务,等等。其中一些是静态数据,可以轻松存储在数据库中,一些必须存储在频繁更新的缓存中,一些必须按需获取。
协调前端与后端是困难的,如果后端的格式或计算发生变化,可能会出现灾难性的故障。此外,展示层的需求几乎肯定会随着时间的推移而变化。最后,前端通常不是放置业务逻辑的理想位置。
APIs 解决了这些问题。前端与定义良好的端点进行通信,后端以定义良好的数据响应。后端如何获取和处理这些数据对前端来说是不可见的。就这一点而言,前端对数据的用途对后端来说也是不可见的。最重要的是,API 本身不需要了解任何一方;它只知道需要什么以及如何获取。
本书面向对象
本书面向至少具备 C#基本知识的程序员,旨在帮助他们创建世界级的 API,通常用于企业级应用。虽然本书不假设读者有 API 方面的经验,但具备 SQL 的基本了解将有所帮助。
本书涵盖内容
第一章 ,入门指南,提供了快速入门指南,以在本地设置开发环境。
第二章 ,我们将构建什么,概述了 API 的一般概念,以及如何使用它来解耦前端和后端系统。
第三章 ,使用 REST 实现,概述了行业通用的 API 开发最佳实践和有见地的设计。
第四章 ,使用 Swagger 进行文档化,展示了如何启用并展示基于 Swagger 的文档。
第五章 ,数据验证 ,概述了如何验证 API 调用,包括使用广泛使用的库进行自定义验证。
第六章 ,Azure Functions ,提供了一个以云为先的托管框架,不仅可以用作 API,还可以作为更多应用的起点。它还介绍了最佳实践,并在此基础上构建,以允许在无需重新部署的情况下进行运行时配置。
第七章 ,Azure Durable Functions ,概述了在遵循几个小设计规则的情况下,在具有状态和可扩展性的系统中实现简化的结果。
第八章 ,高级主题 ,提供了一个简单、经济高效的云日志实现。此外,它还介绍了高级场景,如复杂对象映射、有观点的云为先设计工具以及存储表的创建和使用。
第九章 ,身份验证和授权 ,为云为先的身份验证场景提供了一个即插即用的解决方案,包括授权 Azure 和非 Azure 客户端。
第十章 ,部署到 Azure ,让您快速设置以迭代持续交付和持续集成(CI/CD)管道。
第十一章 ,接下来是什么? ,为您提供了关于经典问题的实用建议:现在怎么办?
要充分利用本书
您至少需要了解 C#的基础知识。了解 SQL 有帮助,但不是必需的。使用 Git 和通用仓库将使您的生活更加轻松。我们假设没有其他技术专长。
| 本书涵盖的软件/硬件(除上述内容外无需额外知识) | 操作系统/其他要求 |
|---|---|
| PC | Windows |
| 中级 C# | Windows |
| Swagger | Windows |
| 基础 DevOps | Azure |
| AutoMapper | Windows |
| SQL Server 或等效产品 | Windows |
| Git | GitHub |
所有必要的设置都会在我们进行时进行解释。
如果您正在使用这本书的数字版,我们建议您亲自输入代码或从书的 GitHub 仓库(下一节中有一个链接)获取代码。这样做可以帮助您避免与代码的复制和粘贴相关的任何潜在错误。
亲自编写代码已被证明是学习新材料的更有效方法。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件:github.com/PacktPublishing/Programming-APIs-with-C-Sharp-and-.NET 。如果代码有更新,它将在 GitHub 仓库中更新。
我们还有来自我们丰富的书籍和视频目录的其他代码包,可在github.com/PacktPublishing/找到。查看它们吧!
使用的约定
本书使用了多种文本约定。
代码 在 文本 中:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL 和用户输入。以下是一个示例:“可以在host.json文件下的extensions > http > routePrefix设置中进行更改:”
代码块如下设置:
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings":{
"isEnabled": true,
"excludedTypes": "Request"
},
"enableLiveMetricsFilters": true
}
},
"extensions": {
"http": {
"routePrefix": "myapi"
}
}
}
当我们希望将您的注意力引到代码块中的特定部分时,相关的行或项目将以粗体显示:
CarDtoValidator validator = new CarDtoValidator();
var result = validator.Validate(carAsDto);
if (!result.IsValid)
{
return BadRequest(result.Errors);
}
任何命令行输入或输出都应如下编写:
git clone https://github.com/MicrosoftDocs/mslearn-dotnet-cloudnative-devops.git eShopLite
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“选择Azure Function App (Linux)并点击下一步。”
小贴士或重要注意事项
看起来是这样的。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有任何疑问,请通过电子邮件发送至 customercare@packtpub.com,并在邮件主题中提及书名。
勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告这一点。请访问www.packtpub.com/support/errata并填写表格。
盗版:如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 copyright@packt.com 与我们联系,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且您有兴趣撰写或为本书做出贡献,请访问authors.packtpub.com。
分享您的想法
读完使用 C#和.NET 编程 API后,我们很乐意听到您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。
下载本书的免费 PDF 副本
感谢您购买本书!
您喜欢在路上阅读,但无法携带您的印刷书籍到任何地方?
您的电子书购买是否与您选择的设备不兼容?
别担心,现在,随着每本 Packt 书籍的购买,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠远不止这些,您还可以获得独家折扣、时事通讯和每日免费内容的访问权限
按照以下简单步骤获取优惠:
- 扫描二维码或访问以下链接

packt.link/free-ebook/978-1-83546-885-2
-
提交您的购买证明
-
就这样!我们将直接将您的免费 PDF 和其他优惠发送到您的电子邮件
第一章:入门
让我们先确保你处于正确的位置。这是一本关于使用.NET 创建应用程序编程接口(APIs)的书。在这个过程中,我们将查看一个非常简单的后端(数据库)和前端(用户界面),以及我们用来创建和测试 API 的工具。
API 的核心职责是将应用程序(Web、移动等)从后端(数据库、另一个 API 等)解耦。API 位于前端(你的应用程序的用户界面)和后端(在我们的情况下,是一个数据库)之间。
在本章中,我们将涵盖以下主要主题:
-
你将需要的软件
-
如何获取所需的软件
-
安装 Visual Studio
技术要求
为了继续阅读本书,你需要一台运行 Windows(10 或 11)的计算机和互联网连接。最好你的计算机至少有 16GB 的内存,当然,你还需要在磁盘上为软件和你要编写的代码腾出空间。
你可以在任何平台上创建 API(例如,Linux)并使用任何开发环境(例如,使用 Visual Studio Code 而不是 Visual Studio)。然而,本书将专注于 Visual Studio 和 Windows,因为它们是最受欢迎的,并且可以说是创建.NET API 最强大和最有效的方式。
本书的相关代码文件可在 GitHub 仓库中找到:github.com/PacktPublishing/Programming-APIs-with-C-Sharp-and-.NET 。
API 适合在哪里?
当你创建一个解耦的应用程序时,你的主要部分包括:
-
前端
-
后端
-
中间件
典型的前端可能是一个 Web 应用、移动应用或其他显示数据的方式。
典型的后端可能是一个数据库或其他服务。
中间件位于前端和后端之间。最重要的中间件是 API。API 的职责是确保前端和后端解耦——也就是说,你可以修改其中一个而不会影响另一个。这仅仅是良好的编程实践,并且如果其中任何一个发生变化(它们会的!),这将为你节省数小时(或数月)的重新编写代码的时间。
参与者
通常,后端和前端由不同的团队创建,尽管当然可以由一个开发者完成所有工作。我们还将把后端限制为一个简单的数据库,尽管任何数据源都可以作为后端。最后,我们不会构建一个完整的客户端(那会分散本书的重点),而是将使用工具Postman来模拟前端。本书中将有更多关于 Postman 的内容。
设置环境
您可以在您喜欢的任何操作系统上创建您的后端、API 和前端。对于本书,我们将使用 Windows 创建所有三个,使用 Visual Studio 2022,最新的 Postman,以及 Dapper 作为简单的 对象关系映射器(ORM),以使我们的工作更轻松。我们还将使用一些其他简单的工具,让我们为您设置好。
下载您需要的免费软件
要开始,如果您尚未安装 Visual Studio,请访问 visualstudio.com 并点击 下载(此网站经常更改,但基本步骤保持不变)。您有三个选择下载哪个版本:社区版、专业版和企业版。社区版是免费的,将提供您在本书中所需的所有内容。
当您点击您的选择时,Visual Studio 设置将下载到您的 下载 目录。双击它,并在安全提示中点击 是。安装程序将更新自身,然后开始安装。这可能需要一点时间,但请不要离开,因为您还有一些选择要做。
注意
如果您已安装 Visual Studio 但出于某种原因还想安装社区版,那没问题,因为它们可以并行运行。
将出现一个类似于 图 1.1 的菜单(如果未出现,请点击 修改)。

图 1.1 – 设置 Visual Studio:请注意,此截图旨在显示布局,因此文本可读性不是必需的
确保已勾选 ASP.NET 和 Web 开发。向下滚动并勾选 数据存储和处理(如果您磁盘空间不足,请跳过此选项)。一旦您满意,请点击 下载时安装,然后点击 修改。
将为您安装 SQL Server,以及 SQL Server Management Studio(SSMS)。您通常将通过 SSMS 与 SQL Server 交互。我们将随着我们的进展查看如何使用此工具以及所有其他工具。
您的下一个工具是 Dapper。这是一个小型、轻量级的 ORM(通常称为微 ORM),它执行了比 SQL 平台 Entity Framework 大得多的一部分工作,但开销却小得多。具体来说,Dapper 将查询映射到对象。
由于我们的需求将是最小的,Dapper 将绰绰有余。您可以在www.learndapper.com/了解更多关于 Dapper 的信息。
我们将使用 Postman 模拟我们的用户界面,我们也将使用它进行端到端测试。您可以在 postman.com/downloads 获取 Postman 的最新版本。您也可以通过浏览器访问 Postman,但我们将使用下载的版本。
Postman 非常强大,随着我们的进行,我们将回顾如何使用它。尽管如此,我们只会触及这个工具所能做到的一小部分,所以某个时候,你可能需要阅读其文档。
我们将使用 Swagger 进行文档编写(见 第四章 ),并使用内置的日志记录功能来记录错误和问题,这些问题不会直接展示给用户,但对于作为程序员的你来说将非常有用。
摘要
在本章中,你了解了你需要哪些软件,如何下载和安装它们。本书中我们将使用的所有软件都是免费的。在下一章中,我们将看到我们将构建的示例应用程序,以展示 API 的有意义使用。
你可以试试
如果你想要在我们创建 API 的过程中跟随操作,请确保下载并安装本章中描述的所有软件。
第二章:我们将构建什么
在本章中,我们将为本书的其余部分提供一个背景。这个背景是一个简单的买卖汽车的示例应用程序。我们不会构建这个应用程序,实际上,我们将在数据库中只有一个对象类型(Car)和一个表。这将使我们能够专注于 API,而不是陷入数据库设计的困境。
在本章中,我们将涵盖以下主要主题:
-
API 是什么以及它的用途是什么
-
本书将使用的后端数据库
-
本书将构建的应用程序
-
我们将使用Car对象来演示 CRUD 操作
我们将仅使用免费软件,如技术要求部分所示,并将利用 Dapper 和 AutoMapper 等开源实用程序,这两者都在第一章中介绍。
技术要求
对于本章,您需要Visual Studio、SQL Server Management Studio(SSMS)以及Postman。请记住,SSMS 是与 Visual Studio 一起安装的。
注意,您还可以使用 Visual Studio 中的 Server Explorer 和 SQL Server Object Explorer 在内部管理您的数据库。
本书的相关代码文件可在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Programming-APIs-with-C-Sharp-and-.NET/tree/main/Chapter02
什么是 API 以及它的用途是什么?
API 的目的是将应用程序的后端(例如,数据库)与前端(例如,Web 应用程序或移动应用程序)解耦,如图2.1所示。

图 2.1 – 关注点分离
客户端不直接与数据库通信,而是与 API 通信,而 API 反过来又与数据库通信。这种方法的巨大优势在于,您可以修改数据库,而客户端无需更改。或者,您可以在不更改数据库的情况下修改前端(例如,一个网站)。
这些修改发生在大型项目开发过程中,并在交付后继续进行。对于移动应用程序来说,能够在不强制用户更新应用程序的情况下修改后端至关重要。即使对于 Web 应用程序,这也可能是关键的,因为维护数据库的团队可能不是维护客户端的团队。
创建数据库
为了说明 API 的所有方面,我们将创建一个非常简单的数据库,并使用 Postman 来代表我们的客户端。Postman 允许您调用 API 并查看客户端会得到什么响应。实际上,它可以做更多的事情,但这是我们主要使用它的方式。
我们将尽可能简化后端和前端,以便我们可以专注于 API。
应用程序
我们将要创建的数据库将用于一个简单的(虚构的)买卖汽车应用程序。数据库将保存汽车列表及其发动机、性能等详细信息。此数据的简短示例如图 图 2.2 所示。

图 2.2 – 我们将要使用的数据表子集
创建汽车表
Car 表的数据来自 Kaggle 的免费 Automobile 数据集 kaggle.com。下载数据并将其导入名为 Cars 的数据库和名为 Car 的表中。列应该会自动处理。
Kaggle 上的数据集经常变化,因此您的汽车列表可能看起来略有不同。所有数据集都作为 .csv 文件呈现,因此只需下载一个,然后在 SSMS 中打开以进行导入。以下是步骤:
-
登录到 SSMS 并创建一个数据库。
-
创建一个名为 CARS 的表。
-
启动 导入数据向导。为此,右键单击表,然后从上下文菜单中选择 导入数据。
-
将会弹出 选择数据源 对话框。选择 平面文件源 并点击 浏览 以找到并选择您从 Kaggle 获取的 CSV 文件。
-
您将被要求输入目的地。选择 SQL Server 原生客户端。
-
检查您的设置并点击 保存。
完成导入后,您需要添加两个列:is_deleted(所有行初始化为 0)和 id,它应该是您的主键,并且应该自动递增。
我们准备好编写一个小应用程序,该程序将支持对我们的闪亮新数据库的基本 CRUD 操作。
如果遇到危机并且无法使其工作,请尝试以下两种选项之一以快速启动:
-
前往 stackoverflow.com 并查找将 csv 导入平面数据库的方法,或者
-
从本书存储库的 第三章 文件夹中获取代码,该代码已设置好数据库。
第二种替代方案的问题是您将拥有我们尚未解释的代码。但不用担心,如果您遵循上述步骤,一切应该都会顺利。(“打开舱门 哈罗,哈尔。”)
注意
CRUD 是 Create, Read, Update, and Delete 的缩写。也就是说,创建新记录、读取符合标准的记录、更新记录和标记记录为已删除。
数据库结构
我们的数据库仅由一个表组成:Car。如图 图 2.2 所示,该表必须存储每辆车的各种属性(名称、每加仑英里数、汽缸数等)。图 2.3 展示了该表:

图 2.3 – 汽车表列
注意 is_deleted 列。我们将使用“软删除”——也就是说,在删除时,我们不会删除行,而是将 is_deleted 设置为 true(1)。这允许我们只需将此值改回 0(false)即可轻松恢复该列。
除了id之外,所有列都是字符串,这将使处理它们更容易。
车对象
对应于Car表,我们的代码有一个Car实体(Cars/Data/Entities/Car.cs):
namespace Cars.Data.Entities;
public class Car
{
public int Id { get; set; }
public string name { get; set; } = null!;
public string mpg { get; set; } = null!;
public string cylinders { get; set; } = null!;
public string displacement { get; set; } = null!;
public string horsepower { get; set; } = null!;
public string weight { get; set; } = null!;
public string acceleration { get; set; } = null!;
public string model_year { get; set; } = null!;
public string origin { get; set; } = null!;
public string? is_deleted { get; set;}
}
在这个例子中,我们不会使用数据传输对象(DTOs),只是为了保持简单,尽管我们会在本书的后面使用它们。
ASP.NET 应用程序
要开始,请使用 ASP.NET Core Web API 模板创建一个新的 ASP.NET 项目。将文件放在您方便的地方,并选择.NET 的最新版本(本书是用.NET 8 编写的)。
我们应用程序的基本结构将如下所示:
-
带端点的控制器
-
服务
-
仓库
我们将在继续阅读时回顾端点和所有其他内容。
Program.cs
您不需要为此应用程序编辑Program.cs,但花几分钟时间查看它是值得的:
using Cars.Data;
using Cars.Data.Interfaces;
using Cars.Data.Repositories;
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();
// Load DB configuration and register the connection factory for
//injection
var configuration = builder.Configuration;
builder.Services.Configure<DbSettings>(configuration.GetSection("ConnectionStrings"));
builder.Services.AddTransient<DatabaseConnectionFactory>();
builder.Services.AddTransient<CarRepository>();
builder.Services.RegisterDataAccessDependencies();
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();
立即要注意的是对 Swagger 的引用。我们将使用 Swagger 自动为我们的项目生成文档,正如您在继续阅读时将看到的。
连接到数据库
默认项目无法知道如何连接到您的数据库。此信息包含在appsettings.json和appsettings.Development.json中(要访问后者,请展开前者)。
appsettings.json相当简单:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": "Data Source=localhost;Initial
Catalog=Cars;Integrated Security=true"
},
"AllowedHosts": "*"
}
关键在于DefaultConnection字符串,它设置使用 localhost 上的Cars数据库。
实际的连接字符串在appsettings.Development.json中:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\
mssqllocaldb;Database=Cars;Trusted_
Connection=True;MultipleActiveResultSets=true"
}
}
完成此连接所需的其他两个文件是DatabaseConnectionFactory和DbSettings。这些文件为您提供了。
以下指的是DbSettings.cs中的对象:
namespace Cars.Data;
public class DbSettings
{
public string DefaultConnection { get; set; } = null!; // https://
learn.microsoft.com/en-us/dotnet/csharp/language-reference/
compiler-messages/nullable-warnings#nonnullable-reference-not-
initialized
}
最后,在属性下,您将找到launchsettings.json文件:
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:35187",
"sslPort": 44306
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5283",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7025;http://
localhost:5283",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
所有这些都值得一看,但不必担心;几乎所有的内容都为您提供了,所有内容都在我们的仓库中的示例代码中。
文件夹
为了组织我们的应用程序,我们将创建以下文件夹:
-
控制器
-
数据
-
接口
-
仓库
-
服务
让我们简要解释一下这些内容:
-
客户端是调用 API 的应用程序(例如,网站或移动应用程序)。控制器文件夹将包含作为端点的方法(端点是客户端通过 URL 连接到的)。
-
数据文件夹将包含我们实体的定义——在我们的案例中,是之前显示的Car对象。
-
接口文件夹正如其名:它将包含我们 C#对象的接口。
-
仓库文件夹将包含我们的方法与数据库调用之间的代码。
-
服务文件夹将包含支持代码。
流程将如下所示:
-
客户端调用控制器中的方法。
-
该方法调用一个服务来处理业务逻辑。
-
服务调用仓库中的方法,然后反过来调用数据库。
我们将在下一章开始时,逐步检查细节。
摘要
在本章中,您了解了 API 是什么以及它是如何用来将前端(例如,一个网站)与后端(例如,一个数据库)分离的。我们还研究了我们将贯穿整本书使用的简单数据库和应用程序。
为了演示 CRUD 操作,我们将构建一个以汽车对象为中心的简单应用程序,就像我们正在处理汽车库存一样。我们从kaggle.com导入它。
这为后续章节以及我们将要构建的简单应用程序奠定了基础。我们的重点将严格放在创建 API 上,因此我们将花费很少的时间在数据库技术甚至前端上。
您试试看
现在是创建您的应用程序和数据库的好时机。如果您喜欢冒险,可以创建一个类似但不同的应用程序、数据库和数据实体。这将确保您巩固我们将要使用的元素。
第三章:使用 REST 实现
在上一章中,我们创建了一个简单的数据库和对象(Car)来与之交互。在本章中,我们将探讨表示状态转移(REST)协议及其在 API 中的应用。REST 是创建 API 最流行的协议。
我们将看到 REST 如何有助于创建客户端/服务器架构,以及与之相关的职责分离。在本章中,我们将涵盖以下主题:
-
理解 REST 是什么
-
查看标准网络协议
-
第一个 REST API 的实现
-
数据传输对象(DTOs)是什么以及如何使用它们
-
使用 Postman 作为我们的前端
到你完成本章时,你将理解创建简单 API 的基础知识。
技术要求
对于本章,你需要Visual Studio、AutoMapper和Dapper。请参阅第一章,“入门”,了解如何获取这些工具。本章的代码文件可以在以下 GitHub 仓库中找到:github.com/PacktPublishing/Programming-APIs-with-C-Sharp-and-.NET/tree/main/Chapter03
REST
REST 中的一个关键概念是职责分离。这个想法对于 C#程序员来说应该是熟悉的。在这里,我们正在分离服务器和客户端的职责。
API 只能响应来自客户端的调用,不能生成自己的调用。请注意,在 API 实现的逻辑中,可以调用其他 API,但不能反向调用客户端。
服务器在没有任何方式上依赖于客户端的设计或实现。
客户端/服务器
REST 应用程序的关键在于 API 可以被任何类型的客户端调用:Web 应用程序、手机应用程序等等。此外,客户端可以位于任何地方,并且对客户端架构没有约束。
后端也是如此,在 API 之后。通常,这将是一个数据库,但它可以是任何类型的数据存储:关系型、对象型、内存型等等。
实现这一点的其中一种方法,是使用标准 Web 协议,如GET、PUT等等。这种方法对于 REST 至关重要。事实上,对于许多人来说,这是 REST 的虚拟定义。
使用 Web 协议
在 REST 中,我们使用与 HTTP 相同的动词:GET、PUT、POST和DELETE。此外,API 会向客户端返回标准 Web 值,例如,404(未找到)、200(成功)、201(已创建)等等。
客户端向 API 的每个请求将包括一个 HTTP 头部、正文和元数据。客户端通过调用 URL 来表示想要的 API,并传递所需的数据(例如,请求实体的id 值,要么在 URL 中,要么如果传递给 URL 的数据过多,则在请求的正文里。例如,这里是一个向后端数据库添加汽车的 POST 请求。现在不必担心语法;而是看看图 3.1 中的 URL 和请求正文。

图 3.1 – 插入数据
在图的最上方,我们看到 URL(https://localhost:7025/Car)。这是 API 的“地址”。请注意,URL 的最后部分是Car。这是控制器的名称。
在 URL 下方有一个菜单,允许您查看正在发送的参数、作为元数据一起发送的授权和任何头部(参见图 3.2),我们在图 3.1中看到的正文,在发送请求之前要运行的任何脚本,确保我们得到正确数据的测试(参见第八章 ,高级主题),以及我们需要的任何请求设置。
前图中编号为 1-12 的行是此请求的主体。我们正在将一辆车插入数据库,因此所有插入数据都在这里以 JSON 格式提供。请注意,这里没有 ID;这将在后端分配。

图 3.2 – 头部
这些头部向服务器提供了关键信息。由于 REST 本质上是无状态的,因此必须为每次交互发送它们。
无状态和缓存
REST API 是无状态的,因此您必须将每个 API 调用视为与其他所有调用独立。
注意,如果您需要在 Azure 上使用状态,您将需要一个持久化函数,这在第七章中有所介绍,即Azure 持久化函数。
虽然您不能在调用之间保持状态,但服务器可以缓存数据以实现更快的检索。这可以显著提高性能。有许多平台可以支持 REST API;我们将在这本书中关注的一个平台,也是.NET 的首选平台,是 ASP.NET Core。
REST 的替代方案是 GraphQL。这个设计旨在解决两个问题:过度获取和不足获取。用户希望向服务器发送单个 API 调用。为了做到这一点,他们必须指定所需的数据。如果请求了 Car 实体,他们可能根本不关心汽车的一些特性,但他们作为 API 的一部分收到了整个汽车。这被称为过度获取。另一方面,如果他们指定了一个只获取汽车类型的 API,这可能就是不足获取(他们必须进行第二次调用以获取所需的其他部分)。GraphQL 旨在解决这个问题,允许客户端指定确切请求哪些属性。
话虽如此,GraphQL 的缺点是每个 API 查询都必须手工制作以指定所需的内容。
由于这一点和其他技术限制,尤其是惯性,REST 是编写 API 最受欢迎的方式,我们不会在本书中涵盖 GraphQL。
在 ASP.NET Core 中实现 REST
ASP.NET Core 中创建的 API 通常有三个主要组件:
-
控制器
-
服务
-
存储库
备注
除了基于控制器的 API 之外,还有一种称为 Minimal APIs 的替代方案。我们将在本章末尾简要讨论这一点(参见图表 Minimal APIs)。由于它们有很多限制,本书的其余部分不会涵盖 Minimal APIs。
当你使用 URL 调用一个 API 时,该地址会被解析为一个控制器。例如,使用我们之前看到的 URL,如果你调用 https://localhost:7025/Car,你将调用该地址处的 CarController。请注意,ASP.NET 使用“约定优于配置”,这意味着按照惯例,单词 Controller 的一部分被从地址中省略,但隐含在其中。所以在这种情况下,CarController,地址只使用 Car(省略 Controller)。
控制器的任务是确保用户已经认证(确实是他们)并且授权(他们有权执行所调用的任何操作)。然后控制器组装所需的数据并将其传递给服务。通常,但不一定,这将被称为 CarService,并将位于包含其他服务的文件夹中。
服务的任务是处理任何业务逻辑并将数据准备好输入数据库。然后它将数据传递给存储库。
与服务类似,存储库通常被称为 CarRepository,并将位于与其他存储库相同的文件夹中。存储库的任务是与底层数据存储(例如,数据库)交互。
通常,你将想要将数据库的属性与通过 API 发送的对象的属性分开。为此,我们使用 DTOs。
DTOs
使用 DTOs 将数据库结构的表示与支持 Plain Old C# Object ( POCO ) 分离是很常见的。
让我们从一个关系型数据库的例子开始。每一行可能有十二列,但对于特定的 API 请求,只需要七列。DTO 将是一个具有七个属性的类,我们将使用一个工具(AutoMapper)将七个列中的值映射到七个属性。
安装 AutoMapper
安装 AutoMapper 最简单的方法是下载 NuGet 包,如图 图 3.3 所示:

图 3.3 – 安装 AutoMapper
对于 AutoMapper 来说,有一些配置要做,但只需做一次。在 Program.cs 中添加以下内容:
builder.Services.AddAutoMapper(typeof(Program));
下一步是创建 DTO 类本身。
创建 DTO 类
我们创建的 POCO 类直接跟踪数据库中的列,如前所述。DTO 类跟踪这些列的一些或全部属性:
namespace Cars.Data.DTOs
{
public class CarDto
{
public int Id { get; set; }
public string Name { get; set; }
public string Mpg { get; set; }
public string Cylinders { get; set; }
public string Displacement { get; set; }
public string Horsepower { get; set; }
public string Weight { get; set; }
public string Acceleration { get; set; }
public string ModelYear { get; set; }
public string Origin { get; set; }
}
}
一旦你的 DTO 设置好,你需要一种方法将 DTO 中的属性映射到数据库(POCO)类中的属性。我们可以在配置文件中做到这一点。
设置配置文件
当我们在这里时,让我们为 Car 实体设置配置文件。AutoMapper 工具(无意中)提供了一个基类,Profile,我们将从中派生。
创建一个名为 CarProfile.cs 的文件,并将此代码放入其中:
Public class CarProfile : Profile
{
Public CarProfile()
{
CreateMap<CarDto, Car>()
.ForMember(car => car.id, opt=>opt.MapFrom(carDto => carDto.
Id))
.ForMember(car => car.name, opt=>opt.MapFrom(carDto => carDto.
Name))
.ForMember(car => car.mpg, opt=>opt.MapFrom(carDto => carDto.
Mpg))
.ForMember(car => car.cylinders, opt=>opt.MapFrom(carDto =>
carDto.Cylinders))
.ForMember(car => car.displacement, opt=>opt.MapFrom(carDto =>
carDto.Displacement))
.ForMember(car => car.horsepower, opt=>opt.MapFrom(carDto =>
carDto.Horsepower))
.ForMember(car => car.weight, opt=>opt.MapFrom(carDto =>
carDto.Weight))
.ForMember(car => car.acceleration, opt=>opt.MapFrom(carDto =>
carDto.Acceleration))
.ForMember(car => car.model_year, opt=>opt.MapFrom(carDto =>
carDto.ModelYear))
.ForMember(car => car.origin, opt=>opt.MapFrom(carDto =>
carDto.Origin))
.ReverseMap();
注意,对于每个我们想要在 DTO 和 POCO 类之间共享的成员,都有一个条目。底部是 ReverseMap,正如你可能猜到的,它反转了映射(例如,它使映射从 DTO > Car 转换为 Car > DTO)。
我们已经处理了 DTO;现在我们需要将我们的 POCO 连接到数据库。我们将使用 Dapper 来完成这项工作。
Dapper
如前所述,我们将使用 Dapper 作为我们的 对象关系模型 ( ORM )。这将极大地简化我们与数据库的交互。Dapper 有自己的语法,但它非常(非常)接近 SQL,起点将很明显。
安装 Dapper
要安装和使用 Dapper,请参阅他们非常直接且全面的说明,见 www.learndapper.com/
例如,这是通过 ID 获取汽车的 Dapper 代码:
Public async Task<Car?> GetCarById(int carId)
{
var sql =
$@"SELECT *
FROM
Cars C
WHERE
C.id = @{nameof(carId)}
AND C.is_deleted = 0";
var param = new
{
carId
};
var car = await QueryFirstOrDefaultAsync<Car>(sql, param);
return car;
}
我们可以使用条件逻辑创建更复杂的语句。
注意
Dapper 使用 C# 而不是 SQL 语法(除了查询本身)。这使得 C# 程序员与数据库的交互变得更加容易。
这就是你需要知道的所有内容,以便开始创建你的 API。让我们尝试使用 API 将一辆车插入到数据库中。
检查 SQL
上述示例中的 SQL(发音为 See-Quill)几乎可以读作一个英文句子。首先,我们使用关键字 SELECT 来表示我们想要选择并返回数据库中的数据子集。
接下来是星号( ***** ),它表示我们想要所有列。另一种选择是列出我们想要的列。
From Cars C 表示我们希望数据来自 Cars 表,并且我们将使用别名 C 来引用该表。
Where 语句将搜索限制为随后的条件,在这种情况下,汽车的 id(使用别名 C)与我们要查找的 id 匹配。然后我们附加“ where is_deleted = 0 ”,表示我们只想获取未标记为已删除的条目。
这里是 Dapper 用来通过 ID 获取汽车的代码:
call: QueryFirstOrDefaultAsync
这将调用 dapper 中的 QueryFirstOrDefault 方法。我们传入我们正在寻找的对象类型(Car)和两个参数。在我们的情况下,第一个始终是 SQL,第二个始终是我们上面创建的集合的名称(param)。
我们将结果(在这种情况下是一辆车)分配给一个变量,并返回该值。
请注意,此类的构造函数将通过依赖注入传递接口,并将这些参数分配给成员变量(例如,_ carService)。如果您不熟悉依赖注入,请参阅存储库中的代码。
将所有内容组合起来(插入一辆车)
让我们使用控制器、服务和存储库结合 Dapper 和 AutoMapper 来插入一辆车:
//Controller
[HttpPost]
public async Task<ActionResult<Car>> Insert([FromBody] CarDto
carAsDto)
{
try
{
if (carAsDto == null)
{
return BadRequest("No car was provided");
}
var carToInsert = _mapper.Map<Car>(carAsDto);
var insertedCar = await _carService.Insert(carToInsert);
var insertedCarDto = _mapper.Map<CarDto>(insertedCar);
var location = $"https://localhost:5001/car/{insertedCarDto.
Id}";
return Created(location, insertedCarDto);
}
catch (Exception e)
{
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
注意,我们已经将 DTO 转换为数据库对象(Car),然后将其传递到服务:
// car service
public async Task<Car> Insert(Car)
{
var newId = await _carRepository.UpsertAsync(car);
if (newId > 0)
{
car.id = newId;
}
else
{
throw new Exception("Failed to insert car");
}
return car;
}
我们现在准备将这辆车传递到存储库,以便在我们的数据库中存储:
public async Task<int> UpsertAsync(Car car)
{
using var db = databaseConnectionFactory.GetConnection();
var sql = @"
DECLARE @InsertedRows AS TABLE (Id int);
MERGE INTO Car AS target
USING (SELECT @Id AS Id, @Name AS Name, @MPG as MPG, @
Cylinders as Cylinders, @Displacement as Displacement, @
Horsepower as Horsepower, @Weight as Weight, @Acceleration as
Acceleration, @Model_Year AS Model_Year, @Origin AS origin, @
Is_Deleted AS Is_Deleted ) AS source
ON target.Id = source.Id
WHEN MATCHED THEN
UPDATE SET
Name = source.Name,
MPG = source.MPG,
Cylinders = source.Cylinders,
Displacement = source.Displacement,
Horsepower = source.Horsepower,
Weight = source.Weight,
Acceleration = source.Acceleration,
Model_Year = source.Model_Year,
Origin = source.Origin,
Is_Deleted = source.Is_Deleted
WHEN NOT MATCHED THEN
INSERT (Name, Mpg, Cylinders, Displacement, Horsepower,
Weight, Acceleration, Model_Year, Origin, Is_deleted)
VALUES (source.Name, source.MPG, source.Cylinders, source.
Displacement, source.Horsepower, source.Weight, source.
Acceleration, source.Model_Year, source.Origin, source.Is_
Deleted)
OUTPUT inserted.Id INTO @InsertedRows
;
SELECT Id FROM @InsertedRows;
";
var newId = await db.QuerySingleOrDefaultAsync<int>(sql, car);
return newId == 0 ? car.id : newId;
}
如果我们得到一个有效的新车,我们将返回newId 值,这在服务中进行检查。然而,我们传递给 API 的数据量很大。我们将在下一节中探讨解决这个问题。
关于依赖注入的说明:正如您所知,我们向方法传递接口,以便支持依赖注入。这已在Program.cs中设置,而注入本身是自动的。
在 Postman 中创建正文
如您所见,我们想要传递给 API 的数据对于查询字符串来说太多,因此我们将将其传递到正文。我们可以在 API 定义中通过编写以下内容来表示这一点:
public async Task<ActionResult<Car>> Insert([FromBody] CarDto carAsDto)
每个 API 调用都将有零个或多个FromQuery、FromUrl和FromBody属性。在这种情况下,我们只是使用FromBody。我们的 Postman 调用如图3.4所示:

图 3.4 – 插入一辆车
在这里,我们正在将车辆的除 ID 值之外的所有属性(如顶部窗口所示)插入。为此,我们需要调整 SQL 语句以获取所有属性。请注意,API 返回插入车辆的属性,包括其id(底部窗口)。返回代码将是201(已创建)。
由于我们正在查找请求正文中的数据,因此 URL 只是控制器的地址。
我们已插入一辆车,但我已经预先在数据库中插入了更多。让我们使用 API 来查看它们。
获取所有
要获取数据库中所有车的列表,我们从控制器开始:
警告:以下示例仅用于说明目的,直接调用存储库。不久之后,我们将讨论正确的方法。
public async Task<IEnumerable<Car>> GetAll(bool returnDeletedRecords = false)
{
return await _carRepository.GetAll(returnDeletedRecords);
}
在这里,我们直接调用存储库。这是调用服务的一种替代方法,但通常是一种不好的做法,但我想要展示如何进行。请注意,我们包括一个布尔参数,以确定是否返回已删除的记录。
通常,我们会使用Service类来实现关注点的分离。服务类将包含程序逻辑,并位于控制器和存储库之间。
在存储库中,我们构建我们的SqlBuilder(如我们在Dapper部分中之前所见)并获取记录:
public async Task<IEnumerable<Car>> GetAll(bool returnDeletedRecords = false)
{
var builder = new SqlBuilder();
var sqlTemplate = builder.AddTemplate("SELECT * FROM car " +
"/**where**/ ");
if (!returnDeletedRecords)
{
builder.Where("is_deleted=0");
}
using var db = databaseConnectionFactory.GetConnection();
return await db.QueryAsync<Car>(sqlTemplate.RawSql,sqlTemplate.
Parameters)
}
让我们逐行分析。第一行表示我们将返回一个Car对象的列表,而是否返回已删除记录的决策默认为false。
我们接下来创建一个SqlBuilder对象,然后设置SqlTemplate对象以选择所有车辆。
注意到/where/语句。这是一个 Dapper 约定,表示可以在这里放置一个where子句。
我们现在将检查是否需要包含已删除的记录,如果不包含,我们将使用我们在第一行创建的构建器添加一个 where 子句。
我们准备好从我们创建的工厂获取数据库,然后查询数据库,传入用于在SqlTemplate 对象中使用的RawSql代码和参数。在这种情况下,没有参数。
我们得到的是一个Car对象的数组,我们将它返回给调用方法。
为了测试这个,我们将 Postman 设置为Get,并将 URL 设置为 https://localhost:7025/Car。由于没有提供 ID,我们的代码将获取所有车辆,如图图 3.5所示:

图 3.5 – 获取数据库中的所有车辆
插入记录后,我们可能想要更改一个或多个。为此,我们将想要使用Put动词。
更新
更新操作使用 HTTP Put动词。让我们追踪一下它是如何完成的。
我们回到控制器,添加一个HttpPut属性。然后我们指出内容将位于请求体中(而不是查询中),正如我们在插入时所见:
[HttpPut]
public async Task<IActionResult> Put([FromBody] Car car)
由于我们无法确定我们想要更新的记录是否仍然在数据库中,我们将调用放在 try 块中,如果发生异常,我们调用BadRequest。有趣的是,如果我们成功,我们调用NoContent,因为我们没有向数据库添加任何内容:
[HttpPut]
public async Task<IActionResult> Put([FromBody] Car car)
{
try
{
await _carService.Update(car);
}
catch (Exception e)
{
return BadRequest(e);
}
return NoContent();
}
如上图所示,从这里我们调用carService的Update方法,传入我们想要更新的车辆。请注意,一些程序员使用OK而不是NoContent。
在服务中,我们将确保我们得到了有效的车辆id 值,然后调用UpsertAsync,传入车辆。如果我们得到除了原始 ID 之外的任何id值,我们抛出异常;否则,我们返回Car对象:
public async Task<Car> Update(Car car)
{
if (car.id == 0)
{
throw new Exception("Id must be set");
}
var oldId = car.id;
var newId = await _carRepository.UpsertAsync(car);
if (newId != oldId)
{
throw new Exception("Failed to update car");
}
return car;
}
在Upsert方法中,我们检查这是否是一辆新车辆(插入)或更新:
public async Task<int> UpsertAsync(Car car)
{
using var db = databaseConnectionFactory.GetConnection();
var sql = @"
DECLARE @InsertedRows AS TABLE (Id int);
MERGE INTO Car AS target
USING (SELECT @Id AS Id, @Name AS Name, @MPG as MPG, @Cylinders as
Cylinders, @Displacement as Displacement, @Horsepower as
Horsepower, @Weight as Weight, @Acceleration as Acceleration, @
Model_Year AS Model_Year, @Origin AS origin, @Is_Deleted AS Is_
Deleted ) AS source
ON target.Id = source.Id
WHEN MATCHED THEN
UPDATE SET
Name = source.Name,
MPG = source.MPG,
Cylinders = source.Cylinders,
Displacement = source.Displacement,
Horsepower = source.Horsepower,
Weight = source.Weight,
Acceleration = source.Acceleration,
Model_Year = source.Model_Year,
Origin = source.Origin,
Is_Deleted = source.Is_Deleted
WHEN NOT MATCHED THEN
INSERT (Name, Mpg, Cylinders, Displacement, Horsepower,
Weight, Acceleration, Model_Year, Origin, Is_deleted)
VALUES (source.Name, source.MPG, source.Cylinders, source.
Displacement, source.Horsepower, source.Weight, source.
Acceleration,source.Model_Year, source.Origin, source.Is_
Deleted)
OUTPUT inserted.Id INTO @InsertedRows
;
SELECT Id FROM @InsertedRows;
";
var newId = await db.QuerySingleOrDefaultAsync<int>(sql, car);
return newId == 0 ? car.id : newId;
}
注意,在最后两行中,我们从查询中获取newId值,如果它是0,则返回原始车辆 ID。否则,我们返回插入的车辆的newid值。
软删除
为了完整性,让我们快速了解一下软删除。你会记得,当用户请求删除一条记录时,我们并不是真正地从数据库中移除它,而是将其标记为已删除(在is_deleted列中),这样我们就可以根据需要获取这些记录。
我们从控制器开始:
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
try
{
await _carService.Delete(id);
}
catch (Exception e)
{
return BadRequest(e);
}
return NoContent();
}
删除操作接受要标记为已删除的汽车的 ID,然后调用服务。服务所做的只是确保 ID 有效,然后调用存储库执行实际删除:
public async Task Delete(int id)
{
var car = await _carRepository.Get(id);
if (car == null)
{
throw new Exception("Car not found");
}
await _carRepository.DeleteAsync(id);
return;
}
仓库中的代码非常简单。它获取数据库连接并创建查询以将is_deleted设置为1。然后执行该查询:
public async Task<int> DeleteAsync(int id)
{
using var db = databaseConnectionFactory.GetConnection();
var query = "UPDATE car SET Is_Deleted = 1 WHERE Id = @Id";
return await db.ExecuteAsync(query, new { Id = id });
}
到目前为止,您已经拥有了一个完整的 API,用于创建、读取、更新、删除(CRUD)操作。
摘要
在本章中,您介绍了使用 Dapper 和 AutoMapper,后者用于 DTO 对象。您还深入研究了 CRUD 操作的工作原理,使用了典型的三个类:控制器、服务和存储库。
您可以看到,对于简单操作,您可以绕过服务,但这被认为是不良实践。
在企业应用程序中,您会希望使用 DTOs(数据传输对象)来分离数据库布局与传递对象布局。随着我们深入探讨增强 API 可用性和理解领域,下一章将强调使用 Swagger 记录项目的重要性。
您尝试一下
创建一个简单的数据库来跟踪您的音乐收藏。将此放入数据库,并使用 Dapper 和 AutoMapper 实现四个 CRUD 操作。为此练习,创建只提供数据库部分列的 DTOs。
最小化 API
最小化 API 提供了一种创建 API 的替代方法,无需使用控制器。虽然它们仍然允许注入所需的服务,但它们主要用于具有最小依赖项的小型端点。
相比之下,控制器附带了一个更大的端点组件层次结构,需要考虑。这包括版本控制、控制器命名和手动路由属性,这些可能会引入不必要的额外模板代码。最小化 API 通过允许您在单个表达式中声明和处理请求来简化此过程。
为了说明,让我们重新实现获取所有汽车的调用。从第三章的代码开始,然后使用 app 变量添加对MapGet的调用。路由将是/car-minimal,处理程序可以保持与GetAll方法相同:调用ICarRepository.GetAll。但如果没有构造函数,我们如何访问服务接口?最小化 API 通过使用参数注入来解决此问题。只需将ICarRepository carRepository作为参数传递给MapGet的委托参数,并使用它来调用. GetAll方法。
许多不同类型都可以注入到最小化 API 的处理程序中。通过将属性应用于单个参数来区分模型绑定类型。常用的包括[FromRoute]、[FromBody]和[FromServices]。其他特殊类型包括HttpContext、HttpRequest、HttpResponse、IFormFile和请求体的Stream。完整列表可在官方 Microsoft 文档中找到。
第四章:使用 Swagger 的文档
在本章中,我们将探讨使用 Swagger 对你的项目进行文档记录。在 API 中,可读性强的文档至关重要。它允许你的客户快速理解每个端点和可能的响应。
我们将通过使用 XML 并在代码中包含属性来确保我们的 Swagger 文档遵循 OpenAPI(以前称为 Swagger)规范。你可以在swagger.io/docs/specification/about/了解更多关于 OpenAPI 的信息。
Swagger 需要一个 OpenAPI 实现,在.NET 的情况下,Swashbuckle 是指定的选项。
在本章中,我们将涵盖以下主题:
-
Swagger 是什么
-
Swagger 的使用方法
-
如何设置 Swagger
-
如何向 Swagger 传递参数
技术要求
对于本章,你只需要 Visual Studio。我们将使用的所有功能都包含在 Visual Studio 中,或者可以通过 NuGet 免费获得。
本书的相关代码文件可在 GitHub 仓库中找到:github.com/PacktPublishing/Programming-APIs-with-C-Sharp-and-.NET/tree/main/Chapter04
设置 Swagger 文档
要安装 Swagger,打开你的解决方案,从菜单中选择工具 | NuGet 包管理器 | 管理解决方案的 NuGet 包。安装Swashbuckle.AspNetCore的最新版本。
右键单击你的项目文件,然后点击属性。在屏幕的左侧,选择应用程序。在右侧,选择控制台应用程序,如图 4.1所示:

图 4.1 – 安装部分 1
在生成下,选择输出并滚动到文档文件。检查图 4.2中显示的生成包含 API 文档的文件框:

图 4.2 – 设置 Swagger 输出
注意
在图 4.2中,生成包含 API 文档的文件被选中。生成的文件可以导入到其他应用程序中,例如 Postman,以获得集成文档和测试体验。
打开Program.cs(通常是解决方案资源管理器中的最后一个文件)并将 Swagger 生成器添加到服务集合中:
builder.Services.AddSwaggerGen(
x =>
{
x.SwaggerDoc(
"v1",
new OpenApiInfo
{
Title = $"{Assembly.GetExecutingAssembly().GetName().
Name}",
Version = "Version 1",
Description = "Create documentation for Cars",
Contact = new OpenApiContact
{
Name = "Jesse Liberty",
Email = "jesseliberty@gmail.com",
Url = new Uri("https://jesseliberty.com")
}
});
var xmlFilename = System.IO.Path.Combine(System.
AppContext.BaseDirectory, $"{Assembly.
GetExecutingAssembly().GetName().Name}.xml");
x.IncludeXmlComments(xmlFilename);
});
保存所有内容。就是这样。这只需要做一次。你现在已经设置了 Swagger 文档。实际上,你已经添加了描述、名称和联系信息。结果如图 4.3所示。这就是当你启动程序且 Swagger 打开浏览器窗口以显示其界面时你会看到的内容:

图 4.3 – Swagger 文档的顶部
注意,在这个页面上,网站和电子邮件是活链接,而文本为汽车创建文档是你之前代码中描述的内容。
Swagger 控制器
Swagger 文档是通过 XML 注释实现的。XML 注释由三个斜杠标记开头。以下是一个示例:
/// XML comment
XML 注释是成对标签,如下所示:
/// <summary>
…
/// </summary>
让我们从 Cars 应用程序的控制器开始。我们将在控制器中的每个方法上方放置 Swagger 文档。
我们将添加的第一个注释是摘要:
/// <summary>
/// Get all the cars in the Database
/// </summary>
这条注释将出现在GET按钮的旁边,如图 图 4.4 所示:

图 4.4 – 显示摘要
在我们继续前进之前,让我们看看在不编写任何额外代码的情况下,我们从 Swagger 中获得了什么。
Swagger 开箱即用
注意 Swagger 将在启动应用程序后自动出现。
要查看自动生成的文档,请点击图 4.4 中圆圈所示的右上角的箭头。Swagger 打开 Get 命令的详细信息,如图 图 4.5 所示:

图 4.5 – Swagger 开箱即用
这有点难以看清,所以让我们放大几个重要部分。在左上角是一个区域,用于发送给 GetAll 命令的所有参数。我们发送了 returnDeletedRecords,默认值为 false。图 4.6 显示了 Swagger 如何描述这一点:

图 4.6 – 参数
注意下拉菜单。它允许你尝试两种可能的值(false 和 true)。
接下来,在左侧是服务器潜在响应的代码和描述。图 4.7 显示第一个潜在响应是 200: 成功:

图 4.7 – 成功
在第一个响应代码下方是架构,它告诉你有哪些属性以及它们的类型,如图 图 4.8 所示:

图 4.8 – 架构
我们已经准备好查看我们注释的效果。让我们看看启动应用程序并启动 Swagger 时会发生什么。
在 Swagger 中运行您的 API
最重要的是,在右上角有一个 尝试一下 按钮。点击此按钮将 Swagger 切换到交互模式,并允许你尝试你的代码。当点击此按钮时,将出现两个其他按钮:执行 和 清除。按下 执行 将导致代码运行,在我们的情况下返回汽车列表和一些其他元数据。让我们专注于这一点(清除将删除结果,以便你可以再次尝试)。
我们看到的第一件事被标记为 Curl,如图 图 4.9 所示:

图 4.9 – 每个 Swagger 页面都显示端点的 curl
根据维基百科,“ Curl 结合了文本标记(如 HTML)、脚本(如 JavaScript)和强大的计算(如 Java、C# 或 C++)在一个统一的框架中 ”。在这本书中,我们将忽略 Curl。
在下面,我们看到我们提交给服务器的请求 URL,如图 图 4.10 所示:

图 4.10 – Swagger 中显示的请求 URL
注意我们正在使用本地主机上的端口 7025(你的可能不同)工作,并且我们像任何 HTML 一样传递我们的参数(returnDeletedRecords=false)。
接下来是最重要的服务器响应:响应体,如图 图 4.11 所示:

图 4.11 – 服务器响应
最后,响应对象的模式显示出来,如图 图 4.12 所示:

图 4.12 – 响应模式
Swagger 对于文档来说很棒,但在测试你的应用程序方面有些局限。正如我们将看到的,Postman 是一个更强大的测试应用程序。
param 标签
如果你的动词有参数,你可以使用 param 关键字在 Swagger 属性中记录它们。例如,在我们的案例中,我们想要记录 returnDeletedRecords 参数。我们可以这样做:
/// <summary>
/// Get all the cars in the Database
/// </summary>
/// <param name="returnDeletedRecords">If true, the method will return all the records, including the ones that have been deleted</param>
结果是参数在 Swagger 中得到记录,如图 图 4.13 所示:

图 4.13 – 记录参数
响应代码
你可以(并且应该)记录所有可能的响应代码及其指示:
/// <response code="200">Cars returned</response>
/// <response code="404">Specified Car not found</response>
/// <response code="500">An Internal Server Error prevented the request from being executed.</response>
结果是返回代码在 Swagger 中得到记录,如图 图 4.14 所示:

图 4.14 – 记录返回代码
虽然这需要一些努力,但它变成了常规操作,是最佳实践。这也使你的客户的生活变得更加容易。
摘要
在本章中,你学习了如何设置 Swashbuckle 以启用 Swagger。Swagger 提供了您 API 的详细文档,允许(人类)客户端了解每个端点及其用途。此外,您还可以记录每个参数和潜在的错误代码。
在下一章中,你将看到我们如何可以在请求过程中验证端点请求——防止资源浪费并保护你免受针对你系统的各种攻击。
你试试看
安装 Swagger 并为 Car 控制器中的 Insert 端点创建文档。运行 Swagger 以确保所有文档都正确显示。
第五章:数据验证
只要将数据从你的 API 发送到数据库,却发现输入无效,这是不必要的昂贵。更好的做法是在数据进入时对其进行测试,以确保它符合基本标准。这个初步的测试集(称为验证)检查输入数据,以确保它符合最低标准,并且格式正确。
在本章中,你将学习以下内容:
-
如何验证输入数据
-
如何响应无效数据
技术要求
对于本章,从现有的数据传输对象(DTO)分支创建一个新的分支(这样我们就可以从有效数据开始)。你需要以下内容:
-
Visual Studio
-
AutoMapper
-
FluentValidation
FluentValidation库是一个强大的工具,我们将在本章中使用它来创建验证器。你可以以各种方式安装它,但最简单的方式是作为一个 NuGet 包。你还需要 ASP.NET 的包,如图 图 5 .1 所示:

图 5.1 – NuGet 安装
你可以在docs.fluentvalidation.net/en/latest/index.html#找到FluentValidation的完整文档。我将在进行过程中提供详细的步骤。
本章的代码文件可以在以下 GitHub 仓库中找到:github.com/PacktPublishing/Programming-APIs-with-C-Sharp-and-.NET/tree/main/Chapter05
调整你的 API
现在我们已经为我们的程序打下了基础,我们想要验证传入的数据,这样既可以加快速度,也可以防止恶意数据。我们可以通过验证来实现这一点,但首先,我们需要做一些修改,以便我们有一个好的代码来工作。
分页
记住,我们正在模拟一个汽车买卖应用程序。我们的数据库中可能有成百上千辆汽车。我们需要在我们的Get端点中添加分页,这样当我们获取车辆列表时,它们不会一次性全部下载。这也会给我们一些东西来验证。
为了方便起见,我们需要知道以下内容:
-
页面的大小——也就是说,每页上的汽车数量
-
客户端想要查看的页码索引
我们将通过修改我们的Get端点来接受两个额外的参数来实现这一点:
-
pageOffset
-
pageSize
这两个都是int数据类型,如下所示:
[HttpGet]
public async Task<IEnumerable<Car>> Get([FromRoute] bool showDeleted, int pageNumber, int pageSize )
第一个新参数(pageNumber)将告诉Get它位于哪一页,第二个(pageSize)将告诉获取多少行。让我们看看 Postman 中的示例,如图 图 5 .2 所示:

图 5.2 – 分页
在这里,我们将pageNumber设置为0,表示我们想要从列表的开始处开始,将pageSize设置为3,表示我们只想获取三条记录。因此,API 将提取前三条记录。如果我们将pageNumber设置为3,我们将得到记录10、11和12(即从第四页开始获取下三条记录)。
为了减少用户混淆,您可能希望对用户进行的一项改进是从页码中减去 1 以获取偏移量。这将使用户能够输入页码 1 以获得偏移量 0。
验证
在执行相关方法之前验证端点上的传入属性是一种最佳实践。这不仅提高了 API 的性能,还能保护您免受某些形式的黑客攻击(例如,注入)。
我们的建议是使用FluentValidation NuGet 包,您可以在本章开头的技术要求部分中按如下所示安装。
安装完成后,您有多个选项来捕获错误。让我们看看几个例子。
捕获错误
您会记得我们的Car对象(在第三章中定义)看起来是这样的:
public class Car
{
public int id { get; set; }
public string name { get; set; }
public string mpg { get; set; }
public string cylinders { get; set; }
public string displacement { get; set; }
public string horsepower { get; set; }
public string weight { get; set; }
public string acceleration { get; set; }
public string model_year { get; set; }
public string origin { get; set; }
public string? is_deleted { get; set;}
}
让我们再假设汽车不能被删除。您可以非常快速地创建一个验证器。首先,添加一个Using语句:
Using FluentValidation
接下来,创建一个从AbstractValidator派生的类:
public class CarDtoValidator : AbstractValidator<Car>
最后,将验证规则放入该类的构造函数中。每个规则都是通过使用RuleFor关键字和一个 lambda 表达式来创建的,该表达式指示您想要验证哪个属性以及验证规则。我们的简单示例将如下所示:
using Cars.Data.DTOs;
using FluentValidation;
namespace Cars.Validators
{
public class CarDtoValidator : AbstractValidator<CarDto>
{
public CarDtoValidator()
{
RuleFor(x => x.Is_Deleted).Equal("0");
}
}
}
Equal运算符是您可以在FluentValidation文档页面上找到的许多运算符之一:docs.fluentvalidation.net/en/latest/built-in-validators.html 。
我们将测试数据,然后根据需要将其与有效数据进行比较,并在适当的情况下返回错误,或者更常见的是,如果数据未通过验证,我们将抛出异常。
测试返回值
处理验证错误有多种方法。一种是将错误返回给调用方法。因此,为了验证Car对象,从概念上讲,您希望以下内容:
CarDto car = new CarDto();
CarDtoValidator carDtoValidator = new CarDtoValidator ();
carDtoValidator.Validate(car);
在我们的insert方法中,我们将检查确保CarDto是有效的(在这种情况下,它没有被删除):
[HttpPost]
public async Task<ActionResult<Car>> Insert([FromBody] CarDto
carAsDto)
{
try
{
if (carAsDto == null)
{
return BadRequest("No car was provided");
}
CarDtoValidator validator = new CarDtoValidator();
var result = validator.Validate(carAsDto);
if (!result.IsValid)
{
return BadRequest(result.Errors);
}
var carToInsert = _mapper.Map<Car>(carAsDto);
var insertedCar = await _carService.Insert(carToInsert);
var insertedCarDto = _mapper.Map<CarDto>(insertedCar);
var location = $"https://localhost:5001/car/
{insertedCarDto.Id}";
return Created(location, insertedCarDto);
}
catch (Exception e)
{
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
让我们看看如何为未通过验证检查的数据返回错误代码。
返回错误
如果我们现在使用 Postman 将CarDto发送到端点,但将Is_Deleted设置为1,我们将得到如图 5.3 所示的错误:

图 5.3 – 验证错误
注意到返回的 HTTP 值是400 – Bad Request。这很合理,因为传入的 DTO 无效。
添加自定义消息
这个输入很棒,提供了很多信息,但 Is_Deleted 字段中的 1 值有特殊含义;具体来说,这意味着记录已经被删除。这导致验证检查失败。
我们可以通过自定义消息使错误更清晰。返回到 CarDtoValidator 并按如下修改规则:
RuleFor(x => x.Is_Deleted).Equal("0").WithMessage("Car must not be deleted");
你可以在 图 5.4 中看到新的错误消息:

图 5.4 – 自定义错误消息
自定义消息为你的客户端提供了关键信息。它们有助于避免混淆,并立即清楚地表明提交的数据有什么问题。
链式调用
如果你想验证属性的多方面,你可以使用 点 操作符来链式调用测试:
RuleFor(x=> x.Is_Deleted).NotEmpty().Equal("0").WithMessage("Car must not be deleted");
这个测试确保 Is_Deleted 字段不为空,并验证其值等于 0,如果提交的值无效,则返回错误消息。
默认情况下,即使第一个测试(NotEmpty)失败,第二个测试(Equal("0")也会运行。你可以通过使用 CascadeMode 来防止这种情况。
如果你不想在第一个测试(Is_Deleted)失败时运行第二个测试(NotEmpty),请使用 CascadeMode.Stop,如下所示:
RuleFor(x=> x.Is_Deleted).Cascade(CascadeMode.Stop).NotEmpty().Equal("0").WithMessage("Car must not be deleted");
这现在在 C# 中像 && 一样工作——也就是说,如果第一个测试失败,第二个测试将不会被执行。CascadeMode 的两个值是 Stop 和 Continue,后者是默认值。
抛出异常
作为验证并检查结果的一种替代方法,你可以调用 ValidateAndThrow。使用这个更简洁的表达式,每个规则都会被评估,如果其中一个失败,则会抛出异常:
CarDtoValidator validator = new CarDtoValidator();
//var result = validator.Validate(carAsDto);
//if (!result.IsValid)
//{
// return BadRequest(result.Errors);
//}
validator.ValidateAndThrow(carAsDto);
抛出的异常是 ValidationException 类型,因此你可以在你的 catch 块中测试它。此外,该异常有一个 Errors 属性,其中包含你的失败尝试的错误信息。
在下一个代码片段中,你可以看到如何设置在验证失败时抛出异常。我们创建验证器,然后调用 ValidateAndThrow,传入 Dto 对象。然后你可以捕获该异常并检查错误:
try
{
if (carAsDto == null)
{
return BadRequest("No car was provided");
}
CarDtoValidator validator = new CarDtoValidator();
validator.ValidateAndThrow(carAsDto);
//…
}
catch(ValidationException e)
{
IEnumerable<ValidationFailure> errors = e.Errors;
return BadRequest(errors);
}
catch (Exception e)
{
return StatusCode(StatusCodes.Status500InternalServerError);
}
为了节省你创建自定义验证器用于常见场景的时间和精力,FluentValidation 提供了多个内置验证器。
内置验证器
除了我们之前看到的 Equal 和 NotEmpty 验证器之外,还有许多内置验证器。我不会提供完整的列表(请参阅文档),但其中一个最有趣的是 PredicateValidator 验证器。它将属性的值传递给一个委托,该委托可以使用自定义验证逻辑。这通过使用 Must 关键字实现,如下所示:
RuleFor(car => car.Is_Deleted).Must(isDeleted => isDeleted == "0").WithMessage("Car must have value zero");
如果这个验证失败,问题将如 图 5.5 所示显示在结果中:

图 5.5 – PredicateValidator 错误
有一个使用 正则表达式 验证器,它使用 Matches 关键字(而不是 Must ),但我最喜欢的是 EmailValidator,它确保提交的值是一个有效的电子邮件地址。同样,还有一个 CreditCard 验证器:
RuleFor(cc => cc.CreditCard).CreditCard();
还有更多,例如 NotNull、NotEmpty、Equal、NotEqual 等等。
要查看内置验证器的完整列表以及如何使用它们,最佳位置是在 FluentValidation 文档中:docs.fluentvalidation.net/en/latest/index.html# 。
摘要
在本章中,你看到了如何在使用 API 的代码执行之前,使用 FluentValidation 验证输入属性。
你看到了如何创建规则,如何将它们链接起来,以及如何确保在链中的第一个规则失败时,第二个规则不会被评估。
你学习了两种处理错误的方式——测试返回的错误或抛出异常——以及你看到了如何创建自定义错误消息。
在下一章中,我们将把注意力转向 Azure Functions——.NET 中编程 API 的关键部分。随后,我们将探讨可持久化的 Azure Functions 以及它们为 Azure Functions 增加了什么。
你试试看
为 Car 类(或你创建的另一个类)创建一组规则,如果违反验证规则则抛出异常。确保处理验证失败的情况。
作为额外奖励,创建一个(必须)自定义规则并对其进行测试。
第六章:Azure Functions
确定您的 API 的执行环境可能会对它们的运行方式、扩展方式、成本以及默认功能产生重大影响。Azure Functions 提供了一种不同的托管选项,它通过关注事件驱动的执行来补充现有的 Azure 服务。虽然 Functions 提供了许多方式来响应系统中的不同事物,但我们将专注于一个特定的事件:HTTP 请求。
在本章中,我们将介绍与托管和计费相关的某些技术方面,并以包括云部署和配置的演练结束。
到本章结束时,您将了解以下方面,从而为继续您的 API 之旅打下良好的基础:
-
影响某些运行时选项和限制的托管考虑因素,这些选项和限制基于您应用程序的需求。
-
您应用程序的某些方面如何影响计费。
-
与特定于 HTTP API 的代码设计相关的结构、一般熟悉度和可能性。
-
从 Visual Studio 部署到 Azure。本书后面将介绍自动构建、持续集成和持续交付。
-
在不重新部署的情况下进行运行时配置更改。
-
如何调整扩展设置以降低公共端点(俗称钱包拒绝攻击)的潜在成本。
技术要求
要在 Visual Studio 中构建 Azure Functions,您需要 Visual Studio 安装器中可用的 Azure 开发工作负载。本章的源代码可在 https://github.com/PacktPublishing/Programming-APIs-with-C-Sharp-and-.NET/tree/main/Chapter06 处获取。
理解 Functions
Azure Functions 在执行环境中扮演着重要角色,同时专注于基于事件的数据处理。传统上,这些环境在硬件或虚拟机(VMs)以及样板代码方面有大量的开销。Functions 是 Microsoft 的执行环境,旨在允许轻松的开发、部署和扩展。虽然有一些底层执行环境可供选择,但它默认为消费模式,这带来了一组适用于大量不同数据处理场景的默认设置,并且按事件执行计费。如果您需要更多的 CPU 核心或内存,其他环境可供选择;这些将在本章后面介绍。虽然 Functions 中的所有执行环境在无服务器意义上都被认为是无需管理单个实例的,但其他非消费环境不是按使用付费,而是根据分配给每个实例的 CPU 核心和内存数量计费。C#是使用的主要语言,尽管支持多种语言和绑定。
这些应用程序执行的主要入口点是触发器。存在各种触发器来响应外部事件。从队列轮询到 ServiceBus 推送,响应 blob 或数据库记录更改,许多现成的绑定允许您混合匹配您的解决方案。对于 API,我们最关心的触发器是 HTTP 触发器。
HTTP 触发器正如其名:对正常 API 请求的请求-响应。这些请求可以来自所有常见的来源:浏览器、webhooks、服务间调用等等。它们可以使用内置的路由模板支持路由到单个函数。来自这些路由的传入数据可以自动匹配到指定的数据类型,反序列化,并绑定。
内置的授权要么是无授权,要么是基于 API 密钥的,后者有两种风味:特定于函数的或单个全局应用程序密钥。其他标准授权可以使用 Entra ID、手动在代码中实现,或者两者结合。
还应该考虑主机,因为您的应用程序需要在某处运行——可能是,但不一定是云中。提供了各种主机和打包选项,允许您根据应用程序的需求定制部署。ZIP 文件、Docker、普通文件复制(xcopy)和本地部署都是可用的。Azure 支持 AMD64 Windows 和 Linux,而 ARM64 在其他场景中得到支持。
到目前为止,您应该对 Azure Functions 运行的环境以及您可用的资源有很好的理解。
主机
有各种主机选项可供选择,以满足几乎任何需求。每个选项都有其优缺点。"消费"是默认选项,将在本章中使用。"灵活消费"(在撰写本文时处于预览阶段)、"高级"(Premium)、"应用程序服务环境"(ASE)和Kubernetes是额外的支持服务,本书将不会讨论。
虽然功能强大,但消费模式也带来了一些限制。首先,实例内存限制为 1.5 GB,这可能会对可以直接在此函数选项内部运行的应用程序类型设置硬限制。第二是实例只有一个核心。第三是超时。Azure 只允许 5 到 10 分钟的超时限制。
然而,许多工作负载都很好地适应了这些限制。从标准的软件即服务(SaaS)、API 和定期更新到 CRUD 和持久函数相关的调用(下一章中解释),大多数这些工作负载都有潜力很好地适应这些限制。
在人工智能工作负载中,需要更多的计算资源可能是常见的,例如。虽然消费模式在概念上容易理解,但高级模式是下一步。你将根据经典虚拟机形式中分配的每个 CPU vCore 和 GB 内存的数量进行计费。这些虚拟机的管理完全委托给 Azure。你可以设置应用程序可以扩展到的实例数量的最大值(和/或最小值),Azure 处理其余部分。当前的限制是每个主机 4 个 vCore 和 14GB 内存。Windows 支持最多 100 个主机,而 Linux 则在 20 到 100 个主机之间。
应用程序打包
根据所需的资源大小,还有一些应用程序打包和部署选项可用。在高级和 App Service 上支持 Docker,允许你精确控制运行时环境。所有运行时都支持 ZIP 文件(包含你整个应用程序的发布存档),在消费模式下是必需的。
历史
注意,尽管大多数运行时托管模式可能仍然可用,但现在它们被认为是过时的。最初,编译的函数是在与函数宿主运行在同一个物理操作系统进程中加载的。这种“进程内”模型允许宿主和自定义函数之间直接调用函数,但引入了与库依赖和语言更新相关的潜在问题。对特定库版本的硬依赖无法更改,如果新的 C#语言版本与现有运行时不兼容,则无法使用。组件加载的怪癖也很常见,解决方法的效果各不相同。
Azure Functions 最初以基于 Azure 的浏览器编辑器启动,尽管它是无服务器的,但该模型仍有改进的空间。后来,一个新版本允许编译标准.NET 组件,以便函数宿主启动物理操作系统进程时可以加载。这引入了依赖项解析冲突,并需要将函数固定到与宿主相同的运行时版本,突显了需要进一步解决方案的需求。
今天,使用进程外托管,上述大多数问题都消失了。你的函数应用程序在自己的操作系统进程中运行,管理自己的启动、依赖注入、语言和.NET 版本。通信通过宿主应用程序和你的函数应用程序之间的内部通道处理。这是.NET 6 和.NET 8 函数推荐的向前发展方式。
虽然了解历史背景对于完整性很重要,但默认设置会引导你直接进入推荐体验。那么,消费计划是如何衡量你的 API 使用的呢?让我们看看。
计费
消耗型计费模型是一个新的度量标准,除非您之前处理过云成本估算,否则您可能不熟悉。每秒千兆字节(GB/s)是消耗型中用于准确计费应用的度量标准,是计费过程的第一个部分。无论是快速响应还是内存密集型多秒响应,这种度量标准允许将其报告为单个数值。例如,一个响应时间为 1 秒的单个请求,使用 1 GB 的内存,将导致计费为 11s1GB=1GB/s。至于相反的情况,四个使用相同 1 GB 内存且响应时间为 250 毫秒的请求也等于 1 GB/s:4250ms1GB=1GB/s。截至编写本文时,Azure 的免费层每月提供 400,000 GB/s 的免费使用量。如果每个请求平均需要 100ms 并使用 256 MB 的内存,那么这将相当于 1600 万个请求。计费的第二个部分更为直接:请求的数量。Azure 每月免费提供 100 万个请求,之后每百万个请求的费用为 0.20 美元。
项目概述
在本节中,我们将开始创建一个新的函数项目,添加一个额外的 HTTP 触发器以及 API 路由,并设置一些几乎所有应用程序都将需要的配置。
启动
创建新的函数项目就像创建任何其他项目一样。Visual Studio 的新项目向导中提供了模板,可以帮助您完成此操作。请按照以下步骤操作:
- 选择Azure Functions或使用functions关键字搜索,然后点击下一步:

图 6.1 – 新项目向导
- 为您的项目命名并点击下一步:

图 6.2 – 项目配置
- 选择运行时和托管配置选项。通常,最新的长期支持选项是一个安全的选择:

图 6.3 – 函数工作器配置
-
选择Http 触发器选项,在授权级别下选择匿名,然后点击创建。
我们将在本书的后面部分介绍身份验证和授权。如前所述,Docker 容器受到支持,但暂时不要勾选此选项。确保选择Azurite选项。这支持各种后端状态管理,也是 Durable Functions 中的“Durable”,这些内容将在下一章中介绍。其他触发器可以在以后添加:

图 6.4 – 额外的函数授权选项
- 可用的触发器种类繁多,您可以根据需要混合搭配。在解决方案资源管理器区域中右键单击项目,然后选择添加 | 新建 Azure 函数:

图 6.5 – 添加新的 Azure 函数
- 您可能需要在列表中搜索 Function,然后点击 Add:

图 6.6 – 新项目对话框
- 应该会显示一个触发器列表。选择 Http trigger,对于 Authorization level 选择 Anonymous,然后点击 Add:

图 6.7 – 新 Azure 函数对话框
注意,模板会随着时间的推移而更新,这个新模板可能与原始函数看起来不同。它还可能添加一些您可能熟悉的新对象,包括 IActionResult 和 OkObjectResult。这些来自 ASP.NET Core 库。如果默认情况下没有引用,请安装最新的 Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore NuGet 包。
有了这些,您已经创建了两个应该可以编译的 HTTP 端点。
选项
大多数应用程序都需要以某种形式提供选项或配置。.NET 提供了内置支持,在应用程序启动时以多种形式(包括自定义形式)传递选项到您的应用程序。这些不同的实例,如环境变量、JSON(appsettings.json)和 XML 文件,以及命令行参数,都是相互叠加的,以便可以根据需要覆盖单个设置。请注意,这与在应用程序运行时更改的设置不同,这些设置在创建 API 时更为高级和具体,因此本书不会涉及。
要添加对这些选项的支持,请按照以下步骤操作:
-
添加一个名为 MyOptions 的新类,将其设置为 public,并添加一个名为 MyReturnValue 的标准 string 属性。我们将使用它来控制函数的返回值。
-
在 MyOptions 类中,将属性值默认设置为任何您喜欢的。我们将在稍后的 appsettings.json 文件中覆盖它,并在本章后面的部署期间更改它。
-
在 appsettings.json 文件中添加一个名为 MyReturnValue 的新属性,并设置一个与代码中默认值不同的值。
现在,我们必须配置应用程序,使其能够在启动时使用配置框架。
-
在 Program.cs 中的 .ConfigureServices 中修改,向 context Lambda 添加一个额外的参数。将调用 .AddOptions
添加到服务变量中,或者将其链接到 .ConfigureFunctionsApplicationInsights 的末尾。在 .AddOptions 后链接着调用 .BindConfiguration,并传入一个空字符串。这将绑定根配置路径中的值到 MyOptions 类中匹配的属性名称。 Program.cs 文件应该看起来像这样:
var host = new HostBuilder() .ConfigureFunctionsWebApplication() .ConfigureServices((context, services) => { services.AddApplicationInsightsTelemetryWorkerService() .ConfigureFunctionsApplicationInsights() .AddOptions<MyOptions>() .BindConfiguration(""); }) .Build(); host.Run();MyOptions.cs 文件应该看起来像这样:
public class MyOptions { public string? MyReturnProperty { get; set; } = "my value in code"; } -
修改Function1.cs文件,使其构造函数中包含IOptions
,然后将其作为类成员保存引用。让OkObjectResult返回_options.Value.MyReturnProperty,这样我们就能看到值的变化。
Function1.cs文件应如下所示:
public class Function1
{
private readonly IOptions<MyOptions> _options;
private readonly ILogger<Function1> _logger;
public Function1(IOptions<MyOptions> options, ILogger<Function1>
logger)
{
_options = options;
_logger = logger;
}
[Function("Function1")]
public IActionResult Run([HttpTrigger(AuthorizationLevel.
Anonymous, "get", "post")] HttpRequest req)
{
_logger.LogInformation("C# HTTP trigger function processed a
request.");
return new OkObjectResult(_options.Value.MyReturnProperty);
}
}
通过运行它来测试。你应该会看到标准输出控制台窗口,其中包含一些包含可见路由的信息性函数消息。在 Windows 上,按住Ctrl键并点击(在浏览器中运行),或者使用任何标准的 HTTP 软件。你应该会看到作为输出的硬编码字符串:

图 6.8 – 浏览器中的预期输出
恭喜!你的函数不仅正在运行,而且已经设置好,可以在部署时更改其值。现在,让我们学习如何将传入的请求路由到不同的代码部分。
路由
你几乎肯定需要在你的函数 API 中使用多个路由,包括路由参数、查询参数等。这构成了你 API 的“形状”或公共契约的一部分。路由支持这个概念。如果你熟悉 ASP.NET Core 路由,那么你会感到非常自在,因为它们也得到了支持,包括约束。
不同的请求如何知道进入你的应用程序的入口点?你应该如何描述允许处理的数据类型?让我们从一个经典的店面产品端点开始:
-
复制Function1.cs并将其重命名为Products.cs,确保在其中包含所有对Function1的引用。
-
在Run方法中添加string category和int id参数。
-
在OkObjectResult中返回字符串而不是返回匿名对象,该对象包含category和id,这样我们就能看到从路由传递过来的值。
Product.cs文件应如下所示:
public class Products
{
private readonly IOptions<MyOptions> _options;
private readonly ILogger<Products> _logger;
public Products(IOptions<MyOptions> options, ILogger<Products>
logger)
{
_options = options;
_logger = logger;
}
[Function(nameof(Products))]
public IActionResult Run([HttpTrigger(AuthorizationLevel.
Anonymous, "get", Route = "products/{category:alpha}/{id:int?}")]
HttpRequest req,
string category, int id = 0)
{
_logger.LogInformation("C# HTTP trigger function processed a
request.");
return new OkObjectResult(new
{
category,
id
});
}
}
注意HttpTrigger属性中的新Route属性,"products/{category:alpha}/{id:int?}"。这允许我们自定义、限制并解析出所需端点的参数。由于这些路由设计可能将成为一个更大的第三方消费者使用的 Web API 的一部分,因此你想要在创建它们时格外小心。你知道产品 ID 必须是数字,但你可能想要限制类别,使其只包含字母。当作为路由模板实现时,它将看起来像我们之前提到的:"products/{category:alpha}/{id:int?}"。这将只匹配具有指定格式的 URI 段的传入请求。问号(?)表示指定的参数是可选的,可以在方法参数中默认设置。
你注意到图 6.8 在没有指定路由时,其 api URL 段被添加了吗?这个前缀从哪里来?默认情况下,api 是函数的默认前缀。这可以在 host.json 文件中的 extensions > http > routePrefix 设置中更改:
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
},
"enableLiveMetricsFilters": true
}
},
"extensions": {
"http": {
"routePrefix": "myapi"
}
}
}
关于部署后覆盖设置的更多详细信息,请参阅 Post-deployment reconfiguration 部分。
现在,让我们创建必要的 Azure 云资源和环境。
部署
首先,我们需要创建一些资源,然后我们才能在 Visual Studio 中查看它们。
Azure 资源创建
让我们部署你的函数到生产环境!为此,创建一个新的 消费 Linux 函数 并按照以下步骤操作:
- 前往
portal.azure.com并创建一个新的资源组,如果你还没有的话:

图 6.9 – 创建新的资源组
将其放置在你地理位置附近的一个 Azure 区域。当我们创建其他资源时,我们将使用相同的区域以最小化延迟和潜在带宽。点击 审查 + 创建 选项卡,然后点击 创建:

图 6.10 – 成功创建新的资源组
- 在此资源组内创建一个新的函数应用:

图 6.11 – 在资源组内创建新的资源
- 如果你没有在默认选择中看到 函数,请搜索它。选择 创建 | 函数应用 以继续:

图 6.12 – 选择函数应用作为要创建的资源
- 接下来,选择 消费 作为你的托管选项:

图 6.13 – 函数托管配置
- 选择 .NET 作为 运行时堆栈,8 (LTS),隔离工作模型 作为 版本,以及 Linux 作为 操作系统。选择你创建资源组时选择的区域,然后点击 下一步:

图 6.14 – 函数详细信息
- 还需要创建一个 存储账户 资源来存储函数运行时所需的各种东西,包括日志、你的部署包,以及可选的 Durable 状态,我们将在下一章中讨论。现在,禁用 Blob 服务诊断设置:

图 6.15 – 存储配置
- 启用 公共访问。然后,选择 否 对于 启用 应用程序洞察:

图 6.16 – 创建应用程序洞察
-
禁用持续部署,这也是第第十章中将要讨论的内容。我们可以使用标签来过滤或分组资源等。如有需要,请添加标签,或者保持此部分为空并点击审查 + 创建。审查您的设置;如果一切看起来都很好,请点击创建。
-
在这一点上,部署过程将继续。点击转到资源;您将被带到您全新的函数!

图 6.17 – Azure 函数的门户概览选项卡
- 点击右侧列出的 URL。您将被带到默认页面,表明您的函数正在运行:

图 6.18 – 默认 Azure 函数登录页面
小贴士
虽然这些步骤和截图在写作时是准确的,但 Azure 经常更改和升级其设计、功能、流程等。建议您查看官方文档以获取最新指南。
现在,函数资源正在使用默认模板运行,让我们从 Visual Studio 进行部署。
从 Visual Studio 发布
让我们手动从 Visual Studio 进行部署。自动化部署,也称为持续交付,将在第十章中讨论:
- 在Visual Studio中,右键单击FunctionsChapter6项目并选择发布…:

图 6.19 – 发布我们的项目
- 然后,选择Azure并点击下一步:

图 6.20 – 发布类型
- 选择Azure Function App (Linux)并点击下一步:

图 6.21 – Azure 发布目标
- 选择创建函数资源时使用的Microsoft 帐户和订阅。通过名称搜索或定位您的函数,选择它,然后点击完成:

图 6.22 – 选择要发布的帐户、订阅和资源
- 点击完成,完成后点击关闭。您的发布配置文件详情将出现,包括其配置和目标运行时,以及它需要的任何依赖项:

图 6.23 – 函数发布配置文件
当您准备好时,点击发布。一旦成功,您可以在 https://<我的资源名称>.azurewebsites.net/api/products/electronics/471337 查看您的新 API 路由。请注意,如果您访问 https://<我的资源名称>.azurewebsites.net/api/Function,您可以看到默认数据。
如本章先前所述,我们现在将探讨在不重新编译函数和不重新部署的情况下更改配置值。
部署后重新配置
现在,让我们将配置更改为不同的值:
- 在 Azure 门户中函数资源的设置部分下的环境变量标签页中,创建一个新的键来覆盖我们之前硬编码的设置。使用MyReturnProperty作为键,因为我们是从根命名空间读取配置变量:

图 6.24 - 添加新的环境变量以覆盖设置
- 应用这个添加,应用更改,并确认保存;你的函数应用将被重新启动。导航到 https://<我的资源名称>.azurewebsites.net/api/Function1,以便您可以看到新的配置值:

图 6.25 – 覆盖的配置值
这可以任意深度嵌套。例如,如果您的MyOptions类的MyReturnValue是一个具有自己的MyOtherReturnValue属性的复杂类型,Azure 中的键路径将是MyReturnValue__MyOtherReturnValue。请注意,属性名之间有两个下划线。
由于这是公开可访问的,如果恶意进程意识到您的新端点,这可能会让您付出金钱。在本书的后面部分将讨论如何使用俗称的简易认证来保护这个新网站。
但与此同时,您可以调整一些托管选项来限制潜在成本。
- 导航到设置 | 扩展,并将最大扩展限制更改为1。然后,点击保存:

图 6.26 – 限制扩展
- 然后,导航到设置 | 配置,将每日使用配额更改为1,然后点击保存,接着继续:

图 6.27 – 添加每日配额
到目前为止,虽然您还没有保护您的网站免受机器人或恶意行为者的攻击,但您至少已经采取了一些措施来减轻任何潜在损害。
摘要
在本章中,我们介绍了在单个 Azure 函数内创建多个 HTTP 端点,添加路由模板以帮助在运行时管理不同的 URI 段。然后,我们将配置选项添加到我们的项目中,将其部署到 Azure,并在部署后调整配置选项。为了限制已部署的实时 HTTP 端点的潜在成本,我们调整了两个设置,直到授权就绪。
现在您已经了解了 Azure 函数中 HTTP 触发器的工作原理,在下一章中,我们将探讨我们可以使用的其他类型的触发器,以构建可靠、有状态和可扩展的工作流程。
你试试看
使用 Azure Functions 编写 API,在 Azure 中创建一个函数资源,然后使用 Visual Studio 将其部署到该资源。
第七章:Azure 可持久化函数
在上一章中,我们讨论了 Azure Functions 中的 HTTP API 如何提供一种替代托管模型,以及解决许多与手动托管相关的传统问题。
您是否需要在任意数量的物理进程或节点(扇出)上大规模并行化数亿个任务,然后等待它们全部完成(扇入)?然后您是否需要应用程序等待人类或其他进程审查并采取行动以继续下一步?如果那个人不可用,您需要将审查过程超时怎么办?这种情况在代码中很容易表达,我们将在本章中探讨这个场景的一个子集。
在本章中,我们将探讨以下内容:
-
在标准 API 的基础上扩展以创建具有弹性和状态的流程
-
与前几章相比,调试这些工作流的不同之处
-
使用标准 HTTP API 与实时系统交互并注入数据
-
简要讨论如何设置其他常见使用模式
技术要求
在 Visual Studio 中构建 Azure 可持久化函数的要求与第六章相同。本章的源代码可在github.com/PacktPublishing/Programming-APIs-with-C-Sharp-and-.NET/tree/main/Chapter07找到。
可持久化函数概述
可持久化函数的名称非常贴切,因为它在底层所做的工作:在发生崩溃、断电、依赖问题等情况时将其状态保存到持久存储中。这不仅允许轻松恢复问题,还允许任务的可扩展性和协调。默认情况下,Azure 函数中的持久性是通过标准存储账户提供的。底层使用表、块和队列:
-
表用于管理函数执行历史,包括参数和返回值
-
块存储用于自动存储传递给活动的较大参数
-
队列用于触发活动和编排实例
SQL Server 和 Netherite 是另外两种支持的可持久化存储选项,本书未涉及,但在高级场景中可以使用。
尽管在分布式系统中没有“一键扩展”按钮,但以下两条具体规则简化了大量平台和依赖相关的开销,使您能够专注于代码本身的逻辑:
-
编排函数必须是确定的
-
活动函数必须是幂等的
编排器函数正是其名称所暗示的:它们编排或控制活动的执行顺序,这些活动通常包含应用程序的大部分工作或逻辑。必须遵循编排器的一个主要限制是它们必须是确定性的,这也意味着它们不能直接进行任何形式的 I/O。所有活动、子编排器或其他等待调用必须保持相同的顺序,以便将执行代码返回到下一个 await 调用之前的精确状态。你必须注意的其他非确定性 API 分为日期时间、GUID、I/O 和其他异步 API 等类别。一般来说,如果你需要从耐久上下文外部获取数据,请使用活动来检索它。
活动函数始终由编排器调用,不能从其他任何地方直接调用。这些地方通常是应用程序逻辑的大部分发生的地方,与编排器相比,活动函数有一个限制:它们必须是幂等的。没有保证活动实例将恰好运行一次,所以请确保连续多次运行具有相同的结果。
将耐久支持添加到现有函数就像添加一个 NuGet 包一样简单。所有函数、触发器等都可以根据你的应用程序要求在单个项目中互操作和共存。
在耐久函数之前,在仅具有基本操作系统平台支持的手动编写情况下,你必须自己管理状态。这包括处理崩溃、标记任务为完成、出错、进行中等。重启也需要管理,以及像并行扇出这样的高级处理。使用耐久函数简化了所有这些要求。
现在,让我们看看如何启动一个耐久实例,以及这些对象如何在有状态系统中协调任务。
编排器
进入耐久系统的“主要入口”是通过一个编排器。这些编排器是在运行函数内部使用 DurableTaskClient 启动的,或者通过稍后将在 编程和调试演练 部分讨论的 HTTP 管理 API 在函数进程外部启动。我之所以说“入口”,是因为我认为从这种角度思考概念上更容易理解。尽管操作系统进程本身仍然使用经典的 Task 或 void Main(...) ,但在耐久系统中,数据处理或消息处理通常从带有 Function 属性的方法开始,以及带有 OrchestrationTrigger 属性的 DurableTaskClient。
可序列化的数据对象,通常以 JSON 形式,可以在创建实例时传递给编排器。这些可以是更大的描述性对象、SAS 令牌到 blob 存储、必须反序列化的二进制数据,或者所有这些的组合。虽然你可以使用标准字符串或JsonNodes(有时你必须这样做),但我首选的方式是使用通用方法重载并创建序列化类来反映系统传递的数据结构。这确保了随着时间的推移修改对象变得容易,将类型检查委托给编译器。这也避免了开发过程中有时出现的问题:缺少或序列化类不匹配,因为这些不会立即显现,因为没有标准的编译错误或运行时异常。同样适用于活动。
由于编排实例是持久的,并且其状态被写入持久存储,这意味着正在运行的编排实例在等待活动完成时不需要保持在 RAM 中。更进一步,它也不需要在相同的操作系统进程、虚拟机或物理机上恢复运行。这可以导致计算和内存资源的非常高效和高效的分配。在极端情况下,你可以有数百或数千个编排实例等待未来任务的完成,而无需使用计算或内存,只需最小的存储空间。
继续深入,你甚至可以有一个无限循环,在等待任务时也不使用任何计算或内存。这个概念,称为永恒编排器,可以响应外部事件,采取行动,并持久地等待下一个事件。但是有一个小的警告:由于编排实例将它们的历史保存到重建它们的状态,永恒编排器可能会有一个不断增长的历史,这最终会导致性能问题。一个名为ContinueAsNew的方法截断了这一历史,从而防止其增长。
对于任何 API 或有状态的流程,你几乎肯定需要处理某些内容,进行网络调用等等。由于我们无法在编排器中这样做,这就是活动出现的地方。
活动
实际包含大部分逻辑的代码片段是活动。它们有一个非常具体的要求:它们必须是幂等的。这意味着如果代码执行多次,它必须没有副作用,因为没有保证特定实例将恰好执行一次。由于各种原因,一些你无法控制的原因,活动可能在执行过程中被终止。这必须在你的活动代码中处理,以便当持久化系统检测到不完整的实例时,从头开始不是问题。
一些你通常不会考虑的内置编程结构,现在对整个系统有深远的影响。例如,Guid.NewGuid()很可能不应该在活动中使用。创建文件、保存数据库记录或使用这个随机 GUID 调用其他 API,如果活动需要从头开始重新启动,现在可能会导致孤立记录。相反,你可以在编排器内部创建新的 GUID,并将其与其对象参数一起传递给活动。传递给编排器的TaskOrchestrationContext实例有一个名为NewGuid的特定方法,以方便这种确切的需求。
编程和调试指南
使用现有的第六章函数代码,让我们为它添加对 Durable 的支持。右键单击项目,选择添加 | 新建 Azure 函数,就像你为第一个 HTTP 端点所做的那样。给它起个名字,然后选择Durable 函数编排。

图 7.1 – 添加新的 Durable 函数编排
你可以从提供的模板中看到,针对我们之前讨论的每个概念都创建了三个静态方法:一个带有HttpTrigger的常规函数,它安排一个新的编排实例运行,然后调用一个或多个活动。Visual Studio 应该已经自动添加了对Microsoft.Azure.Functions.Worker.Extensions.DurableTask的引用,但如果没有,请通过 NuGet 添加。
注意
注意,这些方法都是静态的。虽然这确实可行,但它留下了很多遗憾,因为它阻止了上一章讨论的具有影响力的概念,如选项、依赖注入和更容易的测试。Durable Functions 还支持将代码迁移到上一章中提到的非静态样式。这项练习留给你自己完成。当我们提到正在工作的编排器和活动时,我们使用“the”这个词。这可能会显得有些不合适,因为我们正在引用单个静态方法,而不是一个你可以直观看到的对象或事物。相同的静态方法也可能在不同的线程、进程和虚拟机上同时执行,并使用不同的数据。如果你决定进行代码迁移,具有常规构造函数和非静态方法的单个文件可能有助于你从概念上可视化它们。
我们将专注于将一些数据保存到文件中,并等待一个事件,然后该事件将再次更新文件。让我们首先删除SayHello方法的内容,该方法包含一个带有ActivityTrigger的字符串参数name。这是您从 orchestrator 传递给活动的唯一参数。虽然这可以是一个字符串,但通常它是一种数据传输或普通的 C#类(POCO)对象,它必须是 JSON 可序列化的。FunctionContext executionContext参数可以用来发现有关正在运行的活动等信息。将返回类型改为Task
SayHello方法应该类似于以下内容:
[Function(nameof(SayHello))]
public static async Task<string> SayHello([ActivityTrigger] string contents, FunctionContext executionContext)
{
await File.WriteAllTextAsync(“myfile.txt”, contents);
return default;
}
在RunOrchestrator方法中,删除内容并添加一个对context.CallActivityAsync的 awaited 调用,传递活动的名称,在我们的例子中是SayHello。将返回类型改为Task


浙公网安备 33010602011771号