C-9-和--NET5-企业级应用开发-全-
C#9 和 .NET5 企业级应用开发(全)
零、前言
.NET 5 是一个开源的免费平台,可以编写针对任何平台的应用。该平台还为您提供了轻松编写应用的机会,面向任何平台,包括云。作为软件开发人员,我们被赋予构建复杂企业应用的责任。在本书中,我们将学习使用 C# 9 和构建企业应用的各种高级架构和概念.NET 5。这本书将作为使用构建企业应用所需的所有功能的圣经.NET 5。
通过对基本概念、实例和自我评估问题的逐步解释,您将深入了解和接触到的每个重要组成部分.NET 5 需要构建一个专业的企业应用。
这本书是给谁的
这本书是为已经熟悉的中级到专家级开发人员准备的。. NET 框架或.NET Core 和 C#。
这本书涵盖了什么
第 1 章设计和架构企业应用,首先讨论了常用的企业架构和设计模式,然后介绍了将企业应用设计和架构为由 UI 层、服务层和数据库组成的三层应用。
第二章介绍.NET 5 Core 和 Standard ,从我们意识到运行时是代码运行的地方开始。在本章中,您将了解的核心和高级概念.NET 5 核心运行时组件。
第三章介绍 C# 9 ,谈 C# 9 中的新功能,随发布.NET 5。
第四章线程和异步操作,帮助你详细了解线程、线程池、任务和async await
以及如何.NET Core 允许您构建异步应用。
第五章依存注入.NET ,帮助我们理解什么是依赖注入,以及为什么每个开发人员都涌向依赖注入。我们将学习依赖注入是如何在中工作的.NET 5 并列出其他可用的选项。
第六章配置中.NET Core ,教你如何配置.NET 5,并在应用中使用配置和设置。您还将了解如何扩展?NET 5 配置来定义您自己的部分、处理程序、提供程序等等。
第七章登陆.NET 5 ,讨论事件和日志 API.NET 5。我们还将深入研究使用 Azure 和 Azure 组件进行日志记录,并学习如何进行结构化日志记录。
第 8 章了解缓存,讨论中可用的缓存组件.NET 5 和最佳行业模式和实践。
第九章处理数据.NET 5 ,讨论了两种可能的数据提供者:SQL 和像 RDMS 这样的数据。我们还将在高级别讨论如何使用 NoSQL 数据库进行存储和数据处理.NET 5。本章将讨论.NET Core 与文件、文件夹、驱动器、数据库和内存的接口。
第十章创建 ASP.NET Core 5 Web API,使用 ASP.NET Core 5 Web API 模板开发我们企业应用的服务层。
第十一章创建 ASP.NET Core 5 Web 应用,使用 ASP.NET Core 5 MVC Web 应用模板和 Blazor 开发我们企业应用的 Web 层。
第 12 章了解身份验证,讨论了行业中最常见的身份验证模式,以及如何使用实现它们.NET 5。我们还将介绍如何实现自定义身份验证。
第十三章了解授权,讨论了不同的授权方式,以及 ASP.NET Core 5 让你如何处理。
第 14 章健康与诊断讨论了监控应用健康的重要性,为其构建了健康检查 API.NET Core 应用和 Azure 应用,用于捕获遥测数据和诊断问题。
第十五章测试,论述了测试的重要性。测试是开发中必不可少的一部分,没有适当的测试,任何应用都无法交付,因此我们也将讨论如何对代码进行单元测试。我们还将学习如何衡量应用的性能。
第 16 章在 Azure 中部署应用,讨论了应用在 Azure 中的部署。我们将把我们的代码签入到我们选择的源代码控制中,然后 CI/CD 管道将启动并在 Azure 中部署应用。
为了充分利用这本书
你需要.NET 5 SDK 安装在您的系统上;所有代码示例都是在 Windows 操作系统上使用 Visual Studio 2019/Visual Studio Code 进行测试的。建议使用活动的 Azure 订阅来进一步部署企业应用。可以从https://azure.microsoft.com/en-in/free/创建一个免费账户:
下载示例代码文件
可以从https://GitHub . com/PacktPublishing/Enterprise-Application-Development-with-C-Sharp-9-and-下载本书的示例代码文件.NET-5 。如果代码有更新,它将在现有的 GitHub 存储库中更新。
我们还有来自 https://github.com/PacktPublishing/丰富的书籍和视频目录的其他代码包。看看他们!
下载彩色图片
我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。可以在这里下载:https://static . packt-cdn . com/downloads/9781800209442 _ color images . pdf
使用的约定
本书通篇使用了许多文本约定。
Code in text
:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。这里有一个例子:“这里,如果你在ImageFile
类的构造函数中放一个断点,只有当System.Lazy
类的Value
方法被调用时,它才会被命中。”
代码块设置如下:
Lazy<ImageFile> imageFile = new
Lazy<ImageFile>(() => new ImageFile("test"));
var image = imageFile.Value.LoadImage;
当我们希望将您的注意力吸引到代码块的特定部分时,相关的行或项目以粗体显示:
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context,
config) =>
{
config.AddSql("Connection
string","Query");
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
任何命令行输入或输出都编写如下:
dotnet new classlib -o MyLibrary
粗体:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。例如,菜单或对话框中的单词像这样出现在文本中。下面是一个例子:“在安装选项中,从工作负载中,选择.NET Core 跨平台开发为.NET Core 应用,如下图所示。”
提示或重要注意事项
像这样出现。
取得联系
我们随时欢迎读者的反馈。
一般反馈:如果你对这本书的任何方面有疑问,在你的信息主题中提到书名,发邮件给我们customercare@packtpub.com
。
勘误表:虽然我们已经尽了最大的努力来保证内容的准确性,但是错误还是会发生。如果你在这本书里发现了一个错误,如果你能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata,选择您的图书,点击勘误表提交链接,并输入详细信息。
盗版:如果您在互联网上遇到任何形式的我们作品的非法拷贝,如果您能提供我们的位置地址或网站名称,我们将不胜感激。请通过copyright@packt.com
联系我们,并提供材料链接。
如果你有兴趣成为一名作者:如果有一个你有专长的话题,你有兴趣写或者投稿一本书,请访问authors.packtpub.com。
评论
请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?然后,潜在的读者可以看到并使用您不带偏见的意见来做出购买决定,我们在 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们的书的反馈。谢谢大家!
更多关于 Packt 的信息,请访问packt.com。
一、设计和构建企业应用
企业应用是为企业组织解决大型复杂问题而设计的软件解决方案。它们为信息技术、政府、教育和公共部门的企业客户提供订单到履行功能,并使他们能够通过产品购买、支付处理、自动计费和客户管理等功能实现业务数字化转型。企业应用的集成数量非常多,用户数量也非常多,因为此类应用通常面向全球受众。为了确保企业系统保持高可靠性、高可用性和高性能,正确的设计和架构非常重要。设计和架构是任何好软件的基础。它们构成了软件开发生命周期剩余部分的基础,因此,正确地进行并避免以后的任何返工是非常重要的,根据所需的更改,这可能会非常昂贵。因此,您需要灵活、可扩展、可扩展和可维护的设计和架构。
在本章中,我们将涵盖以下主题:
- 通用设计原则和模式入门
- 了解常见的企业架构
- 确定企业应用需求(业务和技术)
- 设计企业应用
- 企业应用的解决方案结构
到本章结束时,您将能够遵循正确的设计原则来设计和构建应用。
技术要求
你需要对……有一个基本的了解.NET Core、C#和 Azure。
常见设计原则和模式的入门
世界上的每一个软件都解决了一个或另一个现实世界的问题。随着时间的推移,事情发生了变化,包括我们对任何特定软件的期望。为了管理这种变化并处理软件的各个方面,工程师们开发了许多编程范例、框架、工具、技术、过程和原则。随着时间的推移,这些原则和模式已经被证明是工程师们用来协作和构建高质量软件的指路之星。
我们将在本章中讨论面向对象编程(OOP ),这是一种基于“对象”概念及其状态、行为和相互作用的范式。我们还将介绍一些常见的设计原则和模式。
原则是设计时要遵循的高级抽象准则;无论使用何种编程语言,它们都是适用的。它们没有提供实施指南。
模式是低层次的特定实现指南,是针对重复出现的问题的经过验证的、可重用的解决方案。我们先从设计原则说起。
设计原则
如果技术被广泛接受、实践并被证明在任何行业都有用,它们就会成为原则。这些原则成为使软件设计更容易理解、更灵活、更易维护的解决方案。我们将在这一部分介绍固体、KISS 和干燥设计原则。
固体
固体原则是由美国软件工程师兼讲师罗伯特·c·马丁提出的许多原则的子集。这些原则已经成为面向对象世界中事实上的标准原则,并且已经成为其他方法和范例的核心哲学的一部分。
固体是以下五个原则的缩写:
-
Single responsibility principle (SRP): An entity or software module should only have a single responsibility. You should avoid granting one entity multiple responsibilities:
图 1.1-SRP
-
Open-closed principle (OCP): Entities should be designed in such a way that they are open for extension but closed for modification. This means regression testing of existing behaviors can be avoided; only extensions need to be tested:
图 1.2–OCP
-
Liskov substitution principle (LSP): Parent or base class instances should be replaceable with instances of their derived classes or subtypes without altering the sanity of the program:
图 1.3–LSP
-
Interface segregation principle (ISP): Instead of one common large interface, you should plan multiple, scenario-specific interfaces for better decoupling and change management:
图 1.4–互联网服务提供商
-
依赖倒置原则 ( DIP ):应该避免对具体实现有任何直接依赖。高级模块和低级模块不应该直接相互依赖,相反,两者应该尽可能依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
图 1.5–DIP
不要重复自己(干)
有了 DRY,一个系统的设计应该使得一个特性或一个模式的实现不应该在多个地方重复。这将导致维护开销,因为需求的变化将导致需要在多个地方进行修改。如果您错误地在一个地方没有进行必要的更新,系统的行为将变得不一致。相反,该特性应该被打包成一个包,并且应该在所有地方重用。对于数据库,您应该考虑使用数据规范化来减少冗余:
图 1.6–干燥
这个策略有助于减少冗余和促进重用。这个原则也有助于组织的文化,鼓励更多的合作。
保持简单,愚蠢(KISS)
有了 KISS,一个系统应该是设计得尽可能简单,避免复杂的设计、算法、新的未尝试的技术等等。您应该专注于利用正确的面向对象概念,并重用经验证的模式和原则。只有在必要时才包含新的或非简单的东西,并为实现增加价值。
当您保持简单时,您将能够更好地完成以下工作:
- 设计/开发时避免错误
- 保持列车运行(总有一个团队的工作是维护系统,尽管他们不是最初开发系统的团队)
- 阅读并理解您的系统和代码(您的系统和代码需要能够被新用户或未来使用它的人理解)
- 做得更好,不容易出错的变更管理
有了这些,我们就完成了普通设计原理的入门;我们已经学习了固体、干燥和 KISS。在下一节中,我们将在现实世界的例子中查看一些常见的设计模式,以帮助您理解原则和模式之间的区别,以及何时利用哪种模式,这是优秀设计和架构必不可少的技能。
设计图案
在遵循 OOP 范式中的设计原则时,您可能会看到相同的结构和模式一遍又一遍地重复。这些重复的结构和技术是常见问题的成熟解决方案,被称为设计模式。经验证的设计模式易于重用、实现、更改和测试。著名的书籍设计模式:可重用面向对象软件的元素,包括被称为四人组 ( GOF )的设计模式,被认为是模式的圣经。
我们可以将 GOF 模式分类如下:
- 创意:有助于创建对象
- 结构性:有助于处理物体的构成
- 行为:有助于定义对象之间的交互和分配责任
让我们用一些现实生活中的例子来看看模式。
创新设计模式
让我们看看一些创造性设计模式以及下表中的相关例子:
表 1.1
结构设计模式
下表包括一些结构设计模式的例子:
表 1.2
行为设计图案
下表包括一些行为设计模式的示例:
表 1.3
有时候,你可能会被摆在桌面上的所有这些模式淹没,但实际上,任何设计都是好的设计,直到它违反了基本原则。我们可以使用的一个经验法则是回到基础,在设计中,原则是基础。
图 1.7–模式与原则
这样,我们就完成了普通设计原则和模式的入门。到目前为止,您应该已经很好地理解了不同的原则和模式,在哪里使用它们,以及构建一个伟大的解决方案需要什么。现在让我们花一些时间来看看常见的企业架构。
了解常见的企业架构
在设计企业应用时,通常会实践一些原则和架构。首先,任何架构的目标都是以尽可能低的成本(时间和资源成本)支持业务需求。企业希望软件能够支持它,而不是成为瓶颈。在当今世界,可用性、可靠性和性能是任何系统的三个关键绩效指标。
在这一节中,我们将首先研究单块架构的问题,然后我们将了解如何避免使用广泛采用且经过验证的架构来开发企业应用。
考虑一个经典的整体式电子商务网站应用,如下图所示,所有业务提供商和功能都在一个应用中,数据存储在一个经典的 SQL 数据库中:
图 1.8–单片应用
单片架构在 15-20 年前被广泛采用,但是随着时间的推移,当系统增长和业务需求扩展时,软件工程团队会遇到很多问题。让我们看看这种方法的一些常见问题。
单一应用的常见问题
让我们来看看缩放问题:
- 在单一应用中,水平扩展的唯一方法是向系统添加更多计算。这导致更高的运营成本和未优化的资源利用率。有时,由于资源方面的需求冲突,扩展变得不可能。
- 由于所有功能大多使用单个存储,因此存在锁导致高延迟的可能性,并且单个存储实例的扩展范围也存在物理限制。
以下是一些与可用性、可靠性和性能相关的问题:
- 系统的任何变化都需要重新部署所有组件,导致停机和低可用性。
- 任何非持久状态,如存储在网络应用中的会话,在每次部署后都会丢失。这将导致放弃用户触发的所有工作流。
- 模块中的任何错误,如内存泄漏或安全错误,都会使所有模块变得脆弱,并有可能影响整个系统。
- 由于模块内资源的高度耦合性和共享性,总是会出现资源的非优化使用,导致系统的高延迟。
最后,让我们看看对业务和工程团队的影响:
- 变更的影响很难量化,需要广泛的测试。因此,它降低了交付生产的速度。即使是很小的改变也需要重新部署整个系统。
- 在一个高度耦合的系统中,跨团队协作交付任何功能总是有物理限制的。
- 移动应用、聊天机器人和分析引擎等新场景将需要更多的努力,因为没有独立的可重用组件或服务。
- 连续部署几乎不可能。
让我们通过采用一些成熟的原则/架构来尝试解决这些常见问题。
关注点分离/单一责任架构
软件应根据其执行的工作类型划分为组件或模块。每个模块或组件都应该有一个单一的职责。组件之间的交互应该通过接口或消息传递系统进行。让我们看看 n 层和微服务架构,以及如何处理关注点分离。
多层体系结构
n 层架构将系统的应用划分为三个(或 n 层:
- 呈现(称为 UX 层、用户界面层或工作表面)
- 业务(称为业务规则层或服务层)
- 数据(称为数据存储和访问层)
图 1.9–N 层架构
这些层可以单独拥有/管理/部署。例如,多个表示层,如 web、移动和 bot 层,可以利用相同的业务和数据层。
微服务架构
微服务架构由小型、松散耦合、独立和自治的服务组成。让我们看看它们的好处:
- 服务可以独立部署和扩展。一个服务中的问题会对本地产生影响,只需部署受影响的服务即可解决。不需要共享技术或框架。
- 服务通过定义良好的应用编程接口或消息传递系统(如 Azure 服务总线)相互通信:
图 1.10–微服务架构
如上图所示,服务可以由独立的团队拥有,并且可以有自己的周期。服务负责管理自己的数据存储。要求更低延迟的场景可以通过引入缓存或高性能 NoSQL 存储来优化。
领域驱动架构
每个逻辑模块不应直接依赖于另一个模块。每个模块或组件应该服务于单个域。
围绕一个域建模服务可以防止服务爆炸。模块应该是松散耦合的,可能一起改变的模块可以组合在一起。
无状态服务架构
服务不应该有任何状态。状态和数据应该独立于服务进行管理,即在外部进行管理。通过向外部委托状态,服务将有资源以高可靠性服务更多的请求。
不应启用会话关联性,因为它会导致粘性会话问题,并会阻止您获得负载平衡、可伸缩性和流量分布的好处。
事件驱动架构
事件驱动架构的主要特点如下:
- 在事件驱动架构中,模块之间的通信(通常称为发布者-订阅者通信)主要是异步的,并且通过事件来实现。生产者和消费者是完全分离的。事件的结构是他们之间交换的唯一契约。
- 同一事件可以有多个消费者负责他们的特定操作;理想情况下,他们甚至不会意识到对方。生产者可以持续推动事件,而不用担心消费者的可用性。
- 发布者通过消息传递基础设施(如队列或服务总线)发布事件。发布事件后,消息传递基础结构负责将事件发送给符合条件的订户:
图 1.11–事件驱动架构
这种架构最适合本质上异步的场景。例如,长时间运行的操作可以排队等待处理。客户端可能会轮询状态,甚至充当事件的订阅者。
数据存储和访问架构
数据存储和访问架构在整个系统的可扩展性、可用性和可靠性方面发挥着至关重要的作用:
- 服务应该根据操作的需要来决定数据存储的类型。
- 应该根据给定操作的需要对数据进行分区和建模。无论如何都应该避免热分区。如果您需要来自同一数据的多种类型的结构,应该选择复制。
- 应该选择正确的一致性模型来降低延迟。例如,能够承受一段时间的陈旧数据的操作应该使用弱/最终一致性。有可能改变状态并需要实时数据的操作应该选择更强的一致性。
- 缓存适合服务的数据有助于服务的性能。应该确定可以缓存数据的区域。根据给定的需要,可以选择内存内或内存外缓存。
弹性架构
组件之间的通信越多,发生故障的可能性也越大。一个系统应该被设计成能从任何类型的故障中恢复。我们将介绍一些构建容错系统的策略,该系统可以在出现故障时自我修复。
如果您熟悉 Azure,您会知道应用、服务和数据应该在至少两个 Azure 区域中进行全局复制,以应对计划内停机和计划外的暂时或永久故障。在这些场景中,选择 Azure 应用服务来托管网络应用、使用 REST APIs 以及选择一个全球分布式数据库服务(如 Azure Cosmos DB)是明智的。选择 Azure 成对区域将有助于业务连续性和灾难恢复 ( BCDR ),因为如果停机影响多个区域,每对区域中至少有一个区域将优先恢复。现在,让我们看看如何处理不同类型的故障。
瞬时故障可能发生在任何类型的通信或服务中。您需要有一个从瞬时故障中恢复的策略,例如:
- 识别瞬时故障的操作和类型,然后确定适当的重试次数和时间间隔。
- 避免反模式,如具有有限重试次数的无限重试机制或断路器。
如果故障不是暂时的,您应该通过选择以下选项来优雅地响应故障:
- 失败
- 补偿任何失败的操作
- 抑制/阻止不良客户端/参与者
- 在失败的情况下使用领导人选举来选择领导人
遥测在这里起着很大的作用;您应该有自定义的指标来记录任何组件的运行状况。当自定义事件发生或特定指标达到特定阈值时,可以发出警报。
进化和运营架构
演进和运营在持续集成、部署、阶段性功能推出以及减少停机时间和成本方面发挥着至关重要的作用:
- 服务应该独立部署。
- 设计一个可扩展的生态系统,使企业能够随着时间的推移而发展和变化。
- 松散耦合的系统最适合企业,因为任何变更或特性都可以以良好的速度和质量交付。变更可以被管理并限定在单个组件的范围内。
- 规模的弹性导致更好的资源管理,进而降低运营成本。
- 持续的构建和发布管道以及蓝绿色的部署策略有助于在系统早期识别问题。这也使得能够在减少生产流量的情况下测试某些假设。
这样,我们就完成了对常见企业架构的覆盖。接下来,我们将通过我们所了解的设计原则和常见架构的镜头来看看企业应用需求和不同的架构。
识别企业应用需求(业务和技术)
在接下来的几章中,我们将构建一个工作的电子商务应用。它将是一个三层应用,由用户界面层、服务层和数据库组成。让我们看看这个电子商务应用的需求。
解决方案需求是要在产品中实现并可用的解决问题或实现目标的能力。
业务需求只是最终客户的需求。在 IT 界,“业务”一般指“客户”这些需求是从不同的利益相关者那里收集的,并作为单一的真实来源记录下来,供大家参考。这最终成为待完成工作的积压和范围。
技术要求是系统应该实现的技术方面,例如可靠性、可用性、性能和 BCDR。这些都是又称服务质量 ( QOS )的要求。
让我们将电子商务应用网站的典型业务需求细分为以下几类:史诗级、特色和用户故事。
应用的业务需求
以下来自 Azure DevOps 的截图显示了我们业务需求的积压汇总。您可以看到我们的应用中预期的不同特性以及用户故事:
图 1.12–Azure DevOps 的需求积压
应用的技术要求
看了业务需求,现在来看技术需求:
- 电商应用应该是高可用,即任何 24 小时周期 99.99%的时间可用。
- 电商应用应该是高可靠,即任何 24 小时周期 99.99%的时间是可靠的。
- 电子商务应用应该是高性能的 : 95%的操作应该用时小于等于 3 秒。
- 电子商务应用应该是高度可扩展的:它应该根据变化的负载自动上/下扩展。
- 电子商务应用应该有监控和警报:如果出现任何系统故障,应该向支持工程师发送警报。
以下是为电子商务应用确定的技术方面及其要求:
前端
- 使用 ASP.NET 5.0 的网络应用(电子商务)
核心组件
- C# 9.0 和中的日志/缓存/配置.NET 5.0
中间层
- 实现身份验证的 Azure 应用编程接口网关
- 通过 ASP.NET 5.0 网络应用编程接口添加/删除用户的用户管理服务
- 通过 ASP.NET 5.0 网络应用编程接口提供产品和定价服务,从数据存储中获取产品
- 通过 ASP.NET 5.0 网络应用编程接口获取域数据(如国家数据)的域数据服务。
- 通过 ASP.NET 5.0 网络应用编程接口完成支付的支付服务
- 通过 ASP.NET 5.0 网络应用编程接口提交和搜索订单的订单处理服务
- 通过 ASP.NET 5.0 网络应用编程接口生成发票的发票处理服务
- 通知服务通过 ASP.NET 5.0 网络应用编程接口发送电子邮件等通知
数据层
- 数据访问服务,通过 ASP.NET 5.0 网络应用编程接口与 Azure 宇宙数据库进行对话,以读取/写入数据
- 实体框架访问数据的核心
天蓝色堆栈
- Azure 宇宙数据库作为后端数据存储
- 异步消息处理的 Azure 服务总线
- 承载网络应用和网络应用接口的 Azure 应用服务
- Azure 流量管理器可实现高可用性和高响应性
- 用于诊断和遥测的 Azure 应用洞察
- 天蓝色配对区域可获得更好的弹性
- Azure 资源组创建 Azure 资源管理器 (ARM)模板并部署到 Azure 订阅
- 用于持续集成和持续部署的 azure Pipelines(CI/CD)
我们现在已经完成了企业应用需求。接下来,我们将研究企业应用的架构。
架构企业应用
下面的架构图描述了我们正在构建的东西。当我们设计和开发应用时,我们需要记住我们在本章中看到的所有设计原则、模式和需求。下图显示了我们的电子商务企业应用的建议架构图:
图 1.13–我们的电子商务应用的三层架构图
关注点/SRP 的分离在每一层都得到了处理。包含用户界面的表示层与包含业务逻辑的服务层分开,后者又与包含数据存储的数据访问层分开。
高级组件不知道使用它们的低级组件。数据访问层不知道使用它的服务,服务也不知道使用它们的 UX 层。
每个服务都根据它应该执行的业务逻辑和功能进行分离。
封装已经在架构级别得到了的处理,在开发过程中也应该得到处理。架构中的每个组件都将通过定义良好的接口和契约与其他组件进行交互。如果符合契约,我们应该能够替换图中的任何组件,而不用担心它的内部实现。
这里松散耦合的架构也有助于客户更快地开发和部署到市场。多个团队可以独立地并行处理每个组件。他们在开始时共享集成测试的合同和时间表,一旦内部实现和单元测试完成,他们就可以开始集成测试。
请参考下图:
图 1.14–我们的电子商务应用组件,按章节细分
从图中,我们确定了我们将构建的电子商务应用的不同部分将涵盖的章节,解释如下:
- 创建 ASP.NET 网络应用(我们的电子商务门户)将作为第 11 章创建 ASP.NET Core 5 网络应用的一部分进行介绍。
- 认证将作为第 12 章理解认证的一部分。
- 订单处理服务和发票处理服务是生成订单和开具发票的两个核心服务。他们将是电子商务应用的核心,因为他们是负责收入的人。创建 ASP.NET Core web API 将作为第 10 章的一部分,创建 ASP.NET Core 5 Web API 将作为第 5 章**依赖注入的一部分.NET* 、 第六章、、配置中.NET Core、第七章登录.NET 5、分别为。DRY 原则将通过重用核心组件和交叉关注来处理,而不是重复实现。*
** 在 第 8 章了解缓存中,缓存将作为产品定价服务的一部分。缓存将有助于提高我们系统的性能和可伸缩性,经常访问的数据的临时副本在内存中可用。* 数据存储、访问和提供者将作为数据访问层的一部分包含在 第 9 章中.NET 5 。我们采用的体系结构将数据和对数据的访问与应用的其他部分分开,这种体系结构为我们提供了更好的维护。Azure Cosmos DB 是我们的选择,可在全球任意数量的 Azure 地区弹性、独立地扩展吞吐量和存储。默认情况下,它也是安全的,企业级的。*
*我们关于构建企业应用的讨论到此结束。接下来,我们将看看我们企业应用的解决方案结构。
企业应用的解决方案结构
我们将为所有项目提供一个单一的解决方案,以保持简单,如下图所示。当解决方案中的项目数量激增并导致维护问题时,也可以考虑为用户界面、共享组件、网络应用编程接口等提供单独解决方案的另一种方法。下面的截图显示了我们应用的解决方案结构:
图 1.15–电子商务应用的解决方案结构
总结
在这一章中,我们学习了常见的设计原则,如固体、干燥和 KISS。我们还用现实世界的例子研究了各种设计模式。然后,我们查看了不同的企业架构,确定了我们将要构建的电子商务应用的需求,并应用我们所学的知识来构建我们的电子商务应用。现在,当您设计任何应用时,您都可以应用在这里学到的知识。在下一章中,我们将了解.NET 5 核心和标准。
问题
-
What is the LSP?
a.基类实例应该可以用其派生类型的实例替换。
b.派生类实例应该可以用其基类型的实例替换。
c.设计可以处理任何数据类型的泛型。
-
What is the SRP?
a.不要有一个通用的大型接口,而是计划多个场景特定的接口,以实现更好的解耦和变更管理。
b.您应该避免直接依赖于具体的实现;相反,你应该尽可能依赖抽象。
c.一个实体应该只有单一的责任。你应该避免给一个实体多重责任。
d.实体应该以这样的方式设计,它们应该对扩展开放,但对修改关闭。
-
What is the OCP?
a.实体应该对修改开放,但对扩展关闭。
b.实体应该对扩展开放,但对修改关闭。
c.实体应该对组合开放,但对扩展关闭。
d.实体应该对抽象开放,但对继承关闭。
-
Which pattern is used to make two incompatible interfaces work together?
a.代理
b.桥
c.迭代程序
d.介面卡
-
Which principle ensures that services can be deployed and scaled independently and that an issue in one service will have a local impact and can be fixed by just redeploying the impacted service?
a.领域驱动设计原则
b.单一责任原则
c.无状态服务原则
d.弹性原则*
二、.NET 5 核心和标准简介
.NET 是一个开发人员平台,提供构建许多不同类型应用的库和工具,如 web、桌面、移动、游戏、物联网 ( 物联网)和云应用。使用.NET,我们可以开发针对很多操作系统的应用,包括 Windows、macOS、Linux、Android、iOS 等等,它支持 x86、x64、ARM32、ARM64 等处理器架构。
.NET 还支持使用多种编程语言进行应用开发,如 C#、Visual Basic 和 F#,使用流行的集成开发环境 ( IDEs )如 Visual Studio、Visual Studio Code 和 Visual Studio for Mac。
之后.NET Core 3.1,.NET 5 现在是一个主要版本,包括 C# 9 和 F# 5,具有许多新特性和性能改进。
本章涵盖以下主题:
- 介绍.NET 5
- 了解的核心组成部分.NET 5
- 了解命令行界面
- 什么是?NET 标准?
- 理解.NET 5 跨平台和云应用支持
本章将帮助我们了解中包含的一些核心组件、库和工具.NET 开发应用。
技术要求
需要一台 Windows、Linux 或 Mac 机器。
介绍.NET 5
2002 年,微软发布了第一个版本的.NET 框架,一个开发 web 和桌面应用的开发平台。.NET Framework 提供了很多服务,包括托管代码执行、通过基类库的大量 API、内存管理、通用类型系统、语言互操作性,以及开发框架,如 ADO.NET、ASP.NET、WCF、WinForms、Windows Presentation Framework(【WPF】等。最初,它是作为一个单独的安装程序发布的,但后来它被集成并与 Windows 操作系统一起提供。。. NET Framework 4.8 是的最新版本.NET 框架。
2014 年,微软宣布开放源代码,跨平台实现.NET 调用.NET Core。.NET Core 是从零开始构建的,以使其跨平台,目前在 Linux、macOS 和 Windows 上都有。.NET Core 是快速和模块化的,并提供并行支持,因此我们可以运行不同版本的.NET Core 在同一台机器上运行而不影响其他应用。
.NET 5 是一个开源的、跨平台的实现.NET,您可以用它构建可以在 Windows、macOS 和 Linux 操作系统上运行的控制台、网络、桌面和云应用。.NET 5 是.NET 统一之旅,包括许多库、工具/SDK 以及运行时和性能改进。
接下来,让我们了解. NET 的核心特性
了解核心特性
以下是的几个核心特性.NET,我们将更深入地了解:
- 开源:.NET 是一个免费的(没有许可费用,包括商业用途)开源的开发者平台,为 Linux、macOS 和 Windows 提供了许多开发工具。其源代码由微软和.NET 社区。您可以访问.NET 资源库,网址为。
- 跨平台:.NET 应用运行在许多操作系统上,包括 Linux、macOS、Android、iOS、tvOS、watchOS 和 Windows。它们还可以跨 x86、x64、ARM32 和 ARM64 等处理器架构一致运行。
和.NET,我们可以构建以下类型的应用:
表 2.1
- 编程语言:.NET 支持多种编程语言。用一种语言编写的代码可以被其他语言访问。下表显示了支持的语言:
表 2.2
-
IDEs: .NET supports multiple IDEs. Let's understand each one:
a. Visual Studio 是一个功能丰富的 IDE,可在 Windows 平台上构建、调试和发布.NET 应用。它有三个版本:社区版、专业版和企业版。Visual Studio 2019 社区版对学生、个人开发人员和致力于开源项目的组织是免费的。
b.用于 Mac 的 Visual Studio是免费的,可用于 macOS。可以使用. NET 为 iOS、安卓和网络开发跨平台应用和游戏
c. Visual Studio Code 是一个免费、开源、轻量级但功能强大的代码编辑器,可在 Windows、macOS 和 Linux 上使用。它内置了对 JavaScript、TypeScript 和 Node.js 的支持,通过扩展,您可以添加对许多流行编程语言的支持。
d. Codespaces 目前处于预览阶段,是一个由 Visual Studio Code 提供动力的云开发环境,由 GitHub 托管开发.NET 应用。
-
Deployment models: .NET supports two modes of deployment:
a.自包含:当. NET 应用以自包含模式发布时,发布的工件包含.NET 运行时、库和应用及其依赖项。独立的应用是特定于平台的,目标机器不需要.NET 运行时已安装。机器使用.NET 运行时与运行应用的应用一起提供。
b.依赖框架的:当一个. NET 应用以依赖框架的模式发布时,发布的工件只包含应用及其依赖项。那个.NET 运行时必须安装在目标计算机上才能运行应用。
接下来,让我们了解. NET 提供的应用框架
理解应用框架
.NET 通过提供许多应用框架简化了应用开发。每个应用框架都包含一组库来开发目标应用。让我们详细了解每一个:
- ASP.NET 芯:这是一个开源跨平台应用开发框架,让你构建现代的、基于云的、互联网连接的应用,比如 web、IoT、API 应用。ASP.NET Core 是建立在.NET Core,因此您可以跨平台构建和运行,如 Linux、macOS 和 Windows。
- WPF :这是一个 UI 框架,让你为 Windows 创建桌面应用。WPF 使用可扩展应用标记语言 ( XAML ),一种用于应用开发的声明性模型。
- 实体框架 ( EF )核心:这个是一个开源的、跨平台的、轻量级的、对象关系映射 ( ORM )框架来与数据库配合使用.NET 对象。它支持 LINQ 查询、变更跟踪和模式迁移。它适用于流行的数据库,如 SQL Server、SQL Azure、SQLite、Azure Cosmos DB、MySQL 等。
- 语言-集成查询 ( LINQ ):这增加了查询功能.NET 编程语言。LINQ 允许您使用相同的 API 从数据库、XML、内存数组和集合中查询数据。
在下一节中,让我们了解. NET 的核心组件
了解的核心成分。网
.NET 有两个主要组件:运行时和基类库。运行时包括一个垃圾收集器 ( GC )和准时制 ( JIT )编译器,负责管理的执行.NET 应用和基类库 ( BCLs ),也称为运行时库或框架库,它们包含了的基本构造块.NET 应用。
那个.NET SDK 可在https://dotnet.microsoft.com/download/dotnet/5.0下载。它包含一组开发和运行的库和工具.NET 应用。您可以选择安装软件开发工具包或.NET 运行时。去发展.NET 应用,您应该在开发机器上安装 SDK.NET 运行时.NET 应用。那个。. NET 运行时包含在。因此你不必安装。如果您已经安装了.NET 软件开发工具包:
图 2.1–的可视化.NET 软件开发工具包
那个.NET 软件开发工具包包含以下组件:
- 公共语言运行时 ( CLR ): CLR 执行代码并管理内存分配。.NET 应用,在编译时,产生一个中间语言 ( IL )。CLR 使用 JIT 编译器将编译后的代码转换为机器代码。它是一个跨平台运行时,可用于 Windows、Linux 和 macOS。
- 内存管理:垃圾收集器管理的内存分配和释放.NET 应用。对于每一个新创建的对象,内存在托管堆中被分配,当没有足够的可用空间时,垃圾收集会检查托管堆中的对象,如果它们不再被应用使用,就将其删除。更多信息可以参考https://docs . Microsoft . com/en-us/dotnet/standard/垃圾收集。
- JIT :时.NET 代码被编译,它被转换成 IL。IL 与平台和语言无关,因此当运行时运行应用时,JIT 会将 IL 转换为处理器能够理解的机器代码。
- 通用类型系统:这定义了在 CLR 中如何定义、使用和管理类型。它支持跨语言集成并确保类型安全。
- 基类库:这个包含了像
System.String``System.Boolean
这样的原语类型的实现,像List<T>``Dictionary<TKey, TValue>
这样的集合,以及执行 I/O 操作、HTTP、序列化等等的实用函数。它简化了.NET 应用开发。 - 罗斯林编译器:罗斯林是一个开源的 C#和 Visual Basic 编译器,拥有丰富的代码分析 API。它支持使用与 Visual Studio 相同的应用编程接口构建代码分析工具。
- MSBuild :这是一个建造的工具.NET 应用。Visual Studio 使用 MSBuild 来生成.NET 应用。
- NuGet :这是一个开源的包管理器工具,你可以用它来创建、发布和重用代码。NuGet 包包含编译后的代码、其依赖文件和包含包版本号信息的清单。
在下一节中,让我们了解如何设置开发环境来创建和运行.NET 应用。
设置开发环境
建立一个开发环境非常容易。你需要.NET 软件开发工具包来构建和运行.NET 应用;或者,您可以选择安装支持.NET 应用开发。您需要执行以下步骤来设置。. NET 软件开发工具包:
注意
Visual Studio Community Edition 对个人开发者、课堂学习以及为研究或开源项目做出贡献的组织中的无限用户免费。它提供了与专业版相同的功能,但是对于高级功能,如高级调试和诊断工具、测试工具等,您需要有企业版。要比较功能,您可以访问https://visualstudio.microsoft.com/vs/compare。
-
在 Windows 机器上,从https://visualstudio.microsoft.com下载并安装 Visual Studio 16.8 或更高版本。
-
In the installation options, from Workloads, select .NET Core cross-platform development for .NET Core applications, as shown in the following screenshot:
图 2.2–Visual Studio 安装、工作负载选择
-
确认选择并继续完成安装。这将安装 Visual Studio 和.NET 5 软件开发工具包。
或者,您也可以执行以下步骤进行设置:
-
下载并安装.NET 5 SDK,适用于 Windows、macOS 和 Linux,来自https://dotnet.microsoft.com/download/dotnet/5.0。.NET Core 支持并行执行,因此我们可以安装多个版本的。在开发机器上。
-
From Command Prompt, run the
dotnet --version
command to verify the installed version, as shown in the following screenshot:图 2.3–dotnet 命令的命令行输出
-
Optionally, you can download and install Visual Studio Code from https://code.visualstudio.com to use it to develop the .NET application.
既然我们了解了如何为?NET,在下一节中,让我们了解.NET CLI 是什么,以及它如何帮助创建、构建和运行.NET 应用从命令行。
了解命令行界面
那个.NET CLI 是一个跨平台的命令行界面工具,可用于开发、构建、运行和发布.NET 应用。它包含在.NET SDK。
CLI 命令结构包含
command driver
(dotnet
)、command
、command-arguments
和options
,这是大多数 CLI 操作的常见模式。请参考以下命令模式:driver command <command-arguments> <options>
例如,以下命令创建了一个新的控制台应用。
dotnet
是司机,new
是命令,console
是模板名作为参数:dotnet new console
下表说明了命令行界面支持的一些命令和命令的简短描述:
表 2.3
让我们创建一个简单的控制台应用,并使用.NET 命令行界面:
注意
要执行以下步骤,作为先决条件,您应该具有.NET 软件开发工具包安装在您的机器上。可以从https://dotnet.microsoft.com/download/dotnet/5.0下载安装。
-
在命令提示符下,运行以下命令创建一个名为
HelloWorld
的控制台应用:dotnet new console --name HelloWorld
-
This command will create a new project called
HelloWorld
based on theconsole
application template. Refer to the following screenshot:图 2.4–新控制台应用的命令行输出
-
运行以下命令来构建和运行应用:
dotnet run --project ./HelloWorld/HelloWorld.csproj
-
前面的命令将构建并运行应用,并将输出打印到命令窗口,如下所示:
图 2.5–运行时控制台应用的命令行输出
在前面的步骤中,我们创建了一个新的控制台应用,并使用.NET 命令行界面。让我们理解global.json
的意义。
global . JSON 概述
在开发者机器上,如果有多个.NET SDKs 已安装,在global.json
文件中可以定义。用于运行.NET 命令行界面命令。一般来说,当没有定义global.json
文件时,会使用最新的版本的 SDK,但是您可以通过定义global.json
来覆盖此行为。
运行以下命令将在当前目录中创建一个global.json
文件。根据您的要求,您可以选择要配置的版本:
dotnet new globaljson --sdk-version 2.1.811
以下是通过运行前面的命令创建的示例global.json
文件:
{
"sdk": {
"version": "2.1.811"
}
}
这里,global.json
被配置为使用。. NET SDK 版 2.1.8.11。那个.NET CLI 使用这个 SDK 版本来构建和运行应用。
有关的更多信息.NET CLI,可以参考https://docs.microsoft.com/en-us/dotnet/core/tools。
在下一节中,让我们了解一下.NET 标准是。
是什么.NET 标准?
.NET 标准是一组可用于多种应用的 API 规范.NET 实现。每个新版本的都会添加新的 API.NET 标准。每一个.NET 实现的目标是特定版本的.NET 标准,并且可以访问该标准支持的所有 API.NET 标准版。
针对版本构建的库.NET 标准可用于使用。支持该版本的.NET 标准。因此,当构建库时,目标是.NET Standard 允许使用更多的 API,但只能在使用的版本构建的应用中使用.NET 实现来支持它。
下面的屏幕截图列出了的各种版本.NET 实现,这些实现支持.NET 标准 2.0:
图 2.6–.NET 标准 2.0 支持.NET 实现
例如,如果您开发了一个目标库.NET 标准 2.0,它可以访问超过 32,000 个 API,但是支持它的版本较少.NET 实现。如果您希望您的库能够以最大数量的.NET 实现,然后选择尽可能低的.NET 标准版,但是您需要在可用的 API 上妥协。
让我们了解何时使用.NET 5 和.NET 标准。
了解使用.NET 5 和。网络标准
.NET 标准使得在不同的.NET 实现,但是.NET 5 提供了一种更好的方式来共享代码并在多个平台上运行。.NET 5 统一了 API 来支持桌面、web、云和跨平台控制台应用。
.NET 5 实现.NET 标准 2.1,所以你现有的代码.NET 标准与.NET 5;你不需要改变目标框架的名字 ( TFM )除非你想要访问新的运行时特性、语言特性或者 API。您可以多目标到.NET 标准和.NET 5,这样您就可以访问新功能,并使您的代码对其他人可用.NET 实现。
如果您正在构建需要使用的新的可重用库.NET 框架,然后将它们定位到.NET 标准 2.0。如果你不需要支持.NET 框架,那么你可以选择.NET 标准 2.1 或.NET 5。建议瞄准.NET 5 来访问新的 API、运行时和语言特性。
使用.NET 命令行界面,运行以下命令创建一个新的类库:
dotnet new classlib -o MyLibrary
它创建一个类库项目,目标框架为.net5.0
或开发人员机器上最新可用的 SDK。
如果您检查MyLibrary\MyLibrary.csproj
文件,它应该看起来像下面的片段。您会注意到目标框架设置为net5.0
:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
</Project>
在使用创建类库时,您可以强制它使用目标框架的特定版本.NET 命令行界面。以下命令创建一个目标类库.NET 标准 2.0:
dotnet new classlib -o MyLibrary -f netstandard2.0
如果您检查MyLibrary\MyLibrary.csproj
文件,它看起来像下面的片段,其中目标框架是netstandard2.0
:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
</Project>
如果你创建了一个目标库.NET 标准 2.0,它可以在一个应用中访问.NET Core 以及.NET 框架。
可选地,您可以瞄准多个框架;例如,在下面的代码片段中,库项目被配置为以.NET 5.0 和.NET 标准 2.0:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net5.0;netstandard2.0</TargetFrameworks>
</PropertyGroup>
</Project>
当您配置应用以支持多个框架并构建项目时,您会注意到它为每个目标框架版本创建工件。参考以下截图:
图 2.7–构建面向多个框架的工件
我们来总结一下这里的信息:
- 使用.NET 标准 2.0 到之间共享代码.NET 框架和所有其他平台。
- 使用.NET 标准 2.1 在 Mono、Xamarin 和之间共享代码.NET Core 3.x。
- 使用.NET 5 进行代码共享。
在下一节中,让我们了解一下.NET 5 的跨平台能力和云应用支持。
理解.NET 5 跨平台和云应用支持
.NET 有很多实现。每个实现都包含运行时、库、应用框架(可选)和开发工具。有四个.NET 实现:。. NET 框架.NET 5、通用 Windows 平台 ( UWP )、Mono 以及所有这些实现共有的套 API 规范.NET 标准。
多个.NET 实现使您能够创建.NET 应用,目标是许多操作系统。你可以建造.NET 应用,用于以下目的:
表 2.4
让我们了解更多关于.NET 实现:
-
.NET Framework is the initial implementation of .NET. Using .NET Framework, you can develop Windows, WPF, web applications, and web and WCF services targeting the Windows operating system. .NET Framework 4.5 and above implement .NET Standard, so libraries that are built targeting .NET Standard can be used in .NET Framework applications.
注意
。. NET Framework 4.8 是的最后一个版本.NET 框架,未来不会发布新版本。微软将继续在 Windows 中包含它,并通过安全和错误修复来支持它。对于新开发,建议使用.NET 5 或更高版本。
-
UWP 是一个的实现.NET,您可以用它构建可在个人电脑、平板电脑、Xbox 等上运行的支持触摸的窗口应用。
-
Mono 是. NET 的一个实现。它是一个小的运行时,支持 Xamarin 开发原生的 Android、macOS、iOS、tvOS 和 watchOS 应用。它实现了.NET 标准和目标库.NET 标准可以用于使用 Mono 构建的应用。
-
.NET 5 is an open source, cross-platform implementation of .NET with which you can build console, web, desktop, and cloud applications that can run on Windows, macOS, and Linux operating systems. .NET 5 is now the primary implementation of .NET, which is built on a single code base with a uniform set of capabilities and APIs that can be used by .NET applications.
那个.NET 5 SDK,连同库和工具,也包含多个运行时,包括.NET 运行时、ASP.NET Core 运行时和.NET 桌面运行时。跑步.NET 5 应用,您可以选择安装.NET 5 SDK 或相应的平台和工作负载特定的运行时:
-
The.NET 运行时只包含运行控制台应用所需的组件。要运行网络或桌面应用,您需要安装 ASP.NET Core 运行时和.NET 桌面运行时。那个.NET 运行时在 Linux、macOS 和 Windows 上可用,支持 x86、x64、ARM32 和 ARM64 处理器架构。
-
ASP.NET Core 运行时使您能够运行网络/服务器应用,并且在 x86、x64、ARM32 和 ARM64 处理器架构的 Linux、macOS 和 Windows 上可用。
-
.NET 桌面运行时使您能够在 Windows 上运行基于 Windows/WPF 的桌面应用。它适用于 x86 和 x64 处理器体系结构。
的可用性.NET 多平台运行时使得.NET 5 跨平台;在目标机器上,您只需要安装您的工作负载所需的运行时并运行应用。
现在,让我们来探索 Azure 提供的服务.NET 5。
云支持
.NET 5 得到了包括 Azure、谷歌云和 AWS 在内的热门云服务提供商的支持。让我们了解一些可以运行的服务。Azure 中的. NET 5 应用:
- Azure 应用服务支持轻松部署和运行 ASP.NET Core 5 应用。Azure 应用服务为您提供了托管的机会。使用 x86 或 x64 处理器架构的 Linux 或 Windows 平台上的 NET 5 应用。更多信息可以参考https://docs.microsoft.com/en-in/azure/app-service/overview。
- Azure 功能支持部署和运行构建在之上的无服务器功能.NET 5。您可以在 Linux 或 Windows 上托管函数应用。更多信息可参考https://docs . Microsoft . com/en-us/azure/azure-functions/functions-overview。
- 码头工人.NET 5 应用运行在 Docker 容器上。您可以独立构建在 Docker 容器上运行的可部署、高度可扩展的微服务。官方.NET Core Docker 图像可在https://hub.docker.com/_/microsoft-dotnet获得,用于不同的.NET (SDK 或运行时)和操作系统。许多 Azure 服务支持 Docker 容器,包括 Azure 应用服务、Azure 服务结构、Azure 批处理、Azure 容器实例和 Azure Kubernetes 服务 ( AKS )。更多信息可参考https://docs . Microsoft . com/en-us/dotnet/core/docker/introduction。
和.NET 5,我们可以开发可以在云中运行的企业服务器应用或高度可扩展的微服务。我们可以为 iOS、安卓和 Windows 操作系统开发移动应用。.NET 代码和项目文件看起来很相似,开发人员可以重用技能或代码来开发针对不同平台的不同类型的应用。
总结
在这一章,我们学到了什么.NET 是及其核心特性。我们了解了.NET 及其支持的不同部署模型。接下来,我们了解了提供的核心组件、工具和库.NET,并学习了如何在机器上设置开发环境。
我们还看了。并使用创建了一个示例应用.NET 命令行界面。接下来,我们学到了什么.NET 标准是什么以及什么时候使用.NET 5 和.NET 标准,然后通过讨论各种。. NET 实现.NET 5 跨平台支持,以及云支持。
在下一章中,我们将学习 C# 9.0 的新特性。
问题
-
.NET Core is which of the following?
a.开放源码
b.跨平台
c.自由的
d.上述全部
-
The .NET Standard 2.0 library is supported by which of the following?
a.。. NET Framework 4.6.1 或更高版本
b..NET Core 2.0 或更高版本
c..NET 5
d.Mono 5.4+或更高版本
e.上述全部
-
The .NET CLI driver that is mandatory to run CLI commands is which of the following?
a.
net
b.
core
c.
dotnet
d.
none
-
The .NET SDK contains which of the following?
a.那个.NET 命令行界面
b.基类库
c.运行时
d.上述全部
进一步阅读
了解更多.NET 5,可以参考https://docs.microsoft.com/en-us/dotnet/core/introduction。
三、C# 9 简介
C#是一种优雅且类型安全的面向对象编程语言,它使开发人员能够构建许多类型的安全且健壮的应用,这些应用运行在.NET 生态系统,并且在 GitHub 发布的流行编程语言列表中排名前 5。
C#最初是由安德斯·海尔斯伯格在微软开发的,作为.NET 倡议。自 2002 年 1 月第一次发布以来,该语言不断增加新功能,以提高性能和生产力。
撰写本文时的最新版本 C# 9.0 于 2020 年 11 月发布,目标是.NET 5。虽然就新特性而言,C# 9.0 并不是一个主要版本,但是有很多有趣的增加和增强,我们将在本章中了解:
- 理解仅初始化设置器
- 使用记录类型
- 理解顶层语句
- 使用模式匹配检查对象
- 用目标类型表达式理解类型推断
- 理解静态匿名函数
- 使用模块初始化器进行紧急初始化
到本章结束时,您将熟悉 C# 9.0 的新增功能。此外,本章将帮助我们提升自我,以 C#构建我们的下一个企业应用。
技术要求
您将需要以下内容来理解本章的概念:
- 带有的 Visual Studio 2019 版.NET 5.0 运行时
- 对微软的基本了解。网
了解仅初始化设置器
在 C#中,我们使用在 C# 7.0 中引入的对象初始值设定项,通过使其可设置来设置属性值。但是如果我们希望只在对象初始化期间设置它们,我们需要编写大量的样板代码。在 C# 9.0 中,使用只初始化的设置器,可以让属性在对象创建期间初始化。只能为任何类或结构声明 only setters。
下面的代码片段定义了Order
类,其中OrderId
是一个只初始化的设置器。这强制OrderId
仅在对象创建期间初始化:
public class Order
{
public int OrderId { get; init; }
public decimal TotalPrice { get; set; }
}
我们可以在创建对象时实例化Order
类并初始化OrderId
,如下面的代码片段所示:
class Program
{
static void Main(string[] args)
{
Order orderObject = new Order
{
OrderId = 1,
TotalPrice = 10.0M
};
// orderObject.OrderId = 2 // This will result in
compilation error
Console.WriteLine($”Order: Id:
{orderObject.OrderId}, Total price:
{orderObject.TotalPrice}”);
}
}
我们可以在前面的代码中看到,如果我们试图更改OrderId
的值,我们会得到一个编译时错误。
仅初始化属性应用于强制可选属性的不变性,构造函数初始化应用于强制必需值。
在下一节中,我们将探索 C# 9.0 引入的新类型定义。
使用记录类型
作为一名开发人员,您希望引用类型具有不变性,尤其是在使用共享数据进行并发编程时。为了实现引用类型的不变性,我们必须编写相当多的代码。C# 9.0 中引入的记录类型提供类型声明来创建不可变的引用类型。与引用类型不同,他们通过比较属性的相等性来合成相等性的方法,而不是比较对象的哈希代码。下面显示了保存形状名称的Shape
记录类型的声明:
public record Shape(string Name);
构建项目,并在诸如 ILSpy 或 Reflector 之类的反汇编工具中打开库。你可以从https://marketplace.visualstudio.com/items?安装 ILSpyitemName =夏普发展团队。。在 ILSpy 中,我们看到Shape
记录类型的定义如下图所示:
图 3.1–形状记录类型的分解
如果我们仔细观察Shape
类型定义,我们可以看到由 C#编译器为记录类型合成的所有管道。由此我们可以理解,记录类型基本上是一个类,用Equality
和GetHashCode
合成来模仿值类型的行为。它还提供了Deconstruct
方法,可用于将记录类型解构为其组件属性。ToString
方法也被覆盖以打印记录类型的属性。
为了理解所有这些特性,让我们继续创建一个Shape
记录类型的实例,如下面的代码所示:
public record Shape(string Name);
class Program
{
static void Main(string[] args)
{
Shape s1 = new Shape(“Shape”);
Shape s2 = new Shape(“Shape”);
// ToString of record is overwritten to print the
properties of the type
Console.WriteLine(s1.ToString());
// GetHashCode of record is overwritten to generate
the hash code based on values
Console.WriteLine($”HashCode of s1 is :
{s1.GetHashCode()}”);
Console.WriteLine($”HashCode of s2 is :
{s2.GetHashCode()}”);
// Equality operator of record type is overwritten
to check equality based on the values
Console.WriteLine($”Is s1 equals s2 : {s1 == s2}”);
}
}
如果我们运行前面的代码,我们会得到以下输出:
图 3.2–程序控制台输出
从前面的截图中,我们可以看到,正如我们在前面的代码中了解到的,ToString
获得了对象的详细信息。我们收到的s1
和s2
对象的哈希代码与根据对象的详细信息计算的相同。s1
和s2
之间的相等检查返回True
,因为==
操作符被覆盖以比较对象的数据。
我们已经看到了基本上是什么记录类型。在下一节中,我们将了解with
表达式,它可以与记录类型一起使用,通过细化零个或多个属性,从现有对象创建新对象。
带表情的
我们可以使用with
表达式来指示编译器从另一个实例创建一个记录类型的实例:
Person person = new(“Suneel”, “Kunani”);
Person person2 = person with { FirstName = “Mahanya” };
在前面的代码片段中,Person
对象的新实例person2
是通过复制字段的值并仅修改在with
表达式中指定的属性,从现有的person
实例创建的。
我们现在已经了解了记录类型。在下一节中,我们将了解 C# 9.0 中添加的另一个特性,即顶级语句。
理解顶级语句
在 C# 9.0 中,语言团队专注于移除开发人员要编写的冗余代码。这样做的一个特征是顶级语句。这个特性使开发人员能够在应用的主入口点移除仪式代码。
如果我们在 Visual Studio 中创建一个控制台应用,我们在Program.cs
中看到的内容如下面的代码片段所示:
using System;
namespace TopLevelStatements
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(“Hello World!”);
}
}
}
在这段代码中,唯一起作用的语句是Console.WriteLine(“Hello World!”);
。所有剩下的台词都是不必要的仪式声明。使用 C# 9.0,我们可以在不必要的地方去掉这些语句。现在我们可以用一行代码替换所有这些语句,如下面的代码片段所示:
System.Console.WriteLine(“Hello World!”);
这只是 C#编译器提供的语法糖。当它看到任何顶级语句时,它会添加所有必需的管道。让我们看看反射的代码,如下面的截图所示,带有顶级语句:
图 3.3–顶层语句的分解
应用中只有一个文件可以有顶级参数,因为该应用只能有一个入口点。如果多个文件包含顶级语句,它将显示编译错误。我们可以访问用args
变量名传递的命令行参数,如下面的代码所示:
foreach (var v in args)
{
System.Console.WriteLine(v);
}
在下一节中,我们将了解模式匹配,这是 C# 7.0 中引入并在后续版本中增强的 C#令人兴奋的特性之一。
用模式匹配检查对象
模式匹配是一种方法,用于检查对象的值或与序列完全或部分匹配的属性的值。这在 C#中以if…else
和switch…case
语句的形式得到支持。在现代语言中,特别是在函数式编程语言(如 F#)中,有对模式匹配的高级支持。在 C# 7.0 中,引入了新的模式匹配概念。它们在 C#的更高版本(即 8.0 和 9.0)中得到进一步增强。
模式匹配提供了一种不同的方式来表达条件,以获得更易于人类阅读的代码。为了理解模式匹配,我们还将介绍 7.0 和 8.0 版本中的概念。让我们在接下来的章节中更深入地探讨模式匹配。
不变的模式
常量模式匹配根据常量检查对象的值。通过常量模式匹配,我们将针对常量断言一个对象的值。
下面的代码片段是一个示例,应用常量模式检查Width
是否为0
。我们用恒定模式实现的结果也可以用平等操作符==
来实现。唯一的区别是常量模式不能被覆盖:
var rectangle = new Rectangle { Height = 10, Width = 0 };
if (rectangle.Width is 0)
{
Console.WriteLine(“The rectangle]s width is 0, it
will look like a standing line”);
}
常量模式也可以应用于空值检查,如以下代码所示:
if (rectangle is not null)
{
Console.WriteLine(“The rectangle is defined”);
}
类型模式
顾名思义,类型模式用于检查对象的类型,如下面的代码片段所示:
if (rectangle is Rectangle rect)
{
Console.WriteLine($”The area of rectangle is
{rect.Width * rect.Height}”);
}
如果没有模式匹配,我们将不得不使用一个运算符来检查类型,并使用一个运算符来转换为特定的类型。
属性模式
属性模式用于检查和探索对象属性和嵌套属性。下面的代码片段将rectangle
对象的Height
属性声明为0
:
if (rectangle is { Height: 0 })
{
Console.WriteLine(“The rectangle’s height is 0, it will
look like a sleeping line”);
}
属性模式可以用来创建强大的表达式。
连接和分离模式
合取和析取模式很像 的逻辑&&
和||
运算符来配对表达式。
以下代码片段加入了使用and
连接模式检查矩形的Height
属性是否大于0
且小于或等于100
的条件:
if (newRectangle is Rectangle { Height: > 0 and <= 100 })
{
Console.WriteLine(“This is a rectangle with maximum height of 100”);
}
析取模式为or
检查or
条件。在下面的代码片段中,分离模式验证高度小于5
或大于或等于10000
:
if (newRectangle is Rectangle { Height: < 5 or >= 10000 })
{
Console.WriteLine(“This is a rectangle is either too
small or too big”);
}
在下一节中,我们将学习如何使用模式匹配switch
表达式。
与开关表达式的模式匹配
在早期版本的 C#中,switch
表达式只支持常量值的int
、char
、bool
、string
、case
等整型。C#中新的模式匹配特性支持用代码表达控制流的新方法。我们用下面的例子来理解这个。
为了计算给定形状的面积,在旧版本中,我们将检查形状的类型并相应地计算面积。利用 C#中的模式匹配特性,我们可以编写GetShapeArea
如下代码所示:
static double GetShapeArea(object o)
{
var result = o switch
{
Circle c => (22.0/7.0)* c.Radius * c.Radius,
Rectangle r => r.Width * r.Height,
_ => throw new ArgumentException(“Not recognized
shape”)
};
return result;
}
在前面的代码中,switch
表达式验证形状对象的类型,并利用相应的公式计算形状的面积。
在下一个例子中,让我们看看switch
表达式中的关系模式:
static float GetProductDiscount(Product product)
{
float discount = product switch
{
Product p when p.Quantity is >= 10 and < 20 =>
0.05F,
Product p when p.Quantity is >= 20 and < 50 =>
0.10F,
Product p when p.Quantity is >= 50 =>
0.10F,
_ => throw new ArgumentException(nameof(product))
};
return discount * product.UnitPrice * product.Quantity;
}
在上例中,switch
表达式用于根据产品数量识别折扣。每个案例检查产品数量的上限和下限,并确定要应用的折扣。
元组模式
元组模式允许我们基于多个值构建一个表达式。下面的代码利用元组模式来获得AND
门的结果。我们知道,如果所有值都为真,那么AND
门返回true
,否则返回false
:
static bool AndGate(bool x, bool y) =>
(x, y) switch
{
(false, false) => false,
(false, true) => false,
(true, false) => false,
(true, true) => true
};
随着 C#中模式匹配的新增加,我们可以编写更复杂的表达式,并提高代码的可读性。
在下一节中,我们将了解目标类型表达式,其中 C#编译器将帮助根据上下文找到类型。
用目标类型表达式理解类型推理
目标类型表达式删除了创建对象时对类型的多余提及。对于目标类型表达式,类型是从其使用的上下文中推断出来的。在 C#中,我们使用new
关键字通过指定类型来实例化一个对象。要创建StringBuilder
的对象,以下是示例代码片段:
StringBuilder sb = new StringBuilder();
在这个代码片段中,StringBuilder
类型可以从上下文中推断出来。使用 C# 9.0,使用目标类型表达式,我们可以编写与下面示例中所示相同的代码片段。这里StringBuilder
类型是推断出来的:
StringBuilder sb = new ();
在下一节中,我们将学习静态匿名函数如何帮助我们识别和解决程序执行的非预期行为。
理解静态匿名函数
匿名函数是一组语句或表达式,可以在期望委托类型时使用或执行。使用匿名函数时,如果我们不小心引用了匿名函数中的局部变量,我们可能会得到意想不到的行为。通过使用静态匿名函数,我们可以避免无意中使用状态或局部变量。
考虑下面的代码片段来理解静态匿名函数:
string formatString = “Format String”;
void GenerateSummary(string[] args)
{
GenerateOrderReport(() =>
{
return formatString;
});
}
static string GenerateOrderReport(Func<string> getFormatString)
{
var order = new
{
Orderid = 1,
OrderDate = DateTime.Now
};
return string.Format(getFormatString(), order.Orderid);
}
从前面的代码中,我们看到GenerateOrderReport
函数接受了一个接受格式字符串的函数参数。传递到GenerateOrderReport
的匿名函数返回formatString
实例格式字符串值。
如果这里的意图是让成为生成订单报告的格式字符串,我们将得到意想不到的结果。我们不会得到任何编译错误,因为代码是合法的。
为了解决这些错误,我们可以利用静态匿名函数,如下所示:
void GenerateSummary(string[] args)
{
GenerateOrderReport(static () =>
{
// return formatString; // Will get error
return $”Order Id:{1}, Order Date:{1}”;
});
}
如前面的代码片段所示,将匿名函数更改为静态匿名函数将导致编译错误,因为静态函数中使用了非静态变量。这将迫使开发人员修复正确的格式字符串。
在下一节中,让我们了解一下模块初始化器,这将有助于在加载程序集时执行急切的初始化代码。
用模块初始化器进行急切初始化
可能会有这样一种情况,当模块初始化时,必须急切地执行某些代码。虽然这是一个利基场景,但如果我们有这样的要求,它将变得相当难以实现。随着 C# 9.0 中模块初始化器的引入,这很容易实现。
要在模块初始化时运行任何代码,我们只需用ModuleInitializer
属性标记该方法,如下代码所示:
[ModuleInitializer]
internal static void Initialize()
{
Console.WriteLine(“Module Initialization”);
}
那个.NET 运行时将在首次加载程序集时执行标记有ModuleInitializer
属性的方法。
下面是模块初始化器方法的要求:
- 一定是
static
法。 - 返回类型必须是
void
。 - 它不能是泛型方法。
- 它必须是无参数方法。
- 该方法必须可从包含模块中访问。
如果我们不遵守这些,我们将会得到一个编译错误。
总结
在本章中,我们已经了解了 C#语言特性的主要新增功能。我们已经了解了记录类型,它可以帮助我们用最少的管道代码构建不可变的引用类型。我们还学习了模式匹配,通过利用这一点,我们可以编写具有增强的代码可读性的复杂表达式。我们还看到了 C# 9.0 如何通过移除冗余的仪式代码来帮助开发人员处理顶级语句和目标类型表达式。
通过这一章,我们已经掌握了在企业电子商务应用中利用这些新的 C# 9.0 特性的技能,我们将在接下来的章节中构建这些特性。我们将重点介绍 C# 9.0 和的新特性.NET 5,同时实现我们的电子商务应用的不同特性。
在下一节中,我们将了解构成电子商务应用构建模块的交叉问题。
问题
阅读本章后,您应该能够回答以下问题:
-
True or False: We can only set the value to init-only setters during object initialization?
a.真实的
b.错误的
-
True or False: Record types are basically class types?
a.真实的
b.错误的
-
True or False: Top-level statements can be present in more than one file of an application?
a.真实的
b.错误的
-
In which version of C# was pattern matching first introduced?
a.C# 9
b.C# 8
c.C# 1
d.C# 7
四、线程和异步操作
到目前为止,我们看了各种设计原则,模式,什么是新的.NET 5,以及我们将在本书中使用的架构指南。在本章中,我们将看到如何在构建企业应用时利用异步编程。任何 web 应用的关键衡量标准之一是可伸缩性,也就是说,可伸缩性可以减少服务请求所需的时间,增加服务器可以处理的请求数量,并在不增加加载时间的情况下增加应用可以同时服务的用户数量。对于移动/桌面应用,缩放可以提高应用的响应能力,允许用户在不冻结屏幕的情况下执行各种操作。适当使用异步编程技术和并行构造可以在改进这些度量方面创造奇迹,在 C#中最好的事情就是任务并行库 ( TPL )的简化语法,async-wait,用它我们可以编写干净的异步代码。
在本章中,我们将涵盖以下主题:
- 理解行话
- 线程去神秘化、延迟初始化和
ThreadPool
- 理解锁、信号量和
SemaphoreSlim
- 介绍任务和相似之处
- 引入异步等待
- 使用并行集合实现并行
技术要求
你需要对……有一个基本的了解.NET Core、C#和 LINQ 的基础知识。本章的代码示例可以在这里找到:https://github . com/PacktPublishing/Enterprise-Application-Development-with-C-Sharp-9-and-.NET-5/树/主/第 04 章
这里有一些代码说明:
理解行话
在我们深入线程和异步操作的技术细节之前,让我们举一个真实的例子,在现实生活中的多任务和并行编程之间建立一个类比。想象一下,你在一家餐馆排队点餐,在排队等候时,你回复了一封电子邮件。然后,在点了食物并等待食物到达的同时,你回复了另一封邮件。在餐厅里,有多个柜台,在那里接受订单,在下订单时,食物由厨师准备。
当你排队等候时,你同时回复了一封电子邮件。同样,当你在点菜的时候,餐馆也在许多其他柜台平行点菜。下订单时,厨师正在平行烹饪。此外,还会给你一个代币,让你从取货柜台取食物;但是,根据您食物的准备时间,在您之后下的订单可能会在您之前到达取货柜台。
说起并行编程,有些关键术语会出现多次。下图表示了这种行话:
图 4.1–并发与并行和异步
让我们涵盖每个术语:
- 并行:这需要同时独立执行多个任务,就像从不同柜台下多个餐厅订单的例子一样。就企业应用而言,并行性是在多核 CPU 中同时执行多个线程/任务。然而,单核 CPU 也通过超线程支持并行性,这通常涉及将单核逻辑划分为多个内核,如支持超线程的双核 CPU,其行为类似于四核,即四核。
- 并发:这需要同时做很多任务,比如我们前面的例子在餐厅柜台排队的时候回复邮件,或者厨师给菜 1 调味,给菜 2 加热锅。就企业应用而言,并发涉及多个线程共享一个核心,并基于它们的时间分片来执行任务和执行上下文切换。
- 异步:异步编程是一种依赖于异步执行任务的技术,而不是在当前线程等待时阻塞它。在我们的例子中,异步是等待你的令牌被调用,让你去取货柜台,而厨师正在准备你的食物,但是当你等待的时候,你已经离开了订餐柜台,从而允许下其他订单。这就像一个异步执行的任务,在等待输入/输出任务时释放资源(例如,在等待数据库调用的数据时)。异步的美妙之处在于任务可以并行执行,也可以并发执行,这完全是由框架从开发人员那里抽象出来的,让开发人员将开发精力集中在应用的业务逻辑上,而不是管理任务上。我们将在任务和平行部分看到这一点。
- 多线程:多线程是一种实现并发的方式,新线程手动创建并并发执行,就像
CLR ThreadPool
一样。在多核/多处理器系统中,多线程通过在不同的内核中执行新创建的线程来帮助实现并行性。
现在我们已经理解了并行编程中的关键术语,接下来让我们看看如何创建线程以及ThreadPool
在中的作用.NET Core。
解除线程神秘化、延迟初始化和线程池
线程是 Windows 中最小的单元,它在处理器中执行指令。进程是一个更大的执行容器,进程内部的线程是使用处理器时间和执行指令的最小单位。要记住的关键一点是,每当你的代码在一个进程中需要被执行时,它应该被分配给一个线程。每个处理器一次只能执行一条指令;这就是为什么在单核系统中,在任何点时间只有一个线程在执行。有一些调度算法用于为线程分配处理器时间。一个线程通常有一个堆栈(跟踪执行历史)、存储各种变量的寄存器和保存需要执行的指令的计数器。
快速查看任务管理器将为我们提供有关物理和逻辑内核数量的详细信息,导航到资源监视器将告诉我们每个内核的 CPU 使用情况。下图显示了支持超线程的四核 CPU 的详细信息,它可以在任意时间点并行执行八个线程:
图 4.2–任务管理器和资源监视器
中的典型应用.NET Core 启动时只有一个单线程,通过手动创建可以添加更多线程。下面几节将简要介绍是如何做到的。
使用系统。螺纹,螺纹
我们可以通过创建一个System.Threading.Thread
的实例并传递一个方法委托来创建新的线程。下面是一个简单的例子,它模拟从应用编程接口检索数据和从磁盘加载文件:
static void Main(string[] args)
{
Thread loadFileFromDisk = new
Thread(LoadFileFromDisk);
loadFileFromDisk.Start();
Thread fetchDataFromAPI = new
Thread(FetchDataFromAPI);
fetchDataFromAPI.Start("https://dummy/v1/api"); //Parameterized method
Console.ReadLine();
}
static void FetchDataFromAPI(object apiURL)
{
Thread.Sleep(2000);
Console.WriteLine("data returned from API");
}
static void LoadFileFromDisk()
{
Thread.Sleep(2000);
Console.WriteLine("File loaded from disk");
}
在之前的代码中,FetchDataFromAPI
和LoadFileFromDisk
是将在新线程上运行的方法。
小费
在任何时间点,每个内核上只执行一个线程;也就是说,只有一个线程被分配了 CPU 时间。因此,为了实现并发性,操作系统 ( 操作系统)会在分配了 CPU 时间的线程空闲时或高优先级线程到达队列时进行上下文切换(也可能有其他原因,例如线程正在等待同步对象或达到了分配的 CPU 时间)。因为一个被切换出去的线程不会完成它的工作,在某个时候它会再次被分配 CPU 时间。因此,操作系统需要保存线程的状态(其堆栈、寄存器等),并在分配给线程 CPU 时间时再次检索。上下文切换通常非常昂贵,也是性能提升的关键领域之一。
Thread
类的所有属性和方法可以在上进一步查看。view=net-5.0 。
尽管管理的优势在于可以更好地控制线程的执行方式,但它也带来了以下形式的开销:
- 管理线程的生命周期,如创建线程、回收线程和上下文切换。
- 实现一些概念,例如线程执行的进度跟踪/报告。此外,取消相当复杂,支持有限。
- 线程上的异常需要适当处理,否则可能导致应用崩溃。
- 调试、测试和代码维护可能会变得有点复杂,如果处理不当,有时会导致性能问题。
这就是公共语言运行时 ( CLR ) ThreadPool
发挥作用的地方,这将在下一节讨论。
线程池
可以通过使用由管理的线程池来创建线程.NET Core,更俗称 CLR ThreadPool
。CLR ThreadPool
是一组工作线程,与 CLR 一起加载到您的应用中,并负责线程生命周期,包括回收线程、创建线程和支持更好的上下文切换。CLR ThreadPool
可以被System.Threading.ThreadPool
类中可用的各种 API 使用,特别是对于调度线程上的操作,有QueueUserWorkItem
方法,它接受需要调度的方法的委托。在前面的代码中,让我们用下面的代码替换创建新线程的代码,这意味着应用将使用ThreadPool
:
ThreadPool.QueueUserWorkItem(FetchDataFromAPI);
顾名思义,ThreadPool
类的QueueUserWorkItem
确实使用了队列,因此任何应该在ThreadPool
线程上执行的代码都会被排队,然后出列,也就是说,以先进先出(先进先出)的方式分配给一个工作线程。
ThreadPool
的设计方式是它有一个全局队列,当我们执行以下操作时,项目会在其中排队:
- 使用不属于
ThreadPool
线程的线程调用QueueUserWorkItem
或ThreadPool
类的类似方法。 - 通过任务并行库 ( 第三方物流)调用。
当在ThreadPool
中创建新线程时,它维护自己的本地队列,该队列实际上查看全局队列,并以先进先出的方式将工作项出列;但是,如果在这个线程上执行的代码进一步创建了另一个线程,比如一个子线程,那么它就会在本地队列中排队,而不是在全局队列中排队。工作线程的本地队列中的操作的执行顺序总是后进先出,其原因是最近创建的工作项在缓存中可能仍然很热,因此可以快速执行。同样,我们可以说在任何时间点都会有ThreadPool
中的 n+1 队列,其中 n 是ThreadPool
中的线程数,即 n 本地队列,而 1 是指全局队列。
ThreadPool
的高级表示如下图所示:
图 4.3–线程池高级表示
除了QueueUserWorkItem
之外,还有很多其他属性/方法可用于ThreadPool
类,例如:
SetMinThreads
:用于设置程序启动时ThreadPool
将拥有的最小工作线程和异步 I/O 线程。SetMaxThreads
:用于设置ThreadPool
将拥有的最大工作线程和异步 I/O 线程,之后新的请求将排队。
ThreadPool
类的所有属性和方法可以在上进一步查看。view=net-5.0 。
虽然通过ThreadPool
线程的QueueUserWorkItem
编写多线程代码简化了线程的生命周期管理,但它也有自己的局限性:
- 我们无法从
ThreadPool
线程上调度的工作中得到响应,因此委托的返回类型是无效的。 - 跟踪在
ThreadPool
线程上安排的工作进度并不容易,所以像进度报告这样的事情并不容易实现。 - 它不适合长时间运行的请求。
ThreadPool
线程始终是后台线程;因此,与前台线程不同,如果一个进程关闭,它不会等待ThreadPool
线程完成工作。
由于QueueUserWorkItem
有限制,ThreadPool
线程也可以通过第三方物流消耗,我们将在我们的企业应用中使用它,这将在本章后面介绍。英寸 NET Core,第三方语言无疑是实现并发/并行的首选方法,因为它克服了我们迄今为止看到的所有限制,并最终有助于实现允许应用扩展和响应的目标。
惰性初始化
类的惰性初始化是一种模式,其中对象的创建被推迟到第一次使用时。这种模式基于这样一个前提,即只要没有使用类的属性,初始化对象就没有任何好处。因此,这会延迟对象的创建,并最终减少应用的内存占用并提高性能。例如,只有当您要从数据库中检索数据时,才创建数据库连接对象。惰性初始化非常适合保存大量数据并且创建成本可能很高的类。例如,用于加载电子商务应用中所有产品的类只能在需要列出产品时进行延迟初始化。
如下所示,此类的典型实现限制了构造函数中属性的初始化,并具有一个或多个填充类属性的方法:
public class ImageFile
{
string fileName;
object loadImage;
public ImageFile(string fileName)
{
this.fileName = fileName;
}
public object GetImage()
{
if (loadImage == null)
{
loadImage = File.ReadAllText(fileName);
}
return loadImage;
}
}
假设这是一个用于从磁盘加载映像的类,那么在构造函数中加载映像是没有用的,因为在调用GetImage
方法之前无法使用它。因此,惰性初始化模式建议不要在构造函数中初始化loadImage
对象,而应该在GetImage
中初始化,这意味着只有在需要时才会将图像加载到内存中。这也可以通过如下所示的属性来实现:
object loadImage;
public object LoadImage
{
get
{
if (loadImage == null)
{
loadImage = File.ReadAllText(fileName);
}
return loadImage;
}
}
如您所见,这是缓存对象的典型做法,也称为缓存备用模式,当第一次访问特定对象时,我们会在缓存中加载一个对象。然而,这种实现在多线程代码中有一个挑战,在多线程代码中,对同一文件的磁盘调用可能会发生多次;也就是说,如果两个线程调用LoadImage
方法或属性,会导致多次调用磁盘。因此,这里肯定需要通过锁或其他机制进行同步,这显然会增加维护开销,并且类实现可能会变得更加复杂。
因此,即使我们可以实现自己的惰性加载模式,在 C#中,我们也有System.Lazy
类来处理这样的实现。使用System.Lazy
类的一个主要优点是它是线程安全的。
System.Lazy
类提供多个构造函数来实现惰性初始化。以下是我们可以利用的两种最常见的方法:
-
Wrapping the class around
Lazy
and using theValue
method of that object to retrieve data. This is typically used for classes that have initialization logic in constructors. Some sample code follows:public class ImageFile { string fileName; public object LoadImage { get; set; } public ImageFile(string fileName) { this.fileName = fileName; this.LoadImage = $"File {fileName} loaded from disk"; } }
初始化此类时,我们将使用
System.Lazy
类的泛型类型,并将ImageFile
类作为其类型传递,并将ImageFile
的对象作为委托传递:Lazy<ImageFile> imageFile = new Lazy<ImageFile>(() => new ImageFile("test")); var image = imageFile.Value.LoadImage;
这里,如果在
ImageFile
类的构造函数中放置一个断点,那么只有在调用System.Lazy
类的Value
方法时才会被命中。 -
For classes that have a method to load various parameters, we can pass the method to the
Lazy
class as a delegate. Taking the same previous sample code and moving the file-retrieving logic to a separate method is shown here:public class ImageFile { string fileName; public object LoadImage { get; set; } public ImageFile(string fileName) { this.fileName = fileName; } public object LoadImageFromDisk() { this.LoadImage = $"File {this.fileName} loaded from disk"; return LoadImage; } }
在初始化这个类时,我们将一个 lambda 传递给泛型委托,该泛型委托被传递来初始化
System.Lazy
类的一个对象,如下面的代码所示:Func<object> imageFile = new Func<object>(() => { var obj = new ImageFile("test"); return obj.LoadImageFromDisk(); }); Lazy<object> lazyImage = new Lazy<object>(imageFile); var image = lazyImage.Value;
这两种方式都将延迟对象的初始化,直到调用Value
方法。我们需要注意的一件重要的事情是,虽然Lazy
对象是线程安全的,但是通过值创建的对象不是线程安全的。因此,在这种情况下,lazyImage
是线程安全的,但image
不是。因此,它需要在多线程环境中同步。
一般来说,惰性初始化非常适合缓存类和单例类,并且可以进一步扩展到初始化成本较高的对象。
Lazy
类的所有属性可以在https://docs.microsoft.com/en-us/dotnet/api/system.lazy-1?进一步查看视图= net-5.0 #定义。
虽然惰性初始化可以通过用System.Lazy
类包装底层对象来实现,但是在中也有LazyInitializer
静态类.NET,可通过其EnsureInitialized
方法用于惰性初始化。
它有几个构造函数,正如 MSDN 文档中提到的:view=net-5.0。然而,想法是相同的,因为它期望一个对象和一个函数来填充该对象。举前面的例子,如果我们必须使用LazyInitializer.EnsureInitialized
进行惰性初始化,我们需要将对象的实例和创建实际对象的 lambda 传递给LazyInitializer.EnsureInitialized
,如下面的代码所示:
object image = null;
LazyInitializer.EnsureInitialized(ref image, () =>
{
var obj = new ImageFile("test");
return obj.LoadImageFromDisk();
});
这里我们是传递两个参数:一个是保存image
类属性值的对象,另一个是创建image
类对象并返回图像的函数。因此,这就像调用System.Lazy
属性的Value
属性一样简单,而不需要初始化对象的开销。
显然,使用LazyInitializer
进行惰性初始化的一个额外的小优势是,没有额外的对象没有被创建,这意味着内存占用更小。另一方面,System.Lazy
提供了可读性更强的代码。所以,如果有明确的空间优化,那就去找LazyInitializer
;否则,使用System.Lazy
获得更加清晰易读的代码。
理解锁、信号量和信号量限制
在前几节中,我们看到了如何在中使用各种 API.NET 来实现并行性。然而,当我们这样做的时候,我们需要额外注意共享变量。让我们以我们在本书中构建的企业电子商务应用为例。想想购买物品的工作流程。假设两个用户计划购买一个产品,只有一个商品可用。假设两个用户都将商品添加到购物车,用户 1 下订单,当订单通过支付网关处理时,用户 2 也尝试下订单。
在这种情况下,第二个订单应该失败(假设第一个订单成功),因为该书的数量现在是 0;只有对线程间的数量进行适当的同步,这种情况才会发生。此外,如果第一个订单在支付网关中失败或者用户 1 取消他们的交易,则第二个订单应该通过。因此,我们在这里说的是,在处理第一个订单时,数量应该被锁定,并且只有在订单完成时才应该被释放(以成功或失败告终)。在我们进入处理机制之前,让我们快速回顾一下关键部分是什么。
临界截面和螺纹安全
关键部分是应用中读取/写入由多个线程使用的变量的部分。我们可以把这些看作是跨应用使用的全局变量,在不同的时间或同一时间在不同的地方被修改。在多线程场景中,在任何时间点都只允许一个线程修改这些变量,并且只允许一个线程进入临界区。如果应用中没有这样的变量/部分,它可以被认为是线程安全的。因此,在应用中识别那些不是线程安全的变量并相应地处理它们总是明智的。为了保护对临界区的访问不受非线程安全变量的影响,有各种可用的构造,称为同步原语或同步构造,主要分为两类:
- 锁定构造:允许一个线程进入临界区保护对共享资源的访问,所有其他线程等待,直到锁被获取的线程释放。
- 信号结构:这些允许线程通过发信号通知资源的可用性来进入关键部分,就像在生产者-消费者模型中一样,生产者锁定资源,消费者等待信号而不是轮询。
让我们在下一节讨论一些同步原语。
引入锁
锁是一个基本类,允许您在多线程代码中实现同步,其中锁语句之间的任何变量只能由一个线程访问。在锁中,获取锁的线程需要释放锁,在此之前,任何试图进入锁的其他线程都会进入等待状态。可以创建一个简单的锁,如下面的代码所示:
object locker = new object();
lock (locker)
{
quantity--;
}
第一个执行此代码的线程将获取锁,并在代码块完成后释放锁。也可以使用Monitor.Enter
和Monitor.Exit
获取锁,实际上使用锁编译器会将线程内部转换为Monitor.Enter
和Monitor.Exit
。关于锁的几个要点如下:
- 由于它们的线程关联性,它们应该总是用在引用类型上。
- 就性能而言,它们非常昂贵,因为它们会在允许线程恢复之前暂停想要进入关键部分的线程,这增加了一些延迟。
- 双重检查获取锁也是一个很好的实践,就像在单例实现中是如何做到的一样。
锁确实有一些问题:
-
您需要锁定正在被修改或枚举的共享数据/对象。在应用中很容易遗漏关键部分,因为关键部分更像是一个逻辑术语。如果关键部分周围没有任何锁,编译器不会标记它。
-
如果处理不当,你可能会陷入僵局。
-
Scalability is a problem as only one thread can access a lock at a time, while all other threads have to wait.
注意
还有一个重要的概念叫做原子性。只有当没有任何方法可以读取变量的中间状态或将中间状态写入变量时,操作才是原子的。例如,如果一个整数值从 2 修改为 6,任何读取该整数值的线程将只能看到 2 或 6;没有一个线程会看到该线程的中间状态,其中整数只被部分更新。任何线程安全的代码都会自动保证原子性。
使用并发集合(将在后面一节中介绍)而不是锁,因为并发集合在内部处理锁定关键部分。
互斥体(仅限窗口)
互斥体也是锁的一种,它不仅支持锁定进程内的资源,还支持跨多个进程锁定资源。可以使用System.Threading.Mutex
类创建互斥体,任何想要进入临界区的线程都需要调用WaitOne
方法。通过ReleaseMutex
方法释放互斥体;因此,我们基本上创建了一个System.Threading.Mutex
类的实例,并调用WaitOne
/ ReleaseMutex
来进入/退出临界区。以下是关于互斥体的几个要点:
- 互斥体有线程亲缘关系,所以调用
WaitOne
的线程需要调用ReleaseMutex
。 System.Threading.Mutex
类的构造函数接受互斥体的名称,该互斥体可用于跨进程共享。
引入信号量和信号量限制
信号量是一种非排他锁,它通过允许多个线程进入一个关键部分来支持同步。然而,与排他锁不同,信号量用于需要限制对资源池的访问的场景,例如,允许应用和数据库之间固定数量的连接的数据库连接池。回到我们在电子商务应用中购买产品的例子,如果一个产品的可用数量是 10,这意味着 10 个人可以将这个项目添加到他们的购物车中并下订单。如果同时下 11 个订单,应允许 10 个用户下订单,第 11 个订单应暂停,直到前 10 个订单完成。
英寸 NET 中,可以通过创建System.Threading.Semaphore
类的实例并传递两个参数来创建信号量:
- 活动请求的初始数量
- 同时允许的请求总数
下面是一个创建信号量的简单代码片段:
Semaphore quantity = new Semaphore(0, 10);
在这种情况下,0
表示没有请求获得共享资源,最多允许 10 个并发请求。获取共享资源需要调用WaitOne()
,释放资源需要调用Release()
方法。
为了创建信号量,中还有另一个轻量级类.NET,这就是SemaphoreSlim
,苗条的版本,通常依赖于一个叫做旋转的概念。在这种情况下,无论何时需要锁定共享资源,而不是立即锁定资源,SemaphoreSlim
使用一个运行几微秒的小循环,这样它就不必经历阻塞、上下文切换和内部内核转换的昂贵过程(信号量使用 Windows 内核信号量来锁定资源)。最终,如果共享资源仍然需要锁定,则SemaphoreSlim
返回到锁定状态。
创建SemaphoreSlim
实例几乎与创建信号量完全相同;唯一不同的是,对于锁定,它有WaitAsync
而不是WaitOne
。还有CurrentCount
可用,告诉我们获取的锁数。
关于信号量和SemaphoreSlim
的一些关键事实如下:
- 由于信号量是用来访问资源池的,信号量和
SemaphoreSlim
没有线程亲缘关系,任何线程都可以释放资源。 Semaphore
班在.NET Core 支持命名信号量。命名信号量可用于跨进程锁定资源;但是,SemaphoreSlim
类不支持命名信号量。- 与
Semaphore
不同,SemaphoreSlim
类支持异步方法和取消,这意味着它可以很好地与异步等待方法一起使用。async-wait 关键字有助于编写非阻塞异步方法,这将在本章后面介绍。
选择正确的同步结构
还有其他信号结构要覆盖;下表为您提供了它们的高级使用视图和真实例子:
表 4.1
到目前为止,我们已经涵盖了以下内容:
- 使用
Thread
和ThreadPool
类多线程的各种方式及其局限性 - 惰性初始化的重要性及其在多线程环境中的作用
- 中可用的各种同步构造。网
当我们创建一些交叉组件时,我们将在后面的章节中使用这些概念。
在下一节中,我们将看到如何通过任务和第三方物流的使用来克服Thread
和ThreadPool
的限制。
介绍任务和对比
我们知道异步编程有助于我们的应用更好地扩展和响应,所以实现异步应用不应该成为开发人员的开销。Thread
和ThreadPool
在帮助实现异步的同时,增加了大量开销,也带来了局限性。因此,微软提出了任务,这使得开发异步应用变得更加容易。事实上,大多数更新的 API 都在.NET 5 只支持异步的编程方式。例如,通用 Windows 平台 ( UWP )甚至没有公开 API 来创建没有任务的线程。因此,理解任务和第三方语言是使用 C#编写异步程序的基础。我们将在这一节深入探讨这些主题,稍后,我们将看到 c# async-wait 关键字与第三方语言相结合如何简化异步编程。
任务和第三方物流简介
异步编程背后的思想是没有线程应该等待一个操作;也就是说,框架应该有能力将一个操作包装到某种抽象中,然后一旦操作完成就恢复,而不阻塞任何线程。这个抽象只不过是Task
类,它通过System.Threading.Tasks
公开,有助于在. NET 中编写异步代码
Task
类简化了包装任何等待操作,无论是从数据库中检索的数据、从磁盘加载到内存中的文件,还是任何高 CPU 密集型操作,简化了在单独的线程上运行它,如果需要的话。它具有以下重要特征:
Task
支持操作通过其泛型类型Task<T>
完成后返回值。Task
负责调度ThreadPool
上的线程,并相应地负责分区操作和调度来自ThreadPool
的多个线程,所有这些都是在抽象执行它的复杂性的同时完成的。- 报告完成支持通过
CancellationToken
取消,通过IProgress
进度报告。 Task
支持创建子任务,管理子任务和父任务之间的关系。- 即使对于多层次父/子任务,异常也会传播到调用应用。
- 最重要的是,
Task
支持异步等待,一旦任务中的操作完成,这有助于恢复调用应用/方法中的处理。
第三方物流是由提供的一组应用编程接口.NET,并提供创建和管理任务的方法。任务可以通过创建一个System.Threading.Tasks.Task
类的对象并传递一段需要在任务上执行的代码来创建。我们可以通过多种方式创建任务:
-
您可以创建一个
Task
类的对象,并传递一个 lambda 表达式。这种方法需要显式启动,如下面的代码所示:Task dataTask = new Task(() => FetchDataFromAPI("https://foo.com/api")); dataTask.Start();
-
也可以使用
Task.Run
创建任务,如下代码所示,支持在不显式调用Start()
:Task dataTask = Task.Run(() => FetchDataFromAPI ("https://foo.com/api"));
的情况下创建并启动任务
-
创建任务的另一种方法是使用
Task.Factory.StartNew
:Task dataTask = Task.Factory.StartNew(() => FetchDataFromAPI("https://foo.com/api"));
在所有这些方法中,ThreadPool
线程用于运行FetchDataFromAPI
方法,通过dataTask
对象被引用,该对象被返回给调用方以跟踪操作/异常的完成。由于此任务将在ThreadPool
线程上异步执行,并且所有ThreadPool
线程都是后台线程,因此应用不会等待FetchDataFromAPI
方法完成。第三方物流公开了一种等待任务完成的Wait
方法,类似于dataTask.Wait()
。下面是一个使用任务的小型控制台应用的代码片段:
class Program
{
static void Main(string[] args)
{
Task t = Task.Factory.StartNew(() =>
FetchDataFromAPI("https://foo.com"));
t.Wait();
}
public static void FetchDataFromAPI(string apiURL)
{
Thread.Sleep(2000);
Console.WriteLine("data returned from API");
}
}
在这个片段中,我们使用了一个 lambda 表达式。但是,它可以是委托或操作委托(在无参数方法的情况下),因此类似下面这样的东西也可以用来创建任务:
Task t = Task.Factory.StartNew(delegate { FetchDataFromAPI("https://foo.com");});
无论哪种方式,您都会收到对Task
对象的引用,并对其进行相应的处理。如果一个方法是返回值,那么我们可以使用通用版本的Task
类,并使用Result
方法从Task
中检索数据。例如,如果FetchDataFromAPI
返回一个字符串,我们可以使用Task<String>
,如下面的代码片段所示:
Task<string> t =
Task.Factory.StartNew<string>(()
=> FetchDataFromAPI(""));
t.Wait();
Console.WriteLine(t.Result);
这些方法都有各种附加参数,一些重要的参数如下:
- 使用使用
CancellationTokenSource
类生成的CancellationToken
类的对象取消。 - 通过
TaskCreationOptions
枚举控制任务创建和执行的行为。 TaskScheduler
的自定义实现,控制任务如何排队。
TaskCreationOptions
是第三方物流中的一个枚举,它告诉TaskScheduler
我们正在创建什么样的任务。例如,我们可以创建一个长期运行的任务,如下所示:
Task<string> t = Task.Factory.StartNew<string>(() => FetchDataFromAPI(""), TaskCreationOptions.LongRunning);
虽然这不能保证更快的输出,但它更像是对调度程序本身的一个提示,以进行优化。例如,如果调度程序看到一个长时间运行的任务正在被调度,它可以启动更多的线程。这个枚举的所有选项都可以在https://docs . Microsoft . com/en-us/dotnet/API/system . threading . tasks . taskscreationoptions?view=net-5.0 。
Task
还支持一起等待多个任务,通过创建并将所有任务作为参数传递给以下方法:
WaitAll
:等待所有任务完成,阻塞当前线程。不建议用于应用开发。WhenAll
:等待所有任务完成,不阻塞当前线程。通常与异步等待一起使用。推荐用于应用开发。WaitAny
:等待其中一个任务完成,并阻塞当前线程,直到此时。不建议用于应用开发。WhenAny
:等待其中一个任务完成,不阻塞当前线程。通常与异步等待一起使用。不建议用于应用开发。
与线程不同,任务具有全面的异常处理支持。让我们在下一节看到这一点。
处理任务异常
任务中的异常处理很简单,在任务周围写一个try
块,然后捕捉异常,这些异常通常被包裹在AggregateException
中;也就是说,它就像下面的代码片段一样简单:
try
{
Task<string> t =
Task.Factory.StartNew<string>(()
=> FetchDataFromAPI(""));
t.Wait();
}
catch (AggregateException agex)
{
//Handle exception
Console.WriteLine(agex.InnerException.Message);
}
在前面的代码中,agex.InnerException
将给出实际的异常,因为我们正在等待一个任务。然而,如果我们在等待多个任务,我们可以循环通过的将是InnerExceptions
集合。此外,它还带有一个Handle
回调方法,可以在catch
块中订阅,回调一旦被触发就会有关于异常的信息。
与前面的代码一样,对于传播异常的任务,我们需要调用Wait
方法或其他阻塞构造,如WhenAll
来触发catch
阻塞。然而,在引擎盖下,Task
的任何异常实际上都保存在Task
类的Exception
属性中,该属性属于AggregateException
类型,可以观察到任务中的任何潜在异常。
此外,如果任务是附加子任务或嵌套任务的父任务,或者您正在等待多个任务,则可能会引发多个异常。为了将所有异常传播回调用线程,基础设施将它们包装在一个AggregateException
实例中。
关于处理异常的更多细节可以在 https://docs . Microsoft . com/en-us/dotnet/standard/parallel-programming/exception-handling-task-parallel-library 上找到。
实施任务取消
.NET 提供两个主要类来支持任务的取消:
CancellationTokenSource
:创建取消令牌并支持通过Cancel
方法取消令牌的类CancellationToken
:监听取消并在任务被取消时触发通知的结构
对于取消任务,有两种取消类型:一种是任务被错误执行,需要立即取消,另一种是任务已经开始,需要中途停止(中止)。对于前者,我们可以创建一个支持取消的任务。我们使用第三方应用编程接口,并将取消令牌传递给构造函数,如果任务需要取消,则调用CancellationTokenSource
类的Cancel
方法,如以下代码片段所示:
cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
Task dataFromAPI = Task.Factory.StartNew(()
=> FetchDataFromAPI(new List<string> {
"https://foo.com",
"https://foo1.com",}), token);
cts.Cancel();
所有的。支持异步调用的 NET Core APIs,比如HttpClient
类的GetAsync
/ PostAsync
,有接受取消令牌的重载。对于后一种情况(中止任务),决定基于将要运行的操作是否支持取消。假设它支持取消,我们可以将取消令牌传递给方法,并在方法调用中检查取消令牌的IsCancellationRequested
属性并进行相应的处理。
让我们创建一个简单的控制台应用,创建一个支持取消的任务。这里我们正在创建一个FetchDataFromAPI
方法,它接受一个 URL 列表,并从这些 URL 中检索数据。该方法还支持使用CancellationToken
取消。在实现中,我们遍历 URL 列表并继续,直到请求取消或者循环完成所有迭代:
class Program
{
static void Main(string[] args)
{}
public static string FetchDataFromAPI(List<string>
apiURL, CancellationToken token)
{
Console.WriteLine("Task started");
int counter = 0;
foreach (string url in apiURL)
{
if (token.IsCancellationRequested)
{
throw new TaskCanceledException($"data
from API returned up to iteration
{counter}");
//throw new OperationCanceledException($"data from API returned up to iteration {counter}"); // Alternate exception with same result
//break; // To handle manually
}
Thread.Sleep(1000);
Console.WriteLine($"data retrieved from
{url} for iteration {counter}");
counter++;
}
return $"data from API returned up to iteration
{counter}";
}
}
现在用主方法中的四个网址列表调用FetchDataFromAPI
,如下面的代码所示。这里我们使用CancellationTokenSource
类的Token
属性创建CancellationToken
,并将其传递给FetchDataFromAPI
方法。我们正在模拟 3 秒后的取消,以便在检索第四个网址之前取消FetchDataFromAPI
:
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
Task<string> dataFromAPI;
try
{
dataFromAPI = Task.Factory.StartNew<string>(() =>
FetchDataFromAPI(new List<string> {
"https://foo.com","https://foo1.com","https://foo2.com","https://foo3.com",
"https://foo4.com",
}, token));
Thread.Sleep(3000);
cts.Cancel(); //Trigger cancel notification to cancellation token
dataFromAPI.Wait(); // Wait for task completion
Console.WriteLine(dataFromAPI.Result); // If task is completed display message accordingly
}
catch (AggregateException agex)
{// Handle exception}
一旦我们运行了这段代码,我们可以看到三个网址的输出,然后是一个异常/中断(基于在FetchDataFromAPI
方法中被注释掉的那一行)。
在前面的示例中,我们使用for
循环和Thread.Sleep
模拟了一个长时间运行的代码块,取消了任务并相应地处理了代码。但是,可能会出现长时间运行的代码块不支持取消的情况。在这些情况下,我们必须编写一个接受取消令牌的包装器方法,并让包装器在内部调用长时间运行的操作;然后,在主方法中,我们调用包装代码。下面的代码片段显示了一个使用TaskCompletionSource
的包装方法,这是第三方物流中的另一个类。它是用来通过类中可用的Task
属性将非基于任务的异步方法(甚至包括基于异步方法的方法)转换为任务。在这种情况下,我们将取消令牌传递给TaskCompletionSource
,以便其Task
相应更新:
private static Task<string>
FetchDataFromAPIWithCancellation(List<string>
apiURL, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<string>();
tcs.TrySetCanceled(cancellationToken);
// calling overload of long running operation that doesn't support cancellation token
var dataFromAPI = Task.Factory.StartNew(() =>
FetchDataFromAPI(apiURL));
// Wait for the first task to complete
var outputTask = Task.WhenAny(dataFromAPI,
tcs.Task);
return outputTask.Result;
}
在这种情况下,通过TaskCompletionSource
的Task
属性跟踪CancellationToken
,我们创建了另一个任务来调用我们长期运行的操作(没有取消令牌支持的那个),无论哪个任务先完成,都是我们返回的那个。
当然,需要更新Main
方法来调用这里所示的包装器(其余代码保持不变):
dataFromAPI = Task.Factory.StartNew(() =>
FetchDataFromAPIWithCancellation(new
List<string>
{
"https://foo.com",
"https://foo1.com",
"https://foo2.com",
"https://foo3.com",
"https://foo4.com",
}, token)).Result;
这不会取消基础方法,但仍允许应用在基础操作完成之前退出。
任务取消是一种非常有用的机制,有助于减少尚未开始或已经开始但需要停止/中止的任务中不必要的处理。因此,中的所有异步 API.NET 不支持取消。
实现延续
在企业应用中,大多数情况下需要创建多个任务,构建任务的层次结构,创建相关任务,或者创建任务之间的子/父关系。任务延续可以用来定义这样的子任务/子任务。它像 JavaScript 承诺的那样工作,并支持将任务链接到多个级别。就像承诺一样,层次结构中的后续任务在第一个任务之后执行,这可以进一步链接到多个级别。
实现任务延续的方法有很多种,但最常见的方法是使用Task
类的ContinueWith
方法,如下例所示:
class Program
{
static void Main(string[] args)
{
Task.Factory.StartNew(() => Task1(1)) // 1+2 = 3
.ContinueWith(a => Task2(a.Result)) // 3*2 = 6
.ContinueWith(b => Task3(b.Result))// 6-2=4
.ContinueWith(c =>
Console.WriteLine(c.Result));
Console.ReadLine();
}
public static int Task1(int a) => a + 2;
public static int Task2(int a) => a * 2;
public static int Task3(int a) => a - 2;
}
正如你可能已经猜到的,这里的输出将是4
,并且一旦前一个任务的执行完成,每个任务就会执行。
ContinueWith
接受一个名为TaskContinuationOptions
的重要枚举,支持不同条件下的延续。例如,我们可以通过TaskContinuationOptions.OnlyOnFaulted
作为参数来创建一个延续任务,当前面的任务出现异常时执行,或者通过TaskContinuationOptions.AttachedToParent
来创建一个延续任务,强制父子关系,并强制父任务仅在子任务之后完成执行。
就像WhenAll
、WhenAny
一样,ContinueWith
也有类似的兄弟姐妹,如下:
Task.Factory.ContinueWhenAll
:这将接受多个任务引用作为参数,并在所有任务完成时创建一个延续。Task.Factory.ContinueWhenAny
:这将接受多个任务引用作为参数,并在引用的任务之一完成时创建延续。
抓住任务继续对于理解异步等待的幕后工作至关重要,我们将在本章后面讨论。
同步上下文
SynchronizationContext
是System.Threading
中的一个抽象类,有助于线程间的通信。例如,从并行任务更新 UI 元素需要线程连接回 UI 线程并恢复执行。SynchronizationContext
主要通过这个类的Post
方法提供这种抽象,该方法接受一个委托在稍后的阶段执行。所以,在前面的例子中,如果我需要更新一个 UI 元素,我需要取 UI 线程的SynchronizationContext
,调用它的Post
方法,传递必要的数据来更新 UI 元素。
由于SynchronizationContext
是一个抽象类,这个类有多种派生类型。比如 Windows 窗体有WindowsFormsSynchronizationContext
,WPF 有DispatcherSynchronizationContext
,等等。
SynchronizationContext
作为一个抽象的主要优点是,不管Post
方法的覆盖实现如何,它都有助于对委托进行排队。
任务调度器
当我们使用前面描述的各种方法创建任务时,我们看到一个任务在ThreadPool
线程上被调度,但是问题是谁或者什么实际上做了那件事。System.Threading.Tasks.TaskScheduler
是第三方物流中负责在ThreadPool
线程上排队和执行任务委托的类。
当然,这是一个抽象类,框架附带了两个派生类:
ThreadPoolTaskScheduler
SynchronizationContextScheduler
TaskScheduler
公开一个Default
属性,默认设置为ThreadPoolTaskScheduler
。因此,默认情况下,所有任务都被调度到ThreadPool
线程;然而,图形用户界面应用通常使用SynchronizationContextScheduler
,以便任务可以成功返回并更新用户界面元素。
.NET Core 附带了复杂的派生类型TaskScheduler
和SynchronizationContext
类。然而,它们在 async-wait 中起着主要作用,并且它们有助于快速调试任何与死锁相关的问题。
请注意,查看TaskScheduler
和SynchronizationContext
的内部工作方式超出了本书的范围,留给您作为练习。
实现数据并行
数据并行就是将一个源集合划分成多个可并行执行的任务,这些任务并行执行相同的操作。对于第三方物流,这在Parallel
静态类中是可用的,该类公开了像For
和ForEach
这样的具有多个重载的方法来处理这样的执行。
假设你有一百万个数字的集合,你需要找到质数。数据并行性在这里非常有用,因为集合可以分成多个范围,并且可以计算质数。一个典型的并行for
循环如下面的代码片段所示:
List<int> numbers = Enumerable.Range(1,
100000).ToList();
Parallel.For(numbers.First(), numbers.Last(), x
=> CalculatePrime(x));
然而,一个更现实的例子是类似图像处理应用的东西,它需要处理图像中的每个像素,并将每个像素的亮度降低五个点。这种操作可以极大地受益于数据并行性,因为每个像素彼此独立,因此可以并行处理。
同样的,在Parallel
静态类中有一个ForEach
方法,可以使用如下:
Parallel.ForEach(numbers, x => CalculatePrime(x));
这里列出了使用Parallel.For
和Parallel.ForEach
进行数据并行的一些关键优势:
-
适合取消循环;他们在一个常规的
for
循环中工作,就像break
一样。在Parallel.For
中,这是通过将ParallelStateOptions
传递给代表,然后调用ParallelStateOptions.Break
来支持的。当其中一个任务遇到Break
时,ParallelStateOptions
类的LowestBreakIteration
属性被设置,所有并行任务将迭代,直到达到该数量。ParallelLoopResult
是Parallel.For
和Parallel.ForEach
的返回类型,具有IsCompleted
属性,表示循环是否过早执行。 -
他们也支持通过
ParallelStateOptions.Stop
立即停止循环。还有Parallel.For
/Parallel.ForEach
的部分构造函数接受取消代币,也可以用来模拟ParallelStateOptions.Stop
;然而,一个环应该被包裹在一个try…catch
块内,因为OperationCanceledException
会被抛出。 -
如果其中一个任务抛出异常,所有任务将完成当前迭代,然后停止处理。和任务一样,
AggregateException
被抛出。 -
通过传递
ParallelOptions
和设置MaxDegreeOfParallelism
来支持并行度,这将控制任务可以并行执行的内核数量。 -
通过范围分区或块分区支持源集合的自定义分区。
-
支持线程或分区范围内的线程安全局部变量。
-
支持嵌套
Parallel.For
循环,它们的同步是自动处理的,不引入任何手动同步。 -
Thread-safety: If each iteration uses a shared variable, synchronization needs to be implemented explicitly. So, to gain the most out of data parallelism, use it for operations that can execute independently for each iteration without depending on shared resources.
小费
数据并行应该谨慎使用,因为有时会被误用。就像 4 个人分 40 个任务。如果在 4 个人之间组织这项工作(拆分和合并)比仅仅执行 40 个任务的整体工作要多得多,那么数据并行不是正确的选择。进一步阅读请参考 https://docs . Microsoft . com/en-us/dotnet/standard/parallel-programming/data-parallelism-task-parallel-library。
使用平行 LINQ (PLINQ)
PLINQ 是 LINQ 的并行实现;这是ParallelEnumerable
类中可用的一组 API,支持并行执行 LINQ 查询。让 LINQ 查询并行运行的最简单方法是在 LINQ 查询中嵌入AsParallel
方法。请参见下面的代码片段,它调用一个计算 1000 个数字的质数的方法:
List<int> numbers = Enumerable.Range(1, 1000).ToList();
var resultList = numbers.AsParallel().Where(I => CalculatePrime
(i)).ToList();
使用 LINQ 查询语法,如下所示:
var primeNumbers = (from i in numbers.AsParallel()
where CalculatePrime(i)
select i).ToList();
在内部,这个查询被分成多个较小的查询,在每个处理器上并行执行,因此加快了查询速度。分区的源需要在主线程上合并回来,这样结果(输出集合)就可以循环通过,以便进一步处理/显示。
让我们创建一个控制台应用,使用 PLINQ 结合Parallel.For
打印给定范围内的所有素数。加入以下方法,取一个数,如果是质数则返回true
,否则返回false
:
class Program
{
static void Main(string[] args)
{
}
static bool CalculatePrime(int num)
{
bool isDivisible = false;
for (int i = 2; i <= num / 2; i++)
{
if (num % i == 0)
{
isDivisible = true;
break;
}
}
if (!isDivisible && num != 1)
return true;
else
return false;
}
}
现在在主方法中,添加以下代码,它创建了一个前 100 个数字的列表我们将使用 PLINQ 循环遍历,然后将其传递给CalculatePrime
方法;然后,我们将最终使用Parallel.ForEach
显示质数列表:
List<int> numbers = Enumerable.Range(1, 100).ToList();
try
{
var primeNumbers = (from number in numbers.AsParallel() where CalculatePrime(number) == true select number).ToList();
Parallel.ForEach(primeNumbers, (primeNumber) =>
{
Console.WriteLine(primeNumber);
});
}
catch (AggregateException ex)
{
Console.WriteLine(ex.InnerException.Message);
}
这个示例的输出将是一个素数列表;但是,可以看到输出不会是升序的质数,而是随机的顺序,因为CalculatePrime
方法是用多个数字并行调用的。
下面是前面代码的内部工作图:
图 4.4–PLINQ 和平行。为每一个
PLINQ 还提供了一种处理每个分区/线程结果的方法,无需使用ForAll
将结果合并到调用线程的开销,前面的代码可以进一步优化如下:
(from i in numbers.AsParallel()
where CalculatePrime(i) == true
select i).ForAll((primeNumber) =>
Console.WriteLine(primeNumber));
小费
玩 LINQ/PLINQ 最好的工具之一是 LINQPad 我绝对推荐你从https://www.linqpad.net/Download.aspx下载。
PLINQ 需要记住的一些重要事情如下:
- 将结果合并到主线程可以通过使用
WithMergeOption
方法并通过ParallelMergeOperation
枚举传递适当的值来配置。 - 与其他并行扩展一样,任何异常都作为
AggregateException
返回,所有迭代的执行立即停止。当然,如果异常被包含在委托中而不是被抛出,那么执行可以继续。 - 还有各种其他的扩展方法,比如
AsSequential
和AsOrdered
,这些可以组合在一个单独的 LINQ 查询中。比如基于此,AsSequential
可以和AsParallel
结合,让一些分区可以顺序运行,其他分区可以并行执行。 - 支持使用
WithCancellation
方法取消。 - 通过
WithDegreeOfParallelism
支持平行度。
数据并行和 PLINQ 提供了很多 API,可以用来快速启用代码的并行执行,而不会给应用逻辑增加任何额外的开销。但是,正如上一节所解释的,它们之间有细微的区别,因此应该有不同的用法。
小费
PLINQ 和第三方物流共同构成了并行扩展。
在本节中,我们在许多地方使用了Thread.Sleep
,但这主要是为了模拟长时间运行的操作;但是,不建议您在生产中使用它。
在下一节中,我们将看到如何将任务与异步等待联系起来,并在企业应用中使用异步等待。
引入异步等待
到目前为止,我们已经讨论了使用任务编写异步代码,以及第三方物流如何简化任务的创建和管理。然而,任务主要依靠延续、回调或事件在任务完成后继续执行。在企业应用中,管理这样的代码会很困难;如果链接了太多任务,任何运行时异常都很难调试。这就是 C#引入 async-wait 的地方,async-wait 是 C# 5.0 中引入的一种语言特性,它简化了异步代码的编写,使其更具可读性和可维护性,改进了异常处理,并使调试变得容易。所以,让我们开始异步等待。
async
是 C#中的一个关键字,用作修饰符,当作为任何方法(或 lambda)的前缀时,会将方法转换为状态机,使该方法能够在其主体中使用await
关键字。
await
是 C#中的一个关键字,用作运算符,后跟一个返回可调用对象(通常是任务)的表达式。await
只能在有async
修饰符的方法中使用,一旦调用方遇到await
语句,控制就会返回,事情就会恢复;await
之后,使用延续完成任务。
基于任务的异步模式
基于任务的异步模式 ( TAP )是一种用于实现异步方法的模式,在这种模式中,我们使用async
修饰符,然后在包装在任务中的异步操作上使用await
(或任何暴露GetAwaiter()
的自定义唤醒类型)。简单来说,这种模式包括使用一个具有async
修饰符并返回任务的方法来表示异步操作;使用await
进一步等待任何异步操作。下面是一个示例代码片段,它异步下载文件,并使用 TAP 实现:
图 4.5–使用异步等待的异步方法示例
在上图中,控制流程如下(使用图中的数字标签):
- 应用使用
Main
方法开始执行。由于Main
以async
方法为前缀,它被转换成实现状态机的类型。执行继续,直到在await
DownloadFileAsync
遇到await
,线程返回给调用者。 - 在返回给调用者之前,对
DownloadFileAsync
方法的调用被存储在Task
对象中,并且对Task
对象的引用也被保留。Main
方法的剩余代码包装在该任务的延续中。 - 一个
ThreadPool
线程将开始执行一个DownloadFileAsync
方法,并重复相同的步骤;也就是说,它将一个方法转换成实现状态机的类型,继续执行直到遇到await
,然后被引用的任务被传递回来;剩余的代码被移到该任务的继续部分。 - 在某个时刻,当
DownloadDataTaskAsync
方法完成时,任务继续被触发,并将执行剩余的代码。 - 重复该过程,直到引用
DownloadFileAsync
的任务完成并执行其继续,在这种情况下为Console.WriteLine("File downloaded!!")
,然后应用退出。
在粗略的高级别,代码将被转换,如下所示:
图 4.6–转换后的示例异步方法
虽然这是对异步等待的幕后工作的过度简化,但我们可以看到编译器做了很多繁重的工作,包括生成一个实现状态机的类型,并使用回调的状态继续执行。
我们已经看到编写异步方法是多么简单,在本书的整个过程中,我们将在企业应用中编写许多这样的方法。然而,异步等待不是万灵药;它不是每个应用问题的答案。我们需要验证某些因素来使用异步等待。让我们看看使用异步等待的原则是什么。
注意
如果有SynchronizationContext
,前面的代码会稍有变化。例如,在 Windows 窗体或 WPF 应用中,使用SynchronizationContext
或TaskScheduler.FromCurrentSynchronizationContext
的Post
方法在当前的SynchronizationContext
上发布延续。根据标准的命名约定,为了可读性,异步方法以单词async
作为后缀,但是在语法上并不需要。
异步等待的使用原则
当我们开始使用异步等待时,有一些推荐的做法可以让应用利用异步原理。例如,对于嵌套调用,我们应该始终使用 async-wait;不要使用.Result
等等。这里有一些指导方针可以帮助你有效地使用异步等待。
链异步-一路等待
使用 async-await 实现的异步方法应该从 async-await 方法中触发,以便正确等待。如果我们试图使用任务的Result
方法或Wait
方法从同步方法调用异步方法,可能会导致死锁。让我们来看下面来自 WPF 应用的代码片段,该应用通过单击按钮从网络下载文件。然而,我们没有等待对异步方法的调用,而是使用了Task
的Result
方法:
private void Button_Click(object sender,
RoutedEventArgs e)
{
var task = DownloadFileAsync("https://github.com/Ravindra-a/largefile/blob/master/README.md", @$"{System.IO.Directory.GetCurrentDirectory()}\download.txt");
bool fileDownload = task.Result; // Or task.GetAwaiter().GetResult()
if (fileDownload)
{
MessageBox.Show("file downloaded");
}
}
private async Task<bool> DownloadFileAsync(string
url, string path)
{
// Create a new web client object
using WebClient webClient = new WebClient();
// Add user-agent header to avoid forbidden errors.
webClient.Headers.Add("user-agent",
"Mozilla/5.0 (Windows NT 10.0; WOW64)");
byte[] data = await
webClient.DownloadDataTaskAsync(url);
// Write data in file.
Using var fileStream = File.OpenWrite(path);
{
await fileStream.WriteAsync(data, 0,
data.Length);
}
return true;
}
在此方法中,await
webClient.DownloadDataTaskAsync(url);
之后的代码将永远不会执行,原因如下:
- 一旦遇到等待,参考对象通过
GetAwaiter
方法在TaskAwaitable
中捕捉SynchronizationContext
。 - 一旦
async
操作完成,该await
的继续需要在SynchronizationContext
上执行(通过SynchronizationContext.Post
)。 - 然而,
SynchronizationContext
已经被阻塞,因为点击按钮对task.Result
的调用在同一个SynchronizationContext
上,并且正在等待DownloadDataTaskAsync
完成,因此导致死锁。
所以,千万不要挡async
法;一路异步的最好方法。因此,在前面的代码中,您将调用更改为await
DownloadFileAsync
(按钮点击的async void
–await
需要一个方法来拥有async
修饰符)。
注意
同样的代码在 ASP.NET Core 5 应用中运行良好,不会导致死锁,因为 ASP.NET Core 5 没有SynchronizationContext
并且延续在ThreadPool
线程上执行,不涉及任何请求上下文;但是,即使在 ASP.NET Core 5 中,仍然不建议阻塞异步调用。
配置等待
在前面的讨论中,因为我们有端到端的应用代码,所以更容易找到死锁的原因。但是,如果我们正在开发一个可以在 WPF、ASP.NET Core 5 或.NET Framework 应用,我们需要确保库内的异步代码不会导致死锁,即使调用方可能正在通过同步方法(GetAwaiter().GetResult()
)使用库方法。
在这种情况下,Task
提供了一个名为ConfigureAwait
的方法,该方法接受一个布尔值,当true
将使用调用方的原始上下文,当false
将在await
之后恢复操作,而不依赖于原始上下文。通俗地说,await
之后的任何代码都将独立执行,而与发起请求的上下文状态无关。
使用ConfigureAwait(false)
,特别是如果你正在实现一个库方法,因为它将避免在原始上下文上运行一个延续。对于库方法,必须使用ConfigureAwait(false)
,因为它们永远不应该依赖于调用/原始上下文来继续。例如,以下代码不会导致死锁:
private void Button_Click(object sender, RoutedEventArgs e)
{
string output = GetAsync().Result; //Blocking code, ideally should cause deadlock.
MessageBox.Show(output);
}
// Library code
public async Task<string> GetAsync()
{
var uri = new Uri("http://www.google.com");
return await new HttpClient().
GetStringAsync(uri).ConfigureAwait(false);
}
默认情况下,每个await
表达式都有ConfigureAwait(true)
,所以建议尽可能显式调用ConfigureAwait(false)
。除了避免死锁之外,ConfigureAwait(false)
还提高了性能,因为没有原始上下文的封送处理。
这就把我们带到了是否有场景需要使用ConfigureAwait(true)
的问题。答案是,在某些场景中,正在构建的自定义SynchronizationContext
需要回调使用,然后建议使用ConfigureAwait(true)
,或者至少不要使用ConfigureAwait(false)
,因为任何任务的默认行为都与ConfigureAwait(true)
相同。
中央处理器限制与输入/输出限制
总是使用异步等待来处理输入输出绑定的工作,使用第三方语言来处理中央处理器绑定的工作来实现异步。诸如数据库调用、网络调用和文件系统调用等输入/输出操作可以用异步等待异步方法包装。然而,像计算 pi 这样的 CPU 密集型操作最好使用第三方物流来处理。
回到我们前面的讨论,异步编程的思想是释放ThreadPool
线程,而不是等待操作完成。当我们将出站呼叫表示为任务并使用异步等待时,这很容易实现。
然而,对于一个 CPU 密集型操作来说,ThreadPool
线程将继续在工作线程上执行指令(因为这是一个 CPU 密集型操作,需要 CPU 时间),并且显然不能释放该线程。这意味着在异步等待中包装一个 CPU 密集型操作不会产生任何好处,这与同步运行它是一样的。因此,处理 CPU 密集型操作的更好方法是使用第三方物流。
这并不意味着我们会在遇到 CPU 密集型方法时停止使用异步等待。推荐的方法是仍然使用异步等待来管理与第三方语言绑定的中央处理器操作,并且仍然不违反我们始终使用异步等待的第一原则。
下面是一个简单的代码片段,使用 async-wait 来管理受 CPU 限制的工作:
private async Task CPUIOResult()
{
var doExpensiveCalculationTask = Task.Run(() => DoExpensiveCalculation()); //Call a method that does CPU intense operation
var downloadFileAsyncTask = DownloadFileAsync();
await Task.WhenAll(doExpensiveCalculationTask,
downloadFileAsyncTask);
}
private async Task DownloadFileAsync(string url, string path)
{
// Implementation
}
private float DoExpensiveCalculation()
{
//Implementation
}
正如在前面的代码中看到的,仍然可以通过异步等待和第三方语言的混合来管理受 CPU 限制的工作;开发人员需要评估所有可能的选项,并相应地编写他们的代码。
避免异步作废
如果一个方法预计不会返回任何东西,那么一定要确保使用异步等待而不是void
来实现异步方法的返回类型为Task
或Task<T>
。这样做的原因是Task
是一个复杂的抽象,为我们处理很多事情,比如异常处理和任务完成状态。然而,如果一个异步方法有一个异步void
的返回类型,它就像一个一次性的方法,任何调用这个方法的人都不能知道操作的状态,即使有一个异常。这是因为在async
void
方法中,一旦遇到await
表达式,调用就会返回给调用者,而不引用Task
,因此没有为其引发异常的引用。对于像 WPF 这样的用户界面应用来说,async
void
方法上的任何异常都将导致应用崩溃,但是async void
事件处理程序是一个例外。
async
void
方法的另一个缺点是无法正确编写单元测试和断言。因此,总是建议使用异步Task
异常作为顶层事件处理程序(这里顶层是关键),因为顶层事件(如按钮点击或鼠标点击)更多的是单向信号,在异步代码中的使用与同步代码没有任何不同。
在异步 lambdas 的情况下也需要考虑同样的,我们需要避免将异步 lambdas 作为参数传递给以Action
类型为参数的方法。请参见以下示例:
class Program
{
static void Main(string[] args)
{
long elapsedTime = AsyncLambda(async() =>
{
await Task.Delay(1000);
});
Console.WriteLine(elapsedTime);
Console.ReadLine();
}
private static long AsyncLambda(Action a)
{
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 10; i++)
{
a();
}
return sw.ElapsedMilliseconds;
}
}
这里预计elapsedTime
的数值会在 1 万左右。然而,出于同样的原因,它接近 100;也就是说,由于Action
是返回类型void
的委托,对AsyncLambda
的调用会立即返回到Main
方法(就像对任何async
void
方法一样)。这可以通过如下更改AsyncLambda
来解决(或者只需将参数更改为Func<Task>
并相应地处理a()
上的等待),然后强制调用方始终使用异步:
private async static Task<long> AsyncLambda(Func<Task> a)
{
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 10; i++)
{
await a();
}
return sw.ElapsedMilliseconds;
}
提醒一下:如果你的应用中有接受Action
类型参数的方法,建议你有一个接受Func<Task>
或Func<Task<T>>
的重载。一个好的方面是 C#编译器自动处理这个,总是以Func<Task>
作为参数调用重载。
小费
使用 Visual Studio 2019 异常帮助器功能来调试由框架代码重新引发的异步异常。
带 IAsyncEnumerable 的异步流
我们都知道foreach
是用来绕过IEnumerable<T>
或者IEnumerator<T>
的。让我们看一下下面的代码,其中我们从数据库中检索所有员工的身份证,并循环遍历每个员工以打印他们的身份证:
static async Task Main(string[] args)
{
var employeeTotal = await
GetEmployeeIDAsync(5);
foreach (int i in employeeTotal)
{
Console.WriteLine(i);
}
}
GetEmployeeIDAsync
实现如下:
static async Task<IEnumerable<int>>
GetEmployeeIDAsync(int input)
{
int id = 0;
List<int> tempID = new List<int>();
for (int i = 0; i < input; i++) //Some async DB iterator method like ReadNextAsync
{
await Task.Delay(1000); // simulate async
id += i; // Hypothetically calculation
tempID.Add(id);
}
return tempID;
}
在这里,您可以看到,我们必须使用一个临时列表,直到我们从数据库中收到所有记录,最后我们返回该列表。然而,如果我们的方法中有迭代器,那么 C#中的yield
是一个显而易见的选择,因为这有助于立即返回结果并避免临时变量。现在,假设您使用了yield
,如下代码所示:
yield return id;
编译时,您会收到以下错误:
The body of 'Program.GetEmployeeIDAsync(int)' cannot be an iterator block because 'Task<IEnumerable<int>>' is not an iterator interface type
因此,需要能够将yield
用于异步方法,并且能够通过集合循环来异步调用应用。这就是 C# 8.0 通过IAsyncEnumerable
提出异步流的地方,它主要使您能够立即返回数据并异步使用一个集合。因此,前面的代码可以更改如下:
static async Task Main(string[] args)
{
await foreach (int i in GetEmployeeIDAsync(5))
{
Console.WriteLine(i);
}
}
static async IAsyncEnumerable<int>
GetEmployeeIDAsync(int input)
{
int id = 0;
List<int> tempID = new List<int>();
for (int i = 0; i < input; i++)
{
await Task.Delay(1000);
id += i; // Hypothetically calculation
yield return id;
}
}
所以,在这里你可以看到,一旦一个方法开始返回,IAsyncEnumerable
循环可以异步迭代,这在很多情况下有助于编写更干净的代码。
遗留模式的包装器
异步处理程序甚至在第三方物流和异步等待之前就已经存在了.NET 框架主要支持两种模式:
- 异步编程模型 ( APM ):用一对方法表示异步操作,通常采用
Begin
/End
方法的命名约定 - 基于事件的异步模式 ( EAP ):表示通过事件使用方法和回调的异步操作
我们可以使用TaskCompletionSource
类编写一个包装器,它可以使用一个任务来表示前面提到的模式中的库方法,从而使它与 TAP 兼容。让我们看看在下面的例子中。
EAP 上的 TAP 包装
对于包装器方法的实现,让我们来看一个WebClient
类,它有一个DownloadStringAsync
方法和一个完成事件,DownloadStringCompleted
。我们需要将这两者打包成一个任务并返回。我们将利用TaskCompletionSource
并处理其TrySetResult
和TrySetException
来更新TaskCompletionSource
的Task
属性。下面是代码片段:
public static Task<string> DownLoadStringEAPtoTAPWrapper(string url, CancellationToken token)
{
TaskCompletionSource<string> tcsWrapperForEAP =
new TaskCompletionSource<string>();
WebClient wc = new WebClient();
token.Register(() => {
tcsWrapperForEAP.TrySetCanceled();
});
wc.DownloadStringAsync(new Uri(url));
wc.DownloadStringCompleted += (sender,
downloadStringCompletedEventArgs) =>
{
if (downloadStringCompletedEventArgs.Error
!= null)
tcsWrapperForEAP.TrySetException(downloadStringCompletedEventArgs.Error);
else
tcsWrapperForEAP.SetResult(downloadStringCompletedEventArgs.Result);
};
return tcsWrapperForEAP.Task;
}
在这里可以看到我们可以简单地利用TaskCompletionSource
为基于 EAP 的异步 API 编写一个包装器。让我们在下一节中看看如何为基于 APM 的异步库做到这一点。
APM 上的 TAP 包装
让我们以为例,使用FileStream.BeginRead
和FileStream.EndRead
读取遗留代码中的文件。我们编写包装器的方法是创建一个TaskCompletionSource<T>
的对象,并使用其Task
属性来设置成功或异常情况场景并返回任务。下面是一个示例方法:
public Task<int> ReadAsyncAPMWrapper(FileStream filest, byte[] buffer, int offset, int noOfBytes, CancellationToken token)
{
var tcsWrapperForAPM = new
TaskCompletionSource<int>();
//Registering cancellation token, this can be further improved
token.Register(() => {
tcsWrapperForAPM.TrySetCanceled(); });
filest.BeginRead(buffer, offset, noOfBytes,
iAsyncResult =>
{
try
{
var asyncState =
iAsyncResult.AsyncState as FileStream;
var result =
asyncState.EndRead(iAsyncResult);
tcsWrapperForAPM.TrySetResult(result); //Set result on Task
}
catch (Exception ex)
{
tcsWrapperForAPM.TrySetException(ex); // Set exception on Task
}
}, filest);
return tcsWrapperForAPM.Task;
}
正如您在前面的代码中看到的,我们使用了TaskCompletionSource
类的TrySetResult
/ TrySetException
,它在内部更新实例的Task
属性,并最终返回任务,该任务可以在任何异步等待方法中使用。
线程池饥饿
假设您有一个带有异步代码的应用。但是,您已经注意到,在高负载期间,请求的响应时间会周期性地急剧增加。您进一步研究了它,但是您的服务器的 CPU 没有得到充分利用,您的进程的内存也不高,并且您的数据库也没有成为瓶颈。在这种情况下,您的应用可能会导致所谓的ThreadPool
饥饿。
ThreadPool
饥饿是一种状态,在这种状态下,不断添加新的线程来服务并发请求,最终到达一个点ThreadPool
无法添加更多线程,请求开始出现延迟响应时间,甚至在最坏的情况下开始失败。即使ThreadPool
能够以每秒一到两个线程的速度增加线程,新的请求可能会以更高的速度到来(如假日期间网络应用的突发负载)。因此,响应时间显著增加。出现这种情况有多种原因;这里列出了其中一些:
- 消耗更多线程来加速长时间运行的受 CPU 限制的工作
- 使用
GetAwaiter().GetResult()
在同步方法中调用异步方法 - 同步原语的不正确使用,例如长时间持有锁的线程和等待获取锁的其他线程
在前面的所有要点中,常见的是阻塞代码;因此,使用阻塞代码,如Thread.Sleep
即使持续时间很短,或类似GetAwaiter().GetResult()
的东西,或试图为一个受 CPU 限制的项目分配更多的线程,会增加ThreadPool
的线程数量,并最终导致饥饿。
ThreadPool
饥饿可以使用性能视图等工具进一步诊断,在该工具中,您可以捕获例如 200 秒的跟踪,并验证流程中线程的增长。如果您看到线程在峰值负载期间快速增长,那么就有可能出现饥饿。
防止ThreadPool
饥饿的最好方法是在整个应用中使用异步等待,永远不要阻塞任何异步调用。此外,对新创建的操作进行限制也会有所帮助,因为它限制了一次可以排队的项目数量。
在这一节中,我们讨论了两个重要的构造:async-wait 和 TPL,当它们结合在一起时,编写异步代码变得更加简单。在下一节中,我们将了解中可用的各种数据结构.NET 5 来支持同步/线程安全,而无需编写任何额外的代码。
使用并行集合进行并行处理
集合类是封装、检索和修改相关数据的枚举集合的最常用类型之一。Dictionary
、list
、queue
和array
是一些常用的集合类型,但它们不是线程安全的。如果一次只从一个线程访问它们,这些集合是很好的。现实环境将是多线程的,为了使其线程安全,您必须实现各种同步结构,如前一节所述。为了解决这个问题,微软提出了并发收集类,如ConcurrentQueue
、ConcurrentBag
、ConcurrentDictionary
、ConcurrentStack
等,这些类在内部实现同步时是线程安全的。让我们在接下来的章节中详细了解它们。
并发词典
让我们使用字典来模拟一个多线程环境。将任务t1
视为来自正在添加到字典中的客户端的一个操作,将任务t2
视为来自正在读取字典的另一个客户端的第二个操作。
我们在每个任务中添加Thread.Sleep
来模拟现实场景,以确保在本例中一个任务不会先于另一个任务完成。让我们考虑一个包含以下代码片段的示例控制台应用:
// Task t1 as one operation from a client who is adding to the dictionary.
Dictionary<int, string> employeeDictionary = new Dictionary<int, string>();
Task t1 = Task.Factory.StartNew(() =>
{
for (int i = 0; i < 100; ++i)
{
employeeDictionary.TryAdd(i, "Employee"
+ i.ToString());
Thread.Sleep(100);
}
});
这是Task
t2
作为另一个正在读字典的客户端的第二个操作:
Task t2 = Task.Factory.StartNew(() =>
{
Thread.Sleep(500);
foreach (var item in employeeDictionary)
{
Console.WriteLine(item.Key + "-" +
item.Value);
Thread.Sleep(100);
}
});
现在两个任务同时执行,如下图:
try
{
Task.WaitAll(t1, t2); // Not recommended to use in production application.
}
catch (AggregateException ex)
{
Console.WriteLine(ex.Flatten().Message);
}
Console.ReadLine();
当您运行此程序时,您将获得以下异常,该异常表明您不能同时修改和枚举集合:
表 4.2
您可能认为现在我们可以添加一个锁来管理线程同步,并为了线程安全避免多线程场景中的这种异常。我在代码中添加了一个锁,只要修改并枚举字典来同步线程。以下是更新后的代码片段:
首先,我们将Task
t1
作为正在添加到字典中的客户端的一个操作:
Dictionary<int, string> employeeDictionary = new Dictionary<int, string>();
Task t1 = Task.Factory.StartNew(() =>
{
for (int i = 0; i < 100; ++i)
{
//Lock the shared data
lock (syncObject)
{
employeeDictionary.TryAdd(i,
"Employee" + i.ToString());
}
Thread.Sleep(100);
}
});
然后我们有Task
t2
作为另一个正在读字典的客户的第二个操作:
Task t2 = Task.Factory.StartNew(() =>
{
Thread.Sleep(500);
//Lock the shared data
lock (syncObject)
{
foreach (var item in
employeeDictionary)
{
Console.WriteLine(item.Key + "-" +
item.Value);
Thread.Sleep(100);
}
}
});
现在我们同时执行了这两个任务:
try
{
Task.WaitAll(t1, t2); // Not recommended to use in production application.
}
catch (AggregateException ex)
{
Console.WriteLine(ex.Flatten().Message);
}
Console.ReadLine();
当您运行这段代码时,您不会看到任何异常。但是,正如前面提到的,锁有一些问题,因此可以使用并发集合重写这段代码。他们在内部使用多线程同步技术,这有助于很好地扩展,防止数据损坏,并避免锁的所有问题。
我们可以使用ConcurrentDictionary
重写代码,它在System.Collections.Concurrent
命名空间中可用。将样本代码中的Dictionary
替换为ConcurrentDictionary
。您也可以删除对System.Collections.Generic
名称空间的引用,因为现在不使用Dictionary
。此外,拆除所有锁。更新后的代码如下,我们把Dictionary
换成了ConcurrentDictionary
,拆了锁:
我们将Task t1
作为一个来自添加到字典中的客户端的操作,并发集合不需要显式锁:
ConcurrentDictionary<int, string> employeeDictionary = new ConcurrentDictionary<int, string>();
Task t1 = Task.Factory.StartNew(() =>
{
for (int i = 0; i < 100; ++i)
{
employeeDictionary.TryAdd(i, "Employee"
+ i.ToString());
Thread.Sleep(100);
}
});
然后我们有Task t2
作为从字典中读取的另一个客户端的第二个操作,并发集合不需要显式锁:
Task t2 = Task.Factory.StartNew(() =>
{
Thread.Sleep(500);
foreach (var item in employeeDictionary)
{
Console.WriteLine(item.Key + "-" +
item.Value);
Thread.Sleep(100);
}
});
现在两个任务同时执行:
try
{
Task.WaitAll(t1, t2);
}
catch (AggregateException ex) // You will not get Exception
{
Console.WriteLine(ex.Flatten().Message);
}
Console.ReadLine();
当你现在运行程序的时候,你不会得到任何异常,因为所有的操作在ConcurrentDictionary
都是线程安全和原子的。在开发人员实现锁并在项目变大时维护它们没有开销。这里有一些关于并发集合的注意事项,比如ConcurrentDictionary
,你需要记住:
- 如果两个线程调用
AddOrUpdate
,不能保证调用哪个工厂委托,甚至不能保证如果工厂委托产生一个项目,该项目将存储在字典中。 GetEnumerator
调用得到的枚举器不是快照,在枚举过程中可以修改(这不会导致任何异常)。- 键和值属性是相应集合的快照,可能不对应于实际的字典状态。
我们已经详细看了ConcurrentDictionary
;让我们在下一节中看看其他并发集合。
生产者-消费者并发集合
在生产者-消费者并发集合中,一个或多个线程可以产生任务(例如,添加到队列、堆栈或包),一个或多个其他线程可以消费来自同一集合(队列、堆栈或包)的任务。
ConcurrentDictionary
,我们在上一节看到的,是一个通用的集合类,您可以在其中添加您想要的项目,并指定您想要读取的项目。其他并发集合是为特定问题设计的:
ConcurrentQueue
是针对你想要先进先出的场景。ConcurrentStack
是指你想要后进先出 ( 后进先出)的场景。ConcurrentBag
适用于场景,在该场景中,您希望相同的线程产生和消费存储在包中的数据,并且顺序无关紧要。
这三个集合也称为生产者-消费者集合,其中一个或多个线程可以从同一个集合中产生任务和消费任务,如下图所示:
图 4.7–生产者-消费者并发收集
这三个集合都实现了IProducerConsumerCollection<T>
界面,最重要的方法是TryAdd
和TryTake
,如图所示:
// Returns: true if the object was added successfully; otherwise, false.
bool TryAdd(T item);
// Returns true if an object was removed and returned successfully; otherwise, false.
bool TryTake([MaybeNullWhen(false)] out T item);
我们以生产者-消费者为例,使用ConcurrentQueue
进行模拟:
- 生产者:客户端向 web 服务发送请求,服务器将请求存储在队列中
- 消费者:从队列中拉出请求并进行处理的工作线程
实现如下代码所示:
//Producer: Client sending request to web service and server storing the request in queue.
ConcurrentQueue<string> concurrentQueue = new ConcurrentQueue<string>();
Task t1 = Task.Factory.StartNew(() =>
{
for (int i = 0; i < 10; ++i)
{
concurrentQueue.Enqueue("Web request " + i);
Console.WriteLine("Sending "+ "Web request " + i);
Thread.Sleep(100);
}
});
现在我们有了Consumer
,其中Worker
线程从队列中提取请求并处理它:
Task t2 = Task.Factory.StartNew(() =>
{
while (true)
{
if (concurrentQueue.TryDequeue(out
string request))
{
Console.WriteLine("Processing "+
request);
}
else
{
Console.WriteLine("No request");
}
}
});
生产者和消费者任务同时成功执行。等待所有提供的任务在指定的毫秒数内完成执行。请参考以下代码片段:
try
{
Task.WaitAll(new Task[] { t1, t2 }, 1000);
}
catch (AggregateException ex) // No exception
{
Console.WriteLine(ex.Flatten().Message);
}
这是根据微软的方法定义:
concurrentQueue.Enqueue
:这在ConcurrentQueue<T>
的末尾增加了一个对象。concurrentQueue.TryDequeue
:这是在ConcurrentQueue
开始的时候尝试移除和返回物体。
运行程序时,可以看到task
t1
产生请求,task
t2
轮询,然后消耗请求。我们过一会儿再谈细节。我们也说过这些类实现IProducerConsumerCollection<T>
。因此,我将对前面的代码进行三处修改:
- 将
ConcurrentQueue<string>
替换为IProducerConsumerCollection<string>
。 - 将
concurrentQueue.Enqueue
替换为concurrentQueue.TryAdd
。 - 将
concurrentQueue.TryDequeue
替换为concurrentQueue.TryTake
。
代码现在是这样的:
IProducerConsumerCollection<string> concurrentQueue = new ConcurrentQueue<string>();
//Removed code for brevity.
Task t1 = Task.Factory.StartNew(() =>
{
for (int i = 0; i < 10; ++i)
{
concurrentQueue.TryAdd("Web request " + i);
//Removed code for brevity.
Task t2 = Task.Factory.StartNew(() =>
{
while (true)
{
if (concurrentQueue.TryTake(out string
request))
//Removed code for brevity.
现在继续运行程序。您可以看到task
t1
产生请求,task
t2
轮询,然后消费请求。你可以看到所有 10 个请求由task
t1
产生,由task
t2
消耗。但是有两个问题:
- 生产者按照自己的速度生产,消费者按照自己的速度消费,没有同步。
task
t2
有消费者连续不定的轮询,对性能和 CPU 使用率都不好,你可以看到没有请求正在打印,而我们没有收到concurrentQueue.TryTake
的任何处理请求。
这就是BlockingCollection<T>
派上用场的地方。
封锁集合< T >类
BlockingCollection<T>
支持包围和阻挡。边界允许您为集合指定最大容量。控制集合的最大大小有助于防止生成线程在使用线程之前移动太远。多个生产线程可以同时向BlockingCollection<T>
添加项目,直到集合达到其最大大小,之后它们将被阻止,直到某个项目被消费者移除。
类似地,多个消费线程可以同时从阻塞集合中移除项,直到集合变空,之后它们将被阻塞,直到生产者添加一个项。当不再添加项目时,产生线程可以调用CompleteAdding
方法,并指示它已经完成添加。这将有助于消费者监控IsCompleted
属性,以了解当集合为空时不会再添加任何项目。创建BlockingCollection<T>
类以及边界容量时,还可以根据场景指定要使用的并发集合类型。默认情况下,当您没有指定类型时,收集类型为BlockingCollection<T>
的ConcurrentQueue<T>
。
生产者:客户端向网络服务发送请求,服务器将请求存储在队列中。
下面是一个示例代码片段:
BlockingCollection<string> blockingCollection = new BlockingCollection<string>(new ConcurrentQueue<string>(),5);
Task t1 = Task.Factory.StartNew(() =>
{
for (int i = 0; i < 10; ++i)
{
blockingCollection.TryAdd("Web request
" + i);
Console.WriteLine("Sending " + "Web
request " + i);
Thread.Sleep(100);
}
blockingCollection.CompleteAdding();
});
然后具有Worker
线程的消费者从队列中取出项目并处理它:
Task t2 = Task.Factory.StartNew(() =>
{
while (!blockingCollection.IsCompleted)
{
if (blockingCollection.TryTake(out
string request,100))
{
Console.WriteLine("Processing " +
request);
}
else
{
Console.WriteLine("No request");
}
}
});
现在生产者线程和消费者线程正在并发访问。
代码中有几点需要考虑:
5
的指定边界:BlockingCollection<string> blockingCollection = new BlockingCollection<string>(new ConcurrentQueue<string>(),5);
。- 当不再添加项目时,产生线程调用
CompleteAdding
方法来指示它已经完成添加:blockingCollection.CompleteAdding();
。 - 消费者监控
IsCompleted
属性,发现集合为空时不再添加项目:while (!blockingCollection.IsCompleted)
。 - 尝试在指定的时间段内从
BlockingCollection<T>
中移除一个项目。比如我已经去了 100 毫秒:if (blockingCollection.TryTake(out string request, 100))
。
这就是阻塞集合的力量。生产者和消费者都是解耦的,它们可以由不同的团队独立编码,在运行时它们使用一个阻塞的并发集合来互相共享数据;另外,与此同时,流量是通过边界容量来控制的,这样生产者就不会在消费者之前走得太远。
注意
除了我们已经看到的TryTake
方法,您还可以使用foreach
循环从阻塞集合中移除项目。你可以在这里阅读:https://docs . Microsoft . com/en-us/dotnet/standard/collections/thread-safe/how-foreach-to-remove。
在阻止收藏的情况下,将会出现消费者不得不处理多个收藏并购买或添加物品的情况。在这种情况下,TakeFromAny
和AddToAny
方法会对你有所帮助。您可以在这里进一步阅读关于这两种方法:
总结
包装、编写和维护干净的异步代码是困难的。但是,使用中提供的各种构造.NET 和 C#,开发人员现在可以用更少的框架开销编写异步代码,并更加专注于业务需求。在本章中,我们介绍了使用第三方语言、异步等待和并发集合编写可伸缩异步代码的各种方法,还介绍了线程的基础知识和中的ThreadPool
.NET 来理解框架内部,并为企业应用编写更干净的代码。现在,我们对多线程以及如何在多线程环境中保护共享数据有了更深入的了解。我们学习了使用 async-wait 创建任务和实现异步函数,最后我们学习了中可用的并发集合.NET Core 及其在各种并发场景中的实现。
在下一章中,我们将研究.NET 5 以及它如何在松散耦合企业应用中的各种低级类方面发挥重要作用。
问题
-
In a multi-threaded environment, which of the following data structures should you use to protect data from getting overwritten/corrupted?
a.
async
-await
。b.任务。
c.同步结构,如锁。
d.数据永远不会损坏。
-
If you have a WPF application that retrieves data from a REST API, which of the following should you implement for better responsiveness?
a.并行收集
b.平行。为
c.
async
-await
为 REST API 调用 -
Which of the following should be passed to cancel a task?
a.
CancellationToken
b.
ConcurrentDictionary
c.
SemaphoreSlim
-
Which of the following is the recommended return type for an asynchronous method that uses async-await and does not return anything?
a.
async void
b.
async Task
c.
async book
d.
async Task<bool>
进一步阅读
- https://www . packtpub . com/product/动手用-c-8 和-net-core-3 进行并行编程/9781789132410
- https://devblogs.microsoft.com/dotnet/configureawait-faq/
- http://www.albahari.com/threading/
五、.NET 依赖注入
企业应用可能面临的一个大问题是将不同的元素连接在一起并管理它们的生命周期的复杂性。为了解决这个问题,我们使用控制反转 ( IoC )原理,该原理建议消除对象之间的依赖性。通过委派控制流,IoC 使程序可扩展并增加了模块化。事件、回调委托、观察者模式和依赖注入 ( DI )是实现 IoC 的一些方法。
在本章中,我们将了解以下内容:
- 什么是 DI?
- ASP.NET Core 5 中的直接投资
- 管理应用服务
- 使用第三方容器
到本章结束时,您将对直接投资有一个很好的了解,充分利用直接投资.NET 5 应用,ASP.NET Core 5 中提供的范围类型,以及如何在项目中利用它们。
技术要求
对微软的基本了解.NET 是必需的。
什么是 DI?
DI 是一种对象接收它所依赖的对象的技术。DI 模式实现了在 第 1 章设计和架构企业应用中作为固体设计原则的一部分所涵盖的 DI 原则。随着 DI 的使用,代码将变得更加可维护、可读、可测试和可扩展。
DI 是帮助实现更好的可维护代码的最著名的方法之一。
DI 涉及三个实体,如图图 5.1 :
图 5.1–直接投资关系
注入器创建服务的实例,并将其注入到客户端对象中。客户端依赖注入的服务来执行其操作。例如,在我们将要构建的企业应用中,IOrderRepository
负责Order
实体上的 CRUD 操作。IOrderRepository
将由运行时实例化并注入OrderController
。
IoC 容器,也称被称为 DI 容器,是实现自动 DI 的框架。在图 5.1 中,称为喷油器。负责创建或引用依赖关系、注入客户端。
现在我们已经了解了什么是 DI,让我们了解一下 DI 的类型。
DI 的类型
有多种方法可以将服务注入依赖项。根据服务注入客户端对象的方式,DI 分为三种类型:
-
Constructor injection: Dependencies are injected through a constructor while instantiating the dependent. In the following example, the
IWeatherProvider
dependency is injected through the constructor parameter:public class WeatherService { private readonly IweatherProvider weatherProvider; public WeatherService(IWeatherProvider weatherProvider) => this.weatherProvider = weatherProvider; public WeatherForecast GetForeCast(string location) => this.weatherProvider. GetForecastOfLocation (location); }
在前面的例子中,
WeatherService
依赖于IWeatherProvider
,它是通过构造函数参数注入的。 -
Setter 注入:在 setter 注入的情况下,依赖者暴露一个 Setter 方法或者属性,注入者使用这个属性注入依赖者。在本例中,初始化
WeatherService
时不设置IWeatherProvider
相关性。通过WeatherProvider
属性岗位对象初始化public class WeatherService2 { private IWeatherProvider _weatherProvider; public IWeatherProvider WeatherProvider { get => _weatherProvider == null ? throw new InvalidOperationException( "WeatherService is not initialized") : _weatherProvider; set => _weatherProvider = value; } public WeatherForecast GetForeCast(string location) => this.WeatherProvider. GetForecastOfLocation(location); }
设置
-
Method injection: Another way to inject dependency is by passing it as a method parameter. In this example, the
IWeatherProvider
dependency is injected as a method parameter wherever required.在下面的代码片段中,
IWeatherProvider
服务通过GetForecast
方法注入到WeatherService
中:public class WeatherService { public WeatherForecast GetForecast( string location, IWeatherProvider weatherProvider) { if(weatherProvider == null) { throw new ArgumentNullException( nameof(weatherProvider)); } return weatherProvider. GetForecastOfLocation (location); } }
当类有依赖关系时,如果没有依赖关系,功能就无法工作,那么我们使用构造函数注入。此外,当类中的多个函数使用依赖关系时,我们使用构造函数注入。当类实例化后依赖关系可能改变时,我们使用属性注入。当依赖项的实现随着每次调用而改变时,使用方法注入。
在大多数情况下,构造函数注入将用于干净且解耦的代码。但是根据需要,我们还将利用方法和属性注入技术。
我们现在已经学习了 DI 的概念。让我们深入研究一下.NET 5 提供了实现 DI 的方法。
ASP.NET Core 5 的 DI
在本节中,我们将详细了解 ASP.NET Core 5 支持的直接投资。
.NET 5 自带内置的 IoC 容器,简化了 DI。这是附带的Microsoft.Extensions.DependencyInjection
NuGet 包。ASP.NET Core 5 框架本身在很大程度上依赖于此。为了支持 DI,容器需要支持对对象/服务的三个基本操作:
- 注册:容器应该有注册依赖项的规定。这将有助于将正确的类型映射到类,以便它可以创建正确的依赖关系实例。
- 解析:容器应该通过创建依赖对象并将其注入依赖实例来解析依赖。IoC 容器通过传入所有必需的依赖项来管理注册对象的创建。
- 处理:容器负责管理通过它创建的依赖项的生存期。
英寸 NET 5,术语服务指的是由容器管理的依赖关系。提供的服务.NET 5 Framework 被称为框架服务,例如、IConfiguration
、ILoggerFactory
、IWebHostEnvironment
等等。开发人员为支持应用功能而创建的服务称为应用服务。
为了启动应用,ASP.NET Core 5 框架注入了一些依赖项,这些依赖项被称为框架服务。当您创建一个 ASP.NET Core 5 web 应用时,Startup
类被注入了所需的框架服务。当您创建一个 ASP.NET Core 5 网络项目时,您将看到如下定义的Startup
类:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IserviceCollection
services)
{
services.AddScoped<IWeatherProvider,
WeatherProvider>();
services.AddControllersWithViews();
}
}
在前面的例子中,Startup
类通过构造函数注入被注入IConfiguration
。IConfiguration
是一个框架服务,IoC 容器知道如何实例化,并通过构造函数注入来注入。IServiceCollection
、IApplicationBuilder
和IWebHostEnvironment
通过方法注射进行注射。
应用服务是由开发人员注入容器的那些服务。应用服务将通过IServiceCollection
在ConfigureService
方法内注册。从前面的代码中可以看出,IWeatherProvider
应用服务是通过服务注册的。
在本节中,我们已经了解了什么是框架服务和应用服务。在下一节中,我们将了解这些服务的生命周期以及如何管理它们。
注意
参考 第 10 章创建 ASP.NET Core 5 Web API ,了解更多Startup
类中注入的框架服务。
了解服务生存期
当你注册一个有指定生存期的服务时,容器会根据指定的生存期自动处置对象。有三种类型的生存期可供 Microsoft DI 容器使用。T 嘿如下:
-
瞬态:在这个生存期内,每次从服务容器请求对象时都会创建该对象。将此生存期用于无状态的轻量级服务。如果创建服务很耗时,这可能不是正确的范围,因为这会增加延迟。
AddTransient
扩展方法用于注册此寿命:public static IServiceCollection AddTransient(this IServiceCollection services, Type serviceType);
-
Singleton:Singleton 生存期允许容器在每个应用生命周期中只创建一个对象。每次请求时,我们都会得到相同的对象。当对服务有第一个请求时,或者当在注册时直接提供实现实例时,创建对象。当
ServiceProvider
在应用关闭时被处理掉时,注册为单例的服务被处理掉。AddSingleton
扩展方法用于注册此生:public static IServiceCollection AddSingleton(this IServiceCollection services, Type serviceType);
-
作用域:通过使用这个生存期,服务将只在客户端请求作用域中创建一次。这在 ASP.NET Core 5 中特别有用,在那里,每个 HTTP 请求创建一个对象实例。像实体框架核心
DbContext
这样的服务是用作用域生存期注册的。AddScoped
扩展方法用于注册作用域生存期范围:public static IServiceCollection AddScoped(this IServiceCollection services, Type serviceType);
在应用开发中,需要明智地选择生存期类型。一个服务不应该依赖于一个寿命比它自己的寿命短的服务。例如,注册为单例的服务不应该依赖于注册为临时的服务。下表显示了哪些生存期可以安全地依赖于哪些其他生存期范围:
表 5.1
作为开发人员,您不需要担心范围验证。当环境设置为开发时,内置范围验证在启动期间在 ASP.NET Core 5 中完成。在配置错误的情况下,应用启动时会抛出InvalidOperationException
。这可以通过在注册ServiceProvider
时启用ValidateScopes
选项来明确打开,如下代码所示。在以下代码中,在创建宿主构建器时,ValidateScopes
被设置为true
以打开范围验证:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseDefaultServiceProvider(opt => {
opt.ValidateScopes = true; })
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
让我们创建一个 ASP.NET Core 5 web 应用来了解服务生命周期。我们将创建不同的服务,并将它们注册到单一的、有作用域的和短暂的生存期作用域中,并观察它们的行为:
-
创建一个新的 ASP.NET Core web 应用,命名为
DISampleWeb
。 -
Create a new project folder with the name
Services
and add three classes:ScopedService
,SingletonService
, andTransientService
. Add the following code (all these services will be the same without any real code in them; we just register them with different lifetime scopes as per their name):注意
在下面的示例中,接口和类在一个文件中定义。这纯粹是为了演示。理想情况下,接口和类将在两个不同的类中定义。
-
ScopedService.cs
:这个类将注册到作用域生存期范围:public interface IScopedService { } public class ScopedService : IScopedService { }
-
SingletonService.cs
:该类将注册到单体生存期范围:public interface ISingletonService { } public class SingletonService : ISingletonService { }
-
TransientService.cs
:这个类将在public interface ITransientService { } public class TransientService : ITransientService { }
瞬态寿命范围内注册
-
-
现在,用
ConfigureServices
方法在startup.cs
注册这些服务,如下所示:public void ConfigureServices(IserviceCollection services) { //Register as Scoped services.AddScoped<IScopedService, ScopedService>(); //Register as Singleton services.AddSingleton<ISingletonService, SingletonService>(); //Register as Transient services.AddTransient<ITransientService, TransientService>(); services.AddControllersWithViews(); }
-
Now, add the
HomeViewModel
model class under theModels
folder, which will be used to show the data retrieved from the services registered previously:public class HomeViewModel { public int Singleton { get; set; } public int Scoped { get; set; } public int Scoped2 { get; internal set; } public int Transient { get; set; } public int Transient2 { get; internal set; } }
由于我们在 ASP.NET Core 5 IoC 容器中注册了
ScopedService
、SingletonService
和TransientService
,我们将通过构造函数注入获得这些服务。 -
Now, we will add code to get these services in
HomeController
andViews
to show the data retrieved from these objects on the home page. Modify the home controller to get two instances ofScopedService
andTransientService
and setViewModel
with theHash
code of the service object:图 5.2–解决方案结构
注意
GetHashCode
方法返回对象的哈希码。这将因实例而异。 -
修改
HomeController
的构造函数,接受注册的服务,定义私有字段引用服务实例:private readonly ILogger<HomeController> _logger; private readonly IScopedService scopedService; private readonly IscopedService scopedService2; private readonly IsingletonService singletonService; private readonly ItransientService transientService; private readonly ItransientService transientService2; public HomeController(ILogger<HomeController> logger, IScopedService scopedService, IScopedService scopedService2, ISingletonService singletonService, ITransientService transientService, ITransientService transientService2) { _logger = logger; this.scopedService = scopedService; this.scopedService2 = scopedService2; this.singletonService = singletonService; this.transientService = transientService; this.transientService2 = transientService2; }
-
现在,修改在
HomeController
下的Index
方法来设置HomeViewModel
:public IActionResult Index() { var viewModel = new HomeViewModel { Scoped = scopedService.GetHashCode(), Scoped2 = scopedService2. GetHashCode(), Singleton = singletonService. GetHashCode(), Transient = transientService. GetHashCode(), Transient2 = transientService2. GetHashCode(), }; return View(viewModel); }
-
现在,修改
~/Views/Home
文件夹下的Index.cshtml
,如图所示,在页面上显示HomeViewModel
:@model HomeViewModel @{ ViewData["Title"] = "Home Page"; } <h2 class="text-success">Singleton.</h2> <p> <strong>ID:</strong> <code>@Model.Singleton </code> </p> <h2 class="text-success">Scoped instance 1</h2> <p> <strong>ID:</strong> <code>@Model.Scoped</code> </p> <h2 class="text-success">Scoped instance 2</h2> <p> <strong>ID:</strong> <code>@Model.Scoped2</code> </p> <h2 class="text-success">Transient instance 1</h2> <p> <strong>ID:</strong> <code>@Model.Transient</code> </p> <h2 class="text-success">Transient instance 2</h2> <p> <strong>ID:</strong> <code>@Model.Transient2</code> </p>
-
Now, run the application. You will see an output as follows:
图 5.3–首次运行时的样本输出
如果我们观察输出,为两个
ScopedService
实例显示的标识是相同的。这是因为每个请求范围只为IScopedService
创建了一个对象。对于这两种服务,临时服务的标识是不同的。正如我们所了解到的,这是因为对 IoC 容器的每个请求都会创建一个新的实例。
-
现在,再次刷新页面。我们看到如下输出:
图 5.4–第二次运行的样本输出
如果我们比较一下图 5.3 和图 5.4 中的输出,我们可以注意到SingletonService
的 ID 没有变化。这是因为在应用的每个生命周期中,只为单例对象创建一个对象。到目前为止,我们已经看到了如何基于注册来管理服务生存期。了解何时处置对象也很重要。在下一节中,我们将了解服务的处置。
服务处置
正如我们在本章前面所学的,物体的处理是国际奥委会容器的责任。容器在那些实现IDisposable
的服务上调用Dispose
方法。由容器创建的服务永远不应该由开发人员显式处置。
考虑下面的代码片段,其中SingletonService
实例在单例范围内注册:
//Register as Singleton
services.AddSingleton(new DisposableSingletonService());
在前面的代码中,我们创建了一个对象SingletonService
,并将其注册到 IoC 容器中。服务实例不是由容器创建的。在这种情况下,IoC 容器不会处置对象。开发商有责任处置它。当IHostApplicationLifetime
触发ApplicationStopping
事件时,我们可以处理对象,如下代码所示:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime applicationLifetime)
{
applicationLifetime.ApplicationStopping.Register(() => {
_disposableSingletonService.Dispose();
});
}
在前面的代码中,运行时将IHostApplicationLifetime
注入到Startup
类中的Configure
方法中。该界面允许消费者收到ApplicationStarted
、ApplicationStopped
和ApplicationStopping
应用生命周期事件的通知。为了处置单例对象,我们将通过注册到ApplicationStopping
生存期事件来调用处置方法。
注意
请参考以下微软文档,了解更多关于 DI 指南的信息:
https://docs . Microsoft . com/en-us/dotnet/core/extensions/dependency-injection-guidelines
到目前为止,我们已经研究了服务生存期以及它们在中的处理方式.NET 5。在下一节中,我们将学习管理应用服务。
管理应用服务
在 ASP.NET Core 5 中,当MvcMiddleware
收到请求时,路由用于选择控制器和动作方式。IControllerActivator
创建控制器的实例,并从 DI 容器加载构造函数参数。
在上一节了解服务生存期中,我们看到了应用服务是如何注册的,以及它们的生存期是如何管理的。在这个例子中,服务是通过构造函数注入的,这就是构造函数注入。在本节中,我们将看到如何实现方法注入,以及如何通过不同的方式在 ASP.NET Core 5 IoC 容器中注册和访问应用服务。
通过方法注入访问注册服务
在前面的小节中,我们看到了如何将依赖服务注入到控制器构造函数中,并将引用存储在用于调用依赖的方法/应用编程接口的本地字段中。
有时,我们不希望依赖服务在控制器的所有操作中都可用。在这种情况下,可以通过方法注入来注入服务。这是通过使参数具有FromService
属性来完成的,如下例所示:
public IActionResult Index([FromServices] ISingletonService singletonService2)
{
}
在下一节中,我们将看到同一服务类型的多个实例的注册以及如何访问它们。
注册多个实例
如果一个以上的实现注册了相同的服务类型,最后一次注册将优先于所有以前的注册。考虑以下服务注册,其中IWeatherForeCastService
服务注册有两个实现:WeatherForeCastService
和WeatherForeCastServiceV2
:
services.AddScoped<IWeatherForcastService, WeatherForcastService>();
services.AddScoped<IWeatherForcastService, WeatherForcastServiceV2>();
现在,当控制器发出请求IWeatherForeCastService
实例时,WeatherForeCastServiceV2
实例将被服务:
public WeatherForecastController(ILogger<WeatherForecastController> logger, IWeatherForcastService weatherForcastService)
{
_logger = logger;
this.weatherForcastService = weatherForcastService;
}
在前面的例子中,WeatherForecastV2
的注册可能会覆盖WeatherForecastService
的先前注册。然而,ASP.NET Core 5 国际奥委会集装箱将拥有IWeatherForeCastService
的所有注册。要获取所有注册,获取服务为IEnumerable
:
public WeatherForecastController(ILogger<WeatherForecastController> logger, IEnumerable<IWeatherForcastService> weatherForcastService)
{
_logger = logger;
this.weatherForcastService = weatherForcastService;
}
这在场景中可能很有用,例如执行规则引擎,在这里我们希望在处理请求之前运行所有规则。该组规则将通过Startup
调用中的ConfigureServices
方法进行配置。所以,在未来,当有一个规则的增加或一个现有规则的删除时,变化将只在ConfigureServices
发生。
TryAdd 的使用
在本节中,我们将了解如何避免意外覆盖已经注册的服务。
TryAdd
扩展方法仅在同一服务不存在注册时注册服务。TryAdd
扩展方法适用于所有寿命范围(TyrAddScoped
、TryAddSingleton
和TryAddTransient
)。
通过如下代码所示的服务注册,当有IweatherForecastService
请求时,IoC Container 服务WeatherForecastService
,而不是WeatherForecastServiceV2
:
services.AddScoped<IWeatherForcastService, WeatherForcastService>();
services.TryAddScoped<IWeatherForcastService, WeatherForcastServiceV2>();
为了克服重复注册可能产生的副作用,总是建议使用TryAdd
扩展方法来注册服务。
现在,让我们看看如何替换已经注册的服务。
替换现有注册
ASP.NET Core 5 国际奥委会容器提供了一种替代现有注册的方法。在以下示例中,IWeatherForeCastService
最初在WeatherForeCastService
注册。然后替换为WeatherForeCastServiceV2
:
services.TryAddScoped<IWeatherForcastService, WeatherForcastService>();
services.Replace(ServiceDescriptor.Scoped<IWeatherForcastService, WeatherForcastServiceV2>());
与WeatherForeCastServiceV2
的Replace
实例一样,实现服务于WeatherForeCastController
的构造器。在下面的代码片段中,不同于中的注册多个实例部分,我们将在weatherForcastService
构造函数变量中只看到一个对象:
public WeatherForecastController(ILogger<WeatherForecastController> logger, IEnumerable<IWeatherForcastService> weatherForcastService)
{
_logger = logger;
this.weatherForcastService = weatherForcastService;
}
在下一节中,我们将看到如何删除已注册的服务。
删除现有注册
如果您希望删除现有的注册,您可以使用 ASP.NET Core 5 国际奥委会容器提供的Remove
扩展方法。您可以使用RemoveAll
方法删除与服务相关的所有注册,如以下代码片段所示:
//Removes the first registration of IWeatherForcastService services.Remove(ServiceDescriptor.Scoped<IWeatherForcastService, WeatherForcastService>());
在前面的代码片段中,Remove
方法从容器中移除了WeatherForeCastService
实现的注册:
services.RemoveAll<IWeatherForcastService>();
到目前为止,我们已经看到了如何使用复杂的服务。但是当涉及到泛型开放类型时,很难注册每个泛型构造类型。在下一节中,我们将学习如何处理通用开放类型服务。
注意
要了解更多关于泛型类型的信息,您可以参考以下网站:
https://docs . Microsoft . com/en-us/dotnet/csharp/language-reference/language-specification/type
注册泛型
本节将带您了解使用 DI 处理泛型类型服务的。
在泛型类型的情况下,为正在使用的每种实现类型注册服务是没有意义的。ASP.NET Core 5 IoC 容器提供了一种简化泛型类型注册的方法。框架本身已经提供的一种类型是ILogger
:
services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>)));
注意
使用泛型的另一个用例是数据访问层使用的泛型存储库模式。
有了我们所有的注册,ConfigureServices
方法会变大,不再可读。下一部分将帮助您了解如何解决这个问题。
代码可读性的扩展方法
为了使代码看起来更易读,ASP.NET Core 5 框架中遵循的模式是创建一个带有服务注册逻辑分组的扩展方法。下面的代码尝试使用扩展方法对通知相关服务进行分组和注册。一般的做法是使用Microsoft.Extensions.DependencyInjection
命名空间来定义服务注册扩展方法。这将使开发人员仅通过使用Microsoft.Extensions.DependencyInjection
名称空间就可以使用与 DI 相关的所有功能。
在下面的代码片段中,通知相关服务注册到了AddNotificationServices
:
namespace Microsoft.Extensions.DependencyInjection
{
public static class NotificationServicesServiceCollectionExtension
{
public static IServiceCollection AddNotificationServices(this IServiceCollection services)
{
services.TryAddScoped<INotificationService, EmailNotificationService>();
services.TryAddScoped<INotificationService, SMSNotificationService>();
return services;
}
}
}
现在扩展方法已经创建,我们可以使用AddNotificationServices
方法在ConfigureServices
下注册通知服务。这将使ConfigureServices
变得更加易读:
public void ConfigureServices(IServiceCollection services)
{
services.AddNotificationServices();
}
我们已经看到了如何将服务注入控制器和其他类。在下一节中,我们将学习如何将服务注入视图。
剃刀页中的 DI
MVC 中视图的目的是显示数据。大多数情况下,视图中显示的数据是从控制器传递的。考虑到关注点分离原则,建议从控制器传递所有需要的数据。但是可能有些情况下,我们希望从本地化和遥测服务等页面查看特定服务。使用 Razor 视图支持的 DI,我们可以将这样的服务注入到视图中。
要了解如何将服务注入视图,让我们修改在前面几章中创建的DISampleWeb
应用。如果设置了飞行标志,我们将修改DISampleWeb
应用以在主页上显示附加内容。将如下代码片段所示的isFlightOn
配置添加到appsettings.json
:
{
"AllowedHosts": "*",
"isFlightOn": "true"
}
现在,修改Home
下的索引视图,显示Flight
下的内容,如下面的代码片段所示:
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration
@{
string isFlightOn = Configuration["isFlightOn"];
if (string.Equals(isFlightOn, "true", StringComparison.OrdinalIgnoreCase))
{
<h1>
<strong>Flight content</strong>
</h1>
}
}
这里,提供读取配置文件功能的IConfiguration
服务使用@inject
关键字注入到 Razor 视图中。注入的配置服务用于获取配置并根据设置显示附加内容。我们可以使用@inject
关键字将任何在IServiceCollection
注册的服务注入到 Razor 视图中。
到目前为止,我们已经看到了如何利用?NET 5 内置的 IoC 容器。在下一节中,我们将了解如何利用第三方容器。
使用第三方容器
虽然内置容器对于我们的大多数场景来说已经足够了.NET 5 提供了一种与第三方容器集成的方法,如果需要,可以利用这些容器。
让我们仔细看看框架是如何连接服务的。当Startup
类在Program.cs
的HostBuilder
注册时.NET 框架使用反射来识别和调用Configure
和ConfigureServices
方法。
以下是 ASP.NET Core 5 中StartupLoader
类的一个片段:
var configureMethod = FindConfigureDelegate(startupType, environmentName);
var servicesMethod = FindConfigureServicesDelegate(startupType, environmentName);
var configureContainerMethod = FindConfigureContainerDelegate(startupType, environmentName);
从前面的代码中,我们可以看到,前两个方法FindConfigureDelegate
和FindConfigureServicesDelegate
是寻找Configure
和ConfigureServices
的方法。
最后一行是ConfigureContainer
。我们可以在Startup
类中定义ConfigureContainer
方法,将服务配置到第三方容器中。
以下是 ASP.NET Core 5 可用的一些流行的 DI 框架:
- Unity : Unity 最初由微软打造,目前是开源的。这是. NET 最古老的 DI 容器之一。文档可在http://unitycontainer.org/获得。
- Autofac :这是最受欢迎的 DI 容器之一。它在https://autofaccn.readthedocs.io/en/latest/index.html有综合文档。
- 简易注射器:这是名单上的后期进入者之一。文档可在https://simpleinjector.readthedocs.io/en/latest/index.html找到。
- 温莎城堡:这是. NET 可用的 DI 框架中最好的框架。请参见他们在http://www.castleproject.org/projects/windsor/的文档。
尽管这些框架之间有一些不同,但这些框架中通常都有功能对等。决定框架选择的主要是开发人员的专业知识。
在下一节中,让我们看看如何利用 Autofac IoC 容器。
Autofac IoC 容器
Autofac 是开发者社区中最受欢迎的 IoC 容器之一。像任何其他 IoC 容器一样,它管理类之间的依赖关系,以便应用在复杂性和规模增长时易于更改。让我们通过创建一个小应用来学习如何使用自动交流:
-
创建一个新的 ASP。通过选择应用编程接口模板并将其命名为
AutofacSample
,NET Core web 应用。 -
Add the
Autofac.Extensions.DependencyInjection
NuGet package reference to theAutofacSample
project:图 5.5–添加自动空调。扩展。依赖注入获取包
-
我们需要向
HostBuilder
注册AutofacServiceProviderFactory
,这样运行时将使用 Autofac IoC 容器。在Program.cs
中,注册 Autofac 服务提供商工厂,如以下代码片段所示:public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .UseServiceProviderFactory(new AutofacServiceProviderFactory()) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
-
让我们继续为我们的项目添加一个简单的天气预报服务。新建一个名为
Service
的解决方案文件夹,添加IWeatherProvider
和WeatherProvider
界面,如下。WeatherProvider
班生成第二天的随机预报:public interface IWeatherProvider { IEnumerable<WeatherForecast> GetForecast(); } public class WeatherProvider : IWeatherProvider { private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; public IEnumerable<WeatherForecast> GetForecast() { var rng = new Random(); return Enumerable.Range(1, 1).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = rng.Next(-20, 55), Summary = Summaries[rng.Next(Summaries.Length)] }).ToArray(); } }
-
现在,让我们用 Autofac 容器注册
IWeatherProvider
服务。将ConfigureContainer
方法添加到Startup.cs
中的Startup
类中,以注册IWeatherProvider
,实现:public void ConfigureContainer(ContainerBuilder builder) { builder.RegisterType<WeatherProvider>() .As<IWeatherProvider>(); }
-
To get the
IweatherForecast
service injected into theWeatherForecastController
controller, add a constructor argument as shown in the following code. Also, modify theGet
action to make use of theIWeatherProvider
services:public class WeatherForecastController : ControllerBase { private readonly ILogger<WeatherForecastController> _logger; private readonly IWeatherProvider weatherProvider; public WeatherForecastController( ILogger<WeatherForecastController> logger, IWeatherProvider weatherProvider) { _logger = logger; this.weatherProvider = weatherProvider; } [HttpGet] public IEnumerable<WeatherForecast> Get() { return weatherProvider.GetForecast(); } }
现在,当您运行项目时,您将在浏览器中看到如下输出:
图 5.6–容器的最终输出
在前面的示例中,我们已经看到使用第三方 Autofac IoC 容器来代替提供的默认容器.NET 5。
总结
本章向您介绍了 DI 的概念,它有助于编写松散耦合、更易测试和更易读的代码。本章介绍了 DI 的类型以及 ASP.NET Core 5 如何支持它们。我们还看到了如何使用不同类型的注册来管理对象生存期。本章还向您介绍了一些流行的第三方 IoC 容器,供您进一步探索。我们将使用本章中学习的概念来构建我们的电子商务应用。在 第 15 章测试中,我们也将看到 DI 如何帮助测试性。
正如 第 1 章设计和架构企业应用所建议的,在关注点分离/单一责任架构部分,我们将始终尝试通过接口注册服务。这将有助于在不改变客户端实现的情况下随时改变具体的实现。
在下一章中,我们将学习如何配置.NET 5,并了解不同的配置,同时学习如何建立一个自定义的。
问题
-
Which of the following is not a framework service?
a.
IConfiguration
b.
IApplicationBuilder
c.
IWeatherService
d.
IWebHostEnvironment
-
True or false: DI is one of the mechanisms to achieve IoC?
a.真实的
b.错误的
-
True or false: An injected service can depend on a service that has a shorter life span than its own?
a.真实的
b.错误的
-
Which of the following is not a valid lifetime scope of ASP.NET Core 5 IoC Container?
a.审视
b.一个
c.短暂的
d.动态的
六、.NET Core 的配置
中的配置.NET 5 包含应用的默认设置和运行时设置;配置是一个非常强大的特性。我们可以更新功能标志等设置,以启用或禁用功能、相关服务端点、数据库连接字符串、日志记录级别等,并在运行时控制应用行为,而无需重新编译。
在本章中,我们将涵盖以下主题:
- 了解配置
- 利用内置配置提供程序
- 构建自定义配置提供程序
到本章结束时,您将很好地掌握配置概念、配置提供者以及如何在项目中利用它们,并且能够确定适合您的应用的配置和配置源。
技术要求
你需要对微软有一个基本的了解.NET 和 Azure。本章的代码可以在这里找到:
了解配置
配置通常存储为名称-值对,并且可以分组为多级层次结构。在应用启动文件(Startup.cs
)中,您将获得由提供的默认配置.NET 5。此外,您可以配置不同的内置和自定义配置源,然后在应用的任何地方需要时使用不同的配置提供程序读取它们:
图 6.1–应用和配置
上图显示了应用、配置提供程序和配置文件之间的高级关系。应用使用配置提供程序从配置源读取配置;该配置可以是特定于环境的。 Env A 可能是你的开发环境Env B可能是你的生产环境。在运行时,应用将根据运行时的上下文和环境读取正确的配置。
在下一节中,我们将看到默认配置是如何工作的,以及如何从appsettings.json
文件中添加和读取配置。
默认配置
为了理解默认配置是如何工作的,让我们创建一个新的.NET 5 web API,将项目名称设置为TestConfiguration
,打开Program.cs
。以下是来自Program.cs
文件的代码片段:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[]
args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
从前面的代码中,我们看到CreateDefaultBuilder
负责为应用提供默认配置。
配置的加载按以下顺序进行:
ChainedConfigurationProvider
:这将添加主机配置并将其设置为第一个源。有关主机配置的更多详细信息,您可以使用以下链接:https://docs . Microsoft . com/en-us/aspnet/core/foundations/configuration/?视图= aspnet core-3.1 #暖通空调。JsonConfigurationProvider
:这将从appsettings.json
文件加载配置。JsonConfigurationProvider
:从appsettings.Environment.json
文件加载配置;Environment
中的appsettings.Environment.json
可以设置为参考开发、分期、生产等等。EnvironmentVariablesConfigurationProvider
:这将加载环境变量配置。CommandLineConfigurationProvider
:这将从命令行参数键值对加载配置。
如本节开头所述,配置被指定为源中的键值对。稍后添加的配置提供程序(按照顺序)会覆盖以前的键值对设置。例如,如果您在appsettings.json
中有一个DbConnectionString
键以及一个命令行配置源,命令行源中的DbConnectionString
键的值将覆盖appsettings.json
的键值对设置。
Program.cs
之后,接下来要看的重要一课是Startup.cs
。Startup.cs
类是在构建应用的主机时指定的,通常通过调用Program.cs
的主方法中调用的Host.CreateDefaultBuilder(args).UseStartup<Startup>()"
来指定。
下面是一个使用ConfigureServices
和Configure
方法的Startup.cs
类。应用可以在ConfigureServices
中注册附加服务,并通过依赖注入 ( DI )使用它们。应用的请求处理管道通过Configure
方法创建:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this
// method to add services to the container.
public void ConfigureServices(IServiceCollection
services)
{
services.AddControllers();
}
// This method gets called by the runtime. Use this
// method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app,
IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//Removed code for brevity
}
调试Startup.cs
代码时,可以看到CreateDefaultBuilder
提供的默认配置被注入到配置中,如下所示:
图 6.2–默认配置源
在本节中,我们看到了如何为应用提供默认配置以及提供的顺序。您可以在下面的依赖关系映射和控制流的代码映射中看到这一点:
图 6.3–代码映射依赖关系
在下一节中,让我们看看如何添加应用所需的配置。
添加配置
正如我们在上一节中看到的,有多个配置源可用。appsettings.json
文件是现实世界项目中最广泛使用的添加应用所需配置的文件,除非它是一个秘密,不能以纯文本形式存储。
让我们看几个需要配置的常见场景:
- 如果我们需要一个应用洞察仪器键来添加应用遥测,这可以是我们配置的一部分。
- 如果我们有需要调用的依赖服务,这可能是我们配置的一部分
这些可能因环境而异(开发环境和生产环境之间的值不同)。
您可以将以下配置添加到appsettings.json
文件中,以便在发生更改时可以直接更新它,并且无需重新编译和部署即可开始使用它:
"ApplicationInsights": {
"InstrumentationKey": "<Your instrumentation key>"
}
"ApiConfigs": {
"Service 1": {
"Name": "<Your dependent service name 1>",
"BaseUri": "<Service base uri>",
"HttpTimeOutInSeconds": "<Time out value in
seconds>",
"ApiURLs": [
{
"EndpointName": "<End point 1>"
},
{
"EndpointName": "<End point 2>"
}
]
},
"Service 2": {
"Name": "<Your dependent service name 2>",
"BaseUri": "<Service base uri>",
"HttpTimeOutInSeconds": "<Time out value in
seconds>",
"ApiURLs": [
{
"EndpointName": "<End point 1>"
},
{
"EndpointName": "<End point 2>"
}
]
}
}
从前面的代码中,我们看到我们已经为ApplicationInsights
仪器键添加了一个键值对,其中键是InstrumentationKey
字符串,值是应用在ApplicationInsights
仪器遥测所需的实际仪器键。在ApiConfigs
部分,我们按照层次顺序添加了多个键值对,配置为来调用我们的依赖服务。
在下一节中,我们将看到如何阅读我们添加的配置。
读取配置
我们已经看到了如何将配置添加到appsettings.json
中。在这一节中,我们将看到如何使用可用的不同选项在项目中阅读它们。
您在CreateDefaultBuilder
提供的Startup.cs
中获得的配置对象是Microsoft.Extensions.Configuration.IConfiguration
类型,您可以在IConfiguration
中读取以下选项:
// Summary:
// Gets or sets a configuration value.
// Parameters:
// key:
// The configuration key.
// Returns:
// The configuration value.
string this[string key] { get; set; }
// Summary:
//Gets the immediate descendant configuration sub-
//sections.
// Returns:
// The configuration sub-sections.
IEnumerable<IConfigurationSection> GetChildren();
// Summary:
// Gets a configuration sub-section with the specified key.
// Parameters:
// key:
// The key of the configuration section.
// Returns:
// The Microsoft.Extensions.Configuration.IConfigurationSection.
// Remarks:
// This method will never return null. If no matching sub-section is found with the specified key, an empty Microsoft.Extensions.Configuration.IConfigurationSection will be returned.
IConfigurationSection GetSection(string key);
让我们看看如何从Iconfiguration
利用这些选项来阅读我们在上一节添加配置中添加的配置。
要从appsettings.json
读取应用洞察检测键,我们可以使用以下代码使用string this[string key] { get; set; }
选项:
Configuration["ApplicationInsights:InstrumentationKey"];
要阅读ApiConfigs
,我们可以使用以下代码。我们可以在配置 API 的配置键中使用分隔符来读取分层配置:
Configuration["ApiConfigs:Service 1:Name"];
注意
使用分隔符以这种方式读取容易出错,并且难以维护。首选方法是使用 ASP.NET Core 提供的选项模式。options 模式不是逐个读取每个键/设置值,而是使用类,这也将为您提供对相关设置的强类型访问。
当配置设置被场景隔离到强类型类中时,应用遵循两个重要的设计原则:
- 接口隔离原理 ( ISP ,或封装原理
- 关注点分离
通过 ISP 或封装,您可以通过定义良好的接口或合同读取配置,并且只依赖于您需要的配置设置。此外,如果有一个巨大的配置文件,这将有助于分离关注点,因为应用的不同部分不会依赖于相同的配置,从而允许它们解耦。让我们看看如何在代码中利用选项模式。
您可以创建以下ApiConfig
和ApiUrl
类,并将它们添加到您的项目中:
public class ApiConfig
{
public string Name { get; set; }
public string BaseUri { get; set; }
public int HttpTimeOutInSeconds { get; set; }
public List<ApiUrl> ApiUrls { get; set; }
}
public class ApiUrl
{
public string EndpointName { get; set; }
}
在构造函数或Startup.cs
的ConfigureServices
方法中添加以下代码,使用GetSection
方法读取配置,然后调用Bind
将配置绑定到我们拥有的强类型类:
List<ApiConfig> apiConfigs = new List<ApiConfig>();
Configuration.GetSection("ApiConfigs").Bind(apiConfigs);
GetSection
用指定的键将从appsettings.json
读取具体的章节。Bind
将尝试通过将属性名与配置键进行匹配,将给定的对象实例绑定到配置值。如果请求的部分不存在,则GetSection(string sectionName)
将返回null
。在现实世界的程序中,请确保您添加了空检查。
在本节中,我们看到了如何通过使用配置 API 来添加和读取appsettings.json
中的数据。但是我确实提到了我们应该使用appsettings.json
来显示纯文本,而不是秘密。
在下一节中,我们将了解内置的配置提供程序,以及如何使用 Azure 密钥库配置提供程序添加和读取机密。
利用内置配置提供者
除了appsettings.json
之外,还有多个配置源可用.NET 5 提供了几个内置的配置提供程序来读取它们。以下是的内置提供程序.NET 5:
- Azure 密钥库配置提供程序从 Azure 密钥库中读取配置。
- 文件配置提供程序从 INI、JSON 和 XML 文件中读取配置。
- 命令行配置提供程序从命令行参数中读取配置。
- 环境变量配置提供程序从环境变量中读取配置。
- 内存配置提供程序从内存集合中读取配置。
- Azure App 配置提供者从 Azure App 配置中读取配置。
- 每个文件的密钥配置提供程序从目录的文件中读取配置。
让我们看看如何利用 Azure 密钥库配置提供程序和文件配置提供程序,因为这两者都很重要,并且与其他提供程序相比使用更广泛。您可以使用以下链接了解我们在此未详细介绍的其他配置提供商:https://docs . Microsoft . com/en-us/dotnet/core/extensions/configuration-providers
Azure 密钥库配置提供程序
Azure Key Vault 是一项基于云的服务,提供了一个集中式配置源,用于安全存储密码、证书、API 密钥和其他机密。访问密钥库需要身份验证和授权。身份验证通过Azure Active Directory(AAD)完成,授权可以通过基于角色的访问控制 ( RBAC )或密钥库访问策略完成。
让我们看看如何创建一个密钥库,向其中添加一个秘密,并使用 Azure 密钥库配置提供程序从应用中访问它。
创建密钥库并添加机密
在本节中,我们将使用 Azure Cloud Shell 创建一个密钥库并添加一个秘密。Azure Cloud Shell 是基于浏览器的,可以用来管理 Azure 资源。以下是您需要采取的步骤列表:
-
Sign in to the Azure portal using https://portal.azure.com. Select the Cloud Shell icon on the portal page:
图 6.4–天青云外壳
-
You will get an option to select Bash or PowerShell. Choose PowerShell. You can change shells at any time:
图 6.5–Azure 云外壳选项–PowerShell 和 Bash
-
Create a resource group with the folowing command:
az group create --name "{RESOURCE GROUP NAME}" --location {LOCATION}
我为这个演示运行的实际命令如下:
az group create --name "ConfigurationDemoVaultRG" --location "East US"
{RESOURCE GROUP NAME}
代表新资源组的资源组名称,{LOCATION}
代表 Azure 区域(代表您的数据中心)。 -
Create a key vault in the resource group with the following command:
az keyvault create --name {KEY VAULT NAME} --resource-group "{RESOURCE GROUP NAME}" --location {LOCATION}
下面是我为这个演示运行的实际命令:
az keyvault create --name "TestKeyVaultForDemo" --resource-group "ConfigurationDemoVaultRG" --location "East US"
{
KEY VAULT NAME}
:新密钥库的唯一名称{RESOURCE GROUP NAME}
:上一步创建的新资源组的资源组名称{LOCATION}
: Azure 区域(数据中心) -
Create secrets in the key vault as name-value pairs with the following command:
az keyvault secret set --vault-name {KEY VAULT NAME} --name "SecretName" --value "SecretValue"
下面是我为这个演示运行的实际命令:
az keyvault secret set --vault-name "TestKeyVaultForDemo" --name "TestKey" --value "TestValue"
{
KEY VAULT NAME}
:与您在上一步中创建的密钥库名称相同SecretName
:你的秘密的名字SecretValue
:你的秘密的价值
我们现在已经成功创建了名为TestKeyVaultForDemo
的密钥库,并使用 Azure Cloud Shell 添加了一个密钥为TestKey
且值为TestValue
的秘密:
图 6.6–Azure 密钥库机密
也可以使用 Azure 命令行界面 ( CLI )来创建和管理 Azure 资源。你可以在这里阅读更多关于蔚蓝海岸的信息:https://docs.microsoft.com/en-us/cli/azure/?view=azure-cli-latest 。
在下一节中,我们将看到如何让我们的应用访问密钥库。
授予应用对密钥库的访问权限
在本节中,让我们看看我们的测试配置网络应用编程接口如何通过以下步骤获得对密钥库的访问:
-
在 AAD 中注册
TestConfiguration
应用并创建身份。使用 https://portal.azure.com 登录 Azure 门户网站。 -
Navigate to Azure Active Directory | App Registrations. Click on New registration:
图 6.7–AAD 新应用注册
-
Fill in the defaults and click on Register, as shown in the following screenshot, and note down the Application (client) ID value. This will be required later to access Key Vault:
图 6.8–AAD 注册完成
-
Click on Certificates & secrets (1) | New client secret (2) and enter a Description (3) value, then click on Add (4) as shown in the following screenshot. Note down the AppClientSecret value showing under New client secret, which is what the application can use to prove its identity when requesting a token:
图 6.9–AAD 为其身份创建新的应用机密
-
Give the application access to Key Vault using an access policy. Search for the key vault you just created and select it:
图 6.10–密钥库搜索
-
In the key vault properties, select Access policies under Settings and click on Add Access Policy:
图 6.11–密钥库访问策略
-
In the Select principal field, search for your application and select the required permissions for your application to access Key Vault, then click on Add, as shown in the following screenshot:
图 6.12–添加访问策略
-
添加策略后,必须保存。此将完成授予您的应用访问密钥库的过程。
我们现在已经授予我们的应用对密钥库的访问权限。在下一节中,我们将看到如何使用 Azure 密钥库配置提供程序从我们的应用访问密钥库。
利用 Azure 密钥库配置提供程序
在本节中,我们将在我们的应用中进行配置和代码更改,以利用 Azure 密钥库配置提供程序并从密钥库中访问机密,如下所示:
图 6.13–开发期间访问密钥库
以下是变更列表:
-
Add the
key vault
name, theAppClientId
value that you noted down from Figure 6.8, and theAppClientSecret
value that you noted down from AAD from Figure 6.9 to theappsettings.json
file in yourTestConfiguration
web API:图 6.14–app settings . JSON 中的密钥存储区部分
-
Install the following NuGet packages:
Microsoft.Azure.KeyVault
Microsoft.Extensions.Configuration.AzureKeyVault
Microsoft.Azure.Services.AppAuthentication
-
更新
Program.cs
以利用 Azure 密钥库配置提供程序来使用您的密钥库。以下代码将添加 Azure 密钥库作为另一个配置源,并使用 Azure 密钥库配置提供程序获取所有配置:public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration((context, config) => { var builtConfig = config.Build(); config.AddAzureKeyVault( $"https://{builtConfig["KeyVault:Name"]}.vault.azure.net/", builtConfig["KeyVault:AppClientId"], builtConfig["KeyVault:AppClientSecret"]); }) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }); }
-
更新
WeatherForecastController.cs
从密钥库中读取秘密如下:[ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { private readonly ILogger<WeatherForecastController> _logger; private readonly IConfiguration _configuration; public WeatherForecastController(ILogger<Weather ForecastController> logger, IConfiguration configuration) { _logger = logger; _configuration = configuration; } [HttpGet] public IEnumerable<string> Get() { return new string[] { "TestKey", _configuration["TestKey"] }; } }
根据此处共享的代码示例包含所有引用。您可以运行应用并查看结果。应用将能够使用 Azure 密钥库配置提供程序访问密钥库并获取机密。这很简单,因为所有的重担都是由.NET 5,我们只需要安装 NuGet 包并添加几行代码。然而,您现在可能会考虑如何将AppClientId
和AppClientSecret
添加到appsettings.json
配置文件中,以及这如何不是非常安全。你百分之百正确。
我们可以通过两种方式解决这个问题:
- 一种选择是将这些值加密存储在
appsettings.json
中;然后可以在代码中读取和解密它们。 - 另一个选项是使用托管身份来访问 Azure 资源,这允许应用使用 AAD 身份验证使用 Azure Key Vault 进行身份验证,而无需凭据(应用 ID 和密码/客户端机密),如现在所示。
任何支持 AAD 身份验证的服务都可以使用您的应用的身份进行身份验证,例如 Azure Key Vault,这将有助于我们从代码中去除凭据:
图 6.15–部署应用后在生产环境中访问密钥库
注意
对于部署到生产环境中的应用,这是我们遵循的最佳实践。在代码中管理凭据是一项常见的挑战,保持凭据的安全是一项重要的安全要求。AAD 中 Azure 资源的托管身份有助于解决这一挑战。托管身份在 AAD 中为 Azure 服务提供自动管理的身份。您可以使用此身份向任何支持 AAD 身份验证的服务进行身份验证,包括密钥库,而无需您代码中的任何凭据。
您可以在这里阅读更多关于托管身份的信息:https://docs . Microsoft . com/en-us/azure/active-directory/managed-identities-azure-resources/overview。
在本节中,我们看到了如何创建密钥库,如何向密钥库添加秘密,如何在 AAD 中注册我们的TestConfiguration
网络应用编程接口,如何创建秘密或身份,如何让TestConfiguration
网络应用编程接口访问密钥库,以及如何使用 Azure 密钥库配置提供程序从我们的代码访问密钥库。您也可以使用 Visual Studio Connected Services 将密钥库添加到您的 web 应用中,如下所述:https://docs . Microsoft . com/en-us/azure/Key-Vault/general/vs-Key-Vault-add-Connected-service:
图 6.16–作为连接服务的 Azure 密钥库
在下一节中,我们将看到如何利用文件配置提供程序。
文件配置提供程序
文件配置提供程序帮助我们从文件系统加载配置。JSON 配置提供程序和 XML 配置提供程序从文件配置提供程序类派生它们的继承,并分别用于从 JSON 文件和 XML 文件中读取键值对配置。让我们看看如何将它们作为CreateHostBuilder
的一部分添加到配置源中。
JSON 配置提供程序
JSON 配置提供程序可以使用以下代码进行配置:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder
CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context,
config) =>
{
config.AddJsonFile("AdditionalConfig.json",
optional: true,
reloadOnChange: true);
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
在这种情况下, JSON 配置提供者将把AdditionalConfig.json
文件和三个参数加载到AddJsonFile
方法中,为我们提供了选项来指定文件名,文件是否可选,以及对文件进行任何更改时是否必须重新加载文件。
以下是AdditionalConfig.json
样本文件:
{ "TestKeyFromAdditionalConfigJSON":"TestValueFromAdditional
ConfigJSON"}
然后,我们更新WeatherForecastController.cs
以从从AdditionalConfig.json
配置文件加载的配置中读取键值对,如下所示:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly ILogger<WeatherForecastController> _logger;
private readonly IConfiguration _configuration;
public WeatherForecastController(ILogger<WeatherForecast
Controller> logger, IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
}
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { " TestKeyFromAdditionalConfigJSON", _configuration["TestKeyFromAdditionalConfigJSON"] };
}
}
您可以运行应用并查看结果。应用将能够访问AdditionalConfig.json
文件并读取配置。在下一节中,我们将看看 XML 配置提供程序。
XML 配置提供程序
我们将为项目添加一个名为AdditionalXMLConfig.xml
的新文件和所需的配置。然后可以使用下面的代码从我们添加的文件中读取来配置 XML 配置提供程序:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
config.AddXmlFile("AdditionalXMLConfig.xml",
optional: true,
reloadOnChange: true);
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
在这种情况下,XML 配置提供者将加载AdditionalXMLConfig.xml
文件,三个参数为我们提供了指定 XML 文件的选项,无论该文件是否可选,以及在进行任何更改时是否必须重新加载该文件。
AdditionalXMLConfig.xml
样本文件如下:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<TestKeyFromAdditionalXMLConfig>TestValueFrom
AdditionalXMLConfig</TestKeyFromAdditionalXMLConfig>
</configuration>
接下来,我们更新WeatherForecastController.cs
以从从AdditionalXMLConfig.xml
加载的配置中读取键值对,如下所示:
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "TestKeyFromAdditionalXMLConfig", _configuration["TestKeyFromAdditionalXMLConfig"] };
}
您可以运行应用并查看结果。应用将能够访问AdditionalXMLConfig.xml
并读取配置。中提供了 JSON 配置文件和 JSON 配置提供程序.NET 5,您不需要 XML 配置文件和 XML 配置提供程序。也就是说,我们刚刚介绍的是那些喜欢 XML 文件、打开和关闭标签等的人。
在下一节中,我们将看到为什么需要自定义配置提供程序,以及如何构建一个。
构建自定义配置提供程序
在前一节中,我们查看了中内置或预先存在的配置提供程序.NET 5。在某些情况下,许多系统在数据库中维护应用配置设置。这些可以由管理员从门户网站进行管理,也可以由支持工程师通过运行数据库脚本来创建/更新/删除应用配置设置。.NET 5 没有内置的从数据库读取配置的提供程序。让我们看看如何通过以下步骤构建自定义配置提供程序来读取数据库:
- 实现配置源:创建配置提供者的实例
- 实现配置提供者:从合适的源加载配置
- 实现配置扩展:将配置源添加到配置生成器中
让我们从配置源开始。
配置来源
配置源的职责是创建配置提供者的实例并返回到源。它需要从IConfigurationSource
接口继承,这就需要我们实现ConfigurationProvider Build(IConfigurationBuilder builder)
方法。
在Build
方法实现内部,我们需要创建一个自定义配置提供程序的实例并返回相同的。还应该有构建构建器所需的参数。在这种情况下,当我们构建自定义的 SQL 配置提供程序时,重要的参数是连接字符串和 SQL 查询。下面的代码片段显示了一个SqlConfigurationSource
类的示例实现:
public class SqlConfigurationSource : IConfigurationSource
{
public string ConnectionString { get; set; }
public string Query { get; set; }
public SqlConfigurationSource(string
connectionString, string query)
{
ConnectionString = connectionString;
Query = query;
}
public IConfigurationProvider
Build(IConfigurationBuilder builder)
{
return new SqlConfigurationProvider(this);
}
}
这个很简单容易实现,你也看到了。您获得构建提供程序和创建该提供程序的新实例所需的参数,然后返回这些参数。让我们在下一节中看看如何构建一个 SQL 配置提供程序。
配置提供者
配置提供者的职责是从适当的源加载所需的配置并返回。它需要从IconfigurationProvider
接口继承,这就需要我们实现Load()
方法。配置提供程序类可以从ConfigurationProvider
基类继承,因为它已经实现了IConfigurationProvider
接口中的所有方法。这将帮助我们节省时间,因为我们不需要实现未使用的方法,而是可以只实现Load
方法。
在Load
方法实现内部,我们需要有从源获取配置数据的逻辑。在这种情况下,我们将执行一个查询来从 SQL 存储中获取数据。下面的代码片段显示了一个SqlConfigurationProvider
类的示例实现:
public class SqlConfigurationProvider : ConfigurationProvider
{
public SqlConfigurationSource Source { get; }
public SqlConfigurationProvider
(SqlConfigurationSource source)
{
Source = source;
}
public override void Load()
{
try
{
// create a connection object
SqlConnection sqlConnection = new
SqlConnection(Source.ConnectionString);
// Create a command object
SqlCommand sqlCommand = new
SqlCommand(Source.Query, sqlConnection);
sqlConnection.Open();
// Call ExecuteReader to return a DataReader
SqlDataReader salDataReader =
sqlCommand.ExecuteReader();
while (salDataReader.Read())
{
Data.Add(salDataReader.GetString(0),
salDataReader.GetString(1));
}
salDataReader.Close();
sqlCommand.Dispose();
sqlConnection.Close();
}
}
}
让我们在下一节中看看如何构建配置扩展。
配置扩展
与其他提供者一样,我们可以使用扩展方法将配置源添加到配置生成器中。
注意
扩展方法是静态方法,使用它可以向现有类添加方法,而无需修改或重新编译原始类。
以下代码片段显示了配置生成器中SqlConfigurationExtensions
类的示例实现:
public static class SqlConfigurationExtensions
{
public static IConfigurationBuilder
AddSql(this IConfigurationBuilder
configuration, string connectionString,
string query)
{
configuration.Add(new
SqlConfigurationSource(connectionString,
query));
return configuration;
}
}
扩展方法将减少应用启动时的代码。
我们可以像为其他配置提供者添加引导代码一样添加引导代码,如以下代码所示:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context,
config) =>
{
config.AddSql("Connection
string","Query");
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
下面的屏幕截图显示了数据库中的一些示例配置设置。您可以在config.AddSql()
中传递适当的连接字符串和 SQL 查询,并从数据库中加载以下配置。SQL 查询可能是一个简单的select
语句来读取所有键值对,如下图所示:
图 6.17–数据库配置设置
如下更新WeatherForecastController.cs
以从从 SQL 配置提供程序加载的配置中读取键值对:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly ILogger<WeatherForecastController>
_logger;
private readonly IConfiguration _configuration;
public WeatherForecastController(ILogger<WeatherForecastController> logger, IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
}
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "TestSqlKey",
_configuration["TestSqlKey"] };
}
}
您可以运行应用并查看结果。应用将能够访问 SQL 配置并读取配置。
这只是一个自定义配置提供程序的示例。您可能能够想到不同的场景,在这些场景中,您将构建其他不同的自定义配置提供程序,例如当从 CSV 文件读取或从 JSON 或 XML 文件读取加密值并解密它们时。
总结
在本章中,我们看到了配置是如何工作的.NET 5,如何向应用提供默认配置,如何以分层顺序添加键值对配置,如何读取配置,如何利用 Azure Key Vault 配置提供程序和文件配置提供程序,以及如何构建自定义配置提供程序以从 SQL 数据库中读取。现在,您已经掌握了根据具体需求在项目中实现不同配置所需的知识。
在下一章中,我们将了解日志记录以及它在中的工作原理.NET 5。
问题
阅读本章后,您应该能够回答以下问题:
-
What takes care of providing the default configuration for an application in .NET 5?
a.
CreateDefaultBuilder
b.
ChainedConfigurationProvider
c.
JsonConfigurationProvider
d.上述全部
-
Which of the following is not correct?
a.Azure 密钥库配置提供程序从 Azure 密钥库中读取配置。
b.文件配置提供程序从 INI、JSON 和 XML 文件中读取配置。
c.命令行配置提供程序从数据库中读取配置。
d.内存配置提供程序从内存集合中读取配置。
-
Which interface is used to access a configuration at runtime and is injected via DI?
a.
IConfig
b.
IConfiguration
c.
IConfigurationSource
d.
IConfigurationProvider
-
Which provider/source is recommended for storing secrets in production?
a.JSON 从
appsettings.json
开始b.
FileConfiguration
来自一个 XML 文件c.
AzureKeyVaultProvider
从AzureKeyVault
开始d.命令行配置提供程序
进一步阅读
- https://docs . Microsoft . com/en-us/aspnet/core/blaz or/基本面/配置?view = aspnetcore-5.0 #:~:text = Configuration % 20 in % 20 app % 20 settings % 20 file,是%20read%20by%20a%20component。&文本=将% 20an % 20i 配置% 20 实例% 20 注入% 20a %组件% 20 到% 20 访问% 20% 20 配置% 20 数据
- https://docs . Microsoft . com/en-us/aspnet/core/基本面/配置/选项?view=aspnetcore-5.0
七、.NET 5 日志
日志记录有助于您在运行时记录应用对不同数据的行为,并且您可以控制想要记录的内容和位置。一旦您的功能开发完成,您将在开发 PC 上对其进行彻底的单元测试,将其部署到测试环境中进行彻底的集成测试,然后将其部署到生产环境中,最后,为大量用户开放它。当您将应用与开发环境进行比较时,您的应用运行的环境(如服务器、数据、负载等)在测试环境和生产环境中是不同的,并且在最初几天,您可能会在测试和生产环境中遇到意想不到的问题。
这就是日志记录在记录运行时端到端流中的不同组件执行它们的功能并相互交互时所发生的事情方面发挥着非常重要的作用。有了可用的日志信息,我们可以调试生产问题,构建非常有用的见解,等等。我们将了解日志记录最佳实践、可用的不同日志记录提供程序,如 Azure App Service 日志记录和 Application Insights 日志记录,并将构建一个可在不同项目中使用的可重用日志记录库。
在本章中,我们将涵盖以下主题:
- 良好测井的特征
- 了解可用的日志记录提供程序
- 使用 Azure 应用服务
- 应用洞察中的实时遥测
- 创建. NET 5 日志类库
在这一章结束时,您将对日志记录有一个很好的了解,并了解一些在进行部署时要应用的 Azure 应用服务和应用洞察的平台级概念。
技术要求
对微软的基本了解.NET 和 Azure 是必需的。
章节的代码可以在这里找到:https://github . com/PacktPublishing/Enterprise-Application-Development-with-C-Sharp-9-and-.NET-5/树/主/第 07 章
良好测井的特征
这个问题你看过多少次了?虽然实现了日志记录,但是日志中的信息对于构建见解或调试生产问题并不有用。这就是最佳实践的来源,以便在应用中实现良好的日志记录。良好测井的一些特征如下:
- 它不应该影响实际的应用性能。
- 应该准确完整。
- 应该利用它进行数据分析和了解应用使用情况,例如并发用户、峰值负载时间、使用最多/最少的功能等。
- 它应该有助于我们重现报告的问题以进行根本原因分析,并最大限度地减少“无法重现”的情况。
- 它应该是分布式的,每个人都可以轻松访问,包括开发人员、产品所有者和支持人员。
- 它不应包含受保护或敏感信息、个人身份信息 ( PII )或重复或不必要的日志。
除此之外,它还应该捕获以下一些关键信息:
- 关联标识:可用于在日志存储中搜索的问题的唯一标识符。
- 日志级别:信息、警告、错误等。
- 时间戳:日志条目的时间(始终使用一种约定的标准格式,如 UTC 或服务器时间,不要两者兼而有之)
- 用户信息:用户 ID、角色等。
- 消息:要记录的消息。这可能是信息或自定义错误消息、实际异常消息或自定义和实际错误消息的组合。
- 机器/服务器/实例名称:负载均衡器中可能有多台服务器。这将有助于我们找到日志发生的服务器。
- 组件:日志发生的组件名称。
你想录什么?这是图中出现对数级引导的地方:
表 7.1
日志级别是可配置的,基于指定的级别;它将从该指定级别启用到所有更高的级别。例如,如果您在配置中将日志级别指定为信息,则来自信息、警告、错误、致命的所有日志消息都将被记录,而调试和跟踪消息将不会被记录,如下表所示。如果没有指定日志级别,日志默认为信息级别:
表 7.2
你想在哪里录音?这就是日志提供者进入画面的地方。让我们在下一节看看它们。
了解可用的日志记录提供程序
.NET 5 支持多个内置日志提供者以及多个第三方日志提供者。这些提供程序公开的 API 有助于将日志输出写入不同的源,例如提供程序支持的文件或事件日志。您的代码还可以启用多个提供程序,这在您从一个提供程序转移到另一个提供程序时是非常常见的情况,在这种情况下,您可以保留旧的提供程序,监视新的提供程序,一旦您做好了,就可以停用旧的提供程序。让我们详细讨论这两种类型的提供者。
内置日志记录提供程序
所有内置日志提供程序都在Microsoft.Extensions.Logging
命名空间中支持。让我们看看其中的一些:
表 7.3
第三方日志记录提供商
而.NET 5 提供了多个强大的内置日志提供者,它还支持第三方日志提供者。我们来看看他们:
表 7.4
在简单了解了多个内置和第三方提供商之后,让我们在下一部分深入了解一下 Azure 应用服务和应用洞察。
使用 Azure 应用服务
在基础设施即服务 ( IaaS )托管模型中,您可以完全控制安装在机器上的操作系统和软件。这与我们许多人习惯的内部部署非常相似。您可以通过远程桌面访问服务器,浏览 IIS 日志、窗口事件查看器或文件等。当您转移到平台即服务 ( PaaS )托管模型时,Azure 完全负责管理实例。这有助于节省大量时间,因为您的工程师不必花费时间来管理服务器以保持操作系统、基础架构和安全更新的最新状态。
在本节中,我们将了解在 Azure 应用服务计划(微软的 PaaS 产品之一)中部署应用时,如何进行大量的日志记录和监控。
在 Azure 应用服务中启用应用日志
要启用应用日志记录,您需要执行以下步骤:
-
Install the
AzureAppServices
package in any of your existing .NET 5 projects using thedotnet add <.csproj> package <Nuget package> -v <Version number>
command, as shown:图 7.1–从命令行界面安装软件包
您可以获得更多关于的详细信息。来自https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet的命令行界面命令。
您也可以右键单击项目中的依赖项,并选择管理 NuGet 包。搜索找到
Microsoft.Extensions.Logging.AzureAppServices
包并安装,如下图截图所示:图 7.2–从集成开发环境安装软件包
-
配置记录器:在你的
Program.cs
文件中.NET 5 app,在CreateHostBuilder
方法中添加以下高亮显示的代码。CreateHostBuilder
为我们正在开发的应用进行默认配置。让我们在这里添加日志配置,其中还将动态注入_logger
(使用依赖注入 ( DI )发生对象创建,如中 第 5 章 、依赖注入所述.NET ):public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureLogging(logging => logging.AddAzureWebAppDiagnostics()) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
-
添加日志:在核心逻辑所在的任何控制器中,将以下日志代码添加到您的方法中,以测试日志是否工作:
{ //Removed code for brevity _logger.LogInformation("Logging Information for testing"); _logger.LogWarning("Logging Warning for testing"); _logger.LogError("Logging Error for testing"); //Removed code for brevity }
-
发布你的应用:将你的应用服务发布到名为
TestAppServiceForLoggingDemo
的 Azure 资源组。关于如何发布的更多信息,请参考https://docs . Microsoft . com/en-us/visualstudio/deployment/quick start-deploy-to azure?view=vs-2019 。 -
Enable logging: Go to the Azure portal | Your subscription | Resource Group | App service where it is deployed, and select App Service logs under Monitoring. You can see different logging options, as shown:
图 7.3–应用服务日志默认状态
所有日志选项默认关闭,如上一张截图所示。让我们看看这些选项是什么:
- 应用日志记录(文件系统):将日志消息从应用写入网络服务器上的本地文件系统。一旦您打开它,它将被启用 12 小时,之后将被自动禁用。因此,此选项用于临时调试目的。
- 应用日志记录(Blob) :将日志消息从应用写入 Blob 存储,以便在配置的保留期内保留日志。登录 blobs 是为了长期调试。您需要一个 Blob 存储容器来写入日志。您可以在这里阅读更多关于 Blob 存储容器的信息:https://docs . Microsoft . com/en-us/azure/storage/Blobs/storage-Blobs-简介。一旦您在上选择,您将获得创建新存储帐户或搜索现有存储帐户的选项,您可以在其中写入日志。点击 +存储账号,指定名称新建账号,如下图截图所示:
图 7.4–存储帐户配置
- 网络服务器日志记录 : IIS 登录提供诊断信息的服务器,如 HTTP 方法、资源 URI、客户端 IP、客户端端口、用户代理和响应代码。您可以将日志存储在 Blob 存储区或文件系统中。在保留期(天数)中,您可以配置日志应该保留的天数。
- 详细错误消息:服务器传来的 HTTP 400 响应的详细错误消息,可以帮助你判断服务器为什么会返回这个错误。出于安全原因,我们不会在生产中向客户端发送详细的错误页面,但是应用服务可以在每次出现 HTTP 代码为 400 或更高的应用错误时将此错误保存在文件系统中。
- 失败请求跟踪:失败请求的详细信息,包括 IIS 跟踪等。对于每个失败的请求,都会生成一个包含 XML 日志文件和 XSL 样式表的文件夹来查看日志文件。
以下屏幕截图显示了当您打开并启用所有日志选项时的外观:
图 7.5–应用服务日志已启用
- 您可以通过浏览应用服务上托管的网站并导航到登录控制器的页面,从我们启用的任何日志选项中验证日志。例如,让我们通过访问 Blob 存储来检查应用日志记录(Blob) ,在该存储中我们配置了一个日志记录选项,如下图所示:
图 7.6–Blob 存储中的应用服务日志
您还可以从我们添加测试日志的控制器中实时查看日志流中的日志,方法是导航至监控下的日志流:
图 7.7–日志流中的应用服务日志
我们看到了如何在 Azure 应用服务中启用不同的日志,并在应用日志记录(Blob) 和日志流中验证了日志。在下一节中,我们将了解如何监控和发出警报。
使用指标进行监控
您可以使用 Azure Monitor 中的指标来监控您的应用服务计划和应用服务。
导航到您的应用服务计划并查看概述,如下图所示。您可以看到 CPU、内存、数据输入、数据输出等的标准图表:
图 7.8–应用服务计划概述
现在,点击任一图表,例如中央处理器百分比图表。您将获得如下截图所示的视图(默认持续时间为 1 小时):
图 7.9–应用服务指标概述
我在上一张截图所示的图表中强调了三个重要部分。让我们讨论一下:
- 当地时间:点击当地时间,会出现如下截图所示的选项。您可以更改此图表应该代表的时间范围的值:
图 7.10–应用服务指标时间范围
- 添加度量:点击添加度量,会得到如下截图所示的选项。您可以选择希望图表显示的指标:
图 7.11–应用服务–添加指标
- 锁定仪表盘:可以点击锁定仪表盘将图表添加到仪表盘,这样在登录 Azure 门户时就可以看到更新。
当您点击左侧门户菜单时,您可以看到仪表盘,您可以点击该菜单查看所有固定仪表盘:
图 7.12–仪表板的左侧门户菜单选项
您也可以在门户菜单| 所有服务中搜索或前往搜索资源、服务和文档并导航至监视器,您将进入以下屏幕:
图 7.13–Azure 显示器概述
从门户菜单中选择指标刀片,它将显示以下选项,您可以在其中为订阅下的任何资源确定指标范围:
图 7.14–Azure 监控指标
我们可以在中选择 Azure 服务计划选择一个我们一直用于日志演示的范围,选择您想要监控的任何指标(如下图所示):
图 7.15–选择了范围的 Azure 监视器指标
基本上,这与我们在图 7.8 中看到的应用服务计划概述指标中 Monitor 提供的指标数据相同。在下一节中,我们将添加警报。
使用指标发出警报
要添加警报,单击左侧菜单中的警报刀片,您将看到以下屏幕:
图 7.16–Azure 监视器警报
点击 +新的提醒规则,您需要完成以下四个步骤来创建提醒:
- 范围:选择您想要监控的目标资源。
- 条件:通过选择一个信号并定义其逻辑,配置警报规则何时触发。
- 动作组:动作组是通知或动作的集合。每个操作都由以下属性组成:
- 名称:动作组内的唯一标识符
- 动作类型:执行的动作–示例包括发送语音呼叫、短信或电子邮件,或者触发各种类型的自动动作
- 细节:对应的细节,因动作类型而异
- 预警规则详细信息:提供您的预警规则的详细信息,以便您以后识别和管理。
我已经选择了应用服务,并完成了前面的步骤,如下图所示。您可以对您的应用服务计划以及任何 Azure 资源进行同样的操作:
图 7.17–配置的 Azure 监视器警报
当平均响应时间超过 5 秒时,您将收到电子邮件警报。这只是一个示例警报。您可以查看所有指标和警报,并根据应用和客户端的需求进行配置。
当您考虑 Azure 应用服务时,Azure 应用服务日志以及 Azure 监视器指标和警报是一个很好的选择。在下一节中,我们将看看 Azure Application Insights 提供商,这是我最喜欢的遥测技术之一,现在每个人都大量使用。
Azure 应用洞察中的实时遥测
Application Insights 是微软 Azure 为开发人员和 DevOps 专业人员提供的最佳遥测产品之一,作为可扩展的“T2”应用性能管理“T3”(“T4”APM“T5”)服务,可实现以下功能:
- 监控您的实时应用。
- 自动检测性能异常。
- 包括强大的分析工具来帮助您诊断问题。
- 了解用户如何使用您的应用。
- 帮助您不断提高性能和可用性。
有了 Application Insights,我觉得不需要任何其他日志记录提供程序,因为它非常强大,可以满足所有目的。我们将在企业应用代码以及遥测中大量使用这一点。
Microsoft.Extensions.Logging.ApplicationInsights
作为Microsoft.ApplicationInsights.AspNetCore
的从属关系包含在内。Microsoft.ApplicationInsights.AspNetCore
包用于 ASP.NET 芯遥测应用,使用时不需要安装Microsoft.Extensions.Logging.ApplicationInsights
。
如下图所示,您可以在应用中安装此软件包,以启用和写入遥测:
图 7.18–遥测应用洞察仪器
注意
对你的 app 性能没有影响。对 Application Insights 的调用是非阻塞的,分批在不同的线程中发送。
在应用洞察中启用应用日志记录
使用应用洞察时,启用应用日志的步骤如下:
-
Installing the
ApplicationInsights
package: Install theMicrosoft.ApplicationInsights.AspNetCore
package from Tools | NuGet Package Manager | Package Manager Console as shown, using this command:Install-Package <Package name> -version <Version number>
:图 7.19 -从软件包管理器控制台安装软件包
-
应用设置配置:安装软件包后,您需要添加一个遥测部分,在
appsettings.json
中更新您的 Azure 应用洞察资源的仪器键 ( GUID ),以便所有遥测数据都写入到您的 Azure 应用洞察资源的中。如果您没有 Azure 应用洞察资源,请创建一个,然后将其添加到appsettings.json
:{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "Telemetry": { "InstrumentationKey": "Your AppInsights Instrumentation Key " }
-
启用
ApplicationInsights
遥测:在您的Startup
类的ConfigureServices()
方法中,添加高亮显示的代码:// This method gets called by the runtime. Use this //method to add services to the container. public void ConfigureServices(IServiceCollection services) { // The following line enables Application //Insights telemetry collection. services.AddApplicationInsightsTelemetry(); services.AddControllers(); }
现在,您可以构建和运行应用了。开箱后,你会得到很多遥测数据。
导航到应用洞察 | 概述,您可以看到任何失败的请求、服务器响应时间和服务器请求,如下图所示:
图 7.20–应用洞察概述
您可以导航至应用洞察 | 实时指标获取实时性能计数器,如下图所示:
图 7.21 -应用洞察实时指标
您可以导航至应用洞察 | 指标获取不同的指标和图表,如下图中的:
图 7.22–应用洞察指标
可以导航到应用洞察 | 性能分析操作时长、依赖响应时间等,如下图所示:
图 7.23–应用洞察性能
您可以导航至应用洞察 | 失败并分析操作、失败的请求、失败的依赖项、前三个响应代码、异常类型和依赖项失败,如下图所示:
图 7.24–应用洞察失败
添加监控和警报的步骤与我们在使用 Azure 应用服务部分所做的相同。您可以导航至应用洞察 | 提醒并开始添加提醒,如下图所示:
图 7.25–应用洞察警报
我们已经看到了开箱即用的遥测和警报。我们如何为信息、错误或警告添加日志?您可以使用记录器(对象创建使用 DI 进行,这在第 5 章依赖注入中有介绍.NET )。DI 由第二个(应用设置配置)和第三个(启用应用洞察遥测)步骤启用,我们在前面的一组步骤中看到了这些步骤来启用应用洞察的登录。出于测试目的,为了查看它是否工作,您可以将以下代码添加到控制器中并运行应用:
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
//Removed code for brevity
_logger.LogWarning("Logging Warning for
testing");
_logger.LogError("Logging Error for testing");
//Removed code for brevity
您可以导航到应用洞察 | 日志并检查跟踪,在这里您可以看到已记录的警告和错误,如下图所示:
图 7.26–应用洞察日志
Application Insights 使用起来非常简单,是一个非常强大的日志提供程序。我们看到了它开箱即用的丰富遥测技术,并添加了我们自己的日志。在下一节中,我们将开发一个自定义日志类库。中提供的默认记录器.NET 5 对于您的应用遥测来说已经足够了。如果您需要在中默认提供的基础上记录自定义指标和事件.NET 5,您可以利用下面的自定义记录器库。
创建. NET 5 日志类库
我们将创建一个类库(DLL),它将支持 Application Insights 日志记录,如果需要,可以扩展到支持其他来源的日志记录。为此,请执行以下步骤:
-
创建新的。名为
Logger
的. NET 5 类库。 -
安装
Microsoft.ApplicationInsights
包。 -
创建一个名为
ICustomLogger.cs
的新类,并添加以下代码:using System; using System.Collections.Generic; namespace Logger { public interface ICustomLogger { void Dependency(string dependencyTypeName, string dependencyName, string data, DateTimeOffset startTime, TimeSpan duration, bool success); void Error(string message, IDictionary<string, string> properties = null); void Event(string eventName, IDictionary<string, string> properties = null, IDictionary<string, double> metrics = null); void Metric(string name, long value, IDictionary<string, string> properties = null); void Exception(Exception exception, IDictionary<string, string> properties = null); void Information(string message, IDictionary<string, string> properties = null); void Request(string name, DateTimeOffset startTime, TimeSpan duration, string responseCode, bool success); void Verbose(string message, IDictionary<string, string> properties = null); void Warning(string message, IDictionary<string, string> properties = null); } }
-
Create a new class called
AiLogger.cs
and add the following code:using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.DataContracts; using System; using System.Collections.Generic; namespace Logger { public class AiLogger : ICustomLogger { private TelemetryClient client; public AiLogger(TelemetryClient client) { if (client is null) { throw new ArgumentNullException(nameof(client)); } this.client = client; } public void Dependency(string dependencyTypeName, string dependencyName, string data, DateTimeOffset startTime, TimeSpan duration, bool success) { this.client.TrackDependency (dependencyTypeName, dependencyName, data, startTime, duration, success); } public void Warning(string message, IDictionary<string, string> properties = null) { this.client.TrackTrace(message, SeverityLevel.Warning, properties); } public void Error(string message, IDictionary<string, string> properties = null) { this.client.TrackTrace(message, SeverityLevel.Error, properties); } public void Event(string eventName, IDictionary<string, string> properties = null, IDictionary<string, double> metrics = null) { this.client.TrackEvent(eventName, properties, metrics); } public void Metric(string name, long value, IDictionary<string, string> properties = null) { this.client.TrackMetric(name, value, properties); } public void Exception(Exception exception, IDictionary<string, string> properties = null) { this.client.TrackException(exception, properties); } public void Information(string message, IDictionary<string, string> properties = null) { this.client.TrackTrace(message, SeverityLevel.Information, properties); } public void Request(string name, DateTimeOffset startTime, TimeSpan duration, string responseCode, bool success) { this.client.TrackRequest(name, startTime, duration, responseCode, success); } public void Verbose(string message, IDictionary<string, string> properties = null) { this.client.TrackTrace(message, SeverityLevel.Verbose, properties); } } }
AiLogger
使用TelemetryClient
类,该类向 Azure 应用洞察发送遥测数据。 -
Build the library, and our custom .NET 5 logger is ready to consume events in your project.
在接下来的章节中,我们将使用日志库作为我们企业应用开发的一部分。在本章提供的示例中,您可以看到我们是如何在
LoggerDemoService
项目中动态注入这个自定义记录器的。
总结
在本章中,我们了解了良好日志记录的特征、可用的不同日志记录提供程序(如 Azure App Service 日志记录提供程序和 Azure Application Insights 日志记录提供程序),以及如何创建可重用的日志记录库。
现在,您已经掌握了有关日志记录的必要知识,这将有助于您在项目中实现可重用的日志记录程序或扩展当前日志记录程序,使其具有正确的日志记录级别和关键信息,以调试问题并对生产数据进行分析。
在下一章中,我们将学习各种缓存数据的技术.NET 5 应用,以及各种缓存组件和可与. NET 应用集成的可用平台。
问题
-
Which logs highlight when the current flow of execution has stopped due to a failure? These should indicate a failure in the current activity, not an application-wide failure.
a.警告
b.错误
c.批评的
d.信息
-
What can be leveraged by applications and components running anywhere, on Azure, AWS, your own on-premises servers, a mobile platform, and so on, for logging?
a.应用洞察
b.蔚蓝应用服务
c .事件日志
d.serial log(串行日志)
-
What are the logging options available in Azure App Service?
a.应用日志(文件系统)和应用日志(Blob)
b.网络服务器记录和详细错误信息
c.请求跟踪失败
d.上述全部
-
Application Insights is an extensible APM service to do which of the following?
a.监控您的实时应用。
b.自动检测性能异常。
c.包括强大的分析工具来帮助您诊断问题。
d.以上所有。
八、理解缓存
缓存是关键的系统设计模式之一,它有助于扩展任何企业应用并提高响应时间。任何 web 应用通常都需要从数据存储中读写数据,数据存储通常是关系数据库,如 SQL Server 或 NoSQL 数据库,如宇宙数据库。然而,为每个请求从数据库中读取数据并不高效,尤其是当数据没有变化时,因为数据库通常将数据保存到磁盘上,并且从磁盘加载数据并将其发送回浏览器客户端(或者移动/桌面应用中的设备)或用户是一项昂贵的操作。这就是缓存发挥作用的地方。缓存存储可以用作检索数据的主要来源,并且仅当数据在缓存中不可用时才返回到原始数据存储,从而为消费应用提供更快的响应。在这样做的同时,我们还需要确保当原始数据存储中的数据更新时,缓存的数据会过期/刷新。
在本章中,我们将学习各种缓存数据的技术.NET 5 应用以及各种缓存组件和可与. NET 5 应用集成的可用平台。我们将涵盖以下主题:
- 缓存介绍
- 了解缓存的组成部分
- 缓存平台
- 使用分布式缓存设计缓存抽象层
技术要求
对…的基本了解.NET Core、C#、Azure 和.NET 命令行界面是必需的。
章节的代码可以在这里找到:https://github . com/PacktPublishing/Enterprise-Application-Development-with-C-Sharp-9-and-.NET-5/树/主/第 08 章
代码示例的说明可以在这里找到:https://github . com/PacktPublishing/Enterprise-Application-Development-with-C-Sharp-9-and-.NET-5/树/主/企业% 20 应用
缓存介绍
有多种方法可以提高应用的性能,缓存是企业应用中使用的关键技术之一。缓存就像一个临时数据存储,大小和数据都有限,但与原始数据源相比,数据访问速度要快得多,并且通常只保存最常用数据的子集。缓存存储可以像进程在执行过程中使用的计算机内存一样简单,也可以像 Redis 一样使用内存和磁盘来存储数据。这里的关键是,与原始存储层相比,它通常位于访问时间较短的硬件上。
缓存可以在架构中的每一层实现,以便可以从最接近用户的层检索数据。例如,在任何网络应用中,当在浏览器中键入网址并按下进入时,它会通过加载网络应用所涉及的各种网络组件,从浏览器、代理、域名系统、网络服务器和数据库开始。缓存是可以应用于所有这些层的东西。如果数据缓存在浏览器中,可以立即加载。或者,如果数据在最接近用户的层中不可用,它可以退回到更高层,从而减少在多个用户之间共享的更高层上的负载,例如应用服务器和数据库层。下图从高层次描述了这一讨论,其中请求流过不同的层,并且仅当数据在缓存中不可用(由虚线表示)时才被移动到更高的层:
图 8.1–请求流中的缓存层
让我们讨论一下应用架构中可以缓存数据的一些层。
客户端缓存
常用的请求数据可以缓存在客户端,避免不必要的往返服务器。例如,微软的 Outlook 应用从服务器下载最新的电子邮件,并在客户端保存它们的副本,然后定期同步新邮件。如果需要搜索尚未下载的电子邮件,它会返回到服务器。
类似地,浏览器可以缓存各种资源和基于特定头的 web 应用的响应,以及来自浏览器缓存的相同资源负载的后续请求。例如,所有的 JavaScript 文件、图像文件和 CSS 通常都会在浏览器上缓存一段时间。此外,可以通过发送适当的响应头来缓存来自应用编程接口的响应。这就是也叫 HTTP 缓存或响应缓存,这是后面会详细讨论的。
内容交付网络(CDN)
一个内容交付网络 ( CDN )是一组服务器,全球分布,通常用于服务静态内容,如 HTML、CSS 和视频。每当应用请求资源时,如果启用了 CDN,系统将首先从物理上最接近用户的 CDN 服务器加载资源。但是,如果资源在 CDN 服务器中不可用,则会从服务器中检索该资源,并缓存在 CDN 中以服务于后续请求。网飞就是一个很好的例子,它严重依赖定制的内容分发网络向用户提供内容。
微软还附带了 Azure CDN,主要可以用来服务静态内容。此外,微软的 CDN 提供了一个与 Azure Storage 集成的选项,我们将在电子商务应用中使用它来提供各种产品图像。
网络服务器缓存
尽管 cdn 对于静态内容来说非常棒,但是它们在刷新应用服务器数据方面会带来额外的成本和维护开销。为了克服这些限制,应用可以使用 web 服务器或反向代理来提供静态内容。轻量级 NGINX 服务器就是一个这样的例子,可以用来提供静态内容。
网络服务器也可以缓存动态内容,例如来自应用服务器的应用编程接口响应,就像静态文件一样,当配置为反向代理时,NGINX 可以进一步用于缓存动态内容,从而通过从应用服务器的缓存中提供请求来减轻应用服务器的负载。
注意
NGINX 是一个开放的源码解决方案,主要以其 web 服务器能力而闻名;但是,它也可以作为反向代理,用于负载均衡等等。详情请参考https://www.nginx.com/。
数据库缓存
更常见的是,数据库服务器也可以缓存查询的某些组件;例如,SQL Server 通常有缓存执行计划,也有要缓存的数据缓冲区,MongoDB 将最近查询的数据保存在内存中,以便更快地检索。因此,调整这些设置以提高应用的性能是很好的。
请注意,数据库缓存并不能保证同一查询的后续执行在 CPU 消耗为零的情况下执行;也就是说,它实际上不是免费的。后续请求中的相同查询会以更快的速度执行。
应用缓存
应用缓存可以通过缓存从应用服务器内的存储层检索的数据来实现。这主要通过以下两种方式完成:
- 存储在应用服务器的内存中,也称为内存缓存
- 存储在外部存储中,如 Redis 或 Memcached,与底层原始数据存储相比,访问时间更快
应用缓存通常包括在应用逻辑中集成额外的代码来缓存数据。因此,每当发出数据请求时,应用都会首先查看缓存。但是如果它在缓存中不可用,应用将返回到原始数据存储,如数据库。通常,应用缓存的大小相对于原始数据存储是有限的,所以在内部,应用缓存平台会采用各种算法,如最近最少使用的 ( LRU )或最少使用的 ( LFU )来清理缓存中存储的数据。我们将在缓存平台部分讨论更多这样的缓存平台。
应用缓存需要考虑的另一个要点是数据失效,即数据需要多久过期一次或者与原始数据源同步一次。因此,需要考虑诸如缓存过期和用原始数据存储更新缓存的各种策略(读、写)。我们将在缓存访问模式部分讨论更多缓存失效/刷新策略。
了解缓存的组件
在我们了解中可用的各种可能的缓存存储/平台之前.NET 5 应用,我们需要了解中可用的缓存的各种组件.NET 5 以及如何在企业应用中使用它们。在此过程中,我们还将介绍各种缓存回收策略和技术,以保持缓存与原始数据存储同步。
响应缓存
响应缓存是一种由 HTTP 支持的缓存技术,用于在客户端(例如浏览器)或中间代理服务器上缓存对使用 HTTP 或 HTTPS 做出的请求的响应。从实现的角度来看,这是通过在请求和响应中为Cache-Control
头设置适当的值来控制的。典型的Cache-Control
标题如下:
Cache-Control:public,max-age=10
在这种情况下,如果响应中存在标头,服务器会告诉客户端/代理(公共)客户端可以缓存响应 10 秒(max-age=10
)。但是,客户端仍然可以覆盖它,并在更短的时间内缓存它;也就是说,如果请求和响应都设置了缓存头,缓存持续时间将是两者中的最小值。
与max-age
一起,根据 HTTP 规范(https://tools . IETF . org/html/RFC 7234 # section-5.2),Cache-Control
可以额外保存以下值:
- 公共:响应可以缓存在任何地方——客户端/服务器/中间代理服务器。
- Private :响应可以为特定用户存储,但不能存储在共享缓存服务器中;例如,它可以存储在客户端浏览器或应用服务器中。
- 无缓存:响应无法缓存。
在响应缓存中起作用的其他标头如下:
- 年龄:这是一个响应头,指示对象在缓存(代理/浏览器)中存在的持续时间。接受的值是一个整数,以秒为单位表示持续时间。
- variable:这是响应头,当收到时,告知客户端请求头基于哪些响应被缓存。该头的值是请求头之一,并且基于该头的值,客户端可以决定是使用缓存的响应还是从服务器下载数据。例如,如果
Vary
设置为user-agent
值,响应将按照user-agent
进行唯一缓存。
以下屏幕截图显示了与 Postman 中示例请求的缓存相关的响应头:
图 8.2–带有缓存控制和可变标头的示例响应
下面的顺序图显示了一个使用 ASP.NET Core 5 构建的示例应用编程接口,该接口启用了响应缓存中间件:
图 8.3–响应缓存序列图
在创建新的 ASP.NET Core 5 MVC/Web API 应用后,或者对于现有的 ASP.NET Core 5 MVC/Web API 应用,要配置响应缓存,需要进行以下代码更改:
-
在
Startup.cs
的ConfigureServices
方法中增加services.AddResponseCaching()
。这个扩展方法是Microsoft.Extensions.DependencyInjection
命名空间的一部分。这个服务扩展方法映射所需的接口和类。 -
使用
Startup.cs
的Configure
方法中的app.UseResponseCaching()
添加所需的中间件。这个中间件是Microsoft.AspNetCore.Builder
命名空间的一部分,保存缓存数据所需的逻辑。确保在app.UseEndpoints
之前注入该中间件。 -
Handle the response to set cache headers either through custom middleware or using the
ResponseCache
attribute.注意
使用 CORS 中间件必须在
UseResponseCaching
之前调用UseCors
。关于此次订购的更多信息,请参考https://github.com/dotnet/AspNetCore.docs/blob/master/aspnet core/基本面/中间件/index.md 。
ResponseCache
属性可以用于整个控制器或控制器中的特定方法,它接受以下关键属性:
Duration
:设置响应头中max-age
值的数值ResponseCacheLocation
:枚举取三个值–Any
、Client
和None
,并将Cache-Control
表头进一步设置为public
、private
或no-store
VaryByHeader
:控制缓存行为的字符串,根据特定的头进行缓存VaryByQueryKeys
:字符串数组,接受基于其缓存数据的键值
具有ResponseCache
属性的典型方法如下所示:
[HttpGet]
[ResponseCache(Duration = 500, VaryByHeader = "user-agent", Location = ResponseCacheLocation.Any, VaryByQueryKeys = new[] { "Id" })]
public async Task<IActionResult> Get([FromQuery]int Id = 0)
该方法将基于唯一的user-agent
头和Id
值缓存 500 秒。如果这些值中的任何一个发生了变化,服务器就会发出响应,否则,缓存中间件就会发出响应。
正如你在这里看到的,我们需要给每个控制器/方法加上ResponseCache
属性的前缀。因此,如果应用有许多控制器/方法,这可能是一个维护开销,因为要对数据缓存方式进行任何更改(如更改Duration
值),我们需要在控制器/方法级别应用更改,这就是缓存配置文件发挥作用的地方。因此,我们可以将它们分组并在Startup.cs
中给它们命名,而不是单独设置属性,该名称可以在ResponseCache
属性中使用。因此,对于前面的属性,我们可以通过添加在Startup.cs
中显示的代码来创建缓存配置文件:
services.AddControllers(options =>
{
options.CacheProfiles.Add("Default", new CacheProfile {
Duration = 500,
VaryByHeader = "user-agent",
Location = ResponseCacheLocation.Any,
VaryByQueryKeys = new[] { "Id" } });
});
在控制器上,使用CacheProfileName
调用该缓存配置文件:
[ResponseCache(CacheProfileName = "Default")]
对于 MVC 应用,可以在services.AddControllersWithViews()
中配置CacheProfile
。
分布式缓存
我们知道,在分布式系统中,数据存储被分割到多个服务器上;同样,分布式缓存是传统缓存的扩展,在传统缓存中,缓存数据存储在网络中的多个服务器上。在我们进入分布式缓存之前,这里简单回顾一下 cap 定理:
- C :代表一致性,意思是数据在所有节点上都是一致的,并且有相同的数据拷贝
- A :代表可用性,表示系统可用,一个节点故障不会导致系统停机
- P :代表分区容忍,意思是即使节点之间的通信中断,系统也不会中断
根据 CAP 定理,任何分布式系统只能实现上述两个原则,由于分布式系统必须是分区容忍的(P),我们只能实现数据的一致性(C)或数据的高可用性(A)。
因此,分布式缓存是一种缓存策略,其中数据存储在应用服务器之外的多个服务器/节点/碎片中。由于数据分布在多台服务器上,如果一台服务器发生故障,另一台服务器可以作为备份来检索数据。例如,如果我们的系统想要缓存国家、州和城市,并且如果分布式缓存系统中有三个缓存服务器,假设有一个缓存服务器缓存国家,另一个缓存州,还有一个缓存城市(当然,在实时应用中,数据以更复杂的方式分割)。此外,每个服务器还将充当一个或多个实体的备份。因此,在高级别上,一种类型的分布式缓存系统如下所示:
图 8.4–分布式缓存高级表示
如您所见,在读取数据时,数据是从主服务器读取的,如果主服务器不可用,缓存系统将退回到辅助服务器。类似地,对于写操作,直到数据被写入主服务器以及作为辅助服务器的时,写操作才完成,并且在该操作完成之前,读操作可能被阻止,因此危及系统的可用性。另一种写入策略可能是后台同步,这将导致最终的数据一致性,因此在同步完成之前会影响数据的一致性。回到 CAP 定理,大多数分布式缓存系统都属于 CP 或 AP 的范畴。
以下是一些与集成的分布式缓存提供程序.NET 5 应用:
- 重定向高速缓存
- Memcached
- 美洲狮号
- 数据库
- ncache!ncache
这可以进一步扩展到任何集群编排平台,例如 Terracotta,它负责管理各种节点,并可以将数据分发到所有节点。
尽管分布式缓存有很多好处,但与单服务器缓存或进程内缓存相比,分布式缓存的一个可能的缺点可能是由于可能的额外跳转和序列化/反序列化而引入的延迟。因此,如果应用严重依赖缓存数据,设计可以考虑将内存缓存和分布式缓存相结合。然而,大多数场景都是通过集成一个实现良好的分布式缓存系统(如 Redis)来实现的,我们将在本章的后面介绍这个系统。
缓存访问模式
一旦对象数据被缓存,就需要有一个适当的设计来刷新缓存。可以实现多种缓存访问模式来处理这种情况。一些关键模式如下:
- 一旁缓存模式
- 通读/直写
- 提前刷新
- 写后
让我们详细讨论每一个。
一旁缓存模式
顾名思义,在缓存备用模式中,缓存存储与数据存储放在一起。在这种模式下,应用代码检查缓存存储中的数据可用性。如果在缓存存储中不可用,则从底层数据存储中检索数据,并在缓存中更新数据。后续请求将再次查询缓存中的数据,如果数据在缓存中可用,将从缓存中提供。缓存备用模式依赖于 第 4 章线程和异步操作中讨论的延迟加载概念,并在第一次访问数据时填充;对同一实体的后续请求将从缓存中加载。
在更新原始数据存储中的数据时,应该处理缓存存储中数据的过期,然后后续读取会再次将更新的数据添加到缓存中。
以下是该模式的优势:
- 与下一节中介绍的读/写模式相比,简化了实现。由于缓存不是应用中的主要数据源,因此我们不需要额外的类来同步缓存存储和数据存储。
- 因为它依赖于延迟加载原则,所以只有当任何数据至少被访问一次时,才会填充缓存。
然而,有几个缺点与这个模式相关联:
- 这会导致大量缓存未命中的可能性。缓存未命中应该总是最小的,因为由于额外的跳转,它们会在应用中引入延迟。
- 如果在数据更新期间缓存过期未命中,可能会导致缓存中的数据过时。如果数据由后台/外部进程更新,而该进程没有缓存系统的信息,就会出现这种情况。
缓解过期问题的一种方法是为每个实体设置生存时间 ( TTL ),以便对象在一定时间后自动过期。但是,在监控数据刷新率后,需要仔细评估 TTL 持续时间。在缓存备用模式的情况下,另一个常见的做法是在应用启动期间预填充缓存存储,因为这有助于减少缓存未命中的数量。大多数企业应用通常使用缓存备用模式实现缓存层,并用主数据而不是事务数据预填充它。
通读/直写
在读/写通过中,应用直接从缓存存储中读取/写入数据;也就是说,应用将其用作主存储,缓存层负责将数据加载到缓存中,并将缓存存储中的任何更新写回原始存储。
当应用想要读取一个实体时,它会直接从缓存存储中请求它。如果该实体在缓存中可用,则返回响应;但是,如果它不在缓存中,缓存层会从原始数据存储中请求它,原始数据存储会在缓存中更新以备将来使用,然后从缓存层返回响应。
更新实体时,会发生以下步骤:
- 它首先在缓存中更新。
- 缓存层将其写回原始数据存储。
这种系统的主要优点如下:
- 原始数据存储(通常是数据库)上的负载显著减少,因为在大多数企业应用中,除了从缓存层到数据存储的调用之外,所有调用都将从缓存中提供。
- 简化了应用代码,因为它只与一个存储区交互,这与缓存备用模式不同,后者与缓存存储区以及应用代码中的数据存储区交互。
提前刷新
提前刷新策略允许异步将数据加载到缓存存储中;也就是说,在这种设计中,应用仍然直接与缓存存储对话。但是,缓存层会在缓存中的数据过期之前定期刷新数据。对于最近访问过的条目,刷新是异步进行的,并且在到期前从原始存储异步刷新。这样,如果任何项目缓存过期,应用中就不会有任何延迟。
写后
在后写策略中,数据首先更新到缓存存储中,然后异步更新回数据存储,这与直写策略相反,直写策略中数据会立即更新到缓存存储中。这种策略的主要优势之一是减少延迟。但是,由于数据异步更新(写入数据存储和缓存存储是两个不同的事务)到数据存储,如果出现故障,应该实现回滚机制。
通常,与直写相比,实现起来要复杂得多,因为需要额外的处理来避免异步更新期间的任何数据丢失,但是如果需要将缓存存储作为主要来源,这仍然是一种很好的集成模式。
到目前为止讨论的所有模式都可以在较高的层次上可视化,如下图所示:
图 8.5–缓存模式
到目前为止,我们已经看到了各种缓存模式和策略。在下一节中,我们将讨论各种缓存提供程序及其与的集成.NET 5 应用。
缓存平台
.NET 5 支持多个缓存平台。一些常用的缓存平台如下:
- 内存缓存:这里缓存的数据是存储在进程内存里面。例如,如果应用托管在 IIS 上,内存缓存将从
w3wp.exe
开始消耗内存。 - 分布式缓存:数据跨多台服务器存储。可以与集成的数据存储.NET 5 应用是 SQL Server、Redis 和 NCache。
内存缓存
要配置内存缓存,在创建新的 ASP.NET Core 5 MVC/Web API 应用后,或者对于现有的 ASP.NET Core 5 MVC/Web API 应用,需要进行以下代码更改:
-
在
Startup.cs
的ConfigureServices
方法中增加services.AddMemoryCache()
。这个扩展方法是Microsoft.Extensions.DependencyInjection
的一部分(如果缺少,安装这个 NuGet 包),并将IMemoryCache
接口映射到MemoryCache
类。 -
MemoryCache
类是中IMemoryCache
的内置实现.NET 5,并且可以通过IMemoryCache
在任何 C#类中获得。它是使用构造函数注入实例化的。(对象创建使用依赖注入 ( DI )进行,在中的 第 5 章 、依赖注入中介绍.NET 。)因此,要将任何实体缓存到内存缓存中,请创建IMemoryCache
的属性,并使用构造函数注入创建MemoryCache
的实例。IMemoryCache
是Microsoft.Extensions.Caching.Memory
命名空间的一部分(如果缺少这个 NuGet 包,请安装它)。 -
MemoryCache
exposes many methods but a few important ones areGet
(to get the value of a key),Set
(to insert a key and its value), andRemove
(to remove a key (cache expiration)).其他方法可以在https://docs . Microsoft . com/en-us/dotnet/API/Microsoft . extensions . cache . memory . imemorycache?view = dotnet-plat-ext-3.1&view fallbackfrom = net-5.0和https://docs . Microsoft . com/en-us/dotnet/API/Microsoft . extensions . caching . memory . memory cache?view = dotnet-plat-ext-3.1&view fallback from = net-5.0。
-
While creating a cache object (using
Set
or other methods), the memory cache can be configured for various parameters usingMemoryCacheEntryOptions
. The following properties are supported:a.
SetAbsoluteExpiration
:缓存条目的绝对生存时间(TTL) 可以在TimeSpan
中设置,也可以设置缓存有效的确切日期和时间(DateTime
)。b.
SetSlidingExpiration
:缓存的非活动时间,在此时间之后,缓存条目将从缓存中删除。例如,5 秒的滑动到期值将等待缓存条目处于非活动状态 5 秒钟。滑动到期应始终小于绝对到期,因为缓存将在达到绝对到期持续时间后到期,而与滑动到期持续时间无关。c.
SetPriority
:由于在执行缓存回收时缓存大小有限(最近最少使用的 ( LRU) 、最少使用的(LFU )),所以缓存条目的优先级也被考虑。SetPriority
可用于通过CacheItemPriority
枚举设置缓存条目的优先级。其默认值为CacheItemPriority.Normal
。
一个简单的带有内存缓存集成的 Web API 控制器遵循前面的步骤,如下所示:
public class WeatherForecastController : ControllerBase
{
private IMemoryCache cache;
public WeatherForecastController(IMemoryCache cache)
{
this.cache = cache;
}
[HttpGet]
public IActionResult Get()
{
DateTime? cacheEntry;
if (!cache.TryGetValue("Weather", out cacheEntry))
{
cacheEntry = DateTime.Now;
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromSeconds(5))
.SetAbsoluteExpiration(TimeSpan.FromSeconds(10))
.SetPriority(CacheItemPriority.NeverRemove);
cache.Set("Weather", cacheEntry, cacheEntryOptions);
}
cache.TryGetValue("Weather", out cacheEntry);
var rng = new Random();
return Ok(from temp in Enumerable.Range(1, 5)
select new
{
Date = cacheEntry,
TemperatureC = rng.Next(-20, 55),
Summary = "Rainy day"
});
}
}
如您所见,这段代码是不言自明的,并且有一个使用内存缓存的应用编程接口。
在MemoryCache
中可用的另一个方法是集成一个通过RegisterPostEvictionCallback
可用的回调方法。这是MemoryCacheEntryOptions
中的一个扩展方法,它接受PostEvictionDelegate
委托,并在缓存条目过期期间触发回调。PostEvictionDelegate
签名如下:
public delegate void PostEvictionDelegate(object key, object value, EvictionReason reason, object state);
因此,这意味着我们传递给RegisterPostEvictionCallback
的回调应该遵循相同的签名,正如您所看到的,所有输入参数都是不言自明的。那么,我们增加一个回调方法,更新cacheEntryOptions
如下:
private void EvictionCallback(object key, object value, EvictionReason reason, object state)
{
Debug.WriteLine(reason);
}
cacheEntryOptions.RegisterPostEvictionCallback(EvictionCallback);
天气控制器的代码图如下图所示:
图 8.6–天气控制器代码图
一旦我们运行这段代码,我们可以看到在 10 秒绝对到期后对控制器的任何后续调用都会触发回调,并将原因记录为Expiration
。(一旦部署到AppService
,回调自动触发。仅出于调试目的,我们需要再次调用。)
分布式缓存
讨论完内存缓存后,让我们继续讨论其他可以配置分布式缓存的缓存平台。在本节中,我们将从 SQL 开始,了解不同类型的分布式缓存。
结构化查询语言
分布式缓存可以用各种存储来实现,其中之一就是 SQL Server。使用 SQL Server 进行分布式缓存的第一步是创建存储缓存条目所需的 SQL 表。SQL 作为分布式缓存存储的整个设置包括以下步骤:
-
Open a command line in the administrator prompt and run the following command to install the
dotnet-sql-cache
package globally:dotnet tool install ––global dotnet-sql-cache
事情是这样的:
图 8.7–使用安装 sql 缓存包.NET 命令行界面
-
Create the required database (on-premises or using Azure SQL) and run the following command:
dotnet sql-cache create "Data Source=.Initial Catalog=DistributedCache;Integrated Security=true;" dbo cache
在这个命令中,我们将数据库的连接字符串(在本地运行时相应地更新它)作为一个参数传递,另一个是表的名称(
cache
是前面片段中的表的名称):图 8.8–为分布式缓存创建一个 SQL 表
-
Once the command has run successfully, if we open the SQL server in SSMS, we will see a table as shown in the following screenshot that has the columns and indexes required for optimization:
图 8.9–来自 SSMS 的 SQL 分布式缓存中的缓存表
-
创建一个网络应用编程接口应用并安装 NuGet
Microsoft.Extensions.Caching.SqlServer
(通过包管理器控制台 ( PMC )或使用的.NET 命令行界面)。 -
在
Startup.cs
中,在ConfigureServices
方法中添加以下代码:services.AddDistributedSqlServerCache(options => { options.ConnectionString = "Data Source=.;Initial Catalog=DistributedCache;Integrated Security=true;"; options.SchemaName = "dbo"; options.TableName = "Cache"; });
-
要将数据插入缓存,我们需要利用
IDistributedCache
,通过构造函数注入来创建对象。因此,清理WeatherForecastController
(在创建 ASP.NET Core 5 网络应用编程接口项目期间创建的默认控制器)中的所有代码,并添加以下代码(具有Get
方法的网络应用编程接口控制器):public class WeatherForecastController : ControllerBase { private readonly IDistributedCache distributedCache; public WeatherForecastController(IDistributedCache distributedCache) { this.distributedCache = distributedCache; } }
-
增加以下
Get
方法,使用distributedCache
将数据保存到缓存存储(本例中为的 SQL):[HttpGet] public IActionResult Get() { DateTime? cacheEntry; if (distributedCache.Get("Weather") == null) { cacheEntry = DateTime.Now; var cacheEntryOptions = new DistributedCacheEntryOptions() .SetSlidingExpiration(TimeSpan.FromSeconds(5)) .SetAbsoluteExpiration(TimeSpan.FromSeconds(10)); distributedCache.SetString("Weather", cacheEntry.ToString(), cacheEntryOptions); } var cachedDate = distributedCache.GetString("Weather"); var rng = new Random(); return Ok(from temp in Enumerable.Range(1, 5) select new { Date = cachedDate, TemperatureC = rng.Next(-20, 55), Summary = "Rainy day" }); }
-
运行应用我们可以看到缓存条目正在被存储在 SQL 数据库中,如下图所示:
图 8.10–SQL 分布式缓存中的缓存表
如您所见,该代码与MemoryCache
代码非常相似,除了我们在此使用IDistributedCache
向缓存读取/写入数据,以及使用DistributedCacheEntryOptions
在缓存条目创建期间设置附加属性。
可以使用的所有其他方法都记录在 https://docs . Microsoft . com/en-us/dotnet/API/Microsoft . extensions . caching . SQL server . sqlservercache?view = dotnet-plat-ext-3.1 & view fallbackfrom = net-5.0。
使用 SQL Server 作为分布式缓存存储的一些建议如下:
- 如果现有应用不支持诸如 Redis 之类的存储,则可以选择 SQL Server。例如,仅与 SQL Server 集成的内部应用可以轻松扩展 SQL Server 以实现缓存目的。
- 缓存数据库应该不同于应用数据库,因为使用相同的数据库可能会导致瓶颈,并破坏使用缓存的目的。
- SQL Server 的
IDistributedCache
的内置实现是SqlServerCache
,它不支持为缓存表序列化不同的模式。任何定制都必须通过在定制类中实现IDistributedCache
来手动覆盖。
到目前为止,我们已经看到使用 SQL Server 的内存缓存和分布式缓存。在下一节中,我们将看到如何在中使用 Redis(推荐的存储之一,也是广泛用于缓存的存储)进行分布式缓存.NET 5 应用。
使用心得
Redis 是内存中的数据存储,用于各种目的,例如数据库、缓存存储,甚至作为消息代理。Redis 支持的核心数据结构是键值对,其中的值可以像字符串一样简单,也可以是自定义的复杂数据类型(嵌套类)。Redis 使用内存中的数据集,还可以根据需要将数据保存到磁盘上。Redis 还在内部支持 Redis 集群的复制和自动分区。由于所有这些特性都是现成的,所以它是分布式缓存的理想存储。
Azure 为 Redis 服务器提供了一个托管的实例,称为 Redis 的 Azure Cache,和其他任何 PaaS 服务一样,它也是由微软管理的。这允许应用开发人员按原样集成它,并将维护、扩展和升级 Redis 服务器的基础架构开销留给微软。Redis 的 Azure 缓存可用于分布式缓存,并可轻松集成到.NET 5 应用使用以下步骤:
-
First, create an instance of Azure Cache for Redis as outlined at https://docs.microsoft.com/en-in/azure/azure-cache-for-redis/quickstart-create-redis. Navigate to Access keys and copy the value under Primary connection string, as shown in the following screenshot:
图 8.11–来自 Redis Azure 缓存的缓存密钥
-
创建一个 ASP.NET Core 5 网络应用编程接口应用,并安装 NuGet
Microsoft.Extensions.Caching.StackExchangeRedis
包。 -
在
Startup.cs
中,在ConfigureServices
中增加以下代码:services.AddStackExchangeRedisCache( options => { options.Configuration = "<Connection string pasted in step 1>"; });
-
用与上一节 SQL 相同的代码更新默认的
WeatherForecastController
控制器。 -
运行应用,我们可以看到数据在缓存中存储了 10 秒。任何在 10 秒内对该应用编程接口的调用都将从缓存中检索数据。
-
Azure Cache for Redis also comes with a console that allows us to query the Redis server using Redis CLI commands. The console can be found in the Azure portal by navigating to the overview left menu of the Redis instance. Querying it for the
Weather
key will give us the results shown in the following screenshot:图 8.12–Redis 控制台的 Azure 缓存
如果我们选择 Redis 的 Azure Cache 的 Premium 层,它还支持多个碎片,以支持更高的数据量和地理复制以实现高可用性。
-
此外,要从缓存存储中添加/删除键,还有
GetAsync
和SetAsync
方法,可用于存储更复杂的类型或字符串以外的任何类型。但是,这些方法返回/接受Task<byte[]>
,所以应用需要处理序列化/反序列化,这可以在可重用的缓存库中看到。
Redis 是企业应用最首选的缓存存储,在我们的电商应用中,我们将使用 Azure Cache for Redis 作为我们的缓存存储。关于 Redis 的 Azure Cache 的一些附加信息可以在https://docs . Microsoft . com/en-in/Azure/Azure-Cache-for-Redis/找到。
其他提供商
可以看到,分布式缓存在.NET 5 应用由IDistributedCache
驱动,无论哪个存储的实现被注入到Startup
类缓存存储中,都会进行相应的配置。此外,还有两个提供程序.NET 5 内置了以下实现:
- NCache :这是一个第三方缓存存储,开发于.NET/.NET Core 并有一个
IDistributedCache
的实现。NCache 可以像 Redis 或 SQL 一样集成。但是,NCache 服务器需要在本地配置以进行开发,可以使用虚拟机在 IaaS 中配置,也可以使用应用服务在 PaaS 中配置。 - 分布式内存缓存 (
AddDistributedMemoryCache
):这是IDistributedCache
的另一个内置实现,可以类似使用。可以使用进行单元测试。由于它不是分布式缓存,并且使用进程内存,因此不建议用于多个应用服务器场景。AddMemoryCache(IMemoryCache)
和AddDistributedMemoryCache(IDistributedCache)
唯一的区别是后者需要序列化来存储复杂的数据。所以,如果有一个类型不能序列化,需要缓存,就用IMemoryCache
,否则就用IDistributedCache
。
在企业应用中,IDistributedCache
可以处理所有的缓存层实现,开发/测试环境的内存缓存和生产环境的 Redis 的组合将是理想的。如果您的应用托管在单个服务器上,您可以使用内存缓存,但这对于生产应用来说非常罕见,因此最推荐使用分布式缓存。
因此,基于我们讨论的所有原则和模式,我们将设计一个用于电子商务应用的缓存抽象层,这将在下一节中讨论。
使用分布式缓存设计缓存抽象层
在企业应用中,在底层缓存实现之上有一个包装类总是好的,因为它抽象了缓存的核心逻辑,并且还可以作为一个单独的类来保存应用范围内的默认缓存条目选项。
我们将使用IDistributedCache
实现实现一个缓存包装类,其底层存储为 Redis 的 Azure 缓存。是. NET 标准 2.1 类库;这个库的源代码可以在Packt.Ecommerce.Caching
项目中获得。任何想要缓存数据的类都应该使用构造函数注入来注入IDistributedCacheService
,并且可以调用以下各种方法:
AddOrUpdateCacheAsync<T>
:异步添加或更新T
类型的缓存条目AddOrUpdateCacheStringAsync
:异步添加或更新字符串类型的缓存条目GetCacheAsync<T>
:异步获取T
类型的缓存条目GetCacheStringAsync
:异步获取字符串类型的缓存条目RefreshCacheAsync
:异步刷新缓存条目RemoveCacheAsync
:异步移除缓存条目
DistributedCacheService
是继承IDistributedCacheService
并实现前面所有方法的包装类。此外,IDistributedCache
和DistributedCacheEntryOptions
在此类中配置为使用分布式缓存。
对于序列化和反序列化,我们将使用System.Text.Json
,一个自定义的IEntitySerializer
接口,并且EntitySerializer
类是用以下方法创建的:
SerializeEntityAsync<T>
:异步将指定对象序列化为字节数组DeserializeEntityAsync<T>
:异步反序列化指定的流
使用构造函数注入将IEntitySerializer
实现注入到DistributedCacheService
类中,并用于序列化和反序列化。
注意
请参考序列化性能对比一文,讲的是各种序列化器的对标。可以在https://maxondev . com/serialization-performance-comparison-c-net-formats-framework-xmldata contractserializer-xmlserializer-binary formatter-JSON-newtonsoft-service stack-text/找到。
DistributedCacheService
和EntitySerializer
的实现遵循 第四章线程和异步操作中讨论的所有异步原则。
最后,在应用编程接口/MVC 应用中,执行以下步骤:
-
安装 NuGet
Microsoft.Extensions.Caching.StackExchangeRedis
包。 -
通过在
Startup.cs
的ConfigureServices
部分添加以下代码片段来配置缓存:if (this.Configuration.GetValue<bool>("AppSettings:UseRedis")) { services.AddStackExchangeRedisCache(options => { options.Configuration = this.Configuration.GetConnectionString("Redis"); }); } else { services.AddDistributedMemoryCache(); }
-
从配置的角度来看,
appsettings.json
增加了两个属性,如下所示:"ConnectionStrings": { //removed other values for brevity "Redis": "" //Azure Cache for Redis connection string. }, "AppSettings": { //removed other values for brevity "UseRedis": false //Flag to fallback to in memory distributed caching, usually false for local development. },
任何想要缓存数据的类都需要添加对Packt.Ecommerce.Caching
项目的引用并注入IdistributedCacheService
,并且可以调用前面提到的方法来读取/更新/插入数据到缓存存储中。使用缓存服务的方法的代码片段如下:
public class Country
{
public int Id { get; set; }
public string Name { get; set; }
}
public async Task<Country> GetCountryAsync()
{
var country = await this.cacheService.GetAsync<Country>("Country"); // cacheservice is of Type IDistributedCacheService and is injected using constructor injection.
if (country == null)
{
country = await this.countryRepository.GetCountryAsync(); // Retrieving data from database using Repository pattern.
if (country != null)
{
await this.cacheService.AddOrUpdateAsync<Country>("Country", country, TimeSpan.FromMinutes(5));
}
}
return country;
}
这里,我们使用缓存备用模式,首先检查缓存存储中的Country
键。如果找到了,从函数中返回,否则从数据库中检索并将其插入缓存,然后从函数中返回。我们将大量使用 第 10 章中的缓存服务,创建一个 ASP.NET Core 5 网络应用编程接口(参考https://github . com/PacktPublishing/Enterprise-Application-Development-with-C-Sharp-9-and-.NET-5/blob/master/Enterprise % 20 应用/src/平台-API/服务/Packt。Ecommerce.Product/Services/ProductsService.cs就是这样一个例子。).
正如您所看到的,我们已经使用了前面章节中讨论的一些模式。在下一节中还将讨论一些附加注意事项,在企业应用中设计缓存层时需要牢记这些注意事项。
缓存注意事项
拥有缓存层对于提高企业应用的性能和可伸缩性非常关键。因此,在开发缓存层时,需要考虑以下几个因素:
- 如果我们正在构建一个新的应用,那么 Redis 的 Azure Cache 可以作为使用
IDistributedCache
实现的起点,因为它可以很容易地通过几行代码深入到应用中。然而,这伴随着需要评估的成本。 - 对于现有的项目,当前的基础结构起着关键的作用,如果 SQL Server 已经被用作数据存储,那么 SQL 可以是默认的选择。然而,对照 Redis 对使用 SQL 的性能进行基准测试是很好的,可以据此做出决定。
- 在底层缓存存储上有一个包装类是一个很好的方法,因为它将应用从缓存存储中分离出来,并在缓存存储未来发生变化时提供更大的代码灵活性和可维护性。
IMemoryCache
和IDistributedCache
的方法不是线程安全的。例如,假设一个线程从缓存中查询一个键,发现它不在缓存中,并返回到原始数据存储;虽然如果另一个线程查询相同的键,则从原始存储中检索数据,但它不会等待第一个线程完成从数据库的读取。第二个线程也将返回数据库。因此,线程安全需要显式处理,可能在包装类中处理。- 响应缓存应该与应用缓存一起实现,以便进行更多的优化。
- 实体标签 ( ETag )响应头可以进一步用于提高缓存数据的可重用性。ETag 是全局唯一标识符(GUID)-在缓存服务器上的数据时生成,作为响应头发送。该值作为
If-None-Match
请求头的一部分发送回服务器,如果匹配,服务器返回304
(无变化),客户端可以重用缓存的数据版本。对于 ETag,在服务器端没有内置的实现,所以可以使用过滤器或者中间件来实现服务器端的逻辑。 - 虽然我们在实现中使用了 JSON 序列化,但是还有其他格式,例如 BSON 或协议缓冲区,应该针对序列化和反序列化进行评估。
就像应用开发中用于缓存的任何其他组件一样,也没有一刀切的要求。因此,可以评估前面几点,并相应地实施缓存解决方案。
总结
在本章中,我们了解了各种缓存技术、模式及其在提高应用性能方面的优势。此外,我们还了解了 HTTP 缓存、响应缓存如何集成到应用编程接口响应中,以及各种可用的缓存提供程序及其与的集成.NET 5 应用。我们还学习了如何使用IDistributedCache
实现分布式缓存,并构建了一个缓存抽象层,将在后续章节中用于缓存需求。我们一路上学到的一些关键技能是为什么以及何时需要缓存,以及如何在中实现缓存.NET 5 应用。
在下一章中,我们将了解中的各种数据存储和提供程序.NET 5 及其与.NET 5 应用。
问题
-
Which of the following values for the
Cache-Control
header allows the response to be stored in any server (client/server/proxy)?a.
Private
b.
Public
c.
No-cache
-
In a multiple-application server scenario, which of the following caching platforms should we choose?
a.分布式缓存
b.内存缓存
-
True or false: In the cache-aside pattern, data is first updated in the cache store and then in the underlying data store.
a.真实的
b.错误的
-
Which of the following caches is best suited to store static files and image files and supports geo-replication?
a.Web 服务器缓存
b.应用缓存
c.内容交付网络 ( CDN )
进一步阅读
您可以在这里阅读更多关于缓存的信息:
https://github . com/alachisoft/ncache
九、.NET 5 处理数据
任何 web 应用的基本组件之一是将数据保存到永久数据存储中的能力;在选择合适的持久存储时,一些预先考虑可以帮助系统在未来更好地扩展。
任何 web 应用中常见的操作之一是登录到系统,执行一些读取/更新,然后注销,然后稍后再回来查看更改是否被保留。数据库在保持这些操作(通常称为用户事务)方面起着重要作用。除了事务数据之外,出于监控和调试的目的,应用可能还需要存储日志数据和审计数据,例如谁修改了日期。设计任何此类应用的一个重要步骤是理解需求并相应地设计数据库。根据各种数据保留要求和任何数据保护政策选择/设计数据库也很重要,例如通用数据保护条例 ( GDPR )。
一个应用可以有多个数据提供者,如结构化查询语言 ( SQL )数据提供者、NoSQL 数据提供者、文件数据提供者等等。在本章中,我们将讨论可用于中的存储和数据处理的各种数据提供程序.NET 5。我们将涵盖以下主题:
- 数据介绍
- 磁盘、文件和目录
- SQL、天青宇宙数据库和天青存储
- 与英孚核心合作
- 使用 Azure 宇宙数据库设计数据访问服务
技术要求
对…的基本了解.NET Core、C#、Azure 和.NET 命令行界面是必需的。
本章的代码文件可以在以下链接中找到:https://github . com/PacktPublishing/Enterprise-Application-Development-with-C-Sharp-9-and-.NET-5/树/主/第 09 章。
代码的使用说明可以在这里找到:https://github . com/PacktPublishing/Enterprise-Application-Development-with-C-Sharp-9-and-.NET-5/树/主/企业% 20 应用
数据介绍
任何网络应用,无论是内容管理系统、社交网络平台还是电子商务应用,都需要将数据保存到永久存储中,以便用户可以根据需要检索、消费和处理数据。在 第八章理解缓存中,我们讨论了使用缓存存储;但是,缓存存储是临时存储,数据仍然需要保存到永久存储中。所以,我们需要一个不只是支持不同实体上各种 CRUD (简称创建/读取/更新/删除)操作,还支持高可用性,并在宕机时恢复任何数据的存储,也就是灾难恢复。
更好的系统设计的关键标准之一是在系统的早期阶段设计一个数据模型。数据模型应该尝试定义系统运行所需的所有可能的实体,并在各种实体之间进行交互。在系统设计的早期定义数据模型有助于确定如何管理数据和可以使用什么数据存储的正确策略,并有助于决定各种复制/分区策略。
下面几节将解释两种常见的分类数据存储。
关系数据库管理系统
关系数据库将数据存储在表中。每个实体被定义为一个或多个表,并且使用多个表来定义数据库。将表分隔成多个表的过程称为规范化。各种表之间的关系由外键约束定义。实体的属性定义为列,相同类型的多个实体存储为行。一些常用的关系数据库是微软的 SQL Server、MySQL、Postgres 和 Oracle。
存储员工信息的典型关系数据库可能有一个定义员工各种属性的employee
表,如姓名、员工标识等,以及以员工标识为主键的列。多个员工作为单独的行存储在该表中。员工的任何属性都可以进一步规范化到单独的表中;例如,员工的项目可以存储在一个单独的表中(因为可以有多个项目),例如employeeproject
,并且可以使用员工标识链接到员工表,如下图所示:
图 9.1–员工 ER 图
以下是关系数据库的几个关键特征:
- 使用 SQL 查询关系数据库。
- 表大多有定义良好的模式和约束,不太可能改变。
- 所有事务都具有 ACID (原子性/一致性/隔离性/持久性的缩写)属性,因此保持了数据的完整性和一致性。
- 随着数据的规范化,冗余被最小化。
- 关系数据库通常支持垂直扩展,也就是向上扩展(它们确实支持复制,但是与 NoSQL 数据库中的复制相比,这是一个昂贵的操作)。
NoSQL
另一种数据存储是 NoSQL 数据库,它以非结构化格式存储数据,其中数据不需要有预定义的模式。最常见的情况是,数据要么存储为键值对(如在 Redis 中),要么存储为文档(如在 MongoDB 和 CouchDB 中),要么存储为使用图形结构的图形(如在 Neo4j 中)。
如果我们以同一个雇员为例,并将其保存在 NoSQL 数据库(如 MongoDB)中,我们最终会将其存储在一个类似employee
的集合中,每个文档都存储该雇员的所有属性,如下所示:
{
"employee": [
{
"employeeid": 1,
"name": "Ravindra",
"salary": 100,
"Projects": [
{
"id": 1,
"name": "project1",
},
{
"id": 2,
"name": "project2",
}
]
}
]
}
以下是 NoSQL 数据库的几个关键特征:
- 实体不一定需要支持固定的模式,在任何时间点,都可以添加额外的属性。
- 它们非常适合非结构化数据,例如,将位置存储在拼车应用中。
- 与关系数据库相比,它们可以以低得多的成本轻松支持水平扩展。
- 数据高度冗余;然而,这极大地提高了性能,因为无需跨表执行连接就可以轻松获得数据。
Azure Cosmos DB 就是这样一个云管理的 NoSQL 数据库,我们将在我们的电子商务应用中将其用作数据存储。
任何存储平台的核心基础存储介质都是进一步存储在磁盘中的文件。让我们看看中提供的各种 API.NET 5,执行创建和修改文件和目录等操作。
磁盘、文件和目录
在计算机中,一个文件只不过是一组使用唯一名称引用的数据。比如所有员工的详细信息都可以分组存储在一个名为employees
的文件中,每当我们需要查看员工数据时,我们都会打开employees
文件搜索该数据。这是从一个类比中得出的,如果我们想以非数字形式保存所有员工的详细信息,我们将把它们写在纸上,把所有的文件钉在一起,并保存在一个文件中。
磁盘只不过是存储文件的存储介质。根据前面的类比,磁盘可以比作可以存储文件的机架。磁盘应该支持的一个关键点是管理文件、组织文件、定义文件的属性的方法,例如创建/修改文件的时间、在磁盘上查找可用空间的能力等等。这就是文件系统发挥作用的地方,它通常与操作系统一起出现,并包含管理磁盘上文件的这些方面。文件系统实现这一点的一种方法是使用目录。
现在让我们看看中的各种 API.NET 5,它允许我们与文件、磁盘和目录交互。
处理目录
目录或文件夹是文件分组的一种方式,是文件系统定义的一种规则组织文件和子目录。英寸 NET 5 中,我们在框架中提供了与文件系统通信和组织目录的 API。这些类主要是System.IO
命名空间的一部分,有两个类主要用于对目录执行各种操作:
- 目录 :
System.IO.Directory
是一个静态类,公开了各种方法允许我们创建目录,查询目录属性,移动等等。当我们需要执行简单的目录操作时,这个类很有用。 - 目录信息 :
System.IO.DirectoryInfo
是另一个可以用来对目录/文件夹进行各种操作的类;然而,这不是一个静态类,因此需要这个类的一个对象来调用各种方法。更重要的是,这个类提供了一些高级方法,比如在一个工作目录中创建子目录。因此,如果我们需要递归地对单个目录执行操作,可以使用这个类。
让我们创建一个简单的控制台应用,如果目录不存在,就创建一个目录,并列出目录的各种属性。我们还将创建一些文件并(在下一节中详细讨论)枚举它们,然后最后删除目录。
我们还将使用System.Environment
、System.IO.Path
和System.IO.DriveInfo
类来显示与环境、当前驱动器和当前文件夹相关的一些常规属性。那么,让我们来看看怎么做:
-
使用 Visual Studio 2019 或创建控制台应用.NET 命令行界面。
-
Update the
Main
class with the following code:static void Main(string[] args) { string currentDriveLetter = Path.GetPath Root(System.Reflection.Assembly. GetEntryAssembly().Location); DriveInfo di = new DriveInfo(current DriveLetter); if (di.IsReady) // Checking if drive is // ready { Console.WriteLine($"Available space in {currentDriveLetter} is {di.AvailableFreeSpace.ToString()}"); } Console.WriteLine($"Current location (using Environment class) – {Environment.CurrentDirectory}"); Console.WriteLine($"Current location (using path class)- {Path. GetFullPath(Environment. CurrentDirectory)}"); string newDirectoryName = "New Data Directory"; if (Directory.Exists(newDirectoryName)) { Console.WriteLine($"Directory with name {newDirectoryName} already exists!!"); } else { Directory.CreateDirectory( newDirectoryName); Console.WriteLine($"Directory {newDirectoryName} created!!"); } }
在前面的代码中,我们正在执行以下操作:
- 使用
Path
类获取当前驱动器的路径 - 使用
DriveInfo
类显示当前驱动器中的可用空间 - 如果目录不存在,则创建目录
- 使用
-
接下来,在目录创建代码之后,将以下代码添加到
Main
方法中。在这段代码中,我们正在执行以下操作:-
使用
DirectoryInfo
类创建子目录。 -
使用
System.IO.File
静态类创建几个空文件,然后枚举这些文件,显示它们的一些属性。 -
最后,删除创建的文件夹。参考以下代码:
DirectoryInfo dirInfo = new DirectoryInfo( newDirectoryName); dirInfo.CreateSubdirectory($"Sub {newDirectoryName}"); //Create few files and enumerate. for (int i = 0; i < 5; i++) { FileStream fs = File.Create(Path. Combine(Environment.CurrentDirectory, newDirectoryName, $"File {i}")); fs.Dispose(); } foreach (FileInfo fi in dirInfo.GetFiles()) { Console.WriteLine($"File {fi.Name} created on - {fi.CreationTime. TimeOfDay}, Size: {fi.Length}"); } dirInfo.Delete(true);// clean up, passing //true to recursively delete contents. Console.WriteLine("Directory deleted!!");
-
-
一旦我们运行这段代码,我们将看到以下输出:
图 9.2–目录操作输出
如果您打开 Windows 资源管理器,在删除目录之前查看目录的中间内容,它将如下所示:
图 9.3–目录中的文件
如你所见.NET 5 自带了很多 API,允许我们对目录执行各种操作。在下一节中,让我们浏览一下可用于对文件执行类似操作的各种 API。
用流、二进制和字符串读取/写入数据
.NET 5 自带了大量的 API 来读写文件中的数据,从静态的System.IO.File
类到基于实例的类,比如System.IO.FileInfo
。然而,在进入可以对文件进行操作的应用编程接口之前,我们应该了解什么是流。
英寸 NET 5,一个流只不过是一个来自存储的字节序列。底层存储/设备,也称为后备存储,可以是文件、内存或对应用编程接口(网络)的响应。流的概念主要是提供一个抽象,这样应用就不需要知道后备存储。每当应用需要读取一些数据时,它将使用可以从流中读取的 API,并且当它需要更新该数据时,它将更新该流。然后,该流将进一步将其传递给后备存储。
溪流
流在从文件中读取/写入数据时很有用。英寸 NET 5,支持从流中读取和向流中写入的类是System.IO.Stream
,它是一个抽象类,有三个重要的方法:
Read/ReadAsync
:同步和异步从流中读取数据Write/WriteAsync
:将数据同步和异步写入流Seek
:接受整数值,并将流中的当前位置移动到指定值
由于Stream
类是抽象类,.NET 5 附带了一些具体的实现,如下图所示:
图 9.4–流层次结构
让我们创建一个方法,并使用FileStream
类的一个对象来读取和写入数据到一个文件。我们将使用静态File
类的Create
方法,然后使用OpenRead
方法读取文件的内容。
创建控制台应用并添加以下方法:
static async Task ReadStreamAsync()
{
byte[] writeData = new byte[5] { 80, 65, 67, 75
, 84 };
UTF8Encoding temp = new UTF8Encoding(true);
string data = "PACKT";
Encoding.ASCII.GetBytes(data);
using (Stream fs = File.Create("WriteDataUsing
FileStream.txt"))
{
await fs.WriteAsync(writeData);
//String PACKT in ASCII
byte[] readData = new byte[5];
fs.Position = 0; // Setting the stream
//position to 0.
int chunkRed = 0, dataRed = 0;
while ((chunkRed = fs.Read(readData,
dataRed, readData.Length –
chunkRed)) > 0)
{
dataRed += chunkRed;
}
for (int i = 0; i < readData.Length; i++)
Console.Write(readData[i]);
}
一旦我们在控制台应用中调用这个方法,我们可以看到已经创建了一个名为WriteDataUsingFileStream.txt
的文本文件,该文件的内容将是PACKT
字符串。现在,流的Read
方法不能保证它将读取大小计数参数的整个数据(在前面的示例中为readData.Length
),因此我们需要依赖于Read
方法返回的值,该值只不过是已处理的数据流的大小。然而,在前面的代码中,控制台上的输出仍然以字节为单位,即80, 65, 97, 75, 84,
,这就是编码和解码的作用。
编码是将字符转换成字节的一种方式,同样,解码也是将字节转换成字符的一种方式。这里,在示例中,我们需要对字节数组进行解码,以便将内容转换为字符,并以可读的格式显示。英寸 NET 5,这可以通过使用System.Text.Encoding
类来实现。在前面的代码中,按照如下方式更改Console.WriteLine
并运行应用:
Console.Write($"{Encoding.ASCII.GetString(readData)}");
这次你可以看到输出是PACKT
。
读者/作者
大多数现实世界的应用会处理可读性更强的字符串、XML/JSON 数据或该语言支持的任何其他数据类型,而不是字节,尽管在前面的示例中,我们可以用一行代码将字节数组转换为字符串,但随着编码类型的不同,如 ASCII、UTF-8 等,它会变得更加复杂。这就是原因.NET 5 附带了读取器和写入器类,它们可以与流对话,并根据需要转换数据。这些类主要充当适配器,抽象底层流,然后将流转换为所需的类型。一般来说,读者/作家类.NET 5 如下图所示:
图 9.5–读取器/写入器(适配器)层次结构
让我们了解这些是什么:
StreamReader/StreamWriter
:抽象类TextReader/TextWrite
的具体实现,对流进行操作,将它们转换成字符/字符串,反之亦然StringReader/StringWriter
:具体实现TextReader/TextWriter
抽象类对内存中的字符串进行操作,例如字符串操作或者对大字符串段落中的行数进行计数BinaryReader/BinaryWriter
:从流中读取和向流中写入的具体类,专门将 int 和 Boolean 等原始数据类型转换为流,反之亦然XmlReader/XmlWriter
:将流转换为 XML 的具体类,反之亦然
有了这个的理解,让我们创建一个简单的控制台应用,我们将使用StreamWriter
将数据写入文件,然后使用StreamReader
读取数据,使用StringWriter
和StringReader
对内容重新排序,并显示它:
-
创建一个控制台应用,并添加以下将字符串内容写入文件的方法:
static async Task UsingStreamReaderWriter() { // Create a string array with the lines of text string[] lines = { "This is the First line ", "This is the Second line", "This is the Third line" }; // Write the string array to a new file //named "WriteLines.txt". using (StreamWriter outputFile = new StreamWriter("WriteLinesUsingSW.txt")) { foreach (string line in lines) { await outputFile.WriteLineAsync (line); } } Console.WriteLine("File write completed using StreamWriter\n"); }
-
将以下代码添加到
UsingStreamReaderWriter
方法中,在该方法中,我们正在读取之前使用StreamReader
创建的文件。此外,这里我们还利用StringWriter
将其附加写入字符串。当然,这里我们也可以利用StringBuilder
类;然而,如果有一个场景,我们需要将它传递给另一个接受TextReader
类型实例的方法,在这种情况下,StringReader
会派上用场:StringBuilder sb = new StringBuilder(); StringWriter srw = new StringWriter(); using (StreamReader sr = new StreamReader ("WriteLinesUsingSW.txt")) { Console.WriteLine(sr.BaseStream.Length ); // Using base stream to //retrieve length of file contents. while (!sr.EndOfStream) { string line = sr.ReadLine(); srw.Write($"{line}\n"); // Appending . at the end of line for easy //separate later } } Console.WriteLine("Reading from StringWriter"); Console.WriteLine(srw.ToString());
-
最后,添加以下代码,它与文件内容相反,并打印它。由于文件内容已经在
StringWriter
对象中可用,我们将把它读入一个StringReader
对象,并使用StringReader
的方法生成最终输出:using (StringReader str = new StringReader(srw.ToString())) { while (str.Peek() > - 1) // Peeking to check end of string { char currentCharacter = Convert. ToChar(str.Read()); if (currentCharacter != '\n') //Checking current character and inserting at the //beginning of string builder sb.Insert(0, currentCharacter); else sb.Insert(0, '.'); } } if (srw != null) srw.Dispose(); Console.WriteLine("Reading from Stringbuilder aftere reversal"); Console.WriteLine(sb.ToString());
-
然后,我们在主方法中调用这个并执行控制台应用。输出将如下图所示,同时在相同的位置创建
WriteLinesUsingSW.txt
文件:
图 9.6–读取器/写入器输出
StreamWriter
使用的默认编码是 UTF-8;但是,有一个参数化的构造函数允许您指定编码。在StreamReader
的情况下.NET 5 在内部尝试检测流的编码并相应地使用它。然而,如果它不能检测到源的编码,它将回落到 UTF-8。
一般来说,如果给一个选择,我们应该使用一个使用更少字节来存储数据的编码,重要的是,它应该支持你数据中的所有字符。
文本阅读器/文本编写器与二进制阅读器/二进制编写器
一般来说,存储在计算机中的任何东西只不过是一系列 0 或 1 的位,因此所有东西都以二进制形式存储。然而,存储为二进制的文件和二进制或文本的文件内容是有区别的。例如,文本文件的内容可以是文本数据;但是,JPEG 文件不能表示为文本,并且大多数情况下,它将以二进制形式表示。正确的读写器库用于合适的文件是非常重要的。下表给出了各种适配器之间的一些差异:
表 9.1
我们对 C#中可用的各种类的讨论到此结束,这些类可用于写入文件、从文件中读取以及处理目录。让我们简单讨论一下 C#中最常用的序列化库。
JSON。净得很
任何关于文件读/写的讨论如果不涉及 JSON.NET 库都是不完整的,该库是用于将对象序列化为 JSON 的最常见的库,反之亦然。正如我们所知,JSON 是 JavaScript 对象符号,也是交换数据的公认标准。关键思想是能够保存/传输(序列化)对象的状态,以便可以在稍后阶段以相同的形式检索(反序列化)。
使用 JSON.NET 序列化数据最简单的方法是使用JsonConvert
静态类并调用方法,如以下代码片段所示:
// Type
public class Employee
{
public string Name { get; set; }
public int Id { get; set; }
}
// Usage
Employee employee = new Employee
{
Name = "John",
Id = 1
};
// Serialize
File.WriteAllText("employee.json", JsonConvert.
SerializeObject(employee));
// Deserialize
var output = JsonConvert.DeserializeObject<Employee>(File.ReadAllText("employee.json"));
虽然从.NET Core 3.0 之后,微软推出了一个名为System.Text.Json
的新库,用于序列化和反序列化对象;正如在 第 8 章中开发的Packt.Ecommerce.Caching
项目一样,理解 缓存,我们将使用System.Text.Json
进行我们的电子商务应用,除非我们遇到这里提到的一些问题:https://github.com/dotnet/runtime/issues?q=system.text.json。所有可用的 api 都可以在这里找到:https://docs . Microsoft . com/en-us/dotnet/API/system . text . JSON?view=net-5.0 。
到目前为止,我们已经看到了如何在中可用的文件和目录以及各种 API 上进行操作.NET 5。然而,更多的时候,应用不是直接写入文件系统,而是写入一个抽象了文件系统复杂性的数据库系统。让我们看看其中的一些提供者以及它们与.NET 5。
SQL、Azure 宇宙数据库和 Azure 存储
之前,我们讨论了将数据存储分为关系数据库和 NoSQL 的更广泛的分类。在本节中,让我们详细了解微软生态系统中可用的一些数据提供者及其与的集成.NET 5。提供者种类繁多,包括 SQL、Azure Cosmos DB、Azure Storage 等,数据提供者的选择完全由应用需求驱动。然而,在现实生活中,应用的需求会发生很大的变化,因此关键是用业务层和用户界面来抽象数据框架的实现,这进一步有助于根据需要改进设计。接下来,让我们在下一节中看看我们的第一个数据提供者,SQL。
SQL Server
关系数据库管理系统市场中占主导地位的数据库之一是微软 SQL Server,俗称 SQL Server,它使用 SQL 与数据库进行交互。SQL Server 支持所有基于关系数据库管理系统的实体,例如表、视图、存储过程和索引等,并且主要在 Windows 环境下工作。但是,从 SQL Server 2017 开始,它同时支持 Windows 和 Linux 环境。
SQL Server 的主要组件是它的数据库引擎,它负责处理查询和管理文件中的数据。除了数据库引擎之外,SQL Server 还附带了各种数据管理工具,例如:
- SQL Server 管理工作室 ( SSMS ):连接到 SQL Server 并执行操作,如创建数据库、监控数据库、查询数据库和备份数据库
- SQL Server 集成服务 ( SSIS ):用于数据集成和转换
- SQL Server 分析服务 ( SSAS ):用于数据分析
- SQL Server 报告服务 ( SSRS ):用于报告和可视化
要在本地计算机上配置 SQL Server,我们需要安装安装数据库引擎和一个或多个前面组件的 SQL Server 版本之一。安装通常包括下载安装程序并通过图形用户界面或命令行安装。有关安装的更多详细信息,请参考 https://docs . Microsoft . com/en-us/SQL/database-engine/install-windows/install-SQL-server?view=sql-server-ver15。
虽然在内部,但是 SQL Server 已经被广泛使用;管理数据库、升级等总会有开销这也是微软推出 Azure SQL 的地方,Azure SQL 是一个完全托管的 PaaS (简称平台即服务)组件,运行在与内部 SQL Server 相同的数据库引擎上。
Azure SQL 附带以下变体:
-
Azure SQL 数据库(单个数据库):这是一个托管数据库服务器允许你用专用资源创建一个完全隔离的数据库。
-
Azure SQL Database(弹性池):弹性池允许您在单个服务器上的预定义资源池(就 CPU、内存和 I/O 而言)中运行多个单个数据库。它是拥有多个数据库的企业的理想选择混合了低使用率和高使用率。在这种情况下使用弹性池的优点是,需要更多 CPU 使用的数据库可以在高需求期间利用它,并在低需求时释放它。使用弹性池的理想情况是当有一组数据库并且它们的消耗不可预测时。每当您看到一个数据库持续消耗相同的资源集时,它就可以从弹性池中移出,进入单个数据库,反之亦然。
-
Azure SQL 托管实例:该模型提供了一种将内部 SQL 基础架构无缝迁移到 Azure SQL 的方法,而无需重新架构内部应用,并允许您利用 PaaS。这非常适合于拥有庞大的内部数据库基础架构并且需要迁移到云而没有太多运营开销的应用。
-
SQL Server on VM (Windows/Linux): SQL VMs come under the Infrastructure as a Service (IaaS) category and are very similar to on-premises SQL Server, only that VMs are on Azure instead of your local network.
小费
建议安装 SSMS,以便在 SQL Server(内部或云)上执行各种操作,因为它支持所有数据库操作。
从. NET 5 应用的角度来看,连接到 Azure SQL 与连接到内部 SQL Server 是一样的。可以使用 ADO.NET,我们使用System.Data.SqlClient
导入,然后使用SqlConnection
对象连接到 SQL,然后使用SqlCommand
对象执行 SQL 查询,使用SQLReader
类返回值。除此之外,我们可以使用对象关系映射器 ( ORM )如实体框架核心 ( EF 核心)来处理 Azure SQL,这将在处理 EF 核心一节中讨论。
所以,在这个部分,我们已经简单介绍了 Azure SQL。但是,我建议在这里查看 Azure SQL 的所有功能:https://docs.microsoft.com/en-us/azure/azure-sql/。
至此,让我们继续讨论 Azure Cosmos DB,这是我们的电子商务应用将用作持久存储的数据库。
蓝色宇宙 DB
Azure Cosmos DB 是一个完全托管(PaaS)的 NoSQL、全球分布式和高度可扩展的数据库。Azure Cosmos DB 的一个关键之处是它的多模型特性,这有助于使用不同的 API 模型(如 SQL、MongoDB 和 Gremlin)传递各种格式的数据,如 JSON 和 BSON。开发人员可以灵活地使用他们熟悉的应用编程接口查询数据库。例如,SQL 开发人员可以继续使用 SQL 查询语法查询数据库,MongoDB 开发人员可以继续使用 MongoDB 语法查询数据库,等等。在引擎盖下,Azure Cosmos DB 以名为Atom-Record-Sequence(ARS)的格式存储数据库,并根据数据库创建过程中选择的模式将数据作为 API 公开。
Azure Cosmos DB 的另一个重要特点是它能够自动索引所有数据,并且独立于所使用的 API 模型。所有这些都无需开发人员另外创建索引,因此能够更快地检索数据。
Azure Cosmos DB 支持以下 API 来对数据库执行操作,这是我们在创建数据库时选择的:
- 核心(SQL) API :这是可以用来查询数据库的默认 API;查询会有一个 JSON 中输入/输出格式的 SQL 查询的语法。使用核心 SQL 应用编程接口的典型查询如下所示:
SELECT * FROM product WHERE product.Name = ' Mastering enterprise application development Book'
。 - MongoDB API :这个 API 建立在 MongoDB 的有线协议上,可以与 MongoDB 客户端 SDK、驱动和工具无缝集成。该应用编程接口非常适合已经与 MongoDB 集成并迁移到 Azure Cosmos DB 的应用,或者非常适合开发人员已经习惯 MongoDB 查询语言的团队。这个 API 的最新版本支持 MongoDB 服务器 3.6 版本,Azure Cosmos DB 和 Mongo API 之间的典型查询连接将如下所示:
db.product.find({"Name": ' Mastering enterprise application development Book'})
。就像 MongoDB 一样,数据在 BSON 表示。 - Gremlin(图形)API :该 API 支持使用 Gremlin 语言以图形格式查询和遍历数据。这非常适合数据可以以图形的形式表示,并且可以通过它们之间的关系进行查询的情况。一个典型的例子是推荐引擎,它可以建立两个实体之间的关系,并提出一个推荐。
除此之外,还有卡珊德拉应用编程接口,它使用卡珊德拉查询语言对数据库进行操作,然后是表应用编程接口,它可以被构建在 Azure 表存储之上的应用用作它们的数据存储。
正如你所看到的,有相当多的应用编程接口,更多的应用编程接口正在被添加。选择合适的 API 完全取决于应用需求;但是,以下几点可用于缩小选择范围:
- 如果是一个新的应用,那么就用核心应用编程接口。
- 如果它是一个建立在 NoSQL 上的现有应用,请根据底层数据存储选择相关的 API。例如,如果现有的数据库是 MongoDB,选择 Mongo API 等等。
- 对于处理特定的场景,比如建立数据之间的关系,使用 Gremlin API。
对于我们的企业应用,由于我们是从头开始构建这个应用,我们将使用核心(SQL)应用编程接口作为我们与 Azure Cosmos DB 交互的应用编程接口。
让我们先创建一个简单的控制台应用,并在 Azure Cosmos DB 上执行一些操作,稍后我们将在构建我们的数据访问服务时重用这些概念:
-
首先,我们需要有一个 Azure Cosmos DB 帐户,所以登录 Azure 门户,点击创建资源,选择数据库 | Azure Cosmos DB 。
-
This will open the Create Azure Cosmos DB Account page. Fill in the details as shown in the following screenshot and click Review + create. This is the page where we select the API we want to choose, which is the Core (SQL) API in our case:
图 9.7–创建 Azure 宇宙数据库帐户页面
-
一旦账户被创建,导航到蔚蓝公司烟雾数据库账户 | 键。复制 URI 和主键值。
-
打开命令行,使用以下命令创建控制台应用:
dotnet new console –output EcommerceSample
-
导航到
EcommerceSample
文件夹,使用以下命令安装 Azure Cosmos DB SDK】 -
打开
Program.cs
并将以下静态变量添加到Program
类,该类将保存在步骤 3 中复制的 URI 和主键值:private static readonly string Uri = "YOUR URI HERE"; private static readonly string PrimaryKey = "YOUR PRIMARY KEY HERE";
-
现在,让我们添加代码来创建一个
CosmosClient
类的对象,并使用它来创建一个 Azure Cosmos DB 数据库。随后,该对象将用于与我们的 Azure Cosmos DB 数据库进行通信。当CosmosClient
实现IDisposable
时,我们将在using
块内创建它,以便在using
块后自动处理该对象。运行此代码并导航到 Azure 门户中的 Azure Cosmos 资源 | 数据资源管理器后,可以看到将创建一个名为Ecommerce
的数据库。由于我们已经使用核心(SQL)应用编程接口创建了 Azure Cosmos DB 帐户,该数据库将支持 SQL 语法查询:using (CosmosClient cosmosClient = new CosmosClient(Uri, PrimaryKey)) { DatabaseResponse createDatabaseResponse = await cosmosClient.CreateDatabaseIfNotExistsAsync ("ECommerce"); Database database = createDatabaseResponse.Database; }
-
Now, let's create a container that is analogous to a table in SQL by adding the following code after
createDatabaseResponse
. As we are usingCreateDatabaseIfNotExistsAsync
to create the database, running the same code will not cause any exceptions. Once we run this code, we can see in the Azure portal that a container with the nameProducts
is created under theEcommerce
database:var containerProperties = new ContainerProperties ("Products", "/Name"); var createContainerResponse = await database.CreateContainerIfNotExistsAsync(containerProperties, 10000); var productContainer = createContainerResponse. Container;
在前面的代码中,我们在创建容器的时候传递了
ContainerProperties
,可以看到其中一个值就是Name
,无非就是一个分区键。
分区是 Azure Cosmos DB 的关键特性之一,它根据分区键将容器内的数据分隔成多个逻辑分区。使用分区键,Azure Cosmos DB 实现了数据库的水平扩展,从而满足了应用的可伸缩性和性能需求。选择分区键是一个关键的设计决策,因为它将极大地帮助数据库扩展和更好地运行。此外,分区键不能更改,必须在创建容器时定义。选择分区键时,请记住以下几点:
-
应该具有最大数量的唯一值;唯一值的数量越多,分区就越好。例如,如果我们正在为产品创建容器,产品标识或名称可能是分区键,因为这两个属性可以唯一地标识大多数产品。在兜帽下,如果为分区键选择了一个产品名称,并且内部有 100 个产品,则它在 Azure Cosmos DB 中由 100 个逻辑容器表示。这里,产品类别也可以是一个分区键,但是在选择它作为分区键之前,我们需要评估样本数据并根据需求来决定。
-
If there is no obvious unique choice, we can pick the most used field in the filtering query, so basically, a column that is very often used in the
where
clause.小费
在现实应用中,应该使用 ARM 模板来实现 Azure Cosmos DB 帐户的创建,以便模板可以轻松地与连续部署 ( 光盘)集成。
有了这个,让我们在我们的产品容器中添加一些数据并进行查询:
-
We will add this entity based on the following sample JSON. Based on the product category, there could be different attributes.
例如,如果产品类别为
Books
,则Authors
、Format
等字段中会有值;但是,如果类别是Clothing
,则有诸如Size
和Color
等字段的值。这个模式可以在我们的电子商务应用中重用:{ "Id": "Book.1", "Name": "Mastering enterprise application development Book", "Category": "Books", "Price": 100, "Quantity": 100, "CreatedDate": "20-02-2020T00:00:00Z", "ImageUrls": [], "Rating": [ {"Stars": 5, "Percentage": 95}, {"Stars": 4, "Percentage": 5} ], "Format": ["PDF","Hard Cover"], "Authors": ["Rishabh Verma","Neha Shrivastava", "Ravindra Akela","Bhupesh Guptha"], "Size": [], "Color": [] }
-
现在,让我们创建普通旧 CLR 对象 ( POCOs )可以序列化到前面的 JSON。我们需要两个类来表示它们:一个用于产品,一个用于评级,这是
Product
的子类。在带有核心(SQL) API 的 Azure Cosmos DB 中,任何实体的必备字段之一是id
字段,它有点像主键。因此,我们的父模型有必要定义id
字段。这些类如下所示:public class Rating{ public int Stars { get; set; } public int Percentage { get; set; } } public class Product{ [JsonProperty(PropertyName = "id")] public string ProductId { get; set; } public string Name { get; set; } public string Category { get; set; } public int Price { get; set; } public int Quantity { get; set; } public DateTime CreatedDate { get; set; } public List<string> ImageUrls { get; set; } public List<Rating> Rating { get; set; } public List<string> Format { get; set; } public List<string> Authors { get; set; } public List<int> Size { get; set; } public List<string> Color { get; set; } }
-
现在,让我们创建以下
Product
类的对象,并将其插入数据库:Product book = new Product() { ProductId = "Book.1", Category = "Books", Price = 100, Name = "Mastering enterprise application development Book", Rating = new List<Rating>() { new Rating { Stars = 5, Percentage = 95 }, new Rating { Stars = 4, Percentage = 5 } }, Format = new List<string>() { "PDF", "Hard Cover" }, Authors = new List<string>() { "Rishabh Verma", "Neha Shrivastava", "Ravindra Akela", "Bhupesh Guptha" } };
-
Now, we will call the
CreateItemAsync
method using theproductContainer
object, as shown in the following code snippet. Also, we should ensure that an object with the sameProductId
value isn't already present:try { // Check if item it exists. ItemResponse<Product> productBookResponse = await productContainer.ReadItemAsync<Product>(book.ProductId, new PartitionKey(book.Name)); } catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) { ItemResponse<Product> productBookResponse = await productContainer.CreateItemAsync<Product>(book, new PartitionKey(book.Name)); Console.WriteLine($"Created item {productBookResponse.Resource.ProductId}"); }
一旦我们运行了这段代码,数据就应该被插入到
Products
容器下的Ecommerce
数据库中。 -
如果我们想查询这个记录,我们可以使用下面的代码来查询数据库。如您所见,语法非常类似于从 SQL 数据库中查询数据:
string getAllProductsByBooksCAtegory = "SELECT * FROM p WHERE p.Category = 'Books'"; QueryDefinition query = new QueryDefinition(getAllProductsByBooksCAtegory); FeedIterator<Product> iterator = productContainer.GetItemQueryIterator<Product>(query); while (iterator.HasMoreResults) { FeedResponse<Product> result = await iterator.ReadNextAsync(); foreach (Product in result) { Console.WriteLine($"Book retrived – {product.Name}"); } }
类似地,ContainerClass
提供了可用于各种 CRUD 操作的所有相关方法。所有那些 api 都可以在这里找到:https://docs . Microsoft . com/en-us/dotnet/API/Microsoft . azure . cosmos . container?view=azure-dotnet 。
在此基础上,我们将设计我们的电子商务应用所需的数据模型以及各种 API 要消费的相关数据服务层。到目前为止,我们已经看到了 SQL 和 NoSQL 提供程序。让我们看看还有哪些其他选项可以保存数据。
天青储
Azure Storage 是一个高度可用且可扩展的数据存储,支持以各种格式存储数据,包括文件。Azure Storage 主要支持以下四种类型的数据:
- Azure 表:支持持久化无模式数据的 NoSQL 实现。
- Azure Blob:Blob 是非结构化数据,适用于需要上传、下载或流式传输大量文件的应用。
- Azure 队列:这个允许你以任何可序列化的格式对消息进行排队,然后由服务进行处理。队列非常适合具有大量服务对服务通信的场景,并且充当消息的持久层。
- Azure 文件/Azure 磁盘:用于文件的数据存储,非常适合基于本地文件应用编程接口构建的系统。
以下几点使 Azure 存储成为应用开发的重要组成部分之一:
- 高可用性:存储在 Azure Storage 中的数据为跨数据中心/区域的复制提供了开箱即用的支持,这进一步确保了一个区域的硬件故障不会导致数据丢失。
- 性能:对 CDN 集成的现成支持,有助于从离用户和更近的位置(边缘服务器)缓存和加载数据(尤其是静态文件),进一步提高性能。除此之外,存储类型可以升级为高级存储,利用固态硬盘进一步加快磁盘输入/输出并提高性能。
- 完全管理:硬件由 Azure 完全管理,用于任何更新/维护。
- 安全性:磁盘上存储的所有数据都是加密的,对 Azure Storage 中数据的访问进一步支持私有、公共和匿名模式。
- 现收现付:和所有其他 Azure 服务一样,Azure Storage 也支持基于数据/运营规模的现收现付模式。
Azure 存储帐户
让我们创建一个简单的控制台应用,将文件上传到 Blob 并从 Blob 下载文件。要与 Azure Storage 服务通信,先决条件是创建一个 Azure Storage 帐户,该帐户提供对所有 Azure Storage 服务的访问,并通过 Azure Storage 的唯一命名空间,使我们能够通过 HTTP/HTTPS 访问存储在 Azure Storage 中的数据。要创建 Azure 存储帐户,请执行以下步骤:
-
Sign in to the Azure portal, click Create resource, and select Storage Account. This will open the Create storage account page. Fill in the details as shown in the following screenshot and click Review + create:
图 9.8–创建 Azure 存储帐户
标准层有两个可能值的重要属性,对于账户类,我们有:
- StorageV2(通用 v2) :最新版本的账户类型,允许访问所有存储类型,如文件、blobs、队列等。这对于新创建的存储帐户更为可取。
- 存储(通用 v1) :旧版本的账户类型,允许访问所有存储类型,如文件、博客、队列等。
- 【blob 存储:只支持 blob 存储的账户类型。
另一个是复制,支持跨数据中心/区域的存储数据复制。以下屏幕截图显示了可能的值:
图 9.9–Azure 存储帐户中的复制选项
-
创建账户后,导航至存储账户 | 键。复制连接字符串值。
-
创建新的.NET 5 控制台应用并安装
Azure.Storage.Blobs
NuGet 包。 -
要将内容上传到 Azure Storage,我们需要首先创建一个容器。如果容器不存在,我们将使用
Azure.Storage.Blobs.BlobContainerClient
类及其CreateIfNotExistsAsync
方法来创建容器。有了这个,更新Main
方法,如下面的代码片段所示:static async Task Main(string[] args) { string connectionString = "CONNECTION_STRING"; string containerName = "fileuploadsample"; string blobFileName = "sample.png"; // Upload file to blob BlobContainerClient containerClient = new BlobContainerClient(connectionString, containerName); await containerClient.CreateIfNotExists Async(PublicAccessType.None); //Making blob private. }
-
接下来,我们需要将文件上传到我们将使用
Azure.Storage.Blobs.BlobClient
的容器中,该容器将连接字符串、容器名称和 blob 名称作为输入参数。对于这个示例,我们正在将一个本地sample.png
文件上传到 blob,我们将使用FileStream
类读取该文件,并将其传递给Azure.Storage.Blobs.BlobClient
类的UploadAsync
方法。在Main
方法中创建容器后,添加以下代码片段:BlobClient blobClient = new BlobClient(connectionString, containerName, blobFileName); using FileStream fileStream = File.OpenRead(blobFileName); await blobClient.UploadAsync(fileStream, true); fileStream.Close(); Console.WriteLine(blobClient.Uri.ToString());
在此阶段运行示例会将文件上传到 blob,并在命令行中显示 blob 网址。然而,如果我们试图访问该网址,它将无法访问,因为创建的 blob 是私有的。要访问私有 blobs,我们需要生成一个共享访问签名 ( SAS )并将其作为查询字符串参数传递。为此,在Main
方法中的上传代码后添加以下代码:
BlobSasBuilder sasBuilder = new BlobSasBuilder()
{
BlobContainerName = containerClient.Name,
Resource = "b", // c for container
BlobName = blobClient.Name
};
sasBuilder.ExpiresOn = DateTimeOffset.UtcNow.AddHours(1);
sasBuilder.SetPermissions(BlobContainerSasPermissions.Read);
if (blobClient.CanGenerateSasUri)
{
Uri blobSasUri = blobClient.GenerateSasUri(sasBuilder);
Console.WriteLine(blobSasUri.ToString());
}
Console.ReadLine();
这里,我们使用Azure.Storage.Sas.BlobSasBuilder
类来配置各种参数,如权限和到期时间,为上传的文件生成一个 SAS URI。最后,前面代码的输出如下图所示:
图 9.10–Blob 上传输出和存储资源管理器
这是一个利用 Azure Storage 上传文件的小示例。这可以作为一个应用编程接口进一步增强,最终可以用于文件上传和下载场景。对于我们的电子商务应用,我们将使用 Azure Blob 来存储产品的图像。
注意
有关 Azure 存储的更多高级概念和示例,请参考以下链接:
https://docs . Microsoft . com/en-us/azure/storage/common/storage-account-overview
https://github . com/Azure/Azure-SDK-for-net/tree/master/SDK/storage/Azure。Storage.Blobs/samples
https://docs . Microsoft . com/en-us/azure/cdn/cdn-create-a-storage-account-with-cdn
在本节中,我们已经讨论了中可用的各种数据提供程序.NET 5。然而,简化持久化数据的一个重要库是 EF。让我们看看如何将 EF 集成到.NET 5 应用。
与英孚核心合作
EF Core 是一个 ORM,推荐给任何使用关系数据库作为数据存储的 ASP.NET Core 5 应用。早些时候,我们看到在 ADO.NET,我们必须创造Connection
、Command
和Reader
物体。EF 通过提供抽象并允许开发人员编写应用代码来简化这个过程,并且像任何其他 ORM 一样,EF 帮助使用对象模型范式在数据库上执行各种操作。
配置 EF Core 就像安装所需的 NuGet 包一样简单,在Startup
类中注入所需的服务,然后在需要的地方使用它们。作为这个过程的一部分,需要定义的关键类之一是数据库上下文,它需要继承Microsoft.EntityFrameworkCore.DbContext
类。让我们看看如何在剩下的英孚核心配置中做到这一点。
配置和查询
英孚核心中的DbContext
类保存了我们的应用与数据库通信所需的所有抽象,因此需要集成英孚核心的一个关键设置是定义我们特定于应用的上下文类。该类将主要保存DbSet
类型的公共属性形式的所有 SQL 表/视图,如以下代码所示:
public virtual DbSet<Employee> Employees { get; set; }
这里,Employee
是表示我们数据库中的表的 POCO 类。应用上下文类应该具有接受DbContextOptions
或DbContextOptions<T>
并将其传递给基类的参数化构造函数。
让我们基于 Razor Pages 和 SQLite 创建一个简单的 web 应用,并使用 EF Core 读取数据。对于此示例,我们将使用 SQLite 获取一个简单的员工数据库,该数据库包含具有以下数据模型的员工详细信息:
图 9.11–员工数据库模型
如果你之前没有在剃刀页面工作过,那就不用担心;这是一个基于页面的框架,可用于在 ASP.NET Core 5 中构建数据驱动的应用,并在 第 11 章创建 ASP.NET Core 5 网络应用中进行了介绍。
现在,让我们按照以下步骤创建我们的应用:
-
使用命令行中的以下命令创建一个新的 Razor Pages 应用,这会在
EmployeeEF
文件夹内创建一个新的 Razor Pages 应用:dotnet new webapp -o EmployeeEF
-
Navigate to the
EmployeeEF
folder and open it in Visual Studio Code, and then install the following NuGet packages:Microsoft.EntityFrameworkCore.Sqlite
、Microsoft.EntityFrameworkCore.Design
前一个包是 SQLite 的 EF Core 提供程序,后一个包用于使用 EF Core 迁移创建基于 C# POCOs 的数据库。
-
现在,添加
Models
文件夹并添加必要的 POCO 类,如下所示。这些类表示来自图 9.11 :public class Employee { public int EmployeeId { get; set; } public string Name { get; set; } public string Email { get; set; } public ICollection<Address> Address { get; set; } } public class Address { public int AddressId { get; set; } public int EmployeeId { get; set; } public string City { get; set; } public Employee Employee { get; set; } }
的数据库模式
-
这里,数据库表中的所有列都表示为具有相关数据类型的属性。对于诸如外键之类的关系,创建子类型的属性,称为导航属性,该属性的类型由
ICollection
表示,而父类类型的另一个属性在子类中创建。例如,在前面的代码中,这在public Icollection<Address> Addresses
和public Employee Employee
属性中表示,这两个属性定义了Employee
和Address
表之间的外键约束。任何名为ID
或<class name>ID (EmployeeID)
的属性都会被自动视为主键。可以在OnModelCreating
期间使用 Fluent API 或在System.ComponentModel.DataAnnotations
中使用注释来进一步定义约束。有关模型创建的更多示例和详细信息,请参考https://docs.microsoft.com/en-us/ef/core/modeling。 -
添加一个继承自
Microsoft.EntityFrameworkCore.DbContext
的类,并命名为EmployeeContext
。添加以下定义我们的数据库上下文的代码:public class EmployeeContext : DbContext { public DbSet<Employee> Employees { get; set;} public DbSet<Address> Addresses { get; set;} public EmployeeContext (DbContextOptions <EmployeeContext> options) : base(options) { } protected override void OnModelCreating (ModelBuilder modelBuilder) { modelBuilder.Entity<Employee>().ToTable ("Employee"); modelBuilder.Entity<Address>().ToTable ("Address"); } }
-
在
appsettings.json
中添加连接字符串。当我们使用 SQLite 时,在数据源中指定文件名应该足够好。但是,这将根据提供商而改变:"ConnectionStrings": { "EmployeeContext": "Data Source=Employee.db" }
-
现在,在
Startup
类中注入数据库上下文类,以便它在整个应用中可用。在这里,我们还传递连接字符串,并配置任何附加选项,如重试策略、查询日志记录等。在ConfigureServices
方法中添加以下代码:Services.AddDbContext<EmployeeContext>(options => { options.UseSqlite(Configuration.GetConnectionString("EmployeeContext")); });
我们几乎完成了英孚核心的设置。现在,让我们创建一些可用于播种数据库的示例数据:
-
为此,我们将在数据库上下文中创建一个扩展方法,并在启动时调用它。创建一个
DbContextExtension
类,并向其中添加以下代码。这段代码什么都不做但是在数据库中添加了一些记录:public static void SeedData(this EmployeeContext context) { SeedEmployees(context); } private static void SeedEmployees(EmployeeContext context) { if (context.Employees.Any()) { return; } var employees = new Employee[] { new Employee{EmployeeId = 1, Name = "Sample1", Email="Sample@sample.com"}, new Employee{EmployeeId = 2, Name = "Sample2", Email="Sample2@sample.com"}, }; context.Employees.AddRange(employees); var adresses = new Address[] { new Address{AddressId = 1, City = "City1", EmployeeId = 1}, new Address{AddressId = 2, City = "City2", EmployeeId = 1}, new Address{AddressId = 3, City = "City1", EmployeeId = 2}, }; context.Addresses.AddRange(adresses); context.SaveChanges(); }
-
打开
Startup
,在Configure
方法中,添加以下代码,在应用启动时播种数据。由于我们在插入之前检查了雇员表,应用的多次运行不会覆盖数据:using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope()) { using (var context = serviceScope. ServiceProvider.GetRequiredService <EmployeeContext>()) { context.SeedData(); } }
-
现在,运行
dotnet build
并修复任何构建错误。为了从我们的模型中生成数据库并填充数据库,我们需要在本地或全局安装dotnet-ef
并运行如下迁移命令,这将生成Migrations
文件夹,然后生成Employee.db
文件,这是我们的 SQLite 数据库:Dotnet tool install –global dotnet-ef //Installing dotnet ef. Dotnet ef migrations add InitialCreate //Generate DB migrations. Dotnet ef database update //Update database.
-
现在,到阅读
Employee
表,导航到Index.cshtml.cs
并粘贴以下代码。这里我们正在注入EmployeeContext
然后从员工表中读取数据:public class IndexModel : PageModel { private readonly EmployeeContext context; public IndexModel(EmployeeContext context) { this.context = context; } public Ilist<Employee> Employees { get; set; } public async Task OnGetAsync() { this.Employees = await this.context. Employees.Include(x => x.Addresses). AsNoTracking().ToListAsync(); } }
-
Update
Index.cshtml
with the following code, which loops through the employee records populated in theEmployees
property ofIndexModel
and displays them:<table class="table"> <tbody> @foreach (var item in Model.Employees) {<tr> <td>@Html.DisplayFor(modelItem => item.EmployeeId)</td> <td>@Html.DisplayFor(modelItem => item.Name)</td> <td>@Html.DisplayFor(modelItem => item.Email)</td> <td> @foreach (var address in item.Address) { @Html.DisplayFor(modelItem => address.City) @Html.DisplayName(" ") } </td> </tr> } </tbody> </table>
运行这段代码后,我们可以在浏览器中看到以下输出:
图 9.12–员工应用输出
类似地,DbContext
类中还有其他可用的方法,如Add()
、Remove()
和Find()
,来执行各种 CRUD 操作,还有方法,如FromSqlRaw()
来执行原始 SQL 查询或存储过程。
这是一个非常简单的例子,它的主要目的是展示 EF Core 对于现实应用的能力。我们可以使用包含所有 CRUD 方法的通用存储库和特定存储库的存储库模式来对表执行专门的查询。此外,一个工作模式单元可以用于事务。
代码优先还是数据库优先
在之前的示例中,我们有新创建的概念验证操作系统,并由此生成了一个数据库,这种从概念验证操作系统生成数据库的方式被称为代码优先方法。正如定义所示,我们首先定义了概念验证操作系统,然后生成数据库。
然而,很多时候,尤其是在迁移场景中或者有专门数据库团队的情况下,我们需要从数据库表中生成概念验证操作系统。英孚核心通过数据库优先方法支持这样的场景,其中模型和应用数据库上下文类是从现有数据库生成的。从数据库模型生成概念验证操作系统的过程被称为支架。在这种方法中,我们可以使用.NET CLI 或 Visual Studio 中的包管理器控制台,并使用Scaffold-DbContext
命令,该命令接受各种参数,如数据库连接字符串和应用数据库上下文类的名称,然后生成 EF Core 所需的所有必需类。
其余的配置与代码优先的方法相同。带有各种参数的示例脚手架命令如下所示:
Scaffold-DbContext "Data Source=.;Initial Catalog=Employee.DB;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -Namespace Api.Data.Models -ContextNamespaceApi.Data -ContextDir Api.Data/Abstraction -Context EmployeeContext -Force
在这个命令中,我们正在读取一个数据库Employee.DB
,生成Namespace Api.Data.Models
内的所有模型,生成Api.Data/Abstraction
内的上下文,并命名上下文EmployeeContext
。在数据库优先中,类之间的关系是使用 Fluent API 定义的,而不是注释。
这里有一件事就是每次我们运行这个命令,所有的 POCOs 都会和应用上下文类一起被覆盖。其次,这个命令生成一个带有protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
方法的上下文类。仅当上下文类需要维护连接字符串和其他 EF Core 选项时,才需要此方法。然而,在大多数现实世界的应用中,连接字符串是在appsettings.json
中维护的,而 EF Core 是在Startup
类中配置的,因此可以删除该方法。
这意味着每次脚手架搭建之后都要进行清理,避免任何定制的更好方法是为我们的应用数据库上下文创建一个分部类,并在那里进行所有定制,例如为存储过程添加特定的模型或定义任何特定于应用的约束。这样,无论何时我们构建应用,定制都不会被覆盖,这仍然允许我们从数据库自动生成类。
选择数据库优先方法还是代码优先方法完全取决于开发团队,因为这两种方法各有利弊,并且没有任何特定的特性在一种方法中可用但在另一种方法中不可用。
注意
Scaffold-DbContext
支持多参数;例如,您可以指定–一个模式,用于为模式生成概念验证操作,等等。详情请参考https://docs . Microsoft . com/en-us/ef/core/management-schemas/scaffolding?tab = dotnet-core-CLI。
基于这种理解,让我们在下一节中创建将在我们的企业应用中使用的数据访问服务。
使用 Azure Cosmos DB 设计数据访问服务
由于 NoSQL 数据库都是关于快速访问和高可伸缩性的,NoSQL 的模式是非规范化的,因此存在很大的数据冗余可能性。让我们将需求从 第 1 章设计和架构企业应用映射到各个实体。下图显示了体系结构中各种服务的快速更新:
图 9.13–电子商务应用中的服务
为了更容易理解,我们将在进入概念验证之前在 JSON 中表示实体:
-
用户容器:这个容器会保存所有的用户档案信息,比如姓名和地址。对于此容器,
Email
字段用作分区键:{ "Id": "1", "Name": "John", "Email": "John@xyz.com", "Address":[{"Address1":"Gachibowli","City": "Hyderabad","Country":"India"}], "PhoneNumber":12345 }
-
产品容器:产品容器用于浏览产品并保存相关字段,支持分类搜索和到货日期排序。该模式将是我们在前面的示例中使用的模式,并且
Name
字段用作分区键。 -
订单容器:订单容器将存储特定用户的所有历史订单及其状态。该容器将保存具有相关状态的购物车,并在下订单后更新状态。对于此容器,
Id
字段用作分区键:{ "Id": "1", "UserId": "1", "Products": [{"Id":"1","Name": "T-Shirt","Quantity": 1,"Price": 10}], "OrderStatus" : "Processed", "OrderPlacedDate" : "20-02-2020T00:00:00Z", "ShippingAddress": {"Address1":"Gachibowli", "City":"Hyderabad","Country":"India"}, "TrackingId": 1, "DeliveryDate":"28-02-2020T00:00:00Z" }
-
发票容器:发票容器将保存与特定订单的发票相关的所有信息。对于此容器,
Id
字段用作分区键:{ "Id": "1", "OrderId": "1", "PaymentMode": "Credit Card", "ShippingAddress": {"Address1":"Gachibowli", "City":"Hyderabad","Country":"India"}, "SoldBy": {"SellerName": "Seller1", "Email": "seller@ecommerce.com", "Phone": "98765432"}, "Products": [{"Id":"1", "Name": "T-Shirt", "Quantity": 1, "Price": 10}] }
Product
和Order
的组合如下图所示:
图 9.14–电子商务数据库模型的产品和订单模式
正如你看到的,所有 1:N 的关系都是通过将子项目嵌入一个数组来处理的。类似地,Invoice
和User
实体模式如下图所示:
图 9.15–电子商务数据库模型的发票和用户模式
在我们的企业应用中,我们将有一个服务与 Azure Cosmos DB 数据库交互。这项服务包括以下三个项目,下面将对此进行解释:
Packt.Ecommerce.Data.Models
Packt.Ecommerce.DataStore
Packt.Ecommerce.DataAccess
第一个项目是Packt.Ecommerce.Data.Models
,它是一个. NET 标准 2.1 库,包含了我们所有与数据库通信的概念验证操作系统。正如前面所讨论的,所有的概念验证对象都有一个共同的id
属性和前面章节中 JSON 模式中描述的其他属性。
小费
如果有示例 JSON,我们可以在 C#类生成工具中使用 JSON。
Packt.Ecommerce.DataStore
是. NET Standard 2.1 库,是保存通用存储库和实体特定存储库的存储库层。这个项目中一个重要的类是BaseRepository
,它有以下方法,每个方法调用CosmosClient
类各自的方法:
GetAsync(string filterCriteria)
:基于filterCriteria
从容器中获取记录。如果filterCriteria
为空,则检索该容器中的所有记录。GetByIdAsync(string id, string partitionKey)
:这个方法有助于通过容器的 ID 和分区键从容器中检索任何记录。AddAsync(Tentity entity, string partitionKey)
:这个方法允许我们将记录插入容器。ModifyAsync(Tentity entity, string partitionKey)
:这个方法允许我们在容器中UPSERT
(如果有记录,修改,否则插入)一个记录。RemoveAsync(string id, string partitionKey)
:该方法允许从容器中删除记录。
由于在 Azure Cosmos DB 中,每个记录都由 ID 和分区键的组合唯一标识,因此所有这些方法都接受分区键和id
。由于这是一个通用存储库,类的签名如下,这允许我们为应用传递任何 POCO,并在相应的容器上执行 CRUD 操作:
public class BaseRepository<Tentity> : IbaseRepository<Tentity>
where Tentity : class
所有这些方法都需要一个Microsoft.Azure.Cosmos.Continer
对象,我们为其创建一个readonly
私有成员,该成员在类的构造函数中初始化,如下所示:
private readonly Container container;
public BaseRepository(CosmosClient cosmosClient,
string databaseName, string containerName)
{
if (cosmosClient == null)
{
throw new Exception("Cosmos client is
null");
}
this.container = cosmosClient.GetContainer
(databaseName, containerName);
}
现在,CosmosClient
将通过依赖注入进入系统,并在static
类中配置。作为最佳实践,建议在应用的生命周期中只有一个CosmosClient
实例,以更好地重用连接。因此,我们将在 ASP.NET Core 5 依赖注入容器中将其配置为单例。我们过一会儿再谈这个。
回到存储库层,BaseRepository
在以下具体类中被继承,每个存储库代表一个相应的容器:
ProductRepository
UserRepository
OrderRepository
InvoiceRepository
以ProductRepository
为例,它将具有以下实现,其中我们使用Ioptions
模式传递CosmosClient
的单例实例和附加属性:
public class ProductRepository :
BaseRepository<Product>, IproductRepository
{
private readonly Ioptions<DatabaseSettingsOptions>
databaseSettings;
public ProductRepository(CosmosClient,
Ioptions<DatabaseSettingsOptions>
databaseSettingsOption)
: base(cosmosClient, databaseSettingsOption.
Value.DataBaseName, "Products")
{
this.databaseSettings = databaseSettingsOption;
}
}
所有的其他存储库将遵循类似的结构。每个存储库将实现自己的接口来支持依赖注入。
注意
这些存储库将随着我们应用实现的进展而发展。
下一个项目是Packt.Ecommerce.DataAccess
,是一个以 Web API 为目标的项目.NET 5,并将主要拥有所有的控制器来公开我们的存储库。每个存储库都是与相应控制器的 1:1 映射。例如,会有ProductsController
将ProductRepository
方法公开为一个 REST 应用编程接口。所有控制器都将使用构造函数注入来实例化它们相应的存储库。Packt.Ecommerce.DataAccess
中很重要的一点就是 Azure Cosmos DB 数据库的配置。各种控制器的设计将非常类似于Packt.Ecommerce.Product
网络应用编程接口的设计,这将在 第 10 章中讨论,创建 ASP.NET Core 5 网络应用编程接口。
首先,我们将在appsettings.json
中有一个对应的部分,如下所示:
"CosmosDB": {
"DataBaseName": "Ecommerce",
"AccountEndPoint": "",
"AuthKey": ""
}
注意
对于本地开发环境,我们将使用管理用户机密,这里解释一下:https://docs . Microsoft . com/en-us/aspnet/core/security/app-Secrets?视图=aspnetcore-5.0 &选项卡=窗口。我们将设置以下值:
{
"CosmosDB:AccountEndPoint": "", //Cosmos DB End Point
"CosmosDB:AuthKey": "" //Cosmos DB Auth key
}
注意
但是,一旦部署了服务,就应该使用 Azure Key Vault,如中的 第 6 章配置中所述。净芯。
我们将定义一个包含依赖注入映射的扩展类。这里显示了其中的一个片段:
public static class RepositoryExtensions
{
public static IserviceCollection
AddRepositories(this IserviceCollection services)
{
services.AddScoped<IproductRepository,
ProductRepository>();
return services;
}
}
类似地,所有的存储库都会被映射。然后,我们将通过在ConfigureServices
方法中添加以下代码,在Startup
类和 Azure Cosmos DB 配置中对此进行配置:
services.AddOptions();
services.Configure<DatabaseSettingsOptions>(this.Configuration.GetSection("CosmosDB"));
string accountEndPoint = this.Configuration.GetValue<string>("CosmosDB:AccountEndPoint");
string authKey = this.Configuration.GetValue<string>("CosmosDB:AuthKey");
services.AddSingleton(s => new CosmosClient(accountEndPoint, authKey));
services.AddRepositories();
一旦完成配置,该服务就可以在其他服务中使用,例如Products
、Orders
和Invoice
。这个库将拥有所有必要的 REST APIs 来对各种实体执行 CRUD 操作。
这结束了对各种实体执行 CRUD 操作的数据访问服务的创建,所有操作都作为 API 公开。该服务将从我们将在 第 10 章中开发的所有其他服务中调用,创建 ASP.NET Core 5 网络应用编程接口。
总结
在本章中,我们了解了中提供的各种持久性选项.NET 5,从处理文件和目录的 API 到数据库,如微软 SQL Server 和 Azure Cosmos DB。
我们还了解了表单、表单的重要性,以及如何在使用微软 SQL Server 时使用 EF Core 构建持久层。在此过程中,我们使用 Azure Cosmos DB SDK 为我们的电子商务应用构建了一个数据访问层。一些关键要点是我们在 SQL 和 NoSQL 之间做出的设计决策,以及我们如何用应用逻辑抽象数据层和用户界面层,以帮助您构建可扩展的企业应用。
在下一章中,我们将研究 RESTful APIs 的基础和 ASP.NET Core 5 Web API 的内部结构,并进一步为电子商务应用构建各种 RESTful 服务。
问题
-
Say you are migrating an existing web application to use EF Core; however, there isn't any change in the database schema and an existing one can be used as is. What is the preferable mode to use EF Core?
a.数据库优先
b.代码优先
c.两者
-
If we are building a recommendation system for our e-commerce application and we are using Azure Cosmos DB, what API is best recommended in this scenario?
a.核心应用编程接口
b.蒙古应用编程接口
c.卡珊德拉应用编程接口
d.格雷林图形应用编程接口
-
I created a container in SQL API-based databases to store user profile information and defined
Email
as the partition key. My system has 100 unique emails. How many logical partitions will my container have?a.1.
b.0.
c.100.
d.Azure Cosmos DB 不支持逻辑分区。
-
Which of the following classes is derived from the
TextWriter
class?a.
StreamWriter
b.
BinaryWriter
c.
XMLWriter
进一步阅读
下面提供了一些链接来进一步理解本章的主题:
- https://docs.microsoft.com/en-us/ef/core/saving/transactions
- https://docs.microsoft.com/en-us/ef/core/performance/advanced-performance-topics
- https://docs.microsoft.com/en-us/aspnet/core/security/gdpr?view=aspnetcore-5.0
十、创建 ASP.NET Core 5 网络应用编程接口
近年来,web 服务已经成为 web 应用开发的重要组成部分。随着需求的不断变化和业务复杂性的增加,松散耦合 web 应用开发中涉及的各种组件/层非常重要,没有什么比将应用的用户界面部分与核心业务逻辑解耦更好的了。这就是使用 RESTful 方法的 web 服务的简单性帮助我们开发可扩展的 web 应用的地方。
在本章中,我们将学习如何使用ASP.NET核心网络应用编程接口来构建 RESTful 服务,同时,我们将构建电子商务应用所需的所有应用编程接口。
在本章中,我们将详细介绍以下主题:
- 表征状态转移介绍 ( REST )
- 了解 ASP.NET Core 5 网络应用编程接口的内部结构
- 使用控制器和动作处理请求
- 与数据层的集成
- 了解 gRPC
技术要求
这一章,你将需要 C#的基础知识.NET Core、web APIs、HTTP、Azure、依赖注入、邮递员等.NET 命令行界面。
本章的代码可以在这里找到:https://github . com/PacktPublishing/Enterprise-Application-Development-with-C-Sharp-9-and-.NET-5/树/主/第 10 章/测试程序。
更多代码示例,请参考以下链接:https://github . com/PacktPublishing/Enterprise-Application-Development-with-C-Sharp-9-and-.NET-5/树/主/企业% 20 应用
表征状态转移介绍
表示状态转移 ( REST )是一个用于构建 web 服务的架构指南。首先,它定义了一组在设计 web 服务时可以遵循的约束。主要的 REST 方法之一是建议 API 应该围绕资源进行设计,并且应该与媒体和协议无关。应用编程接口的底层实现独立于使用该应用编程接口的客户端。
考虑我们的电子商务应用的一个例子,假设我们正在使用产品的搜索字段在用户界面上搜索产品。应该有一个为产品创建的应用编程接口,在这里,产品只不过是电子商务应用上下文中的一种资源。该 API 的 URI 可能类似于以下内容,其中明确表示我们正在尝试对产品实体执行GET
操作:
GET http://ecommerce.packt.com/products
应用编程接口的响应应该独立于调用应用编程接口的客户端,也就是说,在这种情况下,我们使用浏览器在产品搜索页面上加载产品列表。但是,相同的应用编程接口也可以在移动应用中使用,而无需任何更改。其次,在这种情况下,为了在内部检索产品信息,应用可能使用一个或多个物理数据存储;但是,这种复杂性对客户端应用是隐藏的,API 作为单个业务实体——产品——暴露给客户端。虽然 REST 原则没有规定协议是 HTTP,但是大多数 RESTful 服务都是建立在 HTTP 之上的。基于 HTTP 的 RESTful APIs 的一些关键设计原则/约束/规则如下:
-
确定系统的业务实体,并围绕这些资源设计应用编程接口。在我们的电子商务应用中,我们所有的 API 都围绕着资源,比如产品、订单、支付和用户。
-
REST APIs 应该有一个统一的接口,帮助它独立于客户端。由于所有的应用编程接口都需要面向资源,每个资源都由一个 URI 唯一标识;此外,对资源的各种操作由 HTTP 动词唯一标识,如
GET
、POST
、PUT
、PATCH
和DELETE
。例如,应该使用GET
(http://ecommerce.packt.com/products/1
)来检索 ID 为1
的产品。同样,应该使用DELETE
(http://ecommerce.packt.com/products/1
)来删除产品。 -
由于 HTTP 是无状态的,REST 为 RESTful APIs 规定了许多事情。这意味着 API 应该是原子的,并在同一个调用中完成请求的处理。任何后续请求,即使来自同一个客户端(同一个 IP),都会被视为新请求。例如,如果应用编程接口接受身份验证令牌,它应该接受每个请求的身份验证。无状态的一个主要优势是服务器最终可以实现的可伸缩性,因为客户端可以对任何可用的服务器进行 API 调用,并且仍然会收到相同的响应。
-
除了发回响应之外,应用编程接口还应该利用 HTTP 状态代码和响应头向客户端发送任何附加信息。例如,如果一个响应可以被缓存,那么应用编程接口应该向客户端发送相关的响应头,以便它可以被缓存。响应缓存,在 第 8 章中讨论,理解缓存就是基于这些头。另一个例子是,在成功和失败的情况下,应用编程接口都应该返回相关的 HTTP 状态代码,也就是说,
1xx
表示信息,2xx
表示成功,3xx
表示重定向,4xx
表示客户端错误,5xx
表示服务器错误。 -
Hypermedia As The Engine Of Application State (HATEOAS): APIs should give information about the resource such that client should be easily able to discover them without any prior information relating to the resource. For example, if there is an API to create a product, once a product is created, the API should respond with the URI of that resource so that the client can use that to retrieve the product later.
请参考以下对检索所有产品列表(
GET /products
)的应用编程接口的响应,该应用编程接口具有检索每个产品的更多详细信息的信息:{ "Products": [ { "Id": "1", "Name": "Men's T-Shirt", "Category": "Clothing" "Uri": "http://ecommerce.packt.com/products/1" } { "Id": "2", "Name": "Mastering enterprise application development Book", "Category": "books" "Uri": "http://ecommerce.packt.com/products/2" } ] }
前面的例子是实现HATEOAS原则的一种方式,但是它可以用一种更具描述性的方式来设计,比如包含关于被接受的 HTTP 动词、关系等信息的响应。
REST 成熟度模型
这些是各种各样的指导方针,一个应用编程接口应该遵循这些指导方针,才能使它成为 RESTful。然而,没有必要遵循所有的原则来使它完美地 RESTFUL。更重要的是,应用编程接口应该实现业务目标,而不是 100%符合 REST。RESTful API 设计专家 Leonard Richardson 提出了以下模型来对 API 的成熟度进行分类:
- 0 级:普通旧 XML 的沼泽–任何有一个
POST
URI 来执行所有操作的应用编程接口都属于这一类。一个例子是基于 SOAP 的 web 服务,它只有一个 URI,所有的操作都是基于 SOAP 信封隔离的。 - 级别 1:资源–所有的资源都是由 URI 驱动的,每个资源都有一个专用的 URI 模式的 API 都属于这个成熟度模型。
- 第 2 级:HTTP 动词–除了每个资源有一个单独的 URI,每个 URI 都有一个基于 HTTP 动词的单独动作。如前所述,支持使用相同 URI 和不同 HTTP 动词的
GET
和DELETE
的产品应用编程接口属于这种成熟度模型。大多数企业应用 RESTful APIs 都属于这一类。 - 第 3 级:HATEOAS–API 设计有所有额外的发现信息(资源的 URI,资源支持的各种操作)属于这个成熟度模型。很少有 API 符合这个成熟度级别;然而,正如前面所讨论的,重要的是,我们的 API 应该满足业务目标,并尽可能符合 RESTful 原则,而不是 100%符合但不满足业务目标。
下图说明了理查森的成熟度模型:
图 10.1–理查森的成熟度模型
到目前为止,我们已经讨论了 REST 架构的各种原理。在下一节中,让我们开始使用 ASP.NET Core 网络应用编程接口,我们将在我们的电子商务应用中为其创建各种 RESTful 服务。
了解 ASP.NET Core 5 网络应用编程接口的内部结构
ASP.NET Core是一个统一框架,运行在之上.NET Core,用于开发 web 应用(MVC/Razor)、RESTful 服务(web API)以及最近的基于 web 程序集的客户端应用(Blazor 应用)。ASP.NET Core 应用的基本设计基于模型视图控制器 ( MVC )模式,该模式将代码分为三个主要类别:
- 模型:这是一个 POCO 类保存数据,用于在应用的各个层之间传递数据。层次包括在存储库类和服务类之间传递数据,或者在客户端和服务器之间来回传递信息。该模型主要表示应用的资源状态或域模型,并包含您请求的信息。例如,如果我们想要存储用户简档信息,它可以由 POCO 类
UserInformation
来表示,并且可以包含所有的简档信息。这将进一步用于在存储库和服务类之间传递,也可以在被发送回客户端之前序列化为 JSON/XML。在企业应用中,在与数据层的集成部分为我们的电子商务应用创建模型时,我们会遇到不同类型的模型。 - 控制器:这些是一组类,它们接收所有请求,执行所有需要的处理,填充模型,然后将其发送回客户端。在企业应用中,它们通常利用服务类来处理业务逻辑,并利用存储库来与底层数据存储进行通信。使用 ASP.NET Core 的统一方法,MVC/Razor 应用和 web API 应用都使用同一个
Microsoft.AspNetCore.Mvc.ControllerBase
类来定义控制器。 - 查看:这些是代表 ui 的页面。我们从控制器中检索的所有模型都绑定到视图上的各种 HTML 控件,并呈现给用户。视图通常在 MVC/Razor 应用中很常见;对于 web API 应用,该过程结束于将模型序列化为响应。
因此,在使用 ASP.NET Core 开发的网络应用中,每当请求来自客户端(浏览器、移动应用和类似来源)时,它都会通过 ASP.NET Core 请求管道,到达与数据存储交互的控制器,以填充模型/视图模型,并将它们作为 JSON/XML 形式的响应发送回视图,以进一步绑定响应并将其呈现给用户。正如您所看到的,在控制器不知道任何用户界面方面并在当前上下文中执行业务逻辑并通过模型做出响应的情况下,存在明显的关注点分离,而另一方面,视图接收模型并使用它们在 HTML 页面中向用户呈现它们。这种关注点的分离很容易帮助单元测试应用,以及根据需要维护和扩展它。MVC 模式不仅适用于 web 应用,而且可以用于任何需要分离关注点的应用。
由于本章的重点是构建 RESTful 服务,因此我们将在本章中重点介绍 ASP.NET Core Web API,并在 第 11 章中讨论 ASP.NET MVC 和 Razor 页面创建 ASP.NET Core 5 Web 应用。
为了开发 RESTful 服务,有很多可用的框架,但以下是继续使用 ASP.NET Core 的一些优势.NET 5:
-
跨平台支持:不像 ASP.NET,以前是.NET 框架,它与 Windows 操作系统相结合,ASP.NET Core 现在是应用的一部分,从而消除了平台依赖性,使其兼容所有平台。
-
高度可定制的请求 管道使用中间件和支持注入各种开箱即用的模块,比如日志和配置。
-
Out-of-the-box HTTP server implementation, which can listen to HTTP requests and forward them to controllers. The server implementation includes cross-platform servers such as Kestrel, and platform-specific servers such as
IIS
andHTTP.sys
.注意
默认情况下,红隼是 ASP.NET Core 模板中使用的 HTTP 服务器;但是,这可以根据需要被覆盖。
-
强大的工具支持,以 VS Code、Visual Studio 和 DOTNET CLI 的形式,加上项目模板,意味着开发人员只需很少的设置就可以开始着手实现业务逻辑。
-
最后,整个框架是开源的,可在https://github.com/aspnet/AspNetCore获得。
所以现在我们知道为什么选择 ASP.NET Core 作为我们开发 RESTful 服务的框架了。现在让我们研究一些有助于执行请求的关键组件,并使用以下命令创建一个示例 web API:
dotnet new webapi -o TestApi
前面的命令成功执行后,让我们导航到TestApi
文件夹,在 Visual Studio 2019 或 VS Code 中打开,查看生成的各种文件,如下图截图所示:
图 10.2–用 VS 代码测试网络应用编程接口项目
这里可以看到几个类,比如Program
、Startup
,还有设置文件,比如appsettings.json
,用来运行一个 web API 项目,还有WeatherForecast
,是控制器类中使用的一个模型类。让我们在下面的部分中检查测试应用编程接口的每个组件。
程序和启动类
程序和启动类是用于引导 ASP.NET Core 中的网络应用编程接口项目的类。让我们按照以下步骤来看看程序类执行的活动:
-
Program
类是我们的网络应用编程接口的入口点,它告诉 ASP.NET Core 每当有人执行网络应用编程接口项目时,就从Main
方法开始执行。首先,这是用于引导应用的类。如您所见,它遵循控制台应用的典型惯例,具有static void Main
方法和.NET Core 寻找开始执行的主要方法。类似地,在 ASP.NET Core 应用中,Program
类有这个static void Main
方法,ASP.NET Core 运行时寻找这个方法来开始应用的执行。 -
在
Main
方法中,我们有CreateHostBuilder(args).Build().Run()
,它调用CreateHostBuilder
方法来获取IHostBuilder
的实例,对于我们的应用来说,这个实例只不过是宿主。前面,我们讨论了这样一个事实,即 ASP.NET Core 附带了一个内置的 HTTP 服务器实现和各种中间件来插入,主机只不过是一个封装这些组件的对象,就像默认为红隼的 HTTP 服务器、所有中间件组件以及注入的任何附加服务,如日志记录。CreateHostBuilder
内部调用CreateDefaultBuilder
和ConfigureWebHostDefaults
。 -
CreateDefaultBuilder
方法加载来自各种提供者的所有配置,例如appsettings.json
、环境变量和任何命令行参数(请参见args
作为参数传递)。然后,加载默认日志记录提供程序,以便将所有日志记录到控制台和调试窗口中。 -
ConfigureWebHostDefaults
启用红隼作为默认 HTTP 服务器,并初始化Startup
类。 -
Additionally, we can call additional methods on both
CreateDefaultBuilder
andConfigureWebHostDefaults
. Let's say, for example, that we wanted to call an additional configuration provider, along the lines of what we implemented in Chapter 6, Configuration in .NET Core, or change some Kestrel default parameters, we can configure additional parameters as shown in the following code snippet:public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>() .ConfigureKestrel((options) => { options.AddServerHeader = false; }); });
从前面的代码中,我们看到最后,在
IhostBuilder
的对象上,应用了以下内容:- 调用
Build()
方法运行动作并初始化Host
。 - 调用
Run()
方法保持Host
运行。
- 调用
现在我们已经将Host
加载了所有默认组件,并且它已经启动并运行。作为其中的一部分,我们已经看到其中的一个步骤是实例化Startup
类。让我们研究一下Startup
类是干什么的,看看它如何被用来注入额外的 ASP.NET Core 类/特定于应用的类(存储库、服务、选项)和中间件。Startup
类如下所示:
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
如您所见,除了IConfiguration
属性之外,Startup
类主要有两种方法,使用构造函数注入在构造函数中初始化为应用配置(该配置在主机设置期间加载)。此属性还可以在Startup
类中用于加载任何配置,如中的 第 6 章配置中所述。网芯。Startup
类中的另外两个方法具有以下功能:
-
ConfigureServices: This method is used to inject any ASP.NET Core provided services so that applications can use those services. A few of the common services that enterprise applications can inject are shown in the following code snippet:
services.AddAuthentication() // To enable authentication. services.AddControllers(); // To enable controllers like web API. services.AddControllersWithViews(); // To enable controller with views. services.AddDistributedMemoryCache(); // To enable distributed caching. services.AddApplicationInsightsTelemetry(appInsightInstrumentKey); // To enable application insights telemetry. services.AddDbContext<EmployeeContext>(options => { options.UseSqlite(Configuration.GetConnectionString("EmployeeContext")); }); // Integrating SQL lite.
除了 Apart 芯提供的服务,我们还可以注入任何针对我们应用的定制服务。例如,
ProductService
可以映射到IProductService
并可用于整个应用。首先,这是我们可以使用将任何东西垂直放入依赖注入 ( DI )容器的地方,如中的 第 5 章 、依赖注入所述.NET 。ConfigureServices
方法的一些要点如下: -
由于这将加载特定于应用的服务,因此该方法是可选的。
-
所有服务,包括 ASP.NET Core 服务和定制服务,都可以接入到应用中,并作为
IServiceCollection
的扩展方法提供。 -
Configure: This method is used to integrate all the middlewares required to be applied to the request pipeline. This method primarily controls how applications respond to the HTTP requests, that is, how applications should respond to exceptions or how they should respond to static files, or how URI routing should happen. All can be configured in this method. Additionally, any specific handling on a request pipeline, such as calling a custom middleware or adding specific response headers, or even defining a specific endpoint, can be implemented here in the
configure
method. So, apart from what we have seen earlier, the following code snippet shows a few common additional configurations that can be integrated in this method:// Endpoint that responds to /subscribe route. app.UseEndpoints(endpoints => { endpoints.MapGet("/subscribe", async context => { await context.Response.WriteAsync("subscribed"); }); }); // removing any unwanted headers. app.Use(async (context, next) => { context.Response.Headers.Remove("X-Powered-By"); context.Response.Headers.Remove("Server"); await next().ConfigureAwait(false); });
这里,
app.UseEndpoints
正在为匹配/subscribe
的 URI 配置响应。app.UseEndPoints
与路由规则一起工作,并在使用控制器和动作处理请求一节中解释,而app.Use
则用于添加内联中间件。在这种情况下,我们将从响应中删除X-Powered-By, Server
响应头。
总而言之,Program
和Startup
类在引导应用,然后根据需要定制应用服务和 HTTP 请求/响应管道方面起着至关重要的作用。现在让我们看看中间件如何帮助定制 HTTP 请求/响应管道。
注意
Configure
和ConfigureServices
方法可以是Program
类的一部分,因为IHostBuilder
、IWebHostBuilder
和WebHostBuilderExtensions
中有可用的方法。然而,使用Startup
类使代码更加清晰。
理解中间件
我们提到中间件已经有一段时间了,所以让我们了解什么是中间件,以及我们如何构建一个中间件并在我们的企业应用中使用它。中间件是拦截传入请求,对请求执行一些处理,然后根据需要将其交给下一个中间件或跳过它的类。中间件是双向的,因此所有的中间件都会拦截请求和响应。让我们假设一个应用编程接口检索产品信息,在这个过程中,它通过各种中间件。用图片的形式表现它们看起来像这样:
图 10.3–中间件处理
每个中间件都有一个Microsoft.AspNetCore.Http.RequestDelegate
实例。使用这个的结果是,中间件调用下一个中间件。通常,流程会按照您希望中间件对请求执行的一些处理逻辑来处理请求,然后调用RequestDelegate
将请求移交给管道中的下一个中间件。
如果我们从制造的角度进行类比,它就像制造过程中的装配线,零件从一个工作站到另一个工作站被添加/修改,直到最终产品被生产出来。在上图中,让我们将每个中间件视为一个工作站,因此它将经历以下步骤:(下面对每个中间件的解释只是对我们理解的假设性解释;这些中间件的内部工作原理与这里解释的略有不同。更多详情可以在这里找到:https://docs . Microsoft . com/en-us/dotnet/API/Microsoft . aspnetcore . builder?view = aspnetcore-3.1&view fallbackfrom = aspnetcore-5.0。
- 使用 HTTP 重定向:一个 HTTP 请求到达
GET/Products
并检查协议。如果请求是通过 HTTP 发送的,则通过 HTTP 状态代码发回重定向;如果请求在 HTTPS,它将被移交给下一个中间件。 - 使用静态文件:如果请求是针对静态文件的(通常根据扩展名 MIME 类型来检测),这个中间件会处理请求并发回响应,否则会将请求交给下一个中间件。在这里,正如您所看到的,如果请求是针对静态文件的,那么管道的其余部分甚至不会被执行,因为这个中间件可以处理完整的请求,从而减少服务器上任何不需要的处理的负载,也减少了响应时间。这个过程也被称为短路,每个中间件都可以支持。
- 用户退出:进一步检查请求,识别能够处理该请求的控制器/动作。如果没有匹配,这个中间件通常会用一个 404 HTTP 状态代码来响应。
- 使用授权:这里,如果控制器/动作需要对认证用户可用,那么这个中间件会在报头中寻找任何有效的令牌并做出相应的响应。
一旦控制器从服务/存储库中获取数据,响应就以相反的顺序通过相同的中间件,即先UseAuthorization
,后UseHttpsRedirection
,并根据需要处理响应。
如前所述,所有中间件都是使用Configure
方法安装的(在Startup
类或Program
类中),并使用实现IApplicationBuilder
接口的类实例的扩展方法进行配置,该接口作为参数传递给Configure
方法。因此,中间件执行的顺序将精确地遵循它在Configure
方法中的配置方式。
有了这种理解,让我们创建一个中间件,它将用于处理我们电子商务应用的 RESTful 服务中的异常,因此我们将创建一个中间件,它将在请求管道的开始安装,然后捕获所有的异常,而不是用try…catch
块污染代码。
构建定制中间件
由于中间件将在所有 RESTful 服务中被重用,我们将把中间件添加到Middlewares
文件夹内的Packt.Ecommerce.Common
项目中。
让我们首先创建一个概念验证类来表示错误情况下的响应。通常,这个模型将保存错误消息,一个在我们的日志存储应用洞察中搜索的唯一标识符,以及一个内部异常(如果需要)。在生产环境中,不应暴露内部异常;但是,对于开发环境,我们可以发送内部异常用于调试目的,我们将使用配置标志在中间件逻辑内部控制这种行为。因此,在此基础上,在Packt.Ecommerce.Common
项目的Models
文件夹中添加一个名为ExceptionResponse
的类文件,并添加以下代码:
public class ExceptionResponse
{
public string ErrorMessage { get; set; }
public string CorrelationIdentifier { get; set; }
public string InnerException { get; set; }
}
现在,创建另一个 POCO 类,它可以保存配置来切换在我们的响应中发送内部异常的行为。此类将使用选项模式填充,该模式已在中的 第 6 章配置中讨论过。网芯。因为它只需要保存一个设置,所以它将有一个属性。在Options
文件夹中添加一个名为ApplicationSettings
的类文件,然后向其中添加以下代码:
public class ApplicationSettings
{
public bool IncludeExceptionStackInResponse { get; set; }
}
这个类将被进一步扩展,用于我们所有 API 中通用的任何配置。
导航到Middlewares
文件夹并创建一个名为ErrorHandlingMiddleware
的类。正如我们所讨论的,任何中间件的关键属性之一都是RequestDelegate
类型的属性。此外,我们将为ILogger
添加一个属性,以将异常记录到我们的日志提供程序中,最后,我们将添加一个bool
includeExceptionDetailsInResponse
类型的属性,以保存控制屏蔽内部异常的标志。这样,ErrorHandlingMiddleware
等级将如下:
public class ErrorHandlingMiddleware
{
private readonly RequestDelegate requestDelegate;
private readonly ILogger logger;
private readonly bool includeExceptionDetailsInResponse;
}
添加一个参数化构造函数,在这里我们为日志提供者注入RequestDelegate
和ILogger
,为配置注入IOptions<ApplicationSettings>
,并将它们分配给前面创建的属性。这里,我们再次依赖 ASP.NET Core 的构造函数注入来实例化相应的对象。有了这个,ErrorHandlingMiddleWare
的构造器会出现如下:
public ErrorHandlingMiddleware(RequestDelegate, ILogger<ErrorHandlingMiddleware> logger, IOptions<ApplicationSettings> applicationSettings)
{
NotNullValidator.ThrowIfNull(applicationSettings, nameof(applicationSettings));
this.requestDelegate = requestDelegate;
this.logger = logger;
this.includeExceptionDetailsInResponse = applicationSettings.Value.IncludeExceptionStackInResponse;
}
最后,添加InvokeAsync
方法,该方法将具有处理请求的逻辑,然后使用RequestDelegate
调用下一个中间件。由于这是异常处理中间件作为我们逻辑的一部分,我们要做的就是将请求包装在try…catch
块中。在catch
块中,我们将使用ILogger
将其记录到各自的日志提供者,最后发送一个对象ExceptionResponse
,作为响应返回。这样,InvokeAsync
会出现如下:
public async Task InvokeAsync(HttpContext context)
{
try
{
if (this.requestDelegate != null)
{
// invoking next middleware.
this.requestDelegate.Invoke(context).ConfigureAwait(false);
}
}
catch (Exception innerException)
{
this.logger.LogCritical(1001, innerException, "Exception captured in error handling middleware"); // logging.
ExceptionResponse currentException = new ExceptionResponse()
{
ErrorMessage = Constants.ErrorMiddlewareLog,
CorrelationIdentifier = System.Diagnostics.Activity.Current?.RootId,
};
if (this.includeExceptionDetailsInResponse)
{
currentException.InnerException = $"{innerException.Message} {innerException.StackTrace}";
}
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonSerializer.Serialize(innerException)).ConfigureAwait(false);
}
}
现在我们可以将这个中间件注入到Startup
类的Configure
方法中,代码如下:
app.UseMiddleware<GlobalExceptionHandlingMiddleware>();
由于这是一个异常处理程序,建议在Configure
方法的开始配置它,以便捕获所有后续中间件中的任何异常。此外,我们需要确保将ApplicationSettings
类映射到一个配置,因此将以下代码添加到ConfigureServices
方法中:
services.Configure<ApplicationSettings>(this.Configuration.GetSection("ApplicationSettings"));
在appsettings.json
增加相关章节:
"ApplicationSettings": {
"IncludeExceptionStackInResponse": true
}
现在,如果我们的任何一个 API 中有任何错误,那么响应将会像下面的代码片段中所示的那样:
{
"ErrorMessage": "Exception captured in error handling middleware",
"CorrelationIdentifier": "03410a51b0475843936943d3ae04240c ",
"InnerException": "No connection could be made because the target machine actively refused it. at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port, CancellationToken cancellationToken)\r\n at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean allowHttp2, CancellationToken cancellationToken)\r\n at System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n at System.Net.Http.HttpConnectionPool.GetHttpConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)\r\n at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n at System.Net.Http.DiagnosticsHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n at Microsoft.Extensions.Http.Logging.LoggingHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n at Microsoft.Extensions.Http.Logging.LoggingScopeHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n at System.Net.Http.HttpClient.FinishSendAsyncBuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts, CancellationToken callerToken, Int64 timeoutTime)\r\n at Packt.Ecommerce.Product.Services.ProductsService.GetProductsAsync(String filterCriteria) in src\\platform-apis\\services\\Packt.Ecommerce.Product\\Services\\ProductsService.cs:line 82\r\n at Packt.Ecommerce.Product.Controllers.ProductsController.GetProductsAsync(String filterCriteria) in src\\platform-apis\\services\\Packt.Ecommerce.Product\\Controllers\\ProductsController.cs:line 46\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)\r\n at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)\r\n at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)\r\n at Packt.Ecommerce.Common.Middlewares.ErrorHandlingMiddleware.InvokeAsync(HttpContext context) in src\\platform-apis\\core\\Packt.Ecommerce.Common\\Middlewares\\ErrorHandlingMiddleware.cs:line 65"
}
从前面的代码片段中,我们可以取CorrelationIdentifier
,也就是03410a51b0475843936943d3ae04240c
,在我们的日志提供程序 Application Insights 中搜索该值,我们可以确定关于该异常的其他信息,如下图所示:
图 10.4–在应用洞察中追踪相关标识符
CorrelationIdentifier
在没有内部异常的生产环境中非常有用。
我们关于中间件的讨论到此结束。在下一节中,让我们看看控制器和动作是什么,以及它们如何帮助处理请求。
使用控制器和动作处理请求
控制器是使用 ASP.NET Core 网络应用编程接口设计 RESTful 服务的基本模块。这些是保存处理请求的逻辑的主要类,包括从数据库中检索数据,将记录插入数据库,等等。控制器是我们定义方法来处理请求的类。这些方法通常包括验证输入、与数据存储对话、应用业务逻辑(在企业应用中,控制器也会调用服务类),最后将响应序列化,并以 JSON/XML 形式使用 HTTP 协议发送回客户端。所有这些包含处理请求逻辑的方法都被称为动作。HTTP 服务器接收的所有请求都通过路由引擎传递给动作方法。然而,路由引擎根据可以在请求管道中定义的特定规则将请求转移到动作。这些规则就是我们在路由中定义的。让我们看看 URI 是如何映射到控制器中的特定动作的。
了解 ASP.NET Core 路由
到目前为止,我们已经看到任何 HTTP 请求都要经过中间件,最终移交给配置方法中定义的控制器或端点,但是谁来负责这种移交给控制器/端点,ASP.NET Core 如何知道控制器内部的哪个控制器和方法来触发?这就是路由引擎的用途,它是在添加以下中间件时注入的:
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
这里,app.UseRouting()
注入Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware
,用于基于 URI 做出所有的路由决策。这个中间件的主要工作是用特定 URI 需要执行的动作的值来设置Microsoft.AspNetCore.Http.Endpoint
方法的实例。
例如,如果我们试图根据产品的标识获取产品的详细信息,并且有一个产品控制器具有GetProductById
方法来满足这个请求,当我们对api/products/1
URI 进行 API 调用时,在EndpointRoutingMiddleware
之后的中间件中放置一个断点,向您显示Endpoint
类的一个实例是可用的,其中包含了与 URI 匹配并且应该执行的操作的信息。我们可以在下面的截图中看到这一点:
图 10.5–路由中间件
如果没有任何匹配的控制器/操作,此对象将为空。在内部,EndpointRoutingMiddleware
使用 URI、查询字符串参数以及 HTTP 谓词和请求头来找到正确的匹配。
一旦识别出正确的动作方法,app.UseEndPoints
的工作就是将控制权交给Endpoint
对象识别的动作方法并执行。UseEndPoints
注射Microsoft.AspNetCore.Routing.EndpointMiddleware
以执行适当的方法来完成请求。填充适当的EndPoint
对象的一个重要方面是在UseEndPoints
中配置的各种 URIs,这可以通过 ASP.NET Core 中可用的静态扩展方法来实现。例如,如果我们只想配置控制器,我们可以使用MapControllers
扩展方法,为UseRouting
添加控制器中所有动作的端点,以进一步匹配。如果我们正在构建 RESTful APIs,建议使用MapControllers
扩展。但是,对于以下常用的扩展,有许多这样的扩展方法:
-
MapGet / MapPost :这些是扩展方法,可以为
GET
/POST
动词匹配特定的模式,执行请求。它们接受两个参数,一个是 URI 的模式,第二个是当模式匹配时可以用来执行的请求委托。例如,以下代码可用于匹配/aboutus
路线,并用文本Welcome to default products route
进行响应:endpoints.MapGet("/aboutus", async context => { await context.Response.WriteAsync("Welcome to default products route"); });
-
RazorPages:如果我们使用的是 Razor Pages,需要根据路线路由到合适的页面,就使用这个扩展方法。
-
mapcontrolleroute:这个扩展方法可以用来匹配特定模式的控制器;例如,在 ASP.NET Core MVC 模板中可以看到下面的代码,它匹配基于模式的方法:
endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
请求 URI 基于正斜杠(/
)被拆分,并与控制器、动作方法和标识相匹配。因此,如果你想匹配控制器中的一个方法,你需要在 URI 传递控制器名(ASP.NET Core 自动给控制器关键字加后缀)和方法名。或者,可以将该标识作为参数传递给该方法。例如,如果我在ProductsController
有GetProducts
,你会用绝对的 URIproducts/GetProducts
来称呼它。这种路由被称为常规路由,非常适合基于 UI 的 web 应用,因此可以在 ASP.NET Core MVC 模板中看到。
我们对路由基础知识的讨论到此结束,在 ASP.NET Core 中有许多这样的扩展方法,可以根据应用的需求将其引入请求管道。现在,让我们看看基于属性的路由,这是为使用 ASP.NET Core 构建的 RESTful 服务推荐的路由技术。
注意
与任何其他中间件序列一样,路由的另一个重要方面是注入非常重要,应该在UseEndpoints
之前调用UseRouting
。
基于属性的路由
对于 RESTful 服务,常规的路由违反了一些 REST 原则,尤其是声明动作方法对实体执行的操作应该基于 HTTP 动词的原则,所以理想情况下,为了得到产品,URI 应该是GET api/products
。这就是基于属性的路由开始发挥作用的地方,在这种情况下,路由是在控制器级别、操作方法级别或两者都使用属性来定义的。这是使用Microsoft.AspNetCore.Mvc.Route
属性实现的,该属性将字符串值作为输入参数,用于映射控制器和动作。我们以ProductsController
为例,其代码如下:
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
[HttpGet]
[Route("{id}")]
public IActionResult GetProductById(int id)
{
return Ok($"Product {id}");
}
[HttpGet]
public IActionResult GetProducts()
{
return Ok("Products");
}
}
这里,在控制器级别的Route
属性中,我们传递的是值api/[controller]
,这意味着任何匹配api/products
的 URI 都映射到这个控制器,其中products
是控制器的名称。使用方括号内的controller
关键字是告诉 ASP.NET Core 自动将控制器名称映射到路线的一种特定方式。但是,如果您想坚持使用一个特定的名称,而不考虑控制器名称,则可以不使用方括号。作为最佳实践,建议将控制器名称与路由分离。因此,对于我们的电子商务应用,我们将使用路线中的精确值,即ProductsController
将具有[Route("api/products")]
的路线前缀。
Route
属性也可以添加到动作方法中,并且可以用来唯一地附加识别特定的方法。在这里,我们还传递了一个可以用来标识方法的字符串。例如,[Route("GetProductById/{id}")]
将与 URI api/products/GetProductById/1
匹配,花括号内的值是一个动态值,可以作为参数传递给 action 方法并与参数名匹配。这意味着在前面的代码中,有一个参数标识,花括号内的值也应该命名为ID
,这样 ASP.NET Core 就可以将 URI 的值映射到method
参数。因此,对于api/products/1
URI,如果路线属性看起来像[Route("{id}")]
,则GetProductById
方法中的标识参数将具有值1
。
最后,HTTP 动词由[HttpGet]
等属性表示,这些属性将用于将 HTTP 动词从 URI 映射到方法。下表显示了各种示例和可能的匹配,假设ProductsController
有[Route("api/products")]
:
表 10.1
如您所见,方法的名称在这里是不重要的,因此不是 URI 匹配的一部分,除非在Route
属性中指定。
注意
一个重要的方面是,web API 支持从请求中的不同位置读取参数,无论是请求体、头、查询字符串还是 URI。以下文档涵盖了各种可用选项:https://docs.microsoft.com/en-us/aspnet/core/web-api/?view = aspnetcore-5.0 #绑定-源-参数-推断。
ASP.NET Core 中整个应用编程接口路由的概要可以表示如下:
图 10.6–ASP.NET Core 应用编程接口路由
基于属性的路由更 RESTful,我们在电商服务中也会遵循这种路由。现在,让我们看看 ASP.NET Core 中可用的各种助手类,它们可以用来简化 RESTful 服务的构建。
小费
路由中的表达式{id}
被称为路由约束,ASP.NET Core 附带了各种各样的路由约束,也可以在这里找到:https://docs . Microsoft . com/en-us/aspnet/Core/foundation/routing?视图= aspnetcore-5.0 #路线-约束-参考。
controller base 类、ApiController 属性和 ActionResult 类
如果我们回到迄今为止创建的任何控制器,您可以看到所有控制器都是从ControllerBase
类继承的。在 ASP.NET Core 中,ControllerBase
是一个抽象类,它提供了各种帮助方法来帮助处理请求和响应。例如,如果我想发送一个 HTTP 状态代码 400(错误请求),在ControllerBase
中有一个助手方法BadRequest
,可以用来发送一个 HTTP 状态代码 400,否则我们必须手动创建一个对象并用 HTTP 状态代码 400 填充它。ControllerBase
有很多现成的辅助方法;然而,并不是每个 API 控制器都必须继承自ControllerBase
类。这里提到了ControllerBase
类的所有助手方法:https://docs . Microsoft . com/en-us/dotnet/API/Microsoft . aspnetcore . MVC . controller base?view = aspnetcore-3.1&view fallbackfrom = aspnetcore-5.0。
这让我们开始讨论我们的控制器方法的返回类型应该是什么,因为一般来说,对于任何 API,至少有两种可能的响应,如下所示:
- 具有 2xx 状态代码的成功响应,并且可能以资源或资源列表进行响应
- 带有 4xx 状态代码的验证失败案例
为了处理这样的场景,我们需要创建一个可以用来发送不同响应类型的泛型类型,这就是 ASP.NET Core 的IActionResult
和ActionResult
类型发挥作用的地方,为我们提供了各种场景的派生响应类型。IActionResult
支持的一些重要的响应类型如下:
OkObjectResult
:这是一种响应类型,将 HTTP 状态代码设置为200
,并将资源添加到包含资源详细信息的响应正文中。这种类型非常适合所有用资源或资源列表进行响应的 API,例如 get products。NotFoundResult
:这是将 HTTP 状态码设置为404
的响应类型,为空体。如果找不到特定的资源,可以使用此选项。但是,在没有找到资源的情况下,我们将使用NoContentResult
(204
),因为404
也将用于没有找到的应用编程接口。BadRequestResult
:这是一种将 HTTP 状态码设置为400
的响应类型,在响应正文中有一条错误消息。这是任何验证失败的理想选择。CreatedAtActionResult
:这是一种将 HTTP 状态代码设置为201
的响应类型,可以将新创建的资源 URI 添加到响应中。这是创建资源的 API 的理想选择。
所有这些响应类型都是从IActionResult
继承而来的,并且ControllerBase
类中有可以创建这些对象的方法,所以IActionResult
和ControllerBase
将解决大部分业务需求,这就是我们所有 API 控制器方法的返回类型。
ASP.NET Core 中最后一个有用的重要类是ApiController
类,它可以作为属性添加到控制器类或程序集,并为我们的控制器添加以下行为:
-
它禁用常规路由,并强制使用基于属性的路由。
-
它自动验证模型,所以我们不需要在每个方法中显式调用
ModelState.IsValid
。这种行为在插入/更新方法的情况下非常有用。 -
它有助于从正文/路由/头/查询字符串自动映射参数。这意味着我们不指定应用编程接口的参数是否将成为主体或路线的一部分。例如,在下面的代码中,我们不需要明确地说 ID 参数将是路线的一部分,因为
ApiController
自动地使用了被称为推理规则的东西,以及 ID 中带有[FromRoute]
的前缀:[Route("{id}")] public IActionResult GetProductById(int id) { return Ok($"Product {id}"); }
-
同样,在下面的代码片段中,
ApiController
会根据推理规则自动添加[FromBody]
:public IActionResult CreateProduct(Product product) { // }
-
ApiController
添加的其他一些行为是根据https://tools.ietf.org/html/rfc7807推断多部分/表单数据的请求内容和更详细的错误响应。
因此,总而言之,ControllerBase
、ApiController
、ActionResult
提供了各种各样的助手方法和行为,从而为开发人员提供了编写 RESTful APIs 所需的所有工具,并允许他们在使用 ASP.NET Core 编写 API 时专注于业务逻辑。
在此基础上,让我们在下一节为我们的电子商务应用设计各种 API。
与数据层的集成
来自我们的 API 的响应可能或者可能看起来不像我们的领域模型。相反,它们的结构可以类似于用户界面或视图需要绑定的字段。因此,建议创建一组单独的概念验证类,与我们的用户界面集成。这些概念验证对象被称为数据传输对象 ( 数据验证对象)。
在本节中,我们将实现我们的 dto,与数据层集成的域逻辑,并使用 Cache-average 模式集成 第 8 章中讨论的缓存服务,然后使用控制器和动作最终集成所需的 RESTful APIs。一路走来,我们将使用 HTTP 客户端工厂进行我们的服务到服务的通信,使用AutoMapper
库将领域模型映射到 dto。
我们将选择属于Packt.Ecommerce.Product
的产品服务,这是一个使用的网络应用编程接口项目.NET 5,并详细讨论了它的实现。到本节结束时,我们将已经实现了下面截图中突出显示的项目:
图 10.7–产品服务和 dto
类似的实现在所有 RESTful 服务中进行复制,根据需要对业务逻辑稍作修改,但高级实现在以下各种服务中保持不变:
Packt.Ecommerce.DataAccess
Packt.Ecommerce.Invoice
Packt.Ecommerce.Order
Packt.Ecommerce.Payment
Packt.Ecommerce.UserManagement
首先,我们将在appsettings.json
中有相应的部分,如下所示:
"ApplicationSettings": {
"UseRedisCache": false, // For in-memory
"IncludeExceptionStackInResponse": true,
"DataStoreEndpoint": "",
"InstrumentationKey": ""
},
"ConnectionStrings": {
"Redis": ""
}
对于本地开发环境,我们将使用管理用户机密,如这里所解释的,https://docs . Microsoft . com/en-us/aspnet/core/security/app-Secrets?view=aspnetcore-5.0 &选项卡=windows ,设置如下值。但是,一旦部署了服务,它将使用 Azure KeyVault
,如中的 第 6 章配置所述。网芯:
{
"ApplicationSettings:InstrumentationKey": "", //relevant key
"ConnectionStrings:Redis": "" //connection string
}
让我们从为产品应用编程接口创建 DTOs 开始。
创建 dto
在产品服务方面的关键要求是提供搜索产品、查看与产品相关的其他详细信息,然后继续购买的能力。由于产品列表可能有有限的细节,让我们创建一个概念验证(所有的 dto 都是在Packt.Ecommerce.DTO.Models
项目中创建的),并将其命名为ProductListViewModel
。这个类将具有我们想要在产品列表页面上显示的所有属性,它应该如下所示:
public class ProductListViewModel
{
[JsonProperty(PropertyName = "id")]
public string Id { get; set; }
public string Name { get; set; }
public int Price { get; set; }
public Uri ImageUrl { get; set; }
public double AverageRating { get; set; }
}
如您所见,这些是任何电子商务应用中通常显示的最小字段。因此,我们将继续这些领域,但想法是随着应用的发展而扩展。这里,Id
和Name
属性是重要的属性,因为一旦用户想要检索关于产品的所有进一步细节,这些属性将被用于查询数据库。我们正在用JsonProperty(PropertyName = "id")
属性注释Id
属性,以确保属性名在序列化和反序列化期间保持为Id
。这很重要,因为在我们的宇宙数据库实例中,我们使用Id
作为大多数容器的密钥。现在让我们创建另一个代表产品细节的 POCO,如下面的代码片段所示:
public class ProductDetailsViewModel
{
[Required]
public string Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public string Category { get; set; }
[Required]
[Range(0, 9999)]
public int Price { get; set; }
[Required]
[Range(0, 999, ErrorMessage = "Large quantity, please reach out to support to process request.")]
public int Quantity { get; set; }
public DateTime CreatedDate { get; set; }
public List<string> ImageUrls { get; set; }
public List<RatingViewModel> Rating { get; set; }
public List<string> Format { get; set; }
public List<string> Authors { get; set; }
public List<int> Size { get; set; }
public List<string> Color { get; set; }
public string Etag { get; set; }
}
public class RatingViewModel
{
public int Stars { get; set; }
public int Percentage { get; set; }
}
在这里,您可以看到 POCO 非常像我们的领域模型,这是因为我们的反规范化领域模型。但是,一般来说,如果我们使用规范化的领域模型,您会注意到领域模型和 dto 之间的显著差异。在这里,可以进一步讨论为什么我们不能重用产品领域模型。然而,出于可扩展性的目的,以及为了进一步将领域模型与我们的用户界面松散耦合,最好使用单独的 POCO 类。因此,在这个 DTO,除了Id
和Name
之外,一个重要的属性是Etag
,它将用于实体跟踪,以避免对实体的并发覆盖。例如,如果两个用户访问一个产品,用户 A 在用户 B 之前更新,使用Etag
,我们可以阻止用户 B 覆盖用户 A 的更改,并强制用户 B 在更新之前获取产品的最新副本。
另一个重要的方面是,我们在模型上使用 ASP.NET Core 的内置验证属性来定义模型上的所有约束。首先,我们将按照 https://docs . Microsoft . com/en-us/aspnet/core/MVC/models/validation 使用[Required]
属性和任何相关属性?视图= aspnetcore-5.0 #内置属性。
所有的 d to 都将是Packt.Ecommerce.DTO.Models
项目的一部分,因为它们将在我们的 ASP.NET MVC 应用中重用,该应用将用于构建我们的电子商务应用的用户界面。现在,让我们看看Products
服务所需的合同。
服务类合同
在Packt.Ecommerce.Product
中添加一个Contracts
文件夹,并创建一个产品的服务类的合同/接口,我们将根据需要参考我们的需求并定义方法。首先,它将拥有基于该接口对产品执行 CRUD 操作的所有方法,如下所示:
public interface IProductService
{
Task<IEnumerable<ProductListViewModel>> GetProductsAsync(string filterCriteria = null);
Task<ProductDetailsViewModel> GetProductByIdAsync(string productId, string productName);
Task<ProductDetailsViewModel> AddProductAsync(ProductDetailsViewModel product);
Task<HttpResponseMessage> UpdateProductAsync(ProductDetailsViewModel product);
Task<HttpResponseMessage> DeleteProductAsync(string productId, string productName);
}
在这里你可以看到我们在所有方法中返回Task
,从而坚持我们在 第 4 章线程和异步操作中讨论的异步方法。
使用自动映射器的映射器类
接下来我们需要的是一种将我们的领域模型转换为 dto 的方法,这里我们将使用一个名为AutoMapper
的著名库来配置和添加以下包:
Automapper
AutoMapper.Extensions.Microsoft.DependencyInjection
要配置AutoMapper
,我们需要定义一个继承自AutoMapper.Profile
的类,然后定义各种域模型和 d to 之间的映射。让我们添加一个类AutoMapperProfile
,到项目Packt.Ecommerce.Product
:
public class AutoMapperProfile : Profile
{
public AutoMapperProfile()
{
}
}
AutoMapper
有许多内置的映射方法,其中之一是CreateMap
,它接受源类和目标类,并基于相同的属性名来映射它们。任何不同名的属性都可以使用ForMember
方法手动映射。由于ProductDetailsViewModel
与我们的领域模型有一对一的映射,CreateMap
应该足够好,可以进行映射。对于ProductListViewModel
,我们有一个额外的字段,AverageRating
,我们想要为其计算特定产品的所有评级的平均值。为了简单起见,我们将从Linq
开始使用Average
方法,然后将其映射到平均评级。对于模块化,我们将在一个单独的方法MapEntity
中实现,如下所示:
private void MapEntity()
{
this.CreateMap<Data.Models.Product, DTO.Models.ProductDetailsViewModel>();
this.CreateMap<Data.Models.Rating, DTO.Models.RatingViewModel>();
this.CreateMap<Data.Models.Product, DTO.Models.ProductListViewModel>()
.ForMember(x => x.AverageRating, o => o.MapFrom(a => a.Rating != null ? a.Rating.Average(y => y.Stars) : 0));
}
现在,修改构造函数来调用这个方法。
设置AutoMapper
涉及的最后一步是将其作为服务之一注入,为此我们将使用ConfigureServices
方法的Startup
类,使用下面的行:
services.AddAutoMapper(typeof(AutoMapperProfile));
如前所述,这将把AutoMapper
库注入到我们的 API 中,这将允许我们把AutoMapper
注入到各种服务和控制器中。现在我们来看看HttpClient
工厂的配置,用于调用数据访问服务。
用于服务对服务调用的 HttpClient 工厂
为了检索数据,我们必须调用由我们在Packt.Ecommerce.DataAccess
中定义的数据访问服务公开的 API。为此,我们需要一个能够有效使用可用套接字的弹性库,允许我们定义断路器以及重试/超时策略。IHttpClientFactory
非常适合这类场景。
注意
HttpClient
的一个常见问题是潜在的SocketException
,这种情况发生在HttpClient
即使在对象被处理掉之后仍然保持 TCP 连接打开的情况下,建议将HttpClient
创建为静态/单例,它有自己的开销,同时连接到多个服务。以下链接总结了所有这些问题,https://software engineering . stackexchange . com/questions/330364/我们是否应该为所有请求创建一个新的 httpclient 单实例,这些问题现在都由IhttpClientFactory
解决。
要配置IHttpClientFactory
,请执行以下步骤:
- 安装
Microsoft.Extensions.Http
。 - 我们将使用类型化客户端配置
IHttpClientFactory
,所以添加一个Services
文件夹和一个ProductsService
类,并从IProductService
继承它们。目前,将实现留空。现在,使用以下代码在Startup
类的ConfigureServices
中映射IProductService
和ProductsService
这里,我们将ProductsService
使用的HttpClient
超时定义为 5 分钟,另外配置了重试策略和断路器。
实施断路器政策
为了定义这些策略,我们将使用一个名为Polly
的库,它提供了现成的弹性和故障处理能力。安装Microsoft.Extensions.Http.Polly
包,然后将以下静态方法添加到定义我们的断路器策略的Startup
类中:
private static IAsyncPolicy<HttpResponseMessage> CircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
}
在这里,我们说如果在 30 秒内有 5 次故障,电路就会断开。断路器有助于避免不必要的 HTTP 调用,因为存在无法通过重试修复的严重故障。
实施重试策略
现在,让我们添加我们的重试策略,与在指定时间范围内退出的标准重试相比,该策略稍微聪明一点。因此,我们定义了一个策略,该策略将在五种情况下实现重试和 HTTP 服务调用,并且每次重试都有一个以秒为单位的时间差,其幂为 2。为了在时间变化方面增加一些随机性,我们将使用 C#的一个Random
类来生成一个随机数,并将其添加到时间间隙中。该随机生成将如以下代码所示:
Random random = new Random();
TimeSpan.FromSeconds(Math.Pow(2, retry)) + TimeSpan.FromMilliseconds(random.Next(0, 100));
这里,retry
是一个整数,每retry
增加 1。有了这个,给Startup
类添加一个静态方法,它有前面的逻辑:
private static IAsyncPolicy<HttpResponseMessage> RetryPolicy()
{
Random random = new Random();
var retryPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
.WaitAndRetryAsync(
5,
retry => TimeSpan.FromSeconds(Math.Pow(2, retry))
+ TimeSpan.FromMilliseconds(random.Next(0, 100)));
return retryPolicy;
}
这就完成了我们的 HTTP 客户端工厂配置,ProductsService
可以使用构造函数注入来实例化IHttpClientFactory
,可以进一步用来创建HttpClient
。
有了所有这些配置,我们现在可以实现我们的服务类了。让我们在下一节看看。
实现服务类
现在让我们通过定义我们已经构建的各种属性并使用构造函数注入实例化它们来实现,如下面的代码块所示:
private readonly IOptions<ApplicationSettings> applicationSettings;
private readonly HttpClient httpClient;
private readonly IMapper autoMapper;
private readonly IDistributedCacheService cacheService;
public ProductsService(IHttpClientFactory httpClientFactory, IOptions<ApplicationSettings> applicationSettings, IMapper autoMapper, IDistributedCacheService cacheService)
{
NotNullValidator.ThrowIfNull(applicationSettings, nameof(applicationSettings));
IHttpClientFactory httpclientFactory = httpClientFactory;
this.applicationSettings = applicationSettings;
this.httpClient = httpclientFactory.CreateClient();
this.autoMapper = autoMapper;
this.cacheService = cacheService;
}
我们所有的服务都将使用我们在本章中定义的相同的异常处理中间件,因此在服务对服务调用期间,如果另一个服务出现故障,响应将是ExceptionResponse
类型。因此,让我们创建一个私有方法,反序列化ExceptionResponse
类并相应地提升它。这是必需的,因为在使用IsSuccessStatusCode
和StatusCode
属性时HttpClient
将代表成功或失败,所以如果有异常,我们需要检查IsSuccessStatusCode
并重新启动它。我们称这个方法为ThrowServiceToServiceErrors
和参考下面的代码:
private async Task ThrowServiceToServiceErrors(HttpResponseMessage response)
{
var exceptionReponse = await response.Content.ReadFromJsonAsync<ExceptionResponse>().ConfigureAwait(false);
throw new Exception(exceptionReponse.InnerException);
}
现在让我们实现GetProductsAsync
方法,其中我们将使用CacheService
从缓存中检索数据,如果数据在缓存中不可用,我们将使用HttpClient
调用数据访问服务,最后将Product
域模型映射到 DTO 并异步返回。代码如下所示:
public async Task<IEnumerable<ProductListViewModel>> GetProductsAsync(string filterCriteria = null)
{
var products = await this.cacheService.GetCacheAsync<IEnumerable<Packt.Ecommerce.Data.Models.Product>>($"products{filterCriteria}").ConfigureAwait(false);
if (products == null)
{
using var productRequest = new HttpRequestMessage(HttpMethod.Get, $"{this.applicationSettings.Value.DataStoreEndpoint}api/products?filterCriteria={filterCriteria}");
var productResponse = await this.httpClient.SendAsync(productRequest).ConfigureAwait(false);
if (!productResponse.IsSuccessStatusCode)
{
await this.ThrowServiceToServiceErrors(productResponse).ConfigureAwait(false);
}
products = await productResponse.Content.ReadFromJsonAsync<IEnumerable<Packt.Ecommerce.Data.Models.Product>>().ConfigureAwait(false);
if (products.Any())
{
await this.cacheService.AddOrUpdateCacheAsync<IEnumerable<Packt.Ecommerce.Data.Models.Product>>($"products{filterCriteria}", products).ConfigureAwait(false);
}
}
var productList = this.autoMapper.Map<List<ProductListViewModel>>(products);
return productList;
}
我们将遵循类似的模式,实施AddProductAsync
、UpdateProductAsync
、GetProductByIdAsync
和DeleteProductAsync
。每种方法的唯一区别是使用相关的HttpClient
方法并相应地处理它们。现在我们已经实现了我们的服务,让我们实现我们的控制器。
在控制器中实现动作方法
让我们首先将上一节中创建的服务注入到 ASP.NET Core 5 DI 容器中,这样我们就可以使用构造函数注入来创建一个ProductsService
的对象。我们将在Startup
类的ConfigureServices
中使用以下代码来实现这一点:
services.AddScoped<IProductService, ProductsService>();
还要确保配置了所有需要的框架组件,如ApplicationSettings
、CacheService
和AutoMapper
。
在Controllers
文件夹中添加一个控制器并命名为ProductsController
,默认路由为api/products
,然后添加IProductService
的属性,并使用构造函数注入进行注入。控制器应该实现五个动作方法,每个方法调用一个服务方法,并使用本章的控制器类、加速控制器属性和动作结果类一节中讨论的各种现成的助手方法和属性。下面的代码块显示了检索特定产品和创建新产品的方法:
[HttpGet]
[Route("{id}")]
public async Task<IActionResult> GetProductById(string id, [FromQuery][Required]string name)
{
var product = await this.productService.GetProductByIdAsync(id, name).ConfigureAwait(false);
if (product != null)
{
return this.Ok(product);
}
else
{
return this.NoContent();
}
}
[HttpPost]
public async Task<IActionResult> AddProductAsync(ProductDetailsViewModel product)
{
// Product null check is to avoid null attribute validation error.
if (product == null || product.Etag != null)
{
return this.BadRequest();
}
var result = await this.productService.AddProductAsync(product).ConfigureAwait(false);
return this.CreatedAtAction(nameof(this.GetProductById), new { id = result.Id, name = result.Name }, result); // HATEOS principle
}
方法的实现是不言自明的,并且纯粹基于中讨论的使用控制器和动作处理请求的基本原理。同样,我们将通过调用相应的服务方法并返回相关的ActionResult
来实现所有其他方法(Delete
、Update
、Get
所有产品)。这样,我们将有下表所示的 API 来处理与产品实体相关的各种场景:
表 10.2
小费
应用编程接口的另一个常见场景是拥有一个支持文件上传/下载的应用编程接口。上传场景通过将IFormFile
作为输入参数传递给应用编程接口来实现。这将序列化上传的文件,也可以保存在服务器上。同样,对于文件下载,FileContentResult
可用,可以将文件流式传输到任何客户端。这是留给你进一步探索的活动。
对于测试原料药,我们将使用波兹曼(https://www.postman.com/downloads/)。所有邮差收藏都可以在Solution Items
文件夹文件Mastering enterprise application development Book.postman_collection.json
下找到。要在安装 Postman 后导入集合,请执行以下步骤:
- 打开邮差,然后点击文件。
- 点击导入 | 上传文件,导航到
Mastering enterprise application development Book.postman_collection.json
文件的位置,然后点击导入。
成功导入会在邮递员的收藏菜单中显示该收藏,如下图截图所示:
图 10.8–邮差中的收集
这就完成了我们的Products
RESTful 服务实现。本节开头提到的所有其他服务都是以类似的方式实现的,其中每个服务都是一个单独的 web API 项目,并为该实体处理相关的域逻辑。
了解 gRPC
按照grpc.io
,gRPC 是一个高性能、开源通用远程过程调用 ( RPC )框架。gRPC 最初是由谷歌开发的,使用 HTTP/2 进行传输,一个协议缓冲区 ( protobuf )作为接口描述语言。gRPC 是一个基于契约的二进制通信系统。它适用于多个生态系统。gRPC 官方文档中的下图( https://grpc.io )说明了使用 grpc 的客户端-服务器交互:
图 10.9–gRPC 客户端-服务器交互
像许多分布式系统一样,gRPC 是基于定义服务和指定接口的思想,这些接口的方法可以随契约一起远程调用。在 gRPC 中,服务器实现接口并运行 gRPC 服务器来处理客户端调用。在客户端,它有存根,提供与服务器定义的相同的接口。客户端调用存根的方式与调用任何其他本地对象中的方法来调用服务器上的方法的方式相同。
默认情况下,数据协定使用协议缓冲区来序列化来自和去往客户端的数据。protobufs 在扩展名为.proto
的文本文件中定义。在 protobuf 中,数据被结构化为字段中包含的信息的逻辑记录。在下一节中,我们将学习如何在 Visual Studio 中为. NET 5 应用定义 protobuf。
注意
参考官方文档了解更多关于 gRPC: https://grpc.io 的信息。要了解更多关于 protobuf 的信息,请参考https://developers . Google . com/protocol-buffers/docs/概述。
考虑到高性能、语言无关的实现以及与 gRPC 的 protobuf 相关的网络使用减少的好处,许多团队正在探索 gRPC 在他们构建微服务的努力中的使用。
在下一节中,我们将学习在中构建 gRPC 服务器和客户端.NET 5。
在中构建 gRPC 服务器。网
年首次亮相后.NET core 3.0,gRPC 已经成为了一等公民中的佼佼者.NET 生态系统。使用 Visual Studio 2019 和,完全托管的 gRPC 实现现已在. NET 中提供.NET 5,我们可以轻松创建 gRPC 服务器和客户端应用。让我们使用 Visual Studio 中的 gRPC 服务模板创建一个 gRPC 服务,并将其命名为gRPCDemoService
:
图 10.10–gRPC VS 2019 项目模板
这将创建一个名为GreetService
的示例 gRPC 服务解决方案。现在让我们理解用模板创建的解决方案。创建的解决方案将包含对Grpc.AspNetCore
的包引用。这将包含托管 gRPC 服务和.proto
文件的代码生成器所需的库。该解决方案将在Protos
解决方案文件夹下为GreetService
创建原型文件。以下代码片段定义了Greeter
服务:
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply);
}
Greeter
服务只有一个名为SayHello
的方法,该方法将输入参数作为HelloRequest
,并返回一条HelloReply
类型的消息。HelloRequest
和HelloReply
消息在同一个原型文件中定义,如以下代码片段所示:
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
HelloRequest
有一个名为name
的文件,HelloReply
有一个名为message
的字段。字段旁边的数字显示该字段在缓冲区中的序号位置。原型文件是用Protobuf
编译器编译的,以生成带有所有必需管道的存根类。我们可以从原型文件的属性中指定要生成的存根类的类型。由于这是一个服务器,它将配置设置为服务器仅。
现在,我们来看看GreetService
的实现。这将如下面的代码片段所示:
public class GreeterService : Greeter.GreeterBase
{
private readonly ILogger<GreeterService> _logger;
public GreeterService(ILogger<GreeterService> logger)
{
_logger = logger;
}
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply
{
Message = "Hello " + request.Name
});
}
}
GreetService
继承自Greeter.GreeterService
,由 protobuf 编译器生成。通过构造原型文件中定义的HelloReply
,覆盖SayHello
方法以提供实现,从而将问候返回给呼叫者。
要在. NET 5 应用中公开 gRPC 服务,需要通过调用Startup
类的ConfigureServices
方法中的AddGrpc
将所有需要的 gRPC 服务添加到服务集合中。GreeterSerivce
gRPC 服务通过调用MapGrpcService
暴露:
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<GreeterService>();
});
这就是在. NET 5 应用中公开 gRPC 服务所需的一切。在下一节中,我们将实现一个. NET 5 客户端来消费GreeterService
。
在中构建 gRPC 客户端。网
如理解 gRPC 一节所述.NET 5 也有很好的工具来构建一个 gRPC 客户端。在本节中,我们将在控制台应用中构建一个 gRPC 客户端:
-
创建一个. NET 5 控制台应用并命名为
gRPCDemoClinet
。 -
Now, right-click on the project and click on the menu items Add | Service reference…. This will open the Connected Services tab, as shown in the following screenshot:
图 10.11–gRPC 连接服务选项卡
-
点击服务参考(OpenAPI,gRPC) 下的添加按钮,在添加服务参考对话框中选择 gRPC ,然后点击下一步。
-
In the Add new gRPC service reference dialog, select the File option, select the
greet.proto
file fromgRPCDemoService
, and then click on the Finish button. This will add the proto file link to the project and marks the protobuf compiler to generate theClient
stub classes:图 10.12–添加 gRPC 服务参考
这还会将所需的 NuGet 包
Google.Protobuf
、Grpc.Net.ClientFactory
和Grpc.Tools
添加到项目中。 -
Now, add the following code snippet to the main method of the
gRPCDemoClient
project:static async Task Main(string[] args) { var channel = GrpcChannel.ForAddress("https://localhost:5001"); var client = new Greeter.GreeterClient(channel); HelloReply response = await client.SayHelloAsync(new HelloRequest { Name="Suneel" }); Console.WriteLine(response.Message); }
在这段代码中,我们创建了到
gRPCDemoService
端点的 gRPC 通道,然后通过传入 gRPC 通道实例化Greeter.GreeterClinet
,这是到gRPCDemoService
的存根。
现在,要调用服务,我们只需要通过传递HelloRequest
消息来调用存根上的SayHelloAsync
方法。该呼叫将从服务中返回HelloReply
。
到目前为止,我们已经创建了一个简单的 gRPC 服务和该服务的控制台客户端。在下一节中,我们将了解 gRPC curl,这是一个测试 gRPC 服务的通用客户端。
测试 gRPC 服务
为了测试或调用一个 REST 服务,我们使用了一些工具,比如邮递员或提琴手。gRPCurl
是一个命令行实用程序,帮助我们与 gRPC 服务交互。使用 grpcurl,我们可以在不构建客户端应用的情况下测试 grp 服务。grpcurl 可从https://github.com/fullstorydev/grpcurl下载。
一旦grpcurl
被下载,我们可以使用以下命令调用GreeterService
:
grpcurl -d "{\"name\": \"World\"}" localhost:5001 greet.Greeter/SayHello
注意
目前 gRPC 应用只能在 Azure App Service 和 IIS 中托管。因此,我们没有在 Azure 应用服务上托管的演示电子商务应用中利用 gRPC。但是,在本章演示中有一个版本的电子商务应用,其中根据产品的标识获取产品被公开为自托管服务中的 gRPC 端点。
总结
在本章中,我们介绍了 REST 的基本原理,并为我们的电子商务应用设计了企业级 RESTful 服务。
在此过程中,我们掌握了 ASP.NET Core 5 网络应用编程接口的各种网络应用编程接口内部,包括路由和示例中间件,并熟悉了用于测试我们的服务的工具,同时学习了如何使用控制器及其动作来处理请求,这也是我们学习构建的。此外,我们还看到了如何在中创建和测试基本的 gRPC 客户端和服务器应用.NET 5。到目前为止,您应该能够自信地使用 ASP.NET Core 5 网络应用编程接口构建 RESTful 服务。
在下一章中,我们将学习 ASP.NET MVC 的基础知识,使用 ASP.NET MVC 构建我们的 UI 层,并将其与我们的 API 集成。
问题
-
Which of the following HTTP verbs is recommended for creating a resource?
a.
GET
b.
POST
c.
DELETE
d.
PUT
-
Which of the following HTTP status codes represents
No Content
?a.
200
b.
201
c.
202
d.
204
-
Which of the following middlewares is used to configure routing?
a.
UseDeveloperExceptionPage()
b.
UseHttpsRedirection()
c.
UseRouting()
d.
UseAuthorization()
-
If a controller is annotated with the
[ApiController]
attribute, do I need to classModelState.IsValid
explicitly in each action method?a.是的,模型验证不是
ApiController
属性的一部分,因此,您需要在每个动作方法中调用ModelState.Valid
。b.不,模型验证是作为
ApiController
属性的一部分来处理的,因此,对于所有的动作项都自动触发ModelState.Valid
。
进一步阅读
- https://docs . Microsoft . com/en-us/dotnet/architecture/microservice/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests
- https://docs . Microsoft . com/en-us/aspnet/core/signor/introduction?view=aspnetcore-5.0
- https://docs . Microsoft . com/en-us/aspnet/core/tutories/web-API-help-pages-using-swag?view=aspnetcore-5.0
- https://docs . Microsoft . com/en-us/aspnet/core/grp c/?view = aspnet store-5.0
十一、创建 ASP.NET Core 5 网络应用
到目前为止,我们已经构建了应用的所有核心组件,如数据访问层和服务层,所有这些组件主要是服务器端组件,也称为后端组件。在这一章中,我们将为我们的电子商务应用构建表示层/ 用户界面 ( UI ),它也被称为客户端组件。UI 是应用的外观;拥有一个好的表示层不仅有助于让用户参与到应用中,还能鼓励用户回到应用中来。企业应用尤其如此,在企业应用中,良好的表示层有助于用户轻松浏览应用,并帮助他们轻松执行依赖于应用的各种日常活动。
在本章中,我们将主要关注理解 ASP.NET Core MVC 和使用 ASP.NET Core MVC 开发一个网络应用。首先,我们将讨论以下主题:
- 前端 web 开发简介
- 将应用编程接口与服务层集成
- 创建控制器和操作
- 使用 ASP.NET Core MVC 创建用户界面
- 理解布拉佐
技术要求
对于这一章,你需要一个 C#的基础知识.NET Core、HTML 和 CSS。本章的代码示例可以在这里找到:https://github . com/PacktPublishing/Enterprise-Application-Development-with-C-Sharp-9-and-and.NET-5/树/主/第 11 章/剃刀样本。
您可以在这里找到更多代码示例:
前端 web 开发简介
表示层是关于浏览器可以呈现并显示给用户的代码。每当一个页面被加载到浏览器中时,它会创建一个各种元素的层次结构,如文本框和标签,这些元素出现在页面上。这个层次结构被称为文档对象模型 ( DOM )。一个好的前端就是能够根据需要操纵 DOM,并且有许多技术/库支持操纵 DOM 和使用事实上的网络语言 JavaScript 动态加载数据。不管是 jQuery,它简化了 JavaScript 的使用;成熟的客户端框架,如 Angular、React 或 Vue,支持完整的客户端渲染;或者 ASP.NET Core 框架,如 ASP.NET Core MVC、Razor Pages 或 Blazor,都归结为处理网络的三大构建块:HTML、CSS 和 JavaScript。让我们看看这三个组成部分:
-
超文本标记语言 ( HTML ): HTML,如完整形式所述,是浏览器可以理解和显示内容的标记语言。它主要由一系列标签组成,这些标签是被称为 HTML 元素,允许开发人员定义页面的结构。例如,如果您想要创建一个需要允许用户输入他们的名字和姓氏的表单,可以通过使用输入的 HTML 元素来定义它。
-
Cascading Style Sheets (CSS): The presentation layer is all about presenting data in a way that makes a web application more appealing to users and ensures that the application is usable irrespective of the device/resolution that a user tries to load the application in. This is where CSS plays a critical role in defining how the content is displayed on the browser. It controls various things such as the styling of the pages, the theme of the application, and the color palette, and, more importantly, makes it responsive so that users have the same experience using the application, be it loaded on a mobile or a desktop.
现代网络开发的好处是我们不需要从头开始写所有的东西,并且有很多可用的库可以在应用中按原样选择和使用。我们将为电子商务应用使用这样一个库,这将在使用 ASP.NET Core MVC 创建用户界面一节中解释。
-
JavaScript : JavaScript 是一种脚本语言,有助于执行各种高级动态操作,例如,验证在表单或事物中输入的输入文本如有条件地启用/禁用 HTML 元素或从 API 中检索数据。JavaScript 为网页提供了更多的功能,并增加了许多编程功能,开发人员可以使用这些功能在客户端执行高级操作。就像 HTML 和 CSS 一样,所有浏览器都可以理解 JavaScript,它构成了表示层的重要组成部分。所有这些组件都可以相互链接,如下图所示:
图 11.1–HTML、CSS 和 JavaScript
注意
HTML、CSS 和 JavaScript 都是齐头并进的,在开发客户端/前端应用中发挥着重要作用,需要专门的书籍来全面解释。一些相关链接可以在进一步阅读部分找到。
现在我们已经理解了 HTML、CSS 和 JavaScript 的重要性,我们需要知道如何使用它们来构建 web 应用的表示层,以便它能够支持多种不同分辨率的浏览器和设备,并且能够管理状态(HTTP 是无状态的)。一种技术是创建所有的网页,并把它们放在网络服务器上;然而,虽然这在静态网站上运行良好,并且还涉及从头开始构建所有内容,但是如果我们希望内容更加动态并且想要丰富的 UI,我们需要使用能够动态生成 HTML 页面并提供无缝支持以与后端交互的技术。在下一节中,让我们来看看可以用来生成动态 HTML 的各种技术。
Razor 语法
在我们开始理解 ASP.NET Core 提供的各种可能的框架之前,让我们先了解什么是 Razor 语法。将服务器端组件嵌入到 HTML 中是一种标记语法。我们可以使用 Razor 语法绑定任何动态数据进行显示,或者从视图/页面将其发送回服务器进行进一步处理。Razor 语法主要写在 Razor 文件/Razor 视图页面中,这些页面只不过是 C#用来生成动态 HTML 的文件。它们使用.cshtml
扩展并支持 Razor 语法。Razor 语法由名为视图引擎的引擎处理,默认的视图引擎被称为 Razor 引擎。
为了嵌入 Razor 语法,我们通常使用@
,它告诉 Razor 引擎从引擎中解析和生成 HTML。@
后面可以跟任意 C#内置方法生成 HTML。例如,<b>@DateTime.Now</b>
可用于在 Razor 视图/页面中显示当前日期和时间。除此之外,就像 C#一样,Razor 语法也支持代码块和控制结构和变量等。下图显示了 Razor 引擎中的一些示例 Razor 语法:
图 11.2–Razor 语法
Razor 语法也支持定义 HTML 控件;例如,要定义文本框,我们可以使用以下 Razor 语法:
<input asp-for=' FirstName ' />
前面的代码被称为input
标签帮助器,Razor 语法负责处理所谓的指令标签帮助器,以将数据绑定到一个 HTML 控件并生成丰富的动态 HTML。让我们简单讨论一下:
-
Directives: Under the hood, each Razor view/page is parsed by the Razor engine and a C# class is used to generate dynamic HTML and then sends it back to the browser. Directives can be used to control the behavior of this class, which further controls the dynamic HTML that is generated.
例如,
@using
指令可用于在 Razor 视图/页面中包含任何名称空间,或者@code
指令可用于包含任何 C#成员。最常用的指令之一是
@model
,它允许您将模型绑定到视图,这有助于验证视图的类型,也有助于智能感知。将视图绑定到特定类/模型的过程被称为强类型化视图。我们将在我们的电子商务应用中强烈地输入我们所有的视图,我们将在使用 ASP.NET Core MVC 创建用户界面部分看到。 -
Tag helpers: If you used ASP.NET MVC before ASP.NET Core, you would have come across HTML helpers, which are classes that help to bind data and generate HTML controls.
然而,对于 ASP.NET Core,我们有标签助手,帮助我们将数据绑定到一个 HTML 控件。标签助手相对于 HTML 助手的好处在于,标签助手使用与 HTML 相同的语法,并为可以从动态数据生成的标准 HTML 控件分配附加属性。例如,为了生成一个 HTML 文本框控件,我们通常编写以下代码:
<input type='text' id='Name' name='Name' value=' Mastering enterprise application development Book'>
使用标记助手,这将被重写,如下面的代码所示,其中
@Name
是视图被强类型化到的模型的属性:<input type='text' asp-for='@Name'>
所以,如你所见,这一切都是为了编写 HTML,但利用 Razor 标记来生成动态 HTML。ASP.NETCore 自带很多内置的标签助手,关于它们的更多细节可以在这里找到:
https://docs . Microsoft . com/en-us/aspnet/core/MVC/view/tag-helper/内置/?view=aspnetcore-5.0
不要求了解/记住每个标签助手,我们将在开发应用 UI 时使用该参考文档。
注意
因为 Razor 语法是标记,所以没有必要知道所有的语法。以下链接可以作为 Razor 语法的参考:
https://docs . Microsoft . com/en-us/aspnet/core/MVC/view/razor?view=aspnetcore-5.0
有了这些,让我们看看 ASP.NET Core 中的各种选项,以及开发表示层的其他常见框架。
探索剃刀页面
Razor Pages 是使用 ASP.NET Core 实现网络应用的默认方式。Razor Pages 依赖于一个 Razor 文件的概念,该文件可以直接为请求提供服务,并且有一个可选的 C#文件与该 Razor 文件相关联,用于任何额外的处理。可以使用dotnet new webapp
命令创建一个典型的 Razor 应用,生成的项目看起来像下面截图中所示的项目:
图 11.3–剃刀样本页面
如您所见,该项目有 Razor 页面及其相应的 C#文件。在打开任何 Razor 视图时,我们都会看到一个名为@page
的指令,它有助于浏览页面。所以,比如说/index
会被路由到index.cshtml
。重要的是,所有 Razor 页面的顶部都有@page
指令,并放在Pages
文件夹中,因为 ASP.NET Core 运行时会在该文件夹中查找所有 Razor 页面。
通过使用另一个名为@model
的指令,Razor 页面可以进一步与 C#类(也称为PageModel
类)相关联。以下是index.cshtml
页面的代码:
@page
@model RazorSample.Pages.IndexModel
@{
ViewData['Title'] = 'Home page';
}
<div class='text-center'>
<select name='day' asp-items='Model.WeekDay'></select>
</div>
PageModel
类只不过是一个 C#类,它可以为GET
和POST
调用提供特定的方法,这样 Razor 页面上的数据就可以动态获取,比如说,从一个 API 中获取。这个类需要Microsoft.AspNetCore.Mvc.RazorPages.PageModel
继承,是标准的 C#类,index.cshtml
的PageModel
是index.cshtml.cs
的一部分,如下代码所示:
public class IndexModel : PageModel
{
public IndexModel(ILogger<IndexModel> logger)
{
}
public List<SelectListItem> WeekDay { get; set; }
public void OnGet()
{
this.WeekDay = new List<SelectListItem>();
this.WeekDay.Add(new SelectListItem
{
Value = 'Monday',Text = 'Monday'
});
this.WeekDay.Add(new SelectListItem
{
Value = 'Tuesday',Text = 'Tuesday'
});
}
}
在这里,您可以看到我们正在通过OnGet
方法填充剃刀页面上使用的附加数据,该方法也称为PageModel
处理程序,可用于剃刀页面的初始化。像OnGet
一样,我们可以添加一个OnPost
处理程序,用于将数据从 Razor 页面提交回PageModel
并推进该过程。
如果满足以下两个条件,OnPost
方法将自动绑定PageModel
类中的所有属性:
- 属性用
BindProperty
属性进行注释。 - 剃刀页面有一个与属性同名的 HTML 控件。
因此,例如,如果我们想在前面的代码中绑定select
控件的值,我们需要首先向PageModel
类添加一个属性,如下面的代码所示:
[BindProperty]
public string WeekDaySelected { get; set; }
然后,使用如下所示的select
控件的属性名,Razor Pages 会自动将所选值绑定到该属性:
<select asp-for='WeekDaySelected' asp-items='Model.WeekDay'></select>
我们可以为OnGet
和OnPost
方法使用异步命名约定,这样如果我们使用异步编程,它们就可以被命名为OnGetAsync/OnPostAsync
。
Razor Pages 还支持基于动词的调用方法。方法名的模式应遵循OnPost[handler]/OnPost[handler]Async
惯例,其中[handler]
是在任何标签助手的asp-page-handler
属性上设置的值。
例如,以下代码将从相应的PageModel
类调用OnPostDelete/OnPostDeleteAsync
方法:
<input type='submit' asp-page-handler='Delete' value='Delete' />
对于服务配置部分,通过将AddRazorPages
服务添加到 ASP.NET Core依赖注入 ( DI )容器中,可以使用Startup
类的ConfigureServices
方法配置 Razor Pages。此外,在Configure
方法中,将MapRazorPages
添加到endpoints
,如下面的代码所示,这样就可以使用页面的名称请求所有的 Razor 页面:
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
这就完成了一个简单的 Razor 页面应用设置;我们在 第 9 章中看到了另一个使用数据的示例.NET 5 ,使用 Razor Pages 从数据库中检索数据,使用实体框架核心。
Razor Pages 是 ASP.NET Core 开发网络应用最简单的形式;然而,对于开发能够处理复杂特性的 web 应用的更结构化的形式,我们可以使用 ASP.NET Core MVC。让我们在下一节探索使用 ASP.NET Core MVC 开发网络应用。
探索 ASP.NET Core MVC 网站
顾名思义,ASP.NET Core MVC 是基于 第 10 章中讨论的 MVC 模式,创建一个 ASP.NET Core 5 Web API,是 ASP.NET Core 中的一个框架来构建 Web 应用。我们在 第 10 章创建 ASP.NET Core 5 Web API 中看到,ASP.NET Core Web API 也使用了 MVC 模式;不过,ASP.NET Core MVC 也支持视图显示数据。底层的设计模式是相同的,我们有一个保存数据的模型,一个传输数据的控制器,以及呈现和显示数据的视图。
ASP.NET Core MVC 支持在第 10 章**中讨论的创建 ASP.NET Core 5 网络应用编程接口的所有功能,如路由、DI、模型绑定和模型验证,并使用与使用Program
和Startup
类相同的自举技术。像网络应用编程接口,.NET 5/应用服务在Startup
类的ConfigureServices
方法中配置,中间件在Configure
方法中注入。
*与 MVC 的一个关键区别是视图的额外加载,为此我们需要在Startup
类的ConfigureServices
方法中使用AddControllersWithViews
而不是AddControllers
。下图显示了一个示例:
图 11.4–MVC 请求生命周期
AddControllersWithViews
主要负责加载视图和处理控制器发送的数据,但最重要的是,它负责配置 Razor 引擎服务,该服务用于处理视图中的 Razor 语法并生成动态 HTML。
ASP.NET Core MVC 中的控制器动作需要根据 URL 中传递的动作名称进行路由,因此在路由部分,我们配置MapControllerRoute
并传递一个模式给它,而不是调用MapController
。因此,UseEndpoints
中间件中的默认路由配置如下代码片段所示:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: 'default',
pattern: '{controller=Products}/{action=Index}/{id?}');
});
这里,在模式中,我们告诉中间件,URL 的第一部分应该是一个controller
名称,后面是动作名称和一个可选的id
参数。如果 URL 中没有传递任何内容,则默认路由为ProductsController
的Index
动作方法。因此,首先,这是我们在 第 10 章中讨论的基于约定的路由,创建 ASP.NET Core 5 网络应用编程接口。
就像 Razor Pages 一样,ASP.NET Core MVC 应用中的视图支持 Razor 语法,允许强类型视图;也就是说,视图可以绑定到模型进行类型检查,模型属性可以与支持编译时 IntelliSense 的 HTML 控件相关联。
由于 ASP.NET Core MVC 为应用提供了更多的结构,我们将使用 ASP.NET Core MVC 进行我们的表示层开发,并在后续章节中实现表示层时详细讨论。
了解单页应用
单页应用,俗称 SPAs ,是可以在客户端生成动态 HTML 的应用,大多使用 JavaScript。在 SPAs 中,应用通常在浏览器内部工作,支持从服务器加载数据并导航到应用中的各个页面,而无需重新加载页面,这主要是它们被称为单页应用的原因。
通常,这类应用会将所有必需的 HTML、CSS 和 JavaScript 下载到浏览器中,然后加载页面。服务器所需的任何动态内容都是根据需要请求的,动态 HTML 是在客户端使用预加载的 JavaScript 生成的,并在同一页面中呈现,而无需完全刷新浏览器。React、Vue.js、Angular 和 Ember 是我们可以用来开发 SPa 的几个这样的框架。SPAs 发展很快,如果开发团队在这些框架中的任何一个都有技能,那么 SPAs 应该是开发表示层的首选。然而,由于我们将使用 ASP.NET Core MVC 开发表示层,我们将不讨论 spa 的细节,它们本身需要一本专门的书。不过,一些相关的链接可以在进一步阅读部分进行探索。
注意
选择哪种技术进行前端开发一直是一个常见的问题。以下链接有一些关于这个主题的建议:https://docs . Microsoft . com/en-us/dotnet/architecture/modern-web-apps-azure/在传统 web 和单页应用之间进行选择。在选择前端技术之前,应评估所有利弊,因为没有一刀切的要求。
有了这个基础,让我们进入下一部分,我们将开始将到目前为止开发的后端 API 与我们的表示层进行集成。
将 API 与服务层集成
在这一部分,我们将开发Packt.Ecommerce.Web
ASP.NET Core MVC 应用,这是通过添加ASP.NET Core web application(Model-View-Controller)
模板创建的。由于我们已经开发了表示层所需的各种应用编程接口,我们将首先构建一个包装类,用于与这些应用编程接口进行通信。
这是一个单独的包装类,将用于与各种 API 通信,所以让我们为这个类创建契约。
为了简单起见,我们将把需求限制在电子商务应用中最重要的工作流,如下所示:
- 登录页面,检索系统中的所有产品,并允许用户搜索/过滤产品。
- 查看产品的详细信息,添加到购物车,以及向购物车添加更多产品的能力。
- 完成订单并查看发票。
为了遵循更结构化的方法,我们将把不同的类和接口分离到不同的文件夹中。让我们看看如何:
-
首先,让我们在
Packt.Ecommerce.Web
项目中添加一个Contracts
文件夹,并添加一个名为IECommerceService
的界面。该界面将有以下方法:// Method to retrieve all products and filter. Task<IEnumerable<ProductListViewModel>> GetProductsAsync(string filterCriteria = null); // Method to get details of specific product. Task<ProductDetailsViewModel> GetProductByIdAsync(string productId, string productName); // Method to create and order, this method is primarily used to create a cart which is nothing but an order with order status as 'Cart'. Task<OrderDetailsViewModel> CreateOrUpdateOrder(OrderDetailsViewModel order); // Method to retrieve order by ID, also used to retrieve cart/order before checkout. Task<OrderDetailsViewModel> GetOrderByIdAsync(string orderId); Task<InvoiceDetailsViewModel> GetInvoiceByIdAsync(string invoiceId); // Method to submit cart and create invoice. Task<InvoiceDetailsViewModel> SubmitOrder(OrderDetailsViewModel order); // Method to retrieve invoice details by Id. Task<InvoiceDetailsViewModel> GetInvoiceByIdAsync(string invoiceId);
-
现在,让我们添加一个名为
Services
的文件夹,并添加一个名为EcommerceService
的类。这个类将继承IECommerceService
并实现所有的方法。 -
As we need to call various APIs, we need to make use of the HttpClient factory as mentioned in Chapter 10, Creating an ASP.NET Core 5 Web API. All the API URLs are maintained in the application settings, hence we will also populate
Packt.Ecommerce.Common.Options.ApplicationSettings
using theoptions
pattern.Startup
类的ConfigureServices
方法将为我们的 MVC 应用配置以下服务:-
AddControllersWithViews
:为 ASP.NET Core MVC 注入必要的服务,使用控制器和视图。 -
ApplicationSettings
:使用IOptions
模式配置ApplicationSettings
类,代码如下:services.Configure<ApplicationSettings>(this.Configuration.GetSection('ApplicationSettings'));
-
AddHttpClient
:这将注入System.Net.Http.IHttpClientFactory
和相关类,允许我们创建一个HttpClient
对象。此外,我们将配置重试策略和断路策略,如 第 10 章创建 ASP.NET Core 5 网络应用编程接口中所述。 -
使用将
EcommerceService
映射到IECommerceService
.NET Core DI 容器。
-
-
使用以下代码配置应用洞察:
string appinsightsInstrumentationKey = this.Configuration.GetValue<string>('AppSettings:InstrumentationKey'); if (!string.IsNullOrWhiteSpace(appinsightsInstrumentationKey)) { services.AddLogging(logging => { logging.AddApplicationInsights(appinsightsInstrumentationKey); }); services.AddApplicationInsightsTelemetry (appinsightsInstrumentationKey); }
继续到中间件,除了默认路由中间件之外,我们将使用Startup
类的Configure
方法注入以下中间件:
-
UseStatusCodePagesWithReExecute
:该中间件用于重定向到自定义页面,而不是500
错误代码。我们将在下一节的ProductController
中添加一个方法,该方法将被执行并基于错误代码加载相关视图。这个中间件以一个字符串作为输入参数,无非是出错时应该执行的路由,为了传递错误代码,它允许一个{0}
的占位符。因此,中间件配置如下所示:app.UseStatusCodePagesWithReExecute('/Products/Error/{0}');
-
Error handling: As for the presentation layer, unlike with the API, we need to redirect users to a custom page that in the case of runtime failures has relevant information, such as a user-friendly failure message and a relevant logging ID that can be used to retrieve the actual failure at a later stage. However, in the case of a development environment, we can show the complete error along with the stack. So, we will configure two middlewares as shown in the following code:
{ if (env.IsDevelopment()) app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler('/Products/Error/500'); }
在这里,我们可以看到,对于开发环境,我们使用的是
UseDeveloperExceptionPage
中间件,它将加载完整的异常堆栈跟踪,而对于非开发环境,我们使用的是UseExceptionHandler
中间件,它走的是需要执行的错误动作方法的路径。此外,这里我们不需要定制的错误处理中间件,因为 ASP.NET Core 中间件负责将详细的错误记录到日志提供者,在我们的例子中是 Application Insights。 -
使用 StaticFiles :为了允许各种静态文件,比如 CSS、JavaScript、图像和任何其他静态文件,我们不需要经过整个请求管道,这就是这个中间件发挥作用的地方,它允许服务静态文件,并支持短路静态文件管道的其余部分。
回到EcommerceService
类,我们首先定义这个类的局部变量和构造函数,它将使用以下代码注入HTTPClient
工厂和ApplicationSettings
:
private readonly HttpClient httpClient;
private readonly ApplicationSettings applicationSettings;
public ECommerceService(IHttpClientFactory httpClientFactory, IOptions<ApplicationSettings> applicationSettings)
{
NotNullValidator.ThrowIfNull(applicationSettings, nameof(applicationSettings));
IHttpClientFactory httpclientFactory = httpClientFactory;
this.httpClient = httpclientFactory.CreateClient();
this.applicationSettings = applicationSettings.Value;
}
现在,为了按照我们的IECommerceService
接口实现方法,我们将对获取 API 使用以下步骤:
图 11.5–获取对应用编程接口的调用
基于上图中的步骤,GetProductsAsync
的实现主要用于为登录页面检索产品,并在进行产品搜索时应用任何过滤器,如下代码所示:
public async Task<IEnumerable<ProductListViewModel>> GetProductsAsync(string filterCriteria = null)
{
IEnumerable<ProductListViewModel> products = new List<ProductListViewModel>();
using var productRequest = new HttpRequestMessage(HttpMethod.Get, $'{this.applicationSettings.ProductsApiEndpoint}?filterCriteria={filterCriteria}');
var productResponse = await this.httpClient.SendAsync(productRequest).ConfigureAwait(false);
if (!productResponse.IsSuccessStatusCode)
{ await this.ThrowServiceToServiceErrors(productResponse).ConfigureAwait(false);
}
if (productResponse.StatusCode != System.Net.HttpStatusCode.NoContent)
{
products = await productResponse.Content.ReadFromJsonAsync<IEnumerable<ProductListViewModel>>().ConfigureAwait(false);
}
return products;
}
对于的POST
/PUT
API,我们会有类似的步骤稍加修改,如下图所示:
图 11.6–调用应用编程接口后
基于这一点,主要用来创建购物车的CreateOrUpdateOrder
的策略实现如下代码所示:
public async Task<OrderDetailsViewModel> CreateOrUpdateOrder(OrderDetailsViewModel order)
{
NotNullValidator.ThrowIfNull(order, nameof(order));
using var orderRequest = new StringContent(JsonSerializer.Serialize(order), Encoding.UTF8, ContentType);
var orderResponse = await this.httpClient.PostAsync(new Uri($'{this.applicationSettings.OrdersApiEndpoint}'), orderRequest).ConfigureAwait(false);
if (!orderResponse.IsSuccessStatusCode)
{
await this.ThrowServiceToServiceErrors(orderResponse).ConfigureAwait(false);
}
var createdOrder = await orderResponse.Content.ReadFromJsonAsync<OrderDetailsViewModel>().ConfigureAwait(false);
return createdOrder;
}
同样,我们将使用上述策略之一并使用相关的 API 端点来实现GetProductByIdAsync
、GetOrderByIdAsync
、GetInvoiceByIdAsync
和SubmitOrder
。
现在,让我们创建将与EcommerceService
对话并加载相关视图的控制器和动作方法。
创建控制器和动作
我们已经看到路由负责将请求 URI 映射到一个控制器中的一个动作方法,所以让我们进一步了解动作方法是如何加载各自的视图的。正如您已经注意到的,ASP.NET Core MVC 项目中的所有视图都是Views
文件夹的一部分,当动作方法执行完成时,它只是寻找Views/<ControllerName>/<Action>.cshtml
。
例如,映射到Products/Index
路线的动作方法将加载Views/Products/Index.cshtml
视图。这是通过在每个动作方法的末尾调用Microsoft.AspNetCore.Mvc.Controller.View
方法来处理的。
下面的截图显示了我们的 MVC 应用中的一个图片表示:
图 11.7–控制器-视图映射
有额外的重载和辅助方法可以覆盖这种行为,并根据需要路由到不同的视图。在我们谈论这些助手方法之前,就像 Web API 一样,MVC 控制器中的每个动作方法也可以返回IActionResult
,这意味着我们可以利用助手方法重定向到一个视图。在 ASP.NET Core MVC 中,每个控制器都由一个基类Microsoft.AspNetCore.Mvc.Controller
继承,这个基类带有一些助手方法,通过一个动作方法加载视图由Microsoft.AspNetCore.Mvc.Controller
类中的以下助手方法处理:
View
:该方法有多个重载,主要是根据控制器名称从Views
下的文件夹加载视图。例如,在ProductsController
中调用此方法可以从Views/Products
文件夹加载任何.cshtml
文件。此外,它可以采用视图的名称,如果需要可以加载该名称,并支持传递一个对象,该对象可以通过在视图中强有力地键入视图来检索。RedirectToAction
:虽然View
方法处理大部分场景,但是也会有需要在同一个控制器或者另一个控制器内调用另一个动作方法的场景,这也是RedirectToAction
帮忙的地方。此方法附带各种重载,允许我们指定操作方法、控制器方法和对象的名称,操作方法可以接收这些名称作为路由值。
简而言之,为了加载视图并从控制器传递数据,我们将把各自的模型传递给View
方法,并且根据需要,每当需要调用另一个动作方法时,我们将使用RedirectToAction
。
现在的问题是如何处理数据检索(GET
调用)和数据提交(调用),在 ASP.NET Core MVC 中,所有的动作方法都支持使用HttpGet
和HttpPost
属性用 HTTP 动词进行标注。以下是一些可用于注释方法的规则:
- 如果我们想要检索数据,那么使用
HttpGet
对动作方法进行注释。 - 如果我们想向一个动作方法提交数据,应该使用
HttpPost
进行注释,相关对象作为该动作方法的输入参数。
通常,需要从控制器向视图发送数据的方法应使用[HttpGet]
进行注释,需要从视图接收数据以进一步提交到数据库的方法应使用[HttpPost]
进行注释。
现在,让我们继续添加所需的控制器并实现它们。当我们添加Packt.Ecommerce.Web
时,它会用默认创建的HomeController
创建一个Controllers
文件夹,我们需要删除它。然后我们需要通过右击控制器文件夹| 添加 | 控制器 | MVC 控制器 | 清空,命名为ProductsController
、CartController
和OrdersController
。
所有这些控制器将具有以下两个共同的属性,一个用于日志记录,一个用于调用EcommerceService
的方法。它们使用构造函数注入进一步初始化,如下所示:
private readonly ILogger<ProductsController> logger;
private readonly IECommerceService eCommerceService;
现在让我们讨论一下在这些控制器中定义了什么:
-
ProductsController
:该控制器将包含public async Task<IActionResult> Index(string searchString, string category)
动作方式,加载默认视图,列出所有产品,进一步支持过滤。还有一种方法public async Task<IActionResult> Details(string productId, string productName)
,取产品的 ID 和名称,加载指定产品的详细信息。由于这两种方法都用于检索,因此将使用[HttpGet]
对它们进行注释。此外,该控制器将具有前面讨论的Error
方法。因为它可以从UseStatusCodePagesWithReExecute
中间件接收一个错误代码作为输入参数,所以我们将有简单的逻辑来相应地加载视图:[Route('/Products/Error/{code:int}')] public IActionResult Error(int code) { if (code == 404) { return this.View('~/Views/Shared/NotFound.cshtml'); } else { return this.View('~/Views/Shared/Error.cshtml', new ErrorViewModel { CorrelationId = Activity.Current?.RootId ?? this.HttpContext.TraceIdentifier }); } }
-
CartController
:该控制器包含public async Task<IActionResult> Index(ProductListViewModel product)
动作方法,将产品添加到购物车中,我们将创建一个订单,订单状态设置为'Cart'
,因为这需要接收数据,并进一步传递给 API,API 将使用[HttpPost]
进行注释。为了简单起见,这个左边是匿名的,但是可以对登录的用户进行限制。一旦创建了订单,该方法将使用RedirectToAction
助手方法和重定向到该控制器内的public async Task<IActionResult> Index(string orderId)
动作方法,该方法进一步将所有产品和结账表单加载到购物车中。这个方法也可以用来直接导航到购物车。 -
OrdersController
:这是流程中最后一个控制器,包含填写付款明细后提交订单的public async Task<IActionResult> Create(OrderDetailsViewModel order)
动作方式。该方法将订单状态更新为Submitted
,然后为订单创建发票,最后重定向到另一个动作方法public async Task<IActionResult> Index(string invoiceId)
,该动作方法加载订单的最终发票并完成交易。
下图显示了控制器中各种方法之间的流程,以完成购物工作流:
图 11.8–控制器动作方法之间的流程
有了这些知识,让我们设计下一节的视图。
使用 ASP.NET Core MVC 创建用户界面
到目前为止,我们已经定义了一个与后端 API 通信的服务,并进一步定义了控制器,该控制器将使用模型将数据传递给视图。现在,让我们构建各种视图来呈现数据并呈现给用户。
首先,让我们看看呈现视图所涉及的各种组件:
-
The
Views
folder: All views are part of this folder with each controller-specific view segregated by a subfolder, and finally, each action method is represented by a.cshtml
file.要添加视图,我们可以右键点击动作方法,点击添加视图,系统会自动创建一个文件夹(如果还没有的话),里面有控制器的名字,然后添加视图。此外,在这样做的同时,我们可以指定视图将绑定到的模型。
-
The
Layout
page: This is a common requirement in a web application where we have a common section across the application, such as a header with a menu or left navigation. To have a modular structure for our pages and to avoid any repetition, ASP.Net Core MVC comes with a layout page that is typically named_Layout.cshtml
and is part of theViews/Shared
folder. This page can be used as a parent page for all the views in our MVC project. A typical layout page looks like the one shown in the following code:<!DOCTYPE html> <html lang='en'> <head> <meta charset='utf-8'> <meta name='viewport' content='width=device-width, initial-scale=1'> <meta http-equiv='x-ua-compatible' content='ie=edge'> <title>Ecommerce Packt</title> </head> <body class='hold-transition sidebar-mini layout-top-nav'> <!-- Navbar --> <!-- Main content --> @RenderBody() </body> </html>
这里可以看到,它允许我们定义应用的骨架布局,然后最后还有一个名为
@RenderBody()
的 Razor 方法,它实际上加载了子视图。要在任何视图中指定布局页面,我们可以使用以下语法,它将_Layout.cshtml
作为父页面添加到视图中:@{ Layout = '~/Views/Shared/__Layout.cshtml'; }
但是不需要在所有视图中重复这个代码,这就是
_ViewStart.cshtml
派上用场的地方。让我们看看它如何帮助跨视图重用一些代码: -
_ViewStart.cshtml
:这是一个通用视图,直接位于Views
文件夹下,被 Razor 引擎用来执行任何需要在视图中的代码之前执行的代码。因此,通常情况下,这用于定义布局页面,因此,可以将前面的代码添加到该文件中,以便在整个应用中应用。 -
_ViewImports.cshtml
:这是另一个页面,可以用来跨应用导入任何通用指令或命名空间。就像_ViewStart
一样,这个也是直接位于根文件夹下;然而,_ViewStart
和_ViewImport
都可以在一个或多个文件夹中,并且它们从根视图文件夹中的文件夹到任何子文件夹中的较低级别的文件夹分层执行。为了使用 Application Insights 启用客户端遥测,我们注入了JavaScriptSnippet
,如下面的代码所示。我们在中的 第 5 章中了解了将依赖服务注入视图.NET 。在下面的代码中,JavaScriptSnippet
被注入到视图中:@inject Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet JavaScriptSnippet
-
wwwroot
: This is the root folder of the application and all the static resources, such as JavaScript, CSS, and any image files, are placed here. This can further hold any HTML plugins that we want to use in our application. As we have already configured theUseStaticFiles
middleware in our application, content from the folder can be directly served without any processing. The default template of ASP.NET Core MVC comes with segregation of folders based on their type; for example, all JavaScript files are placed inside ajs
folder, CSS files are placed in acss
folder, and so on. We will stick to that folder structure for our application.注意
通过右键单击动作方法并使用内置模板自动生成视图的过程被称为脚手架,如果您不熟悉 Razor 语法,可以使用该过程。但是,使用支架创建视图或手动将其放置在相应的文件夹中并强有力地键入视图会导致相同的行为。
设置管理、布局页面和视图
在整个应用中获得相同外观和感觉的一个重要事项是选择正确的造型框架。这样做不仅提供了一致的布局,还简化了响应设计,这有助于以各种分辨率正确呈现页面。我们用于Packt.Ecommerce.Web
的 ASP.NET Core MVC 项目模板开箱即用,Bootstrap 作为其样式框架。我们将进一步扩展到一个名为AdminLTE
的主题,它带有一些有趣的布局和仪表板,可以插入到我们的表示层中。
让我们执行以下步骤将AdminLTE
集成到我们的应用中:
- 从这里下载
AdminLTE
的最新版本:https://github.com/ColorlibHQ/AdminLTE/releases。 - 提取上一步下载的 ZIP 文件,导航至
AdminLTE-3.0.5\dist\css
。复制adminlte.min.css
并粘贴到Packt.Ecommerce.Web
的wwwroot/css
文件夹中。 - 导航至
AdminLTE-3.0.5\dist\js
。复制adminlte.min.js
并粘贴到Packt.Ecommerce.Web
的wwwroot/js
文件夹中。 - 导航至
AdminLTE-3.0.5\dist\img
。复制需要的图片,粘贴到Packt.Ecommerce.Web
的wwwroot/img
文件夹中。 - 复制
AdminLTE-3.0.5\plugins
文件夹,粘贴到Packt.Ecommerce.Web
的wwwroot
文件夹中。
这样,我们的项目在解决方案探索者中应该如下所示。更多关于AdminLTE
的信息可以在https://adminlte.io/docs/2.4/installation找到:
图 11.9–管理主题设置
现在,导航到Views/_Layout.cshtml
页面,删除所有的现有代码,并替换为Packt.Ecommerce.Web\Views\Shared\_Layout.cshtml
的代码。在高层次上,布局分为以下几个部分:
- 左侧带有主页导航的标题
- 标题中有一个搜索框,中间有一个带有搜索类别的下拉列表
- 右边头部的购物车
- 显示导航的面包屑轨迹
- 使用
@RenderBody()
渲染子视图的部分
完成AdminLTE
模板集成所需的其他几个关键事项如下:
-
添加在
<head>
标签中定义的以下样式:<link rel='stylesheet' href='~/plugins/fontawesome-free/css/all.min.css'> <link rel='stylesheet' href='~/css/adminlte.min.css'>
-
在
<body>
标记的末尾添加以下 JavaScript 文件:<!-- REQUIRED SCRIPTS (Order shouldn't matter)--> <!-- jQuery --> <script src='~/plugins/jquery/jquery.min.js'></script> <!-- Bootstrap 4 --> <script src='~/plugins/bootstrap/js/bootstrap.bundle.min.js'></script> <!-- AdminLTE App --> <script src='~/js/adminlte.min.js'></script>
有了这个,我们将AdminLTE
主题集成到我们的应用中。要使用应用 Insights 呈现启用客户端遥测所需的 JavaScript ,请在_Layout.cshtml
的头标签中添加以下代码:
@Html.Raw(JavaScriptSnippet.FullScript)
前面的代码注入了从视图中发送遥测数据所需的 JavaScript 以及工具键。与服务器端不同,在客户端,检测密钥是公开的。任何人都可以从浏览器开发工具中看到检测密钥。但这就是客户端遥测的设置方式。此时,风险在于恶意用户或攻击者可能会推送不需要的数据,因为检测密钥具有只写访问权限。如果您希望使客户端遥测更加安全,您可以从服务中公开一个安全的 REST API,并从那里记录遥测事件。您将在 第 14 章健康与诊断中了解更多应用洞察功能。
现在,应用布局已经准备好了。现在让我们继续定义应用中的各种视图。
创建产品/索引视图
该视图将用于列出我们的电子商务应用上可用的所有产品,并使用IEnumerable<Packt.Ecommerce.DTO.Models.ProductListViewModel>
模型进行强类型化。它使用ProductsController
的Index
动作方法来检索数据。
在这个视图中,我们将使用一个简单的 Razor @foreach (var item in Model)
循环,对于每个产品,我们将显示产品的图像、名称和价格。该视图的示例如下图所示:
图 11.10–产品视图
在这里,您可以看到布局页面中有一个搜索栏和一个类别下拉列表。点击产品图像将导航至Products/Details
视图。为了支持该导航,我们将利用AnchorTagHelper
并将产品标识和名称传递给ProductsController
的Details
动作方法,以进一步在Products/Details
视图中加载产品的详细信息。
创建产品/详细信息视图
该视图将根据从Products/Index
视图传递的产品标识和名称加载产品的详细信息。我们将使用来自AdminLTE
的示例页面,如下所示:https://AdminLTE . io/themes/dev/AdminLTE/pages/examples/e _ commerce . html。
该页面将使用Packt.Ecommerce.DTO.Models.ProductDetailsViewModel
进行强类型输入,并将显示产品的所有详细信息。下面的截图显示了该页面的示例:
图 11.11–产品详细信息视图
如您所见,这里有一个添加到购物车按钮;点击它将为用户创建购物车并将项目添加到该购物车。由于我们案例中的购物车只不过是订单状态设置为'Cart'
的订单,因此这将调用CartController
的Index
动作方法来创建购物车。
为了将数据传递回动作方法,我们将借助FormTagHelper
,它允许我们将页面包装成 HTML 形式,并使用以下代码指定页面可以提交到的动作和控制器:
<form asp-action='Index' asp-controller='Cart'>
有了这个代码,一旦点击Submit
类型的添加到购物车按钮,页面就会被提交到CartController
的Index
动作方法,进一步保存到数据库中。但是,我们仍然需要将产品细节传递回Index
动作方法,为此,我们将借助InputTagHelper
为所有需要传递回动作方法的值创建隐藏字段。
这里最重要的是,隐藏变量的名称应该与模型中属性的名称相匹配,因此我们将在表单中添加以下代码,将产品值传递回控制器:
<input asp-for='Id' type='hidden'>
<input asp-for='Name' type='hidden'>
<input asp-for='Price' type='hidden'>
<input asp-for='ImageUrls[0]' type='hidden'>
ASP.NETCore MVC 的模型绑定系统读取这些值,并创建CartController
的Index
方法所需的产品对象,该方法进一步调用后端系统来创建订单。
注意
由于 第 12 章理解认证将涉及认证,因此对于本章,添加到购物车将直接向购物车添加产品。
创建购物车/索引视图
该视图将加载购物车详细信息,并将有一个结账表单来填写所有详细信息并完成订单。在这里,我们可以导航回主页添加更多产品或完成订单。
该视图使用Packt.Ecommerce.DTO.Models.OrderDetailsViewModel
进行强类型化,并使用OrdersController
的Index
动作方法加载数据。这里,我们使用的自助结账表单示例来自于https://getbootstrap.com/docs/4.5/examples/checkout/。
这个表单利用模型验证和 HTML 属性对必需的字段进行验证,我们在 ASP.NET Core MVC 标签助手和一些 HTML 助手的帮助下呈现表单。具有模型验证的示例属性如以下代码所示:
public class AddressViewModel
{
[Required(ErrorMessage = 'Address is required')]
public string Address1 { get; set; }
[Required(ErrorMessage = 'City is required')]
public string City { get; set; }
[Required(ErrorMessage = 'Country is required')]
public string Country { get; set; }
}
由于此表单也需要提交,所以整个表单被包裹在FormTagHelper
中,如下代码所示:
<form asp-action='Create' asp-controller='Orders'>
要在用户界面上显示这些验证,在我们之前添加的所有其他脚本之后,将以下脚本添加到_layout.cshtml
中:
<script src='~/lib/jquery-validation/dist/jquery.validate.min.js'></script>
<script src='~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js'></script>
为了显示错误消息,我们可以使用验证消息标记帮助器,如下面的代码片段所示。在服务器端,这可以使用ModelState.IsValid
进一步评估:
<input asp-for='ShippingAddress.Address1' class='form-control' placeholder='1234 Main St'/>
<span asp-validation-for='ShippingAddress.Address1' class='text-danger'></span>
该页面的示例如下图所示:
图 11.12–购物车和结账页面
我们将使用InputTagHelper
作为隐藏字段和文本框,将任何附加信息传递回动作方法。文本框的好处是,如果文本框的的id
属性与属性名匹配,数据将自动传递回动作方法,ASP.NET Core MVC 的模型绑定系统将负责将其映射到所需的对象,在这种情况下,该对象是Packt.Ecommerce.DTO.Models.OrderDetailsViewModel
类型,最终提交订单,生成发票,并重定向到Orders/Index
动作方法。
注意
在前面的截图中,虽然我们有一个结账表单,其中包括生产应用中的支付信息,但我们将与第三方支付网关集成,通常,出于各种安全原因,整个表单位于应用的支付网关端。https://stripe.com/docs/api和https://razor pay . com/docs/支付网关/服务器集成/dot-net/ 就是这样一对帮助支付网关集成的第三方提供商。
创建订单/索引视图
最后,我们会看到订单的发票视图,这是一个简单的只读视图,显示从OrdersController
的Index
动作方式发送的发票信息。下面的截图显示了该页面的示例:
图 11.13–最终发票
这就完成了各种视图的集成,正如您所看到的,我们将视图限制在电子商务应用中最重要的流程中。但是,您可以使用相同的原则进一步添加更多功能。
理解布拉佐
Blazor 是一个新的框架,可从.NET Core 3.1 继续开发应用的前端层。它是 MVC 和 Razor Pages 的替代品之一,应用模型非常接近 SPA 但是,我们可以用 C#和 Razor 语法编写逻辑,而不是 JavaScript。
用 Blazor 编写的所有代码都放在一个叫做 Razor 组件的东西中,它允许您编写 HTML 以及代码的 C#部分来构建任何网页。Razor 组件带有.Razor
的扩展,用于表示应用;无论是整个网页还是一个小对话框弹出,一切都是作为组件在 Blazor 应用中创建的。典型的 Razor 组件看起来像下面代码片段中的组件:
@page '/counter'
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class='btn btn-primary' @onclick='IncrementCount'>Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{ currentCount++; }
}
在这段代码中,我们创建了一个在点击按钮时递增计数器的页面,点击事件的逻辑在 C#代码中处理,它更新了 HTML 中的值。本页面可使用/counter
相对网址访问。
Blazor 与其他 MVC/Razor Pages 的主要区别在于,与请求-响应模型不同,在请求-响应模型中,每个请求都被发送到服务器,HTML 被发送回浏览器,Blazor 将所有组件打包(就像 SPA 一样)并加载到客户端。当第一次请求应用时,对服务器的任何后续调用都是为了检索/提交任何应用编程接口数据或更新 DOM。Blazor 支持以下两种托管模式:
- Blazor web assembly(WASM):WASM 是可以在现代浏览器上运行的低级指令,这进一步有助于在没有任何额外插件的浏览器上运行用 C#等高级语言编写的代码。Blazor WASM 托管模型利用了 WASM 给出的开放网络标准,并在浏览器的沙箱环境中运行任何 Blazor WASM 应用的 C#代码。在高层次上,所有的 Blazor 组件都被编译成。. NET 程序集和下载到浏览器,WASM 加载.NET Core 运行时并加载所有程序集。它进一步使用 JavaScript 互操作来刷新 DOM 对服务器的唯一调用将是任何后端 API。架构如下图所示:
图 11.14–布拉佐 WASM 托管
- Blazor 服务器:在 Blazor 服务器托管模型中,Blazor 应用托管在一个 web 服务器上,在那里进行编译,然后客户端利用 SignalR 从服务器接收更新。为了保持连接的活跃性,Blazor 创建了一个名为
blazor.server.js
的 JavaScript 文件,并使用 SignalR 接收所有的 DOM 更新,这进一步意味着每个用户交互都将有一个服务器调用(尽管很轻)。架构如下图所示:
图 11.15–Blazor 服务器托管
.NET 5 对这两种托管模型都提供了完整的工具支持,都有自己的项目模板,各有利弊,这里进一步说明:https://docs . Microsoft . com/en-us/aspnet/core/blazor/hosting-models?view=aspnetcore-5.0 。
现在让我们使用 Blazor Server 应用按照以下步骤创建一个前端应用,它允许我们为我们的电子商务应用添加/修改产品详细信息:
-
Add a new Blazor Server application called
Packt.Ecommerce.Blazorweb
to the enterprise solution and add theProducts.razor
,AddProduct.Razor
, andEditProduct.razor
Razor components to thePages
folder, as shown in the following screenshot:图 11.16–Blazor 服务器项目
-
这个项目包含
Program
和Startup
类,它们与任何其他 ASP.NET Core 应用完全一样,只是有一些额外的 Blazor 服务。_Host.cshtml
是应用的根,这个页面接收到对应用的初始调用,并用 HTML 进行响应。本页进一步引用了信号连接的blazor.server.js
脚本文件。另一个重要的组件是App.Razor
组件,它负责基于网址的路由。在 Blazor 中,任何需要映射到特定 URL 的组件都会在组件的开头有@page
指令,指定应用的相对 URL。App.Razor
拦截网址并将其路由到指定的组件。所有 Razor 组件都是Pages
文件夹的一部分,Data
文件夹附带了一个示例模型和一个在FetchData.razor
组件中使用的服务。 -
让我们将以下代码添加到
NavMenu.razor
以将Products
导航添加到左侧菜单。在这个阶段,如果你运行应用,你应该可以看到左侧菜单的Products
导航;但是,它不会导航到任何页面:<li class='nav-item px-3'> <NavLink class='nav-link' href='products'> <span class='oi oi-list-rich' aria-hidden='true'></span> Products </NavLink> </li>
-
当我们要从 API 中检索数据时,我们需要将
HTTPClient
注入到我们的Startup
类中,就像在 ASP.NET Core 应用中一样。因此,将以下代码添加到ConfigureServices
方法中,并将ApplicationSettings:ProductsApiEndpoint
添加到appsettings.json
:services.AddHttpClient('Products', client => { client.BaseAddress = new Uri(Configuration ['ApplicationSettings:ProductsApiEndpoint']); }); 'ApplicationSettings': { 'ProductsApiEndpoint': 'https://localhost:44346/api/products/' },
-
既然我们要绑定
products
数据,那么就添加Packt.Ecommerce.DTO.Models
作为Packt.Ecommerce.Blazorweb
的项目引用。在Pages
文件夹中,将以下代码添加到@code
块内的Products.razor
页面,在该页面中,我们正在使用IHttpClientFactory
创建一个HttpClient
对象,该对象将在下一步注入,并使用OnInitializedAsync
方法检索products
数据:private List<ProductListViewModel> products; protected override async Task OnInitializedAsync() { var client = Factory.CreateClient('Products'); var result = await client.GetAsync('').ConfigureAwait(false); result.EnsureSuccessStatusCode(); products = new List<ProductListViewModel>(); products = await result.Content.ReadFromJsonAsync<List<ProductListViewModel>>().ConfigureAwait(false); }
-
接下来,在
Products.Razor
页面的开始处(在@code
块之外)添加以下代码。在这里,我们通过@page
指令到/products
为该组件设置相对路线。接下来,我们注入IHttpClientFactory
和其他需要的名称空间,然后添加呈现产品列表的 HTML 部分。如您所见,它是 HTML 和 Razor 语法的混合:@page '/products' @inject IHttpClientFactory Factory @using System.Net.Http.Json;@using Packt.Ecommerce.DTO.Models; <h1>Products</h1> <div> <a class='btn btn-info' href='addproduct'><i class='oi oi-plus'></i> Add Product</a> </div> @if (products == null) { <p><em>Loading...</em></p> } else { <table class='table'><thead><tr> <th>Id</th><th>Name</th> <th>Price</th><th>Quantity</th> <th>ImageUrls</th><th></th> </tr></thead><tbody> @foreach (var product in products) {<tr> <td>@product.Id</td> <td>@product.Name</td> <td>@product.Price</td> <td>@product.Quantity</td> <td><img src='@product.ImageUrls[0]' class='product-image w-10 col-3' alt='Product' /></td> <td><a class='btn btn-info' href='editproduct/@product.Id/@product.Name'><i class='oi oi-pencil'></i></a></td></tr> } </tbody></table> }
此时,如果您运行应用,您应该会看到如下截图所示的输出:
图 11.17–产品列表 Blazor 用户界面
接下来,让我们创建Add/Edit
页面,在其中我们将使用 Blazor 表单。表单可用的一些重要工具/组件如下:
-
Blazor 表单是使用 Blazor 中称为
EditForm
的现成模板创建的,它可以使用模型属性直接绑定到任何 C#对象。典型的EditForm
如下代码片段所示。这里,我们定义在表单提交时调用OnSubmit
方法。让我们把这个加到AddProduct.razor
:<EditForm Model='@product' OnSubmit='@OnSubmit'> </EditForm>
-
这里,
product
是我们想要使用的模型的对象,在我们的例子中是Packt.Ecommerce.DTO.Models.ProductDetailsViewModel
。为了将数据绑定到任何控件,我们可以混合使用 HTML 和 Razor 语法,如下面的代码所示。这里,我们将产品对象的Name
属性绑定到一个文本框,类似地,将Category
属性绑定到下拉列表。一旦您在文本框中输入任何值或在下拉列表中选择一个值,这些属性中的将自动将其传递回任何后端应用编程接口或数据库。让我们以类似的方式将所有必需的属性添加到 HTML 元素中:<InputText id='category' @bind-Value='product.Name'></InputText> <InputSelect @bind-Value='product.Category'> <option selected disabled value='-1'> Choose Category</option> <option value='Clothing'>Clothing</option> <option value='Books'>Books</option> </InputSelect>
-
Blazor 表单支持使用数据注释进行数据验证,因此我们想要绑定到 UI 的任何模型都可以有数据注释,Blazor 会将这些验证应用于属性绑定到的控件。为了应用验证,我们添加了
DataAnnotationsValidator
组件,并且可以使用ValidationSummary
组件来显示所有验证失败的摘要。我们可以在控制级别进一步使用ValidationMessage
组件,如下面的代码片段所示:<DataAnnotationsValidator /> <ValidationSummary /> <InputNumber id='quantity' @bind-Value='product.Quantity'></InputNumber> <ValidationMessage For='@(() => product.Quantity)' />
-
在
code
组件中,添加一个ProductDetailsViewModel
的对象,并将其命名为产品,即EditForm
的Model
属性中定义的,进一步执行OnSubmit
方法。
AddProduct.Razor
和EditProduct.Razor
的完整代码可以在 GitHub repo 中找到,一旦我们运行应用,我们可以看到以下页面:
图 11.18–添加产品浏览器用户界面
这是一个使用 Blazor 构建前端的基本示例,它执行列表、创建和更新操作。然而,《布拉佐尔》中有很多概念可以在https://docs.microsoft.com/en-us/aspnet/core/blazor/?进一步探索 view=aspnetcore-5.0 。
总结
在本章中,我们了解了表示层和用户界面设计的各个方面。与此同时,我们还学习了使用 ASP.NET Core MVC 和 Razor Pages 开发表示层的各种技巧,最后,我们使用 ASP.NET Core MVC 和 Blazor 为我们的企业应用实现了表示层。
有了这些技能,您应该能够使用 ASP.NET Core MVC、Razor Pages 和 Blazor 构建表示层,并将其与后端 API 集成。
在下一章中,我们将看到如何跨应用的各个层在我们的系统中集成身份验证。
问题
-
Which one of the following is a recommended page to define the left-side navigation that needs to appear throughout the web application?
a.
_ViewStart.cshtml
b.
_ViewImports.cshtml
c.
_Layout.cshtml
d.
Error.cshtml
-
Which of the following pages can be used to configure the
Layout
page for the entire application?a.
_ViewStart.cshtml
b.
_ViewImports.cshtml
c.
_Layout.cshtml
d.
Error.cshtml
-
Which of the following special characters is used to write Razor syntax in a
.cshtml
page?a.
@
b.
#
c.
<% %>
d.以上都不是
-
Which method will be called on a button click in the following tag helper code in a Razor page application?
<input type='submit' asp-page-handler='Delete' value='Delete' />
a.
OnGet()
b.
onDelete()
c.
OnPostDelete()
d.
OnDeleteAsync()
进一步阅读
- https://www . packtpub . com/web-development/html 5-and-css3-building-responsive-网站
- https://www . packtpub . com/product/bootstrap-for-ASP-net-MVC-第二版/9781785889479
- https://developer.mozilla.org/en-US/docs/WebAssembly
- https://docs.microsoft.com/en-us/aspnet/core/razor-pages/?view=aspnetcore-5.0
- https://docs . Microsoft . com/en-us/aspnet/core/MVC/view/partial?view=aspnetcore-5.0
- https://developer . Mozilla . org/en-US/docs/Learn/Accessibility*
十二、理解认证
到目前为止,我们已经构建了电子商务应用的用户界面 ( UI )和服务层。在本章中,我们将看到如何保护它。我们的电子商务应用应该能够唯一地识别用户并响应用户的请求。建立用户身份的常用模式包括提供用户名和密码。然后,根据存储在数据库或应用中的用户配置文件数据对这些进行验证。如果匹配,则会生成一个带有用户身份的 cookie 或令牌,并将其存储在客户端的浏览器中,以便在后续请求中,将 cookie/令牌发送到服务器并验证以服务请求。
身份验证是一个过程,在此过程中,您可以识别访问应用受保护区域的用户或程序。例如,在我们的电子商务应用中,用户可以浏览不同的页面并浏览显示的产品。但是,要下订单或查看过去的订单,用户需要提供用户名和密码来识别自己。如果用户是新用户,他们应该创建这些来继续。
在本章中,我们将了解 ASP.NET Core 提供的与身份验证相关的功能,并了解实现身份验证的各种方法。本章涵盖以下主题:
- 了解身份验证的要素
- ASP.NET Core 身份介绍
- 理解 OAuth 2.0
- 天青活动目录介绍(天青 AD
- Windows 身份验证简介
- 了解保护客户端和服务器应用的最佳实践
技术要求
对于本章,您需要 Azure 的基本知识、实体框架 ( EF )、Azure AD B2C,以及具有投稿人角色的活跃 Azure 订阅。如果你没有,你可以在https://azure.microsoft.com/en-in/free/注册一个免费账户。
了解中认证的要素.NET 5
ASP.NET 芯的认证是由认证中间件处理,认证中间件使用注册的认证处理程序进行认证。注册的身份验证处理程序及其相关配置称为身份验证方案。
以下是身份验证框架的核心元素:
-
Authentication scheme: This defines the type and behavior of the authentication to be used to authenticate, challenge, and forbid. Authentication schemes are registered as authentication services in the
Startup.ConfigureServices
method. They comprise an authentication handler and have options to configure this handler. You can register multiple authentication schemes to authenticate, challenge, and forbid actions, or specify authentication schemes in authorization policies you configure. The following is some sample code to register anOpenIdConnect
authentication scheme:services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApp(this.Configuration.GetSection("AzureAdB2C"));
在前面的代码片段中,认证服务被注册为使用微软身份平台的
OpenIdConnect
认证方案,并且在AzureAdB2C
部分的配置文件中指定的必要设置被用于初始化认证选项。关于
OpenIdConnect
和AzureAdB2C
的更多细节将在本章的天青 AD 简介部分中介绍。 -
认证处理程序:认证处理程序负责对用户进行认证。根据身份验证方案,他们要么构建身份验证票证(通常是带有用户身份的令牌/cookie),要么在身份验证不成功时拒绝请求。
-
认证:这个方法是负责用用户身份构造一张认证票。例如,cookie 认证方案构建 cookie,而 JavaScript 对象标记 ( JSON ) 网络令牌 ( JWT )承载方案构建令牌。
-
挑战:当未经认证的用户请求需要认证的资源时,通过授权调用这个方法。然后,根据配置的方案,要求用户进行身份验证。
-
禁止:当经过身份验证的用户试图访问不允许他们访问的资源时,授权会调用该方法。
让我们了解如何使用 ASP.NET Core 身份框架添加身份验证。
ASP 简介。N ET 核心标识
ASP.NET Core 身份是一个基于会员资格的系统,提供了一个简单的方法来添加登录和用户管理功能到您的应用。它提供了一个 UI 和应用编程接口(API)来创建新的用户帐户、提供电子邮件确认、管理用户配置文件数据、管理密码(如更改或重置密码)、登录、注销等,并启用多因素身份验证 ( MFA )。它还允许您与外部登录提供商集成,如微软账户、谷歌、脸书、推特和许多其他社交网站,以便用户可以使用他们现有的账户注册,而不是创建新的账户,从而增强用户体验。
默认情况下,ASP.NET Core 身份使用 EF 代码优先方法将用户名、密码等用户信息存储在 SQL Server 数据库中。它还允许您自定义表/列名,并捕获其他用户数据,如出生日期、电话号码等。您还可以对其进行自定义,以将数据保存在不同的持久性存储中,如 Azure 表存储、NoSQL 数据库等。它还提供了一个用于定制密码散列、密码验证等的应用编程接口。
在下一节中,我们将学习如何创建一个简单的 web 应用,并将其配置为使用 ASP.NET Core 身份进行身份验证。
示例实现
在 Visual Studio 中创建新项目,选择ASP.NET Core 网应用模板,然后选择ASP.NET Core 网应用,更改认证。您会发现以下选项可供选择:
- 不认证:如果你的申请不需要认证,选择这个。
- 个人用户账号:如果使用本地店铺或 Azure AD B2C 管理用户身份,选择此项。
- 工作或学校账户:如果您希望针对 AD、Azure AD 或 Office 365 认证用户,请选择此项。
- Windows 身份验证:如果您的应用仅在内部网可用,请选择此项。
对于这个示例实现,我们将使用本地存储来保存用户数据,选择个人用户帐户,然后单击确定来创建项目,如以下 scr 事件所示:
图 12.1–身份验证模式
或者,您可以使用dotnet
命令行界面 ( 命令行界面)创建一个新的网络应用,将个人用户帐户配置为身份验证选项,将SQLite
配置为数据库存储,如下所示:
dotnet new webapp --auth Individual -o AuthSample
要将一个 SQL 数据库配置为存储区,运行以下命令,确保您应用迁移在数据库中创建必要的表:
dotnet new webapp --auth Individual -uld -o AuthSample
现在,运行以下命令来构建和运行应用:
dotnet run --project ./AuthSample/AuthSample.csproj
您应该会看到类似如下的输出:
图 12.2–供参考的 dotnet 运行命令输出
在前面的截图中,您会注意到来自控制台的日志和统一资源定位器 ( 网址)以及应用可访问的端口。
现在你的应用已经启动并运行,在浏览器中打开网址,点击注册,提供所需的详细信息,点击注册按钮。您可能会第一次看到以下错误消息:
图 12.3–由于缺少迁移导致的运行时异常
您可以单击应用迁移运行迁移并刷新页面,这应该可以解决问题。或者,在 Visual Studio 中打开项目,并在包管理器控制台中运行Update-Database
以应用迁移并重新运行应用。现在,您应该能够注册并登录到应用。现在,让我们检查一个为我们创建的项目结构。
在依赖包下,您会注意到以下 NuGe t 包:
-
微软。AspNetCore.Identity.UI :这是一个 Razor 类库,它包含了整个身份 UI,你可以用它从浏览器中导航——例如
/Identity/Account/Register
或者/Identity/Account/Login
。 -
微软。ASP.NET Core 身份用来与数据库存储交互。
-
Microsoft.EntityFrameworkCore.SqlServer: A library used to interact with SQLDB.
这些包可以在下面的截图中看到:
图 12.4–身份验证示例项目的解决方案资源管理器视图
现在我们来考察一下Startup.cs
的Configure
法。
下面的代码注册了启用认证能力的认证中间件:
app.UseAuthentication();
在ConfigureServices
方法中,ApplicationDbContext
通过提供一个options
配置注册为从属服务,该配置具有在appsettings.json
中指定的sql db
连接字符串,如下所示:
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
AddDefaultIdentity
方法注册生成用户界面的服务,并使用IdentityUser
作为模型配置默认身份系统,如下所示:
services.AddDefaultIdentity<IdentityUser>(options =>
options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
ASP.NET Core 身份允许我们配置许多身份选项来满足我们的需求,例如,以下代码允许我们禁用电子邮件确认、配置密码要求和设置锁定超时设置:
services.AddDefaultIdentity<IdentityUser>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
options.Password.RequireDigit = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequiredLength = 8;
options.Lockout.DefaultLockoutTimeSpan =
TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
})
.AddEntityFrameworkStores<ApplicationDbContext>();
脚手架
要进一步自定义 UI 等设置,可以有选择地添加 Razor 类库中包含的源代码,然后可以修改生成的源代码以适合自己的需要。对于脚手架,在解决方案资源管理器中,右键单击项目 | 添加 | 新脚手架项目 | 标识 | 单击添加。
这将打开一个窗口,您可以在其中选择要覆盖的文件,如下图所示:
图 12.5–覆盖身份模块的对话框
您可以选择覆盖所有文件,或者只选择要自定义的文件。选择您的数据上下文类,然后单击添加将源代码添加到您的项目中。这将在Identity
文件夹下添加文件 Razor 和相应的 C#文件都将被添加。以下屏幕截图说明了根据选择添加的文件:
图 12.6–身份验证示例项目的解决方案资源管理器视图
关于定制的更多细节,你可以参考https://docs . Microsoft . com/en-us/aspnet/core/security/authentication/scaffold-identity?视图=aspnetcore-5.0 。
现在,让我们了解如何将 ASP.NET Core 应用与外部登录提供商集成。
与外部登录提供商的集成
在本节中,我们将学习如何集成 ASP.NET Core 应用,以使用外部登录提供商,如微软帐户、谷歌、脸书、推特等,并使用 OAuth 2.0 流进行认证,以便用户可以使用他们现有的凭据注册和访问我们的应用。将 ASP.NET Core 应用与任何外部登录提供程序集成的常见模式如下所示:
-
从各自的开发人员门户获取凭据(通常是客户端标识和密码)以访问 OAuth APIs 进行身份验证。
-
在应用设置或用户机密中配置凭据。
-
Next, we need to add the respective NuGet package to the project at Add Middleware Support to use OpenId and OAuth 2.0 flows.
要与脸书认证集成,安装
Microsoft.AspNetCore.Authentication.Facebook
,可在https://www.nuget.org获得。 -
在
Startup.cs
中,在ConfigureServices
方法下,调用AddAuthentication
方法注册认证服务。
要将谷歌配置为外部登录提供商,您需要执行以下步骤:
-
在https://developers.google.com/identity/sign-in/web/sign-in创建 OAuth 凭证。
-
在用户机密中配置凭据。您可以使用
dotnet
CLI 为您的项目添加秘密,如:dotnet user-secrets set "Authentication:Google:ClientId" "<client-id>" dotnet user-secrets set "Authentication:Google:ClientSecret" "<client-secret>"
-
将
Microsoft.AspNetCore.Authentication.Google
NuGet 包添加到您的项目中,并在Startup.ConfigureServices
方法中添加以下代码:services.AddAuthentication() .AddGoogle(options => { IConfigurationSection googleAuthNSection = Configuration.GetSection ("Authentication:Google"); options.ClientId = googleAuthNSection["ClientId"]; options.ClientSecret = googleAuthNSection["ClientSecret"]; });
-
同样,可以添加多个提供者。
更多详情可参考https://docs . Microsoft . com/en-us/aspnet/core/security/authentication/social/?view=aspnetcore-5.0 。
完成上述步骤后,您应该能够使用谷歌凭据登录到您的应用。关于在应用中对外部登录提供程序使用 ASP.NET Core 身份进行身份验证的部分到此结束。在下一节中,让我们看看 OAuth 是什么。
了解 OAuth 2.0
OAuth 2.0 是一个现代的和行业标准协议,用于保护 web APIs。它通过为 web 应用、单页应用、移动应用等提供特定的授权流来访问安全的 API,从而简化了流程。
考虑一个用例,其中您希望构建一个门户网站,用户可以在其中同步和查看来自他们最喜欢的应用(如 Instagram、脸书或其他第三方应用)的照片/视频。您的应用应该能够代表用户向第三方应用请求数据。一种方法是存储与每个第三方应用相关的用户凭证,您的应用代表用户发送或请求数据。
这种方法会导致许多问题,概述如下:
- 您需要设计应用来安全地存储用户凭据。
- 用户可能不喜欢第三方应用在您的应用中共享和存储他们的凭据。
- 如果用户更改了他们的凭据,他们需要在您的应用中更新。
- 在安全漏洞的情况下,欺诈者可以不受限制地访问第三方应用中的用户数据。这可能会导致潜在的收入和声誉损失。
OAuth 2.0 可以通过解决所有这些问题来处理前面所有的用例。让我们看看如何,如下所示:
- 用户登录到您的应用。要同步图片/视频,用户将被重定向到第三方应用,并且他们需要使用凭据登录。
- Oauth 2.0 审核并批准应用获取资源的请求。
- 用户被重定向回带有授权代码的应用。
- 要同步图片/视频,您的应用可以通过交换授权代码来获取令牌,然后与令牌一起对第三方应用进行 API 调用。
- 在每个请求中,第三方应用验证令牌并做出相应的响应。
在 OAuth 流程中,涉及到四方,包括客户端、资源所有者、授权服务器、资源服务器。参考以下截图:
图 12.7–OAuth 2 流程
从截图中,我们看到了以下内容:
- 资源所有者:这是一个拥有资源/数据的实体,能够授予客户端访问权限。
- 资源服务器:承载与资源所有者相关的资源或数据,使用承载令牌进行验证,并响应或拒绝来自客户端的请求的服务器。
- 客户端:从授权服务器获取令牌,代表资源所有者向资源服务器发出请求的应用。
- 授权服务器:对资源所有者进行认证,并向客户端发放令牌。
代币
授权服务器对用户进行身份验证,并提供一个 ID 令牌、访问令牌和刷新令牌,这些都是本机/web 应用用来访问受保护服务的。让我们进一步了解每一个:
- 访问令牌:由授权服务器作为 OAuth 流的一部分颁发,通常为 JWT 格式;一个 Base64 编码的 JSON 对象,包含发行者、用户、作用域、过期时间等信息
- 刷新令牌:由授权服务器连同访问令牌一起颁发,客户端应用使用该令牌在访问令牌过期前请求新的访问令牌
- 身份令牌:由授权服务器作为 OpenID Connect 流程的一部分颁发,可用于认证用户
授权授予类型
OAuth 2.0 定义了客户端获取令牌以访问安全资源的多种方式,这些方式称为授权。它定义了四种授权类型:授权代码、隐式、代表和客户端凭证流,如下所述:
- 授权代码流:这个流是适合 web、移动、单页应用,你的应用需要从另一个服务器获取你的数据。授权代码流从客户端重定向用户以在授权服务器进行身份验证开始。如果成功,用户同意客户端所需的权限,并使用授权代码重定向回客户端。这里,客户端的身份通过授权服务器中配置的重定向统一资源标识符 ( URI )来验证。接下来,客户端通过传递授权代码来请求访问令牌,并作为回报获得访问令牌、刷新令牌和到期日期。客户端可以使用访问令牌来调用网络应用编程接口。由于访问令牌是短暂的,在它们过期之前,客户端应该通过传递访问令牌和刷新令牌来请求新的访问令牌。
- 隐式流:这是一个简化版的代码流,适用于单页、基于 JavaScript 的应用。使用隐式流,授权服务器只发布访问令牌,而不是发布授权代码。这里,不验证客户端身份,因为不需要指定重定向网址。
- 代表流程:这个流程最适合客户端调用网络应用编程接口(比如说,A)的情况,而在中需要调用另一个应用编程接口(比如说,B)。流程是这样的:用户向 A 发送一个请求和一个令牌;a 通过提供令牌和凭证(如 a 的客户端 ID 和客户端秘密)从授权服务器请求令牌来访问 B。一旦它为 B 获取令牌,它就调用 B 上的 API。
- 客户端凭证流:这个流是用在需要服务器到服务器交互的情况下(比如 A 到 B,其中 A 获取令牌使用其凭证(通常是客户端 ID 和客户端秘密)与 B 交互,然后用获取的令牌调用 API)。该请求在 A 的上下文下运行,而不是在用户的上下文下运行。所需的权限应授予 A 以执行必要的操作。
现在我们已经了解了 OAuth 是什么,在下一节中,让我们了解一下 Azure AD 是什么,以及如何将其与我们的电子商务应用集成,并将其用作我们的身份服务器。
天青公元简介
Azure AD 是微软提供的一款IT3】身份和访问管理 ( IAM )云服务。它是内部用户和外部用户的单一身份存储,因此您可以配置应用使用 Azure AD 进行身份验证。您可以将内部 Windows AD 同步到 Azure AD,从而为您的用户启用单点登录 ( SSO )体验。用户可以使用自己的工作或学校凭证或个人微软账户如Outlook.com
、Xbox、Skype 等登录。它还允许您本地添加或删除用户、创建组、执行自助密码重置、启用 Azure MFA 等等。借助 Azure AD B2C,您可以自定义用户注册、登录和管理其个人资料的方式,它还允许您的客户使用他们现有的社交凭据(如脸书、谷歌等)来登录和访问您的应用和 API。
Azure AD 符合行业标准协议,例如 OpenID Connect ,也称为 OIDC 和 OAuth2.0。OIDC 是建立在 OAuth 2.0 协议之上的身份层,用于验证和检索用户的个人资料信息。OAuth 2.0 用于授权,以使用不同的流(如隐式授权流、代表流、客户端凭据流等)获得对 HTTP 服务的访问。
web 应用中的典型身份验证流程如下所示:
- 用户试图访问应用的安全内容(比如说我的订单)。
- 如果用户没有通过身份验证,他们将被重定向到 Azure 广告登录页面。
- 一旦用户提交了他们的凭证,它们就会被 Azure AD 验证,Azure AD 会将令牌发送回网络应用。
- cookie 会保存到用户的浏览器中,并显示用户请求的页面。
- 在随后的请求中,一个 cookie 被发送到服务器,用于验证用户。
Azure AD B2C 使您的客户能够使用他们首选的社交、企业或原生身份来访问您的应用或 API。它可以扩展到每天数百万用户和数十亿次身份验证。
让我们尝试将我们的电子商务应用与 Azure AD B2C 集成。在高层次上,我们需要执行以下步骤来整合这一点:
-
创建一个 Azure AD B2C 租户。
-
注册申请。
-
添加身份提供者。
-
创建用户流。
-
Update the app code to integrate.
注意
作为先决条件,您应该有一个具有参与者角色的活动 Azure 订阅。如果你没有,你可以在https://azure.microsoft.com/en-in/free/注册一个免费账户。
Azure AD B2C 设置
使用 Azure AD B2C 作为身份服务将允许我们的电子商务用户注册,创建自己的凭据,或者使用他们现有的社交凭据,如脸书或谷歌。让我们研究一下将 Azure AD B2C 配置为电子商务应用的身份服务所需执行的步骤,如下所示:
-
登录 Azure 门户,确保您位于包含您的订阅的同一目录中。
-
在首页页面,点击创建资源搜索 B2C ,从选项中选择 Azure 活动目录 B2C 。
-
Select Create a new Azure AD B2C Tenant, as illustrated in the following screenshot:
图 12.8–Azure AD B2C
-
Provide the required details and click Review + create, then complete the following fields:
机构名称:你的 B2C 租户名称。
内部域名:你租户的内部域名。
国家/地区:选择您的租户应该供应的国家或地区。
订阅****资源群:提供订阅和资源群详情。
这些字段显示在下面的屏幕截图中:
图 12.9–新的 Azure AD B2C 配置部分
-
Review your details and click Create. The creation of your new tenant might take a few minutes. Once it is created, you will see confirmation in the notification section. In the Notifications popup, click on the tenant name to navigate to the newly created tenant, as illustrated in the following screenshot:
图 12.10–确认创建 Azure AD B2C 服务
-
If you notice, in the following screenshot, Subscription status is given as No Subscription, and a warning message says that you should link a subscription to your tenant. You can click the link to fix it, else you can skip to Step 9 to continue to configure Azure AD:
图 12.11–无订阅链接警告消息
-
The link will open the same screen that you have seen in Step 3. This time, click Link an existing Azure AD B2C Tenant to my Azure subscription to continue, as illustrated in the following screenshot:
图 12.12–将 Azure 广告 B2C 租户链接到订阅
-
Select your B2C tenant subscription from the dropdown, provide a Resource group value, and click Create to link the subscription and the tenant, as illustrated in the following screenshot:
图 12.13–订阅选择
-
您可以导航到您的 B2C 租户,继续配置的后续步骤,如下所示:
- 您需要向 Azure AD B2C 租户注册您的应用,以将其用作身份服务。
- 您需要选择用户可以用来登录您的应用的身份提供者。
- 选择用户流来定义用户注册或登录的体验,如下图所示:
图 12.14–配置 Azure AD B2C 的三个步骤
-
Under Manage, click App Registrations and provide the necessary details as follows, and then click Register to create the AD application:
名称:显示您的应用的名称。
支持的账户类型:在任何身份提供者或组织目录中选择账户,这样我们就可以允许用户使用他们现有的凭据进行注册或登录。
重定向 URI :您需要提供您的应用的网址,用户将在成功认证后重定向到该网址。目前,我们可以将其留空。
权限:选择授予管理员对 openid 和 offline_access 权限的许可。
这些字段显示在下面的屏幕截图中:
图 12.15–注册新的 Azure AD 应用
注意
要在本地设置和调试,我们可以使用
localhost
进行配置。这需要替换为托管您的应用的网址。 -
Now, let's choose Identity Providers under Manage to configure, as follows:
本地账户:该选项允许用户通过用户名和密码以传统方式注册并登录我们的应用。下面的截图说明了这一点:
图 12.16–身份提供商选择
-
Let's configure Google as the identity provider for our application. You can follow the steps outlined at https://docs.microsoft.com/en-in/azure/active-directory-b2c/identity-provider-google to acquire the client ID and secret. The details you need to provide are shown in the following screenshot:
图 12.17–谷歌:新的 OAuth 客户端
提供所需的详细信息并保存后,生成客户端 ID 和密码,如下图截图所示:
图 12.18–谷歌 OAuth 客户端
-
Once you create Auth Client, click on Google from identity providers, and then provide the Client ID and Client secret values to complete the configuration. Refer to the following screenshot:
图 12.19–身份提供者配置
-
让我们将脸书配置为我们的电子商务应用的另一个身份提供商。您可以按照中概述的步骤进行操作。
-
创建客户端身份验证设置后,从身份提供者中点击脸书,然后提供客户端 Id 和客户端密码值至完成配置。参见图 12.19 进行概述。
-
Now, let's configure the user flow. The user flow allows you to configure and customize the authentication experience for your users. You can configure multiple flows in your tenant and use them in your application. User flows allow you to add MFA and also to customize information that you capture from a user at the time of registration—for example, given name, country, postal code, and optionally adding them to claims. You can also customize the UI for a better user experience. To create a flow, click User Flows under Policies and choose a flow type, as illustrated in the following screenshot:
图 12.20–新用户流
-
Provide the necessary details and click Create to save:
名称:您要唯一识别的流的名称。
身份提供者:选择身份提供者。
也可以选择其他用户属性,如姓名、邮政编码等,如下图截图所示:
图 12.21–用户流配置
可以选择附加属性,如下图截图所示:
图 12.22–附加属性和索赔配置
-
同样,我们也应该设置密码重置策略。这是本地帐户所必需的。要创建一个,在创建用户流下选择密码重置并提供必要的详细信息,参见图 12.19 。
-
已经完成了 Azure AD B2C 的最低要求设置,我们准备测试流程。选择创建的用户流,点击运行用户流。您可以查看为您创建的注册和登录页面,您可以在下面的屏幕截图中找到这些页面:
图 12.23–登录和注册屏幕
我们来看看Packt.Ecommerce.Web
中需要做哪些改变才能和 Azure AD 集成。
将我们的电子商务应用集成到 Azure AD B2C
我们将在 web 应用上配置身份验证,以使用 Azure AD B2C。让我们对应用进行必要的更改,以便与 B2C 租户集成,如下所示:
-
Add the following two NuGet packages to our
Packt.Ecommerce.Web
project:Microsoft.Identity.Web
:这是与 Azure AD 集成所需的主包。Microsoft.Identity.Web.UI
:这个包生成了登录和注销的 UI。在ConfigureServices
方法下的Startup.cs
中,我们需要使用OpenIdConnect
方案和Azure AD B2C
配置来添加认证服务,如下所示:services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme).AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAdB2C")); services.AddRazorPages().AddMicrosoftIdentityUI();
-
在
Configure
方法下,在app.UseAuthorization()
方法前添加以下代码:app.UseAuthentication();
-
We need to add
AzureAdB2C in appsettings.json
, as follows:Instance
:https://<domain>.b3clogin.com/tfp
。用创建 B2C 租户时选择的名称替换<domain>
。ClientId
:这是你在设置 Azure AD B2C 时创建的 AD 应用 ID。Domain
:<domain>.onmicrosoft.com
。在这里,用您在创建 B2C 租户时选择的域名替换<domain>
。更新
SignUpSignInPolicyId
和ResetPasswordPolicyId
,如下:"AzureAdB2C": { "Instance": "https://packtecommerce.b2clogin.com/tfp/", "ClientId": "1ae40a96-60d7-4641-bb81- bc3a47aad36d", "Domain": "packtecommerce.onmicrosoft.com", "SignedOutCallbackPath": "/signout/B2C_1_susi", "SignUpSignInPolicyId": "B2C_1_packt_commerce", "ResetPasswordPolicyId": "B2C_1_password_reset", "EditProfilePolicyId": "", "CallbackPath": "/signin-oidc" }
-
您可以将
[Authorize]
属性添加到控制器或动作方法中,例如,您可以将其添加到OrdersController.cs
中的OrdersController
中,以强制用户验证自己以访问Orders
信息。 -
The last step is to update the reply URI. To do so, navigate to AD Application in your tenant, navigate to the Authentication section under Manage, and update Reply URI and set implicit grant permissions.
URI 回复是您的应用的网址,用户将在成功验证后被重定向到该网址。要在本地设置应用并进行调试,我们可以配置 localhost URL,但是一旦将应用部署到服务器,就需要更新服务器的 URL。
在隐式授权下,选择访问令牌和身份令牌,这是我们的 ASP.NET Core 应用所必需的,如下图所示:
图 12.24–回复网址配置
现在,运行您的应用并尝试访问订单页面。您将被重定向到登录和注册页面,如图 12.23 所示。我们的电子商务应用与 Azure AD B2C 的集成到此结束。
Azure AD 提供了许多更多选项和定制,以满足您的需求。更多详情可以看https://docs.microsoft.com/en-in/azure/active-directory-b2c。
注意
您可以使用身份服务器 4 来设置自己的身份服务器。这使用 OpenID Connect 和 OAuth 2.0 框架来建立身份。它可以通过 NuGet 获得,并且可以很容易地与 ASP.NET Core 应用集成。更多详情可参考https://identityserver4.readthedocs.io/en/latest。
在下一节中,让我们看看如何使用 Windows 身份验证。
Windows 身份验证简介
ASP.NET Core 应用可以配置为使用 Windows 身份验证,用户可以根据其 Windows 凭据进行身份验证。当您的应用托管在 Windows 服务器上并且您的应用仅在内部网可用时,Windows 身份验证是最佳选择。在本节中,我们将学习如何在 ASP.NET Core 应用中使用 Windows 身份验证。
在 Visual Studio 中,创建新的 ASP.NET Core 项目时,在更改身份验证窗口中选择窗口身份验证。如果选择命令行界面创建项目,使用--auth Windows
参数使用 Windows 身份验证创建新的 web 应用,如下所示:
dotnet new webapp --auth Windows -o WinAuthSample
如果打开launchSettings.json
,会注意到WindowsAuthentication
设置为true
、anonymousAuthentication
设置为false
,如下面的代码片段所示。此设置仅在互联网信息服务快递 ( IIS 快递)中运行应用时适用:
"iisSettings": {
"windowsAuthentication": true,
"anonymousAuthentication": false,
"iisExpress": {
"applicationUrl": "http://localhost:21368",
"sslPort": 44384
}
}
当您在 IIS 上托管应用时,您需要在web.config
中将WindowsAuthentication
配置为true
。默认情况下,不会为添加web.config
.NET Core web 应用,因此您需要添加并进行必要的更改,如以下代码片段所示:
<location path="." inheritInChildApplications="false">
<system.webServer>
<security>
<authentication>
<anonymousAuthentication enabled="false"/>
<windowsAuthentication enabled="true"/>
</authentication>
</security>
</system.webServer>
</location>
前面的配置使每个端点都很安全。即使我们在任何控制器或动作上设置AllowAnonymous
,也不会有影响。如果您想使任何端点可匿名访问,您需要将anonymousAuthentication
设置为true
,并在您想确保安全的端点上设置Authorize
。除此之外,您还需要使用Windows
方案注册认证服务,如下所示:
services.AddAuthentication(IISDefaults.AuthenticationScheme)
这是我们在您的应用中启用 Windows 身份验证所需要做的全部工作。更多详情可以参考https://docs . Microsoft . com/en-us/aspnet/core/security/authentication/window sauth?view=aspnetcore-5.0 。
在下一节中,我们将了解一些保护客户端和服务器应用的最佳实践。
了解保护客户端和服务器应用的最佳实践
有几个最佳实践被推荐用于保护你的网络应用。.NET Core 和 Azure 服务使得确保它们的采用变得很容易。以下是您可能会考虑的关键因素:
- 对网络应用实施 HTTPS。使用
UseHttpsRedirection
中间件将请求从 HTTP 重定向到 HTTPS。 - 使用基于 OAuth 2.0 和 OIDC 的现代身份验证框架来保护您的网络或应用编程接口应用。
- 如果您使用的是微软身份平台,请使用开源库(如 MSAL.js 和 MSAL.js)来获取或续订令牌。
- 配置强密码要求,并在连续失败的登录尝试(例如,五次连续失败的尝试)的情况下锁定您的帐户。这可以防止暴力攻击。
- 为特权帐户(如后台管理或后台员工帐户等)启用 MFA。
- 配置会话超时;注销时使会话无效;透明饼干。
- 在所有安全端点和客户端强制授权。
- 将密钥/密码存储在安全的位置,如密钥库中。
- 如果您正在使用 Azure AD,请单独注册每个逻辑/环境特定的应用。
- 不要以纯文本形式存储敏感信息。
- 确保正确的异常处理。
- 对上传的文件执行安全/恶意软件扫描。
- 防止跨站点脚本攻击—始终对用户输入数据进行 HTML 编码。
- 通过使用存储过程参数化 SQL 查询来防止 SQL 注入攻击。
- 防止跨站点请求伪造攻击—对动作、控制器或全局使用
ValidateAntiForgeryToken
过滤器。 - 使用此策略在中间件中实施 CORS (简称跨来源请求)。
虽然所提供的最佳实践和指导从一开始就很好,但您需要始终考虑应用的上下文,并持续评估和增强您的应用,以解决安全漏洞和威胁。
总结
在这一章中,我们了解了什么是认证,以及 ASP.NET 芯认证的关键要素。我们探索了 ASP.NET Core 框架提供的不同选项,并了解了 ASP.NET Core 身份如何帮助快速向您的应用添加身份验证。我们讨论了 OAuth 2.0 和授权流,以及当您需要验证和连接到多个 API 服务时,它们如何使事情变得容易。我们还研究了将 Azure AD 配置为您的身份服务,在您的应用中使用外部身份验证提供商(如谷歌或脸书),以及在 ASP.NET Core 应用中使用 Windows 身份验证。在本章的最后,我们讨论了在开发服务器端和客户端应用时应该遵循的一些最佳实践。
在下一章中,我们将了解什么是授权,以及授权如何帮助控制对资源的访问。
问题
-
What information can be derived from a JWT?
a.发行人
b.满期
c.领域
d.科目
e.上述全部
-
What are the recommended OAuth grant flows for single-page apps?
a.客户端凭据
b.隐形的
c.代码授权流
d.代表流量
-
What are the minimum required NuGet packages to integrate with Azure AD?
a.
Microsoft.AspNetCore.Identity
b.
Microsoft.Identity.Web.UI
c.
Microsoft.AspNetCore.Identity.UI
d.
Microsoft.Identity.Web
进一步阅读
要了解有关身份验证的更多信息,您可以参考以下内容:
- https://docs . Microsoft . com/en-us/aspnet/core/security/authentication/?view=aspnetcore-5.0
- https://docs.microsoft.com/en-in/azure/active-directory-b2c
十三、理解授权
构建安全应用的一个重要方面是确保用户只能访问他们需要的资源。在现实世界中,当您入住酒店时,前台员工会验证您的身份证和信用卡,并分配一张钥匙卡来进入您的房间。根据您选择的房间类型,您可能会有进入休息室、游泳池或健身房等特权。在这里,你的身份证和信用卡的验证和分配密钥卡是认证,允许你访问各种资源是授权。因此,为了进一步解释,使用钥匙卡,我们不能识别你是谁,但可以确定你能做什么。
授权是一种机制,通过它您可以确定用户可以做什么,以及授予或拒绝对应用资源的访问权限。例如,我们的电子商务应用的用户应该能够浏览产品,将它们添加到购物车,并结账购买,只有管理员或后台用户应该能够添加或更新产品信息,更新产品价格,批准或拒绝订单,等等。
在本章中,我们将学习什么是授权,以及使用 ASP.NET Core 框架实现授权的各种方法。本章涵盖以下主题:
- 在中理解授权.NET 5
- 简单授权
- 基于角色的授权
- 基于索赔的授权
- 基于策略的授权
- 自定义授权
- 客户端和服务器应用中的授权
技术要求
这一章,你需要 Azure 的基础知识,Azure AD B2C,C#.NET Core,和 Visual Studio 2019。
回到几个基础
在深入了解更多细节之前,让我们先了解一下身份验证和授权之间的区别。
认证和授权看似相似,可以互换使用,但本质上是不同的。下表说明了的区别:
表 13.1
注意
更多关于认证在 ASP.NET 5 如何工作的详细信息,请参考 第 12 章了解认证。
总而言之,身份验证和授权是相辅相成的。授权仅在用户身份建立后起作用,当用户试图访问安全资源时,授权会触发身份验证质询。在本章接下来的章节中,我们将了解如何在 ASP.NET 5 应用中实现授权。
理解授权
ASP.NET Core 中的授权由中间件处理。当您的应用收到未经身份验证的用户对安全资源的第一个请求时,中间件会调用身份验证质询,根据身份验证方案,用户要么被重定向以登录,要么被禁止访问。一旦用户的身份在认证后被建立,授权中间件检查用户是否可以访问资源。在随后的请求中,授权中间件使用用户的身份来确定是允许还是禁止访问。
要在您的项目中配置授权中间件,您需要在Startup.cs
的Configure
方法中调用UseAuthorization()
。必须在认证中间件后注册授权中间件,因为授权只能在建立用户身份后执行。参考以下代码:
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
在前面的代码块中,您会注意到在app.UseAuthentication()
之后和app.UseEndpoints()
之前调用了app.UseAuthorization()
。
ASP.NET 5 提供了简单的、声明性的基于角色和声明的授权模型以及丰富的基于策略的模型。在接下来的章节中,我们将了解更多关于这些的细节。
简单授权
在 ASP.NET 芯,授权使用AuthorizationAttribute
配置。您可以在控制器、动作或剃刀页面上应用属性。当您添加此属性 e 时,对该组件的访问仅限于经过身份验证的用户。请参考以下代码块:
public class HomeController : Controller
{
[Authorize]
public IActionResult Index()
{
return View();
}
public IActionResult Privacy()
{
return View();
}
}
在前面的代码中,您会注意到[Authorize]
属性被添加到Index
动作中。当用户试图从浏览器访问/Home/Index
时,中间件会检查用户是否通过身份验证。如果没有,用户将被重定向到登录页面。
如果我们将[Authorize]
属性添加到一个控制器,那么对该控制器下任何操作的访问仅限于经过身份验证的用户。在下面的代码中,您会注意到HomeController
添加了[Authorize]
属性,使其下的所有操作都是安全的:
[Authorize]
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
[AllowAnonymous]
public IActionResult Privacy()
{
return View();
}
}
有时,您可能希望让任何用户都可以访问您的应用的一些区域,例如,登录或重置密码页面应该对所有人开放,无论用户是否经过身份验证。为了满足这些要求,您可以将[AllowAnonymous]
属性添加到控制器或动作中,并使它们对未经身份验证的用户可用。
在前面的代码中,您会注意到[AllowAnonymous]
属性被添加到了Privacy
动作中,尽管我们在控制器上有[Authorize]
属性。该要求被动作方法上的[AllowAnonymous]
属性覆盖,因此所有用户都可以访问Privacy
动作。
注意
[AllowAnonymous]
属性覆盖所有授权配置。如果在控制器上设置[AllowAnonymous]
,在控制器下的任何动作方法上设置[Authorize]
属性都不会有影响。在这种情况下,动作方法上的Authorize
属性被完全忽略。
到目前为止,我们已经看到了如何保护控制器或动作方法。在下一节中,我们将看到如何在 ASP.NET Core 应用中启用全局授权。
全局启用授权
到目前为止,我们已经看到了如何使用[Authorize]
属性来保护控制器或动作方法。在大型项目中,在每个控制器或动作上设置authorize
属性是不可持续的;您可能会错过配置新添加的控制器或操作方法,这可能会导致安全漏洞。
ASP.NET Core 允许您通过在应用中添加回退策略来启用全局授权。您可以在Startup.cs
的ConfigureServices()
方法中定义回退策略。回退策略将应用于未定义明确授权要求的所有请求:
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
全局添加策略会强制用户通过身份验证以访问应用中的任何操作方法。该选项是有益的,因为您不必为应用中的每个控制器/动作指定[Authorize]
属性。
您仍然可以在控制器或动作方法上设置[AllowAnonymous]
属性,以覆盖回退行为,并使其可匿名访问。
既然我们已经理解了如何实现简单授权,在下一节中,让我们了解什么是基于角色的授权,以及它如何简化实现。
基于角色的授权
应用的某些区域只对某些用户可用是很常见的。通常的做法不是在用户级别授予访问权限,而是将用户分组到角色中,并授予对角色的访问权限。让我们考虑一个典型的电子商务应用,其中用户可以下订单,支持员工可以查看、更新或取消订单并解决用户查询,管理员角色批准或拒绝订单,管理库存等等。
基于角色的授权可以解决这些需求。当您创建一个用户时,您可以将其分配给一个或多个角色,当我们配置[Authorize]
属性时,我们可以将一个或多个角色名称传递给Authorize
属性的Roles
属性。
以下代码限制属于Admin
角色的用户访问Admin
控制器下的所有操作方法:
[Authorize(Roles ="Admin")]
public class AdminController : Controller
{
public IActionResult Index()
{
return View();
}
}
同样,您可以在Authorize
属性的Roles
属性中指定逗号分隔的角色名称,这样属于任一已配置角色的用户都可以访问该控制器下的操作方法。
在下面的代码中,您会注意到User,Support
是作为[Authorize]
属性的Roles
属性的值提供的;属于User
或Support
角色的用户可以访问Orders
控制器的动作方法:
[Authorize(Roles ="User,Support")]
public class OrdersController : Controller
{
public IActionResult Index()
{
return View();
}
}
您还可以指定多个授权属性。如果这样做,用户必须是所有指定角色的成员才能访问它。
在下面的代码中,InventoryManager
和Admin
角色在InventoryController
上配置了多个[Authorize]
属性。要访问Inventory
控制器,用户必须具有InventoryManager
和Admin
角色:
[Authorize(Roles ="InventoryManager")]
[Authorize(Roles ="Admin")]
public class InventoryController : Controller
{
public IActionResult Index()
{
return View();
}
[Authorize(Roles ="Admin")]
public IActionResult Approve()
{
return View();
}
}
您可以通过指定授权属性进一步限制对Inventory
控制器下的动作方法的访问。在前面的代码中,用户必须具有InventoryManager
和Admin
角色才能访问Approve
动作。
编程上,如果要检查用户是否属于某个角色,可以使用ClaimsPrinciple
的IsInRole
方法。在下面的例子中,您会注意到User.IsInRole
接受roleName
,并且根据用户的角色,它返回真或假:
public ActionResult Index()
{
if (User.IsInRole("Admin"))
{
// Handle your logic
}
return View();
}
到目前为止,我们已经看到了如何通过在授权属性中指定角色名称来保护控制器或操作。在下一节中,我们将看到如何使用基于策略的角色授权将这些配置集中在一个地方。
基于策略的角色授权
我们也可以在Startup.cs
中将角色需求定义为策略。这种方法非常有用,因为您可以在一个地方创建和管理基于角色的访问需求,并使用策略名称而不是角色名称来控制访问。要定义基于策略的角色授权,我们需要在Startup.cs
的ConfigureServices
方法中注册一个具有一个或多个角色要求的授权策略,并为Authorize
属性的Policy
属性提供一个策略名称。
在下面的代码中,通过添加角色为Admin
的需求来创建AdminAccessPolicy
:
services.AddAuthorization(options =>
{
options.AddPolicy("AdminAccessPolicy",
policy => policy.RequireRole("Admin"));
});
在您的控制器中,您可以按如下方式指定要应用的策略,并且对Admin
控制器的访问仅限于具有Admin
角色的用户:
[Authorize(Policy ="AdminAccessPolicy")]
public class AdminController : Controller
{
public IActionResult Index()
{
return View();
}
}
定义策略时,您可以指定多个角色。当该策略用于授权用户时,属于任何一个角色的用户都可以访问资源。例如,以下代码将允许具有User
或Support
角色的用户访问资源:
options.AddPolicy("OrderAccessPolicy",
policy => policy.RequireRole("User","Support"));
您可以在控制器或操作方法上使用带有Authorize
属性的OrderAccessPolicy
策略来控制访问。
现在我们已经了解了如何使用基于角色的授权,在下一节中,我们将创建一个简单的应用,并将其配置为使用基于角色的授权。
实现基于角色的授权
让我们创建一个 s 充足的应用,使用 ample 核心身份实现基于角色的授权:
-
创建一个新的 ASP.NET Core 项目。可以使用以下
dotnet
CLI 命令创建。这将创建一个新的 ASP.NET Core MVC 应用,使用Individual
帐户作为Authentication
模式,SQLite
作为数据库存储dotnet new mvc --auth Individual -o AuthSample
-
您需要通过调用
Startup.cs
的ConfigureServices
方法中的AddRoles<IdentityRole>()
来启用角色服务。您可以参考以下代码来启用它。您还会注意到RequireConfirmedAccount
设置为false
。这是本示例所必需的,因为我们以编程方式创建用户:public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>(options => options.UseSqlite( Configuration.GetConnectionString("DefaultConnection"))); services.AddDatabaseDeveloperPageExceptionFilter(); services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = false) .AddRoles<IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>(); services.AddControllersWithViews(); }
-
接下来,我们需要创建角色和用户。为此,我们将在
Startup.cs
中添加两种方法SetupRoles
和SetupUsers
。我们可以利用RoleManager
和UserManager
服务来创建角色和用户。在下面的代码中,我们创建了三个角色。使用IServiceProvider
,我们得到一个roleManager
服务的实例,然后我们使用RoleExisysAsync
和CreateAsync
方法创建它://Add this method to Startup.cs private async Task SetupRoles(IServiceProvider serviceProvider) { var rolemanager = serviceProvider .GetRequiredService<RoleManager<IdentityRole>>(); string[] roles = { "Admin", "Support", "User" }; foreach (var role in roles) { var roleExist = await rolemanager.RoleExistsAsync(role); if (!roleExist) { await rolemanager.CreateAsync(new IdentityRole(role)); } } }
-
同样,我们使用
userManager
服务创建用户并分配其中一个角色。在下面的代码中,我们创建了两个用户-admin@abc.com
,分配了admin
角色,support@abc.com
,分配了support
角色://Add this method to Startup.cs private async Task SetupUsers(IServiceProvider serviceProvider) { var userManager = serviceProvider .GetRequiredService<UserManager<IdentityUser>>(); var adminUser = await userManager.FindByEmailAsync("admin@abc.com"); if (adminUser == null) { var newAdminUser = new IdentityUser { UserName = "admin@abc.com", Email = "admin@abc.com", }; var result = await userManager .CreateAsync(newAdminUser, "Password@123"); if (result.Succeeded) await userManager.AddToRoleAsync(newAdminUser, "Admin"); } var supportUser = await userManager .FindByEmailAsync("support@abc.com"); if (supportUser == null) { var newSupportUser = new IdentityUser { UserName = "support@abc.com", Email = "support@abc.com", }; var result = await userManager .CreateAsync(newSupportUser, "Password@123"); if (result.Succeeded) await userManager.AddToRoleAsync(newSupportUser, "Support"); } }
-
要调用这两个方法,我们需要将注入到
Configure
方法中。IServiceProvider
作为注入参数增加:public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider) { // // SetupRoles(serviceProvider).Wait(); SetupUsers(serviceProvider).Wait(); }
-
在
Home
控制器内部,添加以下代码。为了简化实现,我们使用Index
视图。在现实场景中,您需要返回为各个动作方法创建的视图:[Authorize(Roles = "Admin")] public IActionResult Admin() { return View("Index"); } [Authorize(Roles = "Support")] public IActionResult Support() { return View("Index"); }
-
或者,我们可以向
Layout.cshtml
添加逻辑,根据登录用户的角色显示导航链接。以下示例使用IsInRole
检查用户角色并显示链接:<li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a> </li> @if (User.IsInRole("Admin")) { <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Admin">Admin</a> </li> } @if (User.IsInRole("Support")) { <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Support">Support</a> </li> }
有了前面的步骤,示例实现就完成了,您可以运行应用来看看它是如何工作的。运行应用,用admin@abc.com
登录,会注意到菜单项管理可见,支持隐藏。当您使用support@abc.com
登录时,您会注意到支持可见,管理菜单项隐藏。
在下一节中,我们将看到如何使用声明进行授权。
基于声明的授权
声明是成功认证后与身份相关联的键值对。索赔可以是出生日期、性别、邮政编码等。一个或多个声明可以分配给用户。基于声明的授权使用声明的值,并确定是否可以授予对资源的访问权限。您可以使用两种方法来验证索赔,一种方法是检查索赔是否存在,另一种方法是检查索赔是否以特定的值存在。
要使用基于声明的授权,我们需要在Startup.cs
的ConfigureServices
方法中注册一个策略。您需要将索赔名称和可选值传递给RequireClaim
方法进行注册。例如,以下代码将PremiumContentPolicy
注册到PremiumUser
权利要求的要求中:
services.AddAuthorization(options =>
{
options.AddPolicy("PremiumContentPolicy",
policy => policy.RequireClaim("PremiumUser"));
});
在下面的代码中,PremiumContentController
上使用了PremiumContentPolicy
授权策略。检查PremiumUser
声明是否存在于授权用户请求的用户声明中;它不在乎索赔中有什么价值:
[Authorize(Policy ="PremiumContentPolicy")]
public class PremiumContentController : Controller
{
public IActionResult Index()
{
return View();
}
}
您也可以在定义索赔时指定值列表。它们将被验证以授予对资源的访问权限。例如,根据下面的代码,如果用户拥有值为US
、UK
或IN
的Country
索赔,则用户请求被授权:
services.AddAuthorization(options =>
{
options.AddPolicy("ExpressShippingPolicy",
policy => policy.RequireClaim(ClaimTypes.Country, "US", "UK", "IN"));
});
以编程方式,如果您想检查用户是否有索赔,您可以通过指定匹配条件来使用ClaimsPrinciple
的HasClaim
方法,要获取索赔值,您可以使用FindFirst
方法。下面的代码说明了一个示例:
@if (User.HasClaim(x => x.Type == "PremiumUser"))
{
<h1>Yay, you are Premium User!!!, @User.FindFirst(x => x.Type == ClaimTypes.Country)?.Value</h1>
}
如实现基于角色的授权部分中的所示,在向应用添加用户时,您也可以使用UserManager
服务向用户添加声明。在下面的代码中,您会注意到IdentityUser
和Claim
调用了AddClaimAsync
方法:
var user = await userManager.FindByEmailAsync("user@abc.com");
if (user == null)
{
var newUser = new IdentityUser
{
UserName = "user@abc.com",
Email = "user@abc.com",
};
var result = await userManager.CreateAsync(newUser, "Password@123");
if (result.Succeeded)
{
await userManager
.AddToRoleAsync(newUser, "User");
await userManager
.AddClaimAsync(newUser, new Claim("PremiumUser", "true"));
await userManager
.AddClaimAsync(newUser, new Claim(ClaimTypes.Country, "US"));
}
}
在前面的代码中,您将注意到使用AddClaimAsync
方法创建了两个声明并与用户相关联。在下一节中,我们将看到如何使用基于策略的授权。
基于策略的授权
基于策略的授权允许编写自己的逻辑来处理适合自己需求的授权需求。例如,您需要验证用户的年龄,并且仅当用户超过 14 岁时才授权下订单。您可以使用基于策略的授权模型来处理这一需求。
为了配置基于策略的授权,我们需要定义一个需求和一个处理程序,然后用需求注册策略。让我们了解这些组件:
- 策略由一个或多个需求定义。
- 需求是策略用来评估用户身份的数据参数的集合。
- 处理程序负责根据上下文评估需求中的数据,并确定是否可以授予访问权限。
在下一节中,我们将看到如何创建需求和处理程序,以及注册授权策略。
要求
要创建需求,需要实现IAuthorizationRequirement
接口。这是一个标记接口;因此,您没有任何要实现的成员。例如,以下代码使用MinimumAge
作为数据参数来创建MinimumAgeRequirement
:
public class MinimumAgeRequirement: IAuthorizationRequirement
{
public int MinimumAge { get; set; }
public MinimumAgeRequirement(int minimumAge)
{
this.MinimumAge = minimumAge;
}
}
需求处理程序
需求处理程序封装允许或拒绝请求的逻辑。他们使用需求属性来确定访问。处理者可以继承TRequirement
属于IAuthorizationRequirement
类型的Authorizationhandler<TRequirement>
,或者实现IAuthorizationHandler
。
在下面的例子中,MinimumAgeAuthorizationHandler
是通过继承AuthorizationHandler
而创建的,其中MinimumAgeRequirement
为TRequirement
。我们需要覆盖HandleRequirementAsync
来编写自定义授权逻辑,其中用户的年龄是从DateOfBirth
声明中计算出来的。如果用户年龄大于或等于MinimumAge
,我们调用context.Succeed
授予访问权限。如果索赔不存在或不符合年龄标准,则禁止访问:
public class MinimumAgeAuthorizationHandler
: AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MinimumAgeRequirement requirement)
{
if (context.User.HasClaim(
c => c.Type == ClaimTypes.DateOfBirth))
{
var dateOfBirth = Convert.ToDateTime(
context.User.FindFirst(x =>
x.Type == ClaimTypes.DateOfBirth).Value);
var age = DateTime.Today.Year - dateOfBirth.Year;
if (dateOfBirth > DateTime.Today.AddYears(-age)) age--;
if (age >= requirement.MinimumAge)
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
要将需求标记为成功,您需要通过将需求作为参数传递来调用context.Succeed
。您不必处理失败,因为相同需求的另一个处理程序可能会成功。如果要禁止请求,可以调用context.Fail
。
注意
必须通过Startup.cs
的ConfigureServices
方法注册处理程序进行服务收集。
注册策略
政策是在Startup.cs
的ConfigureServices
方法中注册一个名称和一个要求。您可以在定义策略时注册一个或多个要求。
在下面的示例中,通过调用policy.Requirements.Add()
并传递MinimumAgeRequirement
的新实例来创建带有需求的策略。您还会注意到MinimumAgeAuthorizationHandler
被添加到具有单例范围的服务集合中:
services.AddAuthorization(options =>
{
options.AddPolicy("Over14", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(14)));
});
services.AddSingleton<IAuthorizationHandler,
MinimumAgeAuthorizationHandler>();
然后,我们可以在控制器或操作上配置授权策略,根据用户的年龄限制访问:
[Authorize(Policy ="Over14")]
public class OrdersController : Controller
{
public IActionResult Index()
{
return View();
}
}
如果我们注册了一个具有多个需求的策略,那么成功授权必须满足所有需求。
在下一节中,我们将学习如何进一步定制授权。
自定义授权
在上一节中,我们学习了如何使用基于策略的授权并实现自定义逻辑来处理授权需求。但是并不总是可以像这样在Startup.cs
中注册授权策略。在本节中,我们将看到如何使用IAuthorizationPolicyProvider
在您的应用中动态构建策略配置。
IAuthorizationPolicyProvider
界面有三种方法需要实现:
GetDefaultPolicyAsync
:此方法返回要使用的默认授权策略。GetFallbackPolicyAsync
:此方法返回回退授权策略。当没有定义明确的授权要求时使用。GetPolicyAsync
:此方法用于为提供的策略名称构建并返回授权策略。
让我们看一个例子,其中您想要基于不同的年龄标准将请求授权给几个控制器/动作,比如Over14
、Over18
、Over21
、Over60
等等。实现它的一种方法是将所有这些需求注册为策略,并在您的控制器或操作上使用它们。但是使用这种方法,代码的可维护性会降低,并且在具有许多策略的大型应用中不可持续。让我们看看如何利用授权策略提供程序。
我们需要创建一个实现IAuthorizationPolicyProvider
的类,还需要实现GetPolicy
等方法。
在下面的例子中,MinimumAgePolicyProvider
类实现了GetPolicyAsync
。此方法的输入是策略名称。由于我们的策略名称类似于Over14
或Over18
,我们可以使用字符串函数并从中提取年龄,并且一个要求用所需的年龄初始化,并且注册为一个 ne w 策略:
public class MinimumAgePolicyProvider : IAuthorizationPolicyProvider
{
const string POLICY_PREFIX = "Over";
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
if (policyName.StartsWith(POLICY_PREFIX, StringComparison.OrdinalIgnoreCase) &&
int.TryParse(policyName.Substring(POLICY_PREFIX.Length), out var age))
{
var policy = new AuthorizationPolicyBuilder();
policy.AddRequirements(new MinimumAgeRequirement(age));
return Task.FromResult(policy.Build());
}
return Task.FromResult<AuthorizationPolicy>(null);
}
}
注意
MinimumAgeRequirement
的实现请参考政策授权部分。
ASP.NET Core 只使用了IAuthorizationPolicyProvider
的一个实例。因此,您应该定制一个Default
和Fallback
授权策略或者使用一个备份提供商。
在下面的代码中,您将看到一个在MinimumAgePolicyProvider
类中的GetDefaultPolicyAsync
和GetFallbackPolicyAsync
方法的示例实现。
AuthorizationOptions
被注入到构造函数中,用于初始化DefaultAuthorizationPolicyProvider
。BackupPolicyProvider
对象用于实现GetDefaultPolicyAsync
和GetFallbackPolicyAsync
方法:
public MinimumAgePolicyProvider(IOptions<AuthorizationOptions> options)
{
this.BackupPolicyProvider =
new DefaultAuthorizationPolicyProvider(options);
}
Private DefaultAuthorizationPolicyProvider BackupPolicyProvider { get; }
public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
=> this.BackupPolicyProvider.GetDefaultPolicyAsync();
public Task<AuthorizationPolicy> GetFallbackPolicyAsync()
=> this.BackupPolicyProvider.GetFallbackPolicyAsync();
MinimumAgePolicyProvider
的实现到此结束。现在,您可以在控制器或操作方法上使用授权策略。在下面的代码中,您会注意到使用了两种策略,一种是在控制器上使用Over14
,另一种是在Index
动作方法上使用Over18
:
[Authorize(Policy ="Over14")]
public class OrdersController : Controller
{
[Authorize(Policy ="Over18")]
public IActionResult Index()
{
return View();
}
}
年龄在 14 岁以上的用户可以使用OrdersController
以下的任何动作方法,18 岁以上的用户只能使用Index
动作。
在下一节中,我们将学习如何创建和使用自定义授权属性。
自定义授权属性
在前面的示例中,带有年龄的策略名称作为字符串传递,但是代码并不干净。如果能把age
作为参数传递给授权属性就好了。为此,您需要创建一个继承AuthorizeAttribute
类的自定义授权属性。
在下面的示例代码中,AuthorizeAgeOverAttribute
类继承自AuthorizeAttribute
类。该类的构造函数接受age
作为输入。在设置器中,我们通过连接Policy_Prefix
和age
来构造和设置策略名称:
public class AuthorizeAgeOverAttribute : AuthorizeAttribute
{
const string POLICY_PREFIX = "Over";
public AuthorizeAgeOverAttribute(int age) => Age = age;
public int Age
{
get
{
if (int.TryParse(Policy.Substring(POLICY_PREFIX.Length), out var age))
{
return age;
}
return default(int);
}
set
{
Policy = $"{POLICY_PREFIX}{value.ToString()}";
}
}
}
既然自定义属性实现已经完成,我们可以在控制器或动作方法上使用它。在下面的示例中,您可以看到一个示例实现,其中年龄作为参数传递给我们的自定义授权属性AuthorizeAgeOver
:
[AuthorizeAgeOver(14)]
public class OrdersController : Controller
{
[AuthorizeAgeOver(18)]
public IActionResult Index()
{
return View();
}
}
在下一节中,我们将学习如何在 Azure AD 应用中配置角色并使用基于角色的身份验证。
客户端和服务器应用中的授权
在前面的章节中,我们学习了如何使用Azure Active Directory(AAD)作为身份服务来认证用户,但是要使用基于角色的授权,我们需要在 Azure AD 中做一些配置上的更改。在本节中,我们将看到如何在 Azure 广告应用中启用和创建自定义角色,并在我们的电子商务应用中这样做来授权用户。
当用户登录到应用时,Azure AD 会将分配的角色和声明添加到用户的身份中。
先决条件
您应该已经安装了 Azure 广告和广告应用。如果没有,可以参考 第 12 章了解认证的Azure Active Directory 简介部分进行设置。
让我们来看看在 Azure AD 应用上启用角色需要执行的步骤:
-
在 Azure 门户中,导航至您的活动 目录租户。
-
In the left menu, under Manage, select App registrations:
图 13.1–Azure AD 应用
从应用注册页面搜索并选择您的广告应用。参考以下截图:
图 13.2–Azure AD 应用
-
Click on Manifest from the left menu to edit it, shown in previous screenshot.
图 13.3–编辑清单
-
Locate
appRoles
to configure multiple roles. Refer to the following code to add a role:{ "allowedMemberTypes": [ "User" ], "description": "Admin Users", "displayName": "Admin", "id": "6ef9b400-0219-463c-a542-5f4693c4e286", "isEnabled": true, "lang": null, "origin": "Application", "value": "Admin" }
您需要提供
displayName
、value
、description
和id
的值。id
的值是Guid
,对于你添加的每个角色它必须是唯一的。同样,对于value
,您需要提供您在代码中引用的角色名称,并且它应该是唯一的。 -
保存清单以完成它。
保存带有所需细节的清单将在 Azure AD 应用的中启用自定义角色。在下一节中,我们将学习如何将用户分配给这些自定义角色。
为用户分配角色
下一步是给用户分配角色。可以使用 Azure 门户或使用图形应用编程接口以编程方式将角色分配给用户。在本节中,我们将使用 Azure 门户来分配角色,同样也可以使用 Graph API 来实现。更多信息可参考https://docs . Microsoft . com/en-us/graph/azure ad-identity-access-management-concept-overview:
- 在 Azure 门户中,导航至 Azure 活动 目录租户。
- 点击左侧菜单中的企业应用,搜索并选择您的广告应用。
- 转到管理 | 用户和组 | 添加用户。
- 搜索并选择用户,点击确定。
- 点击选择角色选择想要分配的角色。
- 点击分配保存选择。
您可以继续这些步骤,将角色分配给多个用户。
为了保护控制器或动作,您可以添加一个Authorize
属性以及角色。在下面的代码中,Admin
控制器只能由具有Admin
角色的用户访问:
[Authorize(Roles ="Admin")]
public class AdminController : Controller
{
public IActionResult Index()
{
return View();
}
}
到目前为止,我们已经学习了如何在 Azure AD 中启用角色,并使用基于角色的模型进行授权。在下一节中,我们将看到如何在视图中使用用户的身份来访问角色和声明。
视图中的用户身份
用户声明原则可用于视图中,根据需要有条件地显示或隐藏数据。例如,下面的代码检查用户身份的IsAuthenticated
属性,以确定用户是否通过身份验证。如果用户未通过认证,将显示到Sign in
的链接,否则,将显示带有Sign out
链接的用户名:
<ul class="navbar-nav">
@if (User.Identity.IsAuthenticated)
{
//// HTML code goes here
}
else
{
////
}
</ul>
同样,我们可以使用IsInRole
或HasClaim
并编写我们的逻辑向用户显示内容或隐藏内容:
@if (User.HasClaim(x => x.Type == "PremiumUser"))
{
<h1>Yay, you are Premium User!!!, @User.FindFirst(x => x.Type == ClaimTypes.Country)?.Value</h1>
}
总结
在本章中,我们了解了什么是授权,以及使用 ASP.NET Core 框架实现授权的不同方式。我们学习了如何使用简单的、声明性的基于角色和声明的模型来限制或匿名允许用户访问资源,并学习了如何使用丰富的基于策略的授权模型来实现自定义逻辑来授权用户请求。
我们学习了如何使用授权策略提供程序动态添加授权策略,并构建自定义授权属性。我们还学习了如何在 Azure AD 中配置自定义角色,并在 ASP.NET Core 应用中使用它们。根据您的授权要求,您可以使用一个或多个授权模型来保护您的应用。
在下一章中,我们将学习如何监控 ASP.NET Core 应用的运行状况和性能。
问题
阅读本章后,您应该能够回答以下问题:
-
Which of the following is the primary service that determines whether authorization is successful or not?
a.
IAuthorizationHandler
b.
IAuthorizationRequirement
c.
IAuthorizationService
d.
IAuthorizationPolicyProvider
-
In the following code, access to the
Support
action is restricted to only theSupport
role:[AllowAnonymous] public class HomeController : Controller { public IactionResult Index() { return View(); } [Authorize(Roles ="Support")] public IactionResult Support() { return View(); } }
a.真实的
b.错误的
进一步阅读
想了解更多授权信息,可以参考https://docs . Microsoft . com/en-us/aspnet/core/security/authorization/introduction?view=aspnetcore-5.0 。
十四、健康与诊断
现代软件应用已经发展成为复杂和动态的,并且本质上是分布式的。对这些应用的高要求是能够在任何地方、任何设备上全天候工作。为了实现这一点,知道我们的应用随时可用并响应请求是很重要的。客户体验将在服务的未来和组织的收入中发挥重要作用。
应用运行后,监控应用的运行状况至关重要。定期的应用运行状况监控将有助于我们主动检测任何故障,并在它们造成更大损害之前解决它们。应用监控现在已经成为日常操作的一部分。为了诊断实时应用的任何故障,我们需要正确的遥测和诊断工具。我们捕获的遥测数据也将帮助我们识别那些用户没有直接看到或报告的问题。
让我们了解一下应用运行状况监控以及中提供的内容.NET 5。
在本章中,我们将了解以下主题:
- 引入健康检查
- ASP.NET Core 5 的健康检查空气污染指数
- 利用应用洞察监控应用
- 执行远程调试
到本章结束时,您将很好地掌握为构建运行状况检查应用编程接口.NET 5 应用和 Azure 应用,用于捕获遥测数据和诊断问题。
技术要求
我们需要以下软件来完成本章中的任务:
- 安装了 Azure 开发工作负载的 Visual Studio 2019 企业版
- 蓝色订阅
对微软的基本了解.NET 以及如何在 Azure 中创建资源。
引入健康检查
健康检查是对应用的全面审查,有助于我们了解应用的当前状态,并使用可见的指标来采取纠正措施。运行状况检查被应用公开为 HTTP 端点。健康检查端点用作某些协调器和负载平衡器的健康探测器,以将流量从故障节点路由出去。运行状况检查用于监控应用依赖关系,如数据库、外部服务和缓存服务。
在下一节中,我们将了解对在 ASP.NET Core 5 中构建运行状况检查 API 的支持。
ASP.NET Core 5 健康检查 API
ASP.NETCore 5 有一个内置的中间件(可通过Microsoft.Extensions.Diagnostics.HealthChecks
NuGet 包获得)报告作为 HTTP 端点公开的应用组件的健康状态。这个中间件使得集成数据库、外部系统和其他依赖项的健康检查变得非常容易。它也是可扩展的,因此我们可以创建自己的自定义健康检查。
在下一部分,我们将向我们的电子商务门户添加一个健康检查端点。
添加健康检查端点
在本节中,我们将向我们的Packt.Ecommerce.Web
应用添加一个健康检查端点:
-
In order to add a health check endpoint, we need to first add the
Microsoft.Extensions.Diagnostics.HealthChecks
NuGet package reference to thePackt.Ecommerce.Web
project, as shown in the following screenshot:图 14.1–NuGet 参考微软。扩展、诊断、健康检查
-
现在我们需要向依赖容器注册
HealthCheckService
。我们只需在Startup
类的ConfigureServices
方法中调用IServiceCollection
上的扩展方法就可以做到这一点,如下面的代码片段所示。AddHealthChecks
方法增加了DefaultHealthCheckService
模块:public void ConfigureServices(IServiceCollection services) { // Removed code for brevity. // Add health check services to the container. services.AddHealthChecks(); }
-
现在让我们继续并在
Startup
类的Configure
方法中配置健康检查端点。使用MapHealthChecks
方法映射健康端点,如下代码所示。这将向应用添加运行状况检查端点路由。这将在内部配置HealthCheckResponseWriters.WriteMinimalPlainText
框架方法以发出响应。WriteMinimalPlainText
将刚刚发布健康检查服务的整体状态:public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // Removed code for brevity. app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Products}/{action=Index}/{id?}"); endpoints.MapHealthChecks("/health"); }); }
-
运行应用,浏览至
<<Application URL>>/health
网址。您将看到以下输出:
图 14.2–健康检查终点响应
我们添加的健康端点提供了关于服务可用性的基本信息。在下一节中,我们将看到如何监控从属服务的状态。
监控从属 URIs
一个企业应用依赖于多个其他组件,如数据库和 Azure 组件,包括KeyVault
,其他微服务,如我们的电子商务网站依赖于订单服务、产品服务等。这些服务可以由同一组织内的其他团队拥有,或者在某些情况下,它们可能是外部服务。监控依赖服务通常是一个好主意。我们可以利用AspNetCore.HealthChecks.Uris
NuGet 包来监控相关服务的可用性。
让我们继续增强我们的健康端点,以监控产品和订单服务:
-
Add the NuGet package reference to
AspNetCore.HealthChecks.Uris
. Now modify the health check registration to register the Product and Order services as shown in the following code snippet:public void ConfigureServices(IServiceCollection services) { // Add health check services to the container. services.AddHealthChecks() .AddUrlGroup(new Uri(this.Configuration.GetValue<string>("ApplicationSettings:ProductsApiEndpoint")), name: "Product Service") .AddUrlGroup(new Uri(this.Configuration.GetValue<string>("ApplicationSettings:OrdersApiEndpoint")), name: "Order Service"); }
健康检查中间件还提供关于个人健康检查状态的详细信息。
-
Let's now modify our health check middleware to emit the details as shown in the following code:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // Removed code for brevity. app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Products}/{action=Index}/{id?}"); endpoints.MapHealthChecks("/health", new HealthCheckOptions { ResponseWriter = async (context, report) => { context.Response.ContentType = "application/json"; var response = new { Status = report.Status.ToString(), HealthChecks = report.Entries.Select(x => new { Component = x.Key, Status = x.Value.Status.ToString(), Description = x.Value.Description, }), HealthCheckDuration = report.TotalDuration, }; await context.Response.WriteAsync(JsonConvert.SerializeObject(response)).ConfigureAwait(false); }, }); }); }
在这段代码中,健康检查中间件被覆盖,通过向
HealthCheckOptions
提供ResponseWriter
来写入状态、健康检查持续时间、组件名称和描述的细节作为其响应。 -
现在,如果我们运行项目并导航到运行状况检查应用编程接口,我们应该会看到以下输出:
图 14.3–带有状态的健康检查端点响应
我们已经学习了如何定制健康检查端点的响应,以及如何利用第三方库来监控依赖 URIs 的状态。如果您希望集成对通过实体框架核心使用的数据库的检查,您可以利用Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore
库。更多关于使用这个库的信息可以在上找到?view = aspnet core-5.0 # entity-framework-core-db context-probe。在 https://github.com/Xabaril/AspNetCore.可以找到更多不同服务的健康检查套餐诊断。健康检查。在下一节中,我们将学习如何构建自定义运行状况检查。
建立自定义健康检查
ASP.NET Core 5 中的健康检查中间件是可扩展的,这意味着它允许我们扩展和创建定制的健康检查。我们将通过构建过程监视器来学习如何构建和使用自定义运行状况检查。在某些情况下,可能需要监控机器上运行的特定进程。如果进程(例如,反恶意软件服务)没有运行,或者如果第三方 SaaS 产品的许可证即将到期,我们可能会将其标记为健康问题。
让我们开始在Packt.Ecommerce.Common
项目中创建ProcessMonitor
健康检查:
-
Add a project folder named
HealthCheck
toPackt.Ecommerce.Common
and add two classes,ProcessMonitor
andProcessMonitorHealthCheckBuilderExtensions
, as shown in the following screenshot:图 14.4–添加自定义运行状况检查后的项目结构
自定义
HealthCheck
中间件要求 NuGet 引用为microsoft.extensions.diagnostics.healthchecks
。 -
ASP.NET Core 5 中的自定义健康检查应实现
IHealthCheck
界面。这个接口定义了当请求到达healthcheck
应用编程接口时将被调用的CheckHealthAsync
方法。 -
Implement the
ProcessMonitorHealthCheck
class as shown in the following code:public class ProcessMonitorHealthCheck : IHealthCheck { private readonly string processName; public ProcessMonitorHealthCheck(string processName) => this.processName = processName; public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { Process[] pname = Process.GetProcessesByName(this.processName); if (pname.Length == 0) { return Task.FromResult(new HealthCheckResult(context.Registration.FailureStatus, description: $"Process with the name {this.processName} is not running.")); } else { return Task.FromResult(HealthCheckResult.Healthy()); } } }
在
CheckHealthAsync
方法中,获取processName
中指定名称的进程列表。如果没有这样的过程,则返回健康检查失败,否则,返回失败状态。 -
Now that we have the custom health check middleware, let's add an extension method to register. Modify the
ProcessMonitorHealthCheckBuilderExtensions
class as shown in the following code snippet:public static class ProcessMonitorHealthCheckBuilderExtensions { public static IHealthChecksBuilder AddProcessMonitorHealthCheck( this IHealthChecksBuilder builder, string processName = default, string name = default, HealthStatus? failureStatus = default, IEnumerable<string> tags = default) { return builder.Add(new HealthCheckRegistration( name ?? "ProcessMonitor", sp => new ProcessMonitorHealthCheck(processName), failureStatus, tags)); } }
这是
IHealthCheckBuilder
的扩展方法。我们可以看到在代码片段中添加ProcessMonitorHealthCheck
向容器注册了ProcessMonitorHealthCheck
。 -
现在让我们利用我们已经构建的自定义运行状况检查。在下面的代码中,我们注册了
notepad
的ProcessMonitorHealthCheck
健康检查:public void ConfigureServices(IServiceCollection services) { // Add health check services to the container. services.AddHealthChecks() .AddUrlGroup(new Uri(this.Configuration.GetValue<string>("ApplicationSettings :ProductsApiEndpoint")), name: "Product Service") .AddUrlGroup(new Uri(this.Configuration.GetValue<string>("ApplicationSettings :OrdersApiEndpoint")), name: "Order Service") .AddProcessMonitorHealthCheck("notepad", name: "Notepad monitor"); }
-
现在,当您运行应用并导航到运行状况检查应用编程接口时,如果
notepad.exe
正在您的机器上运行,我们将看到如图 14.5 所示的输出:
图 14.5–来自健康检查端点的响应
我们可以在我们的健康检查端点上启用跨来源资源共享 ( CORS )、授权和主机限制。详见https://docs . Microsoft . com/en-us/aspnet/core/host-and-deploy/health-checks?view=aspnetcore-5.0 。
在某些场景中,运行状况检查 API 根据它们探测的应用的状态分为两种类型。它们是:
- 就绪探测:这些指示应用正在正常运行,但是还没有准备好接受请求。
- 活动性探测:这些指示应用是否已经崩溃,必须重新启动。
就绪性和活动性探测器都用于控制应用的运行状况。失败的就绪探测将停止应用服务流量,而失败的活动性探测将重新启动节点。我们在像 Kubernetes 这样的托管环境中使用就绪性和生动性探测器。
我们已经学习了如何将健康检查应用编程接口添加到 ASP.NET Core 5 应用中。在下一节中,我们将了解 Azure Application Insights 以及它如何帮助监控应用。
用应用洞察监控应用
监控应用是为最终用户提供一流体验的关键。在当前超快速数字市场时代,应用监控是推动业务投资回报和保持竞争优势所必需的。我们应该关注的参数是页面/应用编程接口性能、最常用的页面/应用编程接口、应用错误和系统健康等。当系统出现异常时,应该设置警报,以便我们可以纠正它,并将对用户的影响降至最低。
在 第 7 章登录中,已经向您介绍了将应用洞察集成到应用及其关键特性.NET 5 。让我们在 Azure 门户中打开应用洞察,了解它的不同产品。在概览控制面板上,除了 Azure 订阅、位置和工具键之外,我们还看到如下关键指标:
图 14.6–应用洞察仪表板
失败请求图表显示了在所选持续时间内失败的请求数量。这是我们应该关注的关键指标;许多故障代表系统的不稳定性。服务器响应时间代表服务器对呼叫的平均响应时间。如果响应时间过长,更多的用户将会看到应用响应的滞后,这可能会导致沮丧,结果我们可能会失去用户。
服务器请求图表示对应用的调用总数;这将为我们提供系统中使用的模式。可用性图表示应用的正常运行时间。我们将在本章后面配置的可用性测试将显示可用性图。通过点击每个图表,我们可以获得更多关于相应度量的细节,包括请求和异常细节。我们可以更改持续时间来查看所选时间间隔的图表。
概览仪表板上的图表显示了最近的指标。在我们希望了解系统在过去特定时间的工作情况时,这可能很有用。
在下一节中,我们将了解应用洞察的一些最重要的产品,查看实时指标、遥测事件和远程调试功能。
实时指标
默认情况下启用实时指标。实时指标的捕获延迟为一秒钟,这与分析指标不同,后者是随着时间的推移而聚合的。仅当“实时指标”窗格打开时,实时指标的数据才会进行流式传输。收集的数据只有在图表上时才会保留。在实时指标监控期间,所有事件都从服务器传输,不会被采样。如果应用部署在 web 服务器场中,我们还可以按服务器过滤事件。
实时指标显示各种图表,如传入和传出请求,以及内存和 CPU 利用率的总体健康状况。在右侧窗格中,我们可以看到捕获的遥测数据,它将列出请求、依赖调用和异常。在我们希望通过观察故障率和性能来评估发布到生产环境中的修复时,使用了实时度量。我们还将在运行负载测试时监控这些,以查看负载对系统的影响。
对于像我们的电商应用这样的应用,了解用户如何使用应用、最常用的功能以及用户如何遍历应用非常重要。在下一节中,我们将了解应用洞察中的使用分析。
应用洞察的使用分析
在 第 11 章创建 ASP.NET Core 5 Web 应用中,您学习了如何将应用洞察与视图相集成。当应用洞察与视图集成时,应用洞察可以帮助我们深入了解人们如何使用应用。应用洞察的使用部分下的用户刀片提供了使用该应用的用户数量的详细信息。通过使用存储在浏览器 cookies 中的匿名 id 来识别用户。请注意,使用不同浏览器和机器的一个人被视为多个用户。会话和事件刀片分别代表用户活动的会话和特定页面或功能的使用频率。您也可以基于自定义事件生成关于用户、会话和事件的报告,这是您在 第 7 章登录中了解到的.NET 5 。
使用分析下另一个有趣的工具是用户流。用户流工具可视化了用户如何浏览应用的不同页面和功能。用户流提供在用户会话期间给定事件之前和之后发生的事件。图 14.7 显示给定时间的用户流量。这告诉我们,从主页,用户主要导航到产品详细信息页面或帐户登录页面:
图 14.7–电子商务应用中的用户流
让我们添加几个自定义事件,看看这些自定义事件的用户流是什么样的。在Packt.Ecommerce.Web
应用的OrderController
的Create
动作方法中添加一个自定义事件,如下面的代码片段所示。当用户点击购物车页面上的下单按钮时,这将跟踪一个自定义事件:
this.telemetry.TrackEvent("Create Order");
同样,让我们在用户点击产品详情页面上的添加至购物车按钮时添加自定义事件跟踪。为此,添加以下代码片段:
this.telemetry.TrackEvent("Add Item To Cart");
添加自定义事件后,用户流将显示与这些事件相关的应用的不同活动。用户流是一个方便的工具,可以知道有多少用户正在离开页面,以及他们在页面上点击了什么。请参考本章末尾的进一步阅读部分中提供的 Azure 应用洞察文档,了解更多关于其他有趣的使用分析产品的信息,包括队列、漏斗、保留等。
当有足够多的遥测事件时,您可以使用名为智能检测的应用洞察功能,该功能会自动检测系统中的异常并向我们发出警报。在下一节中,我们将了解智能检测。
智能检测
智能检测不需要任何配置或代码更改。它对从系统获取的遥测数据起作用。系统中的智能检测刀片下将显示警报,这些警报将发送给具有监控阅读器和监控贡献者角色的用户。我们可以在设置选项下为这些警报配置其他收件人。一些智能检测规则包括页面加载时间慢、服务器响应时间慢、每日数据量异常增加、依赖量下降。
我们需要监控应用的一个重要方面是可用性。在下一节中,我们将学习如何利用应用洞察来监控应用可用性。
应用可用性
在应用洞察中,我们可以为任何可从互联网访问的http
或https
端点设置可用性测试。这不需要对我们的应用代码进行任何更改。我们可以在(<App Root URL>/health)
为可用性测试配置健康检查端点。
要配置可用性测试,请转到 Azure 门户中的应用洞察资源,并执行以下步骤:
-
Select Availability under the Investigate menu, as shown here:
图 14.8–应用洞察的可用性部分
-
点击上的添加测试添加可用性测试,如前一个截图中突出显示的。
-
在创建测试对话框中,指定测试名称(比如
Commerce availability test
),选择测试类型的网址 Ping 测试,在网址字段中,输入健康检查网址为<<App root url>>/health
。将其他选项保留为默认值,点击创建。 -
Once the test is configured, Application Insights will call the configured URL every 5 minutes from all the configured regions. We can see the availability test results as follows:
图 14.9–可用性测试结果
-
创建测试时选择的默认地区为巴西南部、东亚、日本东部、东南亚和英国南部。我们可以添加或删除将运行可用性测试的任何区域。建议至少配置五个区域。
-
如果我们想在稍后的时间点添加一个新的区域,我们可以编辑可用性测试并选择新的区域(例如西欧),如下图所示,然后点击保存:
图 14.10–编辑可用性测试区域
我们还可以在应用洞察中将配置为多步骤网络测试,作为可用性测试。
注意
您可以使用以下文档来帮助您配置多步骤 web 测试:https://docs . Microsoft . com/en-us/azure/azure-monitor/app/availability-multi step。
Application Insights 为查询捕获的遥测事件提供了一个非常好的工具。在下一节中,我们将了解应用洞察中的搜索功能。
搜索
应用洞察中的搜索功能有助于探索遥测事件,如请求、页面视图和异常。我们还可以查询我们在应用中编码的跟踪。搜索可从概述选项卡或从调查选项卡的搜索选项打开:
图 14.11–搜索结果
通过事务搜索功能,我们可以根据时间和事件类型过滤显示的遥测事件。
我们也可以过滤它们的属性。通过点击特定事件,我们可以查看该事件的所有属性以及该事件的遥测数据。要查看状态代码为 500 的请求,请根据响应代码过滤事件,如下所示:
图 14.12–过滤搜索结果
一旦我们应用了过滤器,在搜索结果中我们将只看到响应代码为 500 的请求,如下图所示的:
图 14.13–过滤后的搜索结果
要了解导致失败的更多原因,请单击事件。点击该事件将显示相关遥测的详细信息,如下图所示:
图 14.14–端到端交易详情
我们甚至可以通过点击异常来深入更多。这将显示详细信息,如方法名和堆栈跟踪,这将帮助我们确定失败的原因。
借助应用洞察,我们可以对捕获的遥测数据编写自定义查询,以获得更有意义的洞察。在下一节中,我们将学习如何编写查询。
日志
要对捕获的遥测数据进行查询,让我们按如下方式导航:
- 转到应用洞察 | 监控 | 日志。这将显示带有示例查询的日志页面,我们可以运行:
图 14.15–应用洞察日志
在建议的样本查询中选择请求计数趋势。这将为我们生成一个查询并运行它。一旦运行完成,我们将看到结果和图表,如下图所示:
图 14.16–记录搜索结果
在应用洞察中捕获的遥测数据进入不同的表,包括请求、异常、依赖、跟踪和页面视图。这里生成的查询总结了请求表中的遥测数据,并呈现了一个时间轴被 30 分钟分割的时间图。
我们根据自己的要求选择时间范围。我们甚至可以在查询中指定时间范围,而不是从菜单选项中选择。在此创建的这些查询可以保存并在以后重新运行。这里还有一个配置提醒的选项,我们在 第 7 章登录中了解到.NET 5 。这里用来编写查询的语言是 Kusto。
注意
参考以下文档了解 Kusto 查询语言:https://docs . Microsoft . com/en-us/azure/data-explorer/Kusto/concepts/
Kusto 基于关系数据库结构。使用库斯托查询语言,我们可以编写复杂的分析查询。Kusto 支持分组聚合、计算列和连接函数。
让我们再举一个例子,我们希望确定每个客户城市的第 95 个百分点的服务响应时间。对此的查询将编写如下:
requests
| summarize 95percentile=percentile(duration, 0.95) by client_City
| render barchart
在前面的查询中,我们使用percentile
函数来识别第 95 个百分位,对每个区域进行汇总。结果呈现为条形图。
对于前面的查询,我们看到了下图:
图 14.17–库斯托百分位汇总结果
从渲染图中,我们可以推断来自钦奈的请求的响应时间比来自 T2 的请求要快。
现在,让我们找到导致请求失败的任何异常,并按请求和异常类型对它们进行总结。为了得到这个结果,我们将把requests
表与exceptions
连接起来,并根据请求名称和异常类型对它们进行汇总,如下查询所示:
requests
| join kind= inner (
exceptions
) on operation_Id
| project requestName = name, exceptionType = type
| summarize count=sum(1) by requestName, exceptionType
如果我们运行查询,我们会得到按请求名称和异常类型汇总的结果,如下图所示:
图 14.18–库斯托请求失败异常
搜索是 Application Insights 的一个强大功能,用于诊断和修复生产现场的故障。建议点击应用洞察的不同功能并进行探索。
为了更好地分析和排除生产故障,我们可能想知道发生特定错误时应用的状态。在下一节中,我们将了解应用洞察的快照调试器功能如何帮助我们实现这一点。
快照调试器
快照调试器监控我们应用的异常遥测。它使用源代码和变量的当前状态自动收集应用中发生的顶级异常的快照。
注意
快照调试器功能仅在 Visual Studio 的企业版中可用。
现在让我们继续为我们的电子商务应用配置快照调试器:
-
将
Microsoft.ApplicationInsights.SnapshotCollector
NuGet 包添加到Packt.Ecommerce.Web
项目中。 -
在
Startup.cs
中增加以下using
语句:using Microsoft.ApplicationInsights.SnapshotCollector;
-
通过在
ConfigureServices
方法中添加以下行,将快照收集器添加到您的服务中:public void ConfigureServices(IServiceCollection services) { services.AddApplicationInsightsTelemetry(this.Configuration["ApplicationInsights:InstrumentationKey"]); services.AddSnapshotCollector((configuration) => this.Configuration.Bind(nameof(SnapshotCollectorConfiguration), configuration)); }
-
要模拟故障,请将以下代码添加到
EcommerceService
类的GetProductsAsync
方法中。如果有任何产品可用,该代码将抛出一个错误:public async Task<IEnumerable<ProductListViewModel>> GetProductsAsync(string filterCriteria = null) { // Code removed for brevity if (products.Any()) { throw new InvalidOperationException(); } return products; }
-
现在让我们继续运行应用。我们在主页上看到一个错误。再次刷新页面,因为调试快照针对的是至少出现两次的错误。
-
Now open the Search tab in Application Insights. Filter by the Exception Event types:
图 14.19–异常遥测
-
Click on the exception to go to the details page. On the details page, we see that the debug snapshot has been created for the EXCEPTION, as highlighted in the following screenshot:
图 14.20–调试快照
-
Click on the Debug Snapshot icon. This will take us to the Debug Snapshot page :
图 14.21–调试快照窗口
-
要查看调试快照,需要应用洞察快照调试器角色。由于调试状态可能包含敏感信息,因此默认情况下不会添加此角色。点击添加应用洞察快照调试器角色按钮。这将向当前登录的用户添加角色。
-
Once the role addition is complete, we can then see the debug snapshot details populated on the page, along with a button to download the snapshot:

图 14.22–下载调试快照
- Click on the Download Snapshot button. The extension of the downloaded debug snapshot file is
diagsession
. Open the downloadeddiagsession
file in Visual Studio:

图 14.23–Visual Studio 中的调试快照视图
- Now click on Debug with Managed Only to start the debug session. Once the debug session is open, we see the exception is broken at the line where we throw
InvalidOperationException
:

图 14.24–在 Visual Studio 中调试快照
在这个会话中,我们可以添加一个观察器,并查看局部变量和类变量的状态。
注意
有关快照调试器配置的更多信息,请参考以下文档:https://docs . Microsoft . com/en-us/azure/azure-monitor/app/Snapshot-Debugger-VM。
随着应用的增长和与其他多种服务的集成,对生产环境中出现的问题进行故障排除和调试将是一项挑战。在某些情况下,不可能在生产前环境中复制它们。借助我们捕获的遥测数据和应用洞察工具,我们将能够分析问题并解决问题。快照调试器是解决关键问题的强大工具。Application Insights 收集遥测数据,并通过后台进程分批发送。使用 Application Insights 对我们的应用的影响很小。
可能有些情况下,我们希望调试一个实时应用。使用 Visual Studio,我们能够将调试器附加到远程运行的应用上来调试它。在下一节中,我们将学习如何实现这一点。
执行远程调试
在本节中,我们将学习如何在 Azure App Service 中将调试器附加到我们部署的应用。使用 Visual Studio 提供的工具调试远程应用很容易。在 Azure 应用服务中部署应用在 第 16 章中介绍了在 Azure 中部署应用。我们可以通过执行以下操作将调试器附加到已经部署的服务:
-
从 Visual Studio | 中打开云资源管理器查看 | 云资源管理器。
-
In Cloud Explorer, locate the deployed Azure App Service instance:
图 14.25–Visual Studio 的云资源管理器
-
要将调试器附加到应用服务,请在操作窗格中选择附加调试器。
-
附加调试器后,应用将从 Azure 应用服务在浏览器中打开。我们可以像在本地开发环境中一样,在 Visual Studio 中添加断点并调试应用。
虽然这是调试远程部署的应用的一个强大功能,但是当将调试器附加到生产实例时,我们应该格外小心,因为我们将看到实时的客户数据。我们可以将调试器附加到 Azure 应用服务的暂存槽来调试和修复该问题,然后从那里交换暂存槽来将修复升级到生产。应用洞察和 Azure Monitor 中还有许多本章没有涉及的重要功能。强烈建议在 Azure 文档中进一步探讨它们。
总结
本章向您介绍了使用 Application Insights 进行运行状况检查和诊断应用问题的概念。我们已经学习了如何构建健康检查应用编程接口,并向我们的电子商务应用添加健康检查模块,这将帮助我们监控应用的健康状况。本章还介绍了 Azure Application Insights 的一些关键特性,这是一个捕获遥测和诊断问题的强大工具。
我们已经了解了 Application Insights 如何使用智能检测功能检测异常和警报。我们还了解了快照和远程调试,它们有助于解决在生产环境中运行的实时应用中的问题。
在下一章中,我们将学习不同的测试方法,以确保应用在部署到生产之前的质量。
问题
读完本章,我们应该能够回答以下问题:
-
Periodic monitoring of the application is not that important for an application once it is deployed to production.
a.真实的
b.错误的
-
What is the interface that a custom health check module should implement?
a.
IHealth
b.
IApplicationBuilder
c.
IHealthCheck
d.
IWebHostEnvironment
-
What is the latency in displaying Live Metrics data in Application Insights?
a.一分钟
b.一秒钟
c.10 秒
d.5 秒钟
-
What is the query language used to write queries in Application Insights logs?
a.结构化查询语言
b.C#
c.Java Script 语言
d .库索
进一步阅读
十五、测试
任何应用的成功都取决于用户使用它的难易程度。任何软件产品的寿命都直接取决于产品的质量。
测试是软件开发生命周期 ( SDLC )的一个重要方面,确保产品满足客户的要求和质量要求。随着我们进入 SDLC 的后期阶段,修复 bug 的成本增加,测试也很重要。
在本章中,我们将了解不同类型的测试和 Visual Studio 为测试提供的工具,以及我们可以用来确保我们构建的产品质量的第三方工具.NET 5。
在本章中,我们将了解以下内容:
- 测试类型
- 理解单元测试
- 理解功能测试
- 理解负载测试的重要性
到本章结束时,您将了解确保产品质量所需了解的一切。
技术要求
您将需要 Visual Studio 2019 企业版。这只是付费版,可以从https://visualstudio.microsoft.com/下载。
你还需要对微软. NET 有一个基本的了解
引入测试
软件测试是检查应用是否按照预期运行的一种方式。这些期望可能与功能性、响应性或软件运行时消耗的资源有关。
根据软件测试的执行方式,软件测试可以大致分为以下两类:
- 手动测试:在手动测试中,测试人员通过使用被测应用并验证预期结果,手动执行测试用例。手动测试需要比替代方法更多的努力。
- 自动化测试:自动化测试由专门的自动化测试软件执行。这个自动化的软件在一个专门的环境中运行在被测试的应用上,并验证预期的输出。自动化测试节省了大量的时间和人力。在某些情况下,要实现 100%自动化并以少得多的投资回报来维护汽车,可能需要付出很大的努力。
根据关于被测应用内部的已知信息,如代码流、相关模块集成等,软件测试也可以用以下方式进行广泛分类:
- 黑盒测试:在黑盒测试中,负责测试的个人没有关于系统内部的信息。这里的重点是系统的行为。
- 白盒测试:在白盒测试中,测试仪有关于系统内部结构、设计和实现的信息。白盒测试的重点是测试实现中存在的替代路径。
在软件测试中,我们验证应用的不同方面。
软件测试也有以下变体,基于它验证的应用的方面和它使用的工具或框架:
- 单元测试:单元测试关注于应用的最小单元。这里我们验证单个类或函数。这主要是在开发阶段完成的。
- 功能测试:这通常被称为集成测试。其主要目标是确保应用按照要求运行。
- 回归测试:回归测试确保任何最近的更改没有对应用性能产生不利影响,并且现有的功能不会受到任何更改的影响。在回归测试中,根据应用中引入的变化,执行全部或部分功能测试用例。
- 冒烟测试 : A 冒烟测试在每次部署后进行,以确保应用稳定并准备好部署。这是也称为构建验证测试 ( BVT )。
- 负载测试:负载测试用于确定系统的整体有效性。在负载测试期间,我们模拟集成系统上的预计负载。
- 压力测试:在压力测试中,我们将系统推到预期的容量或负载之外。这有助于我们识别系统中的瓶颈并识别故障点。性能测试是用于压力和负载测试的总称。
- 安全测试:安全测试是进行的,以确保应用的完美执行。在安全测试中,我们重点评估安全方面的各种元素,例如完整性、保密性和真实性等。
- 可访问性测试:可访问性测试旨在确定不同能力的个人是否能够使用某个应用。
既然我们已经看到了不同类型的测试,在接下来的部分中,我们将详细介绍单元测试、功能测试和负载测试,因为它们对于确保应用的稳定性至关重要。
注意
要进一步了解安全性,请尝试使用静态代码分析工具 : 进行安全性测试。更多关于无障碍的信息可以在这里找到:无障碍测试:https://accessibilityinsights.io/。
性能测试、可访问性测试和安全性测试是我们用来评估应用的非功能方面的测试,如性能、可用性、可靠性、安全性和可访问性。
现在让我们看看如何为我们的电子商务应用执行单元测试。
理解单元测试
单元测试是一种测试应用最小隔离单元的方法。这是软件开发中的一个重要步骤,有助于尽早隔离问题。
单元测试对我们构建的软件质量有直接影响。总是建议一写任何方法就把单元测试写成。如果我们遵循测试驱动开发 ( TDD )的方法论,我们首先编写测试用例,然后继续实现功能。
在下一节中,我们将学习如何创建单元测试并从 Visual Studio 中运行它们。
Visual Studio 中的单元测试
我们选择使用 Visual Studio,因为它有强大的工具来创建和管理测试用例。
使用 Visual Studio,我们可以创建、调试和运行单元测试用例。我们还可以检查执行的测试的代码覆盖率。此外,它还有一个实时单元测试功能,在我们修改代码时运行单元测试用例,并实时显示结果。
我们将在后续章节中探讨所有这些特性。
创建和运行单元测试
让我们继续并创建一个单元测试项目来在Packt.ECommerce.Order
项目上执行单元测试。
执行以下步骤来创建单元测试用例:
-
Add a new project of the
MSTest
Test Project (.NET Core) type to the solution under theTests
folder and name the projectPackt.ECommerce.Order.UnitTest
:图 15.1–带有的 Visual Studio MSTest 测试项目.NET Core
-
Once the project is added, the Solution structure will look like the following screenshot:
图 15.2–测试项目创建后的解决方案结构
向新创建的测试项目添加
Packt.ECommerce.Order
的项目引用。 -
给测试项目增加一个新类,并命名为
OrdersControllerTest
。我们将在这个类中添加所有与OrderController
相关的测试用例。 -
For the
Test
framework to detect the test class, the class should be attributed withTestClass
, as follows:[TestClass] public class OrdersControllerTest { }
现在我们添加一个简单的测试来测试
OrderController
控制器的构造器。我们将要执行的测试是断言OrderController
控制器的成功创建。现在让我们添加测试,如下面的代码所示:[TestMethod] public async Task OrderController_Constructor() { OrdersController testObject = new OrdersController(null); Assert.IsNotNull(testObject); }
OrderController_Constructor
测试方法归于TestMethod
;这是测试框架发现测试方法所必需的。这里我们通过检查创建的对象的空条件来断言。 -
Visual Studio 提供测试资源管理器来管理和运行测试。我们去测试 | 测试浏览器打开,如图图 15.3 。
-
构建解决方案,查看测试资源管理器中的测试。
-
In Test Explorer, we can see all the tests that were present in the solution. We can see the
OrderController_Constructor
test we created here:图 15.3–Visual Studio 测试资源管理器窗口
-
Next, run the test by right-clicking on the test case and selecting Run from the context menu:
图 15.4–测试资源管理器窗口中的测试运行上下文菜单
-
Once the test is executed, we can see the test result in the right pane. From the result, we can see that the test executed and runs successfully, as follows:
图 15.5–测试资源管理器的测试结果
我们在 Visual Studio 中创建和执行了一个简单的测试。在下一节中,我们将学习如何模拟OrdersController
的依赖关系来验证功能。
嘲笑对 Moq 的依赖
通常,被测试的方法调用其他外部方法或服务,我们称之为依赖。为了确保被测方法的功能,我们通过为依赖关系创建模拟对象来隔离依赖关系的行为。
在应用中,类可能依赖于其他类;例如,我们的OrdersController
依赖于OrderService
。在测试OrdersController
的时候,我们应该隔离OrderService
的行为。
为了理解嘲讽,让我们为OrdersController
的GetOrdersAsync
动作方法创建单元测试。
让我们来看看我们正在为其编写单元测试用例的GetOrderById
方法:
//This is the GetOrderById action method in OrdersController.cs
[HttpGet]
[Route(“{id}”)]
public async Task<IActionResult> GetOrderById(string id)
{
var order = await this.orderService.GetOrderByIdAsync(id).ConfigureAwait(false);
if (order != null)
{
return this.Ok(order);
}
else
{
return this.NotFound();
}
}
在这种方法中,调用orderService
的GetOrderByIdAsync
,以便根据传入的id
实例获取订单。控制器动作将返回从OrderService
取回的订单id
;否则,返回NotFound
动作。
正如我们所看到的,代码流有两条路径:一条路径用于订单存在时,另一条路径用于订单不存在时。通过单元测试,我们应该能够涵盖这两条路径。所以,现在出现的问题是,我们如何模拟这两种情况?
我们在这里想要的是嘲笑OrderService
的回应。要嘲笑OrderService
的反应,我们可以利用 Moq 库。
为了利用 Moq,我们需要在Packt.ECommerce.Order.UnitTest
测试项目中添加对Moq
包的 NuGet 引用。
让我们将中的测试方法添加到OrdersControllerTest
类中,如下代码所示,测试OrdersController
的GetOrderById
,以验证OrderService
返回订单对象的情况:
[TestMethod]
public async Task When_GetOrdersAsync_with_ExistingOrder_receive_OkObjectResult()
{
var stub = new Mock<IOrderService>();
stub.Setup(x => x.GetOrderByIdAsync(It.IsAny<string>())).Returns(Task.FromResult(new OrderDetailsViewModel { Id = “1” }));
OrdersController testObject = new OrdersController(stub.Object);
var order = await testObject.GetOrderById(“1”).ConfigureAwait(false);
Assert.IsInstanceOfType(order, typeof(OkObjectResult));
}
从代码中,我们可以观察到以下内容:
- 由于
IOrderService
是通过控制器注入注入到OrderController
的,所以我们可以向OrderController
注入一个经过模拟的OrderService
,这将有助于我们通过改变模拟对象行为来测试OrderController
的所有代码路径。 - 我们利用
Mock
类为IOrderService
创建一个存根(也称为模拟)并覆盖GetOrderByIdAsync
行为,如前面的代码所示。 - 我们为
IOrderService
界面创建Mock
对象的实例,并通过调用Mock
对象上的Setup
方法来设置GetOrderByIdAync
的行为。 GetOrderByIdAsync
方法被模拟为,对于它接收到的任何参数值,mock
对象将返回OrderDetailsViewModel
的对象,其中Id
为1
。- 由于我们通过构造函数注入将被模拟的对象注入到
OrderService
中,所以每当IOrderService
中有对任何方法的调用时,该调用都会转到IOrderService
的被模拟的实现中。 - 最后,通过验证从
OrderController
返回到OkObjectResult
的结果类型来断言测试结果。
现在,让我们添加一个测试用例来验证行为,如果订单不存在,我们将收到NotFound
结果,如以下代码所示:
[TestMethod]
public async Task When_GetOrdersAsync_with_No_ExistingOrder_receive_NotFoundResult()
{
var stub = new Mock<IOrderService>();
stub.Setup(x => x.GetOrderByIdAsync(It.IsAny<string>())).Returns(Task.FromResult<OrderDetailsViewModel>(null));
OrdersController testObject = new OrdersController(stub.Object);
var order = await testObject.GetOrderById(“1”).ConfigureAwait(false);
Assert.IsInstanceOfType(order, typeof(NotFoundResult));
}
在这个测试案例中,我们通过从OrderService
存根返回一个null
值来模拟订单不存在的行为。这将使OrdersController
的GetOrderById
动作方法返回NotFoundResult
,这在测试用例中得到了验证。
注意
OrderService
级依赖于IHttpClientFactory
、IOptions
、Mapper
和DistributedCacheService
。所以,为了增加一个单元测试,我们应该嘲笑他们所有人。更多细节可以看看之前代码的OrderServiceTest
测试类中的When_GetOrderByIdAsync_with_ExistingOrder_receive_Order
测试方法。
在本节中,我们已经看到了如何利用MSTest
框架来创建单元测试。有许多其他的测试框架可以用来创建单元测试.NET Core。这里值得一提的两个这样的框架是 xUnit 和 nUnit。虽然 xUnit 和 nUnit 在执行测试的方式上有一些不同,但这两个框架都很出色,并且提供了模仿和并行执行等功能。
在单元测试中,我们的目标是通过模仿依赖类的行为来测试特定的类。如果我们测试这些类和其他相关类,我们称之为集成测试。我们在不同的层次上编写集成测试:在特定模块或组件的层次上,在微服务层次上,或者在整个应用层次上。
既然我们已经将单元测试用例添加到了我们的电子商务解决方案中,在下一节中,我们将检查这些测试的代码覆盖率。
代码覆盖率
代码覆盖率是描述我们的测试用例覆盖了多少代码的度量。Visual Studio 提供了一个工具来查找单元测试的代码覆盖率。我们可以对所有测试运行测试 | 分析代码覆盖率,如下所示:
图 15.6–文本资源管理器中的分析代码覆盖率上下文选项
这也可以从测试浏览器中的上下文菜单中完成。
这将运行所有测试用例,并识别任何未测试的代码块。我们可以在下面的代码覆盖结果窗口中看到代码覆盖结果:
图 15.7–Visual Studio 代码覆盖窗口
代码覆盖结果将显示覆盖块的百分比和未覆盖块的百分比。因为我们覆盖了GetOrderByIdAsync
的所有块,所以该方法的代码覆盖是 100% 。GetOrdersAsync
的覆盖率是 0.00% ,因为我们没有任何测试用例来测试它。代码覆盖率很好地表明了我们的单元测试有多有效。
注意
MSTest
提供了一个名为假货的模拟框架,可以用来创建模拟,但是这样做的限制是我们将无法获得代码覆盖率.NET Core。微软承诺在未来的版本中增加这项功能。
建议为解决方案中的所有类创建单元测试用例。通过添加单元测试来验证所有的类和功能,单元测试用例将覆盖更高比例的代码。有了更高的代码覆盖率,我们将能够在开发的早期捕捉更多的错误,同时对解决方案进行更改。在我们提交变更之前,我们应该确保所有的测试用例都通过。在下一章 第 16 章在 Azure 中部署应用,我们将学习如何将运行测试用例与 Azure DevOps 管道集成。
到目前为止,我们已经通过模仿依赖关系和编写单元测试用例来测试单个模块或类。在集成和部署整个解决方案后测试功能也很重要。在下一节,我们将学习如何为我们的电子商务应用执行功能测试。
小费
Visual Studio 的代码度量和代码分析工具对于确保我们编写的代码的可维护性和可读性非常有用。你可以在这里找到代码度量的细节:https://docs . Microsoft . com/en-us/visualstudio/code-quality/code-metrics-values?view=vs-2019 。
代码分析,请到这里:https://docs . Microsoft . com/en-us/dotnet/基本面/代码-分析/概述。
理解功能测试
在功能测试中,我们根据功能需求验证我们构建的应用。功能测试是通过提供一些输入并断言应用的响应或输出来执行的。在执行功能测试时,我们将应用视为一个整体;我们不验证单个内部组件。
功能测试可以分为三个任务:识别要测试的系统的功能,确定具有预期输出的输入,然后执行这些测试来评估系统是否根据预期做出响应。功能测试的执行可以通过在应用上执行测试步骤来手动完成,或者我们可以使用工具来自动化它们。自动化功能测试可以大大缩短应用的上市时间。
在下一节中,我们将学习自动化功能测试用例。
自动化功能测试用例
手动执行功能测试用例在应用测试中仍然是相关的。然而,考虑到较短的部署周期和客户对新特性的快速期望,手动测试在早期识别错误方面可能非常耗时和低效。使用自动化,我们可以获得新的效率,加速测试过程,并提高软件质量。有多种工具和框架可用于自动化功能测试用例。
在本节中,我们将学习最流行的自动化框架 Selenium。让我们开始吧:
-
首先,让我们创建一个
MSTest
项目并命名为Packt.ECommerce.FunctionalTest
。 -
在这个项目中,添加
Selenium.WebDriver
、Selenium.WebDriver.ChromeDriver
和WebDriverManager
NuGet 包。我们运行硒测试需要这些包。 -
让我们从一个简单的测试开始,验证我们的电子商务应用的标题。为此,创建一个
HomePageTest
测试类和一个When_Application_Launched_Title_Should_be_ECommerce_Packt
测试方法,就像我们在理解单元测试部分所做的那样,如下面的代码所示:[TestClass] public class HomePageTest { [TestMethod] public void When_Application_Launched_Title_Should_be_ECommerce_Packt() { } }
-
To execute our functional tests, we should launch a browser and use that browser to navigate to the e-commerce application. The
MSTest
framework provides a special function to perform the initialization and cleanup operations required for our tests. We will be creating a Chrome web driver to perform a functional test.让我们继续添加初始化和清理方法,如下代码所示:
[TestClass] public class HomePageTest { ChromeDriver _webDriver = null; [TestInitialize] public void InitializeWebDriver() { var d = new DriverManager(); d.SetUpDriver(new ChromeConfig()); _webDriver = new ChromeDriver(); } [TestMethod] public void When_Application_Launched_Title_Should_be_ECommerce_Packt() { } [TestCleanup] public void WebDriverCleanup() { _webDriver.Quit(); } }
在前面的代码中,
InitializeDriver
方法被赋予了TestInitialize
来通知框架这是测试初始化方法。在测试初始化中,我们正在创建ChromeDriver
并初始化类变量。测试用例完成后,我们应该关闭浏览器实例;我们在WebDriverCleanup
方法中通过调用Quit
方法来做到这一点。要通知测试框架它是清理方法,应该归结为TestCleanup
。 -
Now let’s go and add the test case to navigate to the e-commerce application and validate the title as shown in the following code:
[TestMethod] public void When_Application_Launched_Title_Should_be_ECommerce_Packt() { _webDriver.Navigate().GoToUrl(“https://localhost:44365/”); Assert.AreEqual(“Ecommerce Packt”, _webDriver.Title); }
在我们的 Chrome 网络驱动上调用
GoToUrl
导航到电子商务应用。导航后,我们可以通过声明 web 驱动程序的Title
属性来验证页面的标题。 -
通过右键单击
When_Application_Launched_Title_Should_be_ECommerce_Pact
测试用例并选择运行,从测试资源管理器运行测试用例。这将打开 Chrome 浏览器并导航到指定的电子商务网址,然后它将断言页面的标题。测试用例执行后,浏览器将关闭。我们在测试浏览器中看到结果,如下图截图所示:
图 15.8–测试项目创建后的解决方案结构
现在我们将扩展功能测试来验证搜索功能。为了测试这个功能,我们应该在搜索框中输入文本,然后点击搜索按钮。然后,检查结果,查看返回的测试结果是否仅属于搜索到的产品。
让我们通过添加When_Searched_For_Item
测试方法来自动化测试用例,如下面的代码所示:
[TestMethod]
public void When_Searched_For_Item()
{
_webDriver.Navigate().GoToUrl(“https://localhost:44365/”);
var searchTextBox = _webDriver.FindElement(By.Name(“SearchString”));
searchTextBox.SendKeys(“Orange Shirt”);
_webDriver.FindElement(By.Name(“searchButton”)).Click();
var items = _webDriver.FindElements(By.ClassName(“product-description”));
var invaidProductCout = items.Where(e => e.Text != “Orange Shirt”).Count();
Assert.AreEqual(0, invaidProductCout);
}
在这个测试案例中,导航到主页后,在搜索字符串字段中输入搜索文本,然后点击搜索按钮。通过验证搜索结果来确认是否有任何产品没有作为search
字符串返回。
硒使编写功能测试变得非常容易。我们应该尝试自动化所有的功能测试用例,比如用户管理、向购物车添加产品以及下订单。随着所有功能测试用例的自动化,我们将能够更好地测试和验证新版本的功能,并保持我们应用的质量。还有其他可用的功能测试工具,如 QTP 和 Visual Studio 编码用户界面测试。
我们已经看到了功能测试,它验证了应用的功能。同样重要的是评估应用的响应能力,看它如何响应特定的负载。在下一节中,我们将学习如何在电子商务应用上执行性能测试。我们可以利用自动化的功能测试用例来执行 BVT 或回归测试。
注意
参考文档了解更多关于硒检测的信息:https://www.selenium.dev/documentation/en/。
了解负载测试
用户期望应用对他们的行为做出快速响应。任何反应迟缓都会导致用户沮丧,最终,我们会失去他们。即使一个应用在正常负载下工作正常,我们也应该知道当需求突然达到峰值时,我们的应用是如何工作的,并为此做好准备。
负载测试的主要目标不是发现错误,而是消除应用的性能瓶颈。进行负载测试是为了向涉众提供关于他们应用的速度、可伸缩性和稳定性的信息。在下一节中,我们将学习如何使用 JMeter 执行负载测试。
用 JMeter 进行负载测试
JMeter 是由 Apache 软件基金会构建的开源测试工具。它是执行负载测试最流行的工具之一。JMeter 可以通过创建 web 服务器的虚拟并发用户来模拟应用的繁重负载。
可以从这里下载配置 JMeter:https://jmeter.apache.org/download_jmeter.cgi。
让我们继续为我们的电子商务应用创建一个 JMeter 负载测试。
为了学习如何使用 JMeter 进行负载测试,我们将创建一个包含两个主页和产品搜索页面的测试。尝试以下步骤来创建负载测试:
-
Launch Apache JMeter from the download location. We will see the window as follows:
图 15.9–Apache JMeter
-
通过在左窗格中右键单击测试计划并选择添加 | 螺纹(用户) | 螺纹组来添加螺纹组。线程组定义了将对我们的应用执行测试用例的用户池。使用它,我们可以配置模拟的用户数量、启动所有用户的时间以及执行测试的次数。
-
Let’s name the thread group
Load and Query Products
and set the number of users to30
. Set Ramp-up period to5
seconds as shown in the following screenshot:图 15.10–在 Apache JMeter 中添加线程组
这将在
5
秒内模拟30
的用户负载。使用线程组,我们还可以控制测试应该运行的次数。 -
To add the test request, right-click on Thread Group and select Add | Sampler | HTTP Request.
让我们将协议设置为
https
、服务器名称或、将 IP 设置为localhost
、端口号设置为44365
(本地运行的电商门户的端口号)。命名本次测试Home Page
,如下图截图所示:图 15.11–在 JMeter 中添加主页 HTTP 请求
让我们再添加一个【HTTP 请求采样器来获取特定产品的细节。对于该请求,将
productId
查询参数设置为Cloth.3
,将productName
设置为Orange%20Shirt
,如下图所示:图 15.12–在 JMeter 中添加产品详细信息页面 HTTP 请求
-
点击保存按钮并将其命名为
ECommerce
,保存本次测试计划。 -
为了查看结果,我们应该在这个测试中添加一个监听器。右键单击测试组,选择添加 | 收听者 | 查看表格中的结果。
-
添加监听器后,选择运行 | 启动继续运行测试。
-
测试运行完成后,您将看到如下截图所示的结果。这将为我们提供每个请求的响应时间:
图 15.13–JMeter 中的测试结果表
JMeter 中有多个监听器可以查看结果,例如摘要报告和图形结果,这将给出测试结果的另一种表示。我们可以用 JMeter 轻松配置不同种类的采样器,也可以用不同的 HTTP 方法和动态测试配置请求,其中请求依赖于另一个应用编程接口的响应。一旦在 JMeter 中有了测试计划,我们就可以利用 JMeter 命令行实用程序从多个数据中心运行它来模拟跨地域的负载并整理结果。
JMeter 提供的灵活性,以及其丰富的文档,使其成为最常用的性能测试工具。JMeter 还可以用来执行功能测试。
建议以预期负载的 1.5 到 2 倍运行负载测试。运行性能测试后,建议使用 Application Insights 来分析请求的服务器响应时间、负载情况下相关 API 的响应情况,更重要的是,分析测试过程中出现的任何故障。
小费
建议使用 Azure DevOps 管道运行自动化测试。使用文档查看如何将测试与 Azure DevOps 管道集成。
JMeter 测试:https://github.com/Azure-Samples/jmeter-aci-terraform
总结
在这一章中,我们探讨了软件开发的一个非常重要的方面:测试。我们已经了解了不同类型的测试以及在 SDLC 中使用它们的阶段。
我们学习了单元测试的概念,以及如何通过使用 Moq 框架模仿依赖关系来将测试集中在特定的调用上。我们还被介绍使用 Selenium 创建自动化功能测试,以在将我们的电子商务应用发布到生产之前测试其功能。
最后,我们了解了 JMeter,它是执行负载测试最常用的工具。下一章将重点讨论在 Azure 中部署应用。
问题
-
True or False: We should only start to think about testing an application after the completion of its development?
a.真实的
b.错误的
-
Which of the following is a kind of software testing?
a.安全测试
b.功能测试
c.无障碍测试
d.上述全部
-
True or False: A higher code coverage percentage for unit tests is desirable to achieve a shorter time to market?
a.真实的
b.错误的
十六、在 Azure 中部署应用
部署是我们为使软件应用可供使用而执行的一组活动。一般的方法是获取代码,然后构建、测试并将其部署到目标系统。根据应用的类型和业务需求,部署代码的方法可能会有所不同。这可能很简单,只需关闭目标系统,用新版本替换现有代码,然后重新启动系统,也可能涉及其他复杂的方法,如蓝绿色部署,将代码部署到与生产环境相同的临时环境中,运行测试,然后将流量重定向到临时环境,使其成为生产环境。
现代软件开发采用敏捷和 DevOps 来缩短开发周期,频繁可靠地交付新功能、更新和 bug,为客户提供更多价值。要实现这一点,您需要一套工具来规划、协作、开发、测试、部署和监控。
在本章中,我们将了解什么是 Azure DevOps,以及它为快速可靠的交付提供的工具。
本章涵盖以下主题:
- 介绍 Azure DevOps
- 了解配置项管道
- 了解光盘管道
- 部署 ASP.NET 5 应用
技术要求
对于本章,您需要 Azure、Visual Studio 2019 和 Git 的基本知识,以及一个具有贡献者角色的活动 Azure 订阅。如果你没有,你可以在https://azure.microsoft.com/en-in/free注册一个免费账户。
章节的代码可以在这里找到:https://github . com/PacktPublishing/Enterprise-Application-Development-with-C-Sharp-9-and-.NET-5/树/主/第 16 章
介绍天青 DevOps
为了将一个产品的想法变为现实,不管你的团队规模有多大,你都需要一种有效的方法来规划你的工作,在你的团队中进行协作,以及构建、测试和部署。Azure DevOps 帮助您应对这些挑战,并为您的成功提供各种服务和工具。Azure DevOps 服务可以通过网络访问,也可以从流行的开发 ide 中访问,如 Visual Studio、Visual Studio Code、Eclipse 等。使用 Azure DevOps 服务器,Azure DevOps 服务既可以在云中使用,也可以在内部使用。
Azure DevOps 提供以下服务:
- Boards :提供了一套工具来使用 Scrum 和看板方法来计划和跟踪你的工作、缺陷和问题
- 回购:提供源代码控制,使用 Git 或 Team Foundation 版本控制 ( TFVC )管理您的代码
- 管道:提供一套服务支持持续集成 ( CI )和持续交付 ( CD )
- 测试计划:提供一套测试管理工具,通过端到端的可追溯性来提高应用的质量
- 工件:允许您共享来自公共和私有来源的包,以及与 CI 和 CD 管道集成
除了这些服务,Azure DevOps 还可以帮助您为团队管理维基、管理仪表板、使用小部件共享进度和趋势以及配置通知。它还允许您添加或开发自定义扩展,并与流行的第三方服务集成,如营火、Slack、Trello 等。
Azure DevOps 服务提供免费和付费订阅。要注册免费账户,请按照https://docs . Microsoft . com/en-us/azure/devo PS/用户指南/注册-邀请-队友?view=azure-devops 。
以下是一个示例项目的主屏幕截图:
图 16.1–Azure DevOps 主页
让我们详细了解一下 Azure DevOps 和这些服务。
板子
板帮助您定义项目的流程,并跟踪您的工作。当您在 Azure DevOps 中创建新项目时,您可以选择一个流程模板,如敏捷、基本、Scrum 或 CMMI 流程。流程模板决定了您可以在项目中使用的工作项类型和工作流。工作项帮助您跟踪工作,工作流帮助您跟踪工作项的进度。下图显示了工作项的层次结构和 Scrum 流程模板的工作流:
图 16.2–Scrum 流程中工作项和工作流的层次结构
要进一步定制或定义您的工作流和工作项类型,您可以选择基于前面提到的流程模板创建您自己的流程模板。
让我们更多地了解工作项和工作流。
工作项目
工作项帮助您跟踪项目中的特性、需求和缺陷。您可以在层次结构中对需求进行分组。通常,我们从一个被称为史诗的高级需求开始,它可以进一步细分为功能和产品积压项目。产品积压项目是划分优先级、分配给团队成员并在冲刺阶段交付的交付件。任务是为积压项目和缺陷创建的,用于根据产品积压项目跟踪缺陷。
协作功能通过对工作项目的讨论或提问,实现团队内部的沟通。您可以提及某个团队成员或链接另一个工作项,并随时查看所有操作或讨论的历史记录。您也可以选择跟踪工作项,以便在更新时获得通知。
工作流程
工作流帮助您项目的进度和健康状况。例如,使用新的状态创建产品积压项目。产品负责人审核通过后,转到批准的,然后在冲刺中优先分配给团队成员,转到承诺的,完成后转到完成的。工作流帮助您跟踪项目的运行状况。
您可以使用看板来查看所有工作项的状态,并使用拖放功能轻松地将工作项移动到不同的状态。下面的截图展示了由不同状态的工作项目组成的看板板:
图 16.3–看板仪表板
注意
如果您创建自己的流程模板,则可以自定义工作项或创建新的工作项,并自定义或定义工作流以满足您的业务需求。
要了解更多关于流程模板及其不同之处,您可以参考https://docs . Microsoft . com/en-us/azure/devo PS/boards/入门/什么是 azure-boards?view=azure-devops &选项卡= scrum-process # work-item-type。
接下来,让我们了解更多关于回购。
休息
回购提供版本控制工具,你可以用它来管理你的代码。版本控制系统允许您跟踪团队对代码所做的更改。它为每个更改创建一个快照,您可以随时查看该快照,并在需要时恢复到该快照。Azure DevOps 提供 Git 和 TFVC 作为你的版本控制系统。
Git 是目前最广泛使用的版本控制系统,并日益成为版本控制系统的标准。Git 是一个分布式版本控制系统,有一个版本控制系统的本地副本,使用它你可以在本地查看历史或提交更改即使你离线了,一旦连接到网络,它就会同步到服务器。然而,TFVC 是一个集中的版本控制系统,开发机器上每个文件只有一个版本,历史记录保存在服务器上。关于 Git 的更多信息,可以参考https://docs.microsoft.com/en-in/azure/devops/repos/git/?对于 TFVC,你可以参考 https://docs.microsoft.com/en-in/azure/devops/repos/tfvc/?view=azure-devops 。
以下是回购的关键服务:
-
分支是代码与您的承诺历史的引用。一个版本控制系统至少有一个分支,通常命名为
main
或master
,你可以从中创建另一个分支。通过这种方式,您可以隔离您的更改,以便进行功能开发或错误修复。您可以创建任意数量的分支,在团队成员之间共享它们,提交您的更改,并安全地合并回master
。 -
Branch policies help you to protect your branches during development. When you enable a branch policy on a branch, any change must be made via pull requests only, so that you can review, give feedback, and approve changes. As a branch policy, you can configure a minimum number of required approvers, check for linked work items and comment resolution, and enforce the build to be successful to complete pull requests.
以下屏幕截图说明了在分支上定义的策略:
图 16.4–分支机构策略
这里,创建了一个策略来在代码合并到分支之前验证构建。
- 拉取请求允许您查看代码、添加注释,并确保在代码合并到您的分支之前解决这些请求。根据配置的分支策略,您可以添加强制审阅者来审阅和批准更改。您可以将工作项与拉取请求相关联,以实现变更的可追溯性。下面的截图说明了一个示例拉取请求:
图 16.5–拉取请求
拉取请求有标题和描述,用户可以查看文件并将其与以前的版本进行比较,检查构建和链接工作项的状态,并进行批准。
接下来,让我们了解管道。
管道
管道允许你配置、构建、测试和部署你的代码到任何目标系统。使用管道,您可以启用 CI 和 CD 来实现代码的一致和高质量交付。您可以使用针对使用流行语言构建的许多应用类型的管道,例如.NET、Java、JavaScript、Node.js、PHP、C++等等,并把它们部署到云或内部服务器上。您可以使用 YAML 文件或基于用户界面的经典编辑器来定义管道。
CI 为您的项目自动化构建和测试,以确保质量和一致性。配置项可以配置为在新代码合并到分支时或两者都合并时按计划运行。配置项生成由光盘管道用来部署到目标系统的工件。
光盘使您能够自动将代码部署到目标系统并运行测试。光盘可以配置为按计划运行。
接下来,让我们更多地了解测试计划。
测试计划
Azure DevOps 提供了一套工具来提高项目的质量。它为基于浏览器的测试管理解决方案提供了手动和探索性测试所需的所有功能。它提供了在测试套件或测试计划下组织测试用例的能力,通过这些测试用例,您可以跟踪特性或版本的质量。这些解释如下:
- 测试用例用于验证应用的各个部分。它们包含测试步骤,您可以使用这些步骤来断言需求。您可以通过将测试用例导入测试套件或测试计划来重用它。
- 测试套件是一组测试用例,被执行来验证一个特性或者一个组件。您可以创建静态测试套件、基于需求的套件和基于查询的套件。
- 测试计划是一组测试套件或测试用例,用于跟踪发布的每次迭代的质量。
接下来,让我们了解更多关于工件的知识。
器物
工件使得团队之间共享代码变得容易。您可以轻松创建和共享来自公共和私有来源的 Maven、npm 或 NuGet 包提要,它们在 CI 和 CD 管道中很容易使用。工件基于标准的打包格式,可以很容易地与开发 ide(如 Visual Studio)集成为一个包源。
Azure DevOps 支持团队内部的协调和协作,并帮助您以高质量一致地交付项目。使用配置项和光盘,您可以自动构建和部署代码。
在下一节中,让我们了解 CI 管道。
了解 CI 管道
配置项是一种实践,在这种实践中,您可以自动构建和测试代码。在 Azure DevOps 中,您可以创建管道,并将其配置为当代码合并到您的目标(主/主)分支时自动触发,或者按计划运行,或者两者都有。您可以选择使用 YAML 文件或基于用户界面的经典编辑器创建管道。
下图说明了从开发人员的机器到云的典型代码流:
图 16.6–典型的代码流程
从前面的截图中,我们看到了以下内容:
-
开发人员使用诸如 Visual Studio、Visual Studio Code 或 Visual Studio for Mac 等开发工具来开发代码。
-
代码更改被移动到存储库中。
-
触发配置项管道,验证构建,运行测试,并发布工件。光盘管道被触发,它将代码部署到目标系统。
-
The developer uses Application Insights to continuously monitor and improve the application.
注意
YAML (简称 YAML 不是标记语言)是定义管道的首选方式。它提供了与经典编辑器相同的功能。您可以将这些文件签入存储库,并像管理任何其他源文件一样管理它们。更多详情可以参考https://docs . Microsoft . com/en-us/azure/devo PS/pipelines/YAML-schema?view=azure-devops &选项卡= schema % 2 parameter-schema。
让我们了解一个管道的核心组件和流程。
了解管道的流量和部件
一个管道是一组动作的定义,这些动作将被执行来构建和测试你的代码。管道定义包含一个触发器、变量、阶段、作业、步骤和任务。当我们运行一个管道时,它执行管道定义中定义的任务。让我们在接下来的章节中了解这些组件。
引发
一个触发器是一个配置,它定义了管道应该何时运行。您可以将管道配置为在新代码合并到您的 repo 时、以预定的时间间隔或在另一个构建完成后自动运行。所有这些配置都是在管道的触发器部分定义的。
在下面的代码片段中,管道被配置为当代码被推送到master
分支或releases
文件夹下的任何分支时触发。或者,我们也可以在管道中指定路径过滤器,以便仅当满足路径条件的代码发生更改时才触发:
trigger:
branches:
include:
- master
- releases/*
paths:
include:
- web
exclude:
- docs/README.md
您还可以将管道配置为根据计划自动运行。在下面的代码片段中,管道被配置为每天上午 9:30 运行。计划是使用cron
表达式指定的,您可以指定多个计划。如果将always
设置为true
,即使代码没有变化,也会触发构建:
schedules:
- cron: "30 9 * * *"
displayName: Daily build
branches:
include:
- master
always: false
变量
变量可以用一个值定义,并在管道的多个地方重用。您可以在根、阶段或工作中定义变量。管道中可以使用三种不同类型的变量——用户定义变量、系统变量和环境变量:
variables:
buildConfiguration: 'Release'
. . . .
. . . .
- task: DotNetCoreCLI@2
displayName: Publish
inputs:
command: 'publish'
publishWebProjects: false
projects: '**/*HelloWorld.csproj'
arguments: '--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)/web'
在前面的代码片段中,buildConfiguration
变量是用Release
值定义的,并在任务的arguments
部分使用。build.artifactstagingdirectory
是一个包含工件目录位置的系统变量。
阶段
阶段是默认情况下顺序运行的作业的集合。您还可以指定前一阶段执行状态的条件,或者添加批准检查来控制阶段应该何时运行。
以下是具有多个阶段的管道定义示例:
stages:
- stage: Build
jobs:
- job: build
steps:
- script: echo building code
- stage: Test
jobs:
- job: windows
steps:
- script: echo running tests on windows
- job: linux
steps:
- script: echo running tests on Linux
- stage: Deploy
dependsOn: Test
jobs:
- job: deploy
steps:
- script: echo deploying code
在前面的示例中,配置了三个阶段,每个阶段按顺序运行。Test
阶段包含两个可以并行运行的作业,Deploy
阶段依赖于Test
阶段。
下面是前面示例的执行摘要的屏幕截图,您可以单击每个阶段来查看日志:
图 16.7–管道运行总结
作业
作业是在代理池上运行的步骤的集合。此外,您可以配置为有条件地运行作业或添加对先前作业的依赖。在下面的代码片段中,作业是用testNull
变量上的一个步骤和一个条件定义的:
variables:
- name: testNull
value: ''
jobs:
- job: BuildJob
steps:
- script: echo Building!
condition: eq('${{ variables.testNull }}', '')
在前面的代码中,作业被配置为只有当testNull
为空时才运行。
步骤和任务
步骤是你的管道的一组任务。这些可以是构建你的代码,运行测试,或者发布工件。每个步骤都在代理上执行,并且可以访问管道工作区。
任务是管道自动化的构建模块。您可以使用许多内置任务,也可以创建自己的自定义任务并在管道中使用。例如,下面的代码片段使用DotNetCoreCLI@2
任务构建csproj
:
- task: DotNetCoreCLI@2
displayName: build
inputs:
command: 'build'
projects: '**/*.csproj'
arguments: '--configuration $(BuildConfiguration)'
想了解更多关于管道的信息,可以参考https://docs . Microsoft . com/en-in/azure/devo PS/pipelines/create-first-pipeline?view=azure-devops &选项卡= Java % 2 ctfs-2018-2% 2 browser。
在下一节中,让我们了解更多关于光盘管道的信息。
了解光盘管道
CD 是一个将代码自动部署到目标环境的过程。CD 管道使用 CI 管道产生的工件,并部署到一个或多个环境中。像配置项管道一样,我们可以使用 YAML 文件或经典编辑器来定义光盘管道。您可以指定前一阶段执行状态的条件,或者添加要部署的批准检查,这是生产部署中非常常见的场景。
您还可以配置为运行自动化用户界面测试,以便在部署后执行健全性检查。根据健全性检查结果,您可以配置为自动将代码提升到更高的环境。
在任何时候,如果阶段部署失败,我们可以重新部署以前版本的代码。根据项目设置下配置的保留策略,Azure DevOps 会保留构建工件,以便随时轻松部署任何版本的代码。如果您发现应用部署后有任何问题,您可以很容易地找到最后一个已知的好版本,并部署代码以最大限度地减少业务影响。
让我们在下一节中了解更多这方面的内容。
持续部署与持续交付
连续部署是自动化的部署到目标系统,每当新代码合并到您的 repo,而连续交付使应用可以随时部署到目标系统。Azure DevOps 提供多级管道;您可以配置带有阶段的管道来实现这一点。
连续部署通常配置在较低的环境中,例如开发或测试,而对于较高的环境,例如试运行或生产,您应该考虑连续交付,以便您可以在较低的环境中验证更改,并批准将代码部署到较高的环境中。
下面的截图展示了一个多阶段管道,自动构建并发布到 dev,并在测试阶段等待批准。在这种情况下,要发布代码进行测试,需要批准:
图 16.8–等待批准的多级管道
要了解更多关于如何在 Azure 管道上配置审批和检查的信息,可以参考https://docs . Microsoft . com/en-in/Azure/devo PS/pipelines/process/approvals?视图=天蓝色-devops &选项卡=检查-通过。
要查看管道运行的详细信息,您可以单击任何阶段来查看该运行的日志。日志帮助我们排除部署故障。以下屏幕截图说明了管道运行的日志:
图 16.9–管道执行细节
在前面的截图中,您会注意到您可以查看管道中配置的阶段、作业和任务,并且您可以单击任务查看日志。
在下一节中,我们将学习如何创建管道来构建和部署应用。
部署 ASP.NET 5 应用
到目前为止,在本章中,我们已经探索了 Azure DevOps,了解了它提供的工具和服务,然后了解了 CI 和 CD 管道。在本节中,我们将学习如何创建 Azure DevOps 项目、克隆存储库、将代码推送到存储库,以及创建 CI 和 CD 管道来将代码部署到 Azure App Service。
注意
请务必查看技术要求部分,以确保在部署示例应用之前已经设置好了一切。
您可以按照以下步骤将 ASP.NET 5 应用部署到 Azure:
-
登录您的 Azure DevOps 帐户。如果您没有 Azure DevOps 帐户,请创建一个;可以按照https://docs . Microsoft . com/en-us/azure/devo PS/用户指南/注册-邀请-队友给出的步骤操作?view=azure-devops 。
-
On the home page of Azure DevOps, provide a name for your project, say,
HelloWorld
, for Version control, choose Git, and for Work item process, you can choose Agile. This is shown in the following screenshot:图 16.10–新的 Azure DevOps 项目
-
Now, let's create a service connection, which we will use in the pipeline to connect and deploy code to Azure App Service.
从左侧菜单,导航至项目设置 | 服务连接 | 创建服务连接 | Azure 资源管理器 | 服务主体(自动):
图 16.11–新服务主体
服务主体允许管道连接到您的 Azure 订阅,以管理资源或将您的代码部署到 Azure 服务。
-
Select a subscription and provide a name for the connection to create a service connection. Azure DevOps uses this service connection to connect Azure resources and deploy code:
图 16.12–新服务主体
-
Once the project is created, you should see a page similar to the following. From the left menu, under Repos, select Branches:
图 16.13–Azure DevOps 主屏幕
-
Copy the link, which we will use to clone the repository to our local machine:
图 16.14–克隆存储库
-
To clone the repository to your system, open Command Prompt and navigate to a folder to which you want to clone the code, then run the following command.
将
<organization>
替换为您的 Azure DevOps 组织:git clone https://<organization>@dev.azure.com/<organization>/HelloWorld/_git/HelloWorld
-
由于我们的存储库是新的空的,我们需要向它添加代码。以下命令创建一个 ASP.NET 5 应用和一个 xUnit 项目,创建一个解决方案文件,并向其中添加一个 web 和测试项目。Ru n 每个命令依次继续:
dotnet new mvc --auth Individual -o HelloWorld dotnet new xunit -o HelloWorld.Tests dotnet new sln dotnet sln add HelloWorld/HelloWorld.csproj dotnet sln add HelloWorld.Tests/HelloWorld.Tests.csproj
-
运行以下命令来构建代码并运行测试来验证是否一切正常:
dotnet build
dotnet test
现在我们已经测试了代码,接下来让我们看看如何为使用代码的 CI 和 CD 创建管道。
为配置项和光盘创建管道
在运行测试之后,我们需要看看配置项和光盘管道是如何创建的。请执行以下步骤:
-
Next, we need to create a pipeline for CI and CD. You can use the code available at https://github.com/PacktPublishing/Enterprise-Application-Development-with-C-Sharp-9-and-.NET-5/blob/master/Chapter16/Pipelines/HelloWorld/azure-ci-pipeline.yml and save it in the root directory of the repository. Let's name it
azure-ci-pipeline.yml
.该管道被配置为当新代码合并到
main
分支时触发。 -
它被配置为具有三个阶段——构建、开发和测试——其中构建阶段被配置为构建代码、运行单元测试和发布工件。开发和测试阶段被配置为将代码部署到 Azure 应用服务。
-
Dependencies are configured at the dev and test stages, where the dev stage depends on build and test depends on the dev stage.
让我们检查一下这个 YAML 文件的几个重要部分。
下面的代码片段包含一个定义变量的部分:
trigger: - main variables: BuildConfiguration: 'Release' buildPlatform: 'Any CPU' solution: '**/*.sln' azureSubscription: 'HelloWorld-Con' # replace this with your service connection name to connect Azure subscription devAppServiceName: 'webSitejtrb7psidvozs' # replace this with your app service name testAppServiceName: 'webSitejtrb8psidvozs' # replace this with your app service name
您会注意到在 YAML 文件中声明了三个变量。保存文件之前,请提供适当的值:
-
azureSubscription
:提供服务连接的名称。 -
devAppServiceName
:为开发部署提供应用服务的名称。 -
testAppServiceName
: Provide the name of the app service for test deployment.为了构建代码,我们使用
DotNetCoreCLI@2
任务并配置command
、projects
,以及可选的arguments
:- task: DotNetCoreCLI@2 displayName: Build inputs: command: 'build' projects: '**/*.csproj'
command
配置为build
,对于要构建代码的项目,路径设置为csproj
。此任务运行.NET CLI 命令,因此我们也可以用其他命令来配置这个任务.NET CLI 命令,如run
、test
、publish
、restore
等。
-
-
To publish code, the
PublishBuildArtifacts@1
task is used. It is configured withPathToPublish
,ArtifactName
, andpublishLocation
:- task: PublishBuildArtifacts@1 inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)/web' ArtifactName: 'drop' publishLocation: 'Container'
PathToPublish
配置有构建工件可用的工件目录的位置,ArtifactName
为drop
,而publishLocation
为Container
将工件发布到 Azure Pipelines。或者,我们也可以将publishLocation
配置为FileShare
。以下代码片段执行部署代码所需的操作:
- download: current artifact: drop - task: AzureWebApp@1 displayName: 'Azure App Service Deploy: website' inputs: azureSubscription: '$(azureSubscription)' appType: 'webApp' appName: '$(devAppServiceName)' package: '$(Pipeline.Workspace)/drop/*.zip' deploymentMethod: 'auto'
在部署作业中,第一步是下载工件,工件的名称应该与
PublishBuildArtifacts@1
任务中配置的名称相同,在本例中为drop
。AzureWebApp@1
任务用于将工件部署到 Azure 应用服务。所需参数为azureSubscription
、appType
、appName
、package
、deploymentMethod
(作为auto
)。
既然工件已经准备好了,我们就可以看到代码是如何提交的,以及代码更改是如何推送的。
推送代码
现在代码和管道已经准备好了,下一步是提交这些更改并将其推送到 Azure DevOps 存储库。
-
在命令提示符下,运行以下命令在本地提交更改并将其推送到 Azure DevOps:
git add . git commit -m "Initial Commit" git push
-
In Azure DevOps, navigate to Pipelines and click Create Pipeline to create a new pipeline:
图 16.15–新管道
-
To configure the pipeline, we need to perform four steps. Select the service in which your repo resides, select the repo, configure the pipeline, and save. For this implementation, select Azure Repos Git to continue, and then select your repo:
图 16.16–源代码管理选择
-
In the Configure tab, choose Existing Azure Pipelines YAML file to continue:
图 16.17–配置管道
-
Select the pipeline file we saved earlier in the repo and click Continue, and then click Run to trigger the pipeline:
图 16.18–YAML 文件选择
-
This will open a page in which we can see the state of the pipeline. The following screenshot is taken from the pipeline run. You will notice three stages have been created:
图 16.19–管道运行总结
在构建阶段,您会注意到两个作业正在进行中。
开发阶段和测试阶段正在等待构建完成。
或者,您可以在 Azure 应用服务上启用部署槽,并配置管道将代码部署到非生产部署槽,例如预生产。一旦您检查了部署代码的健全性,您就可以将生产槽与预生产交换。交换是即时的,没有任何停机时间,您可以让用户获得最新的变化。如果您注意到任何问题,您可以切换回上一个插槽,返回到上一个已知良好的版本。更多信息可参考https://docs . Microsoft . com/en-us/azure/app-service/deploy-staging-slots。
-
After the pipeline execution is complete, navigate to Environments under Pipelines from the left menu. You will notice the dev and test environments are created:
图 16.20–环境
-
Click on the test stage and in the more actions selection, select Approvals and checks to continue:
图 16.21–批准和检查
-
You will find many options to choose from, such as Approvals, Branch control, Business Hours, and so on:
图 16.22–添加支票
-
Select Approvals to continue and it will open a dialog where we can select users/groups as approvers. Provide the necessary details and click Create:

图 16.23–添加批准
- Re-run the pipeline to test the changes. You will notice the pipeline is waiting to execute at the test stage:

图 16.24–等待批准的多级管道
- 点击审核,系统将打开一个对话框,进行批准或拒绝。点击批准完成部署:
图 16.25–批准或拒绝
总之,在本节中,我们首先在 Azure DevOps 中创建了一个新项目,然后将 repo 克隆到一个本地系统,使用创建了一个简单的 ASP.NET Core 应用.NET CLI,在 YAML 创建了一个管道来构建、测试和发布工件,并将它们部署到 Azure App Service,并将代码提交并推回到 repo。接下来,我们通过在报告中选择一个 YAML 文件来创建一个新的配置项/光盘管道,并触发了该管道。在环境中,我们配置了批准检查,并触发了管道来查看其工作方式。
总结
在本章中,我们了解了什么是 Azure DevOps,以及它提供的工具和服务。我们理解了诸如板、回购、管道、测试计划和工件等服务如何帮助我们高效地执行项目。
接下来,我们看了 CI 和 CD 管道及其核心组件。我们还了解了它们如何帮助我们自动化代码的构建和部署。我们通过学习创建 ASP.NET 5 应用以及使用配置项和光盘管道构建和部署到 Azure 应用服务的管道来结束本章。
我希望这本书能帮助你提高你的英语水平.NET 的技能,并激励您尝试和构建更多的应用。通过参考笔记和章节的“进一步阅读”部分,您可以探索更多的主题。
对于企业应用,我们还介绍了典型电子商务应用的快乐之路场景,它可以基于第 1 章“设计和构建企业应用”中定义的需求进行进一步扩展。有一些例子可以扩展端到端流的身份验证/授权,使用应用编程接口网关进行服务到服务的通信和身份验证,实现通知服务等,供您了解更多信息。
我们祝愿您在 C#和中一切顺利.NET 项目,快乐学习!
问题
-
How does continuous deployment differ from continuous delivery?
a.连续交付与数据库协同工作,连续部署支持 web 应用。
b.连续部署在每个时间发布到一个环境,而连续交付在任何时间发布到一个环境。
c.持续部署需要云,而持续交付需要内部服务器。
d.连续部署在任何一次发布到环境中,而连续交付在每一次发布到环境中。
-
What are the characteristics of the CD approach? (Choose two.)
a.专注于缩短周期时间
b.少量复杂的释放
c.基于资源的流程管理
d.自我管理和响应迅速的团队
-
Which component provides the first feedback on the quality of committed application code changes?
a.自动化部署
b.自动供应
c.自动化构建
d.自动化测试
进一步阅读
想了解更多关于 Azure DevOps 的信息,可以参考https://docs . Microsoft . com/en-in/Azure/devo PS/用户指南/服务?view=azure-devops ,对于 Pipelines,可以参考https://docs . Microsoft . com/en-in/azure/devo PS/Pipelines/入门/Pipelines-入门?view=azure-devops 。
第一部分:构建企业应用及其基础
在这一部分中,我们将设想一个具有高水平技术和功能需求的企业应用。然后,我们将讨论应用的架构和设计方法。我们将使用来布局层的结构和解决方案.NET 5。我们还将学习 C# 9 的新语言特性以及.NET 运行时,我们将利用它来构建应用。
本节包含以下章节:
第二部分:交叉问题
目前我们有一个企业应用的框架结构。在用业务和技术功能填充这个框架时,我们会遇到许多将在应用的各个层中使用的代码和结构。这些有时被称为交叉问题。这些交叉问题包括线程、集合、日志、缓存、配置、网络和依赖注入。在这一部分,我们将从的角度快速回顾这些基本原理.NET 5 和企业应用。在每一章中,我们将学习这些交叉关注点如何构成企业应用的构建块,以及它们如何与用户界面和服务层集成。
本节包含以下章节:
第三部分:开发您的企业应用
在本节中,我们将开发应用的不同层。我们将从数据层开始,然后开发 API 层和 web 层。在开发过程中,我们将利用我们在第 2 节、交叉关注点中了解到的交叉关注点。
本节包含以下章节:
第四部分:安全
本节讨论编程的安全性方面。涵盖的主要主题是身份验证和授权。在任何企业应用中,用户界面和服务层都需要得到保护。
本节包含以下章节:
第五部分:健康检查、单元测试、部署和诊断
就像人类的健康一样,任何企业应用的健康都应该很容易检查,如果出现任何异常,我们应该提前得到通知,这样我们就可以采取纠正措施,而不会导致任何停机。在这一部分中,我们将把运行状况检查 API 与我们的应用集成在一起,并测试我们的应用,以便为部署做好准备。然后,我们将学习如何以现代的 DevOps 方式部署我们的应用,并了解如何在生产中监控、诊断和排除应用故障。
本节包含以下章节: