ASP-NET-Core-9-0-精要-全-
ASP.NET Core 9.0 精要(全)
原文:
zh.annas-archive.org/md5/8673f50c26a54e67a648f422eb499de1译者:飞龙
前言
你是否曾停下来思考目前存在多少用于开发软件解决方案的技术、语言和框架?由于技术的进步,每天都有新的框架和技术出现,因此当然几乎不可能列出它们。与开源运动提出的协作模式相关联,可以访问主要玩家的技术和源代码,这允许人们开发自己的技术,最终成为开发解决方案的途径。
在网络应用背景下,已经出现了许多发展,随之而来的是不同的方法和工具的涌现,这些方法和工具使得开发越来越丰富和复杂的应用成为可能。通常,传统的客户端-服务器开发模型在某些情况下已经不再足够,随着技术的演变,采用其他方法来提升网络应用的用户体验使得 JavaScript 框架变得流行,从而催生了其他开发模型,例如使用单页应用(SPAs)。结合强大的服务器端处理模型,这使得网络应用比以往任何时候都更加互动和丰富。当我们回顾几年前的开发模型时,我们需要几个 JavaScript 文件、一种解释型语言,如 PHP、Perl 或经典 ASP 来处理服务器端的请求,以及一个数据库来持久化信息。
然而,市场变得更加动态和有需求,使得技术进步的速度越来越快。网络解决方案的开发模型已经从三四种技术的堆叠转变为多种资源、包、框架、标准、设计模式和,同样重要的是,与云环境的交互。虽然我们有一个现代的开发场景,但我们也面临着可用资源的巨大复杂性,以便我们可以创建具有支持市场多样化需求的能力的丰富解决方案,并动态地跟随组织需求的不断演变和变化,以提供越来越符合市场的解决方案,保持竞争力。
处理不同的技术也是许多团队的挑战,这些团队必须学习不同的语言和标准,并使用不同的工具来组合这些技术,以维持可持续的分布式开发模式。本书旨在为需要以动态方式交付网络解决方案、提供持续价值、并从最佳实践中受益、在持续技术演变的场景中保持最新的软件工程师提供工具、标准和可能性。为了实现这些目标,我们将学习 ASP.NET Core 9 平台,这是一个来自微软的强大开源解决方案,使我们能够开发高质量的应用程序,准备好处理云原生应用程序的标准和要求,最好的是,这些都在一个单一平台上集中。
ASP.NET Core 9 是一种不断进化的技术,它具有现代特性,使软件工程师能够在使用高级方法解决必须在不同类型环境中运行的问题时,拥有扩展和定制的功能。使用 ASP.NET Core 9,软件工程师可以受益于使用与 CLI 工具交互等实践。它是独立于操作系统的,得到了提供多个包和提供者的技术社区的广泛支持,并且框架通过开源社区不断进化。它是一种具有广泛文档和支持的技术,为最多样化的解决方案做好了准备,除了适应云原生模型外,当然还有能力与其他技术以及 JavaScript 框架集成。
ASP.NET Core 9 是一个强大的网络开发平台,提供了在不同操作系统上开发的各种工具。然而,理解超越编码过程的概念同样重要。
在本书的整个过程中,我们将学习 ASP.NET Core 9 平台的概念和基础知识,了解开发方法和云架构,学习设计网络解决方案的最佳资源,使用诸如 持续集成 ( CI ) 和 持续交付 ( CD ) 等机制以自动化方式提供持续价值,以及其他云原生解决方案开发模型的最佳实践和更多内容。
本书面向对象
本书提供了对使用 ASP.NET Core 9 技术开发基于 Web 解决方案广泛视角。它超越了传统方面,基于技术市场的需求带来了创新视角。它涵盖了从平台知识、更新、环境准备、实施和最佳安全及开发实践的使用,到通过 CI/CD、云原生开发实践、容器化等自动化解决方案的持续交付,以及 ASP.NET Core 平台的其他几个方面。
本书面向参与后端和前端解决方案开发的开发者,他们熟悉基本的或中级面向对象编程,使用 C# 和 Java 等高级语言,并有一定 HTML 和 CSS 的经验。
本书涵盖的内容
第一章 ,介绍 ASP.NET Core 9 概念,从 ASP.NET Core 9 的基础知识开始,了解 .NET 平台的演变,它从仅适用于 Windows 的开发模型转变为成为一个开源平台,由微软和整个技术社区共同支持并不断更新。我们还将学习如何通过安装开发工具和创建应用程序所需的软件开发工具包(SDK)来准备开发环境。
第二章 ,使用 Razor Pages、MVC 和 Blazor 构建动态 UI,涵盖了涉及 Web 应用程序开发的各个方面,如客户端-服务器和服务器端模型,以及了解和实现使用 ASP.NET Core 9 UI 框架(如 Razor Pages、ASP.NET MVC、Blazor)的应用程序,以及与 JavaScript 框架的集成。
第三章 ,构建用于服务交付的 Web API,讨论了在 API 技术开发中的概念和最佳实践,这些技术在基于 Web 的应用程序中广泛使用。我们将了解通过 HTTP 实现的业务交付模型作为服务,并理解 ASP.NET Core 9 平台中的基本和标准,例如最小 API、过滤器、文档以及其他与基于 REST 的服务开发模型相关的标准。
第四章 ,使用 SignalR 进行实时交互,通过使用 SignalR 的实时应用程序概念提供了一个丰富的用户交互解决方案,我们将学习使用 ASP.NET Core 9 进行实时编程的基础知识,支持的技术,处理流,并了解如何托管实时解决方案。
第五章 ,与数据和持久性一起工作,探讨了大多数应用程序中非常重要且必要的方面,即连接到数据源以实现信息持久化的能力。我们将了解 ASP.NET Core 9 如何通过使用如 Entity Framework Core 和 Dapper 等框架,允许我们使用高级持久化模型连接到数据库,以及了解主要持久化模型和现有技术。
第六章 ,增强安全和质量,涵盖了现代基于 Web 的应用程序中最敏感和重要的方面之一,即安全性。我们将了解保护应用程序的原则,消除授权和认证概念的神秘性,并通过 ASP.NET Core Identity 实现访问管理模型。
第七章 ,为应用程序添加功能,旨在扩展应用程序源代码的上下文,并添加基本和最佳实践,以及现代应用程序中必要的其他资源交互,例如使用缓存策略使应用程序更具弹性。我们还将探索日志记录、跟踪和监控解决方案模型,这对于不仅支持用户,还允许团队优化、解决问题和采取主动行动的应用程序至关重要。
第八章 ,使用 ASP.NET Core 9 中的中间件增强应用程序,探讨了 ASP.NET Core 中的一项强大功能,即通过中间件控制应用程序的请求和响应流。通过中间件,我们能够使用最佳实现实践扩展 Web 应用程序请求流中的功能。我们将了解管道的工作原理,如何添加中间件,以及如何创建自定义中间件。
第九章 ,管理应用程序设置,更详细地阐述了与应用程序中敏感信息安全相关的一些方面。所有解决方案都依赖于一种参数化类型,并且作为良好实践,这些通常通过配置文件进行管理。通过 ASP.NET Core 9 提出的模型,为云环境准备,我们将了解如何以安全的方式连接不同的配置管理提供程序,并使用IConfiguration配置抽象接口的最佳开发实践。我们还将学习如何通过使用功能开关和选项模式,实时更改应用程序配置和行为。
第十章 ,部署和托管应用程序,旨在向您介绍涉及开发流程和持续价值交付的其他方面。现代应用程序需要动态且不断变化,并且必须在任何时间交付,甚至在用户使用它们的时候。因此,我们将学习使用与 DevOps 文化原则相关的自动化流程,如 CI 和 CD。我们将了解在连接到 Azure 的管道中打包和发布应用程序的过程。
第十一章 ,使用 ASP.NET Core 9 进行云原生开发,将向您介绍将 ASP.NET Core 9 开发的应用程序提升到另一个层次的最佳实践、工具和原则,以适应动态和现代的基于网络的解决方案开发模式。在本章中,我们将学习超越代码并培养云原生思维的重要性。我们还将学习最佳实践和原则,如十二要素应用、容器和部署策略,以及支持软件工程师为云设计稳健解决方案的资源。
为了充分利用这本书
本书采用理论/实践方法,为了充分利用现有知识,了解以下内容非常重要:
-
任何技术的软件开发
-
面向对象编程,基础或中级水平
-
高级语言,如 C# 和 Java
-
基础级别的 HTML 和 CSS
此外,本书中将要使用的一些工具总结如下表:
| 本书涵盖的软件/硬件 | 操作系统要求 |
|---|---|
| Visual Studio Code/Visual Studio | Windows, macOS, 或 Linux |
| Docker | Windows, macOS, 或 Linux |
| GitHub(需要账户) | SaaS 平台(github.com) |
| Git | Windows, macOS, 或 Linux |
| Postman | Windows, macOS, 或 Linux |
| Azure | 微软云平台(azure.microsoft.com) |
在各章节中,我们将与前面表格中提到的每个技术和工具一起工作,以简单的方式提供使用和配置的详细信息。
如果您正在使用本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将帮助您避免与代码复制和粘贴相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件,网址为 github.com/PacktPublishing/ASP.NET-Core-9.0-Essentials 。如果代码有更新,它将在 GitHub 仓库中更新。
我们还有其他来自我们丰富的书籍和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!
使用的约定
本书使用了多种文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“请求委托使用Run、Map和Use扩展方法配置,通常在Program.cs文件中配置。”
代码块设置如下:
string key1 = _configuration.GetValue<string>
("MyCustomSetting:Key1");
string key2 = _configuration.GetValue<string>
("MyCustomSetting:Key2");
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
var builder = WebApplication.CreateBuilder(args);
// Add configuration from environment variables
builder.Configuration.AddEnvironmentVariables();
// Add configuration from command-line arguments
builder.Configuration.AddCommandLine(args);
var app = builder.Build();
app.Run();
任何命令行输入或输出都按以下方式编写:
dotnet add package Microsoft.Azure.AppConfiguration.AspNetCore
粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会以粗体显示。以下是一个示例:“在下一屏上,点击+创建选项以添加新资源。”
小贴士或重要注意事项
看起来是这样的。
联系我们
欢迎读者反馈。
一般反馈:如果您对本书的任何方面有疑问,请通过 mailto:customercare@packtpub.com 给我们发邮件,并在邮件主题中提及书名。
勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在此书中发现错误,我们将不胜感激,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata并填写表格。
盗版:如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 mailto:copyright@packt.com 与我们联系,并提供材料的链接。
如果您想成为作者:如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享您的想法
一旦您阅读了ASP.NET Core 9.0 Essentials,我们很乐意听听您的想法!请点击此处直接访问此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。
下载此书的免费 PDF 副本
感谢您购买此书!
您喜欢在路上阅读,但无法携带您的印刷书籍到处走吗?
您的电子书购买是否与您选择的设备不兼容?
别担心,现在每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠不仅限于此,您还可以获得独家折扣、时事通讯和每日免费内容的每日电子邮件访问权限
按照以下简单步骤获取好处:
- 扫描下面的二维码或访问以下链接

packt.link/free-ebook/9781835469064
-
提交您的购买证明
-
就这些!我们将直接将免费 PDF 和其他好处发送到您的电子邮件中
第一部分:ASP.NET Core 9 基础知识
在本第一部分中,我们将重点关注 ASP.NET Core 9 平台的基础知识,了解平台原理和版本中的新功能。我们还将使用开发工具如 Visual Studio Code 和 Visual Studio,以及包含实现解决方案所需的包和 CLI 工具的 软件开发工具包(SDK)来准备开发环境。本第一部分获得的所有知识对于读者能够更清晰地学习本书其余部分涵盖的其他主题至关重要。
本部分包含以下章节:
-
第一章 ,介绍 ASP.NET Core 9 概念
-
第二章 ,使用 Razor Pages、MVC 和 Blazor 构建动态 UI
-
第三章 ,构建用于服务交付的 Web API
-
第四章 ,使用 SignalR 进行实时交互
第一章:介绍 ASP.NET Core 9 概念
在开始使用 ASP.NET Core 9 平台开发解决方案之前,我们必须学习基础知识以及如何准备环境,还要熟悉开发过程中将使用的主要概念。
在本章中,我们将学习如何准备我们的开发环境,了解 .NET 和 .NET Framework 之间的差异,并查看 ASP.NET Core 9 中的新特性。
本章我们将涵盖以下主题:
-
为什么选择 ASP.NET Core 9?
-
比较 .NET 和 .NET Framework
-
准备我们的开发环境
-
ASP.NET Core 9 中的新特性
我的目的是向您介绍 ASP.NET Core 9 的主要概念以及如何使用这个强大的平台来交付基于 Web 的应用程序。我将解释平台的基础知识,为您提供有关 .NET 和 .NET Framework 之间的差异以及如何准备自己的 Windows、Mac 或 Linux 环境的背景信息,您将在本书的结尾之前一直使用这个环境。此外,我们将学习这个平台上最重要的改进,这些改进在几年中增加了新功能。让我们开始学习 ASP.NET Core 9 的基础知识。
技术要求
为了充分利用本书中将要分享的所有知识,重要的是您能够访问一台具有管理员权限的计算机,以及互联网访问权限。
其他必要的软件将在本章及本书的其余部分中根据需要分享。
您可以在以下存储库中找到本书中使用的所有代码示例和其他材料:github.com/PacktPublishing/ASP.NET-Core-9.0-Essentials。
为什么选择 ASP.NET Core 9?
ASP.NET core 是一个自 2016 年以来就存在的平台,它一直在不断改进,允许开发高性能、现代和云就绪的解决方案。
几年前,仅能在 Windows 操作系统上使用 .NET 平台开发解决方案。然而,随着巨大的市场需求和技术快速演进,微软开始了一项单向的重构和平台重新设计过程,采用开源模式,这为开发者社区提供了采用稳健的开发模型的机会,该模型独立于操作系统。
最后一个仅能在 Windows 操作系统上运行的 ASP.NET 的重要版本是 4.x,经过重新设计后,该平台被更名为 ASP.NET Core,目前处于 9 STS(标准长期支持)版本。
最近,ASP.NET Core 9 已经成为一个极其丰富的平台,能够提供针对不同类型目的的解决方案,更重要的是,它得到了开源社区的全面支持和关注。
使用 ASP.NET Core 9 为我们提供了一套丰富的工具,具有以下优势:
-
开发 Web UI 解决方案的能力
-
开发 Web API 的能力
-
操作系统的互操作性
-
云就绪性
-
高性能
-
与现代客户端框架的集成
-
使用最佳实践和设计标准
它是一个完整的平台,统一了使用最佳实践、技术和其他方面开发丰富解决方案所需的一切,这些内容你将在本书的章节中了解到。
性能改进
ASP.NET Core 9 相比 ASP.NET Core 7 有几个重要的性能改进,使其成为迄今为止性能最好的版本。以下是一些重要的改进点:
- 更快的执行和启动:ASP.NET Core 9 比之前的版本更快。每个版本都收到了来自技术社区的许多贡献。负责应用程序执行的运行时有一些改进,例如循环优化、许多其他代码生成和垃圾回收器的改进。
- 最小 API 性能:根据基准测试,新版本的最小 API 比上一个版本快 15%,内存消耗减少了 93%。
- 本地 AOT (提前编译): ASP.NET Core 9 对本地 AOT 的支持得到了扩展,允许应用程序编译成本地代码,从而减少磁盘占用,提高启动时间,并降低内存消耗,这对于云原生环境来说是非常理想的。
- ML.NET:ML.NET 4.0 版本的重要改进使得与机器学习模型的集成成为可能,增加了对现代 IA 模型必要的分词器支持。
- .NET Aspire:.Net Aspire 在上一个版本 .Net 9 中引入,为在 ASP.NET Core 9 中构建可观察的、生产就绪的、分布式应用程序提供了一个改进的云就绪堆栈。在开发阶段与云原生方法相关的许多问题都通过 .NET Aspire 被抽象化,结合了几个 Nuget 包和项目模板。
- .NET MAUI:.Net MAUI(多平台应用程序 UI)提供了一种统一的方式来开发针对 Web 和移动平台(包括 iOS 和 Android)的应用程序。.NET MAUI 有质量改进,如测试覆盖率、端到端场景测试和错误修复。现在,作为 ASP.NET Core 9 的项目模板的一部分,有一个混合项目,其中包括与 Blazor 集成的 MAUI。通过这个项目,软件工程师能够交付不仅适用于 Web,也适用于移动和 Windows 的应用程序。
- Entity Framework Core:Entity Framework Core 是 .Net 中最强大的功能之一,提供了一种使用 ORM(对象关系模型)的方法来抽象应用程序和数据库之间的通信。在新版本中,添加了一些更多功能和改进,如 Azure Cosmos DB NoSQL 提供程序和 AOT(提前编译)工作能力。
- ASP.NET Core:整个 ASP.NET Core 平台在 Blazor、SignalR、最小 API、身份验证和授权以及更好的 OpenAPI 支持方面有许多改进。
- Swagger: 生成 API 文档最著名的库之一,Swagger,来自Nuget包Swashbuckle.AspNetCore的 Swagger 将不再成为 ASP.NET Core 9 的默认 API 模板的一部分。这是由于在.NET 项目中减少对这个库的依赖,并提高对 Open API 的支持,Open API 是一种与语言无关且平台中立的基于 Web 的 API。
更多详情,请查看以下链接:devblogs.microsoft.com/dotnet/performance-improvements-in-net-9/。
然而,在开始准备我们的环境并使用 ASP.NET Core 9 之前,在下一节中,让我们通过比较.NET 平台和.NET Framework,来了解平台是如何在微软和开源社区的合作下不断演进的。
比较.NET 和.NET Framework
.NET 和.NET Framework(通常称为完整框架)有相似之处——那就是它们是允许我们提供优秀解决方案的平台。
通常,平台是一个框架,它包含一系列功能,使我们能够开发不同类型的应用程序。2002 年 2 月,.NET Framework 引入了一种新的开发模型,使用集中式平台,允许我们通过 Windows Forms、ASP.NET Web Forms、ASP.NET MVC 等技术来开发 Windows 和 Web 应用程序,以及其他一些扩展。
从最初版本开始,就已经可以在不同的语言中开发应用程序,例如 C#、Visual Basic,以及任何实现了.NET Framework 规范的其它语言。然而,它依赖于 Windows 操作系统及其系统 API。
.NET Core 平台的演变和重新设计为微软生态系统带来了许多好处,同时保持了作为一个统一平台开发强大解决方案的主要思想。可以使用 C#、F#,甚至 Visual Basic 等语言进行开发。
结构化和重新设计意味着.NET 核心平台,现在称为.NET,是以模块化方式开发的,并且独立于操作系统,得到了开源社区的支持。
现在,整个.NET 平台生态系统由.NET 基金会(dotnetfoundation.org/)维护,这是一个非营利性、独立组织,它支持整个开源平台生态系统。因此,为.NET 社区创造了新的可能性,包括减少框架新版本交付的领先时间,这些新版本包含新功能和错误修复。
.NET 平台的新版本每年发布一次,每年 11 月,以 STS(标准长期支持)版本发布,在偶数年发布并支持 18 个月,或者 LTS(长期支持),在奇数年发布并支持三年。还有每月的补丁更新,这些更新可以加快问题的敏捷修正和漏洞的修复,保持每个补丁之间的兼容性,并消除更新带来的更大风险。
了解平台更新过程可以为开发时间带来巨大好处,因为更新可能会引起应用程序的非一致性,从而产生几个问题。
微软提供了一个完整的路线图,其中包括添加到平台中的功能,以及改进的实施,最重要的是,修复了错误和漏洞。请将路线图链接保存为浏览器中的收藏夹:github.com/MoienTajik/AspNetCore-Developer-Roadmap 。
现在我们已经了解了一些平台的基本知识,让我们在不同的操作系统上准备我们的开发环境。
准备我们的开发环境
.NET 平台提供了一套工具,无论使用哪种操作系统,都可以为开发者提供最佳体验。
ASP.NET Core 9 应用程序可以在 Windows、Linux 和 macOS 操作系统上开发和运行。
本书将展示代码片段,以通过实际示例演示 ASP.NET Core 9 的概念。所有支持材料都可以在 GitHub 仓库中找到,该链接可在 技术要求 部分找到:github.com/MoienTajik/AspNetCore-Developer-Roadmap 。
在本节中,我们将配置三个操作系统上的环境,并创建我们的第一个 ASP.NET Core 9 项目,但首先,让我们看看我们需要准备哪些东西才能开始。
开发工具
我们可以使用任何文本编辑器开发 ASP.NET Core 9 应用程序,然后使用 SDK(软件开发工具包)编译开发代码,这将在下一节中讨论。
微软提供了两个代码编辑工具,Visual Studio 和 Visual Studio Code。
Visual Studio Code 是一个丰富、可扩展且轻量级的代码编辑器,使得开发任何类型的应用程序成为可能。这是一个免费工具,有多个扩展,被社区广泛使用,并且可以在任何操作系统上运行。
相反,Visual Studio 是 IDE 的更强大版本,除了支持开发的一些视觉功能外,还包含应用程序分析等工具,以及丰富的调试工具。Visual Studio 仅在 Windows 操作系统上运行,并且必须购买许可证。然而,Microsoft 提供了一种名为 Visual Studio Community 的免费版本,尽管有一些限制,但提供了出色的开发体验。在本书的剩余部分,我们将使用 Visual Studio Code 作为主要的代码编辑器,因为它可扩展,最重要的是,它是免费的。
Visual Studio for Mac
Microsoft 将不会继续开发 Visual Studio for Mac,其支持将于 2024 年 8 月 31 日结束。
SDK 和运行时
在您的机器上安装.NET 平台时,可能会出现有关 SDK 和运行时的一些问题。
SDK 使我们能够开发和运行 ASP.NET Core 9 应用程序,而运行时仅包含运行应用程序所需的依赖项。
通常,我们总是选择在开发机器上使用 SDK;然而,在托管环境中,只需要运行时。我们将在第十章中更详细地讨论托管应用程序。
CLI(命令行界面)
除了.NET 和/或 Visual Studio Code 之外,还会安装一个广泛使用的CLI,本书中将大量使用。
CLI(命令行界面)不过是通过命令行执行的软件,允许您执行不同目的的任务,例如以下命令:
dotnet new webapp --name hello-world
此处的 CLI 命令称为dotnet。此命令有一些参数,用于确定将要执行的任务类型。
简而言之,之前的命令创建了一个名为(new)webapp的新项目,名称为(-- name)hello-world。
CLI 工具提供了极大的灵活性,避免了 UI 依赖,可扩展,并允许我们通过脚本使用自动化策略。
在本书中,我们将使用一些 CLI 命令来支持解决方案开发和学习。
在接下来的几节中,我们将探讨在所有三种操作系统上安装 ASP.NET Core 9 SDKs。
Windows 安装
Windows 为安装.NET 平台 SDK 提供了以下选项:
-
与 Visual Studio 一起安装。
-
使用 Windows 包管理器Winget。可以通过运行以下命令安装 SDK:
winget install -e --id Microsoft.DotNet.SDK.9 -
通过 PowerShell。
然而,我们将通过 Visual Studio 进行安装,但如果您希望使用其他安装选项,请查看此链接:learn.microsoft.com/en-us/dotnet/core/install/windows。
通过 Visual Studio 的安装非常简单,只需几个步骤:
-
下载 Windows 版本的 Visual Studio 并保存。下载后,运行以下文件:VisualStudioSetup.exe。
-
安装后,找到 VisualStudioSetup.exe 文件,运行它,然后点击 继续。

图 1.1 – Visual Studio 安装程序消息
- 在 工作负载 选项卡上,选择 ASP.NET 和 Web 开发 选项:

图 1.2 – Visual Studio 安装选项
- 然后,点击安装按钮并继续安装。
macOS 安装
macOS 提供了一个可执行文件,允许您遵循简单的安装过程:
-
从
dotnet.microsoft.com/download/dotnet下载 .NET 平台。 -
选择最适合您处理器的版本(ARM64 或 x64)
-
下载后,运行安装程序并完成步骤以完成安装。
支持的版本
微软不支持 6 版本之前的 .NET。从版本 6(LTS)开始,支持当前苹果处理器。
关于在 macOS 上安装的更多详细信息,请参阅此链接:https://learn.microsoft.com/en-us/dotnet/core/install/macos。
Linux 安装
.NET 支持多个 Linux 版本。对于下一步,我们将关注 Ubuntu 22.04 版本,并将根据微软提供的脚本运行此过程:
-
访问命令行终端,并在您的家目录中创建一个名为 dotnet-install 的文件夹。
-
使用以下命令下载微软提供的 sh 脚本:
wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh -
在运行脚本之前,您需要使用以下命令添加权限:
chmod +x ./dotnet-install.sh -
现在,运行安装命令:
./dotnet-install.sh --version latest -
此命令将安装最新的 SDK 版本。
依赖项
.NET 平台依赖于一些特定于每个 Linux 版本的库。有关更多详细信息,请参阅以下链接:https://learn.microsoft.com/en-us/dotnet/core/install/linux-ubuntu#dependencies。
接下来,让我们安装 Visual Studio Code。
Visual Studio Code
Visual Studio Code 是一个优秀的编辑器,其安装非常简单。
只需访问 code.visualstudio.com/download ,下载适用于您操作系统的版本,运行安装程序,并完成步骤。
代码命令
安装完成后,Visual Studio Code 也会安装其 CLI。在 Windows 和 Linux 系统上,CLI 通常会自动添加到系统 PATH 环境变量 中。在 macOS 上,需要额外的配置。为此,请按照以下步骤操作:
-
按下 CMD + Shift + P 键。
-
输入以下命令:
Install 'code' command in PATH -
按 Enter 键,CLI 将被添加到 Path 环境变量中。
现在是验证环境功能的时候了,为此,我们将创建我们的第一个项目。
测试开发环境
在安装了编辑器、代码和 SDK 之后,是时候创建我们的第一个应用程序并确保环境在整本书中都能正常工作了。
在此步骤中,我们将使用终端或 Bash、dotnet CLI 以及 Visual Studio Code 作为 IDE。我们将使用命令行创建一个简单的 Web 应用程序。
要这样做,打开命令终端或 bash 并遵循以下说明:
-
在您喜欢的目录下,创建一个名为 Projects 的文件夹:
mkdir Projects -
使用以下命令访问创建的文件夹:
cd projects -
现在,运行以下命令:
dotnet newdotnet new 命令需要一些指令,以便我们继续创建项目。运行它时,会显示以下指令:

图 1.3 – 执行 dotnet new 命令
- 与 Visual Studio 类似,.NET CLI 在创建项目时有一些模板。您可以通过输入高亮显示的命令来查看已安装在您机器上的模板:

图 1.4 – 可用的 dotnet 模板
-
我们将继续在 MVC 模板中创建一个 Web 应用程序。为此,请运行以下命令:
dotnet new mvc --name my-first-app之前的命令基本上是创建项目类型的模板。在这种情况下,我们将使用 MVC 模型,这是一个使用 模型-视图-控制器 ( MVC ) 架构模式的程序。在此阶段,不必担心这些细节。我们将在整本书中学习更多关于 .NET CLI 工具、项目模型和 MVC 模型的知识。
-
现在,使用以下命令访问创建的应用程序的目录:
cd my-first-app -
接下来,让我们使用以下命令打开 Visual Studio Code,对应用程序的源代码进行一些修改:
code . -
之前的命令将在之前创建的应用程序目录中打开一个新的 Visual Studio Code 实例。

图 1.5 – 在 Visual Studio Code 中打开的第一个应用程序
-
在 Visual Studio Code 中,定位到 Views | Home 文件夹中的 index.cshtml 文件。
-
将 index.cshtml 文件的第 6 行替换为以下代码,然后选择 文件 | 保存,或者直接按 Ctrl + S:
<h1 class="display-4"> Welcome to the ASP.NET Core 9! </h1> -
返回终端或 bash 并运行以下命令:
dotnet run

图 1.6 – 运行 dotnet run 命令
如果您看到如图 1.6 所示的消息,则表示您的环境配置正确。
-
现在,访问浏览器并输入显示的地址,例如 http://localhost:5034。
注意您的终端地址,因为应用程序执行端口可能不同。

图 1.7 – 应用程序正在运行
如果所有之前的步骤都执行成功,这意味着您的代码编辑器配置正确,同样 .NET SDK 也配置正确。
如果有任何问题,请根据您的操作系统回顾安装步骤。
在我们的环境配置完成后,我们准备继续学习该平台并实现书中提出的示例。
ASP.NET Core 9 中有哪些新特性?
由于它是开源的,并且不依赖于任何操作系统,因此 .NET 平台在近年来收到了几项改进,进一步提升了开发者的体验,并不断带来社区不断请求的几个新特性,此外,还有性能提升和错误修复。
一些改进如下:
-
原生 AOT:此选项是在框架的 7.0 版本中引入的,用于创建一个自包含的应用程序,在一个文件中,增加了对 macOS 的 x64 处理器和 ARM64 架构的支持,并在 Linux 环境中大大减少了文件大小,达到高达 50% 的减少。
-
序列化库的改进:应用程序不断与 JSON 数据交互,并且 .NET 本身的 System.Text.Json API 一直在不断修订和改进,避免了依赖第三方库,并显著提高了其性能和支持。
-
性能:性能提升了大约 15%。您可以点击此链接查看更多性能改进详情:
learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-8.0?view=aspnetcore-8.0。
这些以及其他许多特性可以直接从以下链接的平台路线图中进行咨询:github.com/dotnet/aspnetcore/issues/44984。
.NET 路线图由社区、微软和 .NET 基金会维护和更新得非常好。正是这种来自社区的巨大支持,以及其他参与的公司和组织,使得 .NET 成为了不同目的的强大开发平台。通过满足市场的其他需求,它显著提高了基于 Web 的解决方案的开发选项,使用不同的方法来满足不同的需求。
摘要
在本章中,我们学习了 .NET 平台和 ASP.NET Core 9,它们可在主要操作系统上使用。我们学习了 .NET 和 .NET Framework 之间的区别,以及学习框架版本更新过程和 STS 与 LTS 版本之间的区别。我们还配置了不同操作系统上的开发环境,并开发了一个 ASP.NET MVC 应用程序,验证了整个工作环境。
在第二章中,我们将使用已经配置好的环境来学习在 ASP.NET Core 9 中开发 UI 的不同方法和选项。
第二章:使用 Razor Pages、MVC 和 Blazor 构建动态 UI
ASP.NET Core 9 提供了一个完整的 UI 框架,适用于不同类型的方法和应用程序,允许在客户端和服务器端使用页面渲染策略。在本章中,我们将了解 ASP.NET Core UI 框架中可用的选项,以及了解如何为每个场景定义最佳选项。
最初,我们将了解一些重要概念,然后继续以实际的方法进行,以便我们能够练习所学的概念。
在这种方法中,我们最初将了解 ASP.NET Core UI 框架是什么,学习在服务器上使用 Razor Pages 和 ASP.NET MVC 以及客户端使用 JavaScript 框架渲染应用程序的不同方法。最后,我们将了解将不同技术合并到混合解决方案中的力量,该解决方案结合了客户端和服务器两方面的最佳之处。
在本章中,我们将涵盖以下主要主题:
-
了解 ASP.NET Core UI
-
使用 Razor Pages 和 ASP.NET MVC 实现 UI 服务器渲染模型
-
探索使用 Blazor 和 JavaScript 框架进行 UI 客户端渲染
-
与混合解决方案一起工作
技术要求
您可以在以下存储库中找到本章中使用的所有代码示例和其他材料:github.com/PacktPublishing/ASP.NET-Core-9.0-Essentials
了解 ASP.NET Core UI
动态的基于网络的程序具有一种基本涉及两个不同“世界”的流程,即前端和后端。一般来说,前端处理的是可视化的机制,允许用户与解决方案提出的各种功能进行交互。因此,前端由按钮、文本、列表、菜单、图像和其他方面组成,共同构成了用户界面(UI)。后端是允许前端根据用户交互实现动态性的机制的表示。我们将在第三章中进一步讨论与后端相关的方面。
前一段落中表达的所有术语在不同的上下文中都有不同的作用。前端通常在客户端运行,这转化为用户的浏览器。客户端是一个通用术语,可以表达其他类型的用户交互。但在这个情况下,我们将讨论客户端为您的选择浏览器。后端在服务器上运行,无论是在数据中心还是在像 Azure 这样的云服务提供商上。
通过 UI 中的用户交互,用户必须与后端通信,随后能够适应服务器的响应,以提供某种类型的响应和交互,再次,与用户进行交互。
浏览器基本上使用三种技术:HTML、CSS 和 JavaScript。HTML 是静态的;也就是说,它被浏览器解释,然后以 UI 的形式渲染。CSS 负责使视觉元素更具吸引力,定义颜色、阴影和格式等设计方面。JavaScript 用于使静态元素动态化,这包括例如调用服务器,并根据结果修改用户的 UI 以适应处理结果的响应。JavaScript 负责提高基于 Web 的应用程序的动态性和交互性。然而,通过 JavaScript 代码创建和操作元素可能很复杂,此外,还需要管理服务器上的调用。有几种优秀的 JavaScript 框架可用,例如 Angular 或 React,这些框架允许您开发丰富的动态 UI 解决方案。
掌握 UI 技术知识是至关重要的;然而,我们还可以从一种集成的开发模型中受益,它允许我们丰富地同时处理 UI 和后端。
ASP.NET Core 9 拥有一个完整的 UI 框架,以满足 Web 应用程序对任何 UI 的需求,并且与 .NET 平台完全集成,使用最佳实践进行职责分离、管理和维护等重要方面。尽管如此,ASP.NET Core 并不局限于使用平台提出的 UI 渲染器;它还与 JavaScript 框架很好地集成,甚至允许您通过采用混合方法使用最佳选项。
然而,在我们更深入地了解 ASP.NET Core 9 中可用的选项之前,让我们先了解与 Web 系统架构相关的重要方面。
渲染 UI
在谈论技术之前,关于 UI 层开发基于 Web 的解决方案就有不同的方法。
基本上有两种模型,客户端和服务器端,各有其优缺点。还有一种第三种可能性,即使用混合方法。
在客户端模型中,处理在浏览器本地进行。这样,所有 HTML、CSS、JavaScript 和其他资产都由浏览器处理,响应用户的刺激。与用户的交互通过包含 UI 相关逻辑的脚本在本地处理,无需请求服务器。
然而,这种方法依赖于服务器资源,例如访问数据等,因此有必要频繁调用服务器以获取基于用户需求的信息,然后处理并在 UI 中呈现。
在服务器端方法中,所有处理都委托给服务器,服务器返回一个定制的 HTML 页面,准备在浏览器中渲染。服务器处理所有必要的信息,允许访问数据,管理业务逻辑,能够使用密钥抽象敏感信息,并将最小处理委托给客户端。然而,如果服务器不可用,将无法使用该系统。
在混合方法中,结合了“两者之最”。
有许多 JavaScript 框架准备用来使基于 Web 的系统动态化,通过在客户端处理资源提供优秀的用户体验,同样,它们也有与服务器交互的能力,将仅相关的信息处理委托给 UI 而不是整个页面的处理。
幸运的是,ASP.NET Core 9 为使用不同方法开发基于 Web 的解决方案做好了准备。我们将在下一节开始了解第一个 UI 开发模型,即使用 Razor Pages。
使用 Razor Pages 和 ASP.NET MVC 实现一个 UI 服务器端渲染模型
ASP.NET Core 9 提供了两种强大的服务器端渲染模型:Razor Pages 和 MVC。它们是类似模型,但 MVC 更为复杂,并实现了模型-视图-控制器(Model-View-Controller)架构设计模式——我们将在 ASP.NET Core MVC 部分详细讨论这一点。现在,让我们开始学习 Razor Pages。
ASP.NET Core Razor Pages
Razor Pages 是一个基于页面的服务器端渲染框架,它实现了页面模型。基于页面的模型基本上将特定页面的实现上下文化,考虑到 UI 和业务逻辑,但正确地分离了责任。
Razor 是一种标记语言,其作用类似于模板引擎,并将它的使用与 HTML 和 C# 代码相结合。
Razor 的起源
Razor 的发展始于 2010 年 6 月,但直到 2011 年 1 月才随着 MVC 3 一起发布,作为 Microsoft Visual Studio 2010 的一部分。Razor 是一个简单的语法可视化引擎。
这种基于页面的开发模型带来了许多优势,例如创建和更新 UI 的简便性;它是可测试的,保持了 UI 和业务逻辑的分离,尽管它与 ASP.NET Core MVC 有相似之处,但它更简单。考虑到所有这些优势,让我们使用 Razor Pages 创建我们的第一个项目。
使用 Razor Pages 创建我们的第一个项目
要创建 Razor Pages 项目,您可以使用 Visual Studio 或 dotnet CLI 工具。
使用 Visual Studio 创建非常简单;只需打开 IDE,选择 创建一个新项目,然后选择 ASP.NET Core Web App 模板,如图 图 2.1 所示:

图 2.1 – 选择项目模板
Visual Studio 和 CLI 工具都使用模板的概念。使用 .NET 平台,可以开发不同类型的项目,无论是网页、Windows 还是移动应用。每个模板都会创建一个基本的项目结构。
在本书的其余部分,我们将使用 CLI 工具来创建项目,以及我们稍后将要讨论的其他需求。从现在起,我们将使用 .NET CLI 工具来创建 Razor Pages 项目,因为这个工具为我们提供了本书其余部分将要讨论的几个好处。
当您安装 .NET 9 SDK 时,将提供一系列工具。本书中我们将使用的主要工具是 dotnet 。
dotnet CLI 工具也有模板的概念。为了测试这个功能,打开您的操作系统命令提示符,并运行以下命令:
dotnet --version
运行上述命令后,将显示当前工具的版本。这次,仍然在提示符下,运行以下命令:
dotnet new
运行上述命令后,您将看到以下内容:

图 2.2 – dotnet CLI 工具模板
如 图 2.2 所示,列出了一些模板和用法示例。每个模型都有一组参数,用于自定义项目创建。如果您想了解更多关于每个模板的参数,只需键入以下命令;例如,对于 webapp 模板,您将键入此命令:
dotnet new webapp –h
-h(帮助)参数将提供所需模板的参数列表和文档。
根据您的机器上的安装情况,可能会有其他模板。运行以下命令:
dotnet new list
您将看到一个包含许多类型项目模板的列表;我们将关注 图 2.3 中突出显示的项目:

图 2.3 – 所有可用的项目模板
现在我们已经了解了模板,让我们最终通过命令行创建一个新的 Razor Pages 项目。为此,打开命令提示符,创建一个名为 NewRazorPages 的新文件夹,并在该文件夹上运行以下命令:
dotnet new razor -n MySecondWebRazor
之前的命令由 new 命令组成,用于创建新项目,然后是 razor,代表所需模板的简称,最后是 -n 参数,它定义了项目的名称。
执行命令后,将创建一个包含项目的新的文件夹。
命令详情
CLI 工具中可用的每个命令都可以有一组参数。要了解更多关于这些参数的信息,只需在命令后添加 -h 选项;例如,dotnet new -h 。
您还可以在以下链接中查阅工具的文档:learn.microsoft.com/en-us/dotnet/core/tools/dotnet
对于本书的其余部分,我们将考虑通过命令行创建项目。现在,让我们更详细地了解我们创建的 Razor Pages 项目。
理解 Razor Pages 项目
与 ASP.NET Core MVC 相比,Razor Pages 项目的目录结构和配置非常简单。然而,在此项目中使用的许多概念都是 MVC 项目的基石,因此我们将充分利用所有内容。
图 2 .4 展示了上一节创建的项目结构,并已扩展以表达我们将更详细讨论的每个重要项:

图 2.4 – Razor Pages 项目结构
Razor Pages 具有简单的结构,基本上可以划分为四个重要项:
-
wwwroot 文件夹:此文件夹包含应用程序的静态文件,例如 JavaScript、CSS、库和图像。默认情况下,配置了三个子目录,例如 css、js 和 lib,用于包含来自外部库的 JavaScript 文件,例如 jQuery 等。您还可以创建其他目录以包含图像、字体等文件。
-
Pages 文件夹:此文件夹包含应用程序的页面,细分为具有 cshtml 扩展名的成对文件,其中包含使用 Razor 语法编写的 HTML 代码和 C# 代码,以及 cshtml.cs,其中包含负责处理页面事件的 C# 代码。
-
appsettings.json:这是一个 JSON 格式的文件,用于集中维护应用程序设置,以及数据库连接字符串、API 密钥和其他参数。我们将在 第九章 中更多地讨论设置。
-
Program.cs:这是 Razor Pages 项目中最重要的文件,用 C# 编写,包含整个应用程序流程的所有执行设置。
随着我们在学习过程中添加更多功能和概念,有关项目的其他一些细节和配置将在本书的其他章节中看到。目前,理解项目的一些前提条件非常重要。
让我们查看 Program.cs 文件以了解一些细节:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
以下要点解释了前面的代码:
-
文件的第一行创建了一个 ASP.NET Core Web 应用程序的实例,使用一些标准配置,例如在 第 9 行和 第 13 行之间添加中间件,这些中间件作用于应用程序流程和由框架本身提供的生成路由等约定配置。
-
接下来,我们有执行 builder.services.AddRazorPages 行,该行负责配置具有 Razor Pages 项目特性的 Web 应用程序。此项目依赖于平台在运行应用程序时使用的某些类。
-
在 第 4 行 和 第 8 行 之间,有一个与应用程序运行环境相关的检查。此代码块确保如果应用程序未处理任何错误,用户将被重定向到通用错误页面,防止应用程序的详细信息被暴露。app.UseHsts() 代码旨在强制通过 HTTP Strict Transport Security ( HSTS ) 协议进行通信,以增加安全性和 HTTPS 的使用。
-
在 第 9 行 ,我们确保使用 HTTPS 协议。
-
第 10 行 对于 Razor Pages 应用程序考虑静态文件、本地化和 HTTPS 的使用是必要的。
在这一点上,考虑 Program.cs 文件中描述的代码将影响应用程序的运行行为是很重要的。我们还必须考虑文件中每个方法的顺序。
更多的细节和配置变体将在本书的后面部分讨论。我们已经熟悉了 Razor Pages 项目结构,因此让我们更多地了解 HTML 页面中使用的语法以及如何与 C# 代码交互。
与页面一起工作
Razor 充当一个强大的模板引擎,允许你使用 HTML、CSS、JavaScript 和 C# 代码在同一文件中创建页面。这种方法在生成动态页面时提供了极大的灵活性。
让我们看看 Index.html 页面的一个示例:
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a
href="https://learn.microsoft.com/aspnet/core">
building Web apps with ASP.NET Core</a>.</p>
</div>
代码的第一行涉及页面相关设置,然后是纯 HTML 代码。
让我们了解文件中描述的主要组件:
-
@page 指令必须是 Razor 页面视图中的第一个指令。它表示该页面将作为动作处理程序。
-
@model 指令表示将传递给页面的模型类型。Razor Pages 由两个文件组成,.cshtml 和 .cshtml.cs 。
ViewData["Title"] 代码是一个字典,它代表将数据传递给页面的另一种方式。通常,ViewData 用于传递少量数据。在前面代码的情况下,ViewData 正在被用来将页面标题的信息传递到 HTML 模板中。
第一部分涉及 HTML;与前面的示例一样,.cshtml.cs 指的是页面处理程序的 C# 代码,其中包含将在页面上使用的数据或信息。因此,Index.cshtml 文件与 index.cshtml.cs 文件相关联。
在下面的代码中,我们有 IndexModel 类,它表示将在页面上使用的模型:
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
public IndexModel(ILogger<IndexModel> logger)
{
_logger = logger;
}
public void OnGet()
{
}
}
正如我们在示例中提到的,Index.cshtml 页面没有完整的 HTML 页面结构。这是因为 Razor Pages 允许布局的概念。也就是说,页面通常可以共享相似的结构。这样,我们可以重用代码来生成常见的布局和创建共享的视图块。
在项目结构中,布局保存在Pages/Shared文件夹中。根据惯例,共享视图以下划线开头。_Layout.cshtml文件具有常见的 HTML 结构,并且还有一个特殊指令@RenderBody():
<html lang="en">
<head>
<!-- The rest of the code has been omitted for
readability. -->
<title>@ViewData["Title"] - MyFirstRazorWebApp</title>
<!-- The rest of the code has been omitted for
readability. -->
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
RenderBody()方法指定了在服务器处理之后视图将被渲染的位置。在index.cshtml页面的情况下,所有 HTML 都将渲染在主标签之间,在_Layout.cshtml文件中。此外,请注意ViewData["Title"]的使用,它将显示在页面上设置的值,如index.cshtml文件中所示。
此外,还有两个其他特殊文件,根据惯例,应位于Pages文件夹中:
-
_ViewStart.cshtml:这是一个允许我们在显示每个视图之前执行代码的文件。在这种情况下,此文件包含定义将用于视图的布局文件的代码。
-
_ViewImports.cshtml:此文件用于定义命名空间并将功能导入页面,以通用方式。这样,就不需要在每个页面上声明命名空间和其他功能。
现在我们已经了解了整个项目结构以及如何在项目中管理 Razor 页面,让我们对Index页面和IndexModel模型进行一些自定义设置,并学习如何使用 Razor 语法与 C#代码交互。
与 Razor 语法和 C#模型交互
如前所述,Razor 页面有两个文件,一个负责渲染 UI,另一个负责包含页面的业务逻辑。此外,还有使用 C#代码与 HTML 结合的可能性,这意味着页面可以在服务器处理时根据用户交互生成。
我们将对index.cshtml文件进行一些更改,添加一些如图图 2.5所示的控件:

图 2.5 – 使用 Razor 语法自定义 index.cshtml 文件
在 Visual Studio 或 Visual Studio Code 中打开index.cshtml文件进行更改,并按照以下步骤操作:
-
在@{}实例之间添加以下代码:
string subtitle = "It's funny"; -
现在,将div标签之间的所有内容全部更改,这些标签包含页面内容,更改后的代码如下:
<h1 class="display-4">Welcome</h1> <p>Learn about <a href="https://learn.microsoft.com/aspnet/core"> building Web apps with ASP.NET Core</a>.</p> <h2>@Model.Message</h2><br /> <h3>@subtitle</h3> <a asp-page-handler="DefineColor" asp-route-id="1">Red</a> <a asp-page-handler="DefineColor" asp-route-id="2">Green</a> <div style="width: 200px;height:200px; background-color:@Model.Color"></div> <form method="post"> <label>Total:</label><input type="text" name="quantity"/> <input type="submit" value="Load Products" name="btn" /> </form> <table class="table"> <thead> <tr> <th>Id</th> <th>Nome</th> <th>Preço</th> </tr> </thead> <tbody> @foreach (var product in Model.Products) { <tr> <td>@product.Id</td> <td>@product.Name</td> <td>@product.Price.ToString("C")</td> </tr> } </tbody> </table>上述代码为Price属性创建格式,以便以货币格式显示。此格式将考虑您的浏览器区域设置。在本书中运行的示例中,格式将显示en-US文化格式。
-
为了确保属性以特定格式显示,可以创建一个新的属性:
public string FormattedPrice { get { return price.ToString("C", CultureInfo.GetCultureInfo("en-US")); } } -
这样,我们可以更新现有代码以显示价格,如下所示:
<td>@product.FormattedPrice</td> -
然而,我们可以全局定义应用程序文化,避免创建新属性的需求。为了进行此更改,请将以下代码添加到位于var app = builder.Build()行下面的Program.cs文件中:
app.UseRequestLocalization(new RequestLocalizationOptions { DefaultRequestCulture = new RequestCulture("en-US"), SupportedCultures = new List<CultureInfo> { new CultureInfo("en-US") } });
重要提示
有关在 ASP.NET Core 9 中管理文化的更多信息,请参阅以下链接:learn.microsoft.com/en-us/aspnet/core/fundamentals/localization?view=aspnetcore-9-0
除了对服务器代码的一些调用外,还添加了一些使用内联 C# 代码的元素。目前,理解如何将 Razor 语法与 HTML 一起使用非常重要。让我们了解添加到 UI 中的所有元素以及它们如何与 C# 交互。
在第 5 行添加的第一个片段是 C# 代码,声明了一个 string 类型的变量 subtitle = "It's funny"。
注意,所有代码都包含在 @{ 和 } 符号之间,如 index.cshtml 文件中的第 3 行到第 6 行的示例所示。此语法允许添加一个能够包含 C# 和 HTML 指令的代码块。
结合 C# 和 HTML 代码和代码块
在 C# 代码块中,也可以添加 HTML 标签。使用这种策略可以带来很大的优势,例如,根据 if 语句确定将渲染哪种类型的 HTML 标签,如下例所示:
@{
if (total > 0)
{
可用的数量是:@total
}
else
{
没有可用的数量
}
}
除了代码块之外,使用 @ 符号还可以在单行中添加 C# 代码,如前述代码示例所示,这将显示 Model 对象的 Message 属性的值以及显示之前定义的 subtitle 变量的值:
<h2>@Model.Message</h2><br />
<h3>@subtitle</h3>
Razor Pages 提供了指令,这些是添加到 HTML 标签中的功能。以下代码向页面添加了两个锚点:
<a asp-page-handler="DefineColor" asp-route-id="1">Red</a>
<a asp-page-handler="DefineColor"
asp-route-id="2">Green</a>
<div style="width: 200px;height:200px;
background-color:@Model.Color"></div>
注意,有两个属性,asp-page-handler 和 asp-route-id。这些是指令 Razor 页面,分别确定当点击链接时的事件处理器的名称以及将作为参数发送给处理器的值。
此外,请注意,div 样式具有 @Model.Color 代码,作为 background-color 属性的值插入。div 标签的颜色将根据链接动态设置。
对于页面上创建的其他控件,我们有一个纯 HTML 表单和一个列出随机生成产品的表格。该表单没有 action 属性,用于确定处理数据提交的页面或脚本。此属性被省略,因为 Razor Pages 遵循一种约定,在这种情况下是推断表单页本身作为操作。
以下代码生成了乘法表的行和列:
@foreach (var product in Model.Products)
{
<tr>
<td>@product.Id</td>
<td>@product.Name</td>
<td>@product.Price.ToString("C")</td>
</tr>
}
上述代码是 HTML 和 C# 代码的混合体。在执行 foreach 语句后,定义了乘法表的列和行。产品在 Model 对象的 Products 属性中生成。
记得要分离职责
如我们所见,使用 Razor Pages 可以在 HTML 页面中添加任何 C# 代码。然而,使用这种方法来操作 UI 元素,但正确地分离责任很重要,避免将业务规则和 UI 操作规则的多重实现混在一起。
到目前为止,我们需要的所有元素都已添加,我们现在知道如何将 C# 代码添加到我们的 UI 中。让我们完成 Index 页面的设置,在 index.cshtml.cs 文件中添加其操作所需的代码。
与页面模型一起工作
为了使之前创建的 UI 正确工作,我们必须向页面模型添加一些属性和方法。打开 index.cshtml.cs 文件,以便我们可以添加必要的功能。
Index 页面模型实际上是一个继承自 PageModel 类的 C# 类,它是 Razor Pages 中模型使用的多个属性和方法的抽象。
让我们对 IndexModel 类进行修改,并理解添加的每一行代码:
-
在 Index.cshtml.cs 文件中添加一个 Message 属性。它将用于定义将在 UI 中显示的消息:
public string Message { get; set; } -
在项目根目录下,创建一个名为 Models 的文件夹,然后添加一个名为 Product.cs 的类。这个类必须包含以下代码:
public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } } -
返回到 Index.cshtml.cs 文件,并添加一个 Products 属性,该属性将包含一个列表,该列表将列在之前在 UI 中创建的表中:
public List<Product> Products { get; set; } -
还要添加一个 Color 属性:
public string Color { get; set; } -
基本属性已经创建。现在,让我们创建一个随机生成产品列表的方法。按照以下代码添加 GenerateProduct 方法:
private List<Product> GenerateProduct(int quantity) { var random = new Random(); var products = Enumerable.Range(1, quantity).Select(i => new Product { Id = i, Name = $"Product {i}", Price = (decimal)(random.NextDouble() * 100.0) }); return products; } -
我们将更改类构造函数,并为 Products 和 Message 属性添加默认值。这样,一旦页面显示,我们就会有一个随机生成的产品列表和一个 我正在使用 Razor 语法 消息:
public IndexModel(ILogger<IndexModel> logger) { _logger = logger; Products = GenerateProduct(10); Message = "I'm using the Razor Syntax."; } -
生成产品的那个方法在构造函数中被用来生成一个初始列表。然而,我们希望通过 UI 交互并基于在表单中输入的值生成一个列表。为此,我们将创建一个 OnPost 方法。此方法基于在 UI 表单中输入的数量生成一个新的列表:
public void OnPost(int quantity) { Products = GenerateProduct(quantity); } -
最后,让我们定义一个负责设置 Color 属性值的方法:
public void OnGetDefineColor(int id) { Color = id == 1 ? "#FF0000" : "green"; }
我们的模式已经准备好与 UI 交互。但在运行应用程序之前,让我们了解 Razor Pages 规范的一个简单概念。
OnPost方法使用这个名字,遵循约定,并且与GET、POST、DELETE和PUT HTTP 动词相关。这样,通过定义OnGet、OnPost、OnDelete和OnPut等方法,它们将能够根据 HTTP 动词处理页面事件。Index页面的 UI 有一个使用POST方法的表单。因此,当点击加载产品按钮时,Razor Pages 将自动调用IndexModel模型上的OnPost方法。
OnGetDefineColor方法使用这个名字是为了遵循约定,但方法名称中并不强制使用OnGet前缀。在 HTML 中,我们不会将链接处理程序定义为OnGetDefineColor;这是因为,按照约定,Razor Pages 会从方法名称中推断前缀,也因为执行了一个GET请求。但如果你想提供完整的名称,则不会有问题。OnGetDefineColor方法还有一个重要的特性:它的id参数。id参数接收在 HTML 链接中添加的指令中定义的值,如下面的代码示例所示:
<a asp-page-handler="DefineColor" asp-route-id="1">Red</a>
这个操作被称为绑定,这意味着当传递id参数时,Razor Pages 会根据每个参数的名称设置方法参数的值。
指令提示
在之前定义的 HTML 链接中传递参数是通过asp-route指令与在方法中期望的参数名称一起完成的——在这种情况下,id。这样,完整的指令由asp-route-id定义。如果有另一个名为name的参数,例如,指令将是asp-route-name。
现在你已经知道了Index页面的整个实现,运行应用程序并交互之前创建的控件。
到目前为止,我们已经学习了如何使用 Razor Pages 通过服务器端方法创建动态页面。与在Index页面上创建的控件每次交互时,都会调用服务器,服务器将操作信息,解释在 UI 中实现的 Razor 页面代码,然后返回带有结果的 HTML 输出。
这种方法与我们在下一节将要学习的 ASP.NET Core MVC 非常相似。
ASP.NET Core MVC
ASP.NET Core MVC 也是一个非常强大的服务器端框架,它实现了 MVC 设计模式。让我们了解 MVC 设计模式是如何工作的,然后通过创建一个新项目来学习如何从这个方法中受益。
MVC 模式
MVC 是一种基于责任或上下文分离的架构设计模式:

图 2.6 – MVC 模式
如您在图 2.6中看到的那样,控制器充当协调者,通过视图响应用户交互,并将动作委托给模型,该模型代表应用程序状态和业务规则。随后,控制器返回结果,定义哪个视图将负责向最终用户显示 UI。
视图和控制器依赖于模型,但模型是中立的,允许分离责任并使用良好的代码实践,例如使用单元测试,因为与视觉表示独立。
ASP.NET Core MVC 基于 MVC 模式,适应项目模型和约定。让我们了解这种类型的项目中如何实现此模式。
ASP.NET Core MVC 项目结构
创建 ASP.NET Core MVC 项目非常简单,我们除了使用 CLI 工具外,还使用 Visual Studio Code 作为编辑器。
按照以下说明进行操作:
-
打开您的操作系统命令提示符,访问您选择的目录,其中将创建项目。
-
输入以下命令以创建项目:
dotnet new mvc --name MyFirstMVCApp前面的命令使用了dotnet CLI 工具,其中我们通过new命令指定创建新项目的动作。然后,我们定义要创建的项目类型。在这种情况下,我们通知模板将是mvc,并且添加了一个--name参数,通过该参数我们通知项目的名称。
-
将创建一个与应用程序名称相同的文件夹。访问此文件夹,然后通过运行以下命令打开 Visual Studio Code:
cd MyFirstMVCApp code.前面的命令将打开以下内容:

图 2.7 – ASP.NET MVC 项目结构
当查看创建的 MVC 项目的结构时,我们会注意到它与 Razor Pages 的相似性。
有三个主要项目文件夹:
-
视图:它具有与 Razor Pages 中的页面文件夹相同的特性;即它是应用程序的 UI。它包含.cshtml文件,并且这些文件组织成子文件夹,代表一个页面,并包含所有可以作为对动作的响应使用的 UI。然而,没有. cshtml.cs文件。
-
控制器:控制器在 Razor Pages 中定义的类中具有相似的角色。如前所述,它是一个协调者,具有用于操作在视图中执行的事件的方法。在 MVC 中,每个控制器方法被称为动作,可以返回视图、重定向,甚至数据。
-
模型:此文件夹用于管理将用于在视图和 HTML 之间交换信息的业务类和模型。
此结构遵循一种约定,这在某种程度上简化了这种方法的开发。然而,平台允许我们在必要时进行自定义。
MVC 项目在 program.cs 文件中定义的设置与 Razor Pages 有一个小差异。在执行 app.Run() 行之前,有一个对 app.MapControllerRoute 方法的调用。此方法负责配置整个应用程序。
路由定义了将通过应用程序请求访问的内容以及如何访问。
如以下代码所示,配置了一个默认路由,称为 default,它具有 controller / action / parameter 模式。此外,控制器和动作分别具有默认值 Home 和 Index,而参数是可选的:
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
这是一个标准的 MVC 约定,可以很容易地更改。在这个模型中,如果应用程序的访问没有定义控制器和要执行的操作,则默认控制器 Home 和 Index 动作将被定义为对请求的响应。
有趣的是,我们指的是控制器而不是页面。这是因为 MVC 模式允许控制器根据所需操作来协调请求,然后返回结果或视图。
随着我们通过本书的章节进展,我们将有其他使用路由的示例。
理解模式和约定
MVC 项目遵循一个有利于之前提出的文件结构的约定。
如我们所学,控制器在模型和视图之间充当协调者。这样,我们不是使用页面的概念,而是有动作的概念。
给定用户的意图或交互,它将触发一个动作。这个动作被控制器捕获,然后执行处理并返回一个值或视图,如图 2.8 所示:

图 2.8 – MVC 请求流程
Controller 类是一个 C# 类,它具有属性和方法。Controller 类的公共方法被称为动作。
想象一个人物注册表。那么,我们就会有以下类:
public class PersonController : Controller
{
public IActionResult Index()
{
return View();
}
public JsonResult GetPeople()
{
var model = new List<PersonModel>() {
new PersonModel("Person 1",
new DateTime(1980, 12, 11)),
new PersonModel("Person 2",
new DateTime(1983, 12, 15))
};
return Json(model);
}
public IActionResult Register(PersonModel personModel)
{
return RedirectToAction("Result",
new { message = $"The {personModel.Name}
was registered successfully." });
}
public IActionResult Result(string message)
{
ViewData["Message"] = message;
return View();
}
}
PersonController 类遵循命名约定,在类名末尾采用 Controller 后缀。此外,这个类从 Controller 类继承,这是一个基类,它已经包含了一些负责通过控制器处理和返回数据和信息的方法。
接下来,我们有一个名为 Index 的方法,它仅仅返回一个视图,执行 return View() 命令。
返回哪个视图?
View() 方法考虑了 Asp.NET Core MVC 的约定。因此,当执行时,实例化的视图会考虑以下路径:/Views/
View() 方法有其他重载,使得可以将一个对象传递给视图作为模型,甚至可以定义一个应显示的控制器视图。
GetPeople() 方法仅返回一个人员列表的 JSON 格式,如图 图 2.9 所示:

图 2.9 – 包含人员列表的 JSON
Register() 方法处理表单请求,并将一个对象返回到 Result 视图。然而,在这种情况下,它正在调用 PersonController 控制器中的动作,执行名为 RedirectToAction 的方法,该方法期望一个字符串作为参数。图 2.10 展示了注册人员后动作结果的显示:

图 2.10 – 注册人员视图
基础控制器类
抽象的 Controller 类包含一些实用方法,这些方法允许你在控制器和视图之间进行通信流程的工作。
Json()、View() 和 RedirectToAction() 方法是控制器类中常用的一些资源。
MVC 模型和 Razor Pages 有相似之处,但一个很大的区别是使用动作而不是页面。这样,控制器就有能力根据某些用户交互决定应返回哪种类型的视图或信息,从而协调处理流程。
控制器响应用户事件,视图是这个类型项目的一个重要方面。基于 PersonController 示例,我们将了解视图是如何创建的,并学习在下一节中控制器动作的交互工作原理。
在 ASP.NET MVC 中与视图一起工作
ASP.NET MVC 中的视图概念与 Razor Pages 中使用的概念相同,使用 Razor 语法。正如我们可以在以下代码中看到的那样,有一个表单标签,使用 Razor 指令定义了哪个控制器和动作将处理注册:
@model MyFirstMVCApp.Models.PersonModel
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1 class="display-4">Person View</h1>
</div>
<div>
<form method="post" asp-controller="Person"
asp-action="Register">
@Html.LabelFor(p => p.Name)
@Html.TextBoxFor(p => p.Name)
<br/>
@Html.LabelFor(p => p.DateOfBirth)
@Html.TextBoxFor(p => p.DateOfBirth)
<input type="submit" value="Register" />
</form>
</div>
之前创建的文件位于 Views/Person/Index.cshtml 目录结构中。按照惯例,对于 PersonController 控制器,必须有一个 Person 文件夹,它是 Views 的一个子文件夹,将包含将通过 PersonController 控制器显示的所有视图。
视图索引是一个好的实践
在每个控制器中拥有一个 Index.cshtml 文件和一个 Index() 动作是一个好的实践。按照在 Program.cs 文件中定义的路由模式,默认情况下,如果没有指定动作,将执行一个 Index() 动作。在这种情况下,拥有一个视图和动作索引可以避免你的应用程序中出现可用性问题。
为了创建表单中使用的标签和输入,使用了 标签助手。标签助手是 Razor Pages 方法,基本上用于渲染 HTML。在前面展示的代码中,区别在于与模型存在连接。
在Index.csthml页面的第一行中,定义了一个 Model,使用@model MyFirstMVCApp.Models.PersonModel代码。这使得 Model 具有强类型。通过使用标签助手和 Lambda 表达式,我们正在实施与 MVC 绑定模型相关的好做法。
因此,表单标签助手生成的 HTML 将具有与模型属性相关的正确名称,遵循以下概述的流程:
-
数据是输入中提供的信息,包括姓名和出生日期。
-
用户点击注册按钮。
-
ASP.NET 框架向Result操作发送了一个请求。
-
ASP.NET 框架识别出Result操作有一个PersonModel对象作为参数,然后创建了一个实例。
-
ASP.NET 框架将表单提交的数据绑定到实例化的对象的相应属性中。
-
ASP.NET 框架执行了Result操作。
由于绑定是通过属性名完成的,如果任何名称输入错误,某些属性将没有值。
使用标签助手方法还有助于其他方面,例如添加到Model类的属性,允许进行验证等。我们将在第三章和第五章中讨论更多关于属性和绑定。
MVC 模式为我们提供了几个好处,特别是在需要控制不同的业务流程并根据需要返回动态视图的更复杂的项目中。
在 ASP.NET Core 9 中,处理 UI 的方法还有其他几种。让我们在下一节中了解更多关于其他选项,例如使用 Blazor 和与 JavaScript 框架的集成。
探索使用 Blazor 和 JavaScript 框架进行 UI 客户端渲染
ASP.NET Core 9 有几个框架,它们通过客户端和服务器端方法提供高质量的 UI 创建和良好的用户体验。
我们将讨论使用WebAssembly标准的新技术,称为 Blazor,它是一个强大且灵活的 UI 框架。然而,如果你习惯了 JavaScript 框架,你也可以从.NET 平台中受益。
使用 Blazor 创建丰富的 UI
就像 Razor Pages 和 MVC 一样,Blazor 在.NET 平台上提供了一个单页应用程序(SPA)框架,它既可以在客户端运行,也可以在服务器端运行,利用了 C#的所有功能。
Blazor WebAssembly
Blazor 的客户端-服务器版本运行在 WebAssembly 平台上,这是一个紧凑的字节码,具有优化的格式,下载速度快,在客户端运行时提供出色的性能,创造了丰富的 UI 体验。
正如我们在图 2 .11中看到的那样,Blazor 是 ASP.NET Core 为 WebAssembly 提供的抽象。这样,Blazor 代码将生成应用程序程序集,这些程序集需要.NET 运行时来执行,同时也允许 WebAssembly 与 HTML 文档之间的交互。
WebAssembly
WebAssembly 是一个网络标准,你可以在官方网站上了解更多信息:webassembly.org/

图 2.11 – WebAssembly 和 Blazor
WebAssembly 平台使用互操作性模型,这使得它可以与所有浏览器 API 交互,在沙盒中运行,提供对恶意行为的保护,并允许执行.NET 代码。
所有的页面代码都编译成.NET 程序集。因此,当通过浏览器访问页面时,会下载程序集和.NET 运行时。然后,在 WebAssembly 的支持下,应用程序运行并使用 JavaScript 互操作来处理DOM(文档对象模型)操作和浏览器 API 调用。
Blazor 是一个灵活的框架,还允许你创建具有服务器端处理优势的项目,提供卓越的客户端体验。
Blazor 服务器
Blazor 还允许采用服务器端 UI 渲染方法。然而,与 Razor Pages 和 MVC 不同,Blazor 不是为每个客户端请求渲染整个 HTML 并返回完整的文档作为响应,而是创建一个图,该图表示页面组件,考虑属性和状态。
然后,在每次交互中,Blazor 对图进行评估,并生成二进制表示,将其发送回客户端。
Blazor 的服务器端方法使用SignalR技术,这允许你通过直接连接到服务器来更新 UI,带来更好的可用性和丰富的用户体验。我们将在第四章中介绍 SignalR。
Blazor 服务器在基于 Web 的解决方案的开发中带来了巨大的好处,以 C#作为通用语言,带来了安全性、可靠性和性能。
让我们了解使用 Blazor 的开发方法。
Blazor 组件和结构
就像一些 JavaScript 框架,例如 Angular 一样,Blazor 使用组件结构。
组件是根据应用程序的需求,以特定目标开发的一个或多个 UI 元素。该组件可以在整个应用程序中重用,实现职责分离、可重用性和灵活性。
所有 Blazor 组件都具有.razor扩展名,并使用 Razor 语法以及 C#的所有优点。
要创建 Blazor 项目,只需使用dotnet CLI,以下命令:
dotnet new blazor --name MyFirstBlazorApp
交互式渲染模式
ASP.NET Core 9 在 Blazor 应用程序中引入了渲染交互模式。交互式渲染模式功能增强了 Blazor 应用程序处理渲染的方式,引入了一种模式,其中服务器渲染的静态 HTML 逐渐增强为完全交互式的客户端应用程序。
该功能的目的是:
渐进增强:当 Blazor 应用最初加载时,服务器预先渲染 HTML,为用户提供一个完全可交互的页面,用户可以立即与之交互。这允许应用在客户端加载 Blazor 框架后,无缝地从静态 HTML 过渡到完全交互式的 Blazor 应用。
无缝过渡:确保即使在客户端 Blazor 运行时初始化期间,应用看起来也是交互式的,并且用户可以在完整的 Blazor 运行时准备好之前开始与应用交互,使用户体验更加流畅。
提升性能:通过减少用户在传统的 Blazor Server 或 Blazor WebAssembly 应用中可能体验到的明显延迟,优化了交互时间。
提升用户体验:在转换过程中最小化中断或加载指示器,使用户对速度有更好的感知。
如果你使用以下命令创建 Blazor 应用:
dotnet new blazor --name MyFirstBlazorApp
默认的交互模式是服务器。如果你想利用这个新功能,请使用以下命令:
dotnet new blazor --name MyFirstBlazorApp --interactivity Auto
要了解更多关于这个新功能的信息,请访问以下网址:learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-9.0
创建项目后,只需打开Pages文件夹,你将找到一些组件,如图图 2.12所示:

图 2.12 – Blazor 项目结构
有一些组件被定义为页面,因此具有@pages指令,其中定义了访问页面的路由。也有其他组件被添加到页面中。
Counter.razor文件是一个页面组件,包含以下内容:
@page "/counter"
@attribute [RenderModeServer]
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary"
@onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
注意,这里使用了 Razor 和 HTML 语法,正如我们在 Razor Pages 和 MVC 方法中学到的。在@code {}标签之间也有 C#代码。
在前面的文件中定义的 C#代码具有在用户点击点击我按钮时增加计数器的功能。
HTML 按钮标签具有@onclick属性,该属性使用在 C#代码块中定义的名称。
对于简单的组件,在同一个文件中使用 HTML 和 C#代码的方法可能是有效的。然而,将业务规则与 UI 分离是良好的实践。因此,Blazor 允许创建一个包含组件 C#代码的文件。
在前面的代码示例中,将会有两个文件:Counter.razor和Counter.razor.cs。所有 C#代码都可以移动到新文件中,生成以下类:
namespace MyFirstBlazorApp.Pages;
public partial class CounterPartialClass
{
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
Blazor 非常灵活,为开发丰富的基于 Web 的应用程序提供了广泛的可能,这些应用程序与 HTML、CSS 和 JavaScript 集成,并使用最现代的技术。然而,关于 Blazor 的讨论将需要一本专门的书籍;然而,本书将专注于基于 Razor Pages 和 MVC 的方法。
ASP.NET Core 9 在开发 UI 方面非常灵活,提供了不同的框架。但如果你熟悉 Angular、React 或 Vue.js,你可以从 .NET 平台的力量中受益。
ASP.NET Core 9 和 JavaScript 框架
正如我们在本章其他主题中学到的那样,ASP.NET Core 9 提供了多种构建 UI、与 C# 代码交互的方法。有几个相关的优点,包括使用通用开发模型、使用 Razor 语法以及 .NET 平台的所有好处。
然而,如果你习惯于使用 Angular、Vue.js 或 React 等框架来构建 SPAs,.NET 平台为此目的提供了一些模板:

图 2.13 – ASP.NET Core 9 JavaScript 框架模板
React 和 ASP.NET Core 模板创建了两个项目,一个用于前端,另一个用于应用程序的后端。
单页应用(SPAs)采用一种将 UI 独立于后端开发的方法,后端通常是一个外部服务或应用程序。
当使用 ASP.NET Core 提供的模型时,UI 和后端之间也有明显的分离。项目已经配置好了与在 .NET 中开发的 Web API 集成的功能。这种做法的一个巨大好处是,可以方便地将 UI 和后端项目作为一个简单的单元发布,从而简化发布过程:

图 2.14 – ASP.NET Core React 独立项目结构
使用 Angular、Vue.js 或 React 等框架的项目模板完全是可选的。即使 UI 项目是独立创建的,我们也可以通过开发 Web API 来为 UI 提供服务来受益。我们将在 第三章 中讨论创建 Web API。
如我们所见,该平台提供了多种开发高质量基于 Web 系统的方法。每个 ASP.NET Core UI 框架都有一些可以组合起来生成更强大解决方案的好处,我们将在下一节中探讨这些解决方案。
与混合解决方案一起工作
在像 ASP.NET Core 这样强大的平台上工作的一大好处是能够集成不同技术。因此,我们可以将 Razor Pages、MVC 和 Blazor 的所有功能结合到同一个项目中。
在与 Blazor 集成的情况下,使用 .razor 组件的好处是可重用性。
将 Blazor 集成到 Razor Pages 或 MVC 项目中必须按照以下步骤进行配置:
-
在项目根目录下,添加一个名为 _Imports.razor 的文件。此文件将负责导入项目所需的命名空间:
@using System.Net.Http @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web .Virtualization @using Microsoft.JSInterop @using {CHANGE_FOR_THE_NAMESPACE_OF_YOUR_PROJECT}注意,在文件末尾有一个 {CHANGE_FOR_THE_NAMESPACE_OF_YOUR_PROJECT} 标签。将此值更改为您的项目命名空间。
-
现在,需要更改位于 Pages/Shared 目录中的 _Layout.cshtml 文件(如果是 Razor Pages 项目)或位于 Views/Shared 目录中的 MVC 项目。在 head 元素中添加以下代码:
<base href="~/" /> <component type="typeof(Microsoft.AspNetCore .Components.Web.HeadOutlet)" render-mode="ServerPrerendered" />定义 的目的是定义应用程序的基本路径,而 component 标签用于在 HTML 头元素中渲染 Razor 组件的内容。
-
在 @await RenderSection(...) 渲染部分之前添加以下脚本:
<script src="img/blazor.server.js"></script>不要担心脚本路径,更不用说创建它了。这将由框架自动完成。
-
现在,打开 Program.cs 文件进行一些修改。首先,我们必须注册 Blazor 服务,以便在应用程序运行时它们可用。添加以下代码:
builder.Services.AddServerSideBlazor(); -
您还需要添加 Blazor 路由映射控制。在 MapRazorPages 调用(如果是 Razor Pages 项目)或 MapControllerRoute 调用(如果是 MVC 项目)下方添加以下行:
app.MapBlazorHub();
现在项目已经与 Blazor 集成,让我们按照以下步骤创建一个组件:
-
在 Pages/Shared(Razor Pages)或 Views/Shared(MVC)文件夹中创建一个名为 technology.razor 的文件,并添加以下代码:
<h1>Load Technologies</h1> <button class="btn btn-primary" @onclick="LoadTechnologies">Load</button> @if (technologies != null) { <ul> @foreach(var tech in technologies) { <li>@tech</li> } </ul> } @code { private int currentCount = 0; private string[]? technologies; private void LoadTechnologies() { technologies = new[] { "Razor Pages", "MVC", "Blazor"}; } }上述代码创建了一个具有点击事件的 Load 按钮,该事件将加载技术列表。此技术列表是在 @code{} 会话中创建的,使用字符串数组。当运行应用程序时,屏幕将类似于 图 2.15:

图 2.15 – 使用 Blazor 组件的页面/视图
-
要使用此组件,将以下代码添加到任何 MVC Razor 页面或视图中:
<component type="typeof(Counter)" render-mode="ServerPrerendered" />
当运行应用程序时,只需使用该组件即可获得预期的结果,如图 图 2.16 所示。此组件是可重用的,可以添加到任何页面或视图中,为 UI 开发带来更大的灵活性和功能:

图 2.16 – 使用 Blazor 组件与 Razor Pages 和 MVC
结合 ASP.NET Core UI 框架可以在基于 Web 的应用程序开发过程中带来好处,利用每种方法的最佳之处。
摘要
在本章中,你已经深入探索了 ASP.NET Core UI 的丰富世界,获得了关于构建动态和吸引人的 UI 可用工具的宝贵见解。你不仅理解了实现服务器端 UI 所需的关键概念和工具,而且还发现了通过 Blazor 使用 WebAssembly 带来的显著好处,使你能够与 ASP.NET Core 结合创建强大的单页应用(SPAs)。随着本章的结束,你已经学会了如何无缝结合 ASP.NET Core UI 解决方案。现在,在打下坚实基础之后,我邀请你开始下一章的激动人心的旅程第三章,我们将探讨 Web API 的世界及其在提供卓越服务中的关键作用。准备好将你的技能提升到下一个层次!
第三章:为服务交付构建 Web API
作为 ASP.NET Core 9 的一部分,Web API 可以用来构建 HTTP 服务,这些服务可以提供给网页消费和移动应用程序。.NET Core 平台提供的结构使得可以开发出高质量和性能的 API。在本章中,我们将更多地了解 Web API 以及使用它们提供解决方案的标准、约定和最佳实践。
在本章中,我们将涵盖以下主要主题:
-
将业务作为服务交付
-
探索最小 API
-
使用基于控制器的方法实现 API
-
与文档一起工作
技术要求
本章使用 Postman 工具,该工具将用作消费 API 的客户端。此工具还将用于本书的其他章节,其安装和使用是免费的。
您可以通过以下链接在您的操作系统上下载 Postman:www.postman.com/downloads/ .
本章中使用的代码示例可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/ASP.NET-Core-9.0-Essentials/ .
将业务作为服务交付
正如我们在上一章中学到的,ASP.NET Core 9 为开发丰富的基于 Web 的应用程序提供了不同的框架。无论选择哪种模型,无论是客户端还是服务器端,我们都必须实现负责应用程序操作的协商流程。
想象一个数字银行的系统,用户可以在他们的支票账户中执行不同类型的操作,例如转账、分析摘要、检查账户余额,甚至购买新的服务套餐。这些操作中的每一个都有业务需求和规则。例如,如果用户的账户余额是 100 欧元,则不应允许转账 200 欧元。
我们已经学过,可以使用 Razor Pages、MVC 或混合模型完美地实现这种协商流程。我们可以轻松地将基于 Web 的应用程序在服务器上执行。
然而,想象一下,用户请求了一个移动应用程序。它应该提供与网页版本相同的特性。
在这个案例中,可以用于此目的的技术可以是原生或混合开发。但是,应用程序的业务逻辑应该如何开发?如果应用程序规则发生了变化,是否需要更新两种不同的代码以使应用程序正确运行?
在这种情况下,最佳实践是对涉及业务规则的代码进行集中管理。这将允许不同类型的应用程序接口,无论是浏览器、移动应用程序,甚至是其他应用程序,都能与业务环境进行交互。
这个集中式应用程序以 Web API 格式提供,实际上是一个通过互联网分发的应用程序。基于在 Web 应用程序交互中常用 HTTP 协议的表示状态转移(REST)协议,允许客户端(浏览器、移动应用和其他应用程序)以受控和集中的方式独立消费资源。
在互联网上提供业务上下文的模型称为业务即服务(BaaS),允许组织提供特定功能或资源,例如可以被其他公司或应用程序消费的服务。

图 3.1 – BaaS
幸运的是,ASP.NET Core 9 为我们提供了一个强大的模型来创建 Web API;然而,在学习如何创建 BaaS 资源之前,我们必须了解一些基础知识。在本节的其余部分,我们将探讨一些重要的基础概念。
HTTP 动词和约定
与 API 的通信是通过HTTP协议完成的,该协议有一些称为HTTP 动词的操作。
这些动词确定了给定资源中的意图类型。最常见的 HTTP 动词如下:
-
GET:此方法用于请求功能中的数据,例如只读操作;当我们输入浏览器中的 URL 时,作为响应,我们收到一个 HTML 页面。GET 也可以用来确定获取注册用户列表的意图,例如。
-
POST:当你发送 POST 请求时,你通常是在服务器上创建一个新的功能。此方法包括请求体中的数据。
-
PUT:PUT 请求用于更新功能。在这种情况下,对资源属性所做的任何更改都必须在请求体中发送,并且服务器将用发送的数据替换资源。
-
DELETE:DELETE 请求用于请求删除指定的功能。
-
PATCH:PATCH 请求用于对功能应用部分修改。与替换整个功能的 PUT 动词不同,PATCH 只更新功能的指定部分。
-
HEAD:这通常用于验证功能的存在性和元数据,而无需下载其内容。
-
OPTIONS:OPTION 请求用于描述目标功能的通信选项。它可以用来咨询服务器支持的方法和有关功能的其他信息。
动词对于确定 API 将执行哪种类型的操作非常重要。从上述列表中我们可以看到,有一些动词彼此相似,例如 POST 和 PUT。两者都可以用来创建和更新资源。然而,使用正确的动词意味着集成过程始终可以轻松理解。对于某些动词的使用没有严格的规则,但使用正确的动词是一种良好的实践。
REST
REST 是一种架构风格,是一组约束和原则,鼓励无状态、可扩展和易于维护的 Web 服务设计。
REST 服务的特点之一是无状态通信,即客户端向服务器发出的每个请求都必须包含理解和处理请求所需的所有信息。服务器不得在请求之间存储任何关于客户端状态的信息。这确保了请求可以独立处理,使系统可扩展且易于维护。
资源的概念也存在,无论是物理对象、概念实体还是数据片段。每个资源都由一个唯一的 URL 标识。
REST 服务使用标准的 HTTP 方法来对资源执行创建、读取、更新、删除(CRUD)操作。每个 HTTP 方法对应于资源上的特定操作。例如,GET 用于检索数据,POST 用于创建新资源,PUT 用于更新资源,DELETE 用于删除资源。这种方法为与资源交互提供了一个统一和一致的接口。这意味着在不同的资源之间始终一致地使用相同的 HTTP 动词和方法。
HTTP 状态码非常重要,并使 API 的集成和使用变得容易。同样,HTTP 状态码使 API 响应标准化,并允许应用程序适当地处理不同的场景。
HTTP 状态码
HTTP 状态码表示 HTTP 请求的结果,并帮助客户端理解其操作的结果。这些状态码对于客户端和服务器之间有效通信至关重要。
HTTP 状态码被分为五类:
-
信息响应(100-199)
-
成功响应(200-299)
-
重定向响应(300-399)
-
客户端错误响应(400-499)
-
服务器错误响应(500-599)
HTTP 状态码参考
您可以在此处了解更多关于状态码的信息:httpwg.org/specs/rfc9110.html#overview.of.status.codes。
每个状态码都有一个返回类型,可以被客户端应用程序或甚至浏览器使用。在发起 HTTP 请求时,响应有一个头部,其中包含 HTTP 状态码,甚至可能有一个主体提供关于请求响应的更多详细信息。
通常,主要使用的 HTTP 状态码如下:
-
200 OK:表示请求成功
-
201 已创建:表示资源已成功创建
-
400 错误请求:表示客户端请求中存在错误
-
401 未授权:表示客户端没有适当的认证
-
404 未找到:表示请求的资源不存在
-
500 内部服务器错误:表示服务器端存在问题
以下代码表示对 API 请求的成功响应示例:
HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "success",
"message": "Data retrieved successfully",
"data": {
"id": 123,
"name": "Example Resource",
"description": "This is an example resource
for the API.",
"created_at": "2023-10-26T10:00:00Z"
}
}
在这个例子中,状态码(在代码的第一行)是200,表示请求成功。此外,响应体中还包含更多信息。
正确使用 HTTP 状态码可以使 API 轻松集成到不同类型的系统中。随着我们使用它们创建 API,这一点将变得更加清晰。
BaaS 交付为团队提供了将每个上下文的职责分离到可以轻松集成到不同场景中的应用程序的好处。我们已经足够了解 API 的基础知识;现在是时候开始使用 ASP.NET Core 9 最小 API 创建 API 了。
探索最小 API
在 ASP.NET Core 9 中创建 Web 服务的一种方法是通过使用最小 API 方法,这提供了一种简单的方式来提供 API 并按需添加功能和配置。
最小 API 的简单结构允许开发者和团队以敏捷的方式提供基于 REST 的功能。
使用这种方法有许多适用场景,你选择哪种将取决于项目的规模和涉及的团队。事实上,最小 API 通常提供与基于控制器的模型相同的功能,我们将在下一节讨论。
要创建最小 API 项目,我们将基于产品管理模型。为此,我们将根据以下表格提供 API:
| 路由 | 描述 | 请求体 | 响应体 |
|---|---|---|---|
| GET /Product | 获取所有产品 | 无 | 产品数组 |
| GET /Product/ | 通过 ID 获取产品 | 无 | 产品对象 |
| POST /Product | 添加新产品 | 产品对象 | 产品对象 |
| PUT /Product/ | 更新现有产品 | 产品项 | 无 |
| DELETE /Product/ | 通过 ID 删除现有产品 | 无 | 无 |
表 3.1 - 产品管理操作
表 3.1 基本上映射了将在产品 API 中使用的路由,映射相应的 HTTP 动词。
我们还可以在表中看到,一些路由是相似的,只是使用的 HTTP 动词不同。这是 REST 模型中使用的约定,其中 HTTP 动词表示对给定资源的意图。
在这种情况下,资源是产品,由/Product路由定义。在某些情况下,/Product/{id}路由表示将在资源路由中添加一个参数。该参数将是资源 URL 的一部分,并将映射为 API 中要执行的方法的参数。
现在让我们创建一个最小的 API 项目并实现产品注册:
-
打开您操作系统的命令提示符,在您选择的目录中,并运行以下代码行:
dotnet new web --name ProductAPI -
Web 项目模板是创建空 ASP.NET Core 项目的快捷方式,它将被用作最小 API。
将创建一个名为ProductAPI的文件夹,其中包含项目所需的所有文件。主文件是Program.cs。
-
导航到ProductAPI目录,然后键入以下命令并按Enter:
Code.Visual Studio Code 编辑器将出现。
-
然后,打开Program.cs文件,它将具有以下结构:
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapGet("/", () => "Hello World!"); app.Run();如我们所见,没有类的定义。这是主要的应用程序文件,是用于执行 API 的入口点。
在文件的前两行中,我们有应用程序的定义,通过Web 应用程序构建器类。这个定义在第二章中已经介绍过,我们讨论了 ASP.NET Core 9 中项目的结构。然而,需要注意的是,应用程序将使用一些基本的配置被创建,这些配置由框架抽象化,例如过滤器和其他方面。
此文件的另一个重要方面是app变量的MapGet方法。这是一个扩展先前创建的应用程序的方法,允许创建一个将通过 URL 使用 HTTP GET 动词访问的路由。
此方法有一个参数定义了路由模式;在这种情况下,使用/,这意味着应用程序的根。第二个参数是一个操作,它使用 C#的一个特性。当请求此路由时,将执行此操作。
操作和方法
操作可以被视为内联定义的方法,由两个主要部分组成,就像方法一样:
- 设置参数:如有必要设置参数
- 操作体:将要执行的代码
操作可以被方法替代,而不是内联定义。
-
要进行测试,只需在提示符中键入以下命令运行应用程序:
dotnet run -
将显示包含 API URL 的日志。现在,打开Postman,选择文件 | 新建标签页,输入应用程序地址,然后点击发送:

图 3.2 – 使用最小 API 获取 API 资源
如我们所见,仅仅几行代码,就实现了执行一个 API,甚至返回一个简单的Hello World字符串。
最小 API 提供了以简单方式快速使 API 可用,并允许根据项目的需求添加其他功能和配置的能力。这为团队带来了极大的敏捷性。
让我们向ProductAPI项目添加一些功能。为此,在项目的根目录中创建一个名为Product.cs的类。该类将根据以下代码定义:
public class Product
{
public int Id {get; set;}
public string Name { get; set; }
public decimal Price { get; set; }
}
我们只是定义了一个将代表产品的对象。现在你需要更改Program.cs文件;我们将包括前面表格中列出的方法,映射 API 路由并添加到 API 方法的功能。
Program.cs文件将包含一些方法,如下面的示例所示:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
List<Product> products = new List<Product>();
app.MapGet("/Product", () => Results.Ok(products));
app.MapGet("/Product/{id}", (int id) => {
var product = products.FirstOrDefault(p => p.Id == id);
return Results.Ok(product);
});
app.MapPost("/Product", (Product product) => {
if (product != null)
{
product.Id = products.Count() + 1;
products.Add(product);
}
return Results.Ok(product);
});
app.MapPut("/Product/{id}", (int id,
Product updatedProduct) => {
if (updatedProduct != null)
{
var oldProduct = products.FirstOrDefault
(p => p.Id == id);
if (oldProduct == null) return Results.NotFound();
oldProduct.Name = updatedProduct.Name;
oldProduct.Price = updatedProduct.Price;
}
return Results.NoContent();
});
app.MapDelete("/Product/{id}", (int id) => {
var product = products.FirstOrDefault(p => p.Id == id);
if (product == null) return Results.NotFound();
products.Remove(product);
return Results.NoContent();
});
app.Run();
如我们所见,API 的创建方式与之前定义的路由表类似,遵循通过 MapGet、MapPost、MapPut 和 MapDelete 方法定义的 HTTP 动词。
有关问题的代码非常简单,创建了一个产品注册的模拟。为此,使用以下代码定义了一个变量:List
前面的代码中还描述了另一个重要的功能,即使用名为 Results 的实用工具类。此类在所有方法中使用,封装了返回请求的重要功能,例如在响应头中定义与请求相关的状态码。
让我们更详细地查看 POST 动词的 API。
MapPost 方法将 /Products 字符串定义为路由和操作;它期望一个产品作为参数,将其添加到列表中,并返回 OK(状态码 200)。
但我们如何向 API 提交一个产品?ASP.NET Core 9 有一个名为 bind 的概念,它处理请求并根据请求的需求创建和映射一个对象。在这种情况下,必须发送一个 JSON 格式的对象,当执行 POST 方法时,它将被映射到产品对象。
这是 ASP.NET Core 9 的一个优秀功能,它抽象了所有复杂性,并在执行路由时解决动作期望的参数。
让我们使用以下命令通过 API 添加一个产品:
-
使用以下命令运行应用程序:
dotnet run -
然后打开 Postman 并转到 文件 | 新建标签页。
-
将方法设置为 POST 并添加带有 / Product 后缀的 API 地址。
-
然后选择 图 3.3 中的 Body 选项卡。

图 3.3 – 配置 POST 请求
- 选择 raw 选项,然后将类型设置为 JSON,如 图 3.4 所示。

图 3.4 – 定义请求体
-
添加以下 JSON:
{ "id": 0, "Name": "Smartphone", "Price": 100 }前面的代码仅表示一个具有产品属性的 JSON 对象。这些属性在添加到项目的 Product.cs 类中定义。您可以通过书中提供的源代码链接查看完整代码,该链接位于 技术要求 部分。
-
点击 发送 按钮。

图 3.5 – POST 请求结果
因此,返回了注册的产品对象。注意在 图 3.5 中突出显示的 HTTP 状态码,其值为 200,描述为 OK。
当执行对 API 的 POST 调用时,ASP.NET Core 识别了一个映射到 HTTP 动词的路由。然后,它将作为请求体发送的 JSON 对象绑定到定义为 POST 请求参数的产品对象上。之后,执行动作请求,最终在内存列表中注册一个产品并返回,再次序列化为 JSON。
正如我们所学的,使用最小 API 创建 API 非常简单。仅用几行代码,就创建了一个完整的产品注册。当然,作为一个.NET 平台项目,可以定义不同的类来更好地组织项目,因为随着复杂性的增加,以及 API 数量的增加,仅在一个文件中管理所有路由将变得非常困难。
然而,尽管最小 API 支持大多数 ASP.NET Core 9 用于创建 Web API 的功能,但使用更结构化和准备好的方法来处理大型项目可能是一个很好的选择,这就是基于控制器的项目的情况,我们将在下一节中讨论。
使用基于控制器的方法实现 API
基于控制器的项目是另一种使用 ASP.NET Core 9 提供 API 的方法。此项目类型也实现了我们在第二章中学到的模型-视图-控制器(MVC)模式。
基于控制器的方法具有更完整和健壮的结构来提供任何类型的 API,因此它支持不同的业务上下文。与最小 API 一样,可以通过添加不同类型的配置和自定义来扩展 API 功能。
创建基于控制器的 API
要创建基于控制器的 API,你只需要在终端中输入以下代码,在所选目录中:
dotnet new webapi -n ProductMVC -controllers true
此命令使用webapi模板,默认情况下创建一个最小 API 项目。在这种情况下,我们添加了一个-controllers参数来指明应该使用基于控制器的方法创建 Web API。
如图 3.6所示,基于控制器的 API 项目结构与我们在第二章中学到的 MVC 非常相似。

图 3.6 – 基于控制器的项目结构
与最小 API 相比,这种项目与责任分离和项目组织不同。由于每个控制器都与一个特定的资源相关联,因此不需要在Program.cs文件中实现所有 API 代码,这除了带来更大的可能性外,尤其是在大型项目中。
然而,配置和扩展的方法与我们关于最小 API 所学的类似;所有这些都是在Program.cs文件中完成的。
默认项目已经定义了一些设置,这些设置可以很容易地进行修改。以下代码来自自动创建的Program.cs文件:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
在这一点上,我们将关注文件中添加的两个主要配置,它们与AddEndpointsApiExplorer和MapControllers方法有关。其他方法将在稍后讨论:
-
AddEndpointsApiExplorer:这是一个扩展方法,旨在注册用于公开应用程序端点信息的服务。这些信息被 API 文档生成服务(如Swagger)使用,我们将在下一节讨论。
-
MapControllers:这是一个应用程序配置,负责将添加的属性映射到控制器类中,因此自动定义 API 和路由。
这些方法使应用程序更容易适应将服务公开为 API 的需求,并使实现良好实践变得更具动态性,使任何添加和修改都更加灵活。
使用之前在最小 API 方法中创建的产品服务示例,让我们理解这个针对控制器模型调整的实现。
理解产品控制器
使用基于控制器的产品 API 实现遵循类定义模型。因此,每个 API 都必须有一个控制器,该控制器将负责处理每个资源的请求。
基于前面的示例,产品 API 将具有以下定义:
[ApiController]
[Route("[controller]")]
public class ProductController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
// ..
}
[HttpGet("{id}")]
public IActionResult Get(int id)
{
// ..
}
[HttpPost]
public IActionResult Post(Product product)
{
//..
}
[HttpPut]
public IActionResult Put(int id,
Product updatedProduct)
{
//..
}
[HttpDelete]
public IActionResult Delete(int id)
{
//..
}
}
为了使阅读和理解这个类定义中最重要的点更加容易,一些代码块已被省略。
该类代表一个控制器,用于处理对产品 API 的请求。
让我们分析一下代码的一些细节:
-
ApiController:这是一个添加到类中的属性,导致它被映射为 API 控制器。这样,当将MapController配置添加到Program.cs文件时,所有标记为属性的类都将负责处理相应 API 的请求。
-
Route:这个属性可以在控制器(由一个类表示)或动作(由一个方法表示)中使用。它的功能是定义路由的 URL 模式。[controller]参数是一个在运行时自动替换的令牌,替换为不带controller后缀的类名。为了使路由考虑方法名,在用路由属性注解动作的情况下,必须使用[action]令牌。
-
ProductController:这是遵循 MVC 约定的类名。没有义务使用这个后缀,但这是一个好习惯,因为控制器不需要放在Controllers文件夹中,这样其他开发团队成员阅读起来更方便。
-
ControllerBase:所有控制器类都应该继承自ControllerBase类,这对于 API 来说是合适的。这个类提供了许多处理 HTTP 请求时有用的属性和方法。
-
HttpGet:此属性确定一个操作应该响应的 HTTP 动词。对于每个动词,都有一个不同的属性。在先前的示例中,有两个 GET 方法,但一个是重载,有一个参数。为了控制器知道应该请求哪个 GET 方法,必须为具有相同名称的方法定义不同的路由。在这种情况下,第二个 GET 方法有一个{id}参数,它将被包含在路由中,区分操作。ASP.NET Core 9 框架将负责绑定方法中的id参数。
如我们所见,与最小 API 的创新方法相比,一个很大的不同之处在于能够将责任分割到不同的控制器中。此外,基于控制器的方法还带来了在大项目中非常重要的其他功能,例如通过框架和其他在ControllerBase中可用的资源提供的各种类型的实用工具,例如绑定资源和模型验证。
ControllerBase 实用工具
如前所述,ControllerBase 类具有一些属性和方法,这些属性和方法对于处理 HTTP 请求非常有用,使得 API 能够使用 REST API 的最佳实践和约定来处理请求。
通过 HTTP 进行应用程序间通信的标准非常广泛,为了正确地涵盖这个主题,可能需要一本完全致力于此主题的书。但是,让我们关注在ProductController类中使用的某些模式。
正如我们在本章开头所学的,API 中的请求是针对特定资源的,对于每个请求,都有一个与动词相关联的意图。每个请求都有一组信息发送到 API,例如正文和头信息。同样,在处理之后,此请求返回头信息,并且除了头信息外,还可能包含正文。响应中还定义了一个 HTTP 状态码。
此整个模式被ControllerBase中可用的方法抽象化,它负责处理每个请求的返回定义。
让我们分析通过id检索产品的 GET 方法:
public IActionResult Get(int id)
{
var product = ProductService.Get(id);
if (product is null) return NotFound();
return Ok(product);
}
此方法的目的是根据作为方法参数传递的 ID 返回一个产品。例如,此 API 可以在前端使用,用户点击产品链接以查看其详细信息。由于 API 消费者无法访问实现细节,API 需要保持一致性,以返回适当的响应。
在前述方法的情况下,如果找不到产品,将返回一个NotFound响应,使用 HTTP 状态码 404。这与通过浏览器尝试访问不存在的 URL 的方法相同,通常显示一个 404 消息,表明未找到资源。
另一方面,如果找到产品,将返回一个Ok响应,使用 HTTP 状态码 200。请注意,Ok方法有一个参数,正好是找到的产品。在这种情况下,此对象将以 JSON 格式序列化并返回给客户端。Ok方法负责序列化对象并创建响应,考虑到正文、序列化的产品对象和头信息,包括声明Content-Type为application/json。这样,客户端可以正确处理返回的消息。
除了实现 REST 标准和约定外,ControllerBase类还有其他几个方法,这些方法抽象了与 HTTP 协议交互的复杂性。
重要的是要注意,API 服务于不同类型的客户端,无论是准备好的前端、操作系统还是移动应用程序,它们甚至允许系统之间的集成。每个消费者对 API 实现没有任何细节,只能访问所需方法及其参数的签名以及可能的返回值。
因此,正确使用标准和约定是必要的,这可以使 API 在消费者之间保持一致性和互操作性。
更多详情
如果你想了解更多关于ControllerBase类的信息,请参阅以下链接中的文档:learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase?view=aspnetcore-9.0。
ASP.NET Core 9 通过使用 API 来简化这种服务交付模型中的大部分复杂性,同时还提供了其他类型的功能,例如一致的验证模型和对象绑定。
与绑定一起工作
绑定是 ASP.NET Core 中可用的重要功能。它们的主要功能是将 API 请求模型转换为控制器中执行的操作。
在我们观察到的ProductController调用示例中,方法接收的参数是这些原始类型,例如int,甚至是复杂类型,例如product类型的对象。
每个方法或操作都有一个签名或接口,它描述了方法或操作处理所需的属性(如果有)。当你对操作发出请求并按照操作签名输入属性时,这些属性将通过 ASP.NET Core 管道执行流程进行映射,它会绑定每个输入属性的信息,考虑到其类型、属性名称和值。
例如,product对象具有以下属性:
public int Id {get; set;}
public string Name { get; set; }
public decimal Price { get; set; }
这个以 JSON 格式表示的对象可以这样定义:
{
"id": 1,
"name": "Smartphone",
"price": 1000.0
}
如我们所见,这些对象是相同的,但表示方式不同。ASP.NET Core 负责将 JSON 格式的product对象转换为 C#格式的product对象,并按照名称映射属性。
这是框架的标准行为,但存在自定义和绑定请求不同方面的可能性。
如我们所知,一个请求包含主体、URL、查询字符串参数,以及通过表单发送的参数。在 C#中,请求有一个名为HttpRequest的对象抽象。你可以通过之前提到的ControllerBase类的Request属性轻松访问请求的所有属性。
如果有必要,例如,从查询字符串中获取一个值,可以使用以下代码:
string fullname1 = Request.QueryString["fullname"];
string fullname2 = Request["fullname"];
然而,这个相同的值也可以通过使用FromQuery属性提供的绑定模型从查询字符串中获取:
[HttpGet]
public IActionResult GetTasks([FromQuery]bool
isCompleted = false)
{
// ..
}
如前述代码所示,isCompleted被注解为FromQuery属性。这样,ASP.NET 将负责将查询字符串绑定到操作参数。在这种情况下,预期查询字符串的名称与方法参数相同。但如果不是这种情况,只需使用属性重载并定义参数名称,如下所示:
public IActionResult GetTasks([FromQuery("completed")]bool
isCompleted = false) { /**/ }
还可以使用其他类型的属性来执行绑定:
| Attribute | HTTP verb | When to use | Data format | Example of use |
|---|---|---|---|---|
| FromBody | POST, PUT, PATH | 用于从请求主体绑定参数数据。它只能在每个操作方法中使用一次,因为它假设整个请求主体用于绑定到操作参数。 | JSON, XML | [ HttpPost]****public IActionResult Create([FromBody] Product product) { ... } |
| FromForm | POST | 用于从表单字段绑定参数数据。 | 表单数据(键值对) | [ HttpPost]****public IActionResult Update([FromForm] ProductUpdateDto dto) { ... } |
| FromService | Any | 用于直接将服务注入到操作方法中。这在无需构造函数注入的情况下获取服务时很有用。 | 依赖于注入的服务 | public IActionResult Get([FromServices] IProductService productService) { ... } |
| FromHeader | Any | 当需要从 HTTP 头中检索数据时使用。对于令牌或 API 版本控制很有用。 | 简单字符串或单个头中的逗号分隔值 | public IActionResult Get([FromHeader(Name = "X-Custom-Header")] string value) { ... } |
| FromQuery | GET | 用于从 URL 的查询字符串绑定参数。在 RESTful API 中,对于过滤或分页参数来说很理想。 | 如字符串、整数或自定义可转换为字符串的类型等简单类型 | public IActionResult Search([FromQuery] string keyword) { ... } |
| FromRoute | Any | 当参数值嵌入在 URL 路径中时使用。通常与包含资源 ID 的 REST URL 一起使用。 | 与 URL 段兼容的简单类型 | [HttpGet("{id}")] public IActionResult GetById([FromRoute] int id) { ... } |
这些参数中的每一个都可以用作自定义控制器中每个操作的绑定模型的一种方式。
自定义绑定
在某些情况下,ASP.NET Core 中可用的默认绑定模型可能相对于应用程序的需求有限,这通常会有其他更复杂的数据类型。因此,可以实现自定义绑定。这种实现超出了本书的范围,但您可以在以下链接中了解更多信息:learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-9.0。
ASP.NET Core 中可用的绑定模型通过处理每个操作所需值的填充,抽象了许多实现复杂性。然而,没有保证参数是否正确地根据应用程序的业务规则进行了填充。为此,您需要执行验证,而 ASP.NET Core 提供了一个强大的验证模型。
执行验证
当谈到创建健壮的 API 时,模型验证是基本支柱之一。ASP.NET Core 9 通过 ModelState 使这一过程比以往任何时候都更容易、更强大,将其视为一个边境守卫,在数据进入应用程序核心之前进行检查和验证。
ModelState 是 ASP.NET Core 中的一个框架,用于验证数据是否符合在模型中定义的规则。如果某条数据不符合验证标准,ModelState 会将其标记为无效。
让我们来看看产品注册 API:
[HttpPost]
public IActionResult Post(Product product)
{
if (product == null) return BadRequest();
if (!ModelState.IsValid)
return BadRequest(ModelState);
// ..
}
如我们所见,有一个条件用于评估 ModelState.IsValid 属性。如果为假,则返回一个 HTTP 状态码 400(表示请求错误),其中包含一个表示 ModelState 对象的正文:
{
"Name": [
"The field Name is required"
]
}
ModelState 实际上是一个字典,当以 JSON 格式序列化时,它表示为一个对象。每个对象属性代表一个已验证的属性。每个对象属性的值由包含验证结果的字符串数组表示。
为了让 ModelState 考虑模型是否有效,有必要用验证属性注释对象的属性;否则,验证将被忽略。
通过向 Name 属性添加验证,修改了产品类,如下代码所示:
public class Product
{
public int Id {get; set;}
[Required(ErrorMessage ="The field Name is required")]
[MinLength(3, ErrorMessage = "The Name field must have
at least 3 characters.")]
public string Name { get; set; }
public decimal Price { get; set; }
}
如我们所见,Name 属性被认为是必需的,并且还必须至少包含三个字符。这样,就可以在同一个属性中组合验证属性,并且这是由 ASP.NET Core 9 提供的操作执行流程中的 ModelState 管理的。
验证是 API 的一部分,无论是通过添加到模型中的属性,还是通过在操作体中使用 ModelState.AddModelError 方法手动进行,如下例所示:
if (product.Price < 0) ModelState.AddModelError("Price",
"The Price field cannot have a value less than zero.");
if (!ModelState.IsValid)
return BadRequest(ModelState);
其他属性
ASP.NET Core 还提供了其他一些可以用于模型验证的属性:learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-9.0#built-in-attributes。
如我们所见,ASP.NET Core 9 为管理 API 使用的模型状态提供了一个出色的功能,无论是使用参数还是对象,都使我们能够在应用程序的端点上以丰富的方式进行验证,同时为我们提供了一个简单的方式来维护信息的完整性。
这些功能在使用文档、响应格式化和错误管理等方法的帮助下变得更加强大。因此,在下一节中,我们将看到如何使 API 对消费者更加一致。
与文档一起工作
API 是通过服务传递应用程序业务模型的有力资源,为了使 API 项目得到适当提供,添加标准化与客户交互模型的功能非常重要。
为了实现这一点,每个 API 都必须进行文档化,使客户了解哪些资源可用以及如何进行此文档化。
因此,让我们学习如何通过 Swagger 的 NuGet 包自动从 API 功能文档中获益,该包实现了 OpenAPI 规范。
使用 Swagger 记录 API
客户端和其他应用程序通过 HTTP 协议使用 API,其中存在请求和响应。为了使这种通信发生,有必要了解 API 提供的内容,在这种情况下,哪些方法是可用的,以及使用哪些契约来建立连接。
要做到这一点,我们必须建立一个关于 API 提供的资源(如方法、HTTP 动词、参数和主体)的知识来源。为了实现这一目标,有必要拥有文档。
然而,这种文档需要是动态的,因为在开发过程中,API 可以不断变化,增加功能或新特性。对每一部分文档进行更改并将其发送给所有 API 消费者将是劳动密集型的。
ASP.NET Core 9 仍然支持 Swagger 来提供 API 文档。然而,与之前的版本不同,Swagger 现在不再是项目模板的一部分。新项目现在可以支持基于控制器和最小 API 应用程序的 OpenAPI 文档生成。OpenAI 规范提供了一种编程语言无关的 API 文档方法。因此,ASP.NET Core 9 通过 Microsoft.AspNetCore.OpenAI 包内置了对生成应用程序中端点信息的支持,避免了对外部库的依赖。
因此,为了拥有文档以及使用 UI 测试 API 的体验,我们将 API 项目与 Swagger 集成,Swagger 是一套易于使用的 API 开发者工具,除了实现 OpenAPI 规范标准之外。
OpenAPI 规范
OpenAPI 规范是 Linux 基金会的一部分,旨在指定 RESTful 接口,以简化 API 的开发和消费。您可以在 spec.openapis.org/oas/latest.html 了解更多关于 OpenAPI 的信息。
要了解更多关于 ASP.NET Core 9 OpenAPI 的信息,请访问以下网址:learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/overview?view=aspnetcore-9.0
当将 Swagger 集成到您的 API 解决方案中时,Swagger 在 OpenAPI 格式下充当规范生成器,该格式基于 JSON 文件,其中描述了您应用程序中所有可用的 API:
{
"openapi": "3.0.1",
"info": {
"title": "API V1",
"version": "v1"
},
"paths": {
"/api/Todo": {
"get": {
"tags": [
"Todo"
],
"operationId": "ApiTodoGet",
"responses": {
"200": {
"description": "Success",
"content": {
"text/plain": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ToDoItem"
}
}
},
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ToDoItem"
}
}
},
"text/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ToDoItem"
}
}
}
}
}
}
},
"post": {
…
}
},
"/api/Todo/{id}": {
"get": {
…
},
"put": {
…
},
"delete": {
…
}
}
},
"components": {
"schemas": {
"ToDoItem": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int32"
},
"name": {
"type": "string",
"nullable": true
},
"isCompleted": {
"type": "boolean"
}
},
"additionalProperties": false
}
}
}
}
前面的 JSON 描述了 API 可用的资源、响应模式以及可用的动词,以及 API 中使用的对象预测。
要将 Swagger 集成到项目中,我们必须在项目目录中运行以下命令行命令,添加 Nuget 包:
dotnet add package Swashbuckle.AspNetCore
接下来,我们必须更改 Program.cs 文件。我们将为此配置 Product MVC 项目,您可以在下面的代码中分析添加 Swagger 后的更改:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
实现包括以下内容:
-
第 4 行通过 builder.Services.AddSwaggerGen() 方法添加了 Swagger 生成服务。
-
在第 6 行和第 10 行之间,我们通过 app.UseSwagger() 方法将 Swagger 添加到 ASP.NET 执行管道中,并通过 app.UseSwaggerUI() 方法提供 UI。这些方法仅在应用程序以开发模式运行时执行,if (app.Environment.IsDevelopment)。
当运行应用程序时,只需通过带有 swagger 后缀的 API 链接访问,如图 图 3.7 所示:

图 3.7 – 产品 MVC API 的 Swagger UI
查看 图 3.7 ,我们可以看到产品 API 中可用的方法在 UI 中列出,以及 API 上工作的对象规范。不需要对源代码进行任何更改。
Swagger 识别源代码中可用的控制器和操作,生成规范,并随后生成 UI。然而,可以在文档中添加更多详细信息,以丰富 API 使用模型。让我们更详细地看看如何使用 Swagger 包中提供的功能来改进文档。
改进文档
如我们之前所学的,Swagger 默认添加到 ASP.NET Core 9 API 项目中,并自动生成一个包含 API 使用详情的最小版本的 UI,通过读取控制器和操作来推断数据。
正如我们在图 3.8中可以看到的那样,要添加产品,必须提供作为请求体的 JSON;此外,我们还有一个包含 HTTP 状态码 200 的响应描述,该状态码表示成功。

图 3.8 – API 的文档详情
然而,如果我们查看 POST 方法代码(通过书中提到的技术要求部分中可用的ProductController类提供),该代码用于注册产品,没有明确定义 HTTP 状态码 200:
[HttpPost]
public IActionResult Post(Product product)
{
if (product == null) return BadRequest();
if (product.Price < 0)
ModelState.AddModelError("Price",
"The Price field cannot have a value less
than zero.");
if (!ModelState.IsValid)
return BadRequest(ModelState);
product.Id = ProductService.Products.Count() + 1;
ProductService.Add(product);
return CreatedAtAction(nameof(Get),
new {id = product.Id}, product);
}
此方法返回两种可能的 HTTP 状态码,分别是 400,由调用BadRequest方法表示,以及 201,由CreatedAtAction方法表示。
正如我们在图 3.8中可以看到的那样,有一个标记为试一试的按钮。点击此按钮后,UI 将准备就绪,以便可以添加请求体,在这种情况下,将是一些表示产品和其相应属性的 JSON。修改 JSON 以添加新产品,定义属性如图 3.9中提出的示例。

图 3.9 – 从 Swagger UI 运行 API 请求
在定义请求体之后,点击执行按钮。只需确保你的应用程序正在运行。
执行结束后,Swagger UI 显示 API 响应,正如我们可以在图 3.10中清楚地看到的那样,我们有一个 HTTP 状态码 201,以及新注册产品的 JSON 和一些头部信息。

图 3.10 – Swagger API 响应屏幕
CreatedAtAction方法创建一个带有 HTTP 状态码 201 的响应,并在头部添加一个链接,用于通过 GET 方法访问创建的资源,如前图所示,地址为http://localhost:5037/Product/1。这个地址可能因你环境中的执行地址而异。
这种类型的返回是良好的实践,遵循 REST 协议中定义的标准。然而,尽管在我们展示示例的上下文中这不是一个主要问题,API 消费者必须清楚如何消费以及期望得到什么,以便正确处理每个响应。在产品注册方法的情况下,没有信息意味着此方法也会返回错误状态,这可能会对 API 消费者造成一些不合规。
为了调整这种行为,我们必须使用 ASP.NET Core 9 提供的属性,如 ProducesResponseType 和 Consumes,向 API 方法添加更多信息。
ProducesResponseType 属性用于确定将作为响应返回的 HTTP 状态码的类型以及返回的内容类型。此属性还可以用于泛型版本,指定返回类型。
Consumes 属性确定 API 期望的内容类型。内容被定义为媒体类型,完整的列表可以通过 System.Net.Mime 命名空间中可用的 MediaTypeNames 类获得。
让我们分析通过添加属性的新实现的 POST 方法:
[HttpPost]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType<Product>(StatusCodes
.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public IActionResult Post(Product product)
{
// code omitted for readability
}
如前述代码所示,ProducesResponseType 属性可以根据需要添加多次,以表示不同的返回类型。在此示例中,报告了 HTTP 状态码 201,用于创建的项目,以及一个带有 HTTP 状态码 400 的返回类型。
当再次运行应用程序时,我们可以观察到对代码所做的更改以及根据 图 3.11 自动生成的 Swagger UI:

图 3.11 – API 响应文档
现在我们已经有了关于产品控制器 POST 方法所涉及方面的正确文档,这使得 API 能够通过考虑替代响应流程来适当消费。
XML 注释
除了添加到 API 方法的属性之外,还可以使用 XML 注释为每个方法提供作为 Swagger UI 文档的一部分。为此,需要配置项目,以便在编译过程中生成文档 XML,并在 Swagger UI 中获取。您可以在以下地址找到此配置的完整说明:learn.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle?view=aspnetcore-9.0&tabs=visual-studio#xml-comments。
除了能够使用 ASP.NET Core 9 创建高质量的 API 之外,我们还拥有丰富的 API 文档界面支持,以及执行请求和获取有关请求参数和响应的更多细节的选项。
理解这种文档方法将极大地帮助您生成可以集成到不同系统和环境中的高质量服务。随着我们进入接下来的几章,我们将进一步研究 API、文档以及其他技术,如数据库连接的使用。
摘要
在本章中,我们深入探讨了通过 HTTP 提供的 API 的世界,发现了它们为各种客户端提供服务的能力。在 ASP.NET Core 的强大支持下,我们学习了如何充分利用这一潜力,了解了诸如使用最小 API 快速高效地创建 HTTP API 等方法。我们还探讨了使用基于控制器的项目创建健壮 API 的过程。我们还考察了涉及 API 的其他方面,例如文档。在下一章中,我们将继续探索 ASP.NET Core 9 的特性,了解如何使用 SignalR 开发实时应用程序。
第四章:使用 SignalR 进行实时交互
作为 ASP.NET Core 9 的一部分,Web API 是一个用于构建 HTTP 服务的框架,可以为网页消费和移动应用提供。.Net Core 平台提供的结构为 API 的开发提供了高质量和性能。在本章中,我们将更多地了解 WebAPI,包括通过服务提供解决方案的标准、约定和最佳实践。
我们将致力于创建一个实时任务管理应用,我们将使用.NET 平台和 SignalR 提供的各种技术来了解概念,并实现使用流概念的示例应用程序。我们还将探讨在服务器上托管 SignalR 应用的前提条件。
在本章中,我们将涵盖以下主要主题:
-
什么是 SignalR?
-
理解服务器和客户端的概念
-
与流式处理一起工作
-
托管 ASP.NET Core SignalR 应用
技术要求
本章中使用的代码示例可以在本书的 GitHub 仓库中找到:
github.com/PacktPublishing/ASP.NET-Core-9.0-Essentials/tree/main/Chapter04
什么是 SignalR?
正如我们在前面的章节中学到的,基于 Web 的应用有两个部分:客户端和服务器。
浏览器通常代表客户端,用户与应用程序进行交互。应用程序在服务器上执行操作以处理信息并返回响应。
这个过程基于两个阶段,即请求和响应。这两个阶段按顺序发生。每当客户端和服务器之间发生交互时,就会创建一个新的通信过程。
大多数 Web 应用都有这些功能,对于大多数商业环境来说已经足够。然而,在某些场景中,需要实时通信模型,其中应用程序处理的信息是不断更新的。这为用户提供即时响应,丰富了可用性和某些功能需求。
一个很好的实时应用的例子,比如,一个地图应用,用户可以在给定的路线上获得交通信息,并被告知任何影响导航的方面。此外,其他类型的应用,如游戏、社交网络,甚至是协作网络文本编辑器,都依赖于对用户的持续更新。
要开发包含实时功能的应用程序,服务器和客户端需要能够在活动通道上持续通信。
.NET 平台有 SignalR。这是一个库,它以简化的方式增加了构建实时解决方案的能力,立即允许客户端和服务器之间进行持续的通信。

图 4.1 – SignalR 组件
正如你所见,图 4.1 展示了 SignalR 的多数组件,这些组件抽象了客户端和服务器之间的通信模型。客户端和服务器之间的通信通过一个活跃的连接进行,使用传输技术传输 JSON 或二进制消息。WebSockets 是 SignalR 使用的标准通信技术。其他两种选项作为后备使用。换句话说,如果 WebSockets 不受支持,将立即使用服务器发送事件或长轮询。传输的优先级顺序与以下列表中建立的顺序相同:
-
WebSockets:它提供了建立 全双工连接 的能力,即允许客户端和服务器之间进行持续的通信。
-
服务器发送事件:它从服务器到客户端建立一个 单向连接。客户端没有能力通过相同的连接向服务器发送消息,需要单独的 HTTP 请求。
-
长轮询:这是一种更基本的技巧,客户端向服务器发送消息。服务器不会立即发送消息,而是处理信息,并在完成后才返回响应。
SignalR 抽象了传输的选择,使得在必要时只使用 WebSocket 成为可能。
客户端和服务器之间的连接依赖于一个重要的组件,称为 Hub。Hub 是一个特殊对象。它是 SignalR API 的一部分,充当代理,允许服务器和客户端之间的通信,其中服务器可以通过使用 RPC(远程 过程调用)在客户端远程执行函数或方法。
RPC
RPC 是一种自 1970 年以来就存在的通信协议,它是目前存在的一些创新(如 Google 开发的高性能通信模型 gRPC)的基础。你可以在 en.wikipedia.org/wiki/Remote_procedure_call 找到更多关于 RPC 的信息。
SignalR 抽象了连接和通信管理的所有复杂性,同时带来了其他功能,例如向所有已连接客户端、特定客户端或一组客户端发送通知。此外,API 可以与 .NET 应用程序(包括控制台、Java 和 JavaScript)一起使用。
例如,可以有一个与在 .NET 或甚至 Java 平台上开发的控制台应用程序通信的服务器。
起初可能看起来很复杂,但当我们了解主要概念和良好实践时,我们将了解到 SignalR 库是多么强大。让我们探索一些涉及 SignalR 的概念、模式和良好实践,以及如何使用这个库开发实时应用。
理解服务器和客户端的概念
如我们之前所学,SignalR 是一个强大的库,它抽象了创建实时应用的大部分复杂性。
然而,了解与使用 SignalR 库相关的概念和标准对于充分利用其功能非常重要。
如我们所知,Web 应用程序基本上有两个主要组件,客户端和服务器。同样,使用 SignalR 的实时应用程序也需要客户端和服务器组件。我们将通过任务管理应用程序了解这些组件如何相互交互。
使用任务管理应用程序进行工作
任务管理应用程序将使用 Razor Pages 技术创建,并具有以下功能:
-
实时实现概念
-
创建任务
-
完成任务
-
查看创建的任务
-
查看已完成任务
所有功能都将使用 Visual Studio Code 解决。JavaScript 将用于在客户端处理功能,而 C# 将用于服务器端。
我们可以在 图 4.2 中看到应用程序中使用的主要组件的概述:

图 4.2 – TaskManager 应用程序组件
如我们在 图 4.2 中所见,我们将使用一些重要的组件,这些组件将在应用程序中使用。
客户端将使用 Razor Pages 实现,旨在允许用户与应用程序功能进行交互:
-
index-page.js 文件将负责管理服务器与应用程序主页面之间的交互。
-
signalr.js 文件是 SignalR JavaScript SDK 的一部分。
-
服务器是作为协调服务器运行的 Razor Page 应用程序。
-
Hub 实现将负责管理服务器和客户端之间的实时通信。
在创建项目时,我们将解释每个组件的工作原理和实现细节。现在,让我们从创建项目开始。
我们将关注创建任务管理器项目的关键活动。然而,您可以在书中提到的 GitHub 仓库中查看解决方案的完整实现,该仓库在 技术要求 部分中提及。以下是我们将遵循的步骤:
-
要创建项目,请在您选择的目录中打开终端并运行以下命令:
dotnet new webapp -o TaskManager -
将创建一个名为 TaskManager 的新文件夹,包含整个项目结构。使用以下命令访问此页面:
cd TaskManager -
现在项目已创建,我们需要添加 SignalR JavaScript SDK。不需要将 SDK 添加到服务器,因为它在创建项目时自动添加,作为 .NET 平台的一部分。
由于我们使用 Razor Pages,客户端和服务器将位于同一项目中。然而,我们可以在 Asp.NET Core 9 中创建一个 单页应用程序 ( SPA ) 解决方案和 WebAPI,并执行相同的步骤。
我们将继续使用 Razor 页面,并转到 SignalR JavaScript SDK 的安装。我们将使用一个名为LibMan的工具,它是来自 Microsoft 的命令行界面(CLI),负责管理客户端库。其操作类似于Node Package Management(NPM)。
使用它之前,建议卸载操作系统上存在的任何先前版本。因此,我们将遵循以下步骤:
-
按顺序运行以下命令来安装工具:
dotnet to LibraryManager.Cli ol uninstall -g Microsoft.Web. dotnet tool install -g Microsoft.Web.LibraryManager.Cli -
接下来,我们将运行命令来安装 SignalR SDK。运行以下命令并检查应用程序的主目录:
libman install @microsoft/signalr@latest -p unpkg -d wwwroot/js/signalr --files dist/browser/signalr.js之前的命令避免了安装@microsoft/signalr@latest库,然后只向应用程序目录添加了必要的脚本。
在此刻,项目正在创建并准备接收实时功能的实现。我们必须首先创建中心节点(Hub)并配置应用程序。
创建中心节点(Hub)
中心节点(Hub)是实现 SignalR 最重要的组件之一。它充当一个代理,管理所有客户端与服务器之间的连接,并允许客户端和服务器相互通信以实时执行方法。
对于我们的TaskManager应用程序,我们的第一个任务将是创建一个中心节点(Hub)并为其准备与客户端通信。为此,仍然在终端和应用程序目录中,输入以下命令:
code .
此命令将在应用程序目录中打开一个 VS Code 实例。
在项目根目录下创建一个名为Hubs的文件夹,然后创建一个名为TaskManagerHub.cs的文件。
项目类将有两个方法,其代码如下:
public class TaskManagerHub : Hub
{
public async Task CreateTask(TaskModel taskModel)
{
// ..
}
public async Task CompleteTask(TaskModel taskModel)
{
// ..
}
}
需要注意的第一个细节是继承自Hub类。Hub类是一个超类,它抽象了所有连接管理和与客户端的交互,这些功能通过Microsoft.AspNetCore.SignalR包提供。所有自定义Hub类都必须从这个类继承。
接下来,我们有CreateTask和CompleteTask方法。这些方法将通过客户端调用,并同时将在客户端上调用方法。
CreateTask方法接收一个名为TaskModel的类作为参数。此类已被拆分到Model/TaskModel.cs目录中:
public class TaskModel
{
public Guid Id { get; } = Guid.NewGuid();
public string Name { get; set; }
public bool IsCompleted { get; set; }
public TaskModel()
{
IsCompleted = false;
}
public TaskModel(string name) : this()
{
Name = name;
}
public TaskModel(string name, bool isCompleted)
{
Name = name;
IsCompleted = isCompleted;
}
}
如我们所见,TaskModel类只有几个基本属性,如Id、Name和IsCompleted,它们代表一个任务。
客户端和服务器之间的通信是通过传输策略完成的,如前所述,通常使用 WebSockets。传输的信息以 JSON 或二进制格式序列化。然而,二进制数据,通常用于音频、图像和视频,不受支持。只传输文本数据。
现在,让我们看看CreateTask方法的完整实现:
public async Task CreateTask(TaskModel taskModel)
{
_taskRepository.Save(taskModel);
await Clients.All.SendAsync(ClientConstants
.NOTIFY_TASK_MANAGER, taskModel);
}
上述代码执行了两个基本步骤:
-
持久化任务:_taskRepository属性是一个抽象与持久化层通信的接口。对于这个持久化项目,它是在内存中完成的,并且这个实现的完整代码可以在书的 GitHub 仓库中找到(见技术 要求部分)。
-
通知客户:Hub基类有一个Client属性,具有一些功能。在代码示例中,正在向所有连接到Hub的潜在客户端发送通知。SendAsync方法有 10 种不同变体的重载。然而,对于前面的代码,使用了两个主要参数。第一个参数涉及将处理 Hub 响应的客户端方法名称,而第二个参数是本身将被发送到客户端的任务对象。
常量是最佳实践
正如我们在SendAsync方法中提到的,第一个参数引用了一个常量。这是一个好习惯,因为需要知道将通过服务器进行的通信处理的方法名称。由于它是一个字符串,很容易出错。在必要时使用常量来集中包含方法名称的字符串。这将有助于维护和改进。
随着 Hub 的部署完成,现在需要配置应用程序以通过 SignalR 处理通信。
准备服务器应用程序
应用程序需要配置以能够处理客户端和服务器之间的连接性。没有这一步,Hub 将没有用处。
要做到这一点,我们需要更改Program.cs文件中的代码并添加一些重要的代码行。
我们必须在应用程序容器中配置 SignalR 服务并映射客户端将用于建立连接的 Hub 端点。
在更改完成后,文件应类似于以下代码:
using TaskManager.Hubs;
using TaskManager.Service;
using TaskManager.Service.Contract;
var builder = WebApplication.CreateBuilder(args);
// Add Razor Page services to the container.
builder.Services.AddRazorPages();
//Add SignalR Services
builder.Services.AddSignalR();
// ..
var app = builder.Build();
// Some codes have been omitted to facilitate learning
app.MapRazorPages();
// Add Hub Endpoint
app.MapHub<TaskManagerHub>("/taskmanagerhub");
app.Run();
非常重要的是要遵循设置顺序。在builder.Services.AddSignalR()方法被添加到var app = builder.Build()行之前。同样,Hub 路由映射是在app.MapRazorPages()语句之后添加的。
需要注意的是,Hub 路由的映射,配置为/taskmanagerhub。Hub 路由定义遵循相同的 REST API 模式,并且将使用此相同的路由,以便客户端应用程序能够与服务器建立连接。客户端将使用之前安装的 SignalR JavaScript SDK 连接到 Hub。
Hub 已配置并准备好接收连接。现在,是时候配置客户端了。
准备客户端应用程序
在配置好 Hub 后,我们必须向客户端应用程序添加必要的功能。为此,我们将使用Pages/index.cshtml页面并创建 JavaScript 代码,该代码将协调客户端和服务器之间的所有交互。
将Index.cshtml页面的全部内容更改为以下代码:
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div class="text-center">
<h1 class="display-4">SignalR Task Manager</h1>
</div>
<div class="task-form-container">
<h2>Add a New Task</h2>
<form method="post" class="task-form">
<input type="text" id="taskName"
placeholder="Enter task name"
class="task-input"/>
<input type="button" value="Add Task"
id="addTaskButton" class="task-submit"/>
</form>
</div>
<div class="tasks-container">
<h2>Uncompleted Tasks</h2>
<div class="tasks-list" id="uncompletedTaskList">
</div>
<h2>Completed Tasks</h2>
<div class="tasks-list" id="completedTaskList">
</div>
</div>
@section Scripts {
<script src="img/signalr.js"
asp-append-version="true"></script>
<script src="img/index-page.js"
asp-append-version="true"></script>
}
可用的 HTML 代码相当简单,只需添加一个包含一个用于命名任务的字段和一个负责将新任务发送到服务器的按钮的表单。
此外,还有两个列表(已完成任务和未完成任务)将根据用户交互显示。我们没有在 HTML 元素中使用任何 Razor Pages 指令,因此不需要对 Index.cshtml.cs 文件进行任何更改。
重要的是要注意,为了便于理解客户端、服务器以及 SignalR 的概念,我们正在使用一个 Razor Pages 应用程序。然而,可以采用另一种称为 SPA 的方法,这使得可以使用 Angular、React 或 VueJS 等框架在 JavaScript 和 HTML 中创建应用程序,这些应用程序在客户端运行并与服务器交互。
SPA
TaskManager 应用程序使用 Razor Pages 来简化 SignalR 概念的解释,将客户端和服务器集中在同一个项目中。然而,SignalR 可以安装在一个单独的应用程序中,该应用程序使用纯 JavaScript、TypeScript 或任何框架,例如 Angular、Vue.js 等。此外,SPA 概念是开发实时功能的一个很好的实践。否则,如果浏览器正在加载多个页面,每次新的请求都会与服务器建立一个新的连接。SPA 使得在使用 SignalR 时,可以在保持相同连接的同时动态渲染应用程序页面。
注意,HTML 代码使用了一个 @section Scripts {} 指令,其中添加了之前安装的 SignalR 库和我们将创建的 JavaScript 文件。这个部分是在 Pages/Shared/_Layout.cshtml 文件中定义的,正如我们在 第二章 中所学的。
在 wwwroot/js/index 目录中创建一个 index-page.js 文件。该文件的全部内容可在本书的代码库中的应用程序源代码中找到。
让我们关注建立与 Hub 连接的最重要几点。为此,我们基本上需要三个主要步骤:
-
创建一个连接对象。
-
映射事件。
-
启动连接。
这三个步骤在以下代码中定义:
var connection = new signalR.HubConnectionBuilder()
.withUrl(HUB_URL).build();
connection.on(NOTIFY_TASK_MANAGER_EVENT, updateTaskList);
connection.start().then(function () {
addTaskButton.disabled = false;
}).catch(function (err) {
return console.error(err.toString());
});
在第一行,我们使用 SignalR 对象创建了一个连接对象。请注意,withUrl(HUB_URL) 方法使用一个必须包含 Hub URL 值的常量。由于我们使用 Razor Pages,客户端和服务器将通过相同的地址可用,在这种情况下,我们可以输入一个相对 URL,例如 /taskmanagerhub。这个 URL 正是之前在服务器上映射的端点。
接下来,我们有事件实现,它将处理来自服务器的返回。在这种情况下,我们使用连接对象的on方法。此方法接收两个参数,第一个是一个表示事件的字符串。在这种情况下,NotifyTaskManager值被设置为常量。updateTaskList方法将处理返回。我们可以使用内联函数。然而,为了便于维护,我们创建了一个具有以下签名的单独函数:
function updateTaskList(taskModel) {
//Code
}
这个函数可以具有任何名称和不同类型的参数。然而,Hub 中可用的方法会将一个TaskModel对象作为参数发送给客户端。此对象将被序列化为 JSON 或二进制格式,SignalR 会将它添加到对应于处理此返回的事件中。
updateTaskList函数只是获取返回的对象,并使用 JavaScript 动态地将完成的或未完成的任务列表馈入 HTML。
最佳实践
在客户端和服务器上使用对象作为方法参数是一种良好的实践。这防止了在输出和输入参数修改时需要更改应用程序中的方法签名。使用对象简化了客户端和服务器之间的信息流量。
重要的一点是,事件名称在客户端和服务器上必须相同,因此使用常量来便于维护和编写。
第三步是通过连接对象的Start方法启动连接。Start方法传递一个在连接建立后触发的承诺。此外,还可以实现catch方法,用于映射以及在尝试与 Hub 连接时可能出现的任何错误。
现在是时候给添加任务按钮添加点击事件了,该事件将负责根据以下代码请求添加新任务:
var addTaskButton = document
.getElementById("addTaskButton");
addTaskButton.addEventListener("click", function (event) {
let taskName = document.getElementById(TASK_NAME_ID);
connection.invoke(HUB_ADD_TASK_METHOD,
{ name: taskName.value }).catch(function (err) {
return console.error(err.toString());
});
taskName.value = "";
taskName.focus();
event.preventDefault();
});
上一段代码通过connection.Invoke()函数请求服务器,该函数具有在 Hub 中可用的方法名称,以及一个通过用户输入定义名称的TaskModel对象作为参数。
我们目前拥有TaskManager应用程序实现实时功能所需的所有必要要求。尽管这可能看起来很复杂,但方法很简单,需要客户端了解服务器上的方法,以及服务器了解客户端上可以执行的事件。
让我们更详细地分析应用程序执行流程。
理解客户端和服务器通信流程
在正确配置了 Hub 和客户端之后,现在是时候了解通信流程将如何工作了。创建任务的流程根据图 4 .3 表示:

图 4.3 – 使用 SignalR 在客户端和服务器之间进行通信
图 4 .3 中所示的步骤简单地说明了整个应用程序的通信流程。让我们了解每个步骤:
-
在输入任务名称后,用户点击添加任务按钮。按钮的点击事件通过 Hub 请求服务器上的CreateTask方法,通过连接传递一个TaskModel对象,其中Name属性被定义为参数。对服务器端方法的调用是通过之前与 Hub 建立的连接完成的。
-
在收到对CreateTask方法的请求后,Hub 然后处理任务,通过Save方法将其添加到内存中的列表中,该方法向Id和IsCompleted属性添加值。
-
然后,Hub 在客户端上调用NotifyTaskManager方法,并将创建的TaskModel对象作为参数传递。
-
客户端执行处理服务器通知的方法。此方法在 SignalR 连接对象中实现,并更新应用程序屏幕,显示已创建的任务列表和已完成任务的列表。
在本节中,我们学习了 SignalR 的主要概念,用于在客户端和服务器之间实现实时通信。这些概念可用于不同类型的应用程序,例如聊天应用、在线商店中的订单状态更新等。然而,在某些情况下,我们必须在客户端和服务器之间使用恒定的数据发送模型,这取决于另一个可以通过使用流式传输实现的同步模型。这个概念在仪表板和新闻源等应用程序中广泛使用。在下一节中,我们将了解流式传输是如何工作的。
与流式传输一起工作
在 SignalR 的上下文中,流式传输是一种强大的方式,可以从服务器向客户端发送数据,反之亦然,以连续流的形式。与传统请求/响应模型不同,其中数据以单个批次发送,流式传输允许数据持续流动,这对于涉及实时更新的场景特别有用,例如实时流、仪表板或甚至聊天应用。
SignalR 的流式传输具有几个重要特性,使其成为实时应用的卓越选择。通过持续流动,数据一旦可用就立即发送,这对于创建实时用户体验至关重要。这意味着用户会立即收到更新,保持他们持续了解情况。
接下来,SignalR 中的流式传输操作本质上是异步的,确保即使在处理多个流式传输操作或大量数据时,应用程序也能保持响应。最后,SignalR 支持双向流式传输,不仅允许服务器到客户端的数据流,还允许客户端到服务器的数据流。
这种灵活性为交互式应用程序开辟了广泛的可能性,其中服务器和客户端都可以发起并参与数据交换,进一步增强了使用 SignalR 构建的应用程序的动态、实时能力。
流式策略是一个强大的解决方案。然而,重要的是要记住一些限制和挑战:
-
网络依赖性和稳定性:SignalR 上流式处理的主要限制之一是其对网络质量的依赖。由于流式处理涉及数据的持续流动,一个稳定且可靠的网络连接至关重要。不稳定性可能导致连接丢失,损害用户体验。
-
资源密集度:流式处理可能比传统的请求/响应交互更资源密集。由于服务器必须保持一个开放的连接并持续处理和发送数据,这可能会增加 CPU 和内存的使用。在高流量场景或连接大量客户端的情况下,这可能会成为资源管理和扩展策略的一个重大挑战。
-
实现和维护的复杂性:由于需要管理持续连接和处理异步数据流,实现流式逻辑通常比处理标准请求/响应模型更复杂。此外,调试流式应用程序尤其具有挑战性,尤其是在确保数据完整性和处理网络问题时。
-
可扩展性挑战:随着并发用户数量的增加和服务器负载的快速增加,实时流式应用程序的可扩展性可能是一个挑战。
-
有限的浏览器支持和兼容性问题:尽管现代浏览器通常支持 SignalR 背后的技术,但仍可能存在兼容性问题,尤其是在较旧的浏览器中。
-
安全考虑:与开放的持续连接相比,流式应用程序可能需要考虑不同的安全问题,与传统的 Web 应用程序相比。
理解这些限制有助于更好地定义和应用设计策略,以充分利用 SignalR 中可用的最佳功能。
实现基本流式处理
我们已经理解了 SignalR 最重要的概念,例如 Hub 以及客户端和服务器之间通信的工作方式。然而,实现一个简单的应用程序示例来理解流式方法是很重要的。
没有什么比创建一个应用程序来捕捉我们正在学习的概念更好的了。因此,按照以下步骤实现一个使用流式处理的应用程序:
-
访问您的操作系统终端,导航到您选择的目录,并按照以下说明创建一个文件夹:
mkdir SignalRStream cd SignalRStream -
现在,按照以下步骤创建应用程序:
-
运行以下命令以创建项目:
dotnet new webapp -o SignalRStreamingApp -
然后,访问创建的应用程序目录并打开 Visual Studio 代码:
cd SignalRStreamingApp code . -
与我们在 TaskManager 项目中做的一样,第一个任务将是创建一个 Hub。创建一个名为 Hubs 的新文件夹,然后创建一个名为 StreamHub.cs 的类:
using Microsoft.AspNetCore.SignalR; using System.Threading.Channels; namespace SignalRStream.Hubs; public class StreamHub : Hub { public ChannelReader<int> Countdown(int count) { var channel = Channel .CreateUnbounded<int>(); _ = WriteItemsAsync(channel.Writer, count); return channel.Reader; } private async Task WriteItemsAsync(ChannelWriter<int> writer, int count) { for (int i = count; i >= 0; i--) { await writer.WriteAsync(i); await Task.Delay(1000); // Simulates some delay } writer.TryComplete(); } } -
此代码有一个返回从指定数字开始倒计时整数的 Countdown 方法。
-
现在,让我们通过在 Program.cs 文件中添加与 TaskManager 项目中相同的方式的 SignalR 功能来更改该文件。类应如下所示:
using TaskManager.Hubs; using TaskManager.Service; using TaskManager.Service.Contract; var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorPages(); //Add SignalR builder.Services.AddSignalR(); var app = builder.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapRazorPages(); // Add Hub Endpoint app.MapHub<StreamHub>("/streamHub"); app.Run(); -
使用 libman 应用程序添加 signalR 客户端库:
libman install @microsoft/signalr@latest -p unpkg -d wwwroot/js/signalr --files dist/browser/signalr.js -
现在,我们需要创建一个脚本来建立与 Hub 的连接。为此,在 wwwroo/js 目录下创建一个名为 index-stream.js 的文件。此文件必须包含以下代码:
const connection = new signalR.HubConnectionBuilder() .withUrl("/streamHub") .build(); connection.start().then(function () { connection.stream("Countdown", 10).subscribe({ next: (count) => { logStream(count); }, complete: () => { logStream("Stream completed"); }, error: (err) => { logStream(err); } }); }).catch(err => logStream(err.toString())); function logStream(status) { let li = document.createElement("li"); let ul = document.getElementById("ulLog"); li.textContent = status; ul.appendChild(li); }
上述代码旨在使用在上一节课中学到的相同方法将客户端应用程序连接到服务器。在这个例子中,首先,有到 Hub 的连接。这是通过使用新的 SignalR.HubConnectionBuilder() 代码行创建连接来实现的。
然后,在启动连接时,使用 connection.stream(..) 方法启动流式传输方法。该 stream 方法依赖于两个参数,第一个是将在服务器上请求的方法的名称,称为 Countdown,之前在 StreamHub.cs 文件中创建。第二个参数是一个整数值,其中服务器上实现的 Countdown 方法将从该值开始倒计时。需要注意的是,流方法所需的参数数量,除了作为字符串定义的函数名称外,还将根据服务器上实现的参数数量而变化。
流方法有一个嵌套方法称为 subscribe,它有一个从服务器获取响应的实现。该 subscribe 方法有一个包含三个主要回调方法的对象:next、complete 和 error。在流式传输的某个点上执行每个这些事件。next 方法用于从服务器发送的响应。complete 方法用于流式传输流程完成时。如果发生错误,则使用 error 方法。所有三个 subscribe 方法都使用在 index-stream.js 文件中实现的 logStream JavaScript 函数,该函数将响应事件添加到包含 HTML 中的列表元素。
-
接下来,我们需要将 Pages/Index.cshtml 文件更改为以下代码:
@page @model IndexModel @{ ViewData["Title"] = "Home page"; } <div class="text-center"> <h1 class="display-4">Stream</h1> <ul id="ulLog"></ul> </div> @section Scripts { <script src= "~/js/signalr/dist/browser/signalr.js" asp-append-version="true"> </script> <script src="img/index-stream.js" asp-append-version="true"></script> } -
现在,只需使用以下命令运行应用程序:
dotnet run
-

图 4.4 – SignalR 流式应用程序
如图 4 .4 所示,我们从数字10开始倒计时,最后有一个流完成的消息,这决定了流传输的结束。在这个例子中,使用了订阅方法的next和complete事件。这样,我们可以通过流式传输在客户端和服务器之间进行持续交互,从而为我们的应用程序带来更大的力量。
该应用程序非常简单,基本上生成一个通过连接到 Hub 获得的数字列表,并带有小延迟来模拟延迟。
流式传输方法对于传输小块信息并允许部分主动处理,从而确保更好的用户体验,是非常有趣的。
我们已经了解了 SignalR 中可用的主要功能,但现在让我们学习在服务器上托管应用程序所需的内容。
托管 ASP.NET Core SignalR 应用程序
与任何基于网络的程序一样,在开发阶段之后,我们必须通过服务器使它们可用。在 ASP.NET Core SignalR 中开发的应用程序具有相同的特性和 .NET 平台的所有功能,因此可以在内部服务器和不同的云提供商上进行托管。
我们将在第十章中讨论更多关于托管应用程序的内容。现在,我们将仅学习生成 SignalR 应用程序可托管包所需的内容。
主持 SignalR 应用程序的基本知识
托管 ASP.NET Core SignalR 应用程序与托管常规 ASP.NET Core 网络应用程序并没有太大不同。然而,由于 SignalR 的实时特性,有一些特定的考虑因素需要记住。
定义托管模型是什么很重要。一般来说,目前选择的是公共云,例如 Azure、AWS 或 GCP(谷歌)。然而,让我们了解为 ASP.NET Core 9 应用程序可用的每种托管类型:
-
传统托管(IIS、Nginx 和 Apache):这些是标准的网络服务器,也可以托管 SignalR 应用程序。它们主要充当反向代理,将客户端请求转发到 SignalR 应用程序。
-
云托管:云平台提供强大且可扩展的托管环境。例如,Azure App Service 为 ASP.NET Core 应用程序(包括使用 SignalR 的应用程序)提供了一个易于使用的托管模型。
-
容器(Docker 和 Kubernetes):对于那些寻求对其托管环境有更多控制权的人来说,容器化提供了一种将 SignalR 应用程序及其所有依赖项打包的方法,确保在不同环境中的一致性。
在定义托管服务器后,托管应用程序的过程大致遵循以下四个步骤:
-
发布应用程序:使用 Visual Studio 或 .NET CLI 发布您的应用程序,生成可部署单元。以下命令是一个示例,它以 发布 模式编译应用程序,并在 发布 文件夹中生成发布文件:
dotnet pubilish -c Release -o ./Published -
配置服务器:无论是 IIS、带有 Nginx/Apache 的 Linux 服务器,还是云服务,您都需要配置服务器或服务以托管您的应用程序。这包括为 IIS 或 Nginx/Apache 服务器安装必要的 .NET 运行时,以及配置 Web 服务器或云服务。
-
配置反向代理(如有必要):对于 IIS、Nginx 和 Apache,确保它们配置正确,以便正确转发请求到您的 ASP.NET Core 应用程序。这对于 SignalR 至关重要,因为它依赖于持久连接。
-
部署应用程序:将发布的应用程序上传或部署到您的托管环境中。如果您使用云服务或容器化,可以通过 FTP、Web Deploy 或 CI/CD 管道完成此操作。
由于 SignalR 应用程序需要维护与客户端的持久连接,因此托管 SignalR 应用程序会带来独特的挑战,并可能显著增加服务器资源负担。
重要的是要了解您服务器或托管计划的连接限制,因为每个计划都支持的最大并发连接数是有限的。在采用负载均衡的环境下,建议使用粘性会话来保持连接的完整性,确保客户端始终与同一服务器实例进行通信。
此外,随着并发连接数的增加,您可能需要扩展应用程序,包括部署多个应用程序实例并在它们之间分配流量。这将使您能够提高有效管理更大并发连接量的能力。这种方法有助于在重负载下保持 SignalR 应用程序的最佳性能和可靠性。
然而,我们理解 SignalR 应用程序提供了使用 ASP.NET Core 9 开发实时应用程序的选项,并且它们的托管与传统基于 Web 的应用程序没有太大差异。
在 第十章 中,我们将更详细地探讨如何使用最佳实践动态托管任何类型的基于 Web 的应用程序。
摘要
在本章中,我们学习了创建动态实时应用程序的重要技能。我们了解了 ASP.NET Core 9 SignalR 的强大功能、其架构模型和基础知识,以及支持技术,以及创建实时任务管理应用程序。此外,我们解释了 SignalR 中的流概念,并涵盖了在服务器上托管 SignalR 应用程序所需的主要活动。
在下一章中,我们将通过学习 ASP.NET Core 9 应用程序中的数据管理和持久性方面,使用诸如 Entity Framework Core 等技术来探索与数据和工作持久性的工作。我们将更深入地了解数据库交互和状态管理。这些是任何基于 Web 的应用程序的基本组件。
第二部分:数据和安全性
在开发现代 Web 解决方案时,我们必须处理数据持久性模型和,当然,安全性。在本部分中,我们将介绍将 ASP.NET Core 9 开发的应用程序连接到 SQL Server 等数据库的原则、模式和最佳实践。我们将了解实体关系和 NoSQL 持久性模型。我们将学习 ASP.NET Core 9 如何提供与数据访问层交互的强大工具,以及我们将了解 EntityFramework Core 和 Dapper 等技术的使用。除了与数据交互外,我们还将了解与应用程序安全相关的方面,理解授权和认证的使用,以及如何使用 ASP.NET Core Identity 实现限制对信息访问的应用程序。
本部分包含以下章节:
-
第五章 ,与数据和持久性工作
-
第六章 ,增强安全和质量
第五章:使用数据和持久性
每个应用程序,在某个时刻,都会消耗数据,无论是通过服务还是甚至在一个数据源中,例如 SQL Server 数据库或 MySQL。与数据库的交互是一个重要的功能,ASP.NET Core 9 提供了诸如 Entity Framework Core 等机制,并且可以轻松地与其他数据库接口提供商集成,例如 Dapper,这是一个用于以简单方式抽象数据访问的库。
在本章中,我们将涵盖以下主要主题:
-
连接到 SQL 数据库
-
理解 SQL、NoSQL、ORM 和 Micro ORM
-
使用 Entity Framework core 和 Dapper 进行操作
我们将探讨使用诸如 Entity Framework 和 Dapper 等技术之间的 Web 应用程序和数据库通信,以及理解诸如 ORM 的使用和不同的数据持久性模型等重要概念。
技术要求
为了充分利用本章内容,有一些先决条件需要满足。因此,您需要安装 Docker 和 Azure Data Studio。
本章的所有源代码和示例都可以在 GitHub 仓库中找到:github.com/PacktPublishing/ASP.NET-Core-9.0-Essentials/tree/main/Chapter05。
Docker 安装
我们将使用 Docker 作为运行 SQL 数据库服务器的基准。使用 Docker 可以避免在不同操作系统上安装数据库时遇到的问题,因为它是一个可移植的选项。
要安装 Docker,请遵循您操作系统的说明。
Windows
在终端上以管理员身份运行以下命令:
winget install -e --id Docker.DockerDesktop
Mac
访问以下链接,并根据您的处理器遵循安装教程:docs.docker.com/desktop/install/mac-install/#install-and-run-docker-desktop-on-mac。
Linux
Docker 支持 Ubuntu、Debian 和 Fedora。请根据您的平台使用以下说明:docs.docker.com/desktop/install/linux-install/。
Azure Data Studio
Azure Data Studio 是一个专门的数据库编辑器,将用于执行数据库操作,例如创建表、插入和记录查询。
连接到 SQL 数据库
在每一章中,我们都学习了涉及 ASP.NET Core 9 的不同方面以及该平台如何为开发各种类型的应用程序提供大量资源。每个应用程序都有一个目的,即处理生成用户信息的数据。然而,在某个时候,您的应用程序将最终与数据持久性模型交互。
数据持久性以多种方式发生,但通常是将分配在内存中的信息序列化到磁盘上,这可以是文件的形式,通常使用数据持久化平台,如 数据库管理系统(DBMS)或非关系型数据来实现,我们将在不久的将来更深入地讨论这两种持久化模型。
大多数应用程序使用基于数据库的持久化模型,如 SQL Server、Oracle 和 MySQL。每个数据库管理系统都有管理、类型和资源组织的模型;然而,它们共享将数据以表格格式持久化的相同目的,并使用 结构化查询语言(SQL)来操作和管理所有持久化数据。
ASP.NET Core 9 可以与不同类型的数据库管理系统(DBMS)通信,但我们将重点关注 SQL Server 数据库。
要使应用程序连接到数据库,以下内容是必要的:
-
数据库驱动程序(一个 NuGet 包)
-
连接字符串
-
访问所需资源
使用此模型,我们可以连接到任何已将 NuGet 包移植到 .NET 平台的数据库,例如用于 SQL 数据库的 System.Data.SqlClient 包,这使得应用程序能够轻松实现持久化模型。
既然我们已经了解了与应用程序和数据持久性交互相关的原则,让我们学习 ASP.NET Core 9 平台如何与 SQL Server 数据库进行通信。
准备 SQL Server
在当前版本的 .NET 平台中,它们主要使用 依赖注入(DI)设计模式,该模式允许使用称为 控制反转(IoC)的技术,导致类及其依赖项由 .NET 依赖项容器管理。
.NET 中的 DI
DI 模式设计的主要目标是抽象类实例及其相应依赖项的管理。这是大多数高性能解决方案中的常见做法。如果您想了解更多关于 DI 的信息,请访问 learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-9.0 。
通过 DI,我们可以注册一个类来控制对数据库的连接。我们将在学习使用 Entity Framework 的 与 EF Core 和 Dapper 一起工作 部分时采用这种方法。
目前,了解应用程序和数据库之间通信的基本原理很重要。我们将使用 SQL Server 作为 DBMS,为此,您必须查阅 技术要求 部分,并安装 Docker 引擎。Docker 的工作原理超出了本书的范围。然而,它的使用将允许您继续本章中描述的示例,而不会出现任何兼容性问题。
让我们开始配置数据库:
-
第一步是运行一个 Docker 容器来运行 SQL Server。我们将使用以下命令启动 SQL Server 的一个实例:
docker run -d -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=Password123" -p 1433:1433 mcr.microsoft.com/mssql/server:2019-latest让我们回顾一下前面的命令:
-
docker run : 这使用 Docker 引擎来运行容器。
-
-d : 此参数用于在后台执行命令。
-
-e : 这些是环境变量的定义,用于启动容器时使用。在这种情况下,ACCEPT_EULA 变量被配置为同意微软的条款,而 MSSQL_SA_PASSWORD 参数用于定义 SA 用户的密码,这是默认的 SQL 用户管理员。
-
-p : 这定义了主机机器的端口,该端口将用于与容器的 1433 端口通信。在这种情况下,我们定义主机和容器端口相同,为 1433。
-
mcr.microsoft.com/mssql/server:2019-latest : 这是将要运行的 Docker 镜像的类型和版本。
Docker 会将 SQL Server 镜像下载到您的机器上,并以虚拟化的方式运行。前面的命令将返回一个哈希码,指示镜像正在运行。
-
-
现在,运行以下命令:
docker ps -
运行的镜像将按 图 5.1 所示列出,如果您已经使用 Docker,则列出的镜像数量可能不同。

图 5.1 – 运行中的 Docker 镜像
了解更多关于 Docker 的信息
要了解更多关于 Docker 的信息,请查看以下官方文档:docs.docker.com/。
- 现在我们已经运行了镜像,打开 Azure Data Studio,如 技术要求 部分所述安装,然后点击 创建连接 按钮,如图 5.2 所示:

图 5.2 – 创建数据库连接
-
按照以下方式填写字段:
-
服务器 : localhost,1433
-
认证类型 : SQL 登录
-
用户名 : sa
-
密码 : Password123
-
-
保持其他参数不变,然后点击 连接 。在某些情况下,将显示一个弹出窗口,告知您有关证书的使用。只需点击 启用信任服务器证书 按钮。此证书由 Azure Data Studio 自动创建,所以请放心。
-
连接后,您将能够访问服务器,该服务器仅包含标准数据库。点击 新查询 选项,您将看到一个新选项卡,如图 图 5 .3 所示,我们将使用它来创建数据库和表。

图 5.3 – 新查询选项卡
-
现在,在本书的 GitHub 仓库中,在 第五章 文件夹中,从 InitialDb.sql 文件中复制代码,并将其粘贴到之前在 Azure Data Studio 中创建的 新查询 选项卡中。
-
然后点击运行按钮。DbStore 数据库和 Product 表将被创建,并将一些产品作为数据示例插入。
现在我们已经准备好了 SQL 数据库,是时候创建一个简单的控制台应用程序,建立连接,并从产品表中列出数据。
使用 SQL 客户端
如前所述,.NET 平台有更多现代的方式来建立数据库连接,我们将在 ORM 和 Micro ORM 部分更多地讨论这个主题。然而,了解应用程序和数据库之间通信的基本原理是非常重要的。
为了做到这一点,我们将创建一个控制台应用程序,并添加必要的 NuGet 包以连接到之前准备好的 SQL Server。
然后,在您选择的文件夹中打开终端,并依次执行以下命令:
dotnet new console -n MyFirstDbConnection
cd MyFirstDbConnection
dotnet add package System.Data.SqlClient
code.
项目准备就绪后,我们需要执行以下步骤:
-
创建到数据库的连接。我们将使用 SqlConnection 类。
-
打开连接。
-
创建一个将要执行的 SQL 命令。我们将使用 SqlCommand 类。
-
根据 SQL 命令读取数据。我们将使用 SQLDataReader 类。
-
在屏幕上显示数据。
-
关闭连接。
只需六个步骤,我们就能与数据源进行交互。Program.cs 文件中的代码必须与以下内容完全一致:
using System.Data.SqlClient;
SqlConnection sql = new SqlConnection("Server=localhost,
1433;Database=DbStore; user id=sa;
password=Password123");
try
{
sql.Open();
Console.WriteLine("Connection Opened");
SqlCommand cmd = new SqlCommand(
"select * from Product", sql);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
Console.WriteLine($"{reader[0]} - {reader[1]}
- {reader[2]:C2}");
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
sql.Close();
Console.WriteLine("Connection Closed");
}
让我们讨论这个实现最重要的细节。
第一步是创建数据库连接类 SqlConnection,它在构造函数中接收一个连接字符串作为参数。连接字符串可以理解为数据库服务器的地址。在这种情况下,这个地址由三个基本属性组成:服务器、用户 ID 和 密码。默认连接端口是 1433,可以省略,但如果连接端口不同,则必须明确定义它。这些参数与之前通过 Azure Data Studio 通过 UI 连接到数据库时使用的参数相同。
连接字符串提供了几种其他类型的参数,用于确定如何建立连接。我们目前使用的是最简单的一种。
连接字符串
如前所述,连接字符串由不同的参数组成,包括用户名和密码等敏感数据。因此,将连接字符串管理从源代码中分离出来是一种良好的做法,以防止漏洞并防止敏感数据在应用程序的版本控制中可用。我们将在第九章中学习安全的凭证管理方法。有关连接字符串的更多详细信息,请参阅这个优秀的参考资料:www.connectionstrings.com/sql-server/。
创建SqlConnection对象后,是时候使用sql.Open()代码打开连接了。接下来,创建SqlCommand类,其构造函数接收一个 SQL 字符串以及数据库连接对象。
使用的 SQL 代码对Product表执行简单查询,获取所有可用行和列。
SqlCommand有不同的方法,例如ExecuteNonQuery,通常用于执行更改数据库的命令,如Insert、Delete和Update,或者ExecuteReader,如本例中所用,用于从 Product 表获取数据。ExecuteReader方法返回一个SqlDataReader类型的对象,它将行和列抽象为一个对象,其中可以进行交互和访问表信息。
所有这些抽象工作都是由System.Data.SqlClient库完成的,它有权访问 SQL Server 数据库连接驱动程序,并提供几个用于操作数据的类。
通过执行ExecuteReader方法获取数据后,我们最终通过显示从数据库获取的数据来迭代返回的对象。
所有代码都通过try..catch..finally块进行保护,以确保错误处理,最重要的是,在结束时关闭连接。
我们可以分析在图 5.4中列出数据库记录的结果:

图 5.4 – 显示 Product 表中的记录
尽管这是一个简单的应用程序,但我们学到了与应用程序和数据库交互相关的重要概念和基础知识。我们以 SQL Server 为基础,但学到的教训也适用于其他数据源,如 MySQL 或 Oracle,当然,连接、命令和读取对象会有所不同。
此外,我们为本章剩余部分的环境准备奠定了基础,我们将探讨其他概念,从关系型数据库和非关系型数据库之间的区别开始,此外还将了解 ORM 和 Micro ORM 是什么。
理解 SQL、NoSQL、ORM 和 Micro ORM
随着不同应用程序结构和需求的出现,也发展出了不同的数据管理方式。选择合适的数据库和交互方法对应用程序的性能、可扩展性和可维护性有重大影响。
同样,随着系统复杂性的增加,通过数据库管理系统(DBMS)获取数据的新技术也出现了,例如对象关系映射(ORM)和微型 ORM。每种技术都在某些情境下有其优势,正确了解它们很重要,因为不存在万能的解决方案。
SQL 与 NoSQL
应用程序的复杂性和不同的数据管理需求给公司带来了几个挑战,例如,为了管理大量数据而拥有合适的性能,这导致了可维护性和可扩展性的挑战。
与这些变量一起,保持服务器完美运行是昂贵的。关系型数据库管理系统(RDBMS)是跟踪组织信息的主要手段,它们在关系模型中优先考虑信息的完整性和关系的方法已经定义了开发团队多年来处理数据操作的方式。
关系型数据库中的表代表了一种信息类型。这种信息分布在列中,代表了一份数据的特征。完整的数据由表中的一行确定。表可以与其他表建立关系,将不同的数据关联起来以组成信息。这就是为什么它们被称为关系模型。图 5.5中的一些示例展示了某些表及其关系,代表了一个来自银行账户应用程序的数据抽象:

图 5.5 – 基本银行账户数据模型
在图中,你可以看到客户有一个有交易的账户。这是这个情境的基本表示。但在大型情境中,这种关系结构模型给公司带来了不同的挑战,并遵循基于数据应如何持久化的应用程序开发模型。
关系型数据库提供了灵活性;然而,基于图 5.5中所示的数据模型构建应用程序会带来一些挑战。
现代系统需要具备灵活性和弹性,在某些情况下还必须是技术中立的。
随着云的出现,弹性资源的可能性也随之产生。然而,即使在云环境中,维护数据集群、同步和管理它也不是一件容易或便宜的任务。如今,通过平台即服务(PaaS),这些活动被云提供商抽象化;然而,这需要付出代价。
随着技术的进步,其他类型的持久化模型和为应用和公司提供的机会也出现了。几年前在技术社区中普遍被误解的一个大词是NoSQL,它意味着非关系数据库或不仅仅是 SQL。
这种持久化模型对传统的关系模型有不同的方法。NoSQL 数据库具有更灵活的数据结构,对数据如何持久化没有太多限制。
多年来,NoSQL 被视为 DBMS 的新持久化模型,导致公司试图迁移到这种模型,而没有完全理解其基础,并在 NoSQL 结构中使用关系数据库方法。
这种与不同数据持久化源交互的方式带来了几个好处,包括使开发人员、工程师和公司改变他们对应用开发的看法,但更关注业务而不是数据应该如何持久化。
在图 5.6中,我们可以看到关系数据库和 NoSQL 数据库之间的大部分差异。

图 5.6 – 关系数据库和 NoSQL 之间最大的差异
NoSQL 方法提供了不同类型的数据持久化,为应用带来了几个好处。最常见的是以下几种:
-
键值存储(Redis, Memcached)
-
文档数据库(MongoDB, Couchbase)
-
列族数据库(Cassandra, HBase)
-
图谱数据库(Neo4j, OrientDB)
同样,NoSQL 数据库的数据操作模型与关系数据库不同,查询方法根据 NoSQL 数据库类型而异,可能不如 SQL 标准化。
此外,重要的是要理解 NoSQL 数据库通常优先考虑可扩展性、针对特定查询模式的高性能以及处理不断变化的数据结构的灵活性。
但我们何时应该使用一种方法而不是另一种方法?
让我们分析以下表格,以了解持久化方法之间的差异:
| 特性 | RDBMS | NoSQL |
|---|---|---|
| 结构 | 严格、预定义的架构 | 灵活、可适应,架构可以是无架构的或动态定义的 |
| 可扩展性 | 通常垂直扩展(增加硬件功率) | 通常设计为水平扩展(添加更多服务器) |
| 一致性 | 强大的 ACID 保证 | 最终一致性对于更快的写入是常见的 |
| 查询 | 强大的、表达式的 SQL 查询 | 根据数据库类型而异,对于复杂关系可能不如 SQL 强大 |
| 用例 | 具有严格架构、复杂关系、强一致性需求的数据 | 高量数据、快速变化的数据模型、高性能、特定查询模式、分布式系统 |
表 5.1 – 数据持久化模型比较
如 表 5.1 所示,RDBMS 在可预测和结构化的数据模型中表现出色,其中数据准确性和关系至关重要。NoSQL 在需要灵活性、大规模可扩展性和针对特定需求的高性能的场景中脱颖而出。两者都是针对不同类型应用程序的优秀方案,具有不同的适用性,例如一个将数据持久化在 SQL Server 中的应用程序,同时使用 Redis 来管理某些信息的缓存,避免频繁访问数据库。这两种方法都用于同一应用程序。
幸运的是,ASP.NET Core 9 允许我们与不同类型的数据模型一起工作,因为它具有可扩展性和动态性。让我们更深入地探讨两个在关系型数据库中操作数据的重要概念,即 ORM 和 Micro ORM。
ORM 和 Micro ORM
ORM 是一种作为面向对象编程(OOP)世界和数据库关系世界之间桥梁的技术。OOP 将数据建模为具有属性和行为的对象,而数据库则使用表、行和列进行操作。
正如我们在本章开头所学的,我们使用诸如 SqlConnection、SqlCommand 和 SqlDatReader 这样的对象来读取 SQL Server 中的数据。这是一个简单的方法,但随着商业的复杂性增加,获取和映射数据以在应用程序中应用所需业务规则可能成为一个大问题。
在 使用 SQL 客户端 部分实现的 Products 表示例中,我们使用 SQL 查询来获取所有现有记录。在实际的大型应用程序中,用户与任何交互都必需获取表中的所有记录是不切实际的,这可能会在应用程序中引起严重的性能问题。
必须插入、删除和更新数据,甚至可以通过过滤器进行自定义搜索,这意味着为每种情况编写一个 SQL 命令。此外,为了有效地处理数据,有必要抽象化持久性和业务领域。在 C# 中,我们可以在高层次上与面向对象的概念一起工作,在这种情况下,ORM 作为一种强大的技术出现,使我们能够专注于业务,同时提供其他灵活性。
在 .NET 平台上实现 ORM 被称为 Entity Framework(EF)。EF 提供了所有高级机制,用于从对象到数据库或从数据库到 C# 对象的操作和转换数据。
使用 EF,我们不需要担心为数据库中的各种操作编写 SQL 查询。EF 还具有其他功能,例如 迁移,允许您根据开发的代码模型更新数据库,为数据库版本控制提供了一个很好的解决方案。
为了更好地理解 ORM 的工作原理,请参阅 图 5.7:

图 5.7 – 银行系统的简单数据模型
我们知道在 C# 应用程序中可以获取数据,因为我们之前学过。为了翻译 图 5 .7 中显示的数据对象,需要创建三个 C# 类:Customer.cs、Account.cs 和 Movement.cs。然而,对于每个类,都需要编写不同的 SQL 查询来执行对数据库的任何操作。此外,对于每个业务需求,都需要将数据映射到 C# 类,反之亦然以持久化数据。
这意味着要获取客户数据,例如他们的账户和交易,至少需要进行三次数据库查询,与 SqlDataReader 对象交互,并创建相应的 C# 对象。尽管这不是一项非常复杂的工作,但随着软件变得更加复杂,各种变化使得这种模型变得有问题。
假设将 Movement 表中的 Description 列的名称更改为 Event。甚至还需要更改在 C# 中创建的所有 SQL 查询,以及映射。当涉及到更复杂的数据模型时,维护困难和可能出现的问题会呈指数级增长。
当使用 ORM 时,整个任务被抽象化并简化。幸运的是,EF 为此场景提供了一个很好的解决方案,需要以下步骤:
-
连接字符串:数据库地址和访问凭证
-
DbContext 对象:数据库连接和对象映射的协调者
-
DbSet:将被映射到数据库对象的域对象
EF Core 管理与数据库的所有通信、映射和迁移,使开发者能够专注于业务。
图 5 .7 中的示例的 DbContext 类看起来是这样的:
public class BankingDbContext : DbContext
{
public BankingDbContext (DbContextOptions
< BankingDbContext > options)
: base(options)
{
}
public DbSet<Customer> Customers { get; set; }
public DbSet<Account> Accounts { get; set; }
public DbSet<Movement> Movements { get; set; }
}
我们将在下一节中更详细地实现这个类。在此阶段,重要的是要理解所有将由 DbContext 管理的表都是 BankingDbContext 类的 DbSet 类型属性。
数据库对象的映射通常是通过约定完成的,其中 EntityFramework Core 将属性名称和类型与数据库中的表列名称和类型进行比较,但可以通过使用专门的属性或类轻松自定义。
EF Core 的约定
要了解更多关于约定的信息,请访问 learn.microsoft.com/en-us/ef/core/modeling/#built-in-conventions 。
通过映射数据库约定,我们可以通过 BankingDbContext 类获取数据库中的所有客户,如下所示:
public async Task<ICollection<Account>>
GetAllAccountsAsync()
{
return await _context.Accounts.ToListAsync();
}
如前代码所示,GetAllAccountsAsync 方法在数据库中搜索所有账户。EF Core 的 ToListAsync 方法将异步查询数据库,返回一个 Account 对象的列表。
以下代码展示了使用 ORM 的简单而强大的方法,无需管理连接或编写 SQL 命令,因为它们由 EF 生成,此外,无需将数据库对象映射到类中。所有这些功能都已抽象化。
以这种方式,ORM 提供了以下好处:
-
减少样板代码:ORM 自动生成大量重复的 SQL 代码(SELECT,INSERT,UPDATE),使开发者能够专注于应用程序逻辑,而不是数据访问代码。
-
提高生产力:对于习惯于面向对象原则的开发者来说,使用对象通常更直观,可以加快开发速度。
-
提高可维护性:ORM 在您的应用程序代码和特定数据库之间提供了一定程度的抽象,这使得切换数据库提供商或重构数据模型更容易,对代码库的影响更小。
然而,ORM 技术有其优缺点需要考虑:
-
性能开销:在某些情况下,ORM 生成的 SQL 查询可能不是最有效的。经验丰富的开发者通常可以手动编写更高效的 SQL。
-
潜在的抽象问题:ORM 可以隐藏一些底层数据库概念,这可能是有益的,但可能会使那些不熟悉数据库基础的人优化或故障排除更具挑战性。
EF Core 目前处于第 8 版,多年来它得到了改进,添加了不同的功能。然而,我们仍然建议明智地使用我们所能提供的最佳技术。
尽管 ORM 技术变得越来越现代,但仍然存在对性能的担忧,尤其是在您有一个复杂的数据模型,对象之间存在多个级别的关系时。ORM 通常无法生成非常高效的查询,在某些情况下,有必要使用其他资源,例如本章开头学到的微 ORM 方法。
在促进应用程序和数据库对象之间工作的情况下,微 ORM 的概念应运而生。
微 ORM在概念上与 ORM 模型非常相似。然而,微 ORM 将数据库映射对象抽象为 C#类,但更注重性能。在某些情况下,它们甚至提供一些查询的自动生成。
微 ORM 与 ORM 之间的差异如下:
-
占用空间:微 ORM 具有更小的代码库和更少的依赖项,导致显著减少开销。
-
复杂性:微 ORM 提供了一组基本的映射和执行查询的功能,而忽略了传统 ORM 中发现的许多复杂性。
-
控制:由于抽象程度较低,开发者对正在执行的 SQL 查询有更多的直接控制。
-
功能:微 ORM 通常缺乏在大型 ORM 中常见的以下功能:
-
广泛的对象关系管理
-
变更跟踪
-
自动模式迁移
-
标识映射(实体跟踪以防止重复负载)
-
.NET 社区中常用的 Micro ORM 技术有一些,其中最著名的是 Dapper。
Dapper 是开源的
Dapper 库是开源的,并且正在不断更新。有关 EF Core 等不同 ORM 引擎的比较,可以在 GitHub 上找到:github.com/DapperLib/Dapper。
使用 Micro ORM 并不妨碍使用 ORM。根据需要和上下文,它们可以在应用程序中共存。重要的是要记住,这种方法可以使我们的应用程序质量更高。
在下一节中,我们将以实用的方式介绍 EF Core 和 Dapper 的使用,基于之前提到的银行 账户概念。
现在我们已经了解了 ORM 和 Micro ORM 是什么,是时候实现使用这些方法解决方案了。
使用 EF Core 和 Dapper
ORM 和 Micro ORM 是现代应用程序中广泛使用的技术,因为它们具有各种优点。正如我们所学的,ASP.NET Core 9 有几种处理来自不同技术数据模型的方法。我们将学习如何使用 EF Core 作为 ORM,同时也会使用 Dapper 作为 Micro ORM。
EF Core
基于银行账户数据模型的示例,我们有客户、账户和交易表,我们将创建一个项目以连接到本章开头配置的 SQL 数据库,该数据库运行在 Docker 容器中。
因此,完整的解决方案代码将在技术要求部分提到的 GitHub 仓库中可用。
对于这个项目,我们将使用 Minimal API 项目,为此,以管理员模式打开终端并执行以下命令:
dotnet new webapi -n WorkingWithOrm
cd WorkingWithOrm
现在,我们需要添加 EF Core 库,这些库对于应用程序连接到 SQL 服务器数据库是必需的。此外,我们还需要安装一个 EF CLI 工具。这个工具将用于对数据库应用一些更新。
在终端中运行以下命令:
dotnet tool install –global dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
第一个命令安装 EF CLI 工具,接下来的命令是用于将应用程序连接到数据库的 EF 库。
EF Core 数据库提供者
EF Core 能够与不同的数据库一起工作;它不仅限于 SQL Server。有关可用提供者的更多详细信息,可以在learn.microsoft.com/en-us/ef/core/providers/?tabs=dotnet-core-cli找到。
项目现在已准备好配置,我们将执行以下步骤:
-
配置连接字符串。
-
创建模型类。
-
创建一个继承自DbContext的类。
-
配置在 ASP.NET Core 9 DI 容器中创建的DbContext。
-
添加迁移。
-
更新数据库。
在执行这些步骤的过程中,您将注意到与传统方法使用SqlConnection、SqlCommand和SqlDataReader进行数据库通信的一些差异。
使用以下命令在终端中打开 Visual Studio Code 中的项目:
code .
完整的项目结构如图 5.8所示:

图 5.8 – 银行项目项目结构
为了配置连接字符串,我们将使用appsettings.json。重要的是要提到,包含用户凭据的信息不应直接在代码仓库中可用。最佳实践是使用密钥或甚至使用配置服务器,如Azure App Configurator来管理这些信息。我们将在第六章中更多地讨论良好的安全实践。
为了教学目的,我们将连接字符串添加到appsettings.json文件中:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"BankingDbContext": "Server=localhost;
Database=dbBanking;User Id=sa;
Password=Password123;
TrustServerCertificate=True"
}
}
我们使用与通过 Docker 运行的数据库服务器相同的连接字符串,并添加了一个默认的Database=dbBanking,它将用于此应用程序。JSON 中的ConnectionStrings对象是 ASP.NET Core 9 的约定,该对象的每个属性代表不同的连接字符串。
EF Core 负责管理连接、将数据库实体映射到 C#对象以及生成 SQL 命令的所有工作。为此,我们必须使用一个基对象来映射域类。这个基对象被称为DbContext。
DBContext实现了工作单元设计模式,管理内存中操作的所有对象的状态,并在必要时持久化更改。
工作单元模式
工作单元设计模式在不同的上下文中被使用,它倾向于责任分离,例如将应用程序的所有业务规则与与数据库通信和操作数据的责任分离。
在learn.microsoft.com/en-us/archive/msdn-magazine/2009/june/the-unit-of-work-pattern-and-persistence-ignorance了解更多关于工作单元模式的信息。
根据图 5.8 所示的项目结构,我们将创建一个名为BankingDbContext.cs的类,它将包含以下代码:
namespace WorkingWithOrm.Context;
using Microsoft.EntityFrameworkCore;
using WorkingWithOrm.Model;
public class BankingDbContext : DbContext
{
public BankingDbContext(DbContextOptions
<BankingDbContext> options) : base(options)
{
}
public DbSet<Account> Accounts { get; set; }
public DbSet<Customer> Customers { get; set; }
public DbSet<Movement> Movements { get; set; }
}
对于我们正在创建的应用程序,该类相当简单。让我们探索这段代码最重要的几点:
-
DbContext:
BankingDbContext类继承自DbContext超类,该超类提供了应用程序与数据库之间通信、状态管理、映射以及生成 SQL 命令所需的必要抽象。 -
BankingDbContext 构造函数:类构造函数接收一个参数,即泛型 DbContextOptions
类,这允许我们预定义在依赖注入容器中创建 DbContext 对象时将使用的配置。构造函数还可以接收一个连接字符串;然而,使用 C# 选项模式是一种良好的实践。 -
DbSet:DbSet 类型的每个属性代表数据库中的一个表,这些属性为 EF Core 提供信息,以便将数据从表转换为对象,反之亦然。
C# 选项模式
options pattern 在 .NET 平台上被广泛使用,目的是提供对相关设置组的强类型访问。
在 learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-9.0#the-options-pattern 了解更多关于选项模式的信息。
BankingDbContext 类现在已经完整,提供了与 SQL 数据库交互所需的一切。在这种情况下使用的映射模型基于 EF Core 约定,该约定从类及其属性名称推断表和列的名称。
让我们看看 Account.cs 类:
public class Account
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Balance { get; set; }
public int CustomerId { get; set; }
public virtual Customer? Customer { get; set; }
public virtual ICollection<Movement>? Movements
{ get; set; }
}
根据本课程和 EF Core 的约定,预期数据库中将有一个名为 Account 的表,以及名为 Id、Name、Balance 和 CustomerId 的列。
此外,还有一个名为 Customer 的属性和一个 Movement 对象的集合。由于存在 CustomerId 属性,EF 推断出与 Customer 表存在关系,该表在 Account 表中有一个外键(
但是,如果需要遵循不同的命名标准,则可以使用流畅 API 将表、列、主键等的名称直接映射到 DbContext 类中。这可以通过在域类中使用数据注释来完成,甚至可以通过实现每个实体的特定映射类来使用 IEntityTypeConfiguration
以下代码示例表示将 Customer 类手动映射到 tbl_customer 表的自定义映射。为了在数据库实体中自定义类的映射,需要重写从 DbContext 类继承的 OnModelCreating 方法:
override protected void OnModelCreating(
ModelBuilder modelBuilder)
{
modelBuilder.Entity<Customer>(obj => {
obj.ToTable("tbl_customer");
obj.HasKey(c => c.Id).HasName
("pk_customer_id");
obj.Property(c => c.Name).HasColumnName
("customer_name").HasMaxLength(100).IsRequired();
obj.HasMany(c => c.Accounts)
.WithOne(a => a.Customer)
.HasForeignKey(a => a.CustomerId);
});
}
如代码所示,可以定义所有必要的属性以正确映射实体。
随着 BankingDbContext 类的完成,我们必须在依赖注入容器中配置它并配置连接字符串。
我们将在 Program.cs 文件中添加以下行:
// Code omitted for readability
builder.Services.AddDbContext<BankingDbContext>(options =>
options.UseSqlServer(builder.Configuration
.GetConnectionString("BankingDbContext")));
var app = builder.Build();
// Code omitted for readability
我们使用AddDbContext
应用程序实际上已经准备好与数据库通信;然而,仍然需要添加迁移并更新数据库。
在项目目录中打开终端并运行以下命令:
dotnet ef migrations add InitialDatabase
此命令使用我们之前安装的 EF CLI 工具,并添加名为InitialDatabase的迁移。
迁移的目的是使应用程序和数据库在使用的对象上保持同步。在实际应用程序中,数据库的更改,如创建新表或添加或删除列,可能会不断发生。这些更改会影响相关的数据库以及消耗该数据库中对象的相应应用程序。当添加迁移,如前面的命令中所示,我们正在对应用程序使用的领域模型进行快照,EF Core 生成将应用于数据库以保持其最新的脚本。
迁移在项目中创建了一组类。这些类不应手动更改。正如我们在图 5.9中看到的那样,三个文件被添加到应用程序的Migrations文件夹中:

图 5.9 – 初始数据库迁移文件
当观察从InitialDatabase.cs后缀文件中提取的代码片段时,我们发现它们是数据库中的资源创建脚本:
…
protected override void Up(MigrationBuilder
migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Customers",
columns: table => new
{
Id = table.Column<int>(type: "int",
nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(
type: "nvarchar(max)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Customers", x => x.Id);
});
…
随着您的应用程序领域模型的每次更改,都必须添加一个新的迁移。这样,您将维护更改的历史记录,这将有助于数据库和应用程序的维护和演进。
现在,您需要更新数据库。到目前为止,我们还没有在 SQL Server 上运行任何 SQL 脚本,更不用说创建数据库了。我们不会打开 Azure Data Studio 来执行此任务,而是将使用 EF Core CLI 工具根据应用程序中映射的版本更新数据库。
要执行此操作,请在项目目录中的终端运行以下命令:
dotnet ef database update
CLI 工具将连接到 SQL Server 并执行脚本以创建应用程序中映射的数据库和表。图 5.10显示了创建的对象:

图 5.10 – 使用 EF CLI 工具在数据库中创建的对象
与数据库的所有通信都已正确配置。现在是时候添加与数据库交互的 API 了。因此,在Program.cs文件中创建以下路由:
app.MapGet("/customers", async (CancellationToken
cancellationToken, BankingDbContext dbContext) =>
{
var customers = await dbContext.Customers
.ToListAsync(cancellationToken);
return Results.Ok(customers);
});
app.MapGet("/customers/{id}", async (int id,
BankingDbContext dbContext,
CancellationToken cancellationToken) =>
{
var customer = await dbContext.Customers
.FindAsync(id, cancellationToken);
return Results.Ok(customer);
});
app.MapPost("/customers", async (
[FromBody]Customer customer,
BankingDbContext dbContext,
CancellationToken cancellationToken) =>
{
await dbContext.Customers.AddAsync(
customer, cancellationToken);
await dbContext
.SaveChangesAsync(cancellationToken);
return Results.Created();
});
上述路由对客户表执行操作。注意第一个 Get 方法。此方法接收一个BankingDbContext对象的实例作为参数,该实例通过.NET Core 依赖注入 DI 上下文自动解析。
然后,使用dbContext.Customers.ToListAsync(cancellationToken)代码,检索数据库中所有现有的客户。我们只使用Customers DbContext和DbSet,EF Core 负责创建 SQL 查询以选择记录。无需打开连接、创建命令,甚至无需手动映射。所有操作都是透明完成的。
Post方法执行以下操作:
-
dbContext.Customers.AddAsync:将Customer对象作为请求体中的参数传递。然后以与我们向列表中添加项相同的方式将其添加到DbSet中。
-
dbContext.SaveChangesAsync:当执行此方法时,dbContext更新数据库。这意味着如果DbSets上有其他操作,例如删除、更新或添加,这些信息只有在执行SaveChanges或SaveChangesAsync方法后才会更新到数据库中。
异步处理和取消令牌
异步处理是现代 Web 应用程序开发的基本方面。在 ASP.NET Core 9 中,异步方法通过在操作期间(如数据库查询、文件访问或消耗 HTTP 资源)不阻塞线程,允许服务器同时处理更多请求。这种方法允许应用程序在负载下扩展并快速响应。async和await关键字使得编写易于维护和阅读的异步代码成为可能,.NET 平台抽象了管理异步机制复杂性。
结合异步方法,使用取消令牌是一种良好的做法,这允许应用程序正确处理请求的取消,使应用程序更具响应性和弹性。与异步方法关联的取消令牌在整个应用程序的异步操作中传播取消信号,允许它们提前终止并释放资源。ASP.NET Core 9 和 C#简化了异步编程和取消令牌的使用,提供了一个健壮的框架,确保应用程序即使在不同的负载下也能保持响应。
如需了解有关异步编程和令牌取消的更多信息,请访问docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/和learn.microsoft.com/en-us/dotnet/standard/threading/cancellation-in-managed-threads。
通过这种方式,我们可以运行应用程序以简单的方式与数据库交互,使得能够使用 语言集成查询(LINQ)执行任何操作,例如高级筛选或按特定列排序的记录。
使用 ORM 提供了高级应用的多项好处,同样,当我们把数据管理复杂性的管理委托给 ORM 时,也会带来一些挑战。这使得它们运行缓慢,尽管有多个创新和改进,但在许多情况下,它们在性能方面并不是最佳选择。
在这种情况下,Micro ORM 是很好的选择;它们具有与传统方法相似的性能和 ORM 的映射能力。因此,让我们探索如何使用 Dapper 来为数据库通信模型添加更多功能。
Dapper
Dapper 是一个 Micro ORM,它使我们能够以高效的方式与数据库交互,同时将数据库实体映射到 C# 对象中。
它是一个简单易用且功能强大的库。其映射模型有趣且灵活,允许您快速有效地创建不同类型的查询结果投影。
Dapper 和 EF Core 不是相互排斥的技术,在一个项目中同时使用这两种方法可以提供很大的好处。
让我们在终端中执行以下命令,将 Dapper 库添加到之前创建的项目中,在应用程序目录下:
dotnet add package Dapper
在项目中,与 Dapper 一起工作的所有先决条件都已实现,其中最重要的是我们在 appsettings.json 文件中配置的连接字符串。
让我们修改 Program.cs 文件,添加两个新的路由,使用 Dapper 执行查询,我们可以从中获取数据库中的所有客户以及通过 Id 查询客户:
builder.Services.AddScoped(_ => new SqlConnection(builder
.Configuration.GetConnectionString("BankingDbContext")));
var app = builder.Build();
// Codes omitted for readability
app.MapGet("GetAllCustomersUsingDapper", async(SqlConnection connection) =>
{
var customers = await connection.QueryAsync<Customer>
("SELECT Id, Name FROM Customers ORDER BY Name");
return Results.Ok(customers);
});
app.MapGet("GetCustomerByIdUsingDapper",
async(int id, SqlConnection connection) =>
{
var customer = await connection
.QueryFirstOrDefaultAsync<Customer>("SELECT Id,
Name FROM Customers WHERE Id = @Id", new {
Id = id });
if (customer is null) return Results.NotFound();
return Results.Ok(customer);
});
让我们探索前面的代码:
-
在 C# 和 ASP.NET Core 9 中,_ => new 语法被称为丢弃 lambda,当不需要使用 lambda 表达式的输入参数时使用。
-
SqlConnection:我们正在使用与 DbContext 相同的连接字符串将 SqlConnection 对象添加到 DI 容器中。我们使用的是 AddScoped 方法,这意味着每次在请求期间使用 SqlConnection 对象时,它将被重用。
-
QueryAsync:我们使用简单的 SQL 查询从数据库中获取所有客户。所需的列已添加到 SQL 命令中,以及一个 ORDER BY NAME 语句。QueryAsync 是 SqlConnection 扩展方法,当它获取结果时,它将自动根据属性和列的名称将数据映射到 C# 对象。
从数据库中获取所有数据
通常,不建议在单个查询中从数据库中检索所有记录,因为数据库表可能有成千上万或数百万条记录,这可能导致性能问题。请记住,这里提出的示例旨在帮助理解概念,不应在生产应用程序中使用。建议的解决方案是使用分页。分页涉及将数据分成小块,便于管理。要了解更多信息,以下页面包含实现示例:learn.microsoft.com/en-us/ef/core/querying/pagination。
- QueryFirstOrDefaultAsync:与之前的方法相同,在数据库中执行查询,如果找到记录,则将其返回并映射到 Customer 对象。如果没有找到记录,则返回值Null。在 SQL 查询中的重要点是使用@Id参数。Dapper 方法可以在字符串中替换命名参数。因此,在定义带有参数的 SQL 命令后,我们必须定义一个包含与 SQL 命令中定义的参数相同的命名属性的同一参数的对象。在上面的代码示例中,定义的参数名为@Id,这要求传递给参数的对象必须有一个名为 Id 的属性,就像片段new {Id = id}。使用对象允许我们在必要时定义多个参数。
在这种情况下,使用 SQL 命令允许我们为不同的目的创建更高效的查询。同样,Dapper 也可以用来在数据库中添加、更改和删除记录。
在这种情况下,我们不需要管理由 DI 容器控制的 SQL 连接,并从 ORM 的自动映射方法中受益。
Dapper SqlBuilder
Dapper 还有一个扩展,使得编写 Micro ORM 所需的 SQL 命令格式更加容易,称为 Dapper SQL Builder。这是一个非常有用的扩展,即使在需要根据某些条件操作 SQL 字符串时也是如此。
您可以通过访问github.com/DapperLib/Dapper/tree/main/Dapper.SqlBuilder了解更多关于 Dapper SQL Builder 扩展的信息。
正如我们所见,ORM 和 Micro ORM 都是与数据库通信的强大盟友,可以一起使用,在不同的环境中提供不同的好处。
这种灵活性使得 ASP.NET Core 9 能够让我们创建不同类型的应用程序,从最简单的到最复杂的,并使用最佳实践与数据库交互。
摘要
在本章中,我们学习了 ASP.NET Core 9 中的数据持久性,探讨了应用程序如何与数据库交互以存储和管理关键信息。你比较了关系型(SQL)和非关系型(NoSQL)数据库的优势,以便为你的项目选择合适的匹配。此外,你还看到了 ORM(如 EF Core)如何通过将对象映射到数据库记录来简化开发,以及 Micro ORM(如 Dapper)在精细控制性能关键数据库操作方面的好处。
我们将通过学习第六章中的安全最佳实践,再迈出一步,以开发高质量的应用程序。我们将探讨防御应用程序免受漏洞侵害的基本最佳实践和策略。你将学习如何确保用户数据保护、身份验证安全和整体应用程序完整性——这是构建强大、可靠的 Web 应用程序的重要基础。
第六章:提高安全和质量
在快速发展的数字世界中,新的网络威胁以惊人的频率出现,基于 Web 的应用程序安全不仅仅是特性,而是基本需求。因此,为了使应用程序能够应对各种现有漏洞,软件工程师必须将安全性视为基于 Web 解决方案整个开发流程的一部分,以便他们可以保护数据,保证完整性和可用性,并最小化可能损害组织的威胁。
在本章中,我们将了解每个 Web 开发者都应该掌握的基本安全原则,特别是关于 ASP.NET Core 9 作为一个强大的平台,如何帮助我们创建安全、高级的应用程序。
首先,我们将探讨网络安全的基本原则,理解在开发 Web 解决方案的所有阶段都必须考虑安全性。
接下来,我们将讨论身份验证和授权的概念,这两个概念在用户和应用程序、以及应用程序和外部应用程序相互交互时都常用。一旦我们更好地理解了授权和身份验证流程,我们将使用 ASP.NET Core Identity 框架为 API 项目添加安全性,并了解 ASP.NET Core 9 中一些重要的方法,这些方法允许我们加强应用程序中的安全机制。
在本章中,我们将涵盖以下主题:
-
理解基于 Web 应用程序的安全原则
-
比较授权和身份验证
-
使用 ASP.NET Core Identity 框架
-
加强应用程序安全
为了在本章中获得良好的学习体验,我们必须准备一些工具,这些工具对我们充分利用将要介绍的概念至关重要。
技术要求
为了完成本章,以下工具必须存在于您的开发环境中:
-
Docker:必须在您的操作系统上安装 Docker Engine,并运行一个 SQL Server 容器。您可以在第四章中找到有关 Docker 和 SQL Server 的更多详细信息。
-
Postman:我们将使用此工具执行发送到开发应用程序 API 的请求。
-
Azure Data Studio:我们将使用此工具连接到 SQL Server 数据库,以便我们可以执行 SQL 脚本。
本章的代码示例可以在本书的 GitHub 存储库中找到:github.com/PacktPublishing/ASP.NET-Core-9.0-Essentials/tree/main/Chapter06。
理解基于 Web 应用程序的安全原则
每年,针对最多样化的环境和设备,都会出现新的解决方案开发方法。随之而来的是各种挑战。以前专注于 HTML、CSS、JavaScript 和所选编程语言的 Web 应用开发已不再是现实。
软件工程师开始服务于编程 IDE 环境之外的其它上下文,通常与 iInfrastructure 合作,增加了与 DevOps 文化方法一同出现的众多框架和工具,以及持续的价值交付。
DevOps 文化带来了一种新的工作模式,团队避免了孤岛效应,在交流知识的同时共同工作,因此,一个越来越出现在解决方案开发者生活中的主题是安全。安全这个术语早已不再是一个仅针对网络安全团队的孤立主题。它现在在设计的第一阶段就至关重要,必须考虑在解决方案的所有方面。
关注应用和数据的威胁以及控制和管理工作已成为至关重要的因素,甚至对公司和使用应用的用户来说是一个战略性的因素。从数据处理的角度来看,有许多安全标准和政策,例如欧洲的通用数据保护条例(GDPR)。
安全性非常重要,ASP.NET Core 9 提供了多种机制,我们可以利用这些机制来应对通过避免威胁和保持应用的安全和可靠性所提出的挑战。
然而,我们必须了解安全方面是如何应用于 Web 应用以及常见漏洞的,以及 ASP.NET Core 9 是如何防止应用中出现威胁的。
Web 应用中的安全主题
如我们在前面的章节中已经学到的,一般来说,一个 Web 应用有两个主要组件:前端,负责与用户交互,以及后端,负责处理应用的业务规则,提供控制和与数据层交互。
大多数 Web 应用,无论是客户端/服务器应用还是单页应用(SPAs),都以某种方式使用上述方法。如图图 6.1所示,几个组件是前端和后端交互的一部分,例如通信协议、请求、响应、HTTP 头、浏览器、应用服务器、数据库、TCP 协议、凭证、cookies 和本地存储(浏览器)等:

图 6.1 – 单页应用(SPA)的组件
正如我们所看到的,几个组件相互通信。同样,几个漏洞可能会损害你应用的安全性。在某些情况下,信息泄露可能对组织产生严重后果。
作为前提,软件工程师必须从最初的设计阶段就设定一个安全方面,这通常不仅与通信协议和系统之间的交互有关,还与代码开发有关。
假设,在开发过程中,一位软件工程师做出了一项非常重要的更改,旨在修复应用程序中的一个关键问题。为了快速执行更正,软件工程师使用 SQL 命令和字符串连接创建了与数据库的通信。在完成测试后,工程师将代码提交到 Git 仓库,以便更新系统。没有进行代码审查,几分钟内,修复就进入了生产环境。
那么,这个场景中有什么问题呢?最初,软件工程师在快速响应应用程序中发现的问题并进行了更正时表现正确,一切恢复正常。然而,他们与数据库通信的方法中存在一个漏洞,恶意用户可以通过所谓的SQL 注入攻击来利用这个漏洞。
让我们看看一些容易受到 SQL 注入攻击的代码示例:
using System;
using System.Data.SqlClient;
public class VulnerableDataAccess
{
private string connectionString = "TheConnectionString";
public void GetUserData(string username)
{
string query = "SELECT * FROM Users WHERE
Username = '" + username + "'";
using (SqlConnection connection = new
SqlConnection(connectionString))
{
SqlCommand command = new SqlCommand(query,
connection);
try
{
connection.Open();
SqlDataReader reader = command.ExecuteReader();
while (reader.Read())
{
Console.WriteLine(String.Format("{0}, {1}",
reader["Username"], reader["Email"]));
}
reader.Close();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
在前面的代码中,SQL 查询是通过直接将用户的输入(用户名)连接到 SQL 字符串中构建的。这是一种危险的做法,因为它允许攻击者通过向username变量注入 SQL 代码来更改预期的 SQL 查询。例如,如果用户输入类似'; DROP TABLE users; --的内容,生成的 SQL 查询将如下所示:
SELECT * FROM Users WHERE Username = ''; DROP TABLE Users; --'
这将执行DROP TABLE语句,可能会破坏数据。
因此,即使有很好的意图,这样的简单更改也可能产生重大影响。同样,通过实施代码审查,简单的流程可以避免这种情况。代码审查是一种实践,其中工程团队成员分析应纳入主代码的代码,以寻找任何缺陷,评估代码模式和复杂性等。只有当代码满足质量和安全标准时,才能将其纳入主代码。
此外,在将新代码集成到主代码中时,可以执行自动化流程,其中可以添加静态代码分析机制。如果有任何无效的安全和质量标准,应用程序就不能交付到生产环境。我们将在第十章中了解更多关于自动化流程的内容。
静态代码分析
静态代码分析在安全检查、编码标准和循环复杂度分析等方面发挥作用,为与自动化技术相关的应用程序开发流程增加价值,这些自动化技术包括持续集成(CI)和持续交付(CD)。市场上有多款静态代码分析工具,其中最著名的是SonarQube(hub.docker.com/_/sonarqube)。它有一个社区版本,可以托管在任何环境中。然而,它在分析代码行数方面确实有一些限制。作为替代方案,还有一个作为软件即服务(SaaS)提供的版本,称为Sonar Cloud(www.sonarsource.com/products/sonarcloud/)。
将静态分析添加到开发流程中是一种非常好的做法。
在本章中,我们将探讨其他方法来使我们的应用程序更加安全,并讨论各种漏洞。但首先,让我们了解大多数应用程序都使用的一个常见安全模型,该模型基于两个基本过程:认证和授权。
授权与认证的比较
正如我们所学的,安全方面在应用程序开发流程中非常重要。尽管我们有着良好的意图,但我们可能会在我们的代码中包含漏洞,这些漏洞会直接影响我们的用户、应用程序和公司。
然而,除了代码之外,一些功能还需要安全流程。例如,某些服务平台(如电子邮件管理器)的情况就是这样,用户一旦通过登录获得访问权限,就可以私密地访问他们的消息。
登录功能非常重要,尽管它看起来是一个简单的流程,但它需要大量的关注。否则,根据应用程序的不同,可能会有后果。
如果在线银行平台登录时出现漏洞,会发生什么?这可能会给该银行的用户(以及银行本身)带来大问题。
现代系统在处理不同方面时与身份管理协同工作。正如前几章所讨论的,Web 应用程序可以向不同的 API 发起请求。API 允许公司以服务的形式提供业务,这促进了不同应用程序之间的多样化集成。因此,可以拥有具有不同类型功能的应用程序,这些功能可以为用户提供价值,例如地图 API、支付网关,甚至提供 AI 功能的服务 API。
为了使应用程序和 API 能够安全通信,需要一个基于身份的安全机制。有了这个机制,我们可以找出谁在请求某种类型的信息以及为什么。
这种安全机制分为两个概念:认证和授权。
通常,我们知道这种方法涉及登录流程。然而,理解认证和授权之间的区别是至关重要的。
认证
认证旨在回答问题,“你是谁?”:

图 6.2 – 认证流程
图 6 .2 展示了一个认证流程,其中通过登录表单提供了用户的凭证,例如他们的电子邮件和密码。
通过点击登录按钮将此信息发布到服务器,应用程序开始使用提供的凭证识别此用户。
如果根据这些凭证找到用户,那么应用程序就知道了谁想要访问系统。
然而,这只是过程的一部分。现在应用程序已经识别了用户,了解这个用户能做什么就很重要了。这是在授权过程中完成的。
授权
授权旨在回答问题,“这个用户能做什么?”:

图 6.3 – 使用授权流程检查权限
在识别用户后,应用程序开始识别用户的权限,如图 图 6 .3 所示。
授权定义了用户可以行动的范围,无论是管理某些信息还是访问某种类型的数据,以及其他方面。授权流程通常通过角色识别用户,在应用程序的作用域内可以分组访问级别。
使用角色非常常见,因为可以将用户在应用程序中可能拥有的不同权限分组。
通常,认证和授权过程很简单。然而,为了能够安全地实现它们,我们必须了解一些标准:OAuth 2.0 和 Open ID Connect(OIDC)协议。
理解 OAuth 2.0 和 OIDC
想象一个高度安全的建筑。在这里,授权可以被认为是获得进入的许可,而认证则是验证你的身份。
OAuth 2.0 专注于授权,允许用户在平台之间(如社交媒体账户)授予对他们的数据的访问权限,而无需他们共享实际的密码。换句话说,我们允许其他应用程序访问我们信息的一定范围,而无需我们提供某些凭证,例如当我们使用微软、谷歌、Facebook 等平台的凭证登录某些平台时发生的情况。
基本的 OAuth 2.0 流程可以定义为以下:
-
用户使用他们的社交媒体账户登录到一个新的应用程序。
-
应用程序将用户重定向到社交媒体平台(授权服务器)。
-
在登录社交媒体账户后,用户授权应用程序使用他们的数据(例如他们的姓名、电子邮件、个人资料照片等)。
-
授权服务器为应用程序生成特殊的令牌。令牌用于获取用户数据(访问令牌)。在某些情况下,使用刷新令牌来提供访问新令牌。
-
应用程序使用访问令牌从社交媒体平台安全地检索您的数据。
此过程涉及协商两个共享用户信息的应用程序,无需为每个新应用程序输入凭证,从而提高安全性和便利性。
另一方面,OIDC 建立在 OAuth 2.0 之上,增加了一个认证层。它利用 OAuth 授权框架通过像 Google 或 Facebook 这样的受信任提供者验证用户的身份。
让我们看看 OIDC 如何补充 OAuth 2.0:
-
一些应用程序提供了使用其他社交媒体平台登录的能力。在这种情况下,在 OAuth 2.0 流程中,你不会登录到新的应用程序,而是被重定向到你的社交媒体登录页面(即 OpenID 提供者)。
-
用户使用他们的社交媒体凭证进行身份验证,向 OpenID 提供者证明他们的身份。
-
在用户同意的情况下,OpenID 提供者通过 ID 令牌将用户的基本个人资料信息(如姓名和电子邮件)与新的应用程序共享。
OIDC 启用诸如单点登录(SSO)等功能,允许您使用相同的登录凭证访问多个应用程序(例如,使用您的 Google 账户登录多个网站)。
尽管 OAuth 2.0 和 OIDC 流程相似且相互关联,但它们服务于不同的目的:
-
重点:OAuth 2.0 作用于授权(授予数据访问权限),而 OIDC 作用于认证层(验证用户身份)。
-
信息共享:OAuth 2.0 主要处理访问令牌,而 OIDC 引入了包含用户个人信息的 ID 令牌。
我们可以将 OAuth 2.0 视为一把打开房屋大门的钥匙,而 OIDC 则提供身份验证,以便这把钥匙可以被接收。
这种流程在我们使用的许多应用程序中都很常见,如图 图 6.4 所示:

图 6.4 – 基本的 OAuth 2.0 流程
授权和认证流程被应用程序不断使用,允许两者识别用户身份并定义这些用户可以在 Web 系统或 API 中执行的类型权限。
尽管对 OAuth 2.0 和 OIDC 协议以及授权和认证的概念有直接的解释,但实施这种方法并不简单,并且依赖于一些重要的机制来确保这些功能正常运行。
因此,ASP.NET Core 9 提供了支持遵循之前概述的标准开发身份管理的抽象。实现这些资源的抽象称为 ASP.NET Core Identity。它随着框架的每个新版本而不断发展,允许团队在授权和认证流程中使用安全最佳实践,同时允许对其他身份提供者进行集成,并允许进行自定义。我们将在下一节中了解更多关于这种方法的信息。
使用 ASP.NET Core Identity 框架
现代应用程序与不同类型的技术、协议和标准进行交互。正如我们一直在学习的,在任何解决方案实现流程的任何级别,安全性都极为重要。关于授权和认证的主题,一本书很容易被专门讨论。
然而,ASP.NET Core 9 平台每年都在不断发展,因此,身份管理模型已经经历了多次改进,同时消除了某些依赖项。
为了能够在我们的应用程序中实现授权和认证,我们拥有 ASP.NET Core Identity。它是一个会员系统,为在 ASP.NET Core 9 中开发的基于 Web 的应用程序添加功能,并在认证和授权流程中运行。
ASP.NET Core Identity 可用的功能包括 API、UI、用户身份管理和凭证之间的数据库,以及授予和撤销权限的能力。这还包括与外部登录集成、双因素认证(2FA)、密码管理、能够阻止和激活账户以及在应用程序中提供认证等功能。
在我们学习如何将应用程序与 ASP.NET Core 身份集成之前,让我们更多地了解其结构。
理解 ASP.NET Core Identity 架构
ASP.NET Core Identity 的架构结构分为以下几层:
-
身份管理器:这些是负责实现涉及身份的业务逻辑的服务类。我们可以找到用于用户管理的 UserManager 类和用于角色管理的 RoleManager 类。
-
身份存储:身份存储是表示数据库中每条数据的领域实体。我们可以将身份存储视为数据库中的一个表,该表映射到诸如 UserStore 或 RoleStore 等类。
-
数据访问层:这些是具有与数据库交互所需逻辑的类,以便它们可以持久化和检索与身份相关的信息。
-
数据源:数据源是用于持久化的数据机制。默认情况下,ASP.NET Core Identity 使用 SQL Server。然而,还有其他数据库可供选择,并且可以自定义其他数据源。
这四层具有明确的职责,并且完全可扩展,为开发带来了灵活性,并允许根据组织所需的上下文定制身份机制。
ASP.NET Core Identity 管理身份验证和授权,并与以下类型的数据一起工作:
-
用户:这些代表应用程序中的用户。这个实体实现了一些基本属性,但它们可以很容易地进行扩展。
-
用户声明:这是一组关于用户的声明(声明)。声明向用户的身份添加信息。
-
用户登录:这些提供了有关与外部提供者(如 Facebook、Google、Microsoft 等)进行身份验证的信息,如果你的应用程序与这些提供者有任何集成。
-
角色:这些是授权组。
基于由 ASP.NET Core Identity 平台管理的信息,我们拥有了在应用程序中实现授权和身份验证流程所需的一切。然而,这是一个强大且高度可定制的框架,允许在身份类型上实施各种自定义。
自定义 Identity
如果你想要自定义 Identity,请查阅官方文档:learn.microsoft.com/en-us/aspnet/core/security/authentication/customize-identity-model?view=aspnetcore-9.0。
既然我们已经了解了 ASP.NET Core Identity 的架构结构,是时候将其添加到应用程序中了。
开始集成 ASP.NET Core Identity
现在我们对应用程序上下文中的一些安全视角有了更深入的了解,是时候使用 ASP.NET Core Identity 来添加授权和身份验证流程了。
作为基础,我们将使用本书 GitHub 仓库中提供的源代码,如 技术要求 部分所述,在那里你可以下载完整的解决方案。我们将使用的是我们在 第五章 中创建的 API 项目的版本,因为所有与数据库配置相关的假设都已创建。因此,我们将利用之前创建的 API 结构和数据库结构。目标是实现身份验证。
数据库设置
在启动一个新项目时,你需要配置 EntityFrameworkCore 并将应用程序连接到数据库,正如我们在 第五章 中所学的那样。这样,你就能轻松地跟随并配置 ASP.NET Core Identity。
对于这个应用程序,它是一个连接到 SQL Server 数据库的 Web API,我们将使用我们在第五章中学到的相同模型,并使用EntityFrameworkCore。为此,我们需要添加一个额外的库:Microsoft.AspNetCore.Identity.EntityFrameworkCore。这个库允许 Identity 与 Entity Framework Core 一起工作。
通过打开WorkingWithIdentity.csproj文件或在应用程序目录的终端中运行以下命令来确保此库已添加到您的项目中,以安装它:
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore -v 8.0.2
创建 WorkingWithIdentity.csproj 项目
WorkingWithIdentity.csproj项目是一个受 ASP.NET Core Identity 保护的 Web API,它包含在本章的 GitHub 存储库中,如技术要求部分所述。但是,如果您想为自己创建项目,请按照以下步骤操作:
-
打开您的操作系统终端,访问应创建项目的文件夹。
-
运行以下命令以创建项目:
dotnet new webapi -n WorkingWithIdentity
- 访问新项目文件夹:
cd WorkingWithIdentity
- 添加以下 NuGet 包,所有这些包都是使用 SQL Server 数据库所必需的:
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
- 确保您已安装dotnet-ef工具。要这样做,请运行以下命令:
dotnet tool install --global dotnet-ef
默认情况下,项目已经包含了Microsoft.AspNetCore.Identity库,因为我们创建项目时已经添加了它。然而,我们仍然需要遵循几个额外的步骤来配置项目。让我们首先配置数据库上下文,以便存储和 Identity 模型可以通过EntityFramework进行映射。
配置数据库上下文
为了让 ASP.NET Core Identity 能够管理用户、角色、声明和令牌,我们必须通过将此功能添加到DbContext类(负责与 SQL Server 数据库交互的类)来配置应用程序。
要做到这一点,我们必须更改位于本章存储库中参考项目Context目录下的BankingDbContext类。
第一步是将继承类更改为IdentityDbContext
namespace WorkingWithIdentity.Context;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using WorkingWithIdentity.Model;
public class BankingDbContext : IdentityDbContext<IdentityUser>
{
public BankingDbContext(DbContextOptions
<BankingDbContext> options) : base(options)
{
}
public DbSet<Account> Accounts { get; set; }
public DbSet<Customer> Customers { get; set; }
public DbSet<Movement> Movements { get; set; }
}
正如我们在第五章中学到的,DbContext类是Entity Framework Core的一个抽象,它允许应用程序与数据库交互,其中数据库中的每个实体都由DbSet类型的属性表示。这允许类映射到实体,反之亦然。
通过将 BankingDbContext 类的继承改为 IdentityDb Context
为了确保所有数据库设置在应用程序中可用,打开 Program.cs 文件,并确保文件中存在以下代码行:
builder.Services.AddDbContext<BankingDbContext>(
options => options.UseSqlServer(builder
.Configuration.GetConnectionString("BankingDbContext")));
在 ASP.NET Core 依赖注入容器中配置 DbContext 类非常重要。在这种情况下,我们将 BankingDbContext 类添加到依赖注入上下文,同时配置了使用 SQL Server,其连接将基于 ConnectionString 值,该值作为参数传递给 UseSqlServer 方法。这个 ConnectionString 通过应用程序设置获得,在这种情况下,可以在 appsettings.json 文件中找到。
到目前为止,我们已经有了所有必要的配置,以便在数据层中配置 ASP.NET Core Identity。在下一节中,我们将更新数据库,以便我们可以添加用于身份管理的必要表。
更新数据库
在 第五章 中,我们创建了一个模拟数字银行操作的 API,并使用 Entity Framework Core 连接到数据库。对于这个例子,我们将使用相同的数据库——即 dbBanking。目前,它具有以下数据结构:

图 6.5 – dbBanking 数据库的结构
dbBanking 数据库由四个表组成,其中三个是应用程序上下文的一部分——即 dbo.Accounts、dbo.Customers 和 dbo.Movements。第四个表,dbo.EFMigrationsHistory,负责管理使用迁移对数据库所做的更改的状态。
数据库迁移
在 第五章 中,我们探讨了迁移的工作原理、它们在应用程序开发中的重要性以及可以动态对数据库进行的更改。如果您想了解更多关于 ASP.NET Core 9 迁移的信息,请参阅 learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli。
dbo.EFMigrationsHistory 表包含为银行 API 创建的第一个实体的历史记录。您可以通过应用程序目录结构中的代码生成的历史记录进行检查,如图 图 6 .6 所示:

图 6.6 – API 的迁移类
这些类是由 Entity Framework Core 命令行工具自动生成的,不应手动更改。
在修改了 DbContext 类并添加了 ASP.NET Core Identity 模型之后,我们必须更改数据库。为此,我们将创建一个新的迁移。
要执行此操作,请打开您选择的终端,访问 WorkingWithIdentity 应用程序的根目录,并执行以下命令:
dotnet ef migrations add IdentityModels
上述命令使用了 Entity Framework Core 的 ef 工具,并添加了一个名为 IdentityModels 的迁移。
再次,当打开项目的 Migrations 文件夹时,我们可以分析创建了哪些新类,如图 图 6 .7 所示:

图 6.7 – Identity 模型的迁移类
现在我们有了数据库的迁移结构,我们必须更新 dbBanking 以包括 Identity 表。为此,在您的首选终端中,在项目目录中运行以下命令:
dotnet ef database update
上述命令读取项目中的迁移,分析数据库中 dbo.EFMigrationsHistory 表中的迁移历史,并应用更新,在这种情况下涉及创建 ASP.NET Core Identity 正确工作所需的表。我们将看到创建的新表:

图 6.8 – 数据库现在包含 ASP.NET Core Identity 表
通过这样,所有与数据模型相关的 ASP.NET Core Identity 基本设置都已成功添加。然而,我们还需要向项目中添加一些其他配置,以便应用程序能够处理授权和身份验证。因此,在下一节中,我们将向应用程序的依赖注入上下文中添加 ASP.NET Core Identity 服务。
添加 ASP.NET Core Identity 服务和路由
Asp.Net Core Identity 包含处理授权和身份验证机制所需的抽象,这些抽象使用依赖注入容器中可用的服务,以及用于身份验证和令牌生成的工具。
然而,有必要显式激活这些抽象。为此,我们必须向应用程序中添加几行代码。打开 Program.cs 文件以便我们可以编辑它们。在此阶段,我们必须遵循以下步骤:
-
添加所需的 Identity 命名空间 – 即,using Microsoft.AspNetCore.identity;。
-
将负责确定用户身份的认证服务以及认证方法添加到依赖注入容器中。在这种情况下,我们将使用一个 bearer 令牌:builder.Services.AddAuthentication().AddBearerToken();。
-
通过运行 builder.Services.AddAuthorization(); 向依赖注入容器添加授权服务。
-
通过运行 builder.Services.AddIdentityApiEndpoints
().AddEntityFrameworkStores 来添加 Identity API 并通过 Entity Framework Core 配置数据访问。(); -
使用 app.MapIdentityApi
(); 映射 Identity 端点。 -
将每个请求身份验证中间件添加到应用程序的请求处理管道中,使用由 AddAuthentication() 定义的设置来验证和定义用户的身份:app.UseAuthentication(); 。
-
添加中间件以检查授权策略与已认证用户的身份,以确定用户是否被允许继续当前请求:app.UseAuthorization(); 。
通过这些更改,我们将在 Program.cs 文件中获得以下完整代码:
using Dapper; using Microsoft.AspNetCore.Identity; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.SqlServer; using WorkingWithIdentity.Context; using WorkingWithIdentity.Model; using WorkingWithIdentity.RouteHandler; var builder = WebApplication.CreateBuilder(args); builder.Services.AddAuthentication().AddBearerToken(); // Adding the Authorization Services from the Asp.Net Core Identity builder.Services.AddAuthorization(); // Configure the Database access for the Asp.Net Core Identity builder.Services.AddIdentityApiEndpoints <IdentityUser>() .AddEntityFrameworkStores<BankingDbContext>(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddDbContext<BankingDbContext> (options => options.UseSqlServer(builder .Configuration.GetConnectionString( "BankingDbContext"))); builder.Services.AddScoped(_ => new SqlConnection (builder.Configuration.GetConnectionString( "BankingDbContext"))); var app = builder.Build(); // Configure the HTTP request pipeline adding the ASP.NET Core Identity routes app.MapIdentityApi<IdentityUser>(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.RegisterAccountRoutes(); app.RegisterCustomerRoutes(); app.MapGet("GetAllCustomersUsingDapper", async(SqlConnection connection) => { var customers = await connection .QueryAsync<Customer>("SELECT Id, Name FROM Customers ORDER BY Name"); return Results.Ok(customers); }); app.MapGet("GetCustomerByIdUsingDapper", async(int id, SqlConnection connection) => { var customer = await connection .QueryFirstOrDefaultAsync<Customer>( "SELECT Id, Name FROM Customers WHERE Id = @id", new { id }); if (customer is null) return Results.NotFound(); return Results.Ok(customer); }); app.UseAuthentication(); app.UseAuthorization(); app.Run();
注册账户和客户路由
负责处理 Account 和 Customers API 请求的路由是通过 app.RegisterAccountRoutes 和 app.RegisterCustomerRoutes 扩展方法注册的,如前述代码中突出显示的那样。
这是一种良好的实践,可以正确地分离职责,同时提高 Program.cs 文件代码的可维护性。
为了创建这些扩展方法,创建了两个类,如下所示:
public static class AccountRoutes
{
public static void RegisterAccountRoutes(this
IEndpointRouteBuilder routes)
{
var group = routes.MapGroup("/accounts");
// GET: /accounts
group.MapGet("/", async (BankingDbContext dbContext) =>
{
return await dbContext.Accounts.Include(a =>
a.Customer)
.Include(a => a.Movements)
. ToListAsync();
});
// 其他方法
}
}
public static class CustomerRoutes
{
public static void RegisterCustomerRoutes(this
IEndpointRouteBuilder routes)
{
var group = routes.MapGroup("/customers");
// GET: /customers
group.MapGet("/", async (BankingDbContext dbContext) =>
{
return await dbContext.Customers.ToListAsync();
});
// 其他方法
}
}
在这里创建的扩展类具有一个静态方法,负责注册相应实体的路由。这是一种使代码更加组织化、易于阅读和维护的实践。要了解更多关于创建扩展方法的信息,请访问 learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/how-to-implement-and-call-a-custom-extension-method 。
在分析前述代码时,考虑突出显示的代码行顺序非常重要;否则,对象和路由映射将无法正确工作。
到目前为止,解决方案代码展示了一个配置模型,它已经允许我们利用 ASP.NET Core Identity 的授权和身份验证功能。然而,在某些情况下,需要根据用户角色自定义访问类型。幸运的是,ASP.NET Core 9 提供了一个强大的功能,允许我们将对应用程序资源的访问类型进行隔离,正如我们将在下一节中看到的。
基于角色的授权
为了更好地控制应用程序中的用户授权流程,我们可以实现基于角色的授权。这种基于角色的控制允许您根据分配给用户的角色隔离对应用程序部分类型的访问。想象一个场景,有两个角色:管理员和读者。通过使用基于角色的授权方法,您可以确保只有有权访问应用程序特定区域的用户才能访问特定资源或执行应用程序中的特定操作。
在 ASP.NET Core 9 中,可以通过定义策略来实现基于角色的授权方法,这些策略通过更复杂的逻辑扩展了基于角色的授权,提供了对用户权限的精细控制。
一旦定义了策略,它就可以应用于控制器、操作,甚至 Razor 页面,以强制执行所需的授权行为。策略使您的授权逻辑更加模块化和可重用。这在大型应用程序中特别有用,因为访问控制可能会变得复杂。
例如,考虑一个场景,您想创建一个策略,只允许具有管理员角色的用户访问某些管理资源。此外,您可能还想创建另一个策略,只允许具有经理角色的用户工作超过 1 年,以便他们可以访问特定的报告。这些策略可以在Program.cs文件中定义,然后使用[ Authorize]属性应用于控制器或操作。
让我们看看一个可以添加到Program.cs文件的策略的简单示例:
builder.Services.AddAuthorization(options =>
{ options.AddPolicy("AdminOnly",
policy => policy.RequireRole("Admin"));
});
var app = builder.Build();
app.UseAuthorization();
app.MapGet("/admin", [Authorize(Policy = "AdminOnly")]
() => {
return Results.Ok("Welcome, Admin!");
});
app.Run();
在前面的代码中,我们配置了一个名为AdminOnly的策略,该策略设置了一个规则,即用户具有管理员角色。然后,将[Authorize]属性应用于端点,并使用我们之前创建的策略,限制了符合策略标准的用户访问。
现在,让我们看看一个更复杂的例子。在这里,已经定义了一个自定义策略,该策略检查用户的角色,并提供一个需要用户工作期限为 1 年的额外声明:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("EmployeeWithExperience",
policy =>
{
policy.RequireRole("Manager");
policy.RequireClaim("EmploymentDuration", "1Year");
});
});
var app = builder.Build();
app.UseAuthorization();
app.MapGet("/reports",
[Authorize(Policy = " EmployeeWithExperience ")]
() => { return Results.Ok("Access granted to experienced managers.");
});
app.Run();
在这个例子中,EmployeeWithExperience策略要求用户具有经理角色,并拥有一个名为EmploymentDuration的声明,其值为1Year。此策略应用于/reports端点,仅允许经理访问。
基于角色的授权和策略为您提供了一种强大的方式来管理对应用程序资源的访问,允许您构建超出简单角色检查的复杂授权逻辑,并根据需要包含额外的条件和声明。
现在我们对 ASP.NET Core Identity 有了更多的了解,已经将其集成到我们的应用程序中,并且知道如何通过实现授权策略来隔离对应用程序资源的访问,现在是时候向应用程序的路由添加限制。
使用 ASP.NET Core Identity 保护 API
到目前为止,应用程序已经完全集成了 ASP.NET Core Identity。现在,我们将运行它,以便我们可以分析结果。打开您选择的终端并访问应用程序目录。然后,运行以下命令:
dotnet run
将提供一个http://localhost:<端口号>格式的地址。端口号可能与示例中显示的不同,但执行结果将相同。访问http://localhost:<端口号>/swagger/index.html地址;您应该看到以下输出:

图 6.9 – 集成 ASP.NET Core Identity 的银行 API
如我们所见,API 中添加了新的路由。这些路由是由 ASP.NET Core Identity 提供的 API。每个 API 都允许我们管理应用程序的用户并添加不同的功能,例如密码恢复、用户创建或密码重置。
然而,在尝试执行端点时,例如对/accounts API 执行GET请求,我们意识到我们能够获得有效的响应。要进行测试,只需打开/accounts API 的GET方法,点击Try Out按钮,然后点击Execute按钮。我们应该得到以下响应:

图 6.10 – 未经认证和授权请求 API
如我们所见,我们有一个 HTTP 状态码为200,这意味着请求是成功的,即使结果没有返回数据库中的任何现有账户记录。如果您在本地的数据库表中注册了任何记录,结果将是一个以 JSON 格式序列化的账户对象数组。
然而,我们希望在应用程序的 API 中添加认证和授权过程。为此,我们必须对源代码进行一些修改。
如我们所知,每个 API 都在Program.cs文件中注册了其路由。这些路由作为请求的入口点。由于我们希望保护每个路由,以确保只有已知和授权的用户才能使用 API,我们必须向路由添加配置,以便当有人尝试未经认证请求 API 时,请求必须返回 HTTP 401状态码,通知 API 消费者需要进行认证。
在下一节中,我们将学习如何保护路由并防止未经授权的请求。
保护应用程序路由
即使应用程序中所有认证和授权设置都已存在,也必须确定应该保护什么以及不应该保护什么。
我们可以确保给定的 API 方法通过认证和授权中间件受保护的一种方法是在路由中添加显式配置。
在我们正在开发的应用程序中,API 路由作为扩展方法在单独的文件中实现,这是一种良好的实践。因此,让我们在应用程序的RouteHandler目录中的AccountHandler.cs文件中做出必要的更改。
要做到这一点,我们将配置/accounts路由,使其只接受已认证用户的请求。让我们看看修改后的代码:
app.MapGet("/accounts", async (BankingDbContext
dbContext) => {
var accounts = await dbContext.Accounts.ToListAsync();
return Results.Ok(accounts);
}).RequireAuthorization();
在这里,我们添加了RequireAuthorization()方法调用。我们已经了解到,授权是一个验证用户权限的过程,而认证涉及识别用户。在这种情况下,如果用户未认证,则无法授权。
再次,在您选择的终端中,在应用程序目录内,执行以下命令:
dotnet run
接下来,我们将请求/accounts路由。然而,让我们首先执行Postman应用程序。按照以下步骤操作:
-
前往文件 | 新建 | HTTP 。
-
将打开一个新标签页,您可以在其中发起请求。
-
添加运行应用程序的 URL – 即http://localhost:<端口>/accounts – 并检查是否选择了GET方法。
-
然后,点击发送按钮。我们将得到以下输出:

图 6.11 – 请求受保护的路由
图 6.11突出了带有 HTTP 状态码 401 的请求返回,这意味着请求未被授权。
为了使对这条路由的成功请求能够发起,我们必须登录并使用认证用户的详细信息配置请求。
在登录之前,我们必须在应用程序中创建一个用户。为此,请执行以下步骤:
-
在 Postman 中创建一个新的 HTTP 请求。
-
将请求类型设置为POST。
-
将http://localhost:<端口>/register作为路由添加。这是将添加到应用程序中的 ASP.NET Core Identity 用户的默认路由。
-
在这一点上,我们需要定义请求的正文。为此,请点击正文选项卡,选择raw选项,并添加图 6.12中显示的 JSON:

图 6.12 – 配置注册用户请求的正文
-
您可以根据需要更改 JSON 对象的属性。
-
最后,点击发送按钮来发起请求。
执行后,我们应该得到类似于图 6.13所示的响应:

图 6.13:使用 ASP.NET Core Identity 注册新用户
图 6.13 显示了一个 HTTP 状态码 200,告知我们请求成功,并且一个新的用户已经在数据库中注册。
在这一点上,我们必须登录。我们将使用 Postman 来完成此操作。创建一个新的 HTTP 请求并执行以下步骤以配置请求:
-
将请求类型设置为POST。
-
将 URL 设置为http://localhost:
/login 。 -
选择Body选项卡,然后选择raw选项,并添加以下 JSON:
{ "email": "myuser@myemail.com", "password": "P4$$word" } -
点击发送按钮。
-
确保您已根据在您的环境中创建的用户数据添加了 JSON 参数。
-
执行请求后,您应该看到以下响应:

图 6.14 – 获取登录响应
对于登录请求的响应,我们可以看到返回了一个包含一些重要属性的 JSON 对象:
-
tokenType:此值始终为Bearer,这表示此响应提供了一种Bearer令牌,形式为不透明的accessToken,正如我们在Program.cs文件中配置的那样。
-
accessToken:这是为认证用户生成的令牌。它必须作为授权请求头的一部分发送。
-
expiresIn:表示accessToken过期时间的秒数。
-
refreshToken:如果设置,我们可以在过期后通过使用刷新端点来获取新的access_token值,而无需重新输入用户凭据。
在图 6.14中显示的值对于每个请求都是不同的,并不代表JWT。accessToken在本版本 ASP.NET Core Identity 中以专有方式生成和加密,不遵循已知约定。然而,如果您愿意,可以将其更改为JWT,以及令牌生成过程的其它配置参数。
ASP.NET Core Identity 配置
ASP.NET Core Identity 提供了不同的身份验证选项,包括 JWT (jwt.io/introduction )、cookies 以及其他设置。要了解更多关于不同配置选项的信息,请访问learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.identityoptions?view=aspnetcore-9.0。
然而,我们使用的基于令牌的方法,即使它不涉及 JWT,也使用与使用认证用户凭据发出的请求相同的配置过程。在下一节中,我们将使用access token值对/accounts路由进行请求,并获取有效的响应。
使用访问令牌请求 API
访问令牌包含以加密形式存储的认证用户信息。
为了在/accounts路由上发出有效的请求,我们需要在请求头中传递令牌。因此,复制访问令牌值,在 Postman 中打开包含请求的标签页,如图 6.11所示,为/accounts路由。然后执行以下步骤:
-
在/accounts路由的GET请求标签页中,点击授权标签。
-
对于类型,选择Bearer 令牌选项。
-
在令牌字段中,粘贴通过登录请求获得的访问令牌的值。
-
点击发送按钮。
如图 6.15所示,由于数据库中没有注册任何账户,因此返回了一个空数组。请注意,HTTP 状态码是200,这意味着这是一个成功的请求:

图 6.15 – 成功的账户请求
到目前为止,应用程序按预期工作。然而,了解这个授权过程是如何工作的是非常重要的。
当再次请求账户路由时,我们提供之前配置的访问令牌。尽管 Postman 有一个用户友好的界面,但在选择认证类型并输入访问令牌时,Postman 会自动添加一个 HTTP 头。HTTP 头是请求和响应的一部分,由键/值对组成。
对于这个请求,使用授权键和访问令牌值创建了头。您可以通过点击头部标签并查看隐藏的头来检查此头,如图 6.16所示:

图 6.16 – 授权 HTTP 头
在发起请求时,认证和授权中间件开始工作。认证中间件读取授权头中告知的令牌,并在HttpContext.User对象中填充请求的用户凭据,该对象是请求的一部分。此对象允许我们访问诸如声明等信息,这些声明包含诸如用户姓名和电子邮件等数据,以及角色,这些角色允许我们确定用户的访问类型,例如管理员、成员等。
ASP.NET Core 9 的 HttpContext 对象
在 ASP.NET Core 中,HttpContext.User是一个核心属性,它表示与 HTTP 请求关联的用户安全上下文。该属性是ClaimsPrincipal的一个实例,这是一个.NET 类,它以声明的形式包含用户的身份。HttpContext.User是处理 ASP.NET Core 应用程序中用户认证和授权的关键元素。
在认证过程中,当请求到达应用程序时,认证中间件读取附加到请求的认证令牌或 cookies,验证它们,并构建一个ClaimsPrincipal对象。ClaimsPrincipal对象可以包含一个或多个ClaimsIdentity实例。
每个ClaimsIdentity实例可以包含多个声明。声明是由发行者做出的关于主体的陈述,可以代表用户的身份属性,如姓名、角色、电子邮件等。
此过程允许在应用程序范围内的授权检查中使用HttpContext.User,以确定当前用户是否有权限执行某些操作,以确保只有经过适当认证和授权的用户才能访问某些资源或执行特定操作。
以下代码展示了在执行操作时使用HttpContext.User的方式。此对象由执行管道通过中间件自动填充:
public IActionResult ExampleAction(){
var user = HttpContext.User;
if (user.Identity.IsAuthenticated) {
// 为 认证用户执行某些操作
var userName = user.Identity.Name;
// 获取 用户名
return Content($"欢迎,{userName}");
}
else
{
// 处理 未认证用户
return Unauthorized("您必须登录才能 访问此页面。");
}
}
您可以在learn.microsoft.com/en-us/aspnet/core/fundamentals/http-context?view=aspnetcore-8.0#httpcontext-user了解更多关于HttpContext的信息。
授权中间件分析请求的路线是否需要授权。如果是,它将解密令牌,分析它是否是有效的令牌,并允许在路线上的请求正确执行。
中间件是什么?
在 ASP.NET Core 9 中,请求的执行流程中会执行多种类型的操作,例如确定要执行的路线以及其他功能。这个流程被称为管道。在某些情况下,需要向执行管道中添加功能。这是通过中间件实现的,我们通过在Program.cs文件中添加app.UseAuthentication()和app.UseAuthorization()方法调用来实现,这允许我们在认证和授权需求之前预处理请求。通过中间件,可以添加到请求和响应的功能。
我们将在第八章中了解更多关于中间件的内容。
在每次 API 请求中,都会发送令牌,以便在请求执行流程中加载用户信息和相应的访问权限。这是云原生应用程序的一个特性。无状态方法允许应用程序可扩展和弹性,并确保服务器在请求之间不保留任何关于客户端状态的任何信息,消除了管理会话状态的需求。这导致更容易进行扩展和负载均衡,当需要处理高用户需求时,更容易扩展服务器,并允许每个服务器实例能够处理任何请求,而无需了解先前请求的上下文。
通过不依赖服务器端状态,开发者可以避免与会话管理相关的问题,例如会话持久性、分布式系统间的同步以及资源锁定。
现代应用程序必须在设计上以安全为前提,并且认证和授权的实现应用程序上下文中有几个优点。然而,与安全相关的其他方面不仅与用户可用的功能相关,还与应用程序的源代码相关。在下一节中,我们将学习如何加强应用程序的安全性。
加强应用程序安全性
为了能够创建安全的基于 Web 的应用程序,我们必须超越仅基于身份验证和授权的安全层实现,这是我们使用 ASP.NET Core Identity 时所实现的。
ASP.NET Core 9 允许我们在开发应用程序时将安全作为前提,提供工具和机制,以促进实现最小化可能漏洞的功能,这可以防止恶意用户发起攻击。
让我们了解一些应该成为每位软件工程师工具箱一部分的良好安全实践。我们将从了解如何改进我们在开发环境中管理敏感配置的过程开始。
正确管理机密信息
每个应用程序都有配置,其中一些可能是敏感的,例如数据库连接、加密密钥,甚至是访问外部资源的密钥。
到目前为止,我们已经了解到将此类设置与 C# 源代码分开是一种良好的实践,我们可以通过诸如 appsettings.json 和甚至环境变量等文件来管理设置。ASP.NET Core 9 允许我们处理外部配置管理。
将设置硬编码是一种不良实践,因为要更改任何硬编码参数,我们必须重新编译应用程序。此外,如果恶意用户可以访问二进制文件,他们可能会反编译应用程序,然后获取敏感数据。
加密器
代码混淆是指将应用程序源代码转换为人类难以理解但计算机仍能执行的形式的过程。这种技术主要用于通过使攻击者或未经授权的用户难以逆向工程代码和理解其逻辑来保护知识产权。
加密过程涉及多种技术,例如将变量和方法重命名为无意义的符号,删除元数据,加密字符串,以及改变控制流以使代码更复杂。有关更多信息,请访问learn.microsoft.com/en-us/visualstudio/ide/dotfuscator/?view=vs-2022。
想象一下这种情况:我们的应用程序使用 API 密钥连接到支付网关,以处理来自在线商店的交易,或者甚至是数据库连接字符串。如果这个密钥被泄露,恶意用户可能能够操纵交易数据,访问敏感信息,甚至删除您的数据库。
您可能认为,由于您的代码在私有存储库中,并且所有设置都保存在appsettings.json文件中,这个问题已经解决了。
当然,由于这是一个私有存储库,攻击者获取数据的可能性并不高。然而,请考虑您的公司可能与员工和第三方公司合作,这些公司可以访问您的存储库中的数据。
虽然在appsettings.json文件中管理设置是一种良好的实践,但这并不是处理敏感信息的良好方法。因此,我们避免了将应用程序的源代码与应用程序外部存储库同步,这些存储库包含不应共享的信息。
幸运的是,ASP.NET Core 9 实现了最佳开发实践,并在您的本地环境中提供了秘密管理。适当的秘密管理确保敏感数据,如 API 密钥,不会硬编码到应用程序的源代码中,而是安全地存储和访问,从而保护您的基础设施和数据完整性。
秘密管理器工具包含在.NET Core SDK 中,因此如果您已经有了 SDK,通常不需要安装其他任何东西。
要开始使用秘密管理器,您需要为您的项目初始化它。在命令提示符或终端中导航到包含您的.csproj文件的项目WorkingWithIdentity目录,这是我们之前工作过的目录。然后,运行以下命令以初始化秘密存储:
dotnet user-secrets init
上述命令在.csproj(项目文件)中的PropertyGroup值内添加了一个UserSecretsId元素。此 ID 唯一标识您项目的秘密。
您可以通过在代码编辑器中打开.csproj文件来验证UserSecretsId元素的添加,如图图 6 .17 所示:

图 6.17 – 在.csproj 文件中配置的 UserSecretsId 元素
接下来,我们将使用 SQL Server 数据库配置连接字符串。为此,我们必须通过执行以下命令添加一些新代码:
dotnet user-secrets set "ConnectionStrings:BankingDbContext" "YOUR DATABASE CONNECTION STRING"
秘密命名约定
这种表示法通常使用冒号(:)来分隔秘密键名中的不同层级。这种结构不仅有助于逻辑上组织键,而且与 ASP.NET Core 9 配置系统从各种配置源检索值的方式相一致,例如appsettings.json、环境变量和 Secret Manager。
在WorkingWithIdentity应用程序中,我们在appsettings.json文件中有以下配置:
" ConnectionStrings" {
" BankingDbContext: "..."**
}
前面的 JSON 表示一个名为ConnectionStrings的对象类型的属性,它有一个名为BankingDbContext的字符串属性。
基于此,秘密被命名为ConnectionStrings:BankingDbContext。在这里,ConnectionStrings是顶级类别,而BankingDbContext是包含相应秘密的实际键——在这种情况下,是 SQL Server 数据库连接字符串。这种表示法有助于逻辑上分组相关设置。
由于这是 ASP.NET Core 9 使用的约定,因此无需更改应用程序的源代码即可获取数据库连接字符串。
当使用环境变量(在某些操作系统中不允许变量名中包含冒号)时,冒号分隔符通常被替换为双下划线(__)。所以,如果你在生产环境中通过环境变量定义这些秘密,你会这样定义它们:
ConnectionStrings__BankingDbContext
这种命名约定确保当 ASP.NET Core 配置系统读取环境变量时,它可以重建层次结构并将它们视为与在appsettings.json或 Secret Manager 中定义的秘密等效。
由于这是 ASP.NET Core 9 的功能,并且如果你想要使用环境变量,它将以相同的方式工作,因此无需更改应用程序代码来获取秘密。
创建的秘密被保存在操作系统中;其位置可能因环境而异。然而,你可以使用user-secrets工具来管理秘密。例如,你可以用它来列出你本地机器上存在的秘密:
dotnet user-secrets list
你可以使用以下命令来删除特定的秘密:
dotnet user-secrets remove "ConnectionStrings:BankingDbContext"
你甚至可以清除所有秘密:
dotnet user-secrets clear
所有秘密信息都保存在你的操作系统中。当将源代码与远程代码库集成时,秘密不会被共享。
请记住,Secret Manager 工具仅适用于开发目的。对于生产环境,您应使用安全的保险库,例如 Azure Key Vault、AWS Secrets Manager 或其他安全方式来管理敏感配置数据。我们将在 第九章 中了解更多关于配置管理的内容。
现在我们已经了解了如何更好地管理应用程序的秘密,让我们学习其他良好的安全实践,包括使用 超文本传输协议安全 ( HTTPS ) 和 跨源资源共享 ( CORS )。
强制执行 HTTPS 和处理 CORS
HTTPS 强制执行对于确保客户端和服务器之间通过加密网络传输的数据安全通信非常重要。正如我们之前所学的,ASP.NET Core 9 为我们提供了内置的中间件来强制执行 HTTPS,这可以被配置为将所有 HTTP 请求重定向到 HTTPS。
要在 ASP.NET Core 9 应用程序中强制执行 HTTPS,只需将以下代码行添加到 Program.cs 文件中,以向应用程序的执行管道添加中间件:
// Enforce HTTPS
app.UseHttpsRedirection();
重要的是要知道,添加强制使用 HTTPS 的中间件是应用程序的配置步骤。同样,您还必须配置您的 Web 服务器(例如,IIS、NGINX、Azure App Services 等)以强制执行 HTTPS 并从受信任的证书颁发机构获取有效的 SSL/TLS 证书。
除了强制执行 HTTPS,您还可以配置 CORS,这是一种由浏览器实现的功能,用于限制在一个源上运行的 Web 应用程序在没有明确权限的情况下访问不同源上的资源。在像 Angular、React 或纯 JavaScript 这样的 SPA 应用程序中,更常见这种行为。当通过 JavaScript 发送 HTTP 请求时,它将在浏览器中执行,通过安全机制,不允许一个源服务器上的请求发送到资源所在的服务器。幸运的是,ASP.NET Core 9 提供了中间件来配置和管理 CORS 策略,允许您指定哪些源、头和方法是允许的。这个特性很有趣,因为我们只能根据特定的源响应某些请求。
要在 ASP.NET Core 9 应用程序中启用和配置 CORS,您可以将以下代码添加到 Program.cs 文件中:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
// Configure CORS policy
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigin",
builder =>
{
builder.WithOrigins("https://myapp.com")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
var app = builder.Build();
// Enforce HTTPS
app.UseHttpsRedirection();
// Use CORS policy
app.UseCors("AllowSpecificOrigin");
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
让我们理解前面的代码:
-
builder.Services.AddCors(options => { ... }):这一行代码将 CORS 服务添加到应用程序的依赖注入容器中。options 参数,其类型为 Action
,允许您配置 CORS 策略。 -
options.AddPolicy:在这一行中,我们添加了一个名为 AllowSpecificOrigin 的新策略。Lambda 表达式中的 builder 参数是 CorsPolicyBuilder 类的实例,它提供了配置策略的方法。
-
builder.WithOrigins("https://myapp.com"):WithOrigins 方法定义了允许访问应用程序资源的来源。在这种情况下,来自 https://myapp.com 的任何请求都将被此 CORS 策略允许。
-
AllowAnyHeader():AllowAnyHeader 方法允许请求中的任何 HTTP 头部,允许指定的来源包含任何头部而不会被 CORS 策略阻止。
-
AllowAnyMethod():此方法定义了任何 HTTP 方法(GET、POST、PUT、DELETE 以及其他)都可以在请求中使用。
-
app.UseCors("AllowSpecificOrigin"):这将在请求管道中触发 CORS 中间件,全局引用先前创建的策略以应用于应用程序的所有 HTTP 请求。
了解更多关于 CORS 的信息
如前所述,CORS 是一种基于 HTTP 头部的机制,允许您告诉浏览器哪些来源可以加载资源。ASP.NET Core 9 为在应用程序中实现 CORS 提供了一个优秀的框架。要了解更多信息,请访问 learn.microsoft.com/en-us/aspnet/core/security/cors?view=aspnetcore-9.0。
上述示例说明了在 ASP.NET Core 9 应用程序中使用 CORS。然而,除了来源之外,它对 HTTP 头部或方法的使用没有限制。在某些情况下,将有必要明确定义来源可以访问的 HTTP 头部和方法。
然而,ASP.NET Core 9 中可用的功能为我们提供了在定义不同来源的不同策略方面的巨大灵活性,为 CORS 创建了更受限制和更具体的规则。这是一个重要的机制,因为浏览器使用它来允许 SPAs 或在客户端运行的其他应用程序能够适当地消耗外部资源。
CORS 不是一个安全机制,但建议使用。在下一节中,我们将讨论我们可以用来防止应用程序中漏洞的一些安全机制。
防止常见漏洞
当我们谈论应用程序中的漏洞时,可以考虑几个主题,例如源代码、服务器、凭证管理、使用的协议和加密等。
一些常见的漏洞已经众所周知,但如果它们在应用程序中没有得到解决,它们可能会给组织带来一些问题。ASP.NET Core 9 提供了处理 Web 应用程序中几个常见威胁的机制:
-
SQL 注入
-
跨站 脚本(XSS)
-
跨站请求 伪造(CSRF)
让我们学习如何防止这些漏洞中的每一个。
SQL 注入
SQL 注入是一种常见的攻击,攻击者将恶意 SQL 代码插入到 SQL 查询中。
为了防止 SQL 注入,始终使用参数化查询或 ORM 框架,如 Entity Framework,这些框架可以安全地处理查询参数并帮助我们避免字符串连接。我们已经在 Web 应用程序安全主题 部分学习了这一点。
XSS
当攻击者将恶意脚本注入到网页中时,就会发生 XSS 攻击。为了防止 XSS,在将用户输入渲染到浏览器之前,始终对其进行编码或转义。这样,如果输入中存在任何代码注入,例如,它将被特殊字符编码。ASP.NET Core 9 提供了内置的辅助函数来清理输出,如下面的示例所示:
@{
var inputSimulator = "<script>
alert('Injected Code');</script>";
}
<p>@inputSimulator</p>
// output: <script>alert('Injected Code');</script>
在前面的例子中,JavaScript 代码被编码,防止注入的代码被发送和执行,因为编码后它只是一个字符串。要了解更多关于与 XSS 相关的漏洞信息,请访问 learn.microsoft.com/en-us/aspnet/core/security/cross-site-scripting?view=aspnetcore-9.0。
CSRF
CSRF 是一种安全攻击类型,恶意网站欺骗用户的浏览器在用户不知情的情况下,在用户已认证的另一个网站上执行操作。这可能导致未经授权的操作,例如更改设置、转账或购物。ASP.NET Core 提供了内置的防伪造令牌来防止 CSRF 攻击。这些令牌会自动包含在表单中并在服务器上进行验证。
要在 Razor Pages 或 MVC 中以简化的方式使用防伪造令牌,请将以下代码添加到您的表单中:
<form method="post">
@Html.AntiForgeryToken()
<!-- Form fields -->
<input type="submit" value="Submit" />
</form>
在这一点上,我们必须将 ValidateAntiForgeryToken 属性添加到将处理表单请求的动作中,如下所示:
[ValidateAntiForgeryToken]
public IActionResult SubmitForm()
{
// Process the form submission
return View();
}
ASP.NET Core 9 还提供了其他机制来处理这种漏洞。您可以在 learn.microsoft.com/en-us/aspnet/core/security/anti-request-forgery?view=aspnetcore-9.0 上了解更多信息。
正如我们所学的,应用程序可能包含多个漏洞,这些漏洞不仅与源代码相关,还与托管服务器、通信协议以及许多其他方面相关。
在任何情况下,ASP.NET Core 9 平台提供了多种机制和最佳实践,结合应用的需求,使我们能够最小化风险,并保持我们的解决方案在遵循现代应用程序最佳实践的同时,保持稳健、安全和可靠。随着我们继续阅读本书,我们将了解更多可以用来创建越来越稳健应用程序的机制和方法。
摘要
在本章中,我们学习了网络应用程序安全性的原则以及它们如何影响开发模型和与用户及其他应用程序的交互。此外,我们还学习了授权和认证过程,比较了这些过程的流程,并了解了 OAuth 2.0 和 OIDC 等标准。为了加强我们对认证和授权的知识,我们使用了 ASP.NET Core Identity,它提供了支持应用程序中用户认证和授权的所有机制,并与数据库集成以安全地管理身份。为此,我们通过提供 ASP.NET Core Identity 提供的令牌来安全地消费信息。最后,我们讨论了如何增强应用程序的安全性,理解了密钥管理,并学习了使用 CORS 等技术来防止网络应用程序中的常见漏洞。
在下一章中,我们将学习如何为应用程序添加更多功能,了解如何实施最佳实践,以及如何使用缓存和监控。
第三部分:应用最佳实践
在本节中,我们假设您对 ASP.NET Core 9 平台更加熟悉,并且熟悉该技术中大多数强大的功能。随着我们对平台知识的深入和开发越来越丰富解决方案的需求,我们必须坚持最佳实践。因此,我们将涵盖与添加与应用程序交互的功能相关的主题,包括挑战策略、弹性和最佳实践。我们还将学习如何实现监控(日志记录和跟踪),使软件工程师能够处理错误修复、优化和主动行动。我们还将探索使用中间件来自定义应用程序中的交互流程。
本部分包含以下章节:
-
第七章 ,为应用程序添加功能
-
第八章 ,在 ASP.NET Core 9 中使用中间件增强应用程序
-
第九章 ,管理应用程序设置
第七章:为应用程序添加功能
ASP.NET Core 9 提供了不同的特性和工具,使我们能够开发强大的基于网络的解决方案。然而,我们通常需要更多专业化的特性,以便提供更好的端到端体验。在本章中,我们将学习与网络应用程序相关的良好实践,例如添加缓存、使用异步机制、弹性机制和日志记录。我们将探讨使用 ASP.NET Core 9 开发应用程序的必要最佳实践,包括正确使用异步机制、HTTP 请求以及通过日志进行应用程序仪表化。
在本章中,我们将关注以下主题:
-
使用 ASP.NET Core 9 最佳实践进行工作
-
通过缓存策略提高性能并使应用程序具有弹性
-
理解和实现日志记录和监控
技术要求
为了支持本章的学习,以下工具必须在您的开发环境中存在:
-
Docker:必须在您的操作系统上安装 Docker 引擎,并运行 SQL Server 容器。您可以在第五章中找到有关 Docker 和 SQL Server 容器的更多详细信息。
-
Postman:此工具将用于执行对开发应用程序 API 的请求。
-
Redis Insight:此工具用于连接到 Redis 服务器数据库(
redis.io/insight/)。
本章中使用的代码示例可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/ASP.NET-Core-9.0-Essentials/tree/main/Chapter07。
使用 ASP.NET Core 9 最佳实践进行工作
到目前为止,我们已经了解了 ASP.NET Core 9 在创建高质量网络系统中的几个特性和好处。当然,就像任何其他软件开发技术一样,我们处理代码的方式没有限制。这样,我们有自由创造解决方案和新标准,以满足特定的需求。
然而,依赖良好实践不仅可以扩大我们开发高质量应用程序的能力,还可以避免浪费几个小时来实现目标。
在这种情况下,我们将讨论一些必要的良好实践,以提升我们应用程序的质量,从正确使用HTTP 请求开始。
HTTP 请求最佳实践
HTTP 请求是处理网络应用程序时的一个基本组件。正确处理 HTTP 请求可以显著影响应用程序的性能和可靠性。
我们已经在第三章中学习了 HTTP 动词和状态码的类型。然而,应用程序提供的每个 HTTP 方法都必须得到适当的处理,以避免应用程序中的不一致性和避免漏洞。
此外,HTTP 请求的方式直接影响到您解决方案的用户或消费者的体验。
让我们了解一些与 HTTP 请求相关的良好实践。
验证和清理输入
总是验证和清理输入,以防止诸如 SQL 注入和 跨站 脚本(XSS)等安全漏洞。
XSS
XSS 是一种安全漏洞,攻击者会将脚本注入到网页中。要了解更多信息,请访问 learn.microsoft.com/en-us/aspnet/core/security/cross-site-scripting?view=aspnetcore-9.0。
考虑一个场景,即用户提交一个包含用户名的表单。为了防止有害数据被处理,您应该验证输入以确保它符合预期标准,并清理它以移除任何恶意内容:
public IActionResult Submit(string username)
{
if (string.IsNullOrEmpty(username))
{
return BadRequest("Username is required.");
}
username = HttpUtility.HtmlEncode(username);
// Proceed with processing the username
return Ok();
}
以下代码演示了对用户名参数的简单验证,if(string.IsNullOrEmpty),避免错误使用。使用 HttpUtility.HtmlEncode(username) 方法将如 <、>、& 等字符转换为 HTML 编码格式。
使用异步方法
在 HTTP 请求的执行流程中,我们必须避免进行同步处理操作。否则,这可能会降低用户体验,并导致应用程序出现一些问题,例如以下情况:
-
线程阻塞:同步方法在等待 I/O 操作(如数据库查询、文件访问或网络请求)完成时会阻塞线程。在 ASP.NET Core 应用程序中,线程池是一种有限资源。
-
线程池耗尽:当应用程序大量依赖同步方法时,线程池可能会耗尽,尤其是在高负载下,此时所有可用的线程都被阻塞,没有新的线程来处理传入的请求。
建议并良好实践是使用异步方法来提高性能和可伸缩性。例如,当使用 HttpClient 对象在 API 中进行请求时,使用 HttpClient.SendAsync 方法而不是 HttpClient.Send。
异步编程允许您的应用程序同时处理多个任务,而无需等待每个任务完成后再开始下一个任务。这类似于一个繁忙厨房中的厨师可能一次准备多个菜肴,而不是完成一个菜肴后再开始另一个。
我们将在“异步请求和 I/O 优化”部分更详细地介绍异步编程的使用。现在,让我们了解与 HTTP 请求相关的另一个良好实践,即缓存和压缩。
缓存和压缩
通过 HTTP 协议发送的请求有一些属性,包括头和体。在应用程序与后端通信期间,这些信息被传输,并且头由客户端(在这种情况下,是浏览器)和后端使用。
HTTP 头有多种类型,包括与缓存和压缩相关联的头。
通过利用缓存和响应压缩,我们可以减少带宽使用并提高加载时间。浏览器也会识别这些头,避免对服务器进行不必要的请求。
缓存和压缩的工作原理类似于图书馆如何使频繁借阅的书籍易于获取,或者真空包装的包装如何占用更少的空间。这些做法减轻了服务器的负担,并加快了对用户请求的响应。
让我们分析以下从Program.cs类中提取的代码片段:
// Add services to the container. builder.Services.AddResponseCaching();
app.UseResponseCaching();
app.Use(async (context, next) => {
context.Response.GetTypedHeaders().CacheControl =
new Microsoft.Net.Http.Headers.CacheControlHeaderValue
{
Public = true, MaxAge = TimeSpan.FromMinutes(10)
};
await next();
});
让我们了解前面的代码。当您将app.UseResponseCaching添加到应用程序的中间件管道中时,它执行以下功能:
-
检查 Cache-Control 头:
-
中间件根据 Cache-Control 头的存在检查传入的请求是否可以被缓存
-
如果找到有效的 Cache-Control 头并且允许缓存,中间件将继续处理请求
-
-
将响应存储在 缓存中:
-
如果请求的响应可以被缓存,中间件将响应存储在缓存中
-
符合缓存标准的后续请求将直接从缓存中提供,无需再次生成响应
-
-
提供 缓存响应:
-
对于与之前缓存的响应匹配的请求,中间件提供缓存的响应
-
这样可以减少处理时间和服务器负载,因为响应是直接从缓存中检索的
-
app.Use(async (context, next)方法向中间件管道添加了 Cache-Control 头所需的参数,例如缓存持续时间。这是必要的,以便客户端可以知道如何缓存响应。
缓存由应用程序的内存管理,因此,长时间在内存中保留缓存可能引起问题,但这是一种良好的做法。我们将在下一节中详细介绍缓存的使用,通过缓存策略提高性能并使 应用程序具有弹性 。
为了进一步提高响应性能,我们可以通过几行代码自动执行压缩。
为了这个目的,我们必须将Microsoft.AspNetCore.ResponseCompression NuGet 包添加到项目中。您可以在应用程序的项目目录中键入以下命令:
dotnet add package Microsoft.AspNetCore.ResponseCompression
在任何情况下,了解如何在您的应用程序中使用此功能都很重要。
在添加 NuGet 包后,我们必须将压缩服务添加到 Program.cs 文件中。这样做时,我们得到以下修改后的文件,考虑到缓存和压缩:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
// Enable compression for HTTPS requests
options.Providers.Add<GzipCompressionProvider>();
// Add Gzip compression
options.Providers.Add<BrotliCompressionProvider>();
// Add Brotli compression
});
builder.Services.Configure<
GzipCompressionProviderOptions>(options =>
{
options.Level = System.IO.Compression
.CompressionLevel.Fastest;
// Set compression level for Gzip
});
builder.Services.AddResponseCaching();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseResponseCompression(); // Use response compression middleware
app.UseResponseCaching(); // Use response caching middleware
app.Use(async (context, next) =>
{
context.Response.GetTypedHeaders().CacheControl =
new Microsoft.Net.Http.Headers.CacheControlHeaderValue
{
Public = true,
MaxAge = TimeSpan.FromMinutes(10)
};
await next();
});
app.UseAuthorization();
app.MapRazorPages();
app.MapControllers();
app.Run();
以下代码可以这样解释:
-
添加响应 压缩中间件 :
-
使用 builder.Services.AddResponseCompression 方法将响应压缩服务添加到 依赖注入(DI)容器中。
-
options.EnableForHttps 设置为 true 以启用 HTTPS 响应的压缩。
-
options.Providers.Add
() 和 options.Providers.Add() 用于添加对 Gzip 和 Brotli 压缩提供者的支持。
-
-
Configure compression options :
- builder.Services.Configure<GzipCompressionProvider Options>(options => options.Level = System.IO.Compression.CompressionLevel.Fastest) 用于配置 Gzip 的压缩级别。你可以根据需要调整压缩级别(Optimal,Fastest 或 NoCompression)。
-
使用中间件 :
- app.UseResponseCompression() 将响应压缩中间件添加到请求管道中。
中间件的处理顺序很重要
当结合响应缓存和压缩时,中间件的顺序很重要。确保在缓存中间件之前包含压缩中间件。这样,响应在缓存之前被压缩,确保缓存响应已经压缩并准备好高效地提供服务。
通过这些实践,你可以减小响应的大小,从而提高性能和加快用户的加载速度。
现在是更详细地理解异步请求的时候了。
异步请求和 I/O 优化
异步编程是现代 Web 开发的基本方面,它使非阻塞操作成为可能,从而提高了应用程序的响应性和可伸缩性。
异步编程的复杂性被 C#中可用的资源所抽象,使应用程序和功能更加强大。但要更好地理解这个异步过程的重要性,让我们分析以下示例。
想象你在咖啡店排队等候。如果咖啡师必须等待每杯咖啡完全煮好才开始下一杯,队伍会移动得非常慢。相反,咖啡师在准备下一杯饮料的同时开始准备前一杯。同样,异步编程允许你的应用程序在等待前一个任务完成时开始其他任务。
Web 应用程序可以在给定时间内响应用户的大量请求。ASP.NET Core 9 已经足够优化,能够高效地管理请求和内存。然而,如果你选择使用同步方法,这也是可能的,可能会引起一些问题。让我们看看我们如何开发异步方法。
使用 async 和 await 关键字
在 C# 中,async 和 await 关键字让您能够编写更易于阅读和维护的异步代码。
例如,在 ASP.NET Core 应用程序的情况下,使用 async 和 await 允许您的服务器在 I/O 操作期间不阻塞线程,从而同时处理更多请求,如下面的代码所示:
public async Task<IActionResult> GetDataAsync()
{
var data = await _dataService.GetDataAsync();
return Ok(data);
}
让我们看看代码中突出显示的细节:
-
async : 这是用于指示方法为异步的关键字。在声明异步方法时,必须在方法体中使用至少一个
await关键字来执行异步操作。 -
Task
: 这指定了该方法返回一个最终将以 IActionResult完成的任务。Task类型代表 C# 中的异步操作。IActionResult是 ASP.NET Core MVC 中的常见返回类型,它表示操作方法的返回结果。返回类型可以是任何类型的类或结构,例如,返回一个整数,如Task<int>。 -
await :
await关键字用于异步等待GetDataAsync方法的完成。这意味着该方法将返回一个任务,并且执行将在任务完成之前暂停,而不会阻塞线程。 -
_dataService.GetDataAsync() : 这行代码在
_dataService对象上调用异步GetDataAsync方法。_dataService可能是一个处理数据检索的服务类实例。
C# 有几个异步方法,您可以通过在方法名称中添加 async 后缀作为约定来识别它们。
异步编程
C# 中的异步编程有几个其他细节和应用方式,它们不能作为本书的一部分考虑。然而,为了继续您的学习,我建议您阅读 Microsoft Learn 上的这篇优秀内容:learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/。
通过使用 ASP.NET Core 9 中可用的功能,我们可以通过一些关键字实现异步请求。
这些资源可以结合使用,例如与 Entity Framework Core 一起使用。
我们可以使用 ToListAsync() 和 SaveChangesAsync() 等方法使用 Entity Framework Core 实现异步数据访问。
异步数据访问允许您的应用程序在等待从数据库获取数据时执行其他操作,如下面的代码所示,其中通过 Entity Framework 对 Customers 表执行异步查询以获取所有记录:
public async Task<List<Customer>> GetCustomersAsync()
{
return await _dbContext.Customers.ToListAsync();
}
考虑在应用程序设计中使用异步编程。
虽然 ASP.NET Core 9 平台为我们提供了创建健壮应用程序的多种机制,但我们必须记住,在所有开发的应用程序中,除了异步编程模型外,还必须考虑 HTTP 请求、压缩和信息缓存的最佳实践。这保证了用户和集成系统的最佳体验,同时确保应用程序能够足够优化以正确支持大量需求。
在下一节中,我们将更详细地了解缓存策略的使用以及如何使应用程序具有弹性。
通过缓存策略提高性能并使应用程序具有弹性
在 Working with ASP.NET Core 9 best practices 部分的 HTTP request best practices 子部分中,我们了解了一些能够为我们的应用程序带来多项改进的机制。讨论了一些方法,包括对缓存使用的简要介绍。
为了扩展我们的知识并添加技术到我们健壮的应用程序开发模型中,我们将探讨缓存策略的使用以及如何使我们的应用程序具有弹性,这是现代解决方案的基本要求。
让我们从首先了解不同的缓存策略类型开始。
缓存策略
缓存是一种强大的技术,通过在临时存储位置存储频繁访问的数据来提高应用程序性能。这减少了从原始数据源重复检索数据的需求。
在 Caching and compression 子部分中,演示了一段代码,使应用程序能够管理缓存,为 ASP.NET Core 9 中间件添加功能,该中间件在请求处理期间使用。在这种情况下,使用了 内存 缓存策略,将数据存储在内存中以实现快速访问。这对于频繁访问的小到中等数据集是合适的。
然而,对于更健壮的应用程序,另一种称为 分布式缓存 的策略是必要的。
分布式缓存使用某种专门用于分布式缓存的资源,例如 Redis。
Redis 是一种用于大数据集或分布式环境运行时的强大技术。
什么是 Redis?
远程字典服务器 ( Redis ) 是一个开源的内存数据结构存储。它以其高性能、灵活性和对多种数据结构的支持而闻名。
Redis 将数据存储在内存中,这使得它与基于磁盘的数据库相比速度极快,并且还支持定期在磁盘上持久化数据。
Redis 的持久化模型是键/值,支持字符串、散列、列表、集合、有序集合、位图、HyperLogLogs 和地理空间索引等数据结构。这种灵活性允许有各种用例。
Redis 是多个应用程序广泛使用的资源;如果您想了解更多信息,请访问此链接:redis.io/。
许多现代应用程序,主要托管在云环境中,使用 Redis 作为分布式缓存的解决方案,同时完全集成到 ASP.NET Core 9 中。
为了更好地理解 Redis 与 ASP.NET Core 9 集成时的工作方式,让我们实现一个应用程序。
需要考虑 技术要求 部分中提到的要求。让我们学习如何将 Redis 集成到我们的应用程序中。
在我们的应用程序中集成 Redis
我们将首先创建一个应用程序。因此,在您选择的目录中打开终端,并执行以下步骤:
-
通过运行以下命令创建一个新的 ASP.NET Core 9 项目:
dotnet new webapi -n DistributedCacheExample cd DistributedCacheExample -
添加 Redis 缓存包:
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis -
现在,运行以下命令以在应用程序目录中打开 Visual Studio Code:
Code . -
打开 appsettings.json 文件,将其内容更改为以下代码:
{ "ConnectionStrings": { "Redis": "localhost:6379" } }前面的 JSON 定义了我们稍后将要创建的 Redis 服务器的连接字符串。
-
打开 Program.cs 文件,并将其所有内容更改为以下代码:
using Microsoft.Extensions.Caching.Distributed; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // Configure Redis distributed cache builder.Services.AddStackExchangeRedisCache(options => { options.Configuration = builder .Configuration.GetConnectionString("Redis"); options.InstanceName = "myPrefix_"; }); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseAuthorization(); app.MapControllers(); app.Run();您应该已经熟悉了之前描述的大多数代码。builder.Services.AddStackExchangeRedisCache 方法添加了默认所需的对象,作为添加的库 Microsoft.Extensions.Caching.StackExchangeRedis 的一部分,用于配置 DI 容器时管理缓存。
我们有两个主要的配置:
-
options.Configuration:这是提供连接 Redis 服务器地址的地方
-
options.InstanceName:这是一个可选参数,用于定义缓存键的前缀
应用程序的基础配置已经完成,现在是时候实现一个将与 Redis 交互的控制器了。
在控制器类中处理缓存
要做到这一点,仍然在 Visual Studio Code 中,按照以下步骤创建控制器:
-
如果不存在,请在项目的根目录下创建一个名为 Controllers 的文件夹。
-
在 Controller 文件夹中添加一个名为 CacheController 的类
-
将之前创建的类的所有内容修改为以下代码:
using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Distributed; using System.Text.Json; using System.Text; namespace DistributedCacheExample.Controllers; [ApiController] [Route("api/[controller]")] public class CacheController : ControllerBase { private readonly IDistributedCache _cache; public CacheController(IDistributedCache cache) { _cache = cache; } [HttpGet("{key}")] public async Task<IActionResult> Get(string key) { var cachedData = await _cache .GetStringAsync(key); if (string.IsNullOrEmpty(cachedData)) { return NotFound(); } var data = JsonSerializer .Deserialize<MyData>(cachedData); return Ok(data); } [HttpPost] public async Task<IActionResult> Post([FromBody] MyData data) { var cacheKey = data.Key; var serializedData = JsonSerializer .Serialize(data); var options = new DistributedCacheEntryOptions() .SetSlidingExpiration(TimeSpan .FromMinutes(5)) .SetAbsoluteExpiration(TimeSpan .FromHours(1)); await _cache.SetStringAsync(cacheKey, serializedData, options); return CreatedAtAction(nameof(Get), new { key = cacheKey }, data); } } public class MyData { public string Key { get; set; } public string Value { get; set; } }前面的代码创建了一个名为 Cache 的 API,包含 GET 和 POST 方法。让我们更详细地分析代码中的重要点:
-
Microsoft.Extensions.Caching.Distributed:这是一个命名空间,引用了包含处理 CacheController 类中缓存所需依赖项的 NuGet 包。
-
private readonly IDistributedCache _cache:这是类的一个私有属性,它抽象了一个缓存处理对象。
-
public CacheController(IDistributedCache cache):作为一个依赖项,类的构造函数具有 IDistributedCache 接口,该接口将由 DI 注入,并将实例分配给类的 _cache 属性。
-
var cachedData = await _cache.GetStringAsync(key):在执行 Get 方法期间,抽象连接到 Redis 服务器的 _cache 对象将使用键搜索字符串并在请求中返回它;否则,它将返回 NotFound() 状态。
-
Post 方法:Post 方法接收一个 MyData 类型的对象作为参数,该类在文件末尾创建。在获取 MyData 对象时,将使用 Key 属性作为缓存键,var cacheKey = data.Key。然后,将 MyData 对象序列化为 JSON,JsonSerializer.Serialize(data)。随后,创建一个 DistributedCacheEntryOptions 类型的对象,在其中指定缓存中信息的过期参数。最后,通过运行 await_cache.SetStringAsync(cacheKey, serializedData, options) 将缓存持久化到 Redis 中。
-
SetSlidingExpiration 和 SetAbsoluteExpiration
在 DistributedCacheEntryOptions 中使用 .SetSlidingExpiration(TimeSpan.FromMinutes(5)) 和 .SetAbsoluteExpiration(TimeSpan.FromHours(1)) 方法来配置缓存条目过期选项。这些方法有助于管理缓存数据应在缓存中保留多长时间。
SlidingExpiration 指定了缓存条目在从缓存中移除之前可以不活跃(未访问)的时间量。每次访问缓存条目时,过期时间都会重置。
AbsoluteExpiration 指定了缓存条目在缓存中应保留的最大时间,无论其访问频率如何。缓存条目将在指定时间过后从缓存中移除,无论其被访问了多少次。
应用程序开发完成后,我们必须创建一个 Redis 服务器,为此,我们将使用 Docker 来运行它:
-
在应用程序目录中,打开终端并运行以下命令:
docker run --name redis -d -p 6379:6379 redis如果这是您第一次在您的机器上运行 Redis,请等待它下载,然后服务器将启动。
-
仍然在应用程序终端中,使用以下命令来运行它:
dotnet run -
应用程序运行后,打开 Postman 并通过访问 File | New Tab 菜单创建一个新的请求。
-
然后,将请求类型定义为 GET,并在 URL 字段中输入执行应用程序后终端提供的带有后缀 /api/Cache/DataInCache 的 URL。图 7.1 .1 展示了请求配置的一个示例:

图 7.1 – 在 Postman 上配置 API 请求
API URL 端口
在 图 7.1 .1 中显示的 URL 中添加的数字 5277 代表 API 执行端口。此值可能因环境而异。请确保在执行 docker run 命令后输入您终端中可用的执行端口。
- DataInCache 值表示我们想要从缓存中获取值的键。然而,当在 Postman 中点击 发送 按钮时,我们会得到以下返回(图 7.2):

图 7.2 – 缓存中请求数据
如 图 7.2 所示,响应体中的 HTTP 状态和 JSON 返回表示 404 未找到状态。
API 返回是正确的,因为 GET 方法试图从缓存中获取值,如果没有找到,则返回 HTTP 状态 404。
-
仍然在 Postman 中,打开一个新标签页(文件 | 新建标签页),将请求类型设置为 POST,并使用以下后缀定义 API URL:/api/Cache。
-
然后,点击 正文,选择 raw 选项,并添加以下 JSON:
{ "key": "DataInCache", "value": "Value in cache" }整个请求的配置在 图 7.3 中演示:

图 7.3 – 发起请求的配置
POST 请求,如 图 7.3 所示,将调用 API 的 POST 方法,该方法将请求体中定义的值添加到 Redis 缓存中。
-
点击 发送 按钮发起请求,你应该会收到 HTTP 201 状态码作为回应,表示信息已在缓存中创建。
-
如果你想确认缓存的值,在 Postman 中打开包含 GET 请求的先前标签页,你应该会收到 HTTP 200 状态码作为回应,以及表示缓存数据的 JSON 对象。
另一种检查 Redis 缓存中可用值的方法是使用 UI 工具,如 技术要求 部分中提到的 Redis Insight,我们现在将进行配置。
配置 Redis Insight
按照以下步骤配置 Redis Insight 以连接到在 Docker 上运行的 Redis 服务器:
- 在应用程序的主屏幕上,点击 手动添加连接详情 选项,如图 图 7.4 所示:

图 7.4 – 配置 Redis 连接
-
在下一个屏幕上,我们必须将连接参数添加到 Redis 服务器。由于此服务器通过 Docker 运行,默认参数将被使用,已在屏幕上可用:
-
主机:这定义了 Redis 服务器的地址。
-
端口:这定义了服务器执行端口。
-
其他参数:目前不重要。然而,在生产环境中,主机地址、端口、用户和密码可能不同且是必要的。
-
-
对于我们的示例,只需保留默认值并点击 + 添加 Redis 数据库 按钮。
一旦连接创建成功,连接到 Redis Insights 的服务器列表将显示,如图 图 7.5 所示:

图 7.5 – Redis Insight 工具中的连接 Redis 缓存
- 点击显示在连接列表中的创建的连接。然后,点击如图 7.6 所示的放大镜图标,查看缓存中的数据:

图 7.6 – 缓存中的数据列表
如果点击放大镜图标时无法查看任何信息,这意味着之前添加的键已过期。在这种情况下,只需再次进行 POST 请求添加另一个键,并在 Redis Insight 中查看它。
这是一个简单的示例,目的是让我们学习如何与缓存通信并向内存中添加信息。在这种情况下,我们使用 Redis,一个强大的分布式数据管理资源,作为将保存在内存中的信息的服务器。
在实际场景中,这种方法可以与数据库结合使用。这样,在向数据库发送请求之前,会检查缓存中是否存在信息。如果存在,则不需要调用数据库,从而优化过程。
正如我们所学的,缓存是一种强大的解决方案,可以使我们的应用程序性能更优、更可用。
现在我们已经学会了如何快速从缓存服务器检索信息,我们将了解如何在下一主题中使我们的应用程序更具弹性。
弹性机制
要构建健壮的应用程序,实施处理暂时性故障并确保持续可用性的弹性机制至关重要。
将弹性机制视为安全网,如果出现问题,它们会捕捉我们。它们帮助您的应用程序从意外的崩溃中恢复,并保持流畅的用户体验。
最常见的弹性策略如下:
-
重试模式:在放弃之前,自动重试指定次数的失败操作。这对于处理暂时性故障很有用。
-
断路器模式:防止应用程序执行可能失败的操作。当检测到故障时,它停止对服务的请求流,使系统有机会恢复。
为了在我们的应用程序中实现这些模式,我们将使用一个名为 Polly 的库。
Polly
Polly 是一个库,它是 .NET 基金会的一部分,用于向应用程序添加各种弹性功能。它由开源社区不断更新,并在生产环境中的各种应用程序中使用。要了解更多关于 Polly 的信息,请访问 github.com/App-vNext/Polly 。
要在我们的应用程序中使用 Polly,我们只需通过在应用程序的项目目录中执行以下命令将其添加到项目中:
dotnet add package Polly.Core
让我们分析以下代码示例中重试策略的实现:
var retryPolicy = Policy.Handle<Exception>().RetryAsync(3);
public async Task<IActionResult> GetDataWithRetryAsync()
{
return await retryPolicy.ExecuteAsync(async () =>
{
var data = await _dataService.GetDataAsync();
return Ok(data);
});
}
如前述代码所示,实现过程相当简单,并集成了 ASP.NET Core 9 开发模型。在这个例子中,目标是以弹性的方式从服务中获取数据。让我们分析实现的主要点:
-
var retryPolicy = Policy.Handle
().RetryAsync(3) 旨在创建一个重试策略。在这种情况下,策略与异常相关。在执行过程中,如果检测到异常,请求将再次进行。试验被配置为最多运行三次。 -
return await retryPolicy.ExecuteAsync命令是一个使用先前配置的重试策略执行动作的方法。所有执行GetDataAsync方法请求的代码都定义在自动管理重试机制的策略范围内。
在使用外部 API 时,使用重试策略是非常常见的。可能会有间歇性或暂时不可用的情况,在这种情况下,重试可以帮助在暂时不可用的情况下保证更大的弹性。
让我们看看实现断路器策略的一个示例:
var circuitBreakerPolicy = Policy.Handle<Exception>()
.CircuitBreakerAsync(
3, // Number of consecutive faults before breaking the circuit
TimeSpan.FromMinutes(1) // Duration of the circuit break
);
public async Task<IActionResult>
GetDataWithCircuitBreakerAsync()
{
return await circuitBreakerPolicy
.ExecuteAsync(async () =>
{
var data = await _dataService.GetDataAsync();
return Ok(data);
});
}
让我们看看前面代码的细节:
-
CircuitBreakerAsync:此方法创建一个异步断路器策略。
-
断路器在连续三次异常(故障)后将会打开(断开电路)。
-
TimeSpan.FromMinutes(1):一旦电路打开,它将保持打开状态一分钟。在此期间,任何尝试执行动作的行为将立即抛出BrokenCircuitException,而不执行动作。
断路器有以下状态:
-
关闭:所有调用都被允许的正常状态。
-
打开:在指定数量的连续失败后调用被阻止的状态。
-
半开:在打开期间之后,断路器允许有限数量的测试调用以验证底层问题是否已解决。如果这些调用成功,电路将返回到关闭状态。如果它们失败,电路将返回到打开状态。
-
circuitBreakerPolicy.ExecuteAsync:此方法在断路器策略的控制下执行给定的异步委托(代码块内部)。
-
如果_dataService.GetDataAsync()调用成功,该方法将返回包裹在OkObjectResult(HTTP 200 响应)中的数据。
-
如果_dataService.GetDataAsync()调用抛出异常,断路器策略将处理它:
-
如果连续发生的异常少于三次,断路器保持关闭状态,异常将被传播。
-
在连续三次异常之后,断路器打开一分钟。在此期间内的任何进一步调用将立即抛出BrokenCircuitException。
-
-
断路器策略通过在连续三次失败后中断电路并保持开启一分钟来帮助防止系统过载导致的重复失败。在此期间,任何尝试调用数据服务的操作都将立即抛出异常,而不会尝试执行服务调用。这允许系统恢复,并防止依赖系统的级联失败。
断路器和重试策略是强大的弹性策略,尽管它们看起来相似,但有不同的目标,如表 7.1 所示:
| 方面 | 断路器 | 重试 |
|---|---|---|
| 目的 | 防止过载失败的服务 | 通过重试处理瞬态故障 |
| 行为 | 在一定数量的失败后停止请求并开启电路一段时间 | 在重试之间有延迟的情况下重试指定次数的操作 |
| 状态 | 关闭、开启、半开启 | 无状态,仅重试 |
| 失败处理 | 断路器开启后快速失败 | 在失败前多次重试 |
| 使用场景 | 需要避免重复失败以保护系统 | 预期临时故障可以通过重试解决 |
| 复杂度 | 较高,具有状态转换和监控 | 较低,具有简单的重试逻辑 |
| 用户反馈 | 断路器开启时的即时失败反馈 | 所有重试失败后的延迟反馈 |
表 7.1 – 断路器和重试目标
在实践中,这些模式通常一起使用,以提供强大的故障处理机制。例如,您可以使用重试策略来处理瞬态故障,如果尝试持续失败,则可以使用断路器来防止重试并允许系统恢复。
Polly 提供了其他几种弹性机制,可以组合使用,使应用程序更加强大。
这些策略的使用极大地促进了创建为大规模执行模型准备的强大解决方案,尤其是在云环境中。
在任何情况下,即使添加了多个机制来避免失败,它们仍然可能发生。在这种情况下,我们必须能够获取足够的信息来做出纠正,并确保应用程序不受非一致性影响。为此,在应用程序中添加日志非常重要,ASP.NET Core 9 提供了强大的机制来实现这一点,我们将在下一节中了解。
理解和实现记录和监控
在优化性能和弹性之后,我们现在转向记录和监控,这是维护和调试 ASP.NET Core 9 应用程序的基本实践。
记录和监控简介
记录和监控对于理解应用程序的行为、诊断问题和确保其平稳运行至关重要。日志提供了对应用程序过程的可见性,并有助于早期检测异常。
将日志记录想象成记日记,将监控想象成在家安装监控摄像头。日记帮助您记住过去的事件,而摄像头让您能够实时看到正在发生的事情,保持安全和秩序。
使用 ILogger 进行日志记录
.NET 提供了抽象,允许 ASP.NET Core 9 应用程序处理不同的日志策略。由 Microsoft.Extensions.Logging 命名空间提供的 ILogger 和 ILoggerFactory 接口对于在您的应用程序中实现日志记录至关重要,它允许您捕获和记录有关应用程序操作的信息。
日志记录提供了对应用程序行为的洞察,对于调试和监控至关重要。
ASP.NET Core 中的 ILogger 接口允许您以以下各点所述的详细程度记录信息:
-
Trace : 详细信息,通常仅在诊断问题时才有兴趣。
-
Debug : 对调试应用程序有用的信息。
-
Information : 强调应用程序进度的信息性消息。
-
Warning : 可能有害但不是错误的情况。
-
Error : 阻止应用程序执行功能的错误。
-
Critical : 导致应用程序完全失败的严重错误。
ILogger 接口提供了一些有用的方法:
-
Log methods :
- Log
(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) : 记录消息的核心方法。它允许您指定日志级别、事件 ID、状态、异常和一个格式化函数。
- Log
-
Convenience methods :
-
LogTrace(string message, params object[] args) : 记录一个跟踪消息。
-
LogDebug(string message, params object[] args) : 记录一个调试消息。
-
LogInformation(string message, params object[] args) : 记录一个信息消息。
-
LogWarning(string message, params object[] args) : 记录一个警告消息。
-
LogError(string message, params object[] args) : 记录一个错误消息。
-
LogCritical(string message, params object[] args) : 记录一个严重错误消息。
-
-
Scope Method :
- BeginScope
(TState state) : 此方法开始一个逻辑操作范围。它返回一个 IDisposable 接口,在处置时结束范围。范围对于将一组操作与一个共同上下文相关联非常有用。
- BeginScope
以下是一个使用 ILogger 接口的示例:
public class MyService
{
private readonly ILogger<MyService> _logger;
public MyService(ILogger<MyService> logger)
{
_logger = logger;
}
public void DoWork()
{
_logger.LogInformation("Starting work.");
try
{
// Perform some work here
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while doing work.");
}
_logger.LogInformation("Finished work.");
}
}
上一段代码使用了 ILogger 接口提供的几个方法:
-
ILogger
_logger : 这声明了一个私有 readonly 字段,_logger,其类型为 ILogger。ILogger 接口是 .NET 日志记录基础设施的一部分,其中 T 是正在记录的类型。通过指定 MyService,记录器与这个类相关联,这有助于识别日志消息的来源。 -
MyService(ILogger
logger) :ILogger实例通常通过依赖注入提供。这允许集中配置和管理日志基础设施。 -
_logger.LogInformation("Starting work."):记录信息类型,字符串为 Starting work。
-
_logger.LogError(ex, "An error occurred while doing work."):记录信息类型,字符串为 An error occurred while doing work。
ILogger 接口提供了一种强大的抽象方式,以技术无关的方式记录应用程序的执行数据,从而便于维护和扩展。
.NET 中可用的另一个强大抽象机制是 ILoggerFactory。
ILoggerFactory 接口负责创建 ILogger 实例。它通常用于为特定类别创建日志记录器或配置日志提供者和设置。
主要方法如下:
-
CreateLogger(string categoryname):为指定的类别创建一个 ILogger 实例。类别通常是日志记录器关联的类或组件的名称。
-
AddProvider (ILoggerProvider provider):将 ILoggerProvider 添加到工厂中。此方法用于配置日志消息的发送位置和方式,例如发送到控制台、文件或远程日志服务。
-
To discard():丢弃日志工厂及其创建的所有日志记录器。通常用于释放日志提供者持有的任何资源。
我们可以使用 ILoggerFactory 接口,如下面的代码示例所示:
public class MyService
{
private readonly ILogger _logger;
public MyService(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<MyService>();
}
public void DoWork()
{
_logger.LogInformation("Starting work.");
try
{
// Perform some work here
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while doing work.");
}
_logger.LogInformation("Finished work.");
}
}
使用 ILoggerFactory 接口和 ILogger 接口之间的主要区别是创建一个新类别的日志,该日志将用于分组应用程序的日志消息。构造函数通过依赖注入接收一个 ILoggerFactory 实例,然后为 MyService 类创建一个 ILogger 实例。在这种情况下,此类的所有日志消息都将按 MyService 类别分组。
ILoggerFactory 允许集中配置日志设置。这意味着您可以在一个地方设置日志提供者、过滤器和其他设置,通常在应用程序启动期间,并将这些配置应用于工厂创建的所有日志记录器。您还可以动态地为应用程序中的不同类别或组件创建日志记录器。这对于将日志消息与特定应用程序部分关联起来非常有用,使得过滤和分析日志更加容易。
日志与提供者一起工作,提供者是日志将可用的不同来源。应用程序可能包含针对每个目的的不同类型的提供者。
每个提供者在应用程序启动期间集中配置。这样,ILogger 和 ILoggerFactory 抽象将使用配置的提供者提交日志。
在 ASP.NET Core 9 中可以使用几种类型的提供程序,例如用于写入控制台、添加调试信息的提供程序,甚至用于将日志写入外部服务(如 Azure Application Insights 和 Elasticsearch)等提供程序。
让我们看看以下代码中关于日志设置的示例,这段代码来自Program.cs类:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Configure logging
builder.Logging.ClearProviders();
// Optional: clear default providers
builder.Logging.AddConsole(); // Add console logging
builder.Logging.AddDebug(); // Add debug logging
builder.Logging.AddEventSourceLogger();
// Add event source logging
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
app.Run();
如前述代码示例所示,添加了不同的日志提供程序。这意味着当使用类似于_logger.LogInformation、_logger.LogError的方法时,这些日志将被分发到配置的提供程序。
日志是一种强大的工具,在任何应用程序中都是必不可少的,它有助于检测故障甚至优化系统。
在第八章中,我们将探讨与中间件结合使用日志的方法。
摘要
在本章中,我们学习了在 ASP.NET Core 9 中实施最佳实践,正确处理 HTTP 请求,增强响应的可理解性,以及理解使用缓存以提高应用程序性能和弹性的方法。我们还了解了与异步请求相关的概念,以及我们如何通过使用async和await关键字在 ASP.NET Core 应用程序中采用这种方法。最后,我们学习了使用应用程序监控的重要性,利用内部机制抽象编写和日志,例如使用ILogger和ILoggerFactory类,使我们能够获得足够的输入来修复应用程序中的不一致性并优化它们。
在下一章中,我们将学习如何通过使用中间件来扩展 ASP.NET Core 9 应用程序的请求管道。
第八章:在 ASP.NET Core 9 中使用中间件增强应用程序
ASP.NET Core 9 提供了一个强大且灵活的框架,旨在处理高需求网络应用程序。该框架的关键组件是中间件,它允许开发者直接与请求和响应管道交互。理解和利用中间件可以显著增强应用程序的功能。本章将深入探讨中间件,包括其结构、实现和实际应用,如全局错误处理、请求限制等。
在本章中,我们将重点关注以下主题:
-
了解中间件管道
-
实现自定义中间件
-
使用基于工厂的中间件
-
使用中间件为应用程序添加功能
-
为中间件注册创建扩展方法
在本章中,我们将探讨使用 ASP.NET Core 9 开发应用程序的基本最佳实践,包括正确使用异步机制、HTTP 请求以及通过日志进行应用程序仪表化。
技术要求
为了支持本章的学习,以下工具必须存在于您的开发环境中:
-
Docker:必须在您的操作系统上安装 Docker 引擎,并运行一个 SQL Server 容器。您可以在 第五章 中找到有关 Docker 和 SQL Server 容器的更多详细信息。
-
Postman:此工具将用于执行对开发应用程序 API 的请求。
-
Redis Insight:此工具用于连接到 Redis 服务器数据库(
redis.io/insight/)。
本章中使用的代码示例可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/ASP.NET-Core-9.0-Essentials/tree/main/Chapter08
了解中间件管道
在前面的章节中,我们使用了 ASP.NET Core 9 的几个功能,包括中间件。
中间件是在 ASP.NET Core 9 网络应用程序执行流程中用于处理请求和响应的管道模型,本书中开发的应用程序已经使用了一些来自 .NET 平台的标准中间件,例如 身份验证、授权、跨源资源共享(CORS)等。
ASP.NET Core 请求管道由一系列请求委托组成,一个接一个地调用。图 8 .1 展示了这一概念:

图 8.1 – ASP.NET Core 9 中间件管道
使用 Run、Map 和 Use 扩展方法配置请求委托,这些方法通常在 Program.cs 文件中配置。
每个扩展方法都有一个用于注册请求委托的模板:
-
运行:使用 app.Run 方法定义内联中间件,该中间件处理请求并完成响应,如下面的示例代码所示,它实现了内联中间件:
app.Run(async context => { await context.Response.WriteAsync("Hello Inline middleware!"); }); -
映射:使用 app.Map 方法在中间件管道中创建一个分支。在以下代码中,对 /SomeRoute 的请求由这个中间件分支处理。分支中的中间件将一条消息写入响应:
app.Map("/SomeRoute", someRouteApp => { someRouteApp.Use(async (context, next) => { Console.WriteLine("In SomeRoute middleware"); await context.Response.WriteAsync("Hello from the SomeRoute middleware!"); }); }); -
使用:使用 app.Use 方法将中间件添加到管道中。以下代码使用中间件在调用管道中的下一个中间件之前记录请求方法和路径。在下一个中间件完成后,它记录响应状态码:
app.Use(async (context, next) => { // Log the request Console.WriteLine($"Request: {context.Request.Method} {context.Request.Path}"); await next.Invoke(); // Log the response Console.WriteLine($"Response: {context.Response.StatusCode}"); });
中间件的使用为应用程序带来持续的好处;我们将更详细地了解不同方法的使用,例如创建中间件类。
现在,让我们了解中间件执行流程是如何工作的。
理解中间件流程
当应用程序收到请求时,它会按照注册的顺序通过每个中间件组件,并可以执行以下操作:
-
处理请求并将其传递给下一个中间件:这就像接力赛跑,每个运动员将接力棒传给下一位。每个中间件完成其部分工作后,就调用下一个中间件以继续处理请求。
-
处理请求并中断链,防止其他中间件运行:想象一下机场的安全检查站。如果安全人员发现问题,他们可能会让你停下来进行额外检查,阻止你继续前进。同样,中间件可能会决定完全处理请求并停止进一步的处理。
-
在链中向上移动时处理响应:这就像将包裹通过多个检查阶段。一旦包裹到达最终阶段,它将在返回的每个阶段再次进行检查,确保在交付之前一切正常。
分层方法允许强大且灵活地处理 HTTP 请求和响应。中间件可用于各种任务,例如日志记录、身份验证、错误处理等。
此外,注册中间件的顺序至关重要,因为它定义了请求和响应管道的流程。我们可以在 图 8.2 中看到中间件执行流程的表示:

图 8.2 – 中间件执行流程
让我们详细看看流程是如何工作的:
-
请求到达:当请求到达服务器时,它进入管道并到达第一个中间件组件
-
中间件执行:每个中间件可以执行以下操作:
-
修改请求:中间件可以更改请求的某些方面,例如添加头或更改请求路径
-
移动到下一个中间件:处理完毕后,中间件可以使用
await next调用管道中的下一个中间件,我们将在实现自定义 中间件部分进行讨论
-
-
短路管道:中间件可能决定不调用下一个中间件,从而有效地提前结束请求处理
-
响应处理:当请求达到管道的末端时,响应将按相反的顺序通过中间件组件返回
-
修改响应:中间件可以更改响应,例如添加头信息,更改状态码或记录信息
中间件顺序
ASP.NET Core 9 默认提供了一些中间件来处理请求和响应。然而,这些中间件的插入顺序完全改变了应用程序的执行流程,在某些情况下甚至可能导致故障。例如,在中间件授权之前添加中间件身份验证是很重要的;否则,未经认证如何验证授权?
在任何情况下,除了标准中间件外,还有自定义中间件的执行顺序。
要了解更多关于中间件顺序的信息,请参阅以下链接:learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-9.0#middleware-order。
通过对中间件执行流程的工作,我们能够为我们的应用程序添加几个强大的可能性,我们将在下一节中学习更多的好处。
中间件的好处和最佳实践
中间件在 ASP.NET Core 9 应用程序中扮演着关键角色,提供了一系列有助于提高应用程序的健壮性、可维护性和可扩展性的好处。了解这些好处可以使您有效地利用这一资源,因此让我们更详细地看看这一点:
-
模块化:模块化意味着中间件是一个独立的函数单元,可以轻松添加、删除或替换,而不会影响应用程序的其他部分。这种模块化允许开发者创建可重用的中间件组件,这些组件可以在不同的项目或同一项目的不同部分之间共享。
-
组合:中间件可以以多种顺序组合,以实现不同的行为。这种组合特性允许您根据应用程序的具体需求定制请求和响应管道。
假设您有三个中间件组件:一个用于日志记录,一个用于身份验证,一个用于处理错误。您可以按照所需的顺序组合这些中间件组件:
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.UseMiddleware<ErrorHandlingMiddleware>(); app.UseMiddleware<AuthenticationMiddleware>(); app.UseMiddleware<RequestLoggingMiddleware>(); app.Run(async context => { await context.Response.WriteAsync("Hello, World!"); }); app.Run();如您在前面的代码中所见,
app.UseMiddleware方法向应用程序添加了处理错误、身份验证和日志记录的中间件。app.Run方法仅创建一个标准的请求响应,返回一个Hello World消息。考虑以下因素很重要:
-
如果你想重新排列这些中间件组件的顺序,请求的处理方式和错误处理将会不同。
-
关注点分离(SoC):中间件允许不同关注点之间的清晰分离,使得在管道中定义清晰的执行上下文成为可能,并促进代码库的更清晰、可扩展和可维护。
-
可扩展性:你可以开发自定义中间件来扩展应用程序的功能——例如,通过向请求添加验证功能或在应用程序中全局修改响应。
假设你需要自定义中间件来验证请求头中的 API 密钥。你可以按照以下方式创建此中间件:
public class ApiKeyCheckMiddleware { private readonly RequestDelegate _next; private const string API_KEY = "X-API-KEY"; private const string VALID_API_KEY = "XYZ123"; public ApiKeyCheckMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { if (!context.Request.Headers .TryGetValue(API_KEY, out var extractedApiKey) || extractedApiKey != VALID_API_KEY) { context.Response.StatusCode = 401; await context.Response .WriteAsync("Unauthorized"); return; } await _next(context); } }上述代码旨在创建一个自定义中间件,该中间件检查必须包含在请求头中的 API 密钥的存在,其中头键是 X-API-KEY,期望的值正好是 XYZ123。
在执行验证时,如果头和值不是请求的一部分,则用户会收到一个带有 HTTP 状态码 401 的未授权返回消息。
-
事实上,中间件是一个强大的功能,它允许你更好地控制使用 ASP.NET Core 9 开发的应用程序中的请求和响应流程。
不要担心与前面代码示例相关的细节。我们将在 实现自定义 中间件 部分学习中间件类的结构。
尽管使用中间件的应用程序具有很大的好处,但了解良好的实践很重要;否则,可能成为好处的东西可能会变成一个主要问题。
让我们看看一些最佳实践:
-
顺序很重要:中间件组件添加的顺序至关重要,因为它会影响请求和响应的处理方式。
-
保持简单:中间件应该只做一件事,并且做好。在中间件中应避免复杂的逻辑。
-
错误处理:确保你的中间件组件以与其他应用程序中的类相同的方式处理异常和错误。
-
性能:注意中间件对性能的影响,尤其是在高负载场景中。由于它在请求和响应过程中操作,因此在这些阶段避免大量处理,以避免给用户和应用程序造成问题。
-
重用现有中间件:尽可能使用内置中间件以减少对自定义实现的依赖。正如我们已经学到的,ASP.NET Core 9 中有一些可用的中间件。
现在我们已经了解了中间件的原则、好处和最佳实践,让我们实现我们的第一个自定义中间件,并了解这种方法的具体细节。
实现自定义中间件
自定义中间件允许你封装功能并在应用程序的不同部分重用它。
在 ASP.NET Core 9 中创建自定义中间件涉及几个步骤,例如以下内容:
-
中间件类定义
-
Invoke或InvokeAsync方法的实现
-
请求管道中的中间件注册
让我们分析以下代码,它代表一个自定义中间件:
public class BeforeAfterRequestMiddleware
{
private readonly RequestDelegate _next;
public BeforeAfterRequestMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// Logging request information
Console.WriteLine($"Request:
{context.Request.Method}
{context.Request.Path}");
// Call the next middleware in the pipeline
await _next(context);
// Logging response information
Console.WriteLine($"Response:
{context.Response.StatusCode}");
}
}
这段自定义中间件代码的目的是在请求的开始和响应中向控制台写入一个字符串。
然而,理解前面代码的结构是很重要的:
-
RequestDelegate:这是一个代表管道中下一个中间件的委托。这个委托存储在一个名为_next的字段中,用于类的作用域。
-
构造函数:类构造函数接收一个RequestDelegate类的实例作为参数,代表执行流程中的下一个中间件。
-
Invoke或InvokeAsync方法:包含处理 HTTP 请求的逻辑。这两种方法之间的区别在于一个是异步执行的,另一个不是。InvokeAsync方法接收一个HttpContext对象作为参数。HttpContext对象允许你访问请求和响应信息。使用InvokeAsync方法来提高性能和可伸缩性是一种良好的实践。
-
await _next(context):执行_next委托,该委托接收HttpContext对象作为参数。在这个例子中,我们只是在传播下一个中间件的执行之前,在请求信息中写入一个字符串,然后在执行中间件之后,再写入一个包含响应信息的字符串。
中间件中的依赖注入(DI)
自定义中间件类必须使用显式依赖原则(EDP),正如我们在前面的章节中已经学到的,其中类的依赖关系在构造函数中定义。
由于中间件是在应用程序初始化期间构建的,因此无法像在Controller类中的每个请求那样注入添加到作用域生命周期的服务。
因此,如果你想在中间件类中使用 DI 控制中可用的任何服务,请将这些服务添加到InvokeAsync方法的签名中,该方法可以接受由 DI 解析的附加参数。
之前的代码示例,虽然简单,但展示了中间件的基本结构,需要包含一个RequestDelegate字段,一个依赖于RequestDelegate实例的构造函数,以及Invoke或InvokeAsync方法的实现。
为了在应用程序中使用定制中间件,必须通过Program.cs文件将其注册到 ASP.NET Core 9 执行管道中:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseMiddleware<BeforeAfterRequestMiddleware>();
app.Run(async context =>
{
await context.Response.WriteAsync("Hello, World!");
});
app.Run();
之前的代码已被缩短,以便更容易阅读和学习。为了注册中间件,使用UseMiddleware扩展方法,这是一个泛型方法,其中我们定义之前执行的定制中间件作为类型。
在应用程序启动流程中,所有自定义或非自定义中间件都被创建,形成应用程序生命周期的一部分,而不是像作用域服务那样按请求创建。这种行为防止将其他依赖项添加到自定义中间件类的构造函数中,但允许通过 Invoke 和 InvokeAsync 方法添加带有参数的依赖项。
在中间件中获取 HTTP 上下文对象
作为在 InvokeAsync 方法中使用 DI 的替代方案,可以使用 context.RequestService 属性(learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestservices?view=aspnetcore-9.0),如下面的代码所示:
public async Task InvokeAsync(HttpContext context)
{
var logger = context.RequestServices
. GetRequiredService<Ilogger
< BeforeAfterRequestMiddleware >>();
logger.LogInformation($"Request:{context.Request.Method}
{ context.Request.Path}");
await _next(context);
logger.LogInformation($"Response:
{ context.Response.StatusCode}");
}
然而,这多少降低了代码中依赖项的可见性。
然而,ASP.NET Core 9 提供了一种方法,通过基于工厂的中间件在每次请求的基础上启用自定义中间件的用法,我们将在下一节中讨论。
与基于工厂的中间件一起工作
创建自定义中间的另一种方式是使用基于工厂的方法,它通过 DI 提供了更好的性能和灵活性。
这种方法在中间件需要作用域服务时特别有用。
基于工厂的中间件使用 IMiddleware 接口,这使得中间件可以通过 DI 容器(DIC)被激活。
IMiddleware 接口只有一个必须由类实现的 InvokeAsync 方法。使用基于工厂的方法的自定义中间件的结构与上一节中学到的传统方法非常相似。
让我们看看一个代码示例:
public class RequestLimitingMiddleware : IMiddleware
{
private readonly ILogger
<RequestLimitingMiddleware> _logger;
public RequestLimitingMiddleware
(ILogger<RequestLimitingMiddleware> logger)
{
_logger = logger;
}
public async Task InvokeAsync(HttpContext context,
RequestDelegate next)
{
// Logic to limit the number of requests
_logger.LogInformation("Processing request");
await next(context);
}
}
与基于工厂的方法相比,最大的不同在于在类构造函数中定义依赖项,而不是在 InvokeAsync 方法中声明它。在先前的代码示例中,构造函数依赖于一个 ILogger 接口。
InvokeAsync 方法必须只有两个参数,HttpContext 和 RequestDelegate 。
现在,让我们通过分析 Program.cs 类代码,看看如何使用基于工厂的方法进行自定义中间件注册:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<RequestLimitingMiddleware>();
var app = builder.Build();
app.UseMiddleware<RequestLimitingMiddleware>();
app.Run(async context =>
{
await context.Response.WriteAsync("Hello, World!");
});
app.Run();
中间件注册是通过我们已学过的app.UseMiddleware方法完成的。然而,请注意,RequestLimitingMiddleware类是通过builder.Services.AddScoped
服务生命周期
ASP.NET Core 9 提供了不同类型的服务生命周期;你可以在learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection#service-lifetimes了解更多信息。
如我们所见,基于工厂的方法允许我们使用 DIC 资源并通过请求来管理中间件的生命周期。
是否使用基于工厂的方法或传统方法,当然取决于应用需求。然而,两者都是强大的解决方案,为我们的应用增添了宝贵的功能。
在下一节中,我们将创建并使用许多应用中常用的中间件。
使用中间件为应用添加功能
现在我们已经了解了中间件的功能和可能性,我们将着手实现一些功能,这将使我们的 Web 应用质量得到提升。
创建中间件类没有严格的目录结构或命名空间标准。然而,将类组织到定义良好的命名空间中是一种良好的实践。
对于这个例子,我们将默认在应用项目的根目录下创建一个名为Middlewares的文件夹。
在本节中,我们将关注以下中间件:
-
全局错误处理
-
添加请求日志 - 记录请求信息
-
速率限制 - 在你的应用中定义请求限制
包含前面提到的类的项目可在书籍仓库中找到,其链接可在技术要求部分找到。
让我们创建一个新的应用。打开终端,在你的选择文件夹中,使用以下命令创建一个项目:
dotnet new mvc -n CommonMiddlewares
然后,在目录的根目录下创建一个名为Middlewares的文件夹。
现在,我们将开始创建第一个中间件:全局错误处理。
全局错误处理
在应用的执行流程中,可能会出现错误或异常,如果处理不当,可能会给用户带来不便。
在这种情况下,我们必须在代码中处理错误,以防止异常导致应用出现故障。
为了实现这一点,一种良好的实践是使用全局错误处理中间件,它使得能够以集中化的方式管理应用异常流,甚至可以通过在不同的监控工具中添加日志来扩展其功能,这对于错误修正至关重要。
在你之前创建的Middlewares文件夹中创建一个名为ErrorHandlingMiddleware.cs的文件,并添加以下代码:
public class ErrorHandlingMiddleware
{
private readonly RequestDelegate _next;
public ErrorHandlingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private Task HandleExceptionAsync(HttpContext context,
Exception exception)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)HttpStatusCode
.InternalServerError;
return context.Response.WriteAsync(new ErrorDetails()
{
StatusCode = context.Response.StatusCode,
Message = "Internal Server Error from the custom middleware."
}.ToString());
}
}
public class ErrorDetails
{
public int StatusCode { get; set; }
public string Message { get; set; }
public override string ToString()
{
return JsonSerializer.Serialize(this);
}
}
我们可以在前面的代码中看到中间件的常见结构。这个全局错误处理中间件的功能强大之处在于它在 InvokeAsync 方法的主体中使用了 try / catch 块。
await _next(context) 命令在 try 块中执行,因此如果应用程序中发生异常,它将被全局处理。异常处理是通过在 catch 块中调用的 HandleExceptionAsync 方法完成的。
HandleExceptionAsync 方法通过将请求响应的 StatusCode 属性更改为内部服务器错误,即 HTTP 状态码 500,并在请求体中返回一个对象来修改请求响应。此对象由 ErrorDetails 类表示,该类具有 StatusCode 和 Message 属性。
因此,除了保证处理应用程序中的任何异常外,还有一个自定义但通用的返回值,可以适当用于 UI 处理,从而为开发者和应用程序用户提供更好的体验。
ASP.NET Core 9 中的问题详情
ASP.NET Core 9 提供了对问题详情的内建支持,这是一个基于 RFC 7807(https://datatracker.ietf.org/doc/html/rfc7807)的标准化错误响应格式。通过将跟踪 ID 包含在响应中,开发者可以增强调试和错误跟踪。
带有跟踪 ID 的问题详情响应看起来像这样:
{
" type": "https://example.com/probs/server-error",
"title": "An unexpected error occurred.",
" status": 500,
"detail": "The system encountered an issue.",
" instance": "/example-path",
" traceId": "00-abcdef1234567890abcdef1234567890-1234567890abcdef-01"
}
ProblemDetails 类可以与以下实现示例中的中间件一起使用,更改 ErrorHandlingMiddleware 类的 HandleExceptionAsync 方法:
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
var traceId = Activity.Current?.Id ?? context.TraceIdentifier;
var problemDetails = new ProblemDetails
{
Type = " server-error",
Title = "An unexpected error occurred.",
Status = StatusCodes.Status500InternalServerError,
Detail = “来自自定义中间件的内部服务器错误。”,
Instance = context.Request.Path
};
// 在问题详情中包含跟踪 ID。
problemDetails.Extensions["traceId"] = traceId;
context.Response.ContentType = " application/problem+json";
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
return context.Response.WriteAsJsonAsync(problemDetails);
}
前面的代码是通过中间件实现的全球错误处理器的自定义。除了使用 ProblemDetails 类外,还添加了代码以获取跟踪 ID 值:
var traceId = Activity.Current?.Id ?? context.TraceIdentifier;
然后,跟踪 ID 被添加到 ProblemDetails 对象的扩展中:
problemDetails.Extensions["traceId"] = traceId;
跟踪 ID 是极好的信息,应该成为应用程序日志的一部分,便于关联错误响应以解决问题。
ASP.NET Core 9 还提供了错误处理的替代方案,您可以在以下网址了解更多信息:learn.microsoft.com/en-us/aspnet/core/web-api/handle-errors?view=aspnetcore-9.0。
使用全局错误处理为应用程序带来了极大的好处,此外,一些日志记录策略可以在云监控工具、终端或甚至文件中使用,从而便于问题解决。
日志功能可用于其他目的,而不仅仅是处理错误。让我们分析另一种用于记录请求的中间件方法。
添加请求日志
每个 Web 应用程序都有持续的通信和请求处理流程,这些流程生成不同类型的信息,以及必须处理的异常。我们在上一节创建全局错误处理中间件时已经考虑了这种情况。
除了处理错误和异常之外,我们还必须记录这些信息,以便能够进行有效的故障排除。然而,在许多情况下,记录应用程序请求和响应流程中处理的信息是必要的。这种方法提供了以下好处:
-
集中式日志记录:集中日志记录逻辑,确保所有请求在管道中的单个位置一致地记录。
-
请求跟踪:能够跟踪所有请求对于监控应用程序性能、调试问题和理解用户行为非常有用。
-
安全和审计:通过记录请求,您可以维护对应用程序访问的审计跟踪,这对于符合安全规范至关重要。
-
错误诊断:当出现问题时,日志可以帮助您通过提供导致错误的请求活动的详细历史记录来诊断和排除问题。
-
性能监控:记录处理请求所需的时间可以帮助识别性能瓶颈并优化应用程序性能。
-
灵活性:中间件可以被配置为仅记录特定类型的请求或响应,从而在日志记录的实现方式上提供灵活性。
让我们看看一个负责将请求数据记录到应用程序中的中间件的示例。为此,在 Middlewares 文件夹中创建一个名为 PerformanceLoggingMiddleware.cs 的类,并添加以下代码:
public class PerformanceLoggingMiddleware
{
private readonly RequestDelegate _next;
public PerformanceLoggingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context,
ILogger<PerformanceLoggingMiddleware> logger)
{
var timestamp = Stopwatch.GetTimestamp();
await _next(context);
var elapsedMilliseconds = Stopwatch
.GetElapsedTime(timestamp).TotalMilliseconds;
logger.LogInformation("Request {Method} {Path}
took {ElapsedMilliseconds} ms",
context.Request.Method, context.Request.Path,
elapsedMilliseconds);
}
}
上述代码旨在记录请求的执行时间。这是一种有趣的测量应用程序限制并允许您改进实现性能的方法。
在分析代码时,我们有以下内容:
-
依赖注入(DI):
InvokeAsync方法接受一个ILogger<PerformanceLoggingMiddleware>参数,该参数由依赖注入提供 -
日志请求指标:
InvokeAsync方法使用ILogger实例来记录 HTTP 方法、请求路径以及处理请求所需的时间 -
收集请求执行时间:在执行请求之前,使用
Stopwatch对象的GetTimestamp()静态方法获取初始时间戳在通过
_await _next(context)请求委派执行请求之后,使用Stopwatch对象的Stop方法来结束计时器。然后创建一个包含有关请求的信息的日志,例如方法、路径和从
Stopwatch类的GetElapsedTime(timestamp).TotalMilliseconds方法中获取的以毫秒为单位的执行时间。
Stopwatch 类
.NET 中的 Stopwatch 类是由 System.Diagnostics 命名空间提供的具有高分辨率的计时器。它用于以极高的精度测量经过的时间,非常适合性能测量和基准测试任务。有关 Stopwatch 可用功能的更多信息,请参阅文档:learn.microsoft.com/en-us/dotnet/api/system.diagnostics.stopwatch?view=net-9.0。
当我们实现如 PerformanceLoggingMiddleware 这样的自定义日志时,创建自定义中间件增强了我们应用程序的功能,既帮助了使用优质应用程序的用户体验,也支持了在维护过程、诊断以及应用程序演进方面的团队。
然而,ASP.NET Core 9 提供了一些中间件,能够处理应用程序执行流程的几个其他重要方面,例如速率限制,我们将在下一节中了解这些内容。
速率限制
ASP.NET Core 9 中的速率限制中间件是一个强大的功能,对于保护应用程序免受滥用、提高整体性能和可靠性至关重要。此中间件控制客户端在指定时间段内可以向服务器发送的请求数量。
在 ASP.NET Core 9 中使用速率限制中间件是通过在 Program.cs 文件中添加配置来完成的。以下是一个逐步指南:
-
添加所需的 NuGet 包。
-
在 HTTP 请求管道中注册中间件。
-
将速率限制中间件添加到管道中。速率限制中间件包含在 Microsoft.AspNetCore.RateLimiting 中。
让我们通过以下代码查看在 Razor Pages 类型应用程序中的实现示例:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
// Configure rate limiting policies
builder.Services.AddRateLimiter(options =>
{
options.AddPolicy("fixed", context =>
RateLimitPartition.GetFixedWindowLimiter(new
RateLimitPartitionKey(context.Request
.Headers["X-Forwarded-For"].ToString(),
PartitionKeyKind.ClientIP), partition =>
new FixedWindowRateLimiterOptions
{
PermitLimit = 5,
Window = TimeSpan.FromMinutes(1),
QueueProcessingOrder = QueueProcessingOrder
.OldestFirst,
QueueLimit = 2
}));
options.AddPolicy("sliding", context =>
RateLimitPartition.GetSlidingWindowLimiter(new
RateLimitPartitionKey(context.Request
.Headers["X-Forwarded-For"].ToString(),
PartitionKeyKind.ClientIP), partition =>
new SlidingWindowRateLimiterOptions
{
PermitLimit = 5,
Window = TimeSpan.FromMinutes(1),
SegmentsPerWindow = 3,
QueueProcessingOrder = QueueProcessingOrder
.OldestFirst,
QueueLimit = 2
}));
options.AddPolicy("tokenBucket", context =>
RateLimitPartition.GetTokenBucketLimiter(new
RateLimitPartitionKey(context.Request
.Headers["X-Forwarded-For"].ToString(),
PartitionKeyKind.ClientIP), partition =>
new TokenBucketRateLimiterOptions
{
TokenLimit = 10,
TokensPerPeriod = 5,
ReplenishmentPeriod = TimeSpan.FromSeconds(10),
QueueProcessingOrder = QueueProcessingOrder
.OldestFirst,
QueueLimit = 2
}));
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Use rate limiting middleware
app.UseRateLimiter();
app.MapRazorPages();
app.Run();
现在,让我们分析前面代码的重要方面:
-
builder.Services.AddRateLimiter:用于定义在此应用程序中将使用的速率限制策略。每个策略使用一个唯一的客户端标识符,添加X-Forwarded-For HTTP 头,以对每个客户端 IP 地址实施限制。在之前的代码中,我们配置了三个策略——即以下内容:
-
固定窗口:每分钟限制请求量为 5 个。一旦达到限制,将不允许进一步请求,直到窗口重置。
-
滑动窗口:类似于固定窗口策略,但将窗口划分为多个段,允许每分钟有更分散的请求余量。
-
令牌桶:允许您最多有 10 个令牌(请求)可用,每 10 秒补充 5 个新令牌。如果令牌耗尽,则入站请求将被排队。
-
-
app.UseRateLimiter():此行将速率限制中间件添加到请求管道中,使配置的速率限制策略生效。与使用UseMiddleware<>方法添加中间件的传统方式不同,速率限制有一个专用的扩展方法。我们将在下一节中学习如何创建扩展方法以添加中间件。
速率限制是开发 ASP.NET Core 9 应用程序时应考虑的重要功能,它带来了一些好处,例如以下内容:
-
过载保护:防止服务器因过多请求而过载,确保稳定的性能。
-
公平使用:确保没有客户端可以垄断服务器资源,促进所有用户的公平访问。
-
安全性:通过限制客户端请求的速率,减轻了某些类型的攻击,例如分布式拒绝服务(DDoS)攻击。
-
改进的用户体验:通过防止服务器过载,速率限制有助于保持一致的响应时间和服务可用性。
了解更多关于速率限制中间件
速率限制还具有其他一些功能,可以增强您使用此中间件的战略。请参阅文档以获取更多详细信息:learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?view=aspnetcore-9.0。
除了速率限制中间件之外,ASP.NET Core 9 还提供了其他不同类型的中间件,这些中间件在多种类型的应用程序中广泛使用,例如身份验证和授权中间件,如第六章所述。根据您的应用程序需求,您可以组合中间件的功能,以创建在现代化环境中运行的高效、高质量的解决方案。
ASP.NET Core 9 内置中间件
请参考以下 URL 的文档,分析 ASP.NET Core 9 平台上可用的不同中间件:learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-9.0#built-in–middleware。
现在我们已经创建了自定义中间件并学习了如何使用 ASP.NET Core 9 中可用的速率限制中间件,是时候学习使用扩展方法注册中间件的良好实践了。
为中间件注册创建扩展方法
除了内置到 ASP.NET Core 9 中的中间件,它们有自己的扩展方法用于在 HTTP 管道中进行注册外,每个自定义中间件都必须使用扩展方法进行注册,例如 UseMiddleware<>,这在本章的几个代码示例中已经使用过。
然而,向 Program.cs 文件添加不同的中间件可能会在应用程序中创建读取和维护这些资源的复杂性。
良好的实践是创建扩展方法,以便集中注册中间件,并从抽象配置这些机制复杂性的好处中受益,同时适当地集中责任。
让我们创建一个扩展方法来集中配置之前创建的中间件。为此,在 Middlewares 文件夹中,创建一个名为 CommonMiddlewareExtension.cs 的新类。
将以下代码添加到这个类中:
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
public static class CommonMiddlewareExtensions
{
public static IServiceCollection AddCustomRateLimiting
(this IServiceCollection services)
{
services.AddRateLimiter(options =>
{
options.AddPolicy("fixed", context =>
RateLimitPartition.GetFixedWindowLimiter(new
RateLimitPartitionKey(context.Request
.Headers["X-Forwarded-For"].ToString(),
PartitionKeyKind.ClientIP), partition =>
new FixedWindowRateLimiterOptions
{
PermitLimit = 5,
Window = TimeSpan.FromMinutes(1),
QueueProcessingOrder = QueueProcessingOrder
.OldestFirst,
QueueLimit = 2
}));
options.AddPolicy("sliding", context =>
RateLimitPartition.GetSlidingWindowLimiter(new
RateLimitPartitionKey(context.Request
.Headers["X-Forwarded-For"].ToString(),
PartitionKeyKind.ClientIP), partition =>
new SlidingWindowRateLimiterOptions
{
PermitLimit = 5,
Window = TimeSpan.FromMinutes(1),
SegmentsPerWindow = 3,
QueueProcessingOrder = QueueProcessingOrder
.OldestFirst,
QueueLimit = 2
}));
options.AddPolicy("tokenBucket", context =>
RateLimitPartition.GetTokenBucketLimiter(new
RateLimitPartitionKey(context.Request
.Headers["X-Forwarded-For"].ToString(),
PartitionKeyKind.ClientIP), partition =>
new TokenBucketRateLimiterOptions
{
TokenLimit = 10,
TokensPerPeriod = 5,
ReplenishmentPeriod = TimeSpan.FromSeconds(10),
QueueProcessingOrder = QueueProcessingOrder
.OldestFirst,
QueueLimit = 2
}));
});
return services;
}
public static void UseCommonApplicationMiddleware
(this IApplicationBuilder app)
{
builder.UseMiddleware<ErrorHandlingMiddleware>();
builder.UseMiddleware<PerformanceLoggingMiddleware>();
app.UseRateLimiter();
}
}
上述代码包含所有速率限制中间件的设置,以及全局错误处理和请求性能测量中间件的使用。
此扩展方法类公开了两个 AddCustomRateLimiting 方法,负责添加速率限制策略,以及 UseCommonApplicationMiddleware 方法,负责将之前创建的自定义中间件和速率限制中间件添加到 HTTP 管道中。
在创建类之后,我们将修改 Program.cs 文件,其代码如下:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddCustomRateLimiting();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Use custom rate limiting middleware
app.UseCommonApplicationMiddleware();
app.MapRazorPages();
app.Run();
如高亮代码所示,Program.cs 类使用了之前创建的扩展方法,这使得代码更易于阅读和维护。
扩展方法方法是一种良好的实践,可以将一组配置分组到你的应用程序流程中,以正确地分离责任。
使用这些功能在你的 ASP.NET Core 9 网络应用程序部署流程中,并结合不同的中间件来创建更强大的应用程序。
在本书的下一章中,我们将介绍其他不同的技术,以进一步增加你的 ASP.NET Core 9 应用程序的可能性,例如安全配置管理。
摘要
在本章中,我们学习了如何利用中间件的力量来定制 ASP.NET Core 9 应用程序的执行流程,并理解了中间件管道的工作原理。此外,我们还学习了如何实现自定义中间件,与基于工厂的中间件协同工作,并通过与全局错误处理方法、信息日志记录和请求限制设置协同工作来为应用程序添加功能。在下一章中,我们将探讨如何安全地管理应用程序配置。
第九章:管理应用程序设置
在动态的 Web 应用程序世界中,适应不同环境和要求的能力至关重要。ASP.NET Core 9 提供了一个健壮的配置系统,允许开发者有效地管理设置和行为。本章将探讨应用程序设置的重要性,如何使用配置系统来管理它们,以及如何使应用程序在运行时具有适应性。
在本章中,我们将重点关注以下主题:
-
理解IConfiguration概念和抽象
-
与配置提供者一起工作
-
了解 Options 模式
-
与动态配置和行为一起工作
ASP.NET Core 9 提供了一个健壮的配置系统,允许开发者有效地管理配置和行为。本章将探讨应用程序配置的重要性,如何使用配置系统来管理它们,以及如何使应用程序在运行时具有适应性。
技术要求
为了支持本章的学习,以下工具必须存在于您的开发环境中:
-
Docker:必须在您的操作系统上安装 Docker 引擎,并运行一个 SQL Server 容器。您可以在第五章中找到有关 Docker 和 SQL Server 容器的更多详细信息。
-
Postman:此工具将用于执行对开发应用程序 API 的请求。
-
Redis Insight:此工具用于连接到 Redis 服务器数据库(
redis.io/insight/)。
您还需要访问 Azure 订阅。
本章中使用的代码示例可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/ASP.NET-Core-9.0-Essentials/tree/main/Chapter09
理解 IConfiguration 概念和抽象
在动态的 Web 应用程序世界中,适应不同环境和要求的能力至关重要,除了在运行在不同云提供商上的应用程序中越来越重要的安全要求之外。
大多数 Web 应用程序都会在文件或类中管理某种类型的配置,以便集中管理整个应用程序流程中使用的参数。随着配置或甚至应用程序行为的每次更改,都必须生成新的软件版本。此外,当与远程团队合作时,维护配置和敏感数据的正确管理至关重要,并且不要将这些参数版本化在应用程序版本控制中。
ASP.NET Core 9 提供了强大的方式来管理应用程序配置,除了启用使用其他功能外,例如,无需生成新版本的软件即可更改应用程序行为。
我们将通过IConfiguration接口配置管理的基础知识来开始学习这些资源。
IConfiguration 接口
ASP.NET Core 9 拥有IConfiguration接口,旨在提供一种以统一方式管理应用程序设置和配置的机制,允许访问诸如 JSON 文件、环境变量和参数等不同的配置来源。
在关于IConfiguration接口的主要概念中,我们有以下几点:
-
配置来源:支持多个配置来源,可以组合和分层。常见的来源包括 JSON 文件(如appsettings.json)、环境变量、命令行参数和用户密钥。
-
分层配置:配置设置以分层结构组织。这意味着设置可以嵌套到部分中,使得复杂的设置更容易管理。
-
Options 模式:Options 模式使用IConfiguration将配置设置绑定到强类型对象。
在第五章中,我们使用了appsettings.json文件来管理与 SQL Server 数据库的连接字符串,并通过IConfiguration接口在Program.cs类中检索了该配置的值。这种做法带来了以下好处:
-
灵活性:IConfiguration允许您从多个来源提取配置值,在如何管理不同环境(开发、测试、生产)中的配置方面提供了灵活性
-
集中管理:集中配置管理,使得在不分散到整个应用程序的情况下维护和更新设置变得更加容易
-
环境特定设置:支持环境特定配置,允许您根据应用程序运行的环境自定义设置
-
强类型配置:通过 Options 模式,它支持与强类型类链接的配置设置,提高类型安全并减少错误
以下代码示例演示了在appsettings.json文件中定义的配置;然后通过Program.cs文件中的IConfiguration接口检索此值:
-
以下为appsettings.json文件的内容:
{ "ConnectionStrings": { "DefaultConnection": "Server=myServerAddress; Database=myDataBase;User Id=myUsername; Password=myPassword;" } } -
以下为Program.cs文件的内容:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorPages(); string connectionString = builder.Configuration .GetConnectionString("DefaultConnection"); builder.Services.AddDbContext<MyDbContext>(options => // options.UseSqlServer(connectionString)); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseStaticFiles(); app.UseRouting(); app.MapRazorPages(); app.Run();
如前代码片段中突出显示的行所示,连接字符串是通过IConfiguration接口使用GetConnectionString方法从appsettings.json文件中获取的,并告知配置名称或键。
由于 IConfiguration 生命周期考虑了应用程序生命周期,因此通过诸如 appsettings.json 或其他数据源之类的文件获取设置的复杂性都被抽象化,只需使用接口中可用的方法在整个 ASP.NET Core 9 应用程序中获取所需的参数即可。
IConfiguration 接口也存在于 依赖注入容器(DIC)中,允许您在任何将动态解决其依赖关系的应用程序类的构造函数中引用它。
IConfiguration 方法
IConfiguration 接口提供了几个扩展方法,这些方法以不同的方式在 ASP.NET Core 9 应用程序中获取配置数据。有关更多详细信息,请参阅以下链接:learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration?view=net-9.0。
在 JSON 文件中获取配置的过程是 ASP.NET Core 9 应用程序中使用的标准模型。然而,还有其他管理配置的方法,为此可能性,存在提供程序的概念,我们将在下一节中探讨。
与配置提供程序一起工作
配置提供程序允许从各种来源获取配置,例如 JSON 文件、环境变量等。
通过配置提供程序,我们获得了更大的灵活性,并且能够以适当的方式准备我们的应用程序在不同的环境中(如开发、测试或生产)运行,无需在 JSON 文件中实现字符串替换逻辑,这除了带来更高的可靠性和安全性之外。
接下来,我们将了解如何将其他配置提供程序添加到我们的 ASP.NET Core 9 应用程序中。
添加配置提供程序
配置提供程序用于从各种来源读取配置数据。这种灵活性允许您以一致和集中的方式管理应用程序的配置设置。
这使得使用数据库甚至云服务等配置源成为可能。
在 ASP.NET Core 9 中,已经集成了某些配置提供程序,例如以下内容:
-
JSON 配置提供程序:从类似于 appsettings.json 的 JSON 文件中读取配置数据。
-
环境变量配置提供程序:从环境变量中读取配置数据。
-
命令行配置提供程序:从命令行参数中读取配置数据。
-
内存配置提供程序:允许您添加内存中的配置数据,这对于单元测试非常有用。
在本书中实现示例应用程序的过程中,我们一直在 Program.cs 文件中使用以下方法:WebApplication.CreateBuilder(args);。
此方法创建了一个 Builder 对象的实例,它代表一个 Web 应用程序,并允许我们添加诸如服务、中间件和配置等功能。它创建了一个具有一些默认参数的 Web 应用程序构建器,因此无需定义提供程序配置即可从 appsettings.json 文件中获取配置数据。
默认构建器设置
CreateBuilder 方法定义了将要创建的构建器的一些标准化参数。您可以通过访问以下网址了解这些参数的更多信息:learn.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-9.0#default-builder-settings。
然而,请注意以下来自 Program.cs 文件的代码示例,其中显式添加了配置提供程序:
var builder = WebApplication.CreateBuilder(args);
// Add configuration from environment variables
builder.Configuration.AddEnvironmentVariables();
// Add configuration from command-line arguments
builder.Configuration.AddCommandLine(args);
var app = builder.Build();
app.Run();
上述代码使用了两个扩展方法,AddEnvironmentVariables 和 AddCommandLine,允许应用程序从不同的提供程序中获取配置。这些扩展方法是 ASP.NET Core 9 应用程序的原生部分。对于其他类型的提供程序,可能需要添加 NuGet 包以访问用于注册提供程序的扩展方法。
内置的 ASP.NET Core 9 配置提供程序
ASP.NET Core 9 提供了多种类型的提供程序,如文档中所示:learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-9.0#configuration-providers。
在 使用动态配置和行为 部分,我们将使用提供程序连接到一个允许以动态和安全方式管理配置和行为的云资源。现在,让我们了解一些重要基础,例如创建自定义提供程序。
创建自定义配置提供程序
在 ASP.NET Core 9 中创建自定义配置提供程序允许您从框架未原生支持的源加载配置数据。这对于与自定义配置存储、第三方服务或甚至专有格式集成非常有用。
我们已经理解了 ASP.NET Core 9 中原生配置管理;现在,我们将创建我们的第一个自定义提供程序。
要创建一个自定义提供程序,您需要创建两个类:
-
ConfigurationSource:IConfigurationSource 接口代表配置数据的来源。它负责创建一个 IConfigurationProvider 实例,该实例将实际加载数据。将源接口和提供者接口分离,可以明确区分配置数据的来源和加载方式。通过这种方法,我们可以从封装数据源配置和通过 Factory 模式实现最佳实践中受益。
-
ConfigurationProvider:ConfigurationProvider 类是一个实现 IConfigurationProvider 的抽象基类。它负责实际加载数据和提供配置数据。这个类允许你定义如何读取、缓存和访问数据。
我们将通过负责创建自定义提供者实例的类开始创建自定义提供者:
using Microsoft.Extensions.Configuration;
public class CustomConfigurationSource :
IConfigurationSource
{
public IConfigurationProvider Build
(IConfigurationBuilder builder)
{
return new CustomConfigurationProvider();
}
}
IConfigurationSource 接口只有一个方法,Build(),它负责返回一个自定义提供者实例。
尽管这个类很简单,但它允许更好地分离职责,其唯一目标是提供一个提供者实例,该实例将具有与另一个数据源交互所需的所有必要机制。
现在,让我们看看 CustomConfigurationProvider 类:
using Microsoft.Extensions.Configuration;
using System.Collections.Generic;
public class CustomConfigurationProvider :
ConfigurationProvider
{
public override void Load()
{
var data = new Dictionary<string, string>
{
{ "MyCustomSetting:Key1", "Value1" },
{ " MyCustomSetting:Key2", "Value2" }
};
Data = data;
}
}
在前面的代码中实现的自定义提供者继承自抽象的 ConfigurationProvider 类,该类已经有一些实用实现。
对于这个提供者,我们正在创建两个将由 Dictionary<string, string> 类型的对象管理的配置,允许我们根据键和值在内存中创建配置。
管理提供信息的逻辑必须通过覆盖从 ConfigurationProvider 类继承的 Load 方法来实现。
在 Load 方法的末尾,有必要设置从 ConfigurationProvider 类继承的 Data 属性的值。Data 属性的数据类型是 Dictionary<string, string?>;即表示键值对。设置以字符串格式持久化或序列化。然而,即使有这个功能,也可以创建强类型配置。我们将在 学习选项 模式 部分介绍这种方法。
当然,这是一个简单且具有教育意义的例子。然而,我们可以轻松地将自定义提供者连接到 SQL Server 数据库、存储账户或任何其他持久化资源。在本书的代码仓库中,其链接位于 技术要求 部分,你可以分析另一个自定义提供者的版本,其中数据在数据库中持久化。然而,实现原则与之前代码片段中演示的相同。
要使用新的提供者,只需将以下代码添加到 Program.cs 文件中:
builder.Configuration.Add(new CustomConfigurationSource());
您还可以使用在第八章中学到的扩展方法创建技术。
要通过新的提供者获取设置,只需使用Configuration属性方法,例如GetValue
string key1 = _configuration.GetValue<string>
(" MyCustomSetting:Key1");
string key2 = _configuration.GetValue<string>
(" MyCustomSetting:Key2");
如我们所见,创建自定义提供者不会改变 ASP.NET Core 9 中已有的开发模型,带来了更大的灵活性和可能性。
提供者是配置管理的优秀抽象。然而,ASP.NET Core 9 提供了其他类型,例如选项模式,我们将在下一节中讨论,这使得我们应用程序中的配置管理模型更加强大。
学习选项模式
ASP.NET Core 9 通过使用选项模式提供了一种处理应用程序配置的好方法。此模式提供了一种强大的机制,以强类型方式管理和访问配置设置,提高代码的可维护性和可测试性,并将配置设置组织到类中。在本节中,我们将了解选项模式是什么以及如何实现它。
选项模式是什么?
每个应用程序都必须与某种类型的配置进行交互,在前面的章节中,我们使用了IConfiguration接口从appsettings.json文件中获取连续信息。然而,这并不是在 ASP.NET Core 9 中与配置交互的唯一方式,它提供了一个选项模式的实现。
选项模式是一种设计模式,它使用类来表示相关配置的组,允许您将来自各种配置源(如 JSON 文件、环境变量等)的配置部分链接到这些类,从而允许访问配置设置、类型安全的配置、利用 IntelliSense 和编译时检查。
在 ASP.NET Core 9 中,选项模式具有以下类层次结构:

图 9.1 – ASP.NET Core 9 中主选项模式抽象
如图 9.1所示,除了它们各自的实现之外,还有几个接口用于抽象选项模式。让我们简要了解图 9.1中展示的每个接口的目的:
-
IOptions
:用于检索配置选项的基本接口。IOptions 类型注册为单例。然后,当启动应用程序时,配置被加载到内存中,并通过依赖注入(DI)在整个应用程序中提供。但是,如果对应用程序进行了任何更改,以便它们可以反映出来,则必须重新启动应用程序。 -
IOptionsSnapshot
:IOptions 的一个变体,它提供了一种机制,可以在每次请求时更新配置。此接口允许实时更新应用程序设置。此类的生命周期是作用域的;也就是说,配置是在每次请求时加载的。 -
IOptionsMonitor
:一个接口,允许您监控选项更改,并提供在选项更新时接收通知的方式。此接口的生命周期是单例的,在应用程序初始化时可用。 -
没有带有“I”前缀的类代表每个接口的具体实现。
选项模式提供了一个优秀的选项,可以在不调用字符串的情况下操作配置,通过创建强类型配置。
让我们了解选项模式是如何实现的。
实现选项模式
如我们在本书的章节中所学,集中管理配置信息是一个好的实践,在这本书中大部分的代码中,我们使用基本的配置,例如数据库连接字符串。
虽然IConfiguration接口为我们提供了获取配置的机制,但在某些情况下,这可能会影响每个类的管理和职责,每个类必须确切知道它想要从配置文件中获取的字符串。
有一些做法,例如使用常量;然而,使用强类型类来聚合一组信息可以是一种强大的资源。
让我们想象以下上下文:
想象一个电子商务应用程序,其中我们拥有不同的服务和资源,如支付和运输。这些资源有一套独特的设置。在这个例子中,我们可能会有以下配置:
-
PaymentGatewayURL:处理支付的网关的 URL
-
APIKey:用于使用支付网关的 API 密钥
-
Timeout:超时配置
-
DefaultCarrier:交付订单的默认承运人
-
FreeShippingThreshold:免费运输阈值设置
在开发电子商务应用程序时,对于每个配置,可能需要使用IConfiguration类从appsettings.json文件中获取数据,例如。让我们看看以下代码:
// Accessing settings directly using IConfiguration
string paymentGatewayURL =
_configuration["PaymentGatewayURL"];
string apiKey = _configuration["APIKey"];
int timeout = _configuration.GetValue<int>("Timeout");
string defaultCarrier = _configuration["DefaultCarrier"];
decimal freeShippingThreshold =
_configuration.GetValue<decimal>
("FreeShippingThreshold");
上述设置在appsettings.json文件中的表示如下:
{
"PaymentGatewayURL": "https://payment.aspnetcore9.com",
"APIKey": "your-api-key",
"Timeout": 30
"DefaultCarrier": "UPS",
"FreeShippingThreshold": 30.00
}
所展示的设置恢复代码实现是正确的。然而,在更复杂的场景中,可能难以管理不同类型的配置;可能会有代码重复,这使得维护变得困难,如果在配置键中存在类型错误,例如,这个问题只有在运行时才会被发现。
幸运的是,通过 ASP.NET Core 9 中选项模式的抽象,我们可以以简单的方式对配置进行分组。
根据前面的例子,我们基本上有两种类型的信息:
-
PaymentSettings
-
PaymentGatewayURL
-
APIKey
-
Timeout
-
ShipmentSettings
-
DefaultCarrier
-
FreeShippingThreshold
这样,我们可以将配置分组到两个不同的类中,如图 图 9.2 所示:

图 9.2 – 分组配置
前面的类仅具有将分别以分组方式引用相应配置的属性。有了这个,我们将有如下代码来定义 PaymentSettings 和 ShipingmentSettings 类:
public class PaymentSettings
{
public string PaymentGatewayURL { get; set; }
public string APIKey { get; set; }
public int Timeout { get; set; }
}
public class Shippingettings
{
public string DefaultCarrier { get; set; }
public decimal FreeShippingThreshold { get; set; }
}
前面的代码中代表的类仅旨在抽象化一组配置,没有实现任何类型的操作行为,但在必要时可能会有方法。
然而,将在 Options 模式中使用的类必须遵循以下规则:
-
非抽象
-
具有公共读写属性
-
绑定时忽略字段
对于这个例子,我们只保留属性。此外,我们还将修改 appsettings.json 文件,它将包含以下代码:
{
"PaymentSettings": {
"PaymentGatewayURL":
"https://payment.aspnetcore9.com",
"APIKey": "your-api-key",
"Timeout": 30
},
"ShippingSettings": {
"DefaultCarrier": "UPS",
"FreeShippingThreshold": 30.00
}
}
我们可以在前面的代码中看到,设置是通过 PaymentSettings 和 ShippingSettings 分组的,这些名称正好是类的名称,相应的属性也具有相同的名称。
Options 模式使用此约定将设置与将在应用程序中抽象此信息的类绑定。
现在,让我们看看经过修改以使用 Options 模式的 Program.cs 文件:
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<PaymentSettings>
(builder.Configuration.GetSection("PaymentSettings"));
builder.Services.Configure<ShippingSettings>
(builder.Configuration.GetSection("ShippingSettings"));
builder.Services.AddRazorPages();
builder.Services.AddSingleton<EcommerceService>();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseRouting();
app.MapRazorPages();
app.Run();
如您在前面的代码块中高亮显示的代码行所示,这里使用了来自 IConfiguration 接口的 GetSection 方法,通过 Configuration 扩展方法进行访问。GetSection 方法是一个泛型实现,其中输入的类型决定了返回的类型。章节名称作为参数输入,在这个例子中,是期望的配置组。
扩展方法
在前面的代码示例中,为了便于理解,我们直接在 Program.cs 文件中使用 Options 模式注册配置类。然而,创建一个用于注册使用 Options 模式的类的扩展方法是一种良好的实践。
当执行这些方法时,设置将被加载到 PaymentSettings 或 ShipmentSettings 对象中,并且这些对象将作为单例实例化,并在每个应用程序中通过 DIC 可用。
下面的代码示例显示了配置依赖于给定的服务类:
public class OrderService
{
private readonly PaymentSettings _paymentSettings;
private readonly IPaymentGateway _paymentGateway;
public OrderService(IOptions<PaymentSettings>
paymentSettings, IPaymentGateway paymentGateway)
{
_paymentettings = paymentSettings.Value;
_paymentGateway = paymentGateway;
}
public async Task<IOrder> Pay(decimal ammount)
{
var order = _paymentGateway(ammount,
_paymentSettings.ApiKey,
_paymentSettings.PaymentGatewayURL);
// ..
}
}
正如我们在 OrderService 实现中注意到的,我们只是注入了 IOptions 接口用于 PaymentSettings 类型。通过这种方式,依赖关系由 ASP.NET Core 9 DIC 解决。
采用 Options 模式提供了几个好处,例如允许组织配置设置,将它们分组到专用类中,并使用强类型配置。
除了提高维护和实施质量外,在编译时也可以检测到错误。您还可以从使用单元测试中受益,这是一个非常好的实践。
ASP.NET Core 9 中的选项模式是一种强大且灵活的方式来管理配置设置,支持特定环境的配置,并使管理不同部署环境的配置变得更加容易。
现在我们已经学会了如何使用选项模式来正确处理应用程序配置,现在是时候了解如何在云环境中安全地管理这些配置,以及学习如何在我们的应用程序中动态地操作行为。
与动态配置和行为一起工作
在不断发展的 Web 应用程序领域,保持灵活性和响应性至关重要。作为开发者,我们必须确保我们的应用程序能够快速且安全地适应变化。动态配置和行为管理是允许我们实现这种灵活性的关键策略。通过动态管理配置,我们可以更新它们而无需重新部署我们的应用程序。此外,通过使用功能开关等技术实现应用程序行为管理,我们可以实时控制资源可用性,从而轻松测试、部署或回滚功能。
功能开关
功能开关或功能标志是一种软件开发技术,用于在运行时启用或禁用软件应用程序中的特定功能,通过允许代码更改合并到主代码库而不立即向所有用户暴露新功能,从而促进持续集成和交付(CI/CD)。这项技术有助于降低风险、进行 A/B 测试、执行金丝雀发布以及在不重新部署代码的情况下回滚功能。
访问以下文章了解更多关于这项技术的信息:martinfowler.com/articles/feature-toggles.html
让我们深入了解动态配置管理的细节,并实现一个实际示例。
与动态设置一起工作
动态设置指的是在运行时修改应用程序配置的能力,无需重新部署。这种能力对于保持应用程序正常运行、确保对需求变化的快速响应以及通过允许对敏感配置的快速调整来增强安全性至关重要。
现代应用程序必须具备动态配置管理机制,以确保解决方案的质量,以及以下好处:
-
零停机时间:在不重新部署应用程序的情况下更新配置,确保持续可用。这是一个重要的功能,使我们能够为用户提供可靠性和更好的体验。
-
安全性:快速更新安全设置和凭据以应对威胁。
-
灵活性:实时调整设置以适应不断变化的企业需求。
-
简化部署:通过将配置更改与代码更改解耦,减少与应用程序部署相关的复杂性和风险。在具有许多环境(如开发、测试和生产)的场景中,应用程序将根据每个环境的资源具有不同的配置。能够抽象管理应用程序配置的能力可以提高质量、分离责任并保持持续交付流程。
在 ASP.NET Core 9 中,有几种选项可以动态管理配置,包括配置文件、环境变量和基于云的服务,如 Azure App Configuration。Azure App Configuration 以其强大的功能和与其他 Azure 服务的无缝集成而脱颖而出。
Azure App Configuration 是一种提供集中式管理配置设置和功能标志的服务。它允许应用程序在不重新部署的情况下动态调整其行为。
Azure App Configuration
Azure App Configuration 是 Microsoft Azure 中的一项强大功能,允许我们安全地管理配置和功能开关,支持云原生应用程序的部署。
关于 Azure App Configuration 的更多详细信息,我建议阅读关于该资源的丰富文档:learn.microsoft.com/en-us/azure/azure-app-configuration/。
在本书中,我们不会涵盖设置和使用 Azure App Configuration 的所有细节。目前,我们将使用主要资源来举例说明在我们的应用程序中使用动态配置。
让我们创建一个与 Azure App Configuration 交互并使用本章已学到的某些模式(如 Options 模式)的应用程序。
设置 Azure App Configuration
Azure App Configuration 是一种基于云的服务,为应用程序配置提供集中式存储库,实现动态配置管理。
在创建应用程序之前,让我们创建一个 Azure App Configuration 资源。您需要访问 Azure 订阅,如 技术要求 部分所述。
在访问 Azure 订阅后,按照以下步骤创建 Azure App Configuration 资源:
- 导航到 Azure 门户(
portal.azure.com),在顶部栏的搜索字段中输入 App Configuration 并点击图标,如图 图 9 .3 所示:

图 9.3 – 访问 App Configuration 服务
- 在下一屏上,点击 + 创建 选项以添加新资源,如图 图 9 .4 所示:

图 9.4 – 创建新的应用配置资源
-
在下一屏幕上,我们必须配置新资源的参数。我们将保持默认设置。以下参数建议仅供参考:
-
资源组:rg-aspnetcore8。务必点击资源组字段下方的创建新按钮以创建新的资源组。
-
位置:东 US 2
-
资源名称:<您的 姓氏>-配置
-
定价层:标准
-
不勾选创建副本选项。
-
-
点击审阅 + 创建按钮然后点击创建按钮,等待资源创建完成。
-
当你完成创建新资源后,点击转到资源按钮,如图图 9 .5所示,或者按照步骤 1所述访问应用配置列表,然后点击创建的资源:

图 9.5 – 新应用配置创建状态
现在我们已经创建了配置管理资源,是时候创建和配置我们的应用程序以与 Azure 应用配置交互了。
在 Azure 应用配置中创建和连接应用程序
在此示例中,将创建一个 ASP.NET Core 9 MVC 应用程序,并将其连接到之前创建的 Azure 应用配置服务。
按照以下步骤操作:
-
打开终端,并在您选择的文件夹中创建一个名为DynamicConfiguration的目录:
mkdir DynamicConfiguration -
现在,使用以下命令访问目录:
cd DynamicConfiguration -
运行以下命令以创建应用程序:
dotnet new mvc -n DynamicConfiguration -o .之前的命令创建了一个名为DynamicConfiguration的 MVC 应用程序,使用-n参数定义,并在当前目录中,由–o .参数确定。
应用程序创建后,我们将简单地准备它以集成到 Azure 应用配置中。为此,在应用程序目录中运行以下命令以打开 Visual Studio Code:
code .
现在,在项目的根目录下创建一个Options文件夹,然后创建一个名为GlobalOptions.cs的文件。此文件必须包含以下代码:
namespace DynamicConfiguration.Options;
public class GlobalOptions
{
public string Title { get; set; }
}
GlobalOptions类只有一个名为Title的属性,它将通过 Azure 应用配置获取。
当应用程序启动时,它会加载我们之前学到的设置,使用诸如appsettings.json和环境变量等文件,以及其他可以配置的提供者。然而,对于我们的类,获取配置的细节被 ASP.NET Core 9 抽象化,在这种情况下,无论提供者如何,当使用 Options 模式时,我们将拥有正确的职责分离、可维护性、灵活性和可扩展性。
让我们更改HomeController类在Controllers文件夹中的代码,并添加之前使用 Options 模式创建的设置。HomeController类的代码将如下所示:
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly GlobalOptions _globalOptions;
public HomeController(ILogger<HomeController> logger,
IOptionsSnapshot<GlobalOptions> globalOptions)
{
_logger = logger;
_globalOptions = globalOptions.Value;
}
public IActionResult Index()
{
ViewData["Title"] = _globalOptions.Title;
return View();
}
public IActionResult Privacy()
{
return View();
}
[ResponseCache(Duration = 0,
Location = ResponseCacheLocation.None,
NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel {
RequestId = Activity.Current?.Id ??
HttpContext.TraceIdentifier });
}
}
对类进行了简单的更改。让我们了解每个更改:
-
GlobalOptions 字段:为类创建了一个新字段,该字段类型为之前创建的GlobalOptions。
-
构造函数中的更改:配置将通过 ASP.NET Core 9 依赖注入容器(DIC)注入,这就是为什么我们添加了一个类型为IOptionsSnapshot
的参数。使用IOptionsSnapshot<>接口的目的是允许你动态地获取配置,正如我们在什么是 Options 模式?部分中学到的。如果使用其他接口,例如IOptions<>,参数将被加载,但不是动态的。 -
更改 Index 动作:我们更改Index动作,在该动作中,我们使用_globalOptions对象的Title属性的配置值设置ViewData字典中Title属性的值。ViewData["Title"]字典在Views/Home/Index.cshtml文件中使用,用于显示页面标题。
现在,让我们将Views/Home/Index.cshtml页面的代码进行更改,以便在页面主体中显示标题:
<div class="text-center">
<h1 class="display-4">Welcome @ViewData["Title"]</h1>
<p>Learn about <a
href="https://learn.microsoft.com/aspnet/core">
building Web apps with ASP.NET Core</a>.</p>
</div>
如高亮代码所示,我们只渲染ViewData["Title"]字典中包含的值。
应用程序已准备好通过配置渲染数据。现在,是时候将应用程序连接到 Azure App Configuration 了。
首先,在应用程序目录中打开终端,并运行以下命令以添加包含必要 SDK 的 NuGet 包:
dotnet add package Microsoft.Azure.AppConfiguration.AspNetCore
现在,你需要从 Azure App Configuration 获取包含资源的连接字符串。让我们执行以下步骤:
-
访问 Azure 门户(
portal.azure.com)。 -
在门户顶部的搜索字段中,输入App Configuration并单击该选项。
-
然后,在配置资源列表中,单击之前创建的名为<Your Last Name>-configuration的资源。
-
在侧边菜单中,查找访问设置选项,并复制如图 9 .6 所示的连接字符串:

图 9.6 – 从 App Configuration 获取连接字符串
-
现在,在终端中运行以下命令,在应用程序目录下。该命令使用密钥管理器存储一个名为ConnectionStrings:AppConfig的密钥,该密钥存储 App Configuration 存储的连接字符串。将<your_connection_string>占位符替换为您的 App Configuration 存储的连接字符串。这是一种良好的做法,可以防止敏感数据,如包含凭据或密码的连接字符串,在版本控制中持久化,从而给应用程序带来漏洞:
dotnet user-secrets init dotnet user-secrets set ConnectionStrings:AppConfig "<your_connection_string>"
在定义包含 App Configuration 连接字符串的密钥后,我们将修改Program.cs文件以添加必要的服务和中间件。让我们看看修改后的Program.cs代码:
using Microsoft.Extensions.Configuration
.AzureAppConfiguration;
var builder = WebApplication.CreateBuilder(args);
Builder.Services.AddAzureAppConfiguration();
var connectionString = builder.Configuration
.GetConnectionString("AppConfig");
builder.Configuration.AddAzureAppConfiguration(options =>
{
options.Connect(connectionString)
.Select("DynamicConfiguration:*", LabelFilter.Null)
.ConfigureRefresh(refreshOptions =>
refreshOptions.Register("DynamicConfiguration:
Sentinel", refreshAll: true));
});
builder.Services.Configure<GlobalOptions>
(builder.Configuration
.GetSection("DynamicConfiguration:GlobalOptions"));
builder.Services.AddControllersWithViews();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
// Middleware to refresh configuration
app.UseAzureAppConfiguration();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
App Configuration SDK 具有出色的抽象性,并且易于集成到应用程序中。让我们了解对前面代码所做的更改:
-
builder.Services.AddAzureAppConfiguration():此方法注册了 Azure App Configuration 在您的 ASP.NET Core 9 应用程序中工作所需的服务。
-
builder.Configuration.GetConnectionString("AppConfig"):这一行代码获取 Azure 应用程序配置连接字符串,之前从 Azure 门户获取并通过密钥添加到应用程序中。请注意,获取连接字符串与从appsettings.json文件获取相同。通过密钥管理配置的主要区别在于,它们仅在本地计算机上保留。
-
builder.Configuration.AddAzureAppConfiguration:这个扩展方法将 Azure App Configuration 添加为应用程序的配置提供者。
-
options.Connect(connectionString):使用之前获得的连接字符串连接到 Azure 应用配置实例。
-
.Select("Dynamic Configuration:*", LabelFilter.Null):指定选择所有以DynamicConfiguration:前缀开头的键。LabelFilter.Null参数表示只检索未标记的配置。
-
.ConfigureRefresh:ConfigureRefresh方法注册了您想要监控其更改的应用程序配置存储中的键。
-
refreshOptions.Register("Dynamic Configuration", updateAll: true):注册一个哨兵键(DynamicConfiguration:Sentinel),它将触发更新。当此键的值发生变化时,所有设置都将更新(refreshAll: true)。
-
builder.Services.Configure
(builder.Configuration.GetSection("DynamicConfiguration: GlobalOptions")) :这一行代码将之前在 Azure App Configuration 中创建的配置与之前创建的GlobalOptions类绑定。通过这种方式,可以使用 Options 模式通过依赖注入容器(DIC)获取配置。 -
UseAzureAppConfiguration:允许应用程序使用应用程序配置中间件自动更新配置。
总结一下,之前添加到 Program.cs 文件中的配置允许应用程序通过连接字符串连接到 Azure App Configuration。
应用配置 SDK 与池化概念一起工作。在这种情况下,在获取配置时,在应用程序的内存中创建一个缓存,以避免不断请求 Azure 服务并优化应用程序的功能。
默认情况下,应用程序查询 Azure App Configuration 以获取更新的时间是 30 秒。可以使用 refreshOptions.SetCacheExpiration 方法指定刷新调用时间。还配置了一个 Sentinel 参数,负责确定设置是否发生变化。
这很重要,因为它防止 SDK 逐个分析每个配置,并且如果 Sentinel 已更改,所有配置都将更新。
现在我们已经了解了 Azure App Configuration 设置的工作原理,让我们在 Azure 门户中创建配置键:
- 在 Azure 门户(
portal.azure.com)中,选择之前创建的应用配置资源,选择配置资源管理器选项,如图图 9 .7 所示:

图 9.7 – 访问 Azure App Configuration 的配置资源管理器选项
- 然后,点击创建 | 键值选项,如图图 9 .8 所示:

图 9.8 – 在 Azure App Configuration 中添加新的配置
将显示一个表单,我们将输入以下参数:
-
键:DynamicConfiguration:Sentinel
-
值:1
-
将其余参数保留为默认值。
-
点击应用。
-
重复 步骤 2 并添加另一个具有以下配置的键:
-
键:DynamicConfiguration:GlobalOptions:Title
-
值:ASP.NET Core 9
注意 DynamicConfiguration:GlobalOptions:Title 键。此键代表一个遵循
: 模式的层次结构。在这种情况下,它是之前创建的设置类、配置或配置类的期望属性的名称。 -
Azure App Configuration 中的标签
Azure 应用配置中的标签参数用于根据不同的上下文或环境区分配置设置。
标签允许您根据不同的环境(例如,开发、测试、生产)分离配置。
这种方法带来了以下好处:
• 灵活性:轻松地在不同的配置集之间切换
• 隔离:保持不同环境或场景的设置隔离和组织
• 测试:安全地测试新配置,而不会影响其他环境
没有设置标签参数的设置将被视为默认设置。
要了解更多关于标签的信息,请访问以下网址:learn.microsoft.com/en-us/azure/azure-app-configuration/howto-labels-aspnet-core。
现在,应用程序已集成到 Azure App Configuration,打开终端并访问应用程序目录。然后,运行以下命令:
dotnet run
现在,通过以下网址访问您终端上的应用程序 URL:http://localhost:

图 9.9 – 应用程序从 Azure App Configuration 获取设置
如我们所见,配置是直接从 Azure 资源加载的。
在应用程序仍在运行的情况下,访问 Azure 门户(portal.azure.com)和 Azure App Configuration 资源。
然后,访问 配置资源管理器选项。我们将更改设置。
要完成这个操作,在设置显示网格中,点击如图 图 9 .10 所示的三个点(...),然后点击 编辑:

图 9.10 – 编辑配置
提供以下设置:
-
DynamicConfiguration:GlobalOptions:Title : ASP.NET Core 9 With Dynamic config
-
DynamicConfiguration:Sentinel : 2
等待几秒钟,再次访问应用程序,并刷新页面。页面正文消息已更改,如图 图 9 .11 所示:

图 9.11 – 应用程序动态获取配置
尽管这是一个简单的实现示例,但使用配置服务器是云原生应用程序中的良好实践,如十二要素应用方法(12factor.net)中建议的,并且 ASP.NET Core 9 提供了几个扩展机制,例如我们在与 Azure 的 App Configuration 功能集成时使用的机制。
在生产环境的大型应用程序场景中,这种方法可以通过对某些更改提供即时响应来带来几个好处。
十二要素应用方法
十二要素应用方法是一种在创建 SaaS(软件即服务)应用时作为参考的方法,它有 12 个因素,与技术无关,提供了开发云原生解决方案的最佳实践。其中一个因素与配置管理相关,与我们本章所学的内容相关,您可以通过以下网址了解更多关于这个因素的信息:12factor.net/config。
在服务器上管理配置,如 Azure 应用配置,带来了许多好处,其中最重要的是安全性。与 ASP.NET Core 9 应用程序的轻松集成使我们能够允许我们的应用程序动态更改设置,甚至根据环境进行隔离。这种方法在 CI/CD 流程中非常重要。我们将在第十章中更多地讨论 CI/CD。
除了提高用户体验等好处外,另一种可以为我们带来更大容量并改进应用程序的方法是实时行为管理,我们将在下一节中了解。
将 ASP.NET Core 9 应用程序连接到 Azure 应用配置
一定在某个时刻,你已经使用过一种动态更改应用程序行为的机制。想象一下,例如,你安装了一个允许用户发送视频、照片和音频的消息应用程序的场景。
当你收到消息时,这些媒体会自动下载,这需要在你的智能手机上消耗数据。通常,我们首先采取的行动将是禁用自动媒体下载选项。此设置位于设置菜单中;然而,此设置会实时完全改变应用程序的行为方式。
上述示例是实时更改应用程序行为的一个简单概念。同样,在某个时刻,Web 应用程序可能对其行为进行动态管理,并且其使用带来了以下好处:
-
受控发布:逐步向用户子集发布功能,以监控性能和用户反馈
-
A/B 测试:通过为不同用户组轮换功能来开展实验,以确定最佳方法
-
即时回滚:如果功能出现问题,可以快速禁用它,而无需重新部署应用程序
允许在实时更改应用程序行为的技巧被称为功能开关或功能标志。
功能开关,也称为功能标志,是一种软件开发技术,允许你在不部署新代码的情况下,在应用程序运行时启用或禁用功能,从而为更好的风险管理提供灵活性,并改进部署过程,使团队能够向特定用户或环境释放资源,并提高整体开发和运营效率。
在编码方面,功能开关可以表示如图 9.12 所示:

图 9.12 – 功能开关概念表示
如我们在 图 9 .12 中所见,功能切换基本上是应用程序源代码中的一个决策点。这个决策点检查一个称为切换的特定值是否被激活。要检查切换是否被激活,我们可以通过配置文件、环境变量,甚至远程服务器(这是最推荐的方式)来获取这个值。
单一职责原则
在 ASP.NET Core 9 中实现功能切换应遵循最佳实践,以使您的代码更干净、更容易维护和更易于扩展。在使用功能切换时,将多个行为合并到一个类中,例如处理不同的功能或在一个服务中切换新旧逻辑,是不良的做法。一种好的做法是遵循单一职责原则(SRP),这意味着每个类应该只处理一个职责或功能。通过保持每个类专注于一项任务,您可以减少复杂性并使应用程序更容易维护和扩展。
此外,在 ASP.NET Core 9 中使用依赖注入(DI)的工厂方法允许您根据功能切换轻松地交换不同的实现,而不会破坏 SRP。如果您需要添加新功能,您可以简单地为该功能创建一个新类,隔离现有逻辑。要了解更多关于 SRP 的信息,请访问以下网址:learn.microsoft.com/en-us/dotnet/architecture/modern-web-apps-azure/architectural-principles#single-responsibility。
关于功能切换技术及其相应的好处,不仅限于开发团队,还包括管理和复杂性方面,超出了本书的范围。
我们已经掌握了使用这项技术的基本知识,现在,是时候在 ASP.NET Core 9 应用程序中使用它了。
使用 Azure App Configuration 管理功能切换
在上一个主题中,我们使用了 Azure App Configuration 功能进行配置管理;然而,这项服务还包括功能标志,您可以使用它来启用或禁用功能。通过 Azure 门户中的 UI,我们可以创建和管理我们应用程序的功能标志。
让我们对之前创建的 DynamicConfiguration 项目代码进行一些修改,并添加功能切换:
-
打开终端并转到应用程序目录。然后,运行以下命令:
dotnet add package Microsoft.Azure.AppConfiguration.AspNetCore dotnet add package Microsoft.FeatureManagement.AspNetCore -
这些包是必要的,以将管理应用程序切换的 SDK 集成到应用程序中。
-
在 Visual Studio Code 中打开应用程序,然后我们将编辑 Program.cs 文件,该文件将包含以下更新后的代码:
using DynamicConfiguration.Options; using Microsoft.Extensions.Configuration .AzureAppConfiguration; using Microsoft.FeatureManagement; var builder = WebApplication.CreateBuilder(args); builder.Services.AddAzureAppConfiguration(); builder.Services.AddFeatureManagement(); var connectionString = builder.Configuration .GetConnectionString("AppConfig"); builder.Configuration .AddAzureAppConfiguration(options => { options.Connect(connectionString) .Select("DynamicConfiguration:*", LabelFilter.Null) .ConfigureRefresh(refreshOptions => refreshOptions .Register("DynamicConfiguration:Sentinel", refreshAll: true)) .UseFeatureFlags(featureFlagsOptions => { featureFlagsOptions.CacheExpirationInterval = TimeSpan.FromSeconds(5); }); }); builder.Services.Configure<GlobalOptions> (builder.Configuration.GetSection( "DynamicConfiguration:GlobalOptions")); builder.Services.AddControllersWithViews(); var app = builder.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); } // Middleware to refresh configuration app.UseAzureAppConfiguration(); app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); app.Run();基本上,我们对 Program.cs 文件进行了三项更改:
-
using Microsoft.FeatureManagement:添加添加 SDK 的切换管理功能的必要命名空间。
-
builder.Services.AddFeatureManagement():向应用程序的依赖注入容器中添加服务。
-
.UseFeatureFlags:我们更改了与 Azure App Configuration 的连接设置,告知我们将使用切换管理功能。此外,使用featureFlagsOptions.CacheExpirationInterval标准定义了 5 秒的缓存。
-
作为 Azure App Configuration SDK 的一部分,我们可以直接在控制器或服务的代码中使用切换,使用IFeatureManager接口、FeatureGate属性或直接在视图中使用标签助手。对于此示例,我们将使用标签助手。
使用 IFeatureManagement 和 FeatureGate 属性
在某些情况下,我们可以通过IFeatureManagement接口分析切换是否已激活,该接口注入到类中,并提供测试值的方法,如下所示:
public class MyController : Controller
{
private readonly IFeatureManager _featureManager;
public MyController(IFeatureManager featureManager)
{
_featureManager = featureManager;
}
private async Task MyMethod()
{
if (await _featureManager
. IsEnableAsync("FeatureToggleName"))
{
Console.WriteLine("New Approach");
}
else
{
Console.WriteLine("Legacy Approach");
}
}
}
以同样的方式,可以通过FeatureGate属性注释操作或控制器:
using Microsoft.FeatureManagement.Mvc;
[ FeatureGate("FeatureToggleName)]
public class MyController : Controller
{
// ....
}
这样,就可以从使用具有不同实现但相同概念的切换管理中受益。
让我们将应用程序更改为使用标签助手,该助手将使用功能切换。按照以下步骤操作:
-
在 Visual Studio Code 中,编辑视图/_ViewImports.cshtml文件,并在现有代码下方添加以下代码:
@addTagHelper *, Microsoft.FeatureManagement .AspNetCore此代码添加了来自 Azure App Configuration SDK 的标签助手。
-
然后,打开视图/首页/Index.cstml文件,并使用以下代码:
<div class="text-center"> <h1 class="display-4">Welcome @ViewData["Title"]</h1> <p>Learn about <a href="https://learn.microsoft.com/aspnet/core"> building Web apps with ASP.NET Core</a>.</p> </div> <feature name="NewFeature"> <div style="background-color: silver; border: dotted 1px #000000"> <h3>New Feature using Toggles and the Azure App Configuration</h3> </div> </feature>注意功能标签的使用,它将基本上获取有关切换是否启用的信息。如果是,新的div标签将显示在屏幕上。
现在应用程序中已经配置了一切,让我们在 Azure App Configuration 中添加功能切换。
-
打开 Azure 门户(
portal.azure.com)并访问上一节中创建的应用配置资源。 -
然后,点击功能管理器菜单,创建 | 功能标志,如图图 9 .13 所示:

图 9.13 – 添加新的功能标志
-
在下一屏幕上,将新功能标志字段设置为NewFeature值,其余保持默认值。
-
点击应用按钮。
将创建一个新的功能标志并将其禁用,如图图 9.14所示:

图 9.14 – 使用禁用功能标志的应用程序
我们故意这样做配置。现在,再次使用dotnet run命令运行应用程序,通过应用程序目录中的终端执行,你会看到应用程序没有变化。新Div标签没有显示的原因是我们将切换创建为Enabled=false。
- 保持应用程序运行,再次访问 Azure 门户,通过在网格中点击启用列来启用切换,如图图 9.15所示:

图 9.15 – 启用功能标志
- 再次访问应用程序,我们可以在图 9.16中看到页面上已添加了一个新的 HTML 元素:

图 9.16 – 应用程序在运行时行为改变
通过理解和实现动态配置和功能切换,我们可以创建健壮、灵活和响应式的 ASP.NET Core 9 应用程序。
在本章中,我们已使用 Azure App Configuration 作为功能标志管理器。然而,ASP.NET Core 9 与本章学习的技术相同,与其他类型的切换管理服务器有集成。
配置管理技术和功能标志的组合对于不同的应用程序上下文非常强大,主要是在通过自动化机制持续交付价值的情况下与云资源交互。
我们将在下一章讨论如何使用自动化流程在云环境中托管我们的应用程序。
摘要
在本章中,我们深入探讨了通过理解 IConfiguration 接口的概念和抽象来管理应用程序配置的良好实践。我们还与 ASP.NET Core 9 配置提供程序相关的概念一起工作,以及实现选项模式。最后,我们使用 Microsoft Azure 的 Azure App Configurator 实时更改应用程序配置和行为,以实现功能标志或功能切换的概念。与云资源一起工作是软件工程师的重要前提,在下一章中,我们将探讨如何在云环境中部署应用程序。
第四部分:托管、部署和准备云环境
现代应用是动态的,作为软件工程师,我们的工作并不仅仅在同步最新开发的代码后结束。在市场不断变化的场景中,应用需要足够动态,以满足不断的市场需求。因此,开发团队必须适应围绕现代开发模型的新主题。ASP.NET Core 9 准备提供适合云环境的高质量解决方案。在本部分,我们将通过理解和实施在云环境中支持自动化管道(如持续集成(CI)和持续交付(CD))的应用发布流程,了解涉及解决方案持续交付的各个方面。我们将了解云原生思维是什么,以及如何将我们的解决方案引导至不断变化的市场。
本部分包含以下章节:
-
第十章 ,部署和托管应用
-
第十一章 ,使用 ASP.NET Core 9 进行云原生开发
第十章:部署和托管应用程序
在遵循良好的开发流程、实施良好实践并覆盖应用程序所需的所有功能之后,将其发布到环境中是必要的。为了成功完成此操作,了解超越源材料的各种概念、实践和应用打包模型非常重要。在本章中,我们将讨论不同的应用程序托管和部署方法,以及理解与持续集成和持续部署(CI/CD)和容器相关的概念。
ASP.NET Core 9 提供了一个强大的配置系统,允许开发者有效地管理配置和行为。本章将探讨应用程序配置的重要性,如何使用配置系统来管理它们,以及如何在运行时使您的应用程序具有适应性。
在本章中,我们将涵盖以下主题:
-
准备发布您的应用程序并本地托管
-
在云环境中发布解决方案
-
理解 Docker 原则以及如何将应用程序打包到容器中
-
理解 DevOps 方法与 CI/CD
技术要求
为了完成本章,以下工具必须存在于您的开发环境中:
-
Azure 订阅:在本章中,我们将创建 Microsoft Azure 中的资源。为此,如果您还没有,您将需要一个 Azure 订阅,以便您能够访问该平台。您可以在
azure.microsoft.com/en-us/free注册一个带有有限信用额的订阅,以了解本章中介绍的概念。 -
Docker Hub 账户:您需要在 Docker Hub 网站上创建一个账户,网址为
hub.docker.com。 -
Azure 工具扩展:您需要从
marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-node-azure-pack安装 Visual Studio Code 的 Azure Tools 扩展。安装完成后,使用您的 Azure 凭据登录。
本章的代码示例可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/ASP.NET-Core-9.0-Essentials/tree/main/Chapter10。
准备发布您的应用程序并本地托管
发布应用程序是软件开发生命周期中的一个自然过程。在开发和测试之后,下一步是将应用程序提供给用户。这涉及到创建可部署的应用程序版本,并在可以访问和使用的环境中对其进行配置。
让我们了解在遵循各种策略的同时发布应用程序的细节,从手动到自动化的 CI/CD。但在我们这样做之前,让我们了解一些基本概念,例如发布过程的重要性。
发布过程的重要性
发布过程将您的应用程序从源代码转换为可部署的格式。此过程确保所有依赖项、配置和编译的代码打包在一起,使得在不同环境中部署和运行应用程序更加容易。
ASP.NET Core 9 中的发布过程涉及三个步骤:
-
恢复包。
-
编译应用程序。
-
生成可发布的包。
生成的发布包可能因开发的应用程序类型而异。它可能包含不同的文件,例如以下内容:
-
应用程序的 DLL 文件
-
第三方依赖项和库
-
静态文件(例如,JavaScript、CSS 和图片)
-
配置文件(例如,appsettings.json)
-
独立部署的可执行文件
要了解此过程的工作原理,请按照 技术要求 部分提供的说明从本书的 GitHub 仓库下载示例应用程序。这将是本章的基础。
现在,让我们了解如何生成发布包。
生成发布包
我们在本书中广泛使用 .NET 平台 CLI 工具来创建和运行应用程序。CLI 工具还有一个特定命令用于为 ASP.NET Core 9 项目生成发布包。
publish 命令提供了各种选项,允许我们配置发布包的输出,如 表 10.1 中所示:
| 选项 | 描述 | 示例 |
|---|---|---|
| < 项目文件 > | 指定要操作的项目文件。如果未指定,则默认为当前目录。 |
dotnet publish project_name.csproj
|
| -c,--configuration | 定义构建配置(Debug 或 Release)。默认为 Debug。 |
|---|
dotnet publish –c Release
|
| -f,--framework | 指定目标框架 – 例如,net8.0。 |
|---|
dotnet publish –f net8.0
|
| --runtime | 为特定运行时发布应用程序(例如,win-x64、linux-x64 或 osx-x64)。 |
|---|
dotnet publish –runtime linux-x64
|
| -o,--output | 指定发布文件的输出目录。 |
|---|
dotnet publish –o ./publish
|
| --self-contained | 将应用程序作为自包含部署发布,包括 .NET 运行时。 |
|---|
dotnet publish –self-contained
|
| --no-restore | 在发布操作期间禁用恢复项目依赖项的能力。假设恢复已经完成。这在 CI/CD 管道中很有用。 |
|---|
dotnet publish –no-restore
|
| --manifest | 指定一个或多个目标清单,以计算包含在发布输出中的包集。 |
|---|
dotnet publish –manifest ProjectManyfest.xml
|
| -- version-suffix | 设置在构建项目时使用的 $(VersionSuffix) 属性值。对于预发布版本很有用。 |
|---|
dotnet publish –version-suffix beta1
|
表 10.1 – dotnet publish CLI 工具选项
每个选项的使用将取决于每个场景。在我们的上下文中,我们将使用
现在,让我们为 UrlShortener 应用程序生成一个发布包。为此,打开您的终端或 bash,访问之前下载的应用程序目录,并执行以下命令:
dotnet publish UrlShortener.csproj -c Release -o ./published
前面的命令将在发布文件夹中生成发布包。然而,了解-c参数的详细信息非常重要:
-
-c 选项指定构建配置,通常有两种主要配置:调试和发布:
-
调试配置包含额外的调试信息,并针对调试进行了优化。当使用 dotnet run 命令在本地运行项目时,默认配置是调试,这允许执行调试过程。
-
发布配置针对性能进行了优化,不包含调试信息。通常用于将应用程序部署到生产环境。
-
当访问发布文件夹时,我们必须模拟目录/文件结构:
publish/
├── appsettings.Development.json
├── appsettings.json
├── Azure.Core.dll
├── Azure.Identity.dll
├── Microsoft.Bcl.AsyncInterfaces.dll
├── Microsoft.Data.SqlClient.dll
├── Microsoft.EntityFrameworkCore.Abstractions.dll
├── Microsoft.EntityFrameworkCore.dll
├── Microsoft.EntityFrameworkCore.Relational.dll
├── Microsoft.EntityFrameworkCore.SqlServer.dll
├── Microsoft.Identity.Client.dll
├── Microsoft.Identity.Client.Extensions.Msal.dll
├── Microsoft.IdentityModel.Abstractions.dll
├── Microsoft.IdentityModel.JsonWebTokens.dll
├── Microsoft.IdentityModel.Logging.dll
├── Microsoft.IdentityModel.Protocols.dll
├── Microsoft.IdentityModel.Protocols.OpenIdConnect.dll
├── Microsoft.IdentityModel.Tokens.dll
├── Microsoft.SqlServer.Server.dll
├── Microsoft.Win32.SystemEvents.dll
├── runtimes/
│ ├── (runtime-specific files and directories)
├── System.Configuration.ConfigurationManager.dll
├── System.Drawing.Common.dll
├── System.IdentityModel.Tokens.Jwt.dll
├── System.Memory.Data.dll
├── System.Runtime.Caching.dll
├── System.Security.Cryptography.ProtectedData.dll
├── System.Security.Permissions.dll
├── System.Windows.Extensions.dll
├── UrlShortener
├── web.config
└──wwwroot/
├── (static files like css, js, images)
如我们所见,有几个 .dll 文件,这些是应用程序使用的依赖项,除了静态文件如 wwwroot 和配置文件。
该文件夹的内容正是应该在一个环境中发布的,无论是本地还是通过云提供商。
通过在终端中访问发布目录并执行以下命令,可以运行应用程序:
dotnet UrlShorterner.dll
UrlShorterner.dll 文件是一个 ASP.NET Core 9 应用程序的可执行文件。由于您的开发机器上已安装 .NET SDK,因此可以通过 .dll 文件在您的开发机器上运行应用程序。SDK 不应安装在将运行应用程序的服务器上。为此,您只需安装 .NET 运行时 。
.NET 运行时
.NET 运行时是由微软开发的一个软件框架,它为运行 .NET 应用程序提供了一个托管执行环境。它包括运行 .NET 程序、管理内存、处理异常和收集垃圾所需的组件。.NET 运行时通常安装在运行特定 .NET 应用程序的服务器和机器上。与 .NET SDK 不同,.NET 运行时具有运行应用程序的功能,而不是构建和开发它们。要了解更多关于 .NET 运行时的信息,请访问 learn.microsoft.com/en-us/dotnet/core/introduction 。
.NET 平台和 ASP.NET Core 9 是可移植的,这意味着它们可以在安装了 SDK 或.NET 运行时的不同操作系统上运行。表 10.2显示了在最重要的每个操作系统上可以使用的应用程序服务器:
| 操作系统 | 先决条件 |
|---|---|
| Linux |
-
.NET SDK/Runtime, Kestrel
-
可选:Nginx/Apache,托管套餐
|
| macOS |
|---|
-
.NET SDK/Runtime, Kestrel
-
可选:Nginx/Apache
|
| Windows |
|---|
-
.NET SDK/Runtime, Kestrel
-
可选:IIS、NGINX、HTTP.sys、ASP.NET Core 托管套餐
|
| 表 10.2 – ASP.NET Core 9 应用程序的 Web 服务器选项
使用 CLI 工具生成发布包的过程很简单。我们将遵循此过程,并通过 CI/CD 自动化,我们将在理解 CI/CD 的 DevOps 方法部分进行介绍。
现在我们已经学会了如何生成发布包,是时候学习如何在云环境中发布它们了。
在云环境中发布解决方案
使用 ASP.NET Core 9 开发现代应用程序的优势不仅在于能够使用实现最佳实践的能力,还在于需要向应用程序用户提供高质量解决方案。
到目前为止,我们已经了解了使用.NET 平台各种功能的重要性,例如使用.NET CLI 编译应用程序和安装支持工具,如 Entity Framework Core。我们还了解了生成可在本地和任何使用.NET 运行时的环境中运行的发布包的过程。
在撰写本文时,不工作在云环境中是不可能的。这为我们提供了诸如弹性、可用性、安全性以及许多其他简化通过持续交付价值的过程来部署、维护和演进应用程序的好处。
要了解如何在云环境中发布解决方案,我们将使用 Azure 作为我们的云提供商。
将 ASP.NET Core 9 应用程序迁移到 Azure 允许开发者利用这种云提供商提供的动态功能,同时关注应用程序上下文和业务目标。Azure 通过其多样化的资源和服务,允许应用程序处理各种工作负载,保持对用户的可用性,并保护免受安全威胁。
基于我们之前工作的UrlShortener应用程序以及生成可发布包的过程,我们将实现 Azure 的好处并将此应用程序发布在云环境中。
创建 Azure 应用程序服务和数据库资源
Azure 环境为发布应用程序提供了不同类型的资源,这些资源在不同的服务级别和不同的发布方法中有所不同。
对于这个应用程序,我们将使用一个名为 App Service 的资源,这是 Azure 提供的 平台即服务 ( PaaS ) 产品,它允许我们专注于我们的应用程序。App Service 提供了一个出色的应用程序服务器,同时为我们提供了访问已发布应用程序的 URL。
您必须根据 技术要求 部分提供的信息准备您的环境。
应用程序服务和 PaaS
Azure App Service 是一个完全管理的 PaaS,允许开发者在他们选择的编程语言中构建、部署和扩展 Web 应用程序、移动后端和 RESTful API,而无需进行基础设施管理。App Service 提供了一个准备好的服务器,提供运行您的应用程序所需的运行时。PaaS 方法是一种云计算模型,它提供了一个完整的开发部署环境在云中,使开发者从处理基础设施的需求中解放出来。有关更多详细信息,请参阅 Azure App Service 文档:docs.microsoft.com/en-us/azure/app-service/。
我们的目标不会是耗尽 Azure 环境中所有可用的资源选项,因为需要一本专门介绍这个主题的书。
相反,我们将专注于使用 Azure App Service 作为 Web 服务器发布 UrlShortener 应用程序。
按照以下步骤发布应用程序:
-
访问 Azure 门户 (
portal.azure.com)。 -
在主屏幕上,点击 创建资源 按钮,如图 图 10.1 所示:

图 10.1 – 创建新的 Azure 资源
- 然后,在 Web App 资源下选择 创建,如图 图 10.2 所示:

图 10.2 – 创建新的 Web App 资源
- 在新的 Web App 资源的 基本创建 屏幕上,填写 表 10.3 中提供的信息:
| 参数 | 值 | 描述 |
|---|---|---|
| 订阅 | 选择您的订阅。 | 订阅是上线资源和相关成本所必需的。 |
| 资源组 | 在这里,值是 rg-aspnetcore8。如果此资源组不存在,请点击 资源组 字段下方的 创建新链接 并创建它。 | 资源是 Azure 中资源的逻辑组。 |
| 名称 | urlshortener.<您的 姓氏> | 此参数将是您应用程序的 URL。请保持其唯一性。 |
| 发布 | 代码 | App Service 有不同的方式来托管和发布应用程序。在这种情况下,我们使用 代码 选项,因为我们将会发布生成的包。 |
| 运行时堆栈 | .NET 9 (LTS) | 此参数定义将在 App Service 上托管的应用程序类型。它支持 .NET、Node.js、Java、PHP 和 Python。 |
| 操作系统 | Linux. | Linux 是许多用例的良好选择。然而,对于这个练习,你也可以选择 Windows。操作系统的定义取决于应用程序的需求。 |
| 区域 | EastUS 2 | App Service 将托管的应用程序所在的区域。我们使用 East US 2 是因为我们将创建的数据库在 East US 区域不可用。 |
| Linux 平台 | 保持原样。 | 我们将创建一个新的服务计划来托管应用程序。服务计划是应用服务的一个重要组成部分。根据定价计划,服务计划可以托管多个应用程序。 |
| 定价计划 | 基本 B1 . | 基本 B1 选项对于这个例子来说已经足够了。请记住,如果你选择不同的定价计划,可能会产生更高的费用。 |
| 区域冗余 | 禁用。 | 此参数用于需要高可用性配置的生产环境中的应用程序。 |
表 10.3 – 新 Web 应用资源的基本参数
-
点击 下一步:数据库 > 按钮。
-
选择 创建数据库 选项,并按照 图 10 .3 中的说明填写参数:

图 10.3 – 数据库服务器配置
在这个阶段,我们正在配置数据库,这对于 URLShortener 应用程序是必要的。我们将使用 Azure SQL Server 资源,因为它提供了一个可以托管不同数据库的服务器。在这个例子中,我们只使用一个数据库。但是,如果需要,以后可以添加其他数据库。
- 现在,点击 监控 + 安全 选项卡,并将 启用 Application Insights 选项设置为 否 。
此选项旨在为应用程序创建一个监控资源。这是一个最佳实践,特别是对于生产资源。因此,在这个阶段,目标是发布应用程序;目前不需要监控,这可以在以后添加。
-
接下来,点击 审查和创建 。然后,点击 创建 并等待资源创建完成。
-
在审查屏幕上,在 数据库 部分中,你会看到用户名和密码信息,如图 图 10 .4 所示。复制这些详细信息并妥善保管;我们稍后会需要密码来配置数据库:

图 10.4 – 数据库凭据
-
最后,点击 创建 并等待资源创建完成。
-
一旦资源创建完成,点击如图 图 10 .5 所示的 转到资源 按钮:

图 10.5:资源创建屏幕
你将被重定向到之前创建的 应用服务 设置摘要屏幕。
如 图 10 .6 所示,你将能够看到可用的 默认域名 URL,以便你可以访问应用程序:

图 10.6 – 创建的应用程序服务的默认域名 URL
默认域名URL 是 Azure 根据创建应用程序服务时定义的参数自动创建的,可以使用自定义域名进行自定义。我们将保持 URL 可用并运行。点击 URL 后,您将被重定向到一个包含类似图 10.7所示信息的页面:

图 10.7 – 新应用程序服务的默认网站内容
到目前为止,服务器已启动并运行,但我们必须在 Azure 上发布应用程序。然而,在发布应用程序之前,让我们先配置数据库。
配置 Azure SQL 服务器
在创建我们的应用程序服务时,还创建了其他资源,例如数据库服务器和数据库。
我们需要配置数据库,以便我们能够持久化应用程序中使用的 URL 表。
我们将使用 Entity Framework Core 工具以与本地相同的方式更新数据库。然而,由于安全原因,Azure 自动创建的数据库服务是公开不可访问的。
要做到这一点,我们必须提前进行一些配置更改,以便我们可以操作数据库。按照以下步骤操作:
-
前往之前创建的资源组 – 即rg-aspnetcore8。您将能够看到创建的资源列表。
-
搜索UrlshortenerDB资源并访问它。
-
然后,在设置菜单组中,单击连接字符串。
-
连接字符串应如下所示:
Server=tcp:urlshortener-db-server-tanure.database.windows.net,1433;Initial Catalog=UrlshortenerDB;Persist Security Info=False;User ID=urlshortener-db-server-tanure-admin;Password={your_password}; MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30; -
注意Password={your_password}参数。在创建应用程序服务时,将此参数替换为您复制的密码。
-
从ADO.NET (SQL Authentication)字段复制连接字符串。
-
现在,打开URLShortener应用程序的appsettings.json文件,将DefaultConnection属性更改为您之前复制的连接字符串。结果将如下所示:
"Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "ConnectionStrings": { "DefaultConnection": "Server=tcp:urlshortener-db-server-tanure.database.windows.net,1433;Initial Catalog=UrlshortenerDB;Persist Security Info=False;User ID=urlshortener-db-server-tanure-admin;Password=XL6l61uv9t4$K4Q6;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;" }, "AllowedHosts": "*" } -
保存appsettings.json文件。
-
现在,让我们配置对数据库服务器的访问。为此,返回到rg-aspnetcore8资源组,并单击urlshortener-db-server<your last name>资源。
-
从主菜单转到安全 | 网络,然后单击添加您的客户端 IPv4 地址选项,如图图 10.8所示:

图 10.8 – 添加防火墙规则以访问 IPv4 地址的私有数据库
此配置将创建一个规则,使得数据库只能通过其当前 IP 地址访问。请注意,如果您的 IP 地址更改,您将必须再次执行这些步骤以添加新的 IP 地址。
- 最后,单击保存。
通过这种方式,我们已经为托管在 Azure 上的数据库连接字符串配置了应用程序,并添加了防火墙规则,以便可以通过我们当前的 IP 地址访问数据库。
现在,我们需要更新数据库。为此,访问您的操作系统终端并导航到 URLShortener 项目目录。然后,运行以下命令:
dotnet ef database update
等待过程完成。要检查表是否正确创建,请在 Azure 门户(portal.azure.com)中访问 rg-aspnetcore9 资源组,然后访问 UrlShortenerDB 资源。点击 查询编辑器 菜单并使用创建应用程序服务时提供的凭据访问数据库。
如 图 10.9 所示,新表已创建:

图 10.9 – 使用 Entity Framework Core 迁移创建的数据库表
这就是使用 Entity Framework Core 迁移的好处之一。通过这样做,可以应用到本地数据库或服务器上的更改,并保持与应用程序代码的一致性。
现在数据库已配置,是时候发布应用程序了。我们将通过 Visual Studio Code 来完成这项操作。
使用 Visual Studio Code 发布应用程序
在配置了在 Azure 上托管应用程序的所有先决条件后,现在是时候发布应用程序了。
通过 Visual Studio Code 或甚至 Visual Studio 发布的过程相当简单。
在 技术要求 部分,建议您安装和配置 Azure Tools 扩展 (https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-node-azure-pack )。此扩展使手动发布过程更加容易。
按照以下步骤发布我们在之前创建的应用服务中的应用程序版本:
-
访问 UrlShortener 应用程序目录并运行以下命令:
code . -
然后,点击 Visual Studio Code 中的 Azure Tools 扩展图标,如图 图 10.10 所示:

图 10.10 – Visual Studio Code 中的 Azure Tools
将显示您用户可用的订阅列表。通过展开订阅,我们将能够看到为 UrlShortener 应用程序创建的应用服务资源以及资源。
- 现在,右键单击 urlshortener-<你的姓氏> 应用程序,并选择 部署到 Web 应用… 选项,如图 图 10.11 所示:

图 10.11 – 使用 Visual Studio Code 中的 Azure Tools 扩展部署 Web 应用
-
您将需要提供一些配置细节,以便您可以部署。只需确认提供的选项,然后等待部署过程完成。
-
发布过程完成后,将显示类似于图 10 .12 中所示的通知:

图 10.12 – 表示应用程序部署已完成的通知
- 您可以通过点击浏览网站按钮导航到已发布的网站。您将看到类似于图 10.13 .13* 中所示的结果:

图 10.13 – 在 Azure 上运行的 URL 缩短器应用程序
现在,您可以直接在您的 Azure 环境中公开使用包含短网址功能的应用程序。
Azure 工具扩展通过在后台运行以下过程来自动化发布新应用程序版本的过程:
-
恢复包
-
编译应用程序
-
生成发布包
-
将发布包压缩成. zip格式
-
连接到 Azure 环境
-
部署包含发布包的 ZIP 文件
-
解压 ZIP 文件
这些步骤在您每次选择使用 Azure 工具扩展部署时都会执行。我们将在本章的理解 DevOps 方法中的 CI/CD部分了解另一种在 C/ICD 模型中发布包的方法。
在云环境中托管解决方案是当今的一项必要活动,并且有几种资源可以帮助我们完成这项任务。
每个托管资源和服务层的定义将取决于应用程序需求以及团队的知识水平。有些情况下,我们可以使用允许我们在不同环境中托管应用程序的策略,这种方式对云提供商的具体资源是无关紧要的。
为了实现这一点,云原生应用程序中常用的一种策略是容器策略。我们将在下一节中了解这一点。
理解 Docker 原则以及如何在容器中打包应用程序
现在,使用容器策略已经成为一项基本要求,特别是对于云原生应用程序。容器为开发、测试和部署提供了一个一致的环境,确保应用程序无论部署在哪里都能顺利运行。使用容器时,我们实际上拥有在给定环境中运行应用程序所需的一切,无需安装额外的包或运行时。
这样,我们就有了一致性,这对于像 Azure 或其他任何云提供商这样的云环境至关重要,因为它们提供强大的服务来管理和扩展容器化应用程序。作为一个类比,容器策略有助于对抗产生“在我的机器上”这种说法的行为
这个说法是正确的,因为开发环境拥有运行应用程序所需的一切。然而,无论在何种环境中运行应用程序都能敏捷地交付价值,并且没有运行应用程序的依赖性,这是容器的一个巨大优势。在我们创建容器之前,让我们先了解它们是什么。
理解什么是容器
容器是一个包含运行软件所需一切的自包含可执行软件包。容器彼此之间以及与主机系统隔离,提供一致的运行时环境。这种隔离确保了应用程序的行为在它运行的任何环境中都是相同的。
容器提供了一种与传统虚拟(VMs)提供的虚拟化类型不同的虚拟化。
虚拟机
虚拟机是物理计算机的软件模拟。每个虚拟机运行一个完整的操作系统,包括其自己的内核,并模拟操作系统所需的全部硬件。虚拟机在虚拟机管理程序上运行,该管理程序管理单个物理主机上的多个虚拟机。
图 10 .14 展示了容器和虚拟机之间的一些差异:

图 10.14 – 容器和虚拟机之间的差异
如我们所见,容器在执行时并不依赖于完整的操作系统,而仅依赖于共享机器资源的运行时,例如网络和处理。然而,它们是独立且隔离地执行的。
容器提供了几个好处:
-
便携性:容器封装了所有依赖项,使得在不同环境中移动应用程序变得容易,而不会出现兼容性问题。
-
一致性:它们确保应用程序在开发、测试和生产环境中运行的一致性。
-
可扩展性:它们可以轻松地扩展或缩减以处理不同的负载,这使得它们非常适合云环境。
-
效率:容器有效地共享内核和主机系统资源,与传统的虚拟机相比,具有更低的开销。
要运行容器,需要使用运行时来管理它,就像 ASP.NET Core 9 应用程序一样。最著名的容器运行时是Docker。我们将在下一节中了解其基础知识。
理解 Docker 基础知识
Docker 是一个开源平台,它自动化了部署、扩展和管理容器化应用程序的过程,提供了一种简单而强大的方式来构建、运输和操作容器。
Docker 提供了三个组件来管理容器:
-
Docker Engine:管理容器的运行时
-
Docker CLI:用于与 Docker 交互的命令行界面
-
Docker Hub:一个基于云的注册服务,用于共享和存储 Docker 镜像,类似于我们如何使用 GitHub 来管理源代码存储库
通过这些工具,Docker 提供了允许我们操作涉及容器开发策略的组件的机制。在这种情况下,容器具有以下组件:
-
镜像:镜像可以比作您应用程序当前版本的快照,包括运行应用程序所需的一切。镜像是容器的基础,并使用Dockerfile创建,其中包含构建镜像的指令集。
-
容器:容器是镜像的运行实例。我们可以将镜像比作一个类,将容器比作这个类的实例。容器由镜像创建并在 Docker Engine 上运行。每个容器与其他容器隔离,并拥有自己的文件系统、CPU、内存和进程空间。
-
Dockerfile:Dockerfile 是一个包含一系列构建 Docker 镜像指令的文本文件。它指定了要使用的基镜像、应用程序代码、依赖项以及配置环境所需的任何命令。
-
容器注册库:容器注册库是用于存储和分发镜像的仓库。Docker Hub 是一个流行的公共注册库,尽管还有像 Azure Container Registry 这样的私有注册库可用。
Docker 组件及其容器结构之间的关系在图 10.15中显示:

图 10.15 – Docker 及其容器组件之间的关系
如前所述,Docker 是最常用的容器解决方案,但还有其他类型的运行时可用,它们实现了这里介绍的概念。
表 10.4 解释了市场上可用的某些容器运行时:
| 运行时 | 描述 | 网站 |
|---|---|---|
| containerd | Docker 和 Kubernetes 使用的核心运行时。它侧重于简单性和可移植性。 | containerd.io/ |
| CRI-O | 一种轻量级运行时,用于 Kubernetes,实现了 Kubernetes 容器运行时 接口(CRI)。 | cri-o.io/ |
| runc | 根据 OCI 规范启动和运行容器的 CLI 工具。 | github.com/opencontainers/runc |
| Podman | 一种无守护进程的容器引擎,与 Docker 兼容。 | podman.io/ |
| LXC | 一种传统的容器运行时,提供类似虚拟机的体验。 | linuxcontainers.org/ |
| Kata Containers | 此运行时通过运行轻量级虚拟机,结合了虚拟机的安全性和容器的速度。 | katacontainers.io/ |
| Rancher | Rancher Desktop 提供容器和 Kubernetes 管理。 | rancherdesktop.io/ |
表 10.4 – 容器运行时选项
如我们所见,在环境中运行容器有多种选择,每种解决方案的使用将取决于每个应用程序和组织的需求。然而,值得记住的是,容器包括镜像、其他容器、Dockerfile 和容器注册表,所有这些都被运行时使用。
到目前为止,我们可以将 UrlShortener 应用程序打包到容器中。
打包 UrlShortener 应用程序
Docker 引擎为我们提供了不同类型的资源和命令来管理容器。我们将关注 UrlShortener 应用程序的打包过程,并使用容器的主要功能,包括镜像、容器和容器注册表。
在继续之前,请确保您已按照 技术要求 部分安装了 Docker 引擎。
我们的打包过程将通过以下流程进行:

图 10.16 – 容器创建流程
在 Visual Studio Code 中,务必打开 UrlShortener 应用程序并执行以下步骤:
-
在目录的根目录下,创建一个名为 Dockerfile 的文件。此文件没有扩展名。
-
将以下内容添加到文件中:
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base WORKDIR /app EXPOSE 8080 FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY ["UrlShortener.csproj", "MyApp/"] RUN dotnet restore "MyApp/UrlShortener.csproj" COPY . ./MyApp WORKDIR "/src/MyApp" RUN dotnet build "UrlShortener.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "UrlShortener.csproj" -c Release -o /app/publish FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "UrlShortener.dll"] -
保存文件。
前面的代码有点长,看起来有些复杂,所以让我们理解一下我们的 Dockerfile 中的每一行:
-
基础镜像和初始配置:在这个阶段,我们正在配置一个基础镜像,其中包含运行 ASP.NET Core 9 应用程序默认配置。
-
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base:此行指定最终容器的基镜像,它使用来自 Microsoft Container Registry(MCR)的 ASP.NET Core 8.0 运行时镜像。所有 Docker 镜像都以对基镜像的引用开始。AS base 标签将此阶段命名为 base。
-
WORKDIR /app:此行将容器内部的工作目录设置为 /app。所有后续命令都将在此目录中执行。
-
EXPOSE 8080:此行告诉 Docker 容器在运行时将监听端口 8080。这用于文档目的,以及在运行容器时配置端口映射。
-
构建阶段:构建阶段将是一个负责编译应用程序的镜像。它将使用已经安装了 .NET SDK 的镜像作为基础。
-
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build:此行使用来自 MCR 的 .NET SDK 8.0 镜像版本来构建应用程序。AS build 标签将此阶段命名为 build。
-
WORKDIR /src:将工作目录设置为 /src。
-
COPY ["UrlShortener.csproj", "MyApp/"]:将 UrlShortener.csproj 项目文件复制到容器中的 MyApp/ 目录。
-
RUN dotnet restore "MyApp/UrlShortener.csproj":恢复 UrlShortener.csproj 文件中指定的项目依赖项。
-
COPY . ./MyApp : 将主机当前目录中的所有文件复制到容器中的 MyApp 目录。
-
WORKDIR "/src/MyApp" : 将工作目录设置为 /src/MyApp。
-
RUN dotnet build "UrlShortener.csproj" -c Release -o /app/build : 在 Release 配置下构建项目,并将构建结果输出到 /app/build 目录。
-
发布阶段 : 在此阶段,生成发布包。
-
FROM build AS publish : 这行使用构建阶段作为发布阶段的基础。
-
RUN dotnet publish "UrlShortener.csproj" -c Release -o /app/publish : 这行发布项目,意味着它编译应用程序,复制所有必要的文件,并在 /app/publish 目录中生成可部署的应用程序版本。
-
最终阶段 : 最终阶段将之前配置生成的包在包含基础镜像设置的镜像上运行。
-
FROM base AS final : 使用基础阶段作为最终镜像的基础。
-
WORKDIR /app : 将工作目录设置为 /app。
-
COPY --from=publish /app/publish . : 将发布阶段的 /app/publish 目录内容复制到最终阶段的当前目录(/app)。
-
ENTRYPOINT ["dotnet", "UrlShortener.dll"] : 设置容器的入口点,以便可以运行 dotnet UrlShortener.dll 命令,该命令启动 ASP.NET Core 应用程序。
-
如前所述,Dockerfile 文件中可用的代码使用多个阶段来生成应用程序的镜像。
Docker 构建阶段是 Docker 多阶段构建过程中的一个阶段,其中应用程序被编译,依赖项被还原,所有必需的文件都准备就绪以供部署。在多阶段构建中,每个阶段可以使用不同的基础镜像和环境来执行特定任务。
构建阶段通常使用开发镜像或 SDK 来编译和构建应用程序,生成后续阶段可用的输出工件。这种方法有助于您创建一个干净、优化的最终镜像,该镜像仅包含运行时依赖项和应用程序本身,不包含构建工具或中间文件。
Docker 多阶段构建
Docker 的多阶段构建过程允许您生成优化后的镜像,并使用容器技术来编译和生成可发布的应用程序。要了解更多信息,请访问docs.docker.com/build/building/multi-stage/。
在观察多阶段方法的流程时,我们可以通过执行我们之前了解的包创建步骤来自动化生成 Docker 图像的过程。在没有使用多阶段过程的情况下也可以生成 Docker 图像。在这种情况下,需要手动编译和生成应用程序包,然后稍后只需将生成的包复制到图像中。这会导致一个类似于以下内容的Dockerfile文件:
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 8080
COPY ./published .
ENTRYPOINT ["dotnet", "UrlShortener.dll"]
通过这样做,Dockerfile文件将肯定更简单。然而,在生成图像之前,必须执行docker build和docker publish命令,以便我们可以生成将要复制到图像中的应用程序包。
现在我们已经了解了生成Dockerfile文件的原则,是时候生成一个图像了。
生成容器图像
要生成 Docker 图像,需要运行docker build命令。此命令将执行Dockerfile文件中描述的代码。
要这样做,请打开您的终端,在UrlShortener应用程序目录中,执行以下命令:
docker build -t urlshortener:1.0 .
前一个命令使用-t参数编译图像,并将其标记为urlshortener:1.0。图像标签作为要生成的图像的名称,其形式为<name_lower_case>[:
版本是可选的。如果没有输入,Docker 将使用最新版本。然而,为图像定义一个版本是良好的实践。
定义图像标签后,编译过程将要执行的环境被告知这一点。环境由.参数定义,该参数指示包含Dockerfile文件的本地目录。重要的是要记住,build命令需要知道Dockerfile文件的位置,以便正确执行。
一旦图像生成过程运行完毕,运行以下命令以查看您计算机上的图像列表:
docker images
执行前一个命令后,您将能够查看本地容器注册库中的图像列表,如图图 10.17所示:

图 10.17 – 本地容器注册库中的 Docker 图像列表
在此示例中,有三个图像:
-
localhost/urlshortener:1.0:这是之前生成的包含应用程序的图像。图像名称前的localhost/前缀表示图像的所有者——在本例中是本地注册库。
-
mcr.microsoft.com/dotnet/sdk:8.0和mcr.microsoft.com/dotnet/aspnet:8.0:这些图像分别代表用于在容器中编译和运行应用程序的.NET SDK 和.NET 运行时。这些图像自动从 Docker Hub 下载。注意mcr.microsoft.com/dotnet前缀,它指的是图像的所有者。
此外,图 10.17 还显示了图像的大小(以 MB 为单位)。应用程序镜像比其他两个都要小。容器必须进行优化,因为它们可能会影响应用程序的性能和启动。
容器镜像应被视为不可变的——也就是说,每个版本都是唯一的。这意味着如果应用程序有任何更改,我们必须使用不同的标签生成一个新的镜像。遵循此约定是一种最佳实践,因为它允许您控制服务器上运行的容器。
生成图像后,是时候运行一个容器并测试其操作了。
运行 Docker 容器
要从先前生成的图像运行 Docker 容器,请运行以下命令:
docker run -d -p 8899:8080 localhost/urlshortener:1.0
让我们更仔细地看看这个命令:
-
它从 localhost/urlshortener:1.0 镜像运行一个新的容器。
-
它以分离模式(-d)运行容器,允许它在后台运行。
-
它将主机上的端口 8899 映射到容器内的端口 8080(-p 8899:8080)。这使得应用程序在容器内运行在端口 8080 上,可以通过主机上的端口 8899 访问。
成功执行命令的输出将是一个代表容器 ID 的密钥。要检查容器是否正在运行,请运行以下命令:
docker ps
将会显示类似于 图 10.18 的输出:

图 10.18 – 运行中的容器
现在,打开您选择的浏览器并访问 http://localhost:8899 。
连接字符串
如果应用程序的连接字符串已配置为 Azure 中的数据库,并且您通过容器访问应用程序时遇到连接错误,请确保您添加了您的 IP 地址,如 配置 Azure SQL Server 部分所示。您还可以通过使用与 docker run 命令结合的环境变量来更改连接字符串,如下所示:
docker run -d -p 8899:8080 -e ConnectionStrings__DefaultConnection="Server=.;Database=UrlShortenerDB;user id=sa; password=P4sword123;Encrypt=False;" localhost/urlshortener:1.0
前面的命令包含 -e < 环境变量>=<值> 参数。
我们可以为相同的镜像多次运行 docker run 命令,同时更改容器将在其上运行的宿主端口。例如,以下命令将在端口 9900 和 9910 上运行相同应用程序的两个新容器:
docker run -d -p 9900:8080 localhost/urlshortener:1.0
docker run -d -p 9910:8080 localhost/urlshortener:1.0
目前,在您的本地环境中,有三个不同的容器运行着相同的应用程序。
要完成运行容器,请执行以下命令:
docker stop <container ID>
您可以通过运行 docker ps 命令从正在运行的容器列表中获取容器 ID。
理解 Docker 原理以及如何将你的 ASP.NET Core 9 应用程序打包到容器中对于现代软件开发模型非常重要。容器提供了一致性、可移植性和效率,使它们非常适合云原生应用程序。
正如我们所学的,容器使用与图像等组件有关,这些图像是根据运行时和发布的应用程序生成的。后来,所使用的图像通过公共或私有容器注册库提供。最后,我们可以通过使用图像来运行已发布在容器注册库中的应用程序的不同实例,从而生成一个称为容器的执行应用程序。
接下来,我们将学习如何自动化在云环境中发布应用程序的过程。
理解 CI/CD 的 DevOps 方法
深入讨论 DevOps 文化是很常见的。尽管这个术语是两个特定领域的组合——即开发(Dev)和运维(Ops)——但这种方法远远超出了这两个团队。
DevOps 文化将流程、人员和工具连接起来,所有这些共同工作以生成价值,并在面对不断的市场需求时提供持续学习:

图 10.19 – DevOps 文化周期
图 10.19 表示了涉及 DevOps 文化的不同方面的这种持续协作流程。
在 DevOps 文化中建议的实践中,我们有 CI/CD 流程,使开发团队变得敏捷,消除依赖,最小化错误,并使团队能够持续进化和学习。
CI 和 CD 流程通常被称为管道,因为它们代表了一系列顺序指令,允许在这个流程中添加和重新组织新的管道或任务。
在我们查看管道开发模型之前,让我们了解 CI 和 CD 的基本原理。
CI
CI 是一种开发实践,开发人员定期将他们的代码更改合并到一个中央存储库中,之后会有自动构建和测试。
开发团队是远程分布的,需要一个中央存储库,通常基于 Git。当开发新的代码并更改时,它会同步或集成到本地存储库中。
此过程是异步的——也就是说,每个开发人员在不同时间将他们的代码版本与其他开发人员同步。然后,执行 CI 管道,其主要目标是提前检测集成问题,然后频繁地集成代码更改并通过自动化测试进行检查。
图 10.20 展示了一个基本的 CI 管道场景:

图 10.20 – CI 管道流程
如我们所见,图 10.20 展示了我们已经习惯手动执行的流程。自动化这些过程带来了许多好处:
-
早期错误检测:通过频繁集成代码更改,CI 帮助我们尽早识别和解决错误和集成问题。
-
提高代码质量:在每次集成时运行自动化测试,确保代码更改不会破坏现有功能并保持代码质量。
-
更快的反馈周期:开发者可以立即收到关于他们代码更改的反馈,使他们能够立即修复问题并快速迭代。
-
改进的协作:CI 通过将不同开发者的代码更改集成到共享仓库中,促进了团队成员之间的协作,确保每个人都在使用最新的代码库。
持续集成(CI)过程的另一个巨大好处是执行代码审查的实践,这允许团队成员分析要集成的代码,并执行良好实践的审查以及是否编写了单元测试,例如。
代码审查方法不断产生学习机会。然而,尽管它是手动完成的,但对于团队的演变来说却极为强大,并且得到了持续集成(CI)的支持,以确保不会对出现编译错误或测试失败的代码进行任何修订。这是一个持续的学习流程,使团队能够在将任何应用程序版本发布到生产环境之前采取主动。
代码审查
代码审查是一个人工审查过程,其中一个或多个开发者审查另一个开发者编写的代码。要了解更多关于这种方法的信息,请查看以下 GitHub 文章:github.com/resources/articles/software-development/how-to-improve-code-with-code-reviews。
CI 流水线在开发流程中非常重要,因为其主要目标是准备一个符合开发和业务团队定义的质量要求的应用程序包。作为输出,它将此包交付给 CD 过程,该过程执行在一个或多个环境中实施此包的程序。
CD
CD 是 CI 之后的下一步,它自动化了将应用程序部署到生产环境的过程。
CD 流水线模拟了 CI 中发生的情况,使得在为不同环境(如开发、测试和生产)的新版本应用程序执行自动部署程序之前,可以添加自动化测试等流程。
图 10 .21 显示了 CD 流水线。它与 CI 的流水线非常相似,但它由不同的任务和流程组成:

图 10.21 – CD 流水线流程
虽然 CI 流水线基于开发过程的质量流程生成有效的包,但 CD 流水线的主要目标是获取通过 CI 流水线生成的包,并在本地或云环境中进行分发。
CD 流水线带来了以下好处:
-
加速交付:CD 能够加快新功能和错误修复的交付速度,缩短上市时间并提高客户满意度。
-
降低部署风险:通过部署小的、增量更改,CD 最小化了与大型、不频繁部署相关的风险。
-
一致的部署:自动化部署过程确保部署是一致和可重复的,减少了人为错误。
-
提高可靠性:持续监控和自动回滚机制提高了部署过程的可靠性。
虽然 CD 管道是一个自动化流程,这意味着它能够自动在不同环境中发布应用程序的新版本,但可以建立审批流程,让每个环境的负责人可以选择是否批准部署。审批行为会根据需要触发自动部署流程或取消它。
批准门控方法带来了合规性好处,并允许团队在特定环境中完全控制部署流程。
审查部署
根据每个组织的交付流程,需要审查部署,尤其是在生产环境中。这种方法在 CD 管道和审查者之间生成一个自动的通信过程。GitHub、Azure DevOps、GitLab 等工具都有允许你配置此审批流程的机制。你可以在 GitHub Actions 中了解更多关于部署审查过程的信息,请参阅docs.github.com/en/actions/managing-workflow-runs/reviewing-deployments。
CI 和 CD 是自动化流程的绝佳方法,以确保我们的应用程序可以持续且高质量地交付到不同的环境。这是允许我们在同一天多次交付应用程序版本的事情,同时它还帮助我们快速在这些环境中提供纠正和回滚,如果需要的话。
在下一节中,我们将使用 GitHub Actions 实现 CI/CD 流程,并学习如何自动在容器注册库中发布 Docker 镜像。
使用 GitHub Actions 进行自动化
为了展示 CI/CD 的实际应用,我们将使用 GitHub Actions 来自动化从UrlShortener应用程序生成和发布 Docker 镜像的过程。确保你已经安装了技术要求部分中提到的所有内容,以便你可以利用这里描述的步骤。
为了自动化包含 CI/CD 的过程,我们需要做以下几步:
-
在 GitHub 仓库中配置密钥。
-
创建 GitHub Actions。
-
每次向仓库发送的推送事件都激活 GitHub Actions。
-
构建 Docker 镜像。
-
将创建的镜像发布到 Docker Hub。
-
在本地机器上运行之前创建的 Docker 镜像。
在开始自动化之前,你必须了解 GitHub Actions 的基本知识。
理解 GitHub Actions 的基础知识
GitHub Actions 是一个内置在 GitHub 中的自动化工具,它允许你直接在你的仓库中创建、管理和运行工作流程。
如同通常所知,动作可以通过各种事件触发,例如代码提交、拉取请求创建或时间触发器。
可以使用位于本书 GitHub 仓库 .github/workflows 目录中的 YML/YAML 文件结构创建 GitHub Actions。工作流程的基本结构在 图 10.22 中以高层次表示:

图 10.22 – GitHub Actions 的基本结构
YAML 文件
一个 YAML Ain’t Markup Language ( YAML ) 文件是一种人类可读的数据标准,通常用于配置文件和在不同数据结构之间交换编程语言的数据。YAML 文件使用缩进来表示结构,这使得它们易于阅读和编写。它通常用于需要人类可读性和易于机器解析的场景,例如在 CI/CD 管道、云配置文件和应用程序配置设置中。在 GitHub Actions 的上下文中,YAML 文件定义了自动化构建、测试和部署应用程序等流程的工作流程。要了解更多信息,请访问 docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions。
图 10.22 显示了 GitHub Actions 结构中的一部分主要组件。让我们更仔细地看看:
-
workflow.yml:一个定义工作流程的 YAML 文件。
-
on:这指定了触发工作流程的事件。例如包括 push、pull_request 和 schedule。
-
作业:定义了在工作流程中要执行的一组作业。
-
构建作业:一个名为 build 的作业,在 ubuntu-latest 上运行。在这里,ubuntu-latest 是一种代理或机器,它将执行作业的所有步骤。这台机器由 GitHub 本身提供。
此流程包含一系列步骤:
-
检出代码:使用 actions/checkout@v2 动作。
-
设置 .NET Core:使用 actions/setup-dotnet@v2 动作,.NET 版本为 '8.0.x'。
-
构建项目:运行 dotnet build 命令。
-
运行测试:运行 dotnet test 命令。
-
GitHub 主机运行器
在 GitHub 上,代理被称为运行器,并且适用于 Windows、Linux 和 macOS。
运行器是运行 GitHub Actions 的基本组件,并在 CI 或 CD 管道中配置。
要了解更多关于运行器的信息,请访问 docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners。
使用 GitHub Actions,我们有多种自动化流程的可能性,这些流程不仅限于 CI/CD 上下文。
现在我们已经了解了 GitHub Actions 的工作原理,让我们首先配置我们的仓库并创建我们的第一个操作。
准备 GitHub 仓库
要通过 GitHub Actions 自动化创建和发布 Docker 镜像的过程,了解我们将创建的管道如何工作是很重要的,如图 10.23 所示:

图 10.23 – GitHub Actions 与 Docker Hub 集成的示例
此前,我们学习了使用 Docker 时的容器策略基础,并使用 Docker 的构建多阶段方法来实现它。
这种多阶段方法包含了生成应用程序包所需的所有步骤,最终生成一个可以使用的镜像。
如图 10.23 所示,在管道流程中,需要调用 Docker Hub 进行一些操作。这是我们打包URLShortener应用程序时使用的公共容器注册库。然而,我们可以在不需要与 Docker Hub 通信的情况下生成本地镜像。
要能够将 Docker 镜像发布到公共或私有容器注册库,必须执行身份验证。
在 Docker Hub 的情况下,这种身份验证是通过用户名和密码进行的。由于这些信息是敏感的,我们不应将其直接添加到 GitHub Actions YAML 文件中,尤其是如果仓库是公开的。
最佳实践是使用秘密安全地管理这些凭证。因此,按照以下步骤添加必要的秘密:
-
通过您的 GitHub 用户访问您的ASP.NET-8.0-Core-Essentials仓库。这应该按照技术****要求部分准备。
-
然后,访问设置标签页。
-
从侧边菜单,访问秘密和变量|操作。
-
在屏幕中央,点击新建仓库****秘密按钮。
-
将名称字段设置为DOCKER_HUB_USERNAME。
-
在秘密字段中,添加您的 Docker Hub 用户。
-
点击添加****秘密按钮。
-
再次,点击新建仓库****秘密按钮。
-
将名称字段设置为DOCKER_HUB_PASSWORD。
-
将秘密字段设置为您的密码。
-
点击添加****秘密按钮。
在运行 GitHub Actions 时,秘密将被安全访问。现在,让我们创建 CI/CD 管道。
创建 CI/CD 管道
到目前为止,我们已经学习了如何安全地管理 Docker Hub 凭证,这是我们将在管道中进行身份验证并提交新生成的镜像所必需的。
仍然在 GitHub 仓库中,访问操作标签页。如图 10.24 所示,有几个现成的管道模板适用于不同类型的应用程序,是创建管道的一个很好的起点:

图 10.24 – GitHub Actions 模板屏幕
对于这个例子,我们将点击图 10.24中突出显示的设置自己的工作流程链接。
您将看到一个编辑器、一个特色动作列表以及文件名,如图 10.25所示:

图 10.25 – GitHub Actions 编辑器
图 10.25显示了 GitHub Actions 编辑器的三个重要区域:
-
A:这是您可以定义文件名的地方。注意建议的目录结构。我们将文件名设置为cicd-pipeline.yml。
-
B:这是我们添加管道代码的地方。
-
C:管道由称为动作的任务组成。这些动作是执行与特定技术相关的任务的抽象。技术社区共享不同类型的自定义动作,我们可以使用。
将以下代码添加到管道编辑器中:
name: CI/CD Pipeline
on:
push:
branches:
- main
jobs:
build-and-deploy:
name: Build and deploy
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4.1.7
- name: Build and publish Docker image
run: |
docker build -t ${{ secrets.DOCKER_HUB_USERNAME }}
/urlshortener:latest .
echo ${{ secrets.DOCKER_HUB_PASSWORD }} |
docker login -u ${{ secrets
.DOCKER_HUB_USERNAME }} --password-stdin
docker push ${{ secrets.DOCKER_HUB_USERNAME }}
/urlshortener:latest
working-directory: ./Chapter-10/UrlShortener
YAML 文件的层次结构是通过空格创建的。因此,嵌套元素定义了层次结构。如果不尊重这些空格,文件将无效。以下示例显示了在层次结构中的每个项目使用两个空格:
on:
<space><space>push:
<space><space><space><space>branches:
<space><space><space><space><space><space>-main
让我们更仔细地看看这个管道代码:
-
on:这定义了动作的执行方式。在这种情况下,每当主分支有更新或推送时,此动作将被触发,执行 CI 管道。
-
作业:作业是按顺序执行的过程。可能有用于构建、测试、生成包等作业。使用作业允许我们拥有定义良好的步骤和步骤之间的依赖关系,从而创建管道流程。
-
build-and-deploy:这是名为build-and-deploy的作业的定义。name参数在管道执行期间产生一个更用户友好的描述,但可以在管道流程中引用作业的名称。
-
步骤:步骤是在每个作业中执行的任务或动作。对于这个例子,只需要执行两个任务。
-
uses: actions/checkout@v4.1.7:这是一个旨在克隆存储库的本地 GitHub 动作。由于运行器是按需创建的,并且没有应用程序的源代码,因此checkout动作是必要的。
-
name: Build and publish Docker image:在这里,我们执行一个内联动作,其中定义了一个构建和发布 Docker 镜像的脚本。
-
docker build -t ${{ secrets.DOCKER_HUB_USERNAME }} /urlshortener:latest .:此脚本构建 Docker 镜像。注意使用包含 Docker Hub 用户名的秘密。这是必要的,以便我们可以用其所有者的标签标记镜像——即 Docker Hub 用户名。我们在这里使用latest标签以方便理解。
-
echo ${{ secrets.DOCKER_HUB_PASSWORD }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin:此命令在 Linux bash shell 中执行,允许我们通过从先前执行的命令获取输入和输出来组合命令并执行其他命令。在这种情况下,我们正在写入包含 Docker Hub 用户密码的秘密,并将输出作为参数发送到docker login命令。这样,秘密就不会在管道执行日志中暴露。docker login命令是发布新版本镜像所必需的。
-
working-directory: ./Chapter-10/UrlShortener:包含Dockerfile文件的目录。
-
docker push:docker push命令将先前生成的镜像提交到 Docker Hub。
通过这种方式,管道已经配置完毕,并包含 CI 和 CD 管道,在管道的末尾发布 Docker 镜像的新版本。此时,是时候运行创建的 GitHub 操作了。
运行 CI/CD 管道
在管道编辑器中,点击提交更改按钮。您将被带到新页面,在那里您必须再次点击提交更改。这是必要的,因为我们正在在存储库中创建一个新文件。
当您提交这些更改时,管道将自动触发。
点击操作选项卡;您将看到管道的执行,如图图 10 .26 所示:

图 10.26 – 运行 GitHub 操作
运行管道后,新镜像将在 Docker Hub 中生成,如图图 10 .27 所示:

图 10.27 – GitHub Actions 在 Docker Hub 上发布的容器镜像
要在本地机器上测试新镜像,请运行以下命令:
docker run -d -p 7777:8080 -e ConnectionStrings__DefaultConnection="<Your Connection String>" <your username>/urlshortener:latest
新镜像将被下载,这意味着您可以在本地机器上运行应用程序,地址为localhost:7777。
正如我们所学的,GitHub Actions 可以自动化 CD 环境中的任务。这带来了敏捷性和一致性,并允许团队在面对市场对持续变化的需求时迅速行动。
在此阶段,对您的存储库所做的任何更改并提交到 GitHub 都将自动为您的 Docker Hub 用户生成一个新的容器版本。
GitHub Actions 有多个应用,与 ASP.NET Core 9 中开发的解决方案一起,它是一个创建高质量应用程序的强大工具。
在本节中,我们学习了在持续的价值交付流程中工作的基础知识。我们将在下一章探讨如何获得云原生应用程序开发的心态。
摘要
在本章中,我们学习了如何使用dotnet CLI 工具发布 ASP.NET Core 9 应用程序并生成发布包。此外,我们还学习了如何在 Azure 云环境中发布应用程序,并探讨了 Docker 容器策略的基础。利用我们所获得的知识,我们能够了解价值交付如何通过 CI 和 CD 等 DevOps 实践流动,并从 GitHub Actions 的自动化流程中受益。本章所获得的所有知识构成了下一章的基础,我们将学习如何使用 ASP.NET Core 9 进行云原生开发。
第十一章:使用 ASP.NET Core 9 进行云原生开发
现代应用程序被设计成在云环境中运行并利用提供的各种功能,如敏捷性、可伸缩性、可用性和弹性。ASP.NET Core 9 为我们提供了一套强大的工具,使我们能够开发高质量解决方案。然而,了解与云原生开发模型相关的模式和最佳实践同样重要。
在本章中,我们将学习与云环境中托管的应用程序相关的重要方面,探讨模式、最佳实践、云原生应用程序开发所需的心态、十二要素应用的原则以及架构设计原则,以便您能够充分利用您的云环境。
在本章中,我们将重点关注以下主题:
-
培养云原生心态
-
使用云原生工具
-
十二要素应用的原则
-
理解云架构原则
技术要求
本章中使用的代码示例可以在本书的 GitHub 代码库中找到:github.com/PacktPublishing/ASP.NET-Core-9.0-Essentials/tree/main/Chapter11。
为了充分利用本章中提出的所有示例,您需要将本书的代码库进行分支。分支是 GitHub 上的一项功能,它可以将代码库复制出来,以便 Git 用户进行管理。您可以通过以下网址了解如何分支一个代码库:docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo。
培养云原生心态
每年云计算都会带来新的创新:总有新的功能带来新的可能性,以及希望以新的方式向用户交付价值的公司。
在前面的章节中,我们学习了各种工具、模式和最佳实践,并与云资源进行了交互。即使在考虑最佳实践和标准的情况下构建的应用程序,它们是否能够充分利用云环境提供的所有功能?
即使在今天,仍有许多组织在私有环境中(本地)运行应用程序,这带来了许多好处。因此,在这些环境中开发的应用程序具有有限的可伸缩性,并且需要承担购买服务器和聘请合格专业人员维护它们的高昂成本。在这种解决方案模型中,计算资源有限,但同时也为公司带来了更大的控制权、合规性和安全性。
云似乎为组织提供了一种增强计算能力的替代模型,但同时也带来了其他挑战,包括在开发模型和流程中。
要使用云模型,我们需要了解其工作原理、服务层、必要的投资以及如何将我们的应用程序适配到原生云模型。
让我们从查看云环境中提供的服务层开始。
理解云环境中的服务层
也许你已经听说过CapEx和OpEx,以及这两个词在商业世界中的重要性:
-
资本支出 ( CapEx ):CapEx 不是一个仅限于 IT 领域的术语,而是一个与资产相关支出或投资的财务术语。计算很简单:如果需要更多服务器来支持用户需求,CapEx 就会发挥作用,并涉及服务器投资成本、服务器安装的物理位置、电力、不间断电源(UPS)等。
-
运营支出 ( OpEx ):OpEx 指的是日常运营的持续成本。这包括服务、公用事业、租金、软件许可以及其他运营活动,如员工工资。
观察这些概念,每个组织都需要反思其投资、成本和专业人员,以保持竞争力。
当转向涉及云计算的方法时,从 CapEx 到 OpEx 的转变就会发生。这种变化有几个影响:
-
降低前期成本:采用云服务减少了在物理硬件和基础设施上的大量前期投资需求。相反,组织只需为使用的云服务付费。
-
可伸缩的成本:云服务提供按使用付费的模式,允许组织根据需求调整其使用和支出。
-
运营灵活性:能够快速适应业务需求的变化,而不依赖于硬件投资。
-
维护和更新:云服务提供商,如 Azure,提供基础设施维护、更新和安全服务,减轻了组织 IT 团队的操作负担,使他们能够专注于战略举措。
然而,在云中运行服务并不意味着成本会降低,因为存在按使用付费的模式。就像任何工具或策略一样,如果不正确使用,云可能会给组织带来重大问题。
云服务提供商,如 Azure,负责提供计算服务的整个基础设施;然而,与使用这些服务的组织之间存在共同管理。这种服务模式非常重要,需要理解。
在云计算模型中,组织将精力集中在他们的产品和服务上,并从与云提供商的共同管理中受益。组织在决定他们想要的服务模型时,基本上有三个选择:

图 11.1 – 云计算服务提供
如图 11.1所示,我们有以下类型的服务层:
-
基础设施即服务 (IaaS):提供虚拟化计算资源,公司可以动态配置虚拟机、存储和网络。它是在地迁移到云的常见采用模型。
-
平台即服务 (PaaS):提供一个平台,该平台抽象化基础设施,使组织和开发团队能够专注于解决方案和数据。我们在第十章中发布 Azure App Service 应用程序时使用了 PaaS 资源。
-
软件即服务 (SaaS):基于订阅提供软件应用程序,例如流媒体应用程序和 Microsoft 365 应用程序。
云服务是云原生开发的关键组件,有助于定义迁移策略、成本优化、可扩展性、弹性、安全性和部署。
为了了解我们应如何调整我们的工具和开发流程并从云计算的强大功能中受益,从最佳实践开始是非常重要的。
云原生开发最佳实践
微软在 Azure 中提供了广泛的文档和强大的服务,其中可以使用不同的技术托管应用程序,当然,还包括在 ASP.NET Core 9 中开发的解决方案。
除了记录 Azure 中可用的资源外,开发团队了解云采用框架(CAF)和架构良好框架(WAF)非常重要。这两个资源有不同的用例,并帮助团队应对云环境带来的各种挑战。
让我们简要了解这些功能中的每一个。
The CAF
微软的 CAF 拥有一套优秀的文档、实施指南、最佳实践和工具,旨在帮助组织规划和执行其云采用战略。
CAF 基本上包括七个阶段:
-
策略:定义业务成果,建立云采用计划,并优先考虑迁移的工作负载。
-
规划:评估您的当前数字资产,创建云采用计划,并确定所需技能和资源方面的差距。
-
准备:通过配置包括治理、安全和管理工作在内的着陆区来准备云采用环境。
-
迁移:使用工具和方法将工作负载迁移到云,确保平稳过渡。
-
创新:开发新的云原生应用程序或现代化现有应用程序以充分利用云功能。
-
治理:实施治理最佳实践以确保合规性、管理风险并建立安全控制。
-
管理:操作和管理云环境,使用监控和管理工具确保性能、可靠性和成本效率。
CAF
CAF 拥有广泛的文档和资源,这些资源应该是软件工程师日常生活的组成部分。要了解更多信息,请访问learn.microsoft.com/en-us/azure/cloud-adoption-framework/。
CAF 旨在帮助组织采用云原生思维,强调规划、治理和持续改进的重要性,确保云采用与业务目标一致。
CAF 是知识宝库,通常,其重点并不完全在于一个应用程序(或工作负载,如它们通常被称为),而是在于一般性地构建整个环境。然而,它可以作为知识库和规划新工作负载的绝佳来源,因为它涉及业务团队、开发、基础设施和整个持续交付流程。
除了 CAF,在定义云环境中的应用架构模型时,还必须考虑另一个非常重要的资源。
WAF
WAF 是由微软提供的一套能力,包含设计、构建和运营安全、高性能、弹性且高效的云应用程序基础设施的最佳实践、原则和架构指导。WAF 分为五个支柱:
-
运营卓越:这个支柱侧重于保持应用程序平稳高效运行的运营流程。这包括监控、自动化和事件响应。
-
安全性:这个支柱确保应用程序和数据免受威胁。它涵盖了身份管理、基础设施保护、数据加密和威胁检测。
-
可靠性:这个支柱确保应用程序能够从故障中恢复并继续按预期运行。这包括灾难恢复策略、容错和数据备份。
-
性能效率:这个支柱确保应用程序高效地使用资源,并且可以扩展以满足需求。它包括容量规划、资源优化和性能监控。
-
成本优化:这个支柱侧重于在提供最佳性能和价值的同时有效管理成本。这包括成本监控、使用分析和实施成本降低策略。
了解更多关于 WAF
WAF 不仅提供了包含与五个支柱相关的策略和最佳实践的出色文档,还提供了工具,例如评估,可以分析现有的云工作负载以改进它们,清单和其他许多资源。要了解更多关于 WAF 的信息,请访问learn.microsoft.com/en-us/azure/well-architected/。
之前提出的每个支柱都支持云原生思维模式,为每个工作负载提供清晰和实用的指南。这使得团队能够从不同角度分析解决方案,并通过满足组织目标(如成本优化)的高质量解决方案充分利用云环境。
超越代码开发
为了实现云原生解决方案的思维模式,作为软件工程师,我们必须准备好超越代码开发的边界。
DevOps 文化带来了一种协作模式,这种模式不仅限于不同团队的有效沟通,还扩展到知识、标准和最佳实践的共享。
运维团队通过使用基础设施即代码、GitHub 仓库甚至流水线等技术,已经适应了代码开发模型。
同样,学习与网络、基础设施、安全和数据相关的概念也很重要。这将在最佳利用云环境的解决方案的架构设计和开发中产生重大差异,并有助于形成云原生思维模式。
现在我们已经了解了与云计算相关的挑战,是时候学习云原生工具了。
使用云原生工具
在日益竞争激烈的市场中,敏捷和快速交付解决方案已经成为成功的同义词。
云原生方法与敏捷性和速度相关联,允许团队创建解决方案,并通过松散耦合、弹性、管理和可观察性添加服务层和功能层。
然而,当我们谈到开发云原生应用时,我们必须理解敏捷性和速度之间的关系。敏捷并不意味着快速,快速也不一定意味着敏捷。这完全改变了我们思考解决方案的方式。
假设你的团队收到了一个创建 API 的请求,该 API 的目的是提供在线商店中可销售产品的数据,如图 图 11.2 所示:

图 11.2 – 在线商店消费产品 API
在 图 11.12 所示的示例中,产品 API 将是一个 ASP.NET Core 9 应用程序,包含良好的层和包分离实践——假设它托管在 Microsoft Azure 上。从应用程序的角度来看,所有预期的 API 功能很可能都已根据功能和非功能需求实现。还预期质量过程已经得到满意地执行,如图 图 11.3 所示:

图 11.3 – 开发和发布流程
图 11.3 展示了一个软件开发过程的常见场景,涉及任务管理、需求、编码、部署和应用程序维护。
主要目标是快速满足市场需求,即缩短交付价值的时间。
领先时间越短越好。为了使团队和组织能够在敏捷性和速度之间取得良好的平衡,了解图 11.3中所示每个流程步骤中的因素非常重要。
在某个环境中开发和交付的应用程序不一定是云原生解决方案。作为软件工程师,我们必须超越编译代码后生成的工件,并准备应用程序以便充分利用云环境并处理不断增长的用户需求,我们必须准备好在不同的知识领域采取行动。
因此,云原生解决方案必须基于以下因素:
-
基础设施
-
现代设计
-
DevOps
-
支持服务
-
容器和编排器
-
微服务
这些因素可以用以下图表表示:

图 11.4 – 云原生因素
这些因素是云原生解决方案发展的基础,并且必须持续工作,因为服务需求和市场需求的变化是持续的。
在图 11.4中,我们可以注意到的一个重要因素是,这些因素与特定的云提供商(如 Azure、AWS 或 GCP)之间没有关系。相反,云原生模型是一个供应商无关的范式,因此,与通常炒作技术的情况相反,存在一系列的采用模式、定义和最佳实践,这些由云原生计算基金会(CNCF)维护。
了解 CNCF
CNCF 是 2015 年在 Linux 基金会范围内创建的一个联盟。它涉及超过 400 家公司,旨在在技术、标准和最佳实践之间建立一种共同语言,独立于供应商。CNCF 旨在通过汇集开发者、最终用户和供应商的社区来构建可持续的云原生软件解决方案生态系统。
CNCF 推广云原生技术,支持并维护那些使容器化、微服务和动态编排等实践得以采用的项目,促进开放标准和最佳实践,并允许云原生应用程序以互操作的方式工作。此外,CNCF 通过培养贡献者社区并维护一个中立的环境来支持创新,以促进前沿云原生工具和项目的发展。
CNCF 的重要资源之一是技术景观,它是一个视觉表示和交互式指南,以分类方式映射云原生生态系统,展示了一系列工具、项目和与 CNCF 使命相关或属于 CNCF 的技术。
CNCF 的生态系统
CNCF 的景观是组织和专业人士的优秀资源,帮助您了解可用的技术和工具选项,它们之间的关系以及它们在云原生生态系统中的作用。要了解更多关于 CNCF 景观的信息,请访问以下网址:landscape.cncf.io。
为了了解我们如何利用 CNCF 的优势,让我们看看一个托管在 Microsoft Azure 上的 ASP.NET Core 9 解决方案的开发场景。
与 CNCF 合作
想象以下场景:
您正在使用 ASP.NET Core 9 开发一个 Web 应用程序,该应用程序托管在 Azure 上。您希望确保您的应用程序是按照云原生原则开发的,并且利用了可用的最佳工具进行部署、监控和管理工作。
让我们看看 CNCF 的使用如何支持我们在以下场景中定义应用程序架构和开发:
-
容器化策略:基于上一章获得的知识,您选择使用 Docker 并将您的 ASP.NET Core 9 应用程序容器化。为了回顾,Docker 是由 CNCF 形成的一个项目,允许您将应用程序及其依赖项打包到容器中,确保在不同环境中的一致性。
-
这些是好处:
-
确保应用程序在开发、测试和生产环境之间的一致性
-
简化依赖管理和隔离
-
-
-
容器编排:为了使此应用程序托管,您决定使用 Kubernetes,这是另一个 CNCF 毕业项目,用于容器编排。Azure Kubernetes Service(AKS)在 Azure 中提供了一个托管的 Kubernetes 环境,使得部署、管理和扩展您的容器化应用程序变得容易。AKS 使用 CNCF 为 Kubernetes 定义的相同标准,但抽象了创建集群的复杂性,并提供了几项其他服务。
-
这些是好处:
-
管理容器生命周期、扩展和负载均衡
-
确保高可用性和弹性
-
-
-
自动化:您决定使用 CI/CD 工具,如 Jenkins 或 GitHub Actions,来自动化您的构建、测试和部署流程。这些工具确保您的代码更改持续集成和部署,从而提高您的开发工作流程。
-
这些是好处:
-
自动化部署过程,确保一致性并降低人为错误的风险
-
加速开发周期,实现持续集成和部署
-
-
-
监控和日志记录:作为云原生解决方案软件工程师,完成开发、执行持续集成和实现持续部署只是项目工作的第一步。重要的是要整合可观察性工具,如Prometheus和Grafana,这两个都是 CNCF 孵化项目,以监控您应用程序的性能和健康状况。Prometheus 收集指标,Grafana 将其可视化,为您提供有关应用程序行为的洞察。Azure Monitor 也是 CNCF 生态系统中特色的可观察性工具。此外,其他类型的工具,如由 CNCF 孵化的OpenTelemetry,对于应用程序来说,拥有一个无厂商依赖的收集器是一个很好的选择,这可以减少应用程序对专有库的依赖。
-
这些是好处:
-
为您的应用程序提供实时监控和警报
-
帮助您快速诊断和解决性能问题
-
-
利用 CNCF 及其生态系统,为开发、部署和管理云原生应用程序提供了众多工具和最佳实践。通过采用这些工具,在 Azure 上托管 ASP.NET Core 9 应用程序的软件工程师可以确保他们的应用程序是可扩展的、有弹性的和可维护的。使用 Docker 容器化、使用 Kubernetes 进行编排、使用 Prometheus 和 Grafana 进行可观察性,以及使用 Jenkins 或 GitHub Actions 进行自动化的 CI/CD 管道,只是 CNCF 项目如何增强您的云原生开发工作流程的几个例子。
然而,之前提到的场景提供了一个与 Azure 无关的解决方案模型,这使得它可以在不同的云提供商上托管,因为提到的每个工具都遵循 CNCF 为无云提供商解决方案制定的标准。
定期访问 CNCF 网站,了解云模型的新闻和变化,并分析 CNCF 生态系统中可用的工具,以满足您解决方案的需求,这一点非常重要。这些资源将帮助您创建越来越强大的解决方案,以适应您组织和市场不同的需求。
我们知道 CNCF 是一个重要的资源,它为云原生解决方案设定了标准,应该被添加到任何软件工程师的工具箱中。此外,还有一些其他原则,以实际的方式指导我们在开发云原生解决方案的过程。ASP.NET Core 9 可以帮助我们实现这些原则,例如十二要素应用方法。我们将在下一节中讨论这些原则。
十二要素应用原则
十二要素应用方法(12factor.net)是一套最佳实践,旨在帮助开发者构建现代、可扩展和可维护的云原生应用程序。它是由 Heroku 开发者创建的,旨在为开发可以部署和管理的云应用程序提供框架。
Heroku
根据网站自身的定义,Heroku 是一个云平台,允许公司创建、交付、监控和扩展应用程序。
Heroku 是一个成立于 2007 年的云平台即服务(PaaS),允许开发者以简化的方式在云中构建、运行和操作应用程序。了解更多信息,请访问 www.heroku.com/home。
如其名所示,十二要素应用方法包含十二个要素或原则:
-
代码库:使用版本控制跟踪的代码库。
-
依赖项:明确声明并隔离依赖项。
-
配置:将配置存储在环境中。
-
后端服务:将支持服务视为附加资源。
-
构建、发布、运行:严格分离构建和运行阶段。
-
进程:以一个或多个无状态进程运行应用程序。
-
端口绑定:通过端口绑定导出服务。
-
并发:通过进程模型进行扩展。
-
可处置性:通过快速启动和平稳关闭最大化鲁棒性。
-
开发/生产一致性:尽可能保持开发、预发布和生产环境的相似性。
-
日志:将日志视为事件流。
-
管理流程:作为单个进程执行管理/管理任务。
让我们更详细地了解所提到的每个要素。
代码库
在解决方案开发周期中,你应该在远程仓库中维护源代码,例如基于 Git 的仓库。
代码库要素意味着每个应用程序上下文都应该有一个代码库,这有助于正确分离责任并提高代码管理。
图 11.5 描述了应用程序不同上下文的管理,例如配置、源代码和基础设施脚本。这些上下文可以在开发流程的不同时间、不同环境中进行分发。

图 11.5 – 代码库管理
尽管在应用程序开发流程中被视为一个自然的原则,但其重要性超出了远程服务器上的源代码管理。开发团队必须对端到端解决方案负责,定义源代码管理流程,例如分支的使用、开发标准、代码审查、质量流程和文档。
在云环境中,通常有一个用于管理应用程序代码的仓库,另一个专门用于存储基础设施代码的仓库。在仓库中实现上下文的具体化和分离,允许开发团队和运维团队之间持续协作和管理。
代码库原则是所有其他原则的基础。接下来,我们将学习关于依赖项的内容。
依赖项
依赖关系是应用程序开发的一部分,正如我们在书中的一些示例中已经看到的那样,当使用 NuGet 包时。使用包带来了诸如可重用性等好处,并且与包管理机制一起,可以轻松更新包。目前大多数编程语言都提供了基于包管理的可扩展性机制。
依赖关系原则定义了依赖关系必须在清单文件中管理,并且必须使用包管理工具。
例如,ASP.NET Core 9 应用程序有 NuGet 包管理器,所有依赖项都通过
通过此功能,我们可以从 .NET 平台的互操作性中受益,并且与 .NET CLI 工具结合使用,我们可以以简单的方式获取依赖项,执行 dotnet restore 命令,并允许我们构建和生成部署包,而无需担心人为错误的风险。
使用包管理可以避免手动管理依赖文件。
正如依赖关系在应用程序中是必要的,配置同样重要。让我们了解配置因子如何帮助配置管理。
配置
所有应用程序都有某种类型的配置文件,这可能包括敏感信息,如加密密钥和连接字符串。在配置文件中保持设置是一种很好的做法,可以避免在配置更改时需要更改源代码。
在云中,应用程序通常有不同的环境以确保每次更新后,解决方案的质量保持较高。此外,出于安全原因,生产环境有访问限制;生产环境中的应用程序配置不应可访问。
配置因子表示配置必须与代码分开,这使得管理不同的环境变得更容易。图 11.6 说明了配置因子提出的方法:

图 11.6 – 配置服务器和环境
如我们在 图 11.6 中所见,开发流程使用自动化管道、CI 和 CD,当获得工件时,它将在不同的环境中发布。
应用程序随后根据执行环境获取其相应的配置。
在 第十章 中,我们学习了如何使用 Azure App Configuration 管理应用程序配置和行为。这样,我们可以抽象化开发人员配置的管理,并定义对敏感配置的访问,并且应用程序可以在不同的环境中动态部署。
在任何应用程序的上下文中,配置非常重要,所做的集成也是如此。下一个因素建议了一种最佳实践,这直接影响了解决方案的架构定义。
后端服务
大多数应用程序都对外部资源或在这种情况下,后端服务有一定的依赖。这些资源可以是数据库、电子邮件服务器和存储服务器,以及其他服务。
应用程序必须准备好隔离这样的依赖关系,同时,能够独立于执行环境使用这些服务,而无需对代码进行任何更改。后端服务必须通过 URL 和相应的凭据公开,具体取决于资源。资源必须被维护并在隔离中提供,应用程序必须引用它们。让我们看看图 11.7:

图 11.7 – 应用程序与后端服务之间的交互
如我们在图 11.7中可以看到,该应用程序的架构模型提出了外部消费的服务之间的隔离,例如数据库、消息代理和存储服务。当在源代码级别反思架构方法时,使用六边形架构或洋葱架构可以帮助在这种服务隔离的背景下。
六边形架构和洋葱架构
六边形架构(或端口-适配器架构)是一种设计模式,旨在在核心业务逻辑和外部元素之间创建清晰的分离,例如用户界面、数据库和其他服务。在这种架构中,核心应用程序逻辑位于中心,周围环绕着几个端口,这些端口定义了不同功能性的接口。适配器是用于与某些外部资源交互的接口的具体实现。
洋葱架构,也是一种设计模式,强调应用程序内部关注点的分离。它将核心领域放在中心,周围是包含基础设施和表示关注点的层。最内层代表领域模型和业务逻辑,它们独立于外部关注点。围绕这个核心是应用程序服务层,然后是基础设施和用户界面层,最外层。依赖关系流向内部,这意味着外部层可以依赖于内部层,但反之则不行。
要了解更多关于六边形架构和洋葱架构的信息,请访问alistair.cockburn.us/hexagonal-architecture/和jeffreypalermo.com/blog/the-onion-architecture-part-1/。
如果进一步分析后备服务因素,我们可以反思在云环境中重要的其他架构方面,例如弹性和可用性。由于应用程序依赖于其他服务,会引发某些问题,例如:
-
如果数据库或缓存不工作,应用程序应该如何表现?
-
如果电子邮件服务 不工作怎么办?
这些问题的答案使我们能够超越源代码的视野,转向云原生思维。
后备服务因素提出的隔离允许在价值交付流程中使用自动化方法。
构建、发布、运行
在 第十章 中,我们学习了 DevOps 和自动化流程的重要性,在 通过 CI/CD 理解 DevOps 方法 部分中。
自动化正是构建、发布、运行因素所定义的概念。
CI 流程与构建工件的时刻相关联,此时执行下载依赖项、构建和执行质量和安全流程,以及使工件可供其他流程(如 CD)使用。
图 11.8 展示了 CI 流程:

图 11.8 – 持续集成与拉取请求方法
除了 CI 流程外,我们还有 CD,其目标是部署不同环境中的工件。
图 11.9 展示了 CD 流程:

图 11.9 – 使用回滚方法的 CD
如 图 11.9 所示,自动流程的使用给 CD 流程带来了巨大的好处。CD 在 CI 之后执行,如果任何环境中存在不一致,甚至在生产环境中,可以快速执行回滚流程,再次发布应用程序的最新稳定版本。此外,还可以在此过程中使用其他技术,例如功能开关,如 第十章 中所述。
下一个因素对于云原生环境非常重要。
流程
流程因素定义了应用程序应在无状态的环境中独立执行。如果需要状态存储,必须通过外部支持服务进行存储。无状态流程易于调整大小和替换,而不会丢失状态,从而提高可靠性和可伸缩性。
图 11.10 显示了在不同流程中运行的应用程序及其与基于数据库的状态持久化模型的交互的高级视图。

图 11.10 – 使用数据库管理应用程序状态
在 ASP.NET Core 中开发的 Web API 是无状态应用程序的例子,其中没有通过内存中的会话进行状态管理。每个请求都有 API 需要处理的信息的上下文,正如我们在 第六章 中所学,在那里我们实现了身份验证和授权。对于每个请求,用户信息作为令牌发送在请求头中。然后,API 使用 ASP.NET Core 9 中间件将用户的上下文与请求相关联,允许或不允许执行操作。每个请求都是独立的,状态是在请求周期中获得的。
这是一个重要的功能,允许应用程序能够动态扩展,例如在 Docker 容器中执行同一应用程序的多个实例。
请参阅 图 11.11 中的示例:

图 11.11 – 无状态应用程序
图 11.11 中展示的图解说明了应用程序如何通过容器实例执行多个无状态过程来处理请求。以下是图中每个组件的简要描述:
-
用户:用户发起 API 请求。
-
API 网关:API 网关接收请求并充当负载均衡器。
-
负载均衡器:负载均衡器将 API 请求分发到容器应用程序的各个实例。
-
容器实例:几个容器实例独立处理请求。
-
数据库:每个容器实例与共享数据库交互以处理请求并恢复或存储必要的数据。
-
响应流:请求后,每个容器实例通过 API 网关发送响应,API 网关将最终响应返回给用户。
除了进程因素外,理解端口绑定的概念也是至关重要的。
端口绑定
与前面的因素一样,这里,每个应用程序都应该映射并可通过特定的地址和端口提供。
如果每个服务器都有一个地址和 URL,则每个服务器可以同时负责响应多个应用程序。为了区分哪个应用程序将响应特定的请求,必须映射端口。因此,服务 A 可以通过端口 4040 在服务器上托管,服务 B 通过端口 3030,服务 C 通过端口 8080,如 图 11.12 所示:

图 11.12 – 端口绑定
当使用本书中开发的 dotnet run 命令执行应用程序时,我们观察到提供了一个格式为 http:// localhost:<端口> 的 URL。端口号可能因环境而异,应用程序可以定义哪个端口将被执行。
当采用 Docker 的容器策略时,这种模式同样适用,其中我们将主机和容器的端口映射,如 图 11.13 所示:

图 11.13 – 容器的端口绑定
在图 11.13 中,有三个相同应用程序的实例,通过主机响应不同的端口。尽管是相同的应用程序,但每个容器都在一个隔离的进程中执行。拥有不同实例的容器是应用程序中常见的场景,这些应用程序需要扩展以应对来自用户的竞争性请求,这是下一个因素的主题。
并发
云环境允许应用程序以动态方式处理不同的需求。弹性的特性不仅使软件工程师能够根据用户需求保持应用程序正常运行,而且还允许优化应用程序的见解。
在一个环境中托管的应用程序需要资源来运行,无论是内存、CPU 还是存储。这些指标对于定义应用程序的限制以及确定何时需要扩展至关重要。
通常来说,使用带有监控和测试技术的策略应该是持续应用程序流程的组成部分,指导基于具体数据的决策。
如果需要扩展性,我们必须定义策略将是水平还是垂直,如下面的示例所示:

图 11.14 – 垂直和水平扩展性
基本上,有两种类型的扩展性:
-
垂直扩展性:当向服务器添加更多功能时,例如内存、CPU 或存储,以支持应用程序处理时,会应用此方法。
-
水平扩展性:水平扩展性涉及创建新的服务器实例,例如集群。在此方法中,处理在服务器之间交错进行,通过负载均衡器支持负载需求。水平扩展性是 Kubernetes 等编排器广泛使用的策略,用于创建应用程序容器的不同实例以支持持续的用户请求。
除了提到的点之外,每个应用程序在其上下文中可能依赖于不同类型的并发处理,这些处理可以划分为后台进程。也许你的应用程序包括异步处理 HTTP 请求,同时生成必须后台处理的信息,因为它是一个长期执行的任务。因此,其架构可以提供一个用于 HTTP 处理的 Web 应用程序,以及一个与工作进程协同工作的应用程序,能够后台处理长期请求。与这种策略相结合,遵循十二要素应用程序特性的 Web 应用程序和后台进程可以垂直和/或水平扩展。
在下一节中,我们将了解可处置性。
可处置性
可丢弃原则强调最大化应用程序的鲁棒性,包括快速启动和优雅关闭,使应用程序能够处理快速变化的规模、部署和代码,而不会影响用户体验或系统稳定性。
快速启动时间可以实现快速停机并迅速恢复,而优雅的关闭确保在应用程序中断之前,正在进行的请求被完成,资源被正确释放。
在第十章中,当我们学习 Docker 的原理时,我们使用了多阶段构建,目的是生成一个优化的容器镜像,如果需要,它支持容器的快速启动。
此外,可丢弃原则有助于维护系统弹性和可靠性,使应用程序能够更好地抵抗硬件故障,产生动态云环境,其中实例可以频繁地创建和销毁。
见 图 11 .15 :

图 11.15 – 可丢弃示例
展示的图表说明了应用程序应该如何处理快速启动和优雅启动过程,以在原生云环境中保持鲁棒性和可靠性。
在以下各点中,我们可以看到图表中提到的每个项目的详细信息:
-
用户:用户发起负载均衡请求。
-
负载均衡器:负载均衡器将输入请求分配到可用的容器实例。
-
扩展 :
-
新容器实例 - 启动:在扩展时,创建一个新的容器实例。应用程序快速启动,使实例在最短时间内准备好处理请求。
-
一旦准备就绪,负载均衡器开始将请求分配到这个新实例。
-
-
关闭 :
-
旧容器实例:在缩放或部署新版本时,负载均衡器停止向旧容器实例发送新请求。
-
优雅关闭:旧实例在关闭之前完成任何正在进行的请求,确保没有请求被突然终止。
-
终止 实例:在完成所有请求并释放资源后,旧实例被终止。
-
在 ASP.NET Core 9 中,你可以通过设置托管和取消令牌的处理来实现优雅关闭,以确保在应用程序关闭之前完成连续请求。
让我们看看 Program.cs 文件中的一个代码示例:
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
var host = app.Services
.GetService<IHostApplicationLifetime>();
// Handle graceful shutdown
host.ApplicationStopping.Register(() =>
{
var logger = app.Services
.GetService<ILogger<Program>>();
logger.LogInformation("Application is shutting down...");
// Add more cleanup tasks here
});
app.Run();
在前面的代码中,app.Services.GetService
host.ApplicationStopping.Register 方法允许你注册一个回调,当应用程序停止时将被调用。
在注册的回调返回中,你可以执行必要的清理任务,例如注册关闭事件、释放资源以及填写正在进行的任务。
通过遵循这种方法,你可以确保你的应用能够优雅地停止,保持其健壮性和可靠性。此外,在应用和环境层面保持一致性也非常重要,因为当我们查看下一个因素,即开发/生产环境一致性时,我们会理解这一点。
开发/生产环境一致性
多年来,开发者们经常引用的一句话是:
它在我的机器上运行!
在某种程度上,这个说法是正确的。应用在不同的执行环境中可能会有不同的行为。有许多变量可能导致故障,例如 CPU、内存、存储量,甚至对资源和依赖项的访问权限。
为了最小化与环境的关联问题,每个服务器之间必须尽可能多地保持兼容性。
正因如此,对于团队来说,与用于创建基础设施代码的技术合作非常重要,例如 Terraform 和 Bicep,这些技术除了提供敏捷性外,还改善了管理、治理、合规性和安全性。
图 11.16 展示了这一概念:

图 11.16 – 开发/生产环境一致性
图 11.16 展示了一个涉及以自动化方式交付应用和通过 IaC 方法自动化创建基础设施环境的开发流程,确保所有环境都是相似的。
除了基础设施,正如我们已经学到的,容器策略还允许容器化应用在不同的环境中运行。
现在让我们谈谈日志因素,它不仅仅是在文件中记录应用事件。
日志
很长时间以来,日志被纯粹地视为应用执行流程中的事件记录,通常记录在文本文件中,只有在需要纠正问题时才会访问。
然而,在云环境中,将日志写入文件可能会给那些在持续变化的应用上工作的团队带来一些挑战。
日志因素决定了这类信息应该被视为事件流,而不是由文件管理,而是由特定的监控相关环境维护,例如 Elasticsearch、Logstash、Azure Monitor 和 Datadog,或者像 Prometheus 和 Grafana 这样的开源解决方案。
因此,通过应用生成的事件流可以提供关于特定请求执行流程的重要信息,生成趋势图,甚至实时监控应用如何根据需求表现。
日志收集生成数据在决策中起着基本作用,使团队能够主动行动,优化资源,定义自动扩展的限制,当然,也支持问题纠正。
市面上有几种支持事件流管理模型的解决方案,包括付费和开源解决方案。许多这些解决方案在 CNCF 景观中都有所提及。
在实现我们应用程序中的日志因素时,我们必须考虑到遥测和日志信息必须以透明的方式进行收集;然而,管理这些信息不是应用程序上下文的一部分。
因此,每个日志和指标解决方案都有不同的收集方法,这导致应用程序需要依赖 SDK 来连接到收集工具。如果需要更改日志检测工具,这种依赖可能是一个缺点。
隔离机制和依赖关系非常重要。为了帮助完成这项任务,有如OpenTelemetry这样的选项,它提供了一种供应商无关的方法,并允许日志和指标在多个监控服务之间进行协作和分发,从而避免了应用程序之间更大的耦合,如图图 11 .17 所示:

图 11.17 – 使用 OpenTelemetry 隔离日志收集机制
了解更多关于 OpenTelemetry 的信息
OpenTelemetry 是由 CNCF 维护的云原生软件的开源可观察性框架,它提供了一种标准化的方式来收集、处理和导出来自应用程序的遥测数据,如跟踪、指标和日志。OpenTelemetry 提供了一些 SDK,它们抽象了从应用程序中收集数据并将其分发到不同的日志管理工具。有关更多详细信息,请访问opentelemetry.io/docs/。
日志收集是云原生应用程序中的一个重要且战略性的任务,如前所述,它允许团队深入了解应用程序过程的执行流程,并支持应用程序中的错误修复。与日志策略相关联的,还可以包括发送关于不符合规范、异常和应用程序不良行为的警报的策略,这为团队提供了主动采取行动的可能性。
在基于事件和微服务的架构策略中,日志同样至关重要,尤其是在信息分布式处理的情况下。通过日志,可以映射整个执行流程,如果需要进行审计、优化和错误修复。
我们可以看到,当与云原生解决方案一起工作时,我们隔离了责任,确保解决方案的每个部分都是解耦的,这为团队提供了灵活性、改进的维护和更好的安全性,以及其他方面。管理员流程因素也对此有所贡献。
管理员流程
当谈到十二要素时,我们最常提到的是应用域的上下文化,确保其尽可能独立地构建和交付,耦合度尽可能低。
然而,即使在这样的情况下,应用程序的复杂性也意味着需要与行政任务进行交互,例如执行数据库迁移。
尽管数据库是构成应用程序的解决方案的一部分,但诸如迁移和用于播种基本信息的脚本等任务,以及其他类型的行政任务,并不属于应用程序的责任。
管理流程因素建议行政任务必须在与应用程序隔离的情况下执行,在一个单独的流程中,并且必须能够监控此类更改。
如 CI/CD 之类的流程运行在应用程序范围之外。因此,在执行管道和 CI 期间,可以执行诸如生成数据库迁移脚本之类的任务,例如,这些脚本与 CD 管道共享,CD 管道可能需要执行不同的任务以应用更改,如图图 11.18所示:

图 11.18 – 管理流程实现示例
图 11.18展示了通过 CD 流程执行的管道流程,通过这种方式,执行两个不同的任务,以便应用程序可以在环境中正确执行。
执行一次性的管理流程有助于维护应用程序状态,并确保在这些任务期间所做的任何更改都能立即反映在实时环境中,从而减少差异和潜在错误。
管理流程因素在应用程序生命周期中起着重要作用,正如十二要素应用程序方法中提出的其他因素一样。
十二要素应用程序方法的重要性
正如我们在前面章节中讨论的那样,十二要素应用程序方法中描述的原则旨在帮助开发者创建现代、可扩展和可维护的应用程序,强化为提供持续价值的应用程序所需的云原生思维模式。
一些原则已经在软件工程师的日常工作中存在;其他原则则开阔了我们的视野,让我们看到了不同的视角和可能性。然而,我们可以注意到,12 个原则是相互关联的,并且与云原生计算的主要特征(如微服务架构、容器化和 CI/CD)紧密一致。
使用十二要素应用程序方法提出的方法,以及本章中提出的其他方法(如 CAF、WAF 和 CNCF 上的项目),是任何软件工程师最佳实践的典范。
ASP.NET Core 9 提供的概念和能力很容易实现这些原则。
在下一节中,我们将了解与云架构相关的概念。
理解云架构原则
现代云架构是可扩展、弹性高和高度可用应用程序的基础。在本章中,我们学习了软件工程师结合 ASP.NET Core 9 解决方案的开发并充分利用云环境(如 Microsoft Azure)的益处所必需的几个原则和工具。
在云环境中,资源的可用性不足以提供今天这样高要求的市场中用户所需的质量和体验。
开发流程的每个阶段都有助于组织提供满足用户需求的应用程序和服务,同时提高公司的投资回报率,当然,也使用户越来越忠诚于开发出的解决方案。
在这种背景下,我们必须超越源代码和分层定义的界限,考虑允许应用程序处理用户需求和需求的策略。因此,适应云原生应用程序中可用的架构概念非常重要。
让我们了解一些这些架构原则以及它们如何增强在 ASP.NET Core 9 中开发的应用程序。
与现代设计架构打交道
作为软件工程师,我们习惯于处理基于最佳实践和建筑风格(如 Clean Code、六边形架构和设计模式等)的代码实现。
通过采用云原生方法进行开发,我们不仅为应用程序增加了巨大的可能性,还带来了其他挑战,如本章中提到的。
软件工程师必须超越编写代码,探索一个充满不同变量和方法的世界,例如 DevOps、基础设施、网络、弹性、可用性、敏捷性、安全性、成本以及其他方面。
组织已经将重点转向强调不仅重视在业务环境和战略中用户界面(如表单和屏幕)的重要性,而且强调处理大量数据、提供 API 等服务、实施人工智能以及促进不同系统之间无缝集成的关键需求。
处理大量数据请求、摄取和分析是组织需要考虑的重要特性。
因此,一些现代建筑风格允许组织在云环境中获得最佳效果,同时为业务带来极大的鲁棒性。
想象一下,在像黑色星期五这样的促销活动中,如果一个在线商店应用程序没有适应用户需求和处理不断增长的支付网关购买请求的能力,如果虚拟商店应用程序的支付流程中存在一个错误,如果系统在五分钟内处于不活动状态,公司会损失多少?
当然,后果将是严重的。因此,需要具备处理异步处理和与基于事件架构风格协同工作的能力。
事件驱动架构
事件驱动架构允许应用程序异步处理信息,根据事件或状态变化实现实时反应。它们还使重要业务流程(如在线商店的支付处理)的处理一致性更好。事件驱动架构的另一个强大功能是能够解耦组件,生成独立性,并提高应用程序的维护和演进。ASP.NET Core 9 可以与事件驱动系统集成以创建可扩展和健壮的应用程序。让我们看看 图 11.19 中所示的示例:

图 11.19 – 事件驱动架构示例
在此示例中,以下流程作为事件驱动架构实现方法执行:
-
事件生产者 :
- 订单服务 : 订单服务作为事件生产者。当创建订单时,它发布一个订单创建事件。
-
事件代理 :
- Azure 事件网格 : Azure 事件网格作为事件代理。它从订单服务接收事件并将其分发到已订阅的消费者。
-
事件消费者 :
-
库存服务 : 库存服务消费订单创建事件并相应地更新库存。
-
通知服务 : 通知服务消费事件以向用户发送关于订单创建的通知。
-
计费服务 : 计费服务消费事件以处理订单的计费。
-
将此架构示例与其他技术(如可扩展性)相结合,可以进一步提高解决方案的质量。
根据应用程序需求,必须考虑一些事件驱动架构策略,例如这些:
- 事件源 : 事件源的工作方式类似于跟踪,按顺序执行时捕获所有状态变化。这种方法有利于整个执行链的完整可追溯性,同时提供已执行事件的回放。ASP.NET Core 9 可以轻松集成 Azure Event Hubs 和 Apache Kafka 等技术以实现事件源,如下所示 图 11.20 :

图 11.20 – 事件源示例
- 命令查询责任分离 (CQRS) : CQRS 将应用程序的读操作和写操作分离。在持久化或信息写入(称为命令)的流程独立于查询(读取)流程的上下文中,这种方法非常强大。图 11.21 展示了 CQRS 的使用:

图 11.21 – ASP.NET Core 9 中的 CQRS
- 消息代理:消息代理通过发送和接收消息来促进解耦服务之间的通信。它们确保消息可靠地传递,并允许服务独立扩展。这种方法的绝佳例子是在线商店中的支付处理。在收到支付请求后,应用程序将消息发送给代理。该消息由一个或多个应用程序处理,目的是与其他服务(如支付网关)通信。如果服务器上的代理出现问题,消息将保存在一个称为死信队列的队列中。因此,当代理资源重新建立其操作时,未处理的消息将重新进入队列,确保应用程序可以处理它们。ASP.NET Core 9 应用程序可以与消息代理(如 Azure Service Bus 或 RabbitMQ)集成,以处理异步处理和跨服务通信,如 图 11 .22 所示:

图 11.22 – 带有死信队列的消息代理
死信队列
死信队列(DLQ)是一个专门的消息队列,用于存储由于某些服务器或代理故障而无法处理的消息。消息在 DLQ 中保持隔离,并在服务器问题解决后重新提取以进行重新处理。有关更多信息,您可以访问 Azure Service Bus DLQ 文档:docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dead-letter-queues。
理解和应用事件驱动架构方法对于创建准备应对不同类型用户需求的开源云解决方案至关重要。
基于事件驱动架构原则,另一个对云原生开发至关重要的架构范式是微服务,这进一步增强了应用程序的模块化和可扩展性。
理解微服务
微服务是一种架构风格,它完全支持云原生解决方案的开发,并且本质上应用了本章中提到的最佳实践。
微服务提供了一种方法,意味着一个应用程序具有以下特征:
-
它具有有界实现上下文
-
它是自治的,因此可以独立部署
-
它是独立的且可扩展的
-
它不依赖于特定语言,因此可以有不同技术的不同微服务
-
您的过程独立运行,并可以从不同类型的通信协议中受益,例如 HTTP/HTTPS 和 gRPC 以及消息队列
-
通常,微服务独立管理自己的数据
图 11 .23 展示了微服务和单体之间的比较:

图 11.23 – 单体与微服务
非常重要的是要记住,微服务不是为了取代单体应用。每种方法都有其各自的优点、缺点和挑战。选择一种方法而不是另一种方法将取决于上下文和应用需求。
当分析图 11.23时,我们可以看到在单体方法中,应用层负责管理所有相关过程,由符号表示,并且与同一数据库中所有应用状态共享访问权限。
在微服务方法中,每个服务都在一个独立的应用程序中进行上下文化,独立地管理状态,但提供与单体方法相同的业务流程。
与微服务相关联存在几个挑战,例如通信、事务管理、可伸缩性、粒度、团队、分布式数据、一致性、可用性、可靠性和弹性。
微服务甚至可以使用与 ASP.NET Core 9 结合的容器策略进行开发,它提供了一个强大且性能卓越的平台,为软件工程师带来了许多好处。
此外,除了开发过程之外,无论使用哪种架构策略,我们最终都必须在云环境中交付解决方案。这个过程非常重要,并且必须尽可能减少对用户的影响,这需要部署策略。
考虑部署策略
部署策略在云原生开发中至关重要,它使得应用能够以尽可能小的对用户影响进行交付。
云环境和其他技术支持不同的部署策略。以下要点提到了最常用的策略:
- 蓝绿部署:蓝绿部署基于使用两个相同的环境:蓝色(当前生产环境)和绿色(新版本)。应用的新版本在绿色环境中部署,并在进行验证后进行交换;也就是说,流量从蓝色转移到绿色。在 Azure 中,可以通过配置 Azure App Service 中的单独槽位来实现此策略。通过部署槽位,可以安全地进行部署,如果新版本有任何错误,即使在验证后,也可以简单地再次运行交换,以便使旧版本可用。Azure App Service 实现负载均衡并将用户请求导向以避免损失,如图 11.24所示:

图 11.24 – 使用 Azure App Service 的蓝绿部署
- 金丝雀部署:金丝雀部署是蓝绿部署的一种变体,在将新版本推广到整个用户群之前,它逐渐将新版本引入一小部分用户。微软 Azure 提供流量管理工具,如 Azure 流量管理器,将部分流量导向新版本,同时监控其性能,如图 图 11.25 所示:

图 11.25 – 使用 Azure 流量管理器的金丝雀部署
蓝绿和金丝雀部署策略显著提高了部署过程的可靠性、灵活性和安全性,最小化了用户的影响,同时还能以敏捷的方式恢复到最后一个稳定的环境。
这些策略、工具和现代架构模型的结合加强了实现云原生解决方案所需的思维模式。
ASP.NET Core 9 是一个为不同环境和挑战做好准备的平台,在云环境中提供强大的解决方案。
作为一名软件工程师,考虑本书中提到的方法和技巧对于将你的解决方案提升到更高水平非常重要。
摘要
在本章中,我们讨论了各种资源、工具、策略和架构方法,旨在培养一种专注于云原生解决方案的思维模式。我们了解了云环境提供的不同服务层,并了解了 CNCF 及其景观,它详细介绍了实现云概念和最佳实践的杰出开源项目。我们还学习了十二要素应用的原则、CAF 和 WAF,并讨论了现代云架构和价值交付策略的原则。本章中所有内容的组合,加上 ASP.NET Core 9 平台,将使任何软件工程师能够超越代码,交付高价值解决方案。一切始于 Hello World。


浙公网安备 33010602011771号