ASP-NET-Core5-初学者指南-全-
ASP.NET Core5 初学者指南(全)
零、序言
NET Core 是一个强大而有效的开源跨平台框架。它可以帮助您构建支持云计算的现代应用,如 web 应用和服务。ASP.NET Core 5 初学者版包含实践教程、项目和自我评估问题,是一本简单易懂的指南,将教您如何使用 ASP.NET Core 5 框架进行开发。您将了解使用 C#8、Visual Studio 2019、Visual Studio 代码、Razor 页面、Blazor、Kestrel、IIS、HTTP.sys、Apache、Docker、AWS 和 Azure 的框架。
您将学习如何编写应用、构建网站以及将 web 应用部署到 AWS 和 Microsoft Azure。您将彻底探索您的编码环境和推荐的最佳实践,我们将提供代码示例,系统地介绍您将在当今行业中面临的顶级场景。在本书结束时,您将能够利用 ASP.NET Core 5 在各种真实场景中构建和部署 web 应用和服务。
这本书是给谁的
本书面向希望学习如何使用 ASP.NET Core 框架开发基于 web 的应用的开发人员。要充分利用这本书,需要熟悉 C#语言并对 HTML 和 CSS 有基本的了解。
这本书涵盖的内容
第一章ASP.NET Core 5简介提供了一个简短的历史课程,从.NET 1.0 通过不同的路径,到使用.NET Core 的“一个.NET 来统治所有人”,以及 ASP.NET Core 如何在这一基础上发挥作用。我们将介绍和解释很多术语。在阅读本书的过程中,还有一些工具对您很有价值,因此我们将在这里介绍其中的一些工具。
第 2 章跨平台设置解释了在.NET Core 不局限于在 Windows 上运行的情况下,在 Linux 和 Mac 上开发不会成为构建.NET 应用的障碍。对于 Linux,最新的 Windows 10 功能更新为 Windows Subsystem For Linux 2 提供了一个优秀的开发伙伴,它使您能够在 Linux 上本机运行并从 Windows 进行调试。在跨平台时,您需要注意以下几点,这些细节将在本章中指出。
第 3 章依赖注入解释了依赖注入(DI)软件设计模式,并演示了如何使用它实现类与其依赖类之间的控制反转(IoC)。我们将介绍框架服务,并解释服务生命周期和注册方法。最后,您将学习如何为 DI 设计服务。
第 4 章Razor 视图引擎解释了编码页面比以往任何时候都更容易、更高效的概念,您将了解 Razor 如何驱动不同的 ASP.NET Core web 框架生成 HTML 标记(通过使用统一的标记语法)。为了了解不同的 web 框架,您将使用 Model-View-Controller(MVC)和 Razor 页面构建一个简单的 To-Do 列表应用来创建一个动态 web 应用。此外,您还将了解每个 web 框架的优缺点。
第 5 章Blazor 入门介绍了如何熟悉一个框架,使您能够使用.NET 构建交互式 web UI。您可以使用 C#和 JavaScript(而不是 JavaScript)进行编写。您可以共享在.NET 中编写的服务器端和客户端应用逻辑,并且可以将 UI 呈现为 HTML 和 CSS(这对于移动浏览器非常有用)。首先,我们将了解构建强大 web 应用的不同 Blazor 托管模型,并权衡它们的优缺点。然后,我们将看一看高级目标,以实现使用尖端技术构建真实世界应用的目标。在本章中,您将使用 Blazor 创建一个具有实时功能的旅游景点应用。您将开始使用 ASP.NET Core Web API 和 Entity Framework Core 构建应用的后端,最后使用 SignalR 设置实时更新。
第 6 章探索 Blazor Web 框架,将剩余的部分放在一起完成第 5 章中强调的目标,开始 Blazor的学习。在本章中,您将使用不同的 Blazor 托管模型创建两个不同的 web 应用:Blazor 服务器和 Blazor web 组件。本章是本书的核心,在这里,您将体验使用各种相互连接的技术构建不同应用的过程。一步一步的代码示例和直观的插图使本章有趣、令人兴奋且易于理解。
第 7 章API 和数据访问将带您参观,我们将探讨 API 和数据访问如何协同工作以实现两个主要目标:服务和获取数据。我们将带您参观实体框架、RESTAPI、数据库管理系统(DBMSS)、SQL、LINQ 和 Postman。我们将从理解在实体框架核心(EF 核心)中使用真实数据库时的不同方法开始。然后,我们将研究如何在现有数据库中使用 EF-Core,并将使用 EF-Core 的代码优先方法实现与真实数据库对话的 api。您将与 Entity Framework Core 协同构建一个 ASP.NET Core Web API 应用,以在 SQL Server 数据库中执行基本数据操作。您还将实现用于公开某些 API 端点的最常用 HTTP 方法(谓词),我们将执行一些基本测试。
第 8 章身份旨在从前端(用户如何认证)和后端如何验证该身份两个方面传授应用中身份概念的基础知识。它将解释不同的方法,例如基本身份验证和基于声明的身份验证,以及引入现代身份套件(Azure AD)。将解释主要的 OAuth 2 和 OpenID 连接流,以了解在应用中使用哪种连接。
第 9 章容器介绍了分解巨石的概念,我们将提供一个基本的理解,为什么今天每个人似乎都在谈论容器。
第 10 章部署到 AWS 和 Azure解释了我们说 ASP.NET 天生就是部署到云端的意思,然后我们将探索一些平台,包括 Amazon Web Services(AWS)和 Microsoft Azure(我们将解释为什么我们关注这两个平台)。然后,我们将深入研究并向您展示如何在 AWS 和 Azure 上部署您的项目(以快速和基本的方式)!
第 11 章浏览器和 Visual Studio 调试介绍了现代浏览器中用于检测错误原因以及如何解决问题的一些重要功能。我们还将介绍 VisualStudio 对调试的支持,以及 IDE 如何使您成为更好的程序员。
第 12 章与 CI/CD集成,探讨了在现代 DevOps 世界中程序员应该熟悉的工具和实践。
第 13 章云本机解释了如何在如今很多职位描述中都包含云这个词,虽然并非所有生成的代码都会在公共云中运行,但有必要理解云本机的含义,以及设计应用以利用云功能需要采取哪些步骤。这可能是云存储与本地磁盘、扩展与缩小,以及以前由 Ops 处理的一些任务现在由开发人员负责。在本章结束时,您应该理解为什么执行现有应用的升降与在云中启动有很大不同。
充分利用这本书
假设您具备 C 语言的基本工作知识。所有代码都在 Windows10 上进行了测试,并注意到了例外情况。本书中使用的主要软件是 Visual Studio Code 和 Visual Studio 2019,它们都可以从 Microsoft 免费下载。具体说明见以下章节:

如果您使用的是本书的数字版本,我们建议您自己键入代码或通过 GitHub 存储库访问代码(下一节提供链接)。这样做将帮助您避免与复制和粘贴代码相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub 的下载本书的示例代码文件 https://github.com/PacktPublishing/ASP.NET-Core-5-for-Beginners 。如果代码有更新,它将在现有 GitHub 存储库中更新。
我们的丰富书籍和视频目录中还有其他代码包,请访问https://github.com/PacktPublishing/ 。看看他们!
运行中的代码
本书的行动代码视频可在查看 http://bit.ly/3qDiqYY 。
下载彩色图片
我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。你可以在这里下载:
https://static.packt-cdn.com/downloads/9781800567184_ColorImages.pdf
使用的约定
本书中使用了许多文本约定。
Code in text:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。下面是一个示例:“前面的代码以ServerPrerendered作为默认呈现模式呈现App.razor组件。”
代码块设置如下:
<body>
<app>
<component type="typeof(App)" render-mode="ServerPrerendered" />
</app>
@*Removed other code for brevity*@
</body>
当我们希望提请您注意代码块的特定部分时,相关行或项目以粗体显示:
<script src="_framework/blazor.webassembly.js"></script>
<script src="storageHandling.js"></script>
</body>)
任何命令行输入或输出的编写方式如下:
dotnet run Base64 encoded: YW5kcmVhczpwYXNzd29yZA==Response: Hello Andreas
粗体:表示一个新术语、一个重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词出现在文本中,如下所示。下面是一个示例:“选择ASP.NET Core Web 应用模板并单击下一步
提示或重要提示
看起来像这样。
联系
我们欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中注明书名,并发送电子邮件至customercare@packtpub.com。
勘误表:尽管我们已尽一切努力确保内容的准确性,但还是会出现错误。如果您在本书中发现错误,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata,选择您的书籍,单击 errata 提交表单链接,然后输入详细信息。
盗版:如果您在互联网上发现我们作品的任何形式的非法复制品,请您提供我们的位置地址或网站名称,我们将不胜感激。请通过copyright@packt.com与我们联系,并提供该材料的链接。
如果您有兴趣成为一名作家:如果您对某个主题有专业知识,并且您有兴趣撰写或贡献一本书,请访问authors.packtpub.com。
审查
请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?然后,潜在读者可以看到并使用您的无偏见意见做出购买决定,我们 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们书籍的反馈。非常感谢。
有关 Packt 的更多信息,请访问Packt.com。
一、ASP.NET Core 5 简介
.NET 5 是.NET 平台中最新、最强大的。NET5 是.NETCore3.1 的继承者。本章简要介绍了.NET 框架的历史,然后深入讨论了此版本带来的内容。本章最后介绍了在继续探索后续章节中的详细信息之前,您希望使用的实用程序和工具。我们将涵盖广泛的主题,包括.NET 的跨平台使用、创建可视层的不同方法、后端组件(如身份和数据访问)以及云技术。
本章将介绍以下主题:
- 解释 ASP.NET Core
- 进修
- .NET5 和 C#9 有什么新功能
- 网站和网络服务器
- Visual Studio 代码
- Windows 终端
技术要求
本章包括简短的代码片段,以演示所解释的概念。需要以下软件:
- Visual Studio 2019:Visual Studio 可从下载 https://visualstudio.microsoft.com/vs/community/ 。社区版是免费的,将用于本书的目的。
- Visual Studio 代码:Visual Studio 代码可从下载 https://code.visualstudio.com/Download 。
- .NET Core 5:.NET Core 框架可从下载 https://dotnet.microsoft.com/download/dotnet/5.0 。
确保下载 SDK,而不仅仅是运行时。您可以通过打开命令提示符并运行dotnet --infocmd 来验证安装,如图所示:

图 1.1–验证.NET 的安装
请访问以下链接查看 CiA 视频:https://bit.ly/3qDiqYY
讲解 ASP.NET Core
NET 的第一个版本是在 2002 年发布的,所以我们现在的版本 5 听起来并不令人印象深刻,因为它已经是 18 年后的版本了。然而,由于编号系统和各种旁道,它比这稍微复杂一些。一部完整的历史本身可能就是一本书,但为了了解我们现在的处境,我们将带你们沿着记忆之路走一小段路。
当.NET 出现时,根据您的场景,您可以选择几种编程语言。VisualBasic 在介绍性类型编程中很受欢迎,因为顾名思义,它是面向视觉的,并且易于入门。然而,VB 不适合大规模编写高性能的复杂应用,然而,它并不是语言的强项。Windows 本身主要以 C 和 C++编写,是专业级软件的首选路由。虽然这些语言的功能非常强大(现在仍然如此),但它们却因允许程序员自食其果而臭名昭著,因为它们让程序员负责内存管理和其他难以调试和排除故障的低级操作。
与微软直接提供的语言实现并行,Sun Microsystems 发布了 Java 作为这些挑战的解决方案。该工具没有生成本机代码,而是生成了托管代码,抽象了内存管理,使事情变得更简单。该语言的语法是 C++风格的,因此对于想要开发它的开发者来说,过渡并不难。编写的代码应可移植到多个平台,这也是一个明确的目标。这是由安装在给定系统上执行的Java 虚拟机(JVM启用的。
托管代码与非托管代码
编程语言经过多年的发展。在第一批计算机通过实际转动开关和控制杆进行编程的地方,你现在可以编写指令,即使是非程序员也能理解某些命令的含义。
一个通常的是指通过调用语言低级(close)或高级(abstract),与计算机的母语(0 和 1)相对接近。在最低层次上,您有像汇编语言这样的语言,从理论上讲,它们的开销最小(前提是您可以找到非常有才华的程序员),但除了复杂之外,汇编语言还不能跨不同的 CPU 体系结构移植。C#更倾向于另一端,语言更自然,许多“困难的事情”对程序员来说是隐藏的。还有一些更高级的语言,比如 Scratch(一种基于块的语言),面向希望进入编程的孩子。(低与高没有正式定义。)
C#用来实现这一点的机制之一是通过一个中间层(对于.NET 来说,这是公共语言运行时)将代码实时转换为计算机能够理解的底层机器代码。这意味着程序员不需要处理分配和释放内存、不干扰其他程序的进程等等,并且通常会做大量繁重的工作。对于.NET Core,这是处理跨平台执行的关键部分–如果没有这个中间人,为 Windows 笔记本电脑编写的代码如果没有重新编译和特定于平台的调整,将无法在手机上运行。
这个概念对于 C#来说并不新鲜,也不是唯一的,它也是 Java 中使用的概念。最初,它是在 IBM 大型机时代构思的。在个人计算机上,这最初是一个挑战,因为托管代码总是会由于发生的转换而产生开销,而在资源受限的计算机上(当.NET 1.0 发布时),它可能运行缓慢。较新的计算机处理这一问题的效率更高,而且.NET 多年来一直在优化,因此对于大多数应用来说,代码是否得到管理不再是一个大问题。
介绍.NET 平台
微软从 Java 中汲取灵感,从他们提供的生态系统中汲取经验,提出了.NET。平台结构如图 1.2所示。
.NET 也基于托管代码,需要安装一个公共语言运行库(CLR)才能执行。C#语言在同一时间框架内发布,但.NET 也支持 Visual Basic 和 J#,强调它是一个更通用的框架。其他需要安装额外软件才能运行应用的编程语言面临着让最终用户自己安装的挑战。另一方面,微软有提供操作系统的优势,因此他们可以选择将.NET 作为预装二进制文件。

图 1.2–.NET 平台
正如名称的第二部分所暗示的那样,.NET Framework 的目的是要比口述某种语言必须使用并且只能用于特定类型的应用更完整,因此它本质上是模块化的。如果要创建作为 Windows 服务运行的应用,除了具有图形用户界面的应用外,还需要其他库,但可以使用相同的编程语言。
最初的.NET Framework 设计在技术上并不排除在 Windows 以外的其他操作系统上运行,但由于没有看到为 Linux 和 Apple 产品提供该框架的动机,它很快就依赖于仅在桌面 Windows 上可用的组件。
虽然 Windows 在基于 x86 的 PC 上运行得很好,但它并没有在受限制的设备上运行。这导致微软开发了其他版本的 Windows,如用于智能手机的 Windows Mobile,用于 ATM 和收银机等设备的 Windows CE,等等。为了迎合开发人员的需求,使他们能够以最少的再学习体验创建应用,这些平台需要.NET,但.NET 的构建不是为了在没有可用桌面组件的情况下运行。结果是.NET 被分割成多条路径,智能手机和平板电脑使用.NET Compact Framework,类似 Arduino 的设备使用.NET Micro Framework。
基本上,如果你精通 C#,你可以以多种形式的设备为目标。不幸的是,这在现实世界中并不总是那么容易。
图书馆是不同的。如果您在桌面上编写代码并希望将其移植到移动设备,则必须了解如何实现.NET 精简版中没有的功能。您还可能遇到令人困惑的事情,例如两个平台上都有 XML 生成器,尽管它们看起来很相似,但生成的输出却不一样。
.NET Framework 是随 Windows 操作系统一起发布的,但这通常不是最新版本,因此您仍然需要安装更新才能使其正常工作或安装其他组件。
更糟糕的是,当您必须在同一台机器上运行多个版本的.NET 时,经常会出现这样的情况,即这些版本之间不能很好地配合,您必须确保您的应用调用了正确版本的库。当 Windows 上的 C++出现时,对.NET 进行的挑战可能会听到这被称为“DLL 地狱”。
本书的标题中也使用了术语ASP(ASP.NET)。ASP 在本历史课中有自己的轨迹。在 WindowsNT 的旧时代,呈现网页不是服务器的核心组件,而是可以通过一个名为活动服务器页面(ASP简称)的附加组件安装在 Internet 信息服务器之上。当.NET 发布时,它被作为 ASP.NET 进行保存。与.NET 的基本组件非常相似,多年来,这也见证了各种形式的多次迭代。最初,您使用 ASP.NET Web 表单,在其中编写代码和脚本,引擎将这些代码和脚本呈现为 HTML 输出。2009 年,极具影响力的 ASP.NET MVC 发布,实现了模型-视图-控制器模式,该模式仍然存在。
模式
模式是解决软件中常见问题的一种方法。例如,如果您有一个用于在在线商店订购产品的应用,则会涉及一组通用的对象和操作。产品、订单等通常存储在数据库中。您需要使用这些对象的方法–在客户订购产品时减少库存,由于客户有购买历史记录而应用折扣。您需要在网页上显示一些内容,以便客户可以查看商店及其产品并执行操作。
这通常以所谓的模型视图控制器(MVC模式)实现。
产品和订单描述为型号。执行的操作(如减少数量、检索定价信息等)在控制器中实现。在视图中实现对最终用户可见的输出的呈现,以及接受来自最终用户的输入。我们将在本书后面的代码中看到这一点。
模式涵盖了一系列问题,通常是通用的,并且独立于它们所用的编程语言。
本书将涉及适用于 ASP.NET Core 应用的模式,但不涵盖一般模式。
令人困惑的是,还有另外一些基于网络的项目单独启动,例如 Silverlight,它作为浏览器中的插件运行。当时的想法是,由于浏览器将代码限制在沙箱中,因此这可以作为访问通常仅在浏览器外部可用的功能的桥梁。它并没有成为热门,所以尽管您仍然可以让它运行,但它被认为是不推荐的。
使用 Windows 8 的应用模型,您可以使用 HTML 为用户界面编写可安装在设备上的应用,与实际的 web 应用不直接兼容。依赖 Windows 应用商店进行分发,但并非所有用户都能立即升级到新的 Windows 版本,这一事实阻碍了它的发展,开发人员大多更愿意接触到最大的受众。
在 Windows8 和.NET4.5 发布的同时,微软推出了.NET 标准。这是任何.NET 堆栈的基类库中的一组 API。这意味着某些代码在桌面 Windows 应用中的工作效果与用于 Windows Phone 的移动应用一样好。这并没有禁止在顶层使用特定于平台的附加功能,但更容易实现代码的基本可移植性。这并不意味着您实现了write once run everywhere用例,而是我们现在看到的跨平台生态系统的开始。
微软主要关注 Windows 生态系统的发展,但在公司之外,Mono 项目致力于创建一个可以在 Linux 上运行应用的开源版本的.NET。Linux 的努力最初并没有取得进展,但当创造者米格尔·德·伊卡扎(Miguel de Icaza)创办了 Xamarin 公司,专注于利用这项工作使.NET 在 iOS 和 Android 设备上运行时,它获得了吸引力。与精简版的.NET 非常相似,它与桌面上的类似,但并不完全相同。
在.NET 领域之外,技术多年来发生了变化。到 2020 年,你可以得到比 2002 年台式机更强大的移动设备。苹果设备在 2020 年随处可见,而在 2002 年,离 iPhone 和 iPad 发布还有几年的时间。另一个重要的事情是,在 2002 年,微软编写的代码将主要由其员工阅读和更新。开源不是从 Redmond 出来的东西。
这些趋势是以不同的方式处理的。微软早在 2008 年就开始对.NET 进行开源,尽管它还不是完整的软件包,而且对所选择的许可证也有抱怨,有些人认为它只是半开源的。
快进到 2016 年.Net Core 发布时。NET 当时的版本是 4.6.2,.NETCore 是从 1.0 开始的。从那时起,最初的.NET 就被称为“经典的.NET”。
Windows mobile/Phone 在市场上的失败部分解决了移动平台问题。Xamarin 也是在 2016 年被收购的,这意味着移动设备意味着谷歌和苹果的操作系统。
此时,微软已经完全致力于开源,甚至开始接受外部对.NET 的贡献。该语言的设计仍由微软负责,但策略是公开的,非微软开发者做出了相当大的贡献。
微软从过去吸取了教训,认识到使用.NETCore 而不是.NETClassic 不会有大的转变。不管开发人员是否同意新版本更好,每个人都不可能在短时间内重写现有代码,特别是因为.NET Core 的初始版本中没有 API。
重新迭代了.NET 标准消息。您可以在.NET 4.6 中针对.NET 标准 1.3 编写代码,这也可以在.NET Core 1.0 中使用。其目的是,这可以用于迁移策略,您可以将代码逐段移动到与.NET 标准兼容的项目中,并在编写与.NET Core 一起使用的新代码时将不兼容的代码留在后面。
不幸的是,人们很难跟踪所有术语--.NET、.NET Classic、.NET Core、.NET Standard 以及所有相应的版本号,但时至今日,将这些术语混合在一起仍然是一种可行的策略。
如前所述,.NET Core 的版本号为 1.0。从那时起,它增加了数量,达到 3.1。乍一看,这意味着下一个版本将被称为.NETCore5 听起来不符合逻辑。放弃此编号的主要原因有三个:
- .NETCore4.x 很容易与.NET4.x 混淆。
- 因为有一个.NET4.x(非核心),下一个主要的数字是 5。
- 为了说明这两条路径如何“合并”,它们在版本 5 中相遇。为了避免混淆,从版本名中删除了“Core”。
在新版本方面,.NETClassic 已经走到了生命的尽头,因此,今后,(在.NET5 之后)将以.NET6、.NET7 等命名,以.NETCore 作为基础框架。
.NET Classic 不会很快被支持或弃用,因此现有代码将继续工作,但不会进行新功能和投资。
保障性策略
传统的.NET Classic 版本具有很长的可支持性,尽管没有固定的生命周期,但这取决于 service pack 版本及其发布时使用的操作系统。
借助.NET Core 2.1,微软转向了 Linux 生态系统中常见的模式,其版本被称为LTS(长期支持)和非 LTS。LTS 版本将有 3 年的支持期,而非 LTS 版本只有一年的支持期。次要版本预计在支持窗口期间发布,但在主要版本发布时设置结束日期。
图 1.3 显示了.NET 发布时间表,重点是其可支持性时间表。

图 1.3–.NET 可保障性计划
显然,我们不能保证每年都会部署一个新版本,但这是当前的计划。从.NET Core 3.1 开始,计划周期为每年 11 月的新版本,每隔一年更新一次。NET 5 于 2020 年 11 月作为非 LTS 版本发布。NET6 的目标是在 2021 年 11 月发布 LTS。
这并不意味着在不受支持的版本中编写的代码会中断或停止工作,但不会发布安全补丁,也不会为较旧的运行时维护库,因此相应地计划升级。(微软在指导如何将代码更新到新版本方面有着良好的记录。)
它有时感觉像是一段颠簸的旅程,但除非您必须处理遗留系统,否则当前的状态比很长一段时间以来更加简洁。
这一节主要是一节历史课,讲的是我们是如何走到现在这一步的。在下一节中,我们将友好地介绍一个基于 C#代码的基本 web 应用。
刷新你的 C#知识
C#语言非常广泛,有专门的书籍,而且确实有一些书籍涵盖了从以前从未见过的编程到高级设计模式和优化的所有方面。本书不打算涵盖仅适用于高级开发人员的非常基本的内容或深奥的概念。目标受众是初学者,我们将通过一个Hello World类型的示例进行简短的介绍,为您的机器搭建舞台,并确保机器正常工作。
如果您对 VisualStudioWeb 应用模板的工作方式感到满意,并希望深入了解新的内容,请跳过本节。
我们将从以下步骤开始:
- 启动 Visual Studio 并选择新建项目。
- 选择ASP.NET Core Web 应用,点击下一步。
- 将解决方案命名为
Chapter_01_HelloWeb并为本书练习选择合适的位置(如C:\Code\Book\Chapter_01,然后点击创建。 - 在下一个屏幕上,确保 ASP.NET 5 内核 AuthT1。无需检查Docker 支持或配置认证。
- 一旦代码加载并准备就绪,您应该通过按F5以调试模式运行 web 应用来验证您的安装是否正常工作。第一次可能需要一点时间,但希望没有错误,您的浏览器中会显示以下内容:

图 1.4–运行默认 web 应用模板
没什么特别的,但这意味着你可以在后面的章节中做更复杂的事情。如果在运行时遇到问题,在继续之前,现在是修复问题的时候了。
让我们看一下构成此功能的一些组件和代码。
在 Visual Studio 的右侧,您将看到解决方案中的文件:

图 1.5–Visual Studio 2019 中 web 应用的文件结构
此结构特定于空的 web 应用模板。您更可能使用 MVC 或 Blazor 模板来构建更高级的内容,除非您想从头开始编写所有内容。
我们来看看Program.cs的内容:
using Microsoft.AspNetCore.Hosting;using Microsoft.Extensions.Hosting;
namespace Chapter_01_HelloWeb { 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>(); }); }}
我们看到一个Main方法,在这个文件中,它的唯一目的是启动一个处理 web 请求和进程的进程。您可以运行不同类型的主机进程,因此建议您运行一个通用主机进程,然后进一步自定义它以指定它是一个 web 主机进程。由于这是本书的第一章,您还没有被介绍过其他类型的主机,但是在第 2 章跨平台设置中,我们将进入一个旋转不同主机类型的示例。
在本例中,我们使用了Emptyweb 模板,但这是样板代码,与其他基于 web 的模板类似。
在前面的代码片段中有一个引用Startup,这是指Startup.cs的内容:
using Microsoft.AspNetCore.Builder;using Microsoft.AspNetCore.Hosting;using Microsoft.AspNetCore.Http;using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.Hosting;
namespace Chapter_01_HelloWeb { public class Startup { // This method gets called by the runtime. Use this method // to add services to the container. public void ConfigureServices(IServiceCollection services) { }
// 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(); }
app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); }); }); } }}
如果您最近没有用 C#编写 web 应用,这可能是您不熟悉的。在.NETClassic 中,为 web 应用设置配置的仪式分布在多个配置文件中,配置类型之间的语法可能略有不同。一个特别令人发指的问题是,当你有一个“隐藏的”web.config文件覆盖了你认为应该应用的文件时。这也是一种一刀切的设置,其中包含了与应用无关的 XML 行。
在.NET Core 中,这是集中到一个模块化程度更高的文件中。在更复杂的应用中,可能需要使用其他文件,但启动模板不需要这些文件。这里要观察的模式是它的形式为app.UseFeature。例如,如果您添加了app.UseHttpsRedirection,这意味着如果用户在http://localhost中输入,他们将自动重定向到https://localhost。(强烈建议所有网站都使用https)虽然本示例中没有添加太多逻辑,但您还应该注意检查环境是否为开发环境的if语句。可以创建更高级的每环境设置,但对于决定是否应在浏览器中显示详细异常这样的简单事情,这是一个有用的选项。
从代码本身来看并不明显,但引入的这些特性被称为中间件。
中间产品比你从这里得到的印象更强大;这将在后面的章节中更详细地介绍。
Configure方法作为序列运行,将特性动态加载到 web 托管进程的启动中。这意味着陈述的顺序很重要,如果你不注意,很容易混淆。如果app.UseB首先依赖于app.UseA加载,请确保代码中也是这样。
需要注意的是,这种方法并不特定于基于 web 的应用,但也适用于其他基于主机的应用。
此处生成可见输出的行如下所示:
app.UseEndpoints(endpoints =>{ endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); });});
让我们将更改为以下内容:
app.UseEndpoints(endpoints =>{ endpoints.MapGet("/", async context => { await context.Response.WriteAsync("<h2>The time is now:</h2>" + DateTime.UtcNow.ToString()); });});
这段代码意味着我们告诉.NET 运行时连接一个在 URL 根侦听的端点,并直接向 HTTP 会话写入响应。为了证明我们可以比原始的"Hello World!"字符串更进一步,除了使用一个生成动态值的变量外,我们还输出 HTML 作为它的一部分。(注意:在本例中,浏览器决定是否呈现 HTML,因此您可能会在计算机上看到没有格式的标记。)
如果再次运行应用,您应该会看到打印的当前时间:

图 1.6–打印当前时间的 Hello World
如果您从事过更多以前端为中心的任务,您可能会注意到,虽然前面的代码段使用 HTML,但它似乎缺少一些内容。通常,您会使用层叠样式表(.css文件)将样式应用于 web 页面,但这种方法是一种更精简的版本,我们不涉及它。后面的章节将向您展示比我们在这里看到的更令人印象深刻的造型方法。
如果你以前曾涉猎过任何 web 内容,你可能已经学会了,无论是通过艰苦的方式还是通过被告知,你都不应该将代码和 UI 混为一谈。这个例子似乎很好地违反了这个规则。
一般来说,确实不鼓励以这种方式实现 web 应用,因为软件工程的基本原则之一是分离关注点。例如,您可以让前端专家创建用户界面,而对代码中的幕后工作知之甚少,让后端开发人员处理业务逻辑,只关心应用“引擎”的输入和输出。
不过,上述方法并非完全无用。web 应用具有“健康端点”的情况并不少见。在处理微服务时,可以通过监控解决方案或容器编排解决方案调用该端点。它们通常只寻找 web 应用处于活动状态的静态响应,因此我们不需要为此构建用户界面和复杂的逻辑。要实现这一点,您可以在Startup.cs中添加以下内容,同时并行执行“适当”的 web 应用:
endpoints.MapGet("/health", async context =>{ await context.Response.WriteAsync("OK");});
如果您使用过早期版本的 Visual Studio(2017 年之前),您可能已经体验过为代码使用项目和解决方案文件的烦恼。如果您在 VisualStudio 之外添加或编辑了文件,然后尝试返回编译和运行代码,那么在 IDE 中经常会收到关于某些错误的投诉。
这一问题已经解决,您现在可以处理其他应用和其他文件夹中的文件,只需将生成的文件保存在项目结构中的正确位置即可。
.NET 经典 web 应用的项目文件(.csproj从 200 多行代码开始。作为比较,我们刚刚创建的 web 应用包含 7 行(其中包括 2 行空格):
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <TargetFramework>net5.0</TargetFramework> </PropertyGroup>
</Project>
要在 Visual Studio 中查看此内容,您必须右键单击项目名称并选择卸载项目,然后选择编辑.csproj。编辑完文件后,需要重新加载项目才能再次使用它。
在这一点上,我们建议您对代码进行修改,并在继续之前查看结果。
在本演练中,我们依靠 Visual Studio 2019 为我们提供了一组模板和图形用户界面,以供点击。NET 不强制使用 VisualStudio,因此如果您想使用其他编辑器,可以从命令行复制它。运行dotnet new命令查看可用选项,并提供一些提示:

图 1.7–列出.NET 中可用的模板
要复制我们在 VisualStudio 中所做的工作,请键入dotnet new web。默认项目名称将与您所在的文件夹相同,因此请确保命名文件夹并进行相应更改。
这将使您有一些示例代码来测试和验证系统是否正常工作。然而,C 语言还有更多内容,接下来,我们将看看最新版本的 C 语言带来了什么。
学习.NET5 和 C#9 的新功能
一般的经验法则是.NET、C#和 Visual Studio 的新版本在同一时间段发布。这当然是处理它的最简单的方法——获取最新的 VisualStudio,其他两个组件在安装过程中会自动跟进。
该工具并不总是紧密耦合的,因此如果出于某种原因您无法使用最新版本,您可以研究是否有方法使其与 Visual Studio 的早期版本一起工作。(这通常可以在 Microsoft 的需求文档中找到。)
一个常见的误解是.NET 和 C#必须处于同一版本级别,升级一个意味着升级另一个。然而,.NET 和 C#的版本并不是直接耦合的。C#已达到 9 版,而.NET 达到 5 版,这一事实进一步说明了这一点。NET 也与使用 C 语言无关。(在过去,你有 Visual Basic,现在也有 F。)如果你想保持特定的 C 版本(不升级到 C 的最新版本),那么在升级.NET 之后,这种组合通常仍然有效。
C 语言定义的东西通常是向后兼容的,但模式可能不是。
例如,在 C#3 中引入了var关键字。这意味着以下声明有效:
var i = 10; // Implicitly typed.int i = 10; // Explicitly typed.
这两种变体都可以,而且.NETCore5 不会强制使用这两种样式。
作为.NET 发展的一个例子,从.NET Core 1.x 到.NET Core 2.x 发生了一些变化,其中 C#的语法没有改变,但.NET 希望在代码中设置身份验证的方式意味着,即使 C#代码完全有效,您的代码也会无法工作。确保您了解.NET 在何处强制使用某种样式,以及 C#是罪魁祸首。
您可以通过编辑项目文件(.csproj并添加LangVersion属性来指定使用哪个 C#版本:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net5.0</TargetFramework> </PropertyGroup> <PropertyGroup> <LangVersion>9.0</LangVersion> </PropertyGroup></Project>
很难跟踪代码中可以更改和优化的内容。随着.NET 编译器平台于 2014 年发布,昵称为 Roslyn,通过引入对代码的实时分析,这一点得到了极大的改进。以前您必须为 IDE 编译代码以显示错误和警告,现在在编写代码时会显示这些错误和警告。它不局限于指出阻止代码运行的问题,还将建议进行改进。
例如,考虑以下内容:
Console.WriteLine("Hello " + name);
Roslyn 将建议String插值作为选项:
Console.WriteLine($"Hello {name}");

图 1.8–代码改进建议
对于这样一个简单的示例,它可能看起来不像的改进,但它通常会使较长的字符串更具可读性。不管怎样,这都是一个建议,而不是强加给你的东西。
这意味着当主题为“新内容”时,可以将其分为两部分-.NET 和 C。NET 中的新功能将主要在其他章节中介绍。C#中的新功能将在这里介绍,并将在后续章节的代码示例中使用。请注意,并非本书中的所有代码都会在任何地方使用 C#9 语法,只要新语法主要是风格,如果您不是强制执行一组标准的大型开发团队的一员,建议您选择自己的风格。
在.NET5 中有什么新功能?
许多改进都在幕后进行,使事情更顺利、更全面。然而,也有一些更明显的改进。本章将只提供几个亮点,因为细节将在本书后面介绍。
用.NET Classic 缩小差距
对于.NET Core 1.0,许多项目不可能从.NET 4.x 进行移植,因为对于某些功能根本没有对应的库。NET Core 3.1 在大多数实际应用中消除了这一障碍,而在.NET Core 5 中,该框架被认为是 API 和库端的完整功能。
一些技术已被弃用,因此没有继续使用(参见本章后面的删除/更改功能部分):
-
统一的.NET 和单一基类库:以前,Xamarin 应用(移动应用)是基于 Mono BCL 的,但现在它已经迁移到了.NET 5 中,并因此提高了兼容性。
-
多平台原生应用:单个项目可以针对多个平台。如果使用 UI 元素,.NET 将以平台本机控件的形式处理此问题。
-
云本机:当前的.NET 代码肯定会在云中运行,但将采取进一步的步骤将.NET 标记为云本机框架。这包括减少占用空间,以便在容器和单文件可执行文件中更方便地使用,因此您不需要安装.NET 运行时,并调整云故事和本地开发人员体验,使它们处于功能对等状态。
-
Blazor WebAssembly: .NET Core 3.1 introduced Blazor apps that were rendered server-side. With .NET 5, they can also be rendered client-side, enabling offline and standalone apps.
目标是代码几乎相同,因此很容易从一个托管模型切换到另一个托管模型。
-
多平台网络应用:Blazor 应用最初被认为是网络应用的载体,在浏览器中非常有效。我们的目标是,这对于移动设备或本机桌面应用同样有效。
-
持续改进:BCL 中更快的算法,运行时中的容器支持,对 HTTP3 的支持,以及其他调整。
在讨论了.NET5 的新功能之后,让我们继续讨论 C#9。
C#9 有什么新功能?
C#9 的首要目标是简化。这门语言已经足够成熟,你可以用某种方式做你想做的大部分事情,因此与其增加更多的功能,不如让这些功能更可用。在本节中,我们将介绍构造代码的新方法,并解释您可以创建的一些新代码。
顶级课程
简化的一个很好的例子是顶级程序。使用 C#8,Visual Studio 模板创建了以下代码作为控制台应用的起点:
using System;
namespace ConsoleApp2 {
class Program {
static void Main(string[] args) { Console.WriteLine("Hello World"); } }}
有一个原因,为什么有这么多的代码行做这么少,但对于初学者来说,这是一个很大的仪式开始。前面的代码段现在可以这样编写:
Using System;
Console.WriteLine("Hello World");
这不支持在整个程序中省略类和方法。这是关于简化Main方法的,该方法通常只会引导应用,并且在给定的应用中只能有一个。
仅初始化属性
当处理对象时,您通常会这样定义和创建它们:
static void Main(string[] args){ InfoMessage foo = new InfoMessage { Id = 1, Message = "Hello World" };}
public class InfoMessage { public int Id { get; set; } public string Message { get; set; }}
在这段代码中,属性是可变的,因此如果您以后想要更改 ID,这是可以的(当访问器是公共的)。为了涵盖希望公共属性不可变的情况,引入了一种新类型的属性,其中包含仅限 init 的属性:
public class InfoMessage { public int Id { get; init; } public string Message { get; init; }}
这使得属性不可变,因此一旦您定义了它们,它们就不能更改。
初始化访问器和只读字段
初始化访问器仅用于初始化期间,但此与只读字段不冲突,如果需要构造函数,则可以同时使用这两个字段:
public class City { private readonly int ZipCode; private readonly string Name;
public int ZipCode { get => ZipCode; init => ZipCode = (value ?? throw new ArgumentNullException(nameof(ZipCode))); }
public string Name { get => Name; init => Name = (value ?? throw new ArgumentNullException(nameof(Name))); }}
记录
Init 对单个属性起作用,但如果要使其应用于类中的所有属性,可以使用record关键字将类定义为记录:
public record class City { public int ZipCode {get; init;} public string Name {get; init;}
public City(int zip, string name) => (ZipCode, Name) = (zip,name);}
当您将对象声明为记录时,这将为您带来其他新功能的价值。
带着表情
由于对象具有无法更改的值,因此如果值发生更改,则必须创建一个新对象。例如,您可以拥有以下内容:
City Redmond = new City("98052","Redmond");
//The US runs out of zip codes so every existing code is // assigned //a 0 as a suffix City newRedmond = new City("980520","Redmond");
使用with表达式可以复制现有属性,只需重新定义更改的值:
var newRedmond = Redmond with {ZipCode = "980520"};
基于价值的平等
新程序员的一个陷阱是平等的概念。给定以下代码,输出是什么?
City Redmond_01 = new City { Name = "Redmond", ZipCode = 98052 };City Redmond_02 = new City { Name = "Redmond", ZipCode = 98052 };if (Redmond_01 == Redmond_02) Console.WriteLine("Equals!");else Console.WriteLine("Not equals!");
输出将是Not equals,因为即使值相同,它们也不是相同的对象。要实现我们在非编程术语中所称的相等,您必须重写Equals方法并比较各个属性:
class Program { static void Main(string[] args) { City Redmond_01 = new City{ Name = "Redmond", ZipCode = 98052 }; City Redmond_02 = new City{ Name = "Redmond", ZipCode = 98052 };
if (Redmond_01.Equals(Redmond_02)) Console.WriteLine("City Equals!"); else Console.WriteLine("City Not equals!"); }}
public class City { public int ZipCode{get; set;} public string Name{get; set;}
public override bool Equals(object obj) { //Check for null and compare run-time types. if ((obj == null) || !this.GetType().Equals(obj.GetType())) { return false; } else { City c = (City)obj; return (ZipCode == c.ZipCode) && (Name == c.Name); } } …}
这将使两个城市的产出相等。
在Records中,默认情况下暗示此行为,您不必编写自己的Equals方法来实现基于值的比较。代码中有if (Redmond_01.Equals(Redmond_02))应该像前面的代码片段一样工作,没有额外的public override bool Equals(object obj)部分。
如果需要,您仍然可以覆盖Equals,但对于需要基本相等性检查的情况,使用内置功能更容易。
数据成员
对于记录,您通常希望属性是公共的,其目的是首选仅初始化值设置。这也是 C#9 的假设,因此可以进一步简化。
考虑下面的代码:
public data class City { public int ZipCode {get; init;} public string Name {get; init;}}
可以这样写:
public data class City {int ZipCode; string Name;}
通过显式添加修饰符,仍然可以使数据成员私有。
位置记录
以下行代码明确设置属性:
City Redmond = new City{ Name = "Redmond", ZipCode = 98052 };
了解属性的定义顺序后,可以将其简化为以下内容:
City Redmond = new City(98052, "Redmond");
仍然存在使用额外代码的有效用例,以使代码的意图更加清晰,因此请谨慎使用。
继承和记录
在进行平等性检查时,继承可能很棘手,因此 C#在后台有一点魔力。让我们添加一个新类:
public data class City {int ZipCode; string Name;}public data class CityState : City {string State;}
由于有一个隐藏的虚拟方法处理对象的克隆,以下代码是有效的:
City Redmond_01 = new CityState{Name = "Redmond", ZipCode = 98052, State = "Washington" };City Redmond_02 = Redmond_01 with {State = "WA"};
如果要比较这两个对象的基于值的相等性,该怎么办?
City Redmond_01 = new City { Name = "Redmond", ZipCode = 98052 };City Redmond_02 = new CityState { Name = "Redmond", ZipCode = 98052, State = "WA" };
这些是平等的吗?Redmond_02具有Redmond_01的所有属性,但Redmond_01缺少属性,因此取决于您的视角。
有一个名为EqualityContract的虚拟受保护属性,它在派生记录中被重写。若要相等,两个对象必须具有相同的EqualityContract属性。
改进的目标类型
当可以从所使用的上下文中获取表达式的类型时,使用术语目标类型。
例如,当编译器有足够的信息来推断正确的类型时,您可以使用var关键字:
var foo = 1 //Same as int foo = 1 var bar = "1" //Same as string bar = "1"
目标类型的新表达式
当使用new实例化新对象时,必须指定类型。如果(编译器)清楚指定给哪种类型,您现在可以省略此:
//Old City Redmond = new City(98052,"Redmond");
//New City Redmond = new (98052, "Redmond");
//Not valid var Redmond = new (98052,"Redmond");
参数空值检查
检查参数是否为空值(如果这会导致错误)是方法的一种常见模式。您可以在执行操作之前检查该值是否为 null,也可以抛出错误。通过空检查,可以将此部分作为方法签名:
//Old – nothing happens if name is null void Greeter(string name){
if (name != null) Console.WriteLine($"Hello {name}");}
//Old – exception thrown if name is null void Greeter(string name){
if (name is null) throw new ArgumentNullException(nameof(name)); else Console.WriteLine($"Hello {name}");}
//New void Greeter(string name!){ Console.WriteLine($"Hello {name}");}
对于接受多个参数的方法,这应该是一个值得欢迎的改进。
模式匹配
C#7 引入了一种称为模式匹配的特性。此功能用于规避这样一个事实,即您不必控制自己代码中内部使用的所有数据结构。您可能会引入不符合对象层次结构的外部库,并重新安排层次结构以与之对齐,这只会带来其他问题。
为了实现这一点,您使用了一个switch表达式,它类似于switch语句,但是切换是基于类型模式而不是值来完成的。
C#9 通过提供更多可用于匹配的模式对此进行了改进。
删除/更改的功能
尝试新功能总是很有趣的,但也有一些功能和技术已经从.NET 中删除。
在推出新的主要版本时,打扫房间是很常见的,并且有许多小的变化。微软在上维护了一份突破性变化列表(在.NET 5 中)https://docs.microsoft.com/en-us/dotnet/core/compatibility/3.1-5.0 。
如本章前面所述,.NET Core 1.0 与.NET Classic 相比功能不完整。NETCore2 添加了很多 API,.NETCore3 添加了更多的.NET 框架。转换现在已经完成,因此如果您依赖.NET Classic 的一项功能,而该功能在.NET 5 中找不到,那么以后将不会添加该功能。
Windows 通信框架
Web 服务已经存在多年了,早期的.NET 框架之一就是Windows 通信框架(WCF)。WCF 有时可能很难使用,但它在 VisualStudio 中提供了数据交换合同和方便的代码生成实用程序。这在.NET Core 3 中已被弃用,因此如果您想要保留这些服务中的任何一个,则无法将它们移植到.NET 5。这适用于服务器端和客户端。
可以在.NET Core 中手动创建客户机实现,但这并不简单,不推荐使用。推荐的替代方案是转移到另一个称为 gRPC 的框架。这是一个开源远程过程调用(RPC)系统。gRPC 由谷歌开发,支持更现代的协议,如传输层的 HTTP/2,以及通过称为 ProtoBuf 的格式签订的合同。
网络表单
Windows 窗体是创建“经典”Windows 桌面应用的框架(经典是 Windows 8 之前的设计语言)。这是用.NETCore3.0 移植的。
这种方法的 web 版本称为 web 表单。也就是说,从技术上讲,代码之间存在差异,但采用所谓“代码隐藏”方法的模型在两者之间是相似的。建议在较新版本的.NETClassic 中也使用 MVC 和 Razor 风格的语法,但仍然支持 Web 表单。这还没有被带到.NETCore 中,您需要研究 MVC 或 Blazor 作为替代方案。
在介绍了新内容和新内容之后,我们现在将更仔细地研究向全世界展示 web 应用的组件。
了解网站和网络服务器
Web 服务器是 ASP.NET 应用的重要组成部分,因为根据定义,它们需要有一个在场才能运行。它也是 web 应用“它在我的机器上工作”挑战的主要贡献者(它在你的机器上工作,但不适用于你的客户)。
.NET 的历史与作为互联网信息服务(IIS的 web 服务器紧密相连。IIS 比.NET 早几年发布,但在更高版本中添加了对.NET 的支持。要使 web 应用正常工作,需要安装一些外部部件,这些部件不是由开发人员编写的代码处理的。这包括域名映射、用于加密流量中数据的证书以及一系列其他内容。IIS 处理所有这些事情,甚至更多。不幸的是,这也意味着创建最佳配置可能需要比一般.NET 开发人员更多的服务器和网络主题知识。
IIS 设计为在服务器操作系统上运行,并且由于 Visual Studio 可以安装在 Windows server 上,因此完全可以设置生产级开发环境。Microsoft 还提供了一个名为 IIS Express 的简化版本,作为 Visual Studio 的一部分,它使您能够在不安装服务器操作系统的情况下测试 ASP.NET 应用。
IIS Express 可以完成开发人员测试 ASP.NET 应用所需的大部分工作,最重要的区别在于,它仅用于处理本地流量。如果您需要从与正在开发的设备不同的设备测试您的 web 应用,IIS Express 并不是为您设计的。
我们将介绍一些您应该了解的配置组件,以及用于对基于 web 的应用进行故障排除的实用程序和方法。
Web 服务器配置
虽然这本书以开发人员为目标,但在您需要与负责基础设施的人员进行对话时,有一些关于 web 服务器的内容值得您去理解。
在开发 web 应用时,必须能够读取流量,而为了使其更容易,通常要做的一件事就是通过普通 HTTP 运行应用,允许您“通过网络”检查流量。您不应该在生产环境中运行此应用。您应该获得 TLS/SSL 证书,并为生产启用 HTTPS,理想情况下,还应该设置本地开发环境以使用 HTTPS 使这两个环境具有可比性。VisualStudio 支持自动生成可信证书,您需要在初始设置中批准该证书一次,因此这应该很容易配置。
证书信托
证书由公钥基础设施(PKI)颁发,该基础设施以分层方式构建,通常至少有三层。要使证书有效,客户端设备需要能够验证此链。这是在多个级别上完成的:
- 根证书颁发机构(CA可信吗?这必须安装在设备上。通常,这是预配置了公共 CA 的操作系统的一部分。
- 证书是否颁发给您的站点所在的域?如果您有
northwind.com的证书,如果您的站点运行在contoso.com上,则此项操作将不起作用。 - 证书将过期,因此如果您的证书在 2020 年过期,它将无法在 2021 年验证。
作为一名开发人员,您没有简单的方法来确保访问您站点的用户在其设备上正确配置了时钟,但至少要确保服务器设置正确。
会话粘性
Web 应用可以是有状态的,也可以是无状态的。如果它们是有状态的,这意味着在客户端和服务器之间有一种对话,其中下一段通信取决于先前的请求或响应。如果它们是无状态的,服务器将响应每个请求,就像这是双方第一次通信一样。(您可以在请求中嵌入 ID,以跨无状态会话维护状态。)
一般来说,您应该努力使会话成为无状态的,但有时您无法避免这种情况。假设您拥有以下记录类:
public data class City {int ZipCode; string Name;}
您还花时间创建了每个州的前 10 个(按人口)城市的列表,并通过 API 将其公开。API 支持查找单个邮政编码或名称,但它也有一个检索所有记录的方法。这不是一个大的数据集,但您进行了一些计算,并发现一次只应发送 100 条记录,以避免超出 HTTP 数据包大小限制的任何限制。
有多种方法可以解决这个问题。您可以在文档中写入客户机应附加开始和结束记录(如果省略,则假定结束为开始+99):
https://contoso.com/Cities?start=x&end=y
您还可以通过计算返回给客户端的nextCollectionId参数使其更高级,这样他们就可以在不重新计算开始和结束的情况下循环多次调用:
https://contoso.com/Cities?nextCollectionId=x
然而,这里有一个潜在的问题发生在服务器级别,您需要注意。
由于您的 API 很流行,因此需要添加第二个 web 服务器来处理负载并提供冗余。(这通常称为 web 场,如果需要,可以扩展到大量服务器。)要在这两个服务器之间分配流量,需要在它们前面放置负载平衡器。如果负载平衡器将第一个请求定向到第一个 web 服务器,将第二个请求定向到第二个服务器,会发生什么情况?
如果您没有任何逻辑使nextCollectionId对两台服务器都可用,它可能会失败。对于服务于数百万请求的复杂 API,您可能应该投入时间来实现一个解决方案,该解决方案将允许 web 服务器访问公共缓存。对于简单的应用,您需要的可能是会话粘性。这是负载平衡器上的一个常见设置,它将使特定客户机的请求粘附到特定的 web 服务器实例,并且您通常需要要求负责基础结构的人员启用它。这样,第二个请求将与第一个请求转到同一个 web 服务器,事情将按预期进行。
与 web 服务器通信故障排除
你最终会遇到这样的情况:你会问自己为什么事情不起作用,以及交通到底发生了什么。在一些用例中,您正在实现服务器,并且需要一种快速的方法来测试客户端,而无需实现客户端应用。这方面的一个有用工具是 Telerik 的 Fiddler,您可以在找到它 https://www.telerik.com/fiddler 。
这很可能在后面的章节中有用,所以您应该现在就开始安装它。默认情况下,它只捕获 HTTP 流量,所以您需要进入工具选项HTTPS并启用捕获 HTTPS 连接和解密 HTTPS 流量的复选标记,如图所示:

图 1.9–Fiddler HTTPS 捕获设置
将生成一个您需要接受安装的证书,然后您也应该能够监听加密通信。
这种方法在技术上被称为中间人攻击,也可以恶意使用。对于在您自己的开发过程中使用,这不是一个问题,但是对于生产故障排除,您应该使用其他机制来捕获您需要的信息。web 应用将能够截获它接收到的有效流量(即它拥有用于解码的证书),但通过在网络级别捕获的工具,您可能会收集到不应有的额外信息。
Fiddler 还可以用于手工制作 HTTP 请求,因此即使您没有追踪 bug,它也是一个有用的工具:

图 1.10–Fiddler HTTP 请求构造函数
如果这是一个错误,您可以通过单击网站来复制自己,VisualStudio 就是您的朋友。您有输出窗口,该窗口将提供过程级信息:

图 1.11–Visual Studio 输出窗口
故障排除通常很复杂,很少有趣,但在处理 web 应用时,直接查看协议级别是一项有用的技能,这些工具应该可以帮助您解决问题。
选择 web 服务器选项
如前所述,默认情况下,VisualStudio2019 中包含 IIS Express,如果您正在开发的代码打算在具有 IIS 完整版本的 windows 服务器上运行,则它是一个不错的选择。但是,IIS Express 也有一些缺点:
- 虽然需要的开销比完整的 IIS 要少,但它是“沉重的”,如果您发现自己正在运行调试周期,不断地启动和停止 web 服务器,那么这可能是一个缓慢的过程。
- IIS Express 是 Windows 唯一的工具。若您的代码在 Linux 上运行(这是一个在.NETCore 中具有跨平台支持的真实场景),那个么它就不能作为选项使用。
- 如果您正在为容器/微服务编写代码,那么当您有多个实例运行各自的 web 服务器时,完整的 IIS 会增加大量开销。(使用微服务,您通常不会在 web 服务器上同时定位多个网站,这就是 IIS 的设计目的。)
为了支持更多的场景,.NETCore 包括一个称为 Kestrel 的精简优化 web 服务器。回到我们在本章前面创建的Hello Worldweb 应用,您可以打开根文件夹的命令行并执行命令dotnet run:

图 1.12–dotnet 运行的输出
如果将浏览器打开至https://localhost:5001,则应与从 Visual Studio 启动 IIS Express 相同。
你不必进入命令行就可以使用 Kestrel。您可以在 Visual Studio 中定义多个配置文件——默认情况下都会添加。通过使用 WSL2 安装名为.NET Core 调试的 Visual Studio 扩展,您还可以直接部署到 Linux 安装。(Linux 配置将在第 2 章、跨平台设置中介绍)您可以通过打开launchSettings.json手动编辑设置:
{ "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:65476", "sslPort": 44372 } }, "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "Chapter_01_HelloWorld": { "commandName": "Project", "launchBrowser": true, "applicationUrl": "https://localhost:5001; http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "WSL 2": { "commandName": "WSL2", "launchBrowser": true, "launchUrl": "https://localhost:5001", "environmentVariables": { "ASPNETCORE_URLS": "https://localhost:5001;http://localhost:5000", "ASPNETCORE_ENVIRONMENT": "Development" } } }}
此文件仅用于您机器上的开发目的,而不是用于生产的配置。
对于生产使用,Kestrel 和 IIS 是主要的选择。使用哪一个取决于部署到的位置和目标。对于拥有 Windows 服务器的本地场景,部署到 IIS 仍然是一个可行的选择。它附带了现成的有用功能–例如,如果您希望将应用限制为已登录 Active Directory 的用户,则可以在 IIS 中启用此功能,而无需修改代码。(对于细粒度访问控制,您可能还需要代码中的一些机制。)
如果部署到容器中,Kestrel 是一条更容易的路径。但是,在没有生态系统的情况下,您不应该部署到 Kestrel。Kestrel“与代码一起生活”——当代码未运行时,没有可配置的管理界面。这意味着诸如管理证书之类的活动不是现成的。如果您部署到云环境,这通常意味着您将引入其他组件来覆盖 Kestrel 本身没有的组件。证书处理由容器主机或放置在 web 服务器前面的单独服务提供。
探索 Visual Studio 代码
NET 中的开发一直与 VisualStudio 相关联,其模式是随着 VisualStudio 的新版本而来的是.NET 的新版本。VisualStudio 仍然是开发人员的好伴侣,因为它经过多年的优化,可以为您提供所需的一切,从编写代码、改进代码到将其引入生产环境。
作为一个纯文本编辑器,它的光芒并不那么强烈。2015 年,微软决定通过发布 Visual Studio 代码来改善这一点。VS 代码提供语法高亮显示、文件的并排比较以及优秀编辑器应该具备的其他功能。提供了一个集成终端,因此如果您正在编写脚本,则不需要切换应用来执行脚本。此外,它还支持使您或其他开发人员能够扩展内置功能的扩展。例如,您可能打开了一个 JSON 文件,却发现它有一些断行和缩进——有一个名为美化 JSON的扩展名修复了这个问题。
VS 代码不限于编辑各种基于文本的文件。它具有内置的 Git 支持,可以配置调试器并连接到构建代码的实用程序,等等。它也不局限于.NET 生态系统——它可以用于 JavaScript、Go 和一系列其他语言的编程。事实上,在撰写本文时,它是跨语言和平台的最流行的堆栈溢出开发工具。
在 VS 代码中导航主要在 windows 的左侧完成:

图 1.13–Visual Studio 代码导航菜单
安装扩展时,列表中可能会出现更多图标。(并非所有扩展都有图标。)
在左下角,您还可以找到添加帐户的选项(例如,如果您使用的是利用 Azure 扩展的 Azure 帐户)。有关 Visual Studio 帐户图标,请参见图 1.14。

图 1.14–Visual Studio 帐户
在中至右下方窗格中,您可以启用一些控制台窗口:

图 1.15–Visual Studio 输出选项卡
请注意,您可能需要第一次通过菜单(查看****输出/调试控制台/终端/问题启用这些功能。通过这些,您可以轻松访问应用的运行输出、用于运行命令行操作的终端等。这些文件的相关性取决于您正在编辑的文件类型–对于 JSON 文件之类的文件,调试控制台选项卡不会提供任何功能。
在本书的上下文中,您需要安装 C#扩展:

图 1.16–Visual Studio 代码的 C#扩展
这是微软提供的一个扩展,它使 VS 代码能够理解 C#代码和相关工件,如.NET 项目文件。
如果您使用 Git 存储库,还应该查看名为 GitLens 的第三方扩展,它具有跟踪代码更改的有用功能。
利用 Windows 终端
在 MS-DOS 的计算时代,一切都围绕着命令行,直到今天,大多数高级用户都必须不时打开 cmd 窗口。问题是,到目前为止,在 Windows 中,这并不总是一种很好的体验。在 Build 2020 期间,微软发布了 1.0 版的Windows 终端。虽然您可以完全不用它来完成大部分编程,但我们建议您安装它,因为它有许多优点,我们将在本书后面向您展示。
Windows 终端支持多个选项卡,不仅支持“经典”cmd,还支持 PowerShell、Azure Cloud Shell 和适用于 Linux 的(WSL的Windows 子系统):

图 1.17–Windows 终端
Azure Cloud Shell 提供了 Azure 的命令行界面实例,即托管在 Azure 中的 Azure CLI。这意味着,与在本地安装 Azure CLI 并使其保持最新不同,您将始终准备好最新版本。您需要一个 Azure 订阅才能使其正常工作,但对于充当包含可执行文件的容器的本地磁盘的存储来说,除了几美分之外,它没有其他成本。
WSL 将在下一章中更详细地介绍,但其简短版本是它为您提供了 Windows 中的 Linux。这是 Linux Shell(不是图形用户界面),因此它也适合 Windows 终端体验。
不管您运行的是哪种类型的终端,它们都有许多可配置的选项,这使它们对程序员特别有用。您可以选择比 Word 文档更适合编程的字体。您可以安装所谓的 glyph,例如,直接在提示符上显示关于您在哪个 Git 分支上的信息。本书不要求您使用 Git,因为它旨在管理和跟踪您的代码,但即使不知道详细的命令,也很容易入门,因此强烈建议您尝试使用它。在当今的大多数开发环境中,它实际上是源代码管理技术。Microsoft 在 Azure DevOps 和 GitHub 中都提供了对 Git 的支持,但也有其他提供商,而且它不是特定于 Microsoft development 或.NET 的。
最终结果可能如下所示:

图 1.18–启用 Git 支持的 Windows 终端
它可以从 Windows 应用商店下载,也可以直接从 GitHub 下载,但是如果您想要自动更新,应用商店会更好。
扩展的 Git 信息需要一些额外的步骤,您可以在找到这些步骤 https://docs.microsoft.com/en-us/windows/terminal/tutorials/powerline-setup 。
总结
我们从一堂历史课开始,让您了解.NET Core 的来源,让您能够与经验丰富的.NET 开发人员共享上下文,并对.NET 环境有一个共同的理解。这是一段漫长的旅程,偶尔会有旁敲侧击和奇怪的混乱命名。这一部分的结尾展示了事情是如何简化的,以及微软如何仍在努力使.NET 的故事更易于开发人员理解,无论是大三还是大四的开发人员。
我们还使用了一个基本的 web 应用来更新你的 C#技能。重点主要是展示构成 MVC 模式的 web 应用的不同组件,而不是深入研究一般编程技能。如果您在这一部分遇到了困难,那么在返回本书之前,您可能需要先阅读一篇关于 C 语言的教程。
在学习.NET Core 框架和 C#9 版的新功能时,我们引入了一系列新功能。这是一个高级视图,介绍了将在后面章节中更详细介绍的功能。
因为这本书是关于创建 web 应用的,所以我们介绍了一些特定于 web 服务器的细节,以提供在本书后面和现实生活中都有用的背景知识。
本章最后展示了一些推荐用于编程工具带的工具和实用程序。记住,你带的工具越多,你的职业机会就越多!
在下一章中,我们将介绍.NET5 的跨平台故事。这包括在 Linux 和 macOS 上开始使用.NET,以及解释有关跨平台支持的一些概念。
问题
- 为什么引入.NETCore?
- .NET Core 的可支持性战略是什么?
- 你能解释一下 MVC 模式吗?
- 什么是仅限 init 的属性?
- 你能在.NET5 中使用 WCF 服务吗?
进一步阅读
- 由 Gaurav Aroraa 和 Jeffrey Chilberto 创作的具有 C#和.NET Core 的动手设计模式,来自 Packt Publishing,可在上获得 https://www.packtpub.com/application-development/hands-design-patterns-c-and-net-core
- C#编程:考试 70-483(MCSD)指南由 Simaranjit Singh Bhalla,Srinivas Madhav Gorthi 编写,来自 Packt 出版社,可在上获得 https://www.packtpub.com/application-development/programming-c-exam-70-483-mcsd-guide
二、跨平台设置
微软在推出.NET Core 时提到的一个主要改进是可以在 Windows 以外的平台上运行.NET 代码。随着每一次迭代,跨平台的故事都得到了改进,除了确保代码可以在其他操作系统上运行之外,在使 Linux 能够在 Windows 上运行方面也有了很大的改进。在运行 web 应用的环境中,Linux 是一个很好的主机操作系统,在本章中,我们将介绍如何跨平台开始使用.NET。您将学习如何利用.NET framework,以及如何在 Windows 计算机以及 Linux 和 macOS 上进行设置和启动。我们还将了解如何在 Windows 上对各种 Linux 场景进行故障排除,包括针对 Linux 版本 2(WSL2)的 Windows 子系统。在本章结束时,您将为跨平台开发做好系统准备。
我们将讨论以下主题:
- 利用.NET 框架
- Windows、Linux 和 macOS 上的入门
- 使用 Visual Studio 2019 在 Windows 上调试 Linux
技术要求
本章是关于在不同操作系统上运行代码的,因此,如果您想测试所有选项,您将需要几个设备:
- Windows 和 Linux 的代码将在 Windows 计算机上运行。
- macOS 的代码需要一个 Mac 系统。
- 如果使用 Fusion/Parallels 或 Bootcamp,Windows 代码可以在 Mac 上运行。
除设备外,您还需要以下设备:
- Visual Studio 代码,可用于 Windows、Linux 和 macOS
- Visual Studio 2019,可用于 Windows 和 macOS
请访问以下链接查看 CiA 视频:https://bit.ly/3qDiqYY
查看本章的源代码:https://github.com/PacktPublishing/ASP.NET-Core-5-for-Beginners/tree/master/Chapter%2002
利用.NET 框架
从一些琐事开始,曾经有一段时间微软在其他操作系统上玩得很好。在开发 Windows 3.0 时,微软与 IBM 合作开发了一个名为 OS/2 的操作系统。Windows 运行在 MS-DOS 的之上,所以从技术上讲,它不像今天这样是一个操作系统。相比之下,OS/2 是一个完整的操作系统,不需要先通过 DOS。OS/2 的妙处在于它包含了来自 Windows 的二进制文件,因此它能够在非 MS 操作系统上运行 Windows 应用。不仅如此,由于 OS/2 拥有不同的操作模式和更先进的(当时)内存管理,它能够比 Windows 本身更好地运行 Windows 应用。当应用崩溃时,不是整个计算机都被锁定,而是在继续进行之前终止了应用。
这一伙伴关系在文化和技术上并非没有问题。这两家公司有分歧,所以没有持续下去。微软继续为专业市场构建 Windows NT 平台,为消费市场构建 Windows 95 平台,OS/2 也自行消亡。从那时起,微软就不再是一个与“跨平台”一词用在同一句话中的名字了,它的每一点努力都投入到了构建 Windows 生态系统中。
.NET 从 Windows 诞生之初就不存在,多年来它也有自己的成长烦恼,我们在上一章中已经介绍过。
快进到现代,微软将非常高兴地告诉你 Linux 在他们的云计算平台上运行得有多好,并将为你提供使.NET 代码在 Linux 操作系统上运行所需的一切。这艘船花了 20 年的时间才得以扭转,但如今的道路肯定不同了。让我们先看看为什么我们应该跨平台,什么时候不应该跨平台。
为什么要跨平台?
当我们使用术语跨平台时,我们实际上可能指的是不同的东西。
您拥有开发.NET 应用所需的.NET 5 SDK。这在 macOS 上起作用的事实意味着开发人员不需要 Windows 计算机来开发 Windows 软件,而且由于 MacBook 在科技界很受欢迎,这就拓宽了.NET 的潜在开发受众。
您还拥有运行.NET 应用所需的.NET 运行时。这在 Linux 上工作的事实意味着您不必在 Windows 上运行应用,对于服务器来说,这是一件大事。由于经典的 Windows 服务器具有运行 Internet 信息服务的 UI,仅操作系统就占用了数千兆字节的空间。一个精简的 Linux 安装,加上一个命令行,可能只有 50 兆字节。如果你想运行云本地应用,这是一个重大的胜利。
为什么不跨平台?
OS/2 是一个有趣的实验,但即使合作关系保持友好,从长远来看,启用这种跨平台解决方案可能会变得复杂。我们在第一章中解释了托管代码和本机代码之间的区别,IBM 使用的方法基本上是引入 Windows 来提供本机功能。NET 当时还没有发明,其他框架也不一定具有强大的跨平台功能。随着时间的推移,这将不是一种可持续的方法。想象一下,Windows 中的一个安全缺陷需要 IBM 更新其操作系统,并通过广泛的测试和验证来保持兼容性。
这个解释的简短版本是:如果依赖本机/非托管代码,跨平台可能会很痛苦。
某些应用仍然需要本机代码和非托管代码,因此跨平台可能不是这些情况下的最佳选择。例如,在 iPhone 的早期,没有手电筒应用,但一些聪明的人发现他们可以与相机互动,并将手电筒用作手电筒。这是在 Xamarin 成为一个可行的选项之前,但这很可能超出了实现它的.NET 托管代码的范围。
如果您想从运行代码的设备中挤出最后一个 CPU 周期,那么收集的内存对象(即垃圾)可能会将您丢弃,因为您无法可靠地预测它们。如果您可以自己处理管理内存的开销,那么您可能希望使用较低级别的语言进行全面优化。一个传统的例子是游戏,早期的 3D 游戏都有用汇编代码编写的关键部分,以及使用库时无法控制的数学运算的算法调整。另一方面,这不仅仅影响跨平台;对于代码中的某些指令,开发人员还必须说明机器运行哪一代 CPU。
跨平台编码与单平台编码相结合
你可能会认为,如果你必须跟踪实际的硬件而不依赖于库,那么编写整个游戏听起来很难。这是正确的。这很难,大多数开发人员使用多种语言组合来创建他们的游戏,因为不太重要的部分当然可以用更友好的开发人员语言实现。
这就引出了这样一个问题:是否也可以用.NET 实现这一点。答案是肯定的,这是通过一个名为平台调用服务或简称P/Invoke的功能实现的。这是一种转义托管.NET 运行时的机制。您可以调用 API 和服务,这些 API 和服务是通过平台本机接口或以.NET 系列以外的语言实现的组件公开的。例如,您可以调用为.NET 不支持的特定硬件编写的驱动程序。
虽然 Microsoft 可以确保.NET 运行时跨平台运行,但当您走出.NET 生态系统时,无法保证这一点。因此,您可能有一个混合了跨平台和单一平台的.NET 应用。可以制定处理这一问题的策略,但这一跨平台实现级别超出了本书的范围。然而,我们将在 Blazor 的覆盖范围内探索一个类似的概念,在这里,您可以执行所谓的 JavaScript 互操作,从而超越.NET 提供的功能。
.NET 跨平台可用性
那么,当我们说跨平台时,我们是指每个平台吗?不,不是真的,但是有很多选择:
- Windows x86/x64/ARM:ARM 在 OEM 中并不广泛可用,但 Microsoft 拥有在 ARM 上运行 Windows 的 Surface Pro X 设备。请注意,并非所有常规 Windows 应用都可在此平台上使用,因此即使有仿真选项,您的里程数也可能有所不同。
- 马科斯
- Linux
- iOS(通过 Xamarin)
- Android(通过 Xamarin)
请注意,尽管 macOS 适合于开发.NET web 应用,但它并不是为其他环境运行应用的一个真正选项,即使从技术上讲,没有什么可以阻止您。Web 应用本质上意味着有一个运行后端代码的服务器。苹果没有为服务器用例提供硬件,他们的设备设计成客户端。
基于 ARM 的 mac
苹果公司宣布,他们将使用自己设计的 CPU,而不是英特尔的 CPU。此体系结构与当前版本的.NET for macOS 不兼容。NET 不需要 Intel CPU 或特定的 CPU 体系结构(如 ARM 体系结构的 Windows 所示),但运行时仍需要更新。
在写这本书的时候,还不知道苹果对未来的设备有什么计划,也不知道微软将采取什么措施来确保.NET 在这些设备上运行。在本书中,我们使用了基于 Intel 的 Mac 设备,目前无法推测未来会发生什么。
什么跨平台不适合你
NET 支持跨平台的事实并不意味着您必须实现一个可以在所有操作系统上运行的应用。如果您想使用 Windows 开发一个只在 Linux 上工作的应用,这是可以的。但是,您应该知道,跨平台支持并不能保证您编写的所有代码都能跨所有平台工作。
例如,如果您的应用将文本保存到文件系统,您可能会尝试将文本文件写入c:\WebApp\HelloWorld.txt。这种类型的文件引用是 Windows 操作系统工件。编写此代码时不会出现警告,.NET 也不会阻止其编译。只要应用在 Windows 上运行,一切都应该是好的。
但是,如果应用在 Linux 上运行,则会出现运行时异常,因为 Linux 不理解这种类型的文件系统。Linux 希望您将该文件引用为/mnt/c/webapp/HelloWorld.txt。(不同的发行版对实际的文件层次结构有不同的约定。)如果你有良好的异常处理,应用可能会优雅地绕过这个问题,但如果没有,它将停止运行,并给你留下糟糕的跨平台体验。
在本章的后面,在我们介绍了在多平台上运行的基本知识之后,我们将重新讨论如何应对这些挑战。
在 Windows、Linux 和 macOS 上入门
跨平台之旅的第一步是让基础工作在我们提到的 Windows、Linux 和 macOS 平台上运行。我们将在以下部分中介绍这一点,以确保您在多平台故事的这一部分中走上正轨。
窗户
在上一章中,我们在开始使用 Windows 上的.NET 5 时提到了,因此,如果您遵循该指南,您应该已经有了该平台的功能设置。因此,我们在此不再重复这些说明。
Linux
Linux 是一种流行的服务器工作负载操作系统,它为大量在 Azure 中运行的虚拟机提供了动力。对于桌面上的普通终端用户来说,Linux 不像 Windows 那样受欢迎,但是对于开发人员来说,使用 Linux 有很多好处。
在开发在容器中运行的微服务时,Linux 是一个不错的选择,因为在许多情况下,您将能够运行精简的映像。容器不是本章的主题,您可以期待第 8 章、容器,但 Linux 是.NET 跨平台故事的一部分,即使没有容器。
*您可以直接在计算机上安装 Linux,也可以安装.NET 开发所需的一切,但这里我们将向您展示如何通过 Linux 的 Windows 子系统在 Linux 上使用 Windows 进行开发。
Linux Windows 子系统(WSL)
Linux 非常适合开发,因为程序员所需的许多工具都是操作系统的一部分。但是,对于涉及 Outlook 和 Word 等应用的一般办公用途,Windows 通常是大多数人的更好选择。当然,如果你能同时拥有 Linux 和 Windows,那就太好了。
很长一段时间以来,Windows 一直以不同的形式支持虚拟化,而且由于 Linux 与 Windows 在同一硬件上运行(并且在许多情况下是免费的),因此,如果需要,使用 Linux 运行虚拟机是一种常见的选择。然而,虚拟机的意义在于拥有与主机分离的东西。因此,即使是很小的事情,比如文件进出 Linux 虚拟机,也不是很顺利。
2016 年,微软推出了针对 Linux 的Windows 子系统(WSL),让 Linux 操作系统更接近于 Windows 的一部分,您可以在 Windows 10 中安装所选发行版的特殊版本。WSL2 进一步改进了这一点,它是在 Windows 10 2004 中引入的,在 Windows 10 2004 中,Linux 可以成为 Windows 的一个集成部分。(Windows 10 的当前版本名为 20204,表示该版本首次发布于 2020 年,第四个月为 4 月。)
让我们先安装 WSL2,然后再继续在 Linux 上运行代码。
注意这是自 2020 年 5 月版本的 Windows 10 开始的安装过程。在未来的版本中,情况可能会发生变化。
您的计算机需要能够运行 Hyper-V 和 Windows 10 2004(或更高版本)。大多数现代计算机将能够运行 Hyper-V,但如果您的开发人员机器是虚拟化的,那么启用 WSL2 可能会出现问题。
要安装 WSL2,请执行以下步骤:
-
以管理员身份打开命令提示符。
-
运行以下命令安装 WSL:
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart -
使用以下命令启用虚拟机平台:
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart -
重新启动计算机。
-
从下载最新的 WSL2 内核 https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi 。
-
Run the installer, as shown in Figure 2.1:
![]()
图 2.1–WSL2 内核安装程序
-
使用以下命令将 WSL2 设置为默认版本:
wsl –-set-default-version 2 -
Download a Linux distribution from the Microsoft Store. For this book, we have used Ubuntu 20.04 LTS (see Figure 2.2):
![]()
图 2.2–Microsoft 应用商店中的 Ubuntu 20.04 LTS
-
Click Launch to start Linux for the first time:
![]()
图 2.3–设置 Linux 的用户名和密码
-
为 Linux 安装定义用户名和密码(请参见图 2.3。(这与您的 Windows 凭据无关,可能会有所不同。)您现在应该在一个普通的 Linux shell 中找到自己。
-
Since this operating system lives its own life, it is suggested to start by updating to the latest patches by running
sudo apt update && sudo apt upgrade, as shown in Figure 2.4:

图 2.4–更新 Linux 发行版
- 按Y继续,您应该可以出发了。
Windows 还应该自动配置与非 Linux 硬盘驱动器分区的集成。因此,如果打开 Windows 资源管理器,您应该会在那里找到 Tux(Linux 吉祥物):

图 2.5–Windows 资源管理器中的 Linux 集成
您还可以从 Windows 浏览 Linux 文件系统,并将文件复制到您的 Linux 分区和从您的 Linux 分区复制文件(请参见图 2.6:

图 2.6–Windows 资源管理器中的 Linux 文件系统
请注意,在后台,Linux 文件系统的处理方式与 Windows 文件系统不同,因此只将您打算在 Linux 内部运行的文件放在这些文件夹中,反之亦然。如果您有在 Linux 内部运行的应用,则不应将这些应用放在 Windows 分区中。这样做不会导致损坏,但性能可能会降低。
Ubuntu 安装程序会自动启动一个命令行,但如果您按照上一章中的说明安装 Windows 终端,Ubuntu 20.04 应该会自动添加。本书将在本章中继续使用 Windows 终端,但这两个选项都可以使用。
在 Linux 上安装.NET
我们建议您使用 APT 在 Ubuntu 上安装.NET:
-
运行以下命令添加 Microsoft 的存储库:
wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb sudo dpkg -i packages-microsoft-prod.deb -
安装 SDK:
sudo apt-get update; \sudo apt-get install -y apt-transport-https && \sudo apt-get update && \sudo apt-get install -y dotnet-sdk-5.0
笔记
在 Linux 上安装.NET 有几种不同的方法,随着时间的推移,情况可能会发生变化。如果您在安装.NET 时遇到问题,请在线查看上的说明 https://docs.microsoft.com/en-us/dotnet/core/install/linux-ubuntu 。
现在,创建和运行.NET 应用所需的一切都已就绪。是时候在实践中检验理论了:
-
创建新目录并更改为:
mkdir LinuxHelloWorld && cd LinuxHelloWorld -
在 WSL2 中运行的 Linux 还不支持图形用户界面,因此我们需要通过非图形实用程序进行编辑:
sudo vi View/Home/Index.cshtml -
Vi 不是完全直观,但按插入并编辑代码,如下所示:
@{ ViewData["Title"] = "LinuxHelloWorld";}<div class="text-center"> <h1 class="display-4">Running on @Environment. OSVersion</h1></div> -
要保存并退出,请按Esc,然后按:wq,然后点击进入。
-
Test the app with
sudo dotnet run. You should see the output indicate that it is running. See Figure 2.7:![]()
图 2.7–在 Linux 上运行 dotnet
-
You can test this with some more cross-platform magic. You do not have a browser running on this Ubuntu. You most likely do have one in Windows 10, so you can open that and browse to
https://locahost:5001. See Figure 2.8 for an example of browsing a web app that's running on Linux:![]()
图 2.8–浏览 Linux 上运行的 web 应用
-
返回 Linux shell,用Ctrl+C终止正在运行的应用。
我们看到实用程序足以对代码进行小的编辑,但并不是每个人都想把 Vi 作为编写 C#代码的编辑器。
六“退出战略”
如果您是 Vi 新手,它可能会让人困惑,因为它的工作方式与您在 Windows 世界中可能习惯的大多数文本编辑器不同。您可能最终不确定您实际编辑了什么,或者如何更正它。退出策略(如果您觉得犯了错误)是在不保存更改的情况下退出 Vi。按Esc键,按:(冒号)(您应该看到它出现在左下角),然后输入q!(包括感叹号),后接回车。然后,您可以重新尝试使用干净的板岩进行编辑。
幸运的是,这里还有另一个选择。在上一章中,我们向您展示了 VisualStudio 代码是多么有用,因此,如果您还没有安装它,请这样做。我们将逐步介绍如何使用 Visual Studio 代码(VS 代码)作为 Linux 上代码的编辑器:
-
打开 Visual Studio 代码(在 Windows 10 中)。
-
Install the Remote – WSL extension from within VS Code. See Figure 2.9:
![]()
图 2.9–Visual Studio 代码远程 WSL 扩展
-
返回 WSL 中的 Linux shell 并键入
code .(包括标点符号)。 -
After an initial bit of setup work, Visual Studio Code will load in Windows 10. You will observe that there's an indicator in the lower-left corner referring to WSL. See Figure 2.10:
![]()
图 2.10–连接到 WSL 的 Visual Studio 代码
-
If you have the C# Extension installed in VS Code, you can go to the debug pane (at the bottom). See Figure 2.11:
![]()
图 2.11–.NET Linux 调试选项卡
-
单击绿色小箭头启动调试器。当事情完成构建后,您应该看到与之前相同的输出,并且
LinuxHelloWorld应用在浏览器中运行。(VS 代码为您启动浏览器。)如果您查看终端窗口,您将看到应用在 WSL 中启动。参见图 2.12:

图 2.12–Visual Studio 代码终端输出
此会话与您在 Windows 终端 shell 中运行的会话是独立的,因此如果您愿意,可以在那里并行工作。
现在您可以在 Windows 中开发代码,该代码在 Windows 上运行的 Linux 上执行。这可能需要一点时间来理解,但本节的要点是 Linux 的跨平台故事非常强大。
如果你有一个苹果设备(运行 macOS),那么你现在就可以把它拿出来。接下来,我们来看一下.NET 的 mac 故事。
马科斯
有两种主要工具可用于在 Mac 上开发.NET 应用。您可以使用 Visual Studio for Mac 或 Visual Studio 代码。我们首先来看一下如何使用 VisualStudio 代码(VS 代码)。您可以从下载 https://code.visualstudio.com/ 。
安装 Visual Studio 代码后,我们建议您使其可以从 shell 访问,以便可以从终端启动它。
要使 VS 代码可访问,请执行以下步骤:
-
启动 VisualStudio 代码。
-
Open the command palette (Shift+cmd+P) and type
shell command, as shown in Figure 2.13:![]()
图 2.13–shell 命令安装程序
-
You will also want to make sure the C# extension is installed for VS Code. See Figure 2.14:
![]()
图 2.14–Visual Studio C#扩展
完成后,您可以转到安装.NEThttps://dotnet.microsoft.com/download?initial-os=macos。
-
Open the installer, and you will be greeted with a wizard for installing .NET. See Figure 2.15:
![]()
图 2.15–macOS 的.NET 安装程序
除非您想修改安装的存储位置,否则您可以通过选择下一个选项来点击安装位置。
-
To verify the .NET version on macOS, open the Terminal and run
dotnet –version. See Figure 2.16:![]()
图 2.16–在 macOS 上验证.NET 版本
-
You also need to generate certificates to run with HTTPS. This is done with the
sudo dotnet dev-certs https –-trustcommand, as shown in Figure 2.17:![]()
图 2.17–在 macOS 上生成和安装开发人员证书
-
创建一个文件夹(
mkdir webapp)并将其更改为(cd webapp)。 -
运行
dotnet new mvc生成一个简单的 web 应用。然后,运行code .在 Visual Studio 代码中打开它。 -
You might see a notification in the lower-right corner about missing assets. See Figure 2.18:
![]()
图 2.18–Visual Studio 代码中缺少的资产
您应该点击是添加资产。
-
VS Code shows the file structure on the left-hand side of the UI. See Figure 2.19:

图 2.19–Visual Studio Mac 代码中的文件结构
- 打开
Index.cshtml并对
```cs
@{ ViewData["Title"] = "LinuxHelloWorld";}<div class="text-center"> <h1 class="display-4">Running on @Environment. OSVersion</h1></div>
```
内容进行小编辑
- 要设置断点,请单击行号(6)旁边的。
- There is a separate debug section:

图 2.20–macOS 上的 Visual Studio 代码调试窗格
- 单击绿色小箭头启动程序。它应该启动浏览器,如下图所示:

图 2.21–浏览在 macOS 上运行的 web 应用
你会注意到它并没有说 Mac 或苹果,但对于初学者来说,你解决的主要问题是你设法让.NET 工作。这就完成了在 Mac 上安装 VS 代码。
如前所述,您还可以在 macOS 上安装更完整的 Visual Studio 版本。
Visual Studio 2019 for Mac
Visual Studio 代码是一种不错的体验。但是,Visual Studio 2019 在 macOS 上可用,因此您可能更喜欢它。
总的来说,它给人的感觉更像“Mac 风格”。(外观、感觉和交互都与 Mac 的整体体验相似。)文件层次结构在左窗格中,如图 2.22所示:

图 2.22–Visual Studio 2019 for Mac 文件层次结构
在 VisualStudio 的中间,主窗格与 Windows 对应的外观略有不同(参见图 2.23)

图 2.23–Visual Studio 2019 for Mac 主窗格
与 Windows 体验一样,VisualStudio2019(VS 2019)中的选项比 VisualStudio 代码更多。因此,对于 web 应用开发,主要取决于您喜欢哪种工具,VS2019 中的旋钮和刻度盘比 VS 代码中的多,而基本的功能在这两种工具中都存在。对于 VS2019,Visual Studio Community For Mac 是免费版本。
Visual Studio for Mac 最初基于 Xamarin Studio for Mac。如果您对苹果平台的移动开发感兴趣,那么使用完整版本的 VisualStudio 而不是 VisualStudio 代码可能是更好的选择。我们将在本章后面的移动设备跨平台一节中再次讨论此主题。
跨平台和 Docker 上的一个词
Docker 是当今的热门话题,将在第 9 章、Docker中详细介绍。然而,我们应该解释 Docker 和跨平台之间的关系。
前面的部分向我们展示了直接在平台上运行代码。Linux 版本在 Ubuntu 上运行,macOS 版本在 Macbook 上运行。对于更高级的用例,您可能希望将代码容器化,但这并不意味着您可以自由地混合和匹配这些技术。
容器可与虚拟机 lite相媲美,它取决于运行它的主机。这意味着 Linux 容器需要在 Linux 主机上运行。运行 Windows Server 2019 容器需要 Windows Server 2019 主机。这也适用于 Windows Server 版本—Windows Server 2016 主机将运行 Windows Server 2016 容器,并且不支持 Windows Server 2019 容器。Windows 10 上的 Linux 容器不包含在跨平台兼容性中。
然而,WSL2 可以作为 Linux 主机运行。因此,您可以在 WSL 之上运行 Linux 容器,并实现跨平台容器开发。我们将在第 9 章Docker中对此进行扩展。
有了合适的硬件,您可以将 Windows 与 Hyper-V 一起使用,并将 Linux 虚拟机作为 Linux 主机,在上面运行基于 Linux 的容器。
难怪您可能会对其中涉及的所有虚拟化层感到困惑。
让你的代码跨平台
在开始构建跨平台解决方案时,您需要确保 Hello World web 应用在多个 Windows 上运行。然而,享受这些平台带来的好处还有很多。让我们看一下 Microsoft 如何提供内置机制,以及您自己可以做些什么。
背景工作者服务
在 ASP.NET web 应用中,用户界面中发生的许多事情都是事件驱动的。例如,在上一章中,我们向您展示了 C#9 中的一些新特性,包括美国城市的示例类,它由名称和邮政编码组成。因此,如果将其扩展到 ASP.NET 应用,您可能会构建一个网页,其中包括一个用于输入邮政编码的文本框和一个用于查找相应城市名称的按钮。
邮政编码是相当固定的,不是每周都会改变的。但是,您可能仍然希望确保数据库是最新的,因此您可以选择与美国邮政服务的主数据库执行同步(例如)。这不会由最终用户在 UI 中单击来驱动,但会在后台自行发生。
.NET 有一个模板适用于此工作者,该模板生成一个控制台应用,您可以使用此类功能进行扩展。默认行为是打印当前日期时间,这对于我们的目的来说已经足够了,但是您可以自己使其更高级。
打开命令行,创建一个新目录,然后切换到此目录。完成后,执行以下步骤创建新解决方案:
-
运行
dotnet new worker。 -
运行
dotnet add package Microsoft.Extensions.Hosting.WindowsServices。 -
运行
dotnet add package Microsoft.Extensions.Hosting.Systemd。 -
Run
dotnet run:![]()
图 2.24–使用 dotnet 运行辅助服务
-
运行
code .以 Visual Studio 代码加载项目。
这很好地工作,但有一个缺少的部分。它目前作为控制台应用运行,这意味着它必须在控制台窗口中启动和运行。这不适合一个网站,因为它应该完全在后台完成。
在 Windows 中,这是通过将应用作为 Windows 服务安装完成的(请参见图 2.25:

图 2.25–Windows 服务
这听起来不像是跨平台的,这可能并不奇怪。
Linux 有一个类似的结构,称为systemd,因此是一个操作系统级别,您不会被阻止。在 Linux 中,服务是通过.NET 支持的 systemd 守护进程实现的。
WSL 与 systemd
请注意,在编写本书时,Linux 的 Windows 子系统不支持 systemd。这意味着为了在 Linux 上完全测试此代码,您需要本地运行的 Linux 虚拟机或 Azure 中运行的 Linux 虚拟机的实例。
换句话说,我们需要修改应用以支持两个操作系统概念。这听起来很复杂,但实际上相当简单。
回到 VisualStudio 代码,打开Program.cs并做一些小的更改,看起来像这样:
using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.Hosting;
namespace Chapter_02_Workers { public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); }
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .UseWindowsService() .UseSystemd() .ConfigureServices((hostContext, services) => { services.AddHostedService<Worker>(); }); }}
这里的两个重要部分是UseWindowsService和UseSystemd。NET 运行时能够理解它是在 Windows 还是 Linux 上执行,然后它将使用相应的版本。它将忽略另一个,因此您不需要使用额外的逻辑来确定使用哪一个。
运行前面的代码将产生与前面相同的输出,因此您不会立即注意到更改。重要的是要理解,尽管前面的代码将使代码跨平台,但它不会自动将自身安装为 Windows 服务或 systemd 守护程序。
要在开发人员计算机上安装 Windows 服务,请在命令行窗口中运行以下命令:
dotnet publish –configuration Releasesc create dotnetService binPath = c:\code\foo.exe(其中foo.exe为上一条命令生成的文件)sc start dotnetService
这将帮助您完成开发目的,但当将代码移动到本地开发人员计算机上未运行的其他环境时,它可能不起作用。在这些情况下,设置服务可能是一个更复杂的过程,因此,如果需要这样做,可以使用另一个配置过程。本章附录中有关于如何设置服务的说明。
对于 Linux,说明如下所示:
-
运行
sudo nano /etc/systemd/system/dotnetd.service创建服务。 -
确保内容与此类似:
[Unit] Description=.NET Chapter 02 systemd daemon [Service] WorkingDirectory=/var/www/dotnetd ExecStart=/usr/local/bin/dotnet /var/www/dotnetd/dotnetd.dll Restart=always # Restart service after 10 seconds if the dotnet service # crashes. RestartSec=10 KillSignal=SIGINT SyslogIdentifier=dotnet-daemon User=apache Environment=ASPNETCORE_ENVIRONMENT=Production [Install] WantedBy=multi-user.target -
启用服务:
sudo systemctl enable kestrel-dotnetd.service。 -
启动服务:
sudo systemctl start kestrel-dotnetd.service。 -
Verify that the service is running:
sudo systemctl status kestrel-dotnetd.service.输出与此类似:
kestrel-dotnetd.service - .NET Chapter 02 systemd daemon Loaded: loaded (/etc/systemd/system/kestrel-dotnetd.service; enabled) Active: active (running) since Thu 2020-10-18 04:09:35 CET; 35s ago Main PID: 9021 (dotnet) CGroup: /system.slice/kestrel-dotnetd.service └─9021 /usr/local/bin/dotnet /var/www/dotnetd/dotnetd.dll
这是一个很好的例子,说明了.NET 是如何帮助您的,但并不是所有的用例都能那么容易地解决。接下来,我们将介绍一个更详细的跨平台功能示例。
更复杂的跨平台示例
在某些情况下,您需要处理跨平台的问题,这些问题比.NET 能够自动处理的要多。我们已经提到 Linux 如何不理解c:\WebApp\HelloWorld.txt,所以让我们看一个稍微复杂一点的例子。
假设我们有一个网站,我们依赖于对文本字符串进行加密和/或签名。(这可能是更大的身份系统的一部分。)我们建议使用证书来实现这一点。我们希望这段代码能够在 Windows 和 Linux 上运行,并且大多数使用证书的方法都应该是完全跨平台兼容的。但是,Windows 和 Linux 在操作系统级别上使用证书的方式不同。更具体地说,它们以不同的方式生成和访问。我们将实施这两种选择。
要在 Windows 上生成证书,请执行以下步骤:
-
使用 PowerShell 选项卡打开 Windows 终端。
-
运行以下命令:
$cert = New-SelfSignedCertificate -Type Custom -Subject "CN=Chapter_2_Certificate" -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3") -KeyUsage DigitalSignature -KeyAlgorithm RSA -KeyLength 2048 -NotAfter (Get-Date).AddYears(2) -CertStoreLocation "Cert:\CurrentUser\My" $cert.Thumbprint -
记下指纹,因为我们需要它在代码中。参见图 2.26:

图 2.26–在 Windows 上生成证书
您还可以验证 Windows 10 中用户证书存储中是否存在该证书(请参见图 2.27。(可以通过在 Windows 中的搜索栏上开始键入certificate来找到它。):

图 2.27–Windows 10 中的用户证书存储
要在 Linux 上生成证书,请执行以下步骤:
-
使用 Ubuntu 20.04 选项卡打开 Windows 终端。
-
Run the following commands:
openssl req -x509 -newkey rsa:4096 -keyout myKey.pem -out cert.pem -days 365 -nodes openssl pkcs12 -export -out keyStore.p12 -inkey myKey.pem -in cert.pem openssl x509 -in cert.pem -noout -fingerprint生成证书时需要提供一些值,但出于本章的目的,这些值不需要与任何实际数据相一致。
提示时不要输入密码,只需按enter键设置空/空密码即可。
-
记下指纹,因为我们以后需要它。
您可能会注意到,在 Windows 和 Linux 中,指纹看起来不同。Windows 使用的格式是12AB…,而 Linux 则输出12:AB:…。这纯粹是视觉表现的问题。Linux 以更可读的格式打印,但实际的指纹格式并不不同。如果从 Linux 版本中删除冒号,您将看到字符数与 Windows 版本相同(如图 2.28所示):

图 2.28–在 Linux 上生成证书
有了 Windows 和 Ubuntu 的证书,我们将创建一个使用它的 web 应用。为了不使事情复杂化,此代码只加载证书并打印出指纹和通用名称,以验证代码能够读取(和使用)证书。创建使用证书的应用的步骤如下:
-
打开 Windows 终端并新建目录:
C:\Code\Book\Chapter_02_Certificates。 -
切换到目录并运行
dotnet new mvc。 -
运行
dotnet add package Microsoft.IdentityModel.Tokens。 -
使用
code .启动 Visual Studio 代码。 -
打开
HomeController.cs。 -
在顶部添加以下两行
using:using System.Security.Cryptography.X509Certificates;using Microsoft.IdentityModel.Tokens; -
Edit the controller to look like this (some parts are omitted for readability):
public class HomeController : Controller { private readonly ILogger<HomeController> _logger; private static Lazy<X509SigningCredentials> SigningCredentials; public HomeController(ILogger<HomeController> logger) { _logger = logger; } public IActionResult Index() { var SigningCertThumbprint = "WindowsThumbprint"; SigningCredentials = new Lazy<X509SigningCredentials>(() => { X509Store certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser); certStore.Open(OpenFlags.ReadOnly); X509Certificate2Collection certCollection = certStore.Certificates.Find( X509FindType.FindByThumbprint, SigningCertThumbprint, false); // Get the first cert with the thumbprint if (certCollection.Count > 0) { return new X509SigningCredentials(certCollection[0]); } throw new Exception("Certificate not found"); }); var myCert = SigningCredentials.Value; ViewBag.myCertThumbprint = myCert.Certificate.Thumbprint.ToString() ViewBag.myCertSubject = myCert.Certificate.SubjectName.Name.ToString();; return View(); }…这里重要的一点是,控制器尝试使用特定于访问 Windows 证书存储的.NET 库(与 Windows 10 和 Windows Server 兼容)。证书被加载到一个数组中。我们指定了一个只对一个证书唯一的指纹。如果您定义了不正确的指纹,或者由于某种原因,应用无法访问证书存储,则会抛出一个无法找到证书的错误。
如果找到证书,则读取值。指纹和主题名称属性存储在
ViewBag中,以便在视图中检索。 -
编辑
Index.cshtml文件,如下所示:@{ ViewData["Title"] = "Home Page";}<div class="text-center"> <h1 class="display-4">Certificate info</h1> <p>Certificate thumbprint: @ViewBag. myCertThumbprint</p> <p>Certificate subject: @ViewBag.myCertSubject</p></div> -
运行应用。您将看到证书信息,如图 2.29所示:

图 2.29–Windows 证书的输出
下一个逻辑步骤是切换到 Linux,执行dotnet run并刷新浏览器。遗憾的是,这会给您一个错误,如图 2.30所示:

图 2.30–在 Linux 上使用 Windows 证书时出错
失败的原因有两个:
- 我们没有改变指纹。
- 我们尝试通过 Windows 证书存储查找证书。
我们将解决这个问题,但首先需要在 Linux 中准备证书。当我们之前在 Linux 中生成证书时,我们在home目录中(如果您在不同的目录中,请在说明中相应地替换它)。
通过执行ls -l,我们看到有几个文件用于证书。参见图 2.31:

图 2.31–列出 Linux 中的证书文件
我们希望使我们的代码更友好,同时也便于部署。按照以下步骤重命名证书:
-
使用
mv keyStore.p12 LinuxThumbprint.p12重命名.p12文件。 -
使用
mv cert.pem LinuxThumbprint.pem重命名cert.pem文件。 -
这些文件应移动到更合适的位置。在本章中,这将是我们代码所在的目录:
mv LinuxThumbprint.p12/mnt/c/Code/Book/Chapter_02_Certificates/LinuxThumbprint.p12 mv LinuxThumbprint.cert /mnt/c/Code/Book/Chapter_02_Certificates/LinuxThumbprint.cert
这意味着我们的代码将能够轻松定位证书文件。
为部署到云中的应用集成证书
这里有一句忠告。只要我们在代码的生命周期内管理证书的生命周期,这种方法就可以工作。对于经常单独管理证书的云部署,它不是最好的解决方案。
Azure 建议将私人证书(.p12文件)存储在/var/ssl/private中,前提是您在 Azure app Services 中运行应用并将证书存储在 Azure 密钥库中。
现在证书已经就位,我们可以修复代码了。执行以下步骤:
-
返回 Visual Studio 代码(如果愿意,您仍然可以在 Windows 中编辑)并打开
HomeController.cs。 -
Change the code here:
var SigningCertThumbprint = "WindowsThumbprint";对下列事项:
var SigningCertThumbprint = "LinuxThumbprint"; -
注释掉当前证书加载:
/*SigningCredentials = new Lazy<X509SigningCredentials>(() =>… throw new Exception("Certificate not found");});*/ var myCert = SigningCredentials.Value; -
Insert the following code instead:
public IActionResult Index(){ /* Windows Certificate Loading */ var SigningCertThumbprint = "LinuxThumbprint"; var bytes = System.IO.File.ReadAllBytes($"{SigningCertThumbprint}. p12"); var cert = new X509Certificate2(bytes); SigningCredentials = new Lazy<X509SigningCredentials>(() => { if (cert != null) { return new X509SigningCredentials(cert); } throw new Exception("Certificate not found"); }); var myCert = SigningCredentials.Value;此代码的用途与 Windows 版本相同。它读取证书并将其中两个属性写入 ViewBag 以进行渲染。它与处理 Windows 的代码不同之处在于 Linux 没有证书存储。代码只是尝试定位文件并读取字节值。如果文件不存在,或者无法将内容转换为证书,则会抛出一个关于如何找不到证书的错误。
-
运行应用。
打开浏览器,您应该看到一个类似的视图,但其他值如以下屏幕截图所示:

图 2.32–Linux 证书的输出
如果你想拥有一个真正的跨平台应用,你可以多做一点,增加代码运行平台的检查。添加一些检查:
public IActionResult Index(){ //Windows if (Environment.OSVersion.Platform.ToString() == "Win32NT") { //Windows logic ... }
//Linux if (Environment.OSVersion.Platform.ToString() == "Unix") { //Linux logic ... }
var myCert = SigningCredentials.Value; ViewBag.myCertThumbprint = myCert.Certificate.Thumbprint.ToString(); ViewBag.myCertSubject = myCert.Certificate.SubjectName.Name.ToString();;
return View();}
这说明在构建跨平台应用时可能需要做一些额外的工作,而不仅仅是确保运行.NET5。然而,这是可能的,也可能是值得的。在这里显示的示例中,这意味着您可以让开发人员主要在 Windows 上工作,并且仍然在生产环境中部署到 Linux 主机上(前提是您测试这些边缘情况)。
独立的.NET 应用
本章到目前为止的讨论围绕着确保所有东西都能跨不同平台工作。然而,有时你没有这种需要,你可能想更具体地说明你将支持什么。
这可能适用的两个示例如下所示:
- 您创建了一个要部署在 Windows 服务器上的 web 应用。您无法控制这些服务器,并且拥有这些服务器的操作团队尚未部署.NET 5 运行时。不幸的是,它们的更新计划与您计划的版本不一致。
- 您有一个连接到 Raspberry Pi 的温度传感器,一个.NET 应用负责将数据发送到 Azure,以便随时间构建图表。在设备上编译应用不是一个选项。
这两个用例都可以通过创建自包含的.NET 应用来解决。如果应用是自包含的,这意味着它拥有运行所需的一切,而无需安装.NET 运行时。
为 Windows Server 生成文件
如果您不控制 Windows 服务器上的操作系统,这意味着您可以部署.NET 5 应用,即使服务器上只安装了.NET Core 3.1,或者根本没有.NET 运行时。
要为此生成文件,请运行dotnet publish -r win-x64命令。生成的文件可以复制到服务器并执行,而不必抱怨.NET 运行时。
为 Raspberry Pi 生成文件
对于 Raspberry Pi,即使您的开发人员机器运行 Windows 10,您也可以为不同的操作系统编译。(这称为交叉编译。)生成的位可以复制到设备并立即运行。
要生成这些文件,请运行dotnet publish -r linux-arm64命令。
如果您希望为其他平台生成文件,可以使用一个有效标识符列表,您可以在找到该列表 https://docs.microsoft.com/en-us/dotnet/core/rid-catalog 。
这种方法的缺点是应用更大,因为没有共享组件。如果您的服务器/设备只运行一个应用,那么这可能不是问题,但是如果您有 20 个不同的.NET 应用,这些应用都是自包含的,那么会有很多开销。这对于存储空间充足的机架式服务器可能不是问题,但对于 Raspberry Pi 来说,这可能是一个问题。
很难给出确切的数字。NET 团队不断迭代改进关于大小的所有方面,无论它是否是自包含的。在使用证书读取示例应用(在上一节中)进行测试后,我们确定了下图中给出的金额:

图 2.33–dotnet 发布命令的大小比较
在您的机器上测试时,您可能看不到完全相同的数字,但它给出了大小差异的一般概念。可以对输出进行微调,但即使如此,很明显,使用自包含应用并不能为每个应用节省空间。
对于已经安装了.NET 运行时的存储受限设备,您可能需要采用结合两种策略中最好的策略。您可以使其依赖于运行时且特定于平台。这意味着您可以创建一个包含跨平台组件的文件,以及一个包含特定于目标平台的组件的不同文件。
您可以通过运行dotnet publish –r linux-arm64 –-self-contained false命令来实现这一点。
移动设备跨平台
本书没有介绍开发移动应用,您也不太可能将 web 应用部署到移动设备上。不过,这是跨平台讨论的一部分,因此有必要进行简要介绍。
在上一章中,我们介绍了不同.NET 框架的历史,并提到了一个事实,即支持在移动设备上运行.NET 代码最初并不是微软的倡议。换句话说,尽管你可以使用 C#来创建移动应用,但它并不是.NET 技术堆栈的正式组成部分。自从微软收购 Xamarin 以来,Xamarin 已经正式上市,并且在使这些工具与.NET 和 Visual Studio 集成方面做出了重大努力。
我们已经问过,为什么您通常需要跨平台功能,但这个问题值得在移动设备上重复。苹果为 iOS 提供工具和框架,谷歌为 Android 提供工具和框架,那么你为什么要使用.NET 呢?
要回答这个问题,您应该从几个方面来考虑。
首先,你在写什么样的申请?它是一款相当通用的数据输入业务应用,还是针对苹果或安卓生态系统进行了高度优化?Xamarin 支持的内容和本机工具支持的内容之间总会有一些差距(就像 Windows 中的.NET 一样),有时候 Xamarin 无法满足您的需要。
您的开发人员具备哪些技能以及团队中有多少开发人员?如果你精通 C#,Xamarin 是很棒的,因为你不必学习新的语言。但是,如果您有很强的 Java 背景,那么使用 Kotlin 创建 Android 应用可能会更容易。
如果您的开发团队足够大,能够支持专门的 iOS 开发人员,那么他们使用苹果的 Xcode 也没有什么问题。
尽管有好处,比如跨平台重用代码,但在开始一个新的移动应用项目之前,您应该反思这些事情,但出于学习的目的,我们当然鼓励您看看它是如何工作的。
要安装 Xamarin,您需要在Visual Studio 安装程序中使用.NET 检查移动开发。参见图 2.34。(您可以在初始安装期间执行此操作,也可以稍后重新打开以修改安装。):

图 2.34–支持 Visual Studio 的移动开发
这将为 Android 和 iOS 安装必要的位。
对于 Android,您可以选择安装一个 Android 仿真器并快速启动。
对于 iOS,还有一些额外的障碍。您可以在 Windows 计算机上为 iOS 开发,但要生成和发布代码,您需要一个带有 macOS 的设备。Visual Studio 支持远程连接到 Mac 以完成此任务,因此您无需将 Mac 用作开发人员体验。然而,这是另一件需要解决的事情,特别是如果您是一个单人开发团队。您可以在团队中的开发人员之间共享一台 Mac 电脑,还可以支付“云中 Mac 电脑”的费用
创建 HelloWorld iOS 应用
出于这个原因,为了创建一个 iOS 应用,返回到您的 Mac 并启动 Visual Studio 2019 For Mac 会更容易。执行以下步骤:
-
创建新的解决方案并选择
iOS-App-Single View App。 -
Fill in the app name, the organization identifier, which devices to support (iPhone, iPad, or both), and the operating system level required. See Figure 2.35:
![]()
图 2.35–配置 iOS 应用
-
Fill in the solution name, as shown in Figure 2.36:
![]()
图 2.36–配置单视图应用
-
Open
LaunchScreen.storyboardand add a label with a short message. See Figure 2.37:![]()
图 2.37–为 iOS 应用创建启动屏幕标签
-
您还可以查看
Main.cs文件,确保一切正常:using UIKit; namespace HelloWorldiOS { public class Application { // This is the main entry point of the application. static void Main(string[] args) { // if you want to use a different Application // Delegate class // from "AppDelegate" you can specify it here. UIApplication.Main(args, null, "AppDelegate"); } } -
单击播放图标开始调试。将加载一个模拟器,如图 2.38 所示:

图 2.38–启动 HelloWorldiOS 应用
为了让这个起作用,你应该已经在你的 Mac 上下载并安装了 Xcode。
为了继续介绍跨平台移动体验,让我们在 Android 上创建一些类似的东西。
创建 HelloWorld Android 应用
返回到 Windows,一旦您确保为 Visual Studio 安装了必要的组件,您可以按照以下步骤创建 Android 应用:
-
Create a new
HelloWorldAndroidproject in Visual Studio by using theMobile Apptemplate. See Figure 2.39:![]()
图 2.39–创建 Android 应用
-
Choose a name for the project, as shown in Figure 2.40:
![]()
图 2.40–配置 Android 项目
-
Select a new UI template, as shown in Figure 2.41:
![]()
图 2.41–设置 UI 模板
-
您还可以查看
MainActivity.cs文件(为了可读性而省略部分),以确保一切就绪:using System;using Android.App;using Android.OS;using Android.Runtime;using Android.Support.Design.Widget;using Android.Support.V7.App;using Android.Views;using Android.Widget; namespace AndroidApp { [Activity(Label = "@string/app_name", Theme = "@style/AppTheme.NoActionBar", MainLauncher = true)] public class MainActivity : AppCompatActivity { protected override void OnCreate(Bundle savedInstanceState) { base.OnCreate(savedInstanceState); Xamarin.Essentials.Platform.Init(this, savedInstanceState); SetContentView(Resource.Layout.activity_main); Android.Support.V7.Widget.Toolbar toolbar = FindViewById<Android.Support.V7.Widget.Toolbar> (Resource.Id.toolbar); SetSupportActionBar(toolbar); FloatingActionButton fab = FindViewById<FloatingActionButton>(Resource. Id.fab); fab.Click += FabOnClick; } public override bool OnCreateOptionsMenu(IMenu menu) { … } public override bool OnOptionsItemSelected(IMenuItem item) { … } private void FabOnClick(object sender, EventArgs eventArgs) { … } … }} -
通过调试器运行 app,如图 2.42所示:

图 2.42–启动 HelloWorldAndroid 应用
在查看了 iOS 和 Android 的代码之后,我们可以看到它可以识别为 C#代码,但样板代码与使用 web 应用模板时生成的代码不同。这突出了另一个重要的一点,关于移动平台上的跨平台。如果你对意大利跑车感兴趣,攒钱买一辆法拉利可能是一个好的开始,但拥有一辆法拉利并不意味着你能以最高速度驾驶它。一般来说,通过了解如何驾驶汽车,你将能够完成基本任务,但高速驾驶需要经过培训(如果你想安全驾驶的话)。移动设备也是如此,在编写性能代码之前,您需要了解平台的细微差别。
.NET 也无法解决平台的非编码问题。例如,苹果公司对其设备上运行的应用有相当严格的规定。因此,如果你想将被拒绝的几率降到最低,在 App Store 上发布时,你需要先阅读一些指导原则。
这并不是要阻止您创建移动应用或为此目的使用.NET,而是我们想强调的是,即使有.NET 提供的帮助,跨平台仍然是复杂的。
即使您无法测试我们在这里介绍的所有内容,如果您发现自己有更多用于开发目的的设备,也可以随时参考这些说明。虽然我们介绍了很多测试和实验,但有一些细节我们没有深入讨论,比如在不使用 VisualStudio 代码和 WSL2 组合的情况下,如何调试 Linux 上运行的代码。因此,接下来我们将为调试过程需要一些额外步骤才能开始工作的用例设置一些东西。
使用 Visual Studio 2019 在 Windows 上调试 Linux
在本章前面,我们创建了一个可以作为辅助服务运行的辅助服务,并通过 VisualStudio 代码中的远程扩展运行它。然而,在某些情况下,您无法通过 Visual Studio 代码完成所有需要的操作,或者 Linux 主机甚至没有在您将调试的同一台机器上运行。
这并不妨碍您调试在 Linux 中运行的代码,但还有一些额外的问题需要解决。我们将介绍如何使用 VisualStudio2019 并通过 SSH 进行连接,SSH 是远程连接到 Linux 系统的常用协议。
我们仍然可以使用 WSL2 进行测试,因此在这种情况下,我们仍然将连接到本地机器。可以为其他 Linux 发行版执行类似的设置。以下说明用于在我们已经设置的 Ubuntu 20.04 上启用 SSH:
-
启用 SSH 服务器:
sudo apt-get install openssh-server unzip curl -
编辑
sshd_config以允许密码登录:sudo vi /etc/ssh/sshd_config -
找到行
PasswordAuthentication no并将其更改为#PasswordAuthentication no。(按插入允许编辑。) -
按Esc退出
vi,然后进入:wq。 -
启动的
ssh服务:sudo service ssh restart -
To check the IP address of the Ubuntu installation that we are using, use the command
ip addr. This is the one found attached to inet. In Figure 2.43, it is172.28.88.220:![]()
图 2.43–验证 WSL2 中的 IP 地址
-
测试是否可以使用 Windows 10 SSH 客户端连接到 SSH 服务器。见图 2.44。SSH 客户端是 Windows 中的可选功能,因此请确保已安装它。然后,从 PowerShell 或命令行输入以下命令:
ssh user@ipaddress
以下是输出的样子:

图 2.44–测试 Windows SSH 客户端
请注意,屏幕截图中的第一行显示 Windows 提示符(C:\,而最后一行显示 Ubuntu shell(andreas@AH-BOOK。
一旦到位,您就可以打开 Visual Studio 2019 并连接到我们的代码:
-
要启动要调试的应用,请在 Windows 终端中打开 Linux 实例,并在示例中正确的文件夹
/mnt/c/Code/Book/Chapter_02_Workers中运行dotnet run。 -
确保它运行时没有任何问题,然后在 Visual Studio 2019 中打开相同的解决方案。
-
按Ctrl+Alt+P打开附加过程窗口。
-
选择
SSH作为连接类型。 -
Connect to the same SSH server as when we were testing it. Connect to user@ipaddress. Refer to Figure 2.45 as an example of the username and IP address:
![]()
图 2.45–附加到流程对话框
-
You will be prompted to enter your password as well, and if things work you should see a list of running processes. See the following screenshot:
![]()
图 2.46–在远程主机上运行进程
-
找到
dotnet run并点击附加。 -
If everything went to plan, you should be able to hit breakpoints, read variables, output, and so on, directly from Visual Studio 2019 on Windows.
防火墙
首次打开远程调试下拉列表时(打开附加到进程窗口后),系统将提示您允许通过 Windows 防火墙进行连接。接受此选项以允许调试器建立连接。
在本例中,Linux 实例是在 WSL2 上运行的,但 Visual Studio 2019 不认为这是一种特殊情况,因此连接到其他主机并不重要。这可能不像 VisualStudio 代码那样简单,但它对于需要执行更复杂操作的用例非常有用。
我们已经以多种组合方式介绍了跨平台.NET,本章到此结束。
总结
在本章中,我们看到跨平台可能是一个复杂的主题,但我们介绍了 Linux 和 macOS 的简单 web 应用的基本用例,以及同时支持 Linux 和 Windows 的更高级跨平台 web 应用。
Web 应用可能非常需要后台的支持应用,因此我们还研究了如何创建后端工作者服务。对于这些应用,.NET 提供了处理 Windows 和 Linux 服务的幕后魔术,以实现跨平台服务。在将应用作为服务安装时,还需要一些额外的步骤,我们讨论了如何在操作系统中将这些应用作为服务安装。
iOS 和 Android 设备的移动应用非常流行,尽管它们不是本书的重点,但我们探讨了如何使用.NET 的跨平台功能在这两个平台上启动和运行。我们还解释了这个过程中的一些怪癖。
在本章的最后,我们通过演示在 Windows 上运行的 Visual Studio 2019 如何通过 SSH 连接到远程 Linux 系统,来了解如何启用更高级的 Linux 调试用例。现在,您可以在您可以使用的平台上运行代码了。如果在代码中遇到问题,还应该了解如何调试这些问题。
在下一章中,当我们探索依赖注入时,我们将深入探讨 C 语言的最佳实践。
问题
- 您可以在哪些操作系统上运行.NET 5?
- 什么是 Linux 的 Windows 子系统?
- 什么是独立的.NET 应用?
- 什么时候跨平台实现(使用.NET)会变得复杂?
附录
在本章前面,我们向您展示了如何在开发机器上安装 Windows 服务。这种方法是一种简化的方法,可能不适用于机器之外的环境。因此,这里有一种更高级的方法可以将应用配置为 Windows 服务。
将应用安装为 Windows 服务–高级方法
对于生产使用,权限可能更细粒度,并且被锁定。执行以下步骤将应用设置为服务:
-
登录到将在其中部署服务的 Windows 服务器。
-
打开 PowerShell 提示符,并运行以下命令:
New-LocalUser -Name dotnetworker。 -
You need to grant permissions to the service account you just created in order to enable it to start the services. Follow these steps:
A.运行
secpol.msc打开本地安全策略编辑器。B 展开本地策略节点,选择
User Rights Assignment。C 打开作为服务登录策略。
D 选择
Add User或Group。E 使用以下任一方法提供服务帐户的名称(
dotnetworker。F 在对象名称字段中键入用户帐户({
DOMAIN OR COMPUTER NAME\USER}),然后选择OK将用户添加到策略中。G 选择
Advanced。选择Find Now。从列表中选择用户帐户。选择OK。再次选择OK将用户添加到策略中。H 选择
OK或Apply接受更改。 -
将文件复制到服务器,如
C:\dotnetworker\。 -
Run the following PowerShell cmdlets:
$acl = Get-Acl "C:\dotnetworker" $aclRuleArgs = dotnetworker, "Read,Write,ReadAndExecute", "ContainerInherit,ObjectInherit", "None", "Allow" $accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule($aclRuleArgs) $acl.SetAccessRule($accessRule) $acl | Set-Acl "C:\dotnetworker" New-Service -Name DotnetWorker -BinaryPathName C:\dotnetworker\dotnetworker.exe -Credential {SERVERNAME\dotnetworker} -Description ".NET Worker Service" -DisplayName ".NET Worker Service" -StartupType Automatic等几秒钟,它应该已经启动了。*
三、依赖注入
本章讨论了 ASP.NET Core 环境下的依赖注入(DI)。此外,本章将让您了解 DI 的概念、功能以及如何在 ASP.NET Core 应用中使用 DI。我们将通过下面的代码示例来回顾不同类型的 DI,以便您能够理解在可能需要它们的情况下如何以及何时应用它们。在本章中,我们还将介绍 DI 容器、服务生命周期以及如何处理复杂场景。在本章结束时,您将能够通过以下一些实际示例了解 DI 是如何工作的。然后,您应该能够将所学的知识和技能应用于构建真实世界和强大的 ASP.NET Core 应用,并利用 DI 提供的好处。
以下是我们将在本章中介绍的主题列表:
- 在 ASP.NET Core 中学习依赖注入
- 审查依赖注入的类型
- 理解依赖注入容器
- 理解依赖生存期
- 处理复杂场景
技术要求
本章包含用 C#编写的代码片段,用于演示各种场景。请确认您已安装第 1 章ASP.NET Core 5 简介中列出的所需软件和工具。
在深入阅读本章之前,请确保阅读了前两章,以便大致了解 ASP.NET Core 和 C#,以及它们如何协同工作。
请访问以下链接查看 CiA 视频:https://bit.ly/3qDiqYY
如果你准备好了,我们就开始吧。
ASP.NET Core 中的学习依赖注入
在.NETCore 出现之前,让您了解一下背景,在应用中获得 DI 的唯一方法是通过使用第三方框架,如 Autofac、LightInject、Unity 和许多其他框架。好消息是,在 ASP.NET Core 中,DI 被视为一等公民。这仅仅意味着你不需要做很多事情来让它工作。
不过,内置的 MicrosoftDI 容器也有其局限性。例如,默认 DI 不提供高级功能,例如属性注入装饰器、基于名称的注入、子容器、基于约定的注册和自定义生存期管理。因此,如果您发现默认 DI 容器中不可用的特性,那么您需要考虑前面提到的其他第三方 DI 框架作为替代方案。但是,仍然建议使用默认 DI 框架来构建 ASP.NET Core 应用,而不需要您实现任何特定功能。这将减少您的应用包依赖性,使您的代码更干净、更易于管理,而无需依赖第三方框架。NET Core 团队在为我们提供最常见的功能方面做得很好,您可能不需要其他任何东西。
在本节中,我们将为您进行一些实践编码,使您能够更好地理解 DI 的优点和好处。我们将从一个常见问题开始,然后应用 DI 来解决这个问题。
了解 DI 是什么
web 上有大量信息定义了 DI,但一个简单的定义如下:
“依赖项注入是一种设计模式,使开发人员能够编写松散耦合的代码。”
换句话说,DI 通过解决依赖性问题来帮助您编写干净且更易于维护的代码。DI 使模拟对象依赖关系进行单元测试变得很容易,并通过交换或替换依赖关系使应用更加灵活,而无需更改使用类。事实上,ASP.NET Core 框架的核心基础很大程度上依赖于 DI,如下图所示:

图 3.1–ASP.NET Core 框架提供的服务
所有框架提供的服务,如托管、配置、应用生命周期、日志记录、路由等许多在引擎盖下使用 DI,默认情况下,在构建应用 web 主机时注册到 DI 容器。
.NET Core 中的默认 DI 位于Microsoft.Extensions.DependencyInjection名称空间下,其实现打包到一个单独的 NuGet 包中(您可以在了解更多信息)https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection/ 。
当您从默认模板创建 ASP.NET Core 应用时,该应用引用Microsoft.AspNetCore.AppNuGet 包,如以下屏幕截图所示:

图 3.2–Microsoft.AspNetCore.App NuGet 软件包
此程序集提供一组 API,包括用于构建 ASP.NET Core 应用的Microsoft.Extensions.DependencyInjection程序集。
ASP.NET 团队单独设计了 DI 框架,这样您仍然能够在 ASP.NET Core 应用之外利用其功能。这意味着您将能够在 Azure 函数和 AWS Lamda 等事件驱动的云应用中,甚至在控制台应用中使用 DI。
DI 的使用主要支持以下两个相关概念的实现:
- 依赖倒置原则****DIP:这是一个软件设计原则,代表了面向对象编程坚实原则中的“D”。它为避免依赖性风险和解决常见的依赖性问题提供了指南。然而,这一原则并没有规定任何具体的实现技术。
- 控制反转(IoC):这是一种遵循 DIP 准则的技术。这个概念是在分离状态下创建应用组件的过程,防止高级组件直接访问低级组件,并允许它们仅通过抽象进行交互。
DI 是一种遵循 IoC 概念的实现技术。它使您能够通过组件注入从较高级别的组件访问较低级别的组件。DI 遵循两个坚实的原则:DIP 和单一责任原则(SRP)。这些概念对于创建设计良好和解耦的应用至关重要,并且您应该考虑在需要的任何情况下应用它们。查看本章末尾的进一步阅读部分,了解更多关于坚实原则的信息。
你可能听说过这些术语和概念,但你仍然觉得它们很混乱。好吧,这里有一个类比可以帮助你更好地理解它们。
假设您正在制作自己的歌曲,并且希望将其上传到 web 上,以便您的朋友可以观看和收听。你可以把 DIP 看作是一种录制音乐的方式。你怎么录这首歌并不重要。你可以使用录像机、照相机、智能手机或录音机。国际奥委会正在选择如何实际录制音乐,并借助一些工具对其进行润色。例如,您可以使用录音机和摄像机的组合来录制歌曲。通常,它们被记录为原始文件。然后,您将使用一个编辑器工具对原始文件进行过滤和润色,以获得良好的输出。现在,如果您想添加一些效果、文本可视化或图形背景,那么这就是 DI 发挥作用的地方。它允许您注入文件所依赖的任何文件,以生成预期的输出。请记住,在这个类比中,IoC 和 DI 都依赖于使用编辑器工具根据原始文件(低级组件)生成最终输出(高级组件)。换句话说,IoC 和 DI 通过使用编辑器工具改进视频输出来引用相同的概念。
为了说明这一点,让我们看一个简单的例子。
常见的依赖性问题
考虑一下,我们有下面的一个 To.T0.页面,它显示了一个典型的 MVC Web 应用中的音乐列表:

图 3.3–音乐列表页面
让我们分析一下是如何得出上一个屏幕截图中显示的结果的。为了便于您快速参考,这里有一个名为MusicManager的类,它公开了一种获取音乐列表的方法:
using Chapter_03_QuickStart.Models;
using System.Collections.Generic;
namespace Chapter_03_QuickStart.DataManager
{
public class MusicManager
{
public List<SongModel> GetAllMusic()
{
return new List<SongModel>
{
new SongModel { Id = 1, Title = "Interstate Love Song", Artist ="STP", Genre = "Hard Rock" },
new SongModel { Id = 2, Title = "Man In The Box", Artist ="Alice In Chains", Genre = "Grunge" },
new SongModel { Id = 3, Title = "Blind", Artist ="Lifehouse", Genre = "Alternative" },
new SongModel { Id = 4, Title = "Hey Jude", Artist ="The Beatles", Genre = "Rock n Roll" }
};
}
}
}
前面的代码只是一个普通的类,它包含一个方法GetAllMusic()。此方法负责从列表中返回所有音乐条目。根据您的数据存储,实现可能会有所不同,您可以从数据库或通过 API 调用获取它们。然而,在这个例子中,为了简单起见,我们只返回一个静态数据列表。
SongModel类位于Models文件夹中,其结构如下:
namespace Chapter_03_QuickStart.Models
{
public class SongModel
{
public int Id { get; set; }
public string Title { get; set; }
public string Artist { get; set; }
public string Genre { get; set; }
}
}
没什么特别的。前面的代码只是一个哑类,它包含了View所期望的一些属性。
如果没有 DI,我们通常会从类中直接调用一个方法到Controller类中来呈现View,如下代码块所示:
public IActionResult Index()
{
MusicManager musicManager = new MusicManager();
var songs = musicManager.GetAllMusic();
return View(songs);
}
当您执行 HTTPGET请求时,将调用前面代码中的Index()方法。该方法负责将数据呈现到View中。您可以看到,它通过调用new操作符创建了MusicManager类的实例。这被称为“依赖关系”,因为Index()方法现在依赖于MusicManager对象来获取所需的数据。
下面是代码逻辑正在执行的高级图形表示:

图 3.4–紧密耦合的依赖关系
在上图中,Controller框表示更高级别的组件,将具体的类实现称为直接依赖,它表示较低级别的组件。
当现有的实现工作时,这种方法可能会导致代码难以管理,因为对象与方法本身紧密耦合。假设您有一组依赖于MusicManager对象的方法,并且当您将来重命名它或更改其实现时,您将被迫更新依赖于该对象的所有方法,这可能更难维护,并且在单元测试Controllers时会出现问题。请注意,重构糟糕的代码可能非常耗时和昂贵,因此最好从一开始就正确地进行重构。
避免这种混乱的理想方法是清理代码并利用接口和 DI。
利用 DI
为了解决HomeController的依赖性问题,我们需要进行一些代码重构。以下是我们目标的图形说明:

图 3.5–松散耦合的依赖关系
从前面的图中可以看到,我们只需要创建一个接口来解决依赖性问题。这种方法避免了对低级组件的直接依赖,相反,它创建了两个组件都依赖的抽象。这使得Controller类更易于测试和扩展,并使应用更易于维护。
让我们继续并开始创建一个接口。创建接口有两种方法:要么自己创建,要么使用 VisualStudio2019 提供的内置重构功能。因为我们已经有了一个我们想要提取作为接口的类,所以使用重构特性非常有意义。为此,您需要执行以下步骤:
-
Just simply right-click on the
MusicManagerclass and select Quick Actions and Refactorings..., as shown:![Figure 3.6 – The built-in Quick Actions and Refactorings feature]()
图 3.6–内置的快速操作和重构功能
-
Then, select Extract interface…:
![Figure 3.7 – The built-in Extract interface feature]()
图 3.7–内置提取接口功能
-
Now, you should be presented with a pop-up dialog to configure the interface, as shown in the following screenshot:
![Figure 3.8 – The Extract Interface pop-up window]()
图 3.8–提取界面弹出窗口
-
如果愿意,您可以更改默认配置,但在本练习中,让我们坚持使用默认配置,然后单击确定。下面是 Visual Studio 自动创建的生成代码:
using Chapter_03_QuickStart.Models; using System.Collections.Generic; namespace Chapter_03_QuickStart.DataManager { public interface IMusicManager { List<SongModel> GetAllMusic(); } }
前面的代码只是一个简单的接口,带有返回类型为List<SongModel>的GetAllMusic()方法签名。在本书中,我们不会深入探讨接口的细节,但为了给您一个简单的概述,与接口相关的两个好处是,它提供了抽象,帮助减少代码中的耦合,并使我们能够在不影响其他类的情况下为方法提供不同的实现。
现在,当您返回到MusicManager类时,您将看到该类已被更新以继承接口:
public class MusicManager : IMusicManager
整洁的只需单击几下,VisualStudio 就会自动为我们设置所有内容。我们在这里要做的是重构HomeController类以使用接口和 DI,然后向 DI 容器注册接口映射。让我们继续切换回HomeController类,并更新代码,使其看起来类似于以下内容:
namespace Chapter_03_QuickStart.Controllers
{
public class HomeController : Controller
{
private readonly IMusicManager _musicManager;
public HomeController(IMusicManager musicManager)
{
_musicManager = musicManager;
}
public IActionResult Index()
{
var songs = _musicManager.GetAllMusic();
return View(songs);
}
}
}
前面的代码首先定义了一个IMusicManager接口类型的私有read-only字段。将其设置为read-only和private被认为是最佳做法,因为这样可以防止您意外地将字段指定给class中的不同值。下一行代码定义了constructor类,并使用“构造函数注入”方法初始化依赖对象。在这种情况下,HomeController类中的任何方法都将能够访问_musicManager字段并调用其所有可用的方法和属性。我们将在本章后面更多地讨论不同类型的 DI。
当前代码现在支持 DI 模式,因为在构建类时,我们不再向Controller方法传递具体的依赖关系。通过接口抽象,我们不再需要创建具体类的新实例来直接引用GetAllMusic()方法。但是,我们引用interface字段来访问该方法。换句话说,我们的方法现在与实际的类实现松散耦合。这有助于我们更容易地维护代码并方便地执行单元测试。
注册服务
最后,让我们用 DI 容器注册接口映射。继续导航到Startup.cs文件,然后在ConfigureServices()方法中添加以下代码:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IMusicManager, MusicManager>();
//register other services here
}
前面的代码将IMusicManager接口注册为服务类型,并将MusicManager具体类映射为 DI 容器中的实现类型。这告诉框架在运行时解析已注入HomeController类构造函数的所需依赖项。DI 的美妙之处在于,只要它实现了接口,它就允许您更改任何您想要的组件。这意味着,只要MucisManager类实现了IMusicManager接口而不影响HomeController实现,您就可以始终将其替换为其他类映射。
ConfigureServices()方法负责定义应用使用的服务,包括平台特性,如实体框架核心、身份验证、您自己的服务,甚至第三方服务。最初,提供给ConfigureServices()方法的IServiceCollection接口有框架定义的服务,包括Hosting、Configuration和Logging。我们将在本章后面讨论更多关于 DI 容器的内容。
直接投资的好处
正如您从前面的示例中了解到的,DI 带来了许多好处,使您的 ASP.NET Core 应用易于维护和发展。这些好处包括:
- 它促进了组件的松散耦合。
- 它有助于分离关注点。
- 它促进了组件的逻辑抽象。
- 它有助于单元测试。
- 它提升了代码的干净性和可读性,这使得代码维护易于管理。
在了解了 DI 是什么并讨论了它的好处之后,我们现在将在下一节继续讨论它的类型。
审查依赖注入的类型
在 ASP.NET Core 应用中实现 DI 时,有几个选项,这些包括以下方法:
- 构造函数注入
- 方法注入
- 属性注入
- 视图注入
让我们在通信部分详细讨论每种类型。
构造函数注入
在前面的音乐列表示例中,我们已经看到了如何实现构造函数注入。但总而言之,这种方法基本上允许您将较低级别的依赖组件作为参数传递到constructor类中,从而将它们注入到类中。
这种方法是构建 ASP.NET Core 应用时最常用的方法。事实上,当您从默认模板创建 ASP.NET Core MVC 项目时,您将看到 DI 在默认情况下是集成的。您可以通过查看HomeController类来验证这一点,您应该会看到ILogger接口被注入到类构造函数中,如下代码所示:
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}
}
在前面的代码中,请注意这个概念与我们前面的示例非常相似,我们将MusicManager类引用与IMusicManager接口交换以执行 DI。
ILogger<HomeController>接口由日志抽象的基础设施注册,在框架中默认注册为Singleton:
services.AddSingleton(typeof(ILogger<>), typeof(Logger<>));
前面的代码将服务注册为Singleton,并使用通用的开放类型技术。这允许 DI 容器解析依赖项,而不必显式地用泛型构造的类型注册服务。
方法注射
方法注入是另一种 DI 方法,它允许您将较低级别的依赖组件作为参数注入到方法中。换句话说,依赖的对象将被传递到方法中,而不是传递到类构造函数中。当类中的各种方法需要调用子对象依赖项来完成其工作时,实现方法注入非常有用。一个典型的例子是根据调用的方法写入不同的日志格式。让我们举一个实际的例子,让您更好地理解这种方法。
让我们扩展前面关于音乐列表的示例,但是这次,我们将实现类似于通知程序的东西来演示方法或函数注入。
首先,创建一个名为INotifier的新接口,如下代码块所示:
namespace Chapter_03_QuickStart.DataManager
{
public interface INotifier
{
bool SendMessage(string message);
}
}
在前面的代码中,我们已经定义了一个简单的接口,其中包含一个名为SendMessage的方法。该方法接受表示消息的string参数,并返回boolean类型以确定操作是否成功。就这么简单。
现在,让我们继续创建一个实现INotifier接口的具体类。下面是类声明的内容:
namespace Chapter_03_QuickStart.DataManager
{
public class Notifier : INotifier
{
public bool SendMessage(string message)
{
//some logic here to publish the message
return true;
}
}
}
前面的代码显示了SendMessage()方法是如何实现的。请注意,除了返回true的boolean值之外,方法中实际上没有实现任何逻辑。这是有意的,因为实现与这个主题无关,我们不想让您注意这个领域。但是,在实际应用中,您可能会创建不同的类来实现发送消息的逻辑。例如,您可以使用消息队列、发布/订阅、事件总线、电子邮件、SMS,甚至 REST API 调用来广播消息。
现在,我们已经通过接口抽象了通知程序对象。让我们将IMusicManager接口修改为,包括一个名为GetAllMusicThenNotify的新方法。更新的IMusicManager.cs文件现在应该如下所示:
using Chapter_03_QuickStart.Models;
using System.Collections.Generic;
namespace Chapter_03_QuickStart.DataManager
{
public interface IMusicManager
{
List<SongModel> GetAllMusic();
List<SongModel> GetAllMusicThenNotify(INotifier notifier);
}
}
请注意,GetAllMusicThenNotify()方法还返回SongModel对象的List,但这一次,我们将INotifier接口作为参数传递。
让我们继续在MusicManager类中实现GetAllMusicThenNotify()方法。下面是该方法的代码实现:
public List<SongModel> GetAllMusicThenNotify(INotifier notifier)
{
//invoke the notifier method
var success = notifier.SendMessage("User viewed the music list page.");
//return the response
return success
? GetAllMusic()
: Enumerable.Empty<SongModel>().ToList();
}
前面的代码调用INotifier接口的SendMessage()方法,然后将消息作为参数/参数传递。这个过程称为方法注入,因为我们已经将INotifier接口注入GetAllMusicThenNotify()方法,因此,不必实例化 notifier 对象的具体实现。请记住,在这个特定的示例中,SendMessage()方法总是返回true来模拟流程,并且不包含任何实际实现。这仅仅意味着success变量的值将始终为true。
前面代码中的第二行返回响应,并使用 C#三元条件运算符(?:)根据表达式值计算方法应返回的数据。Ternary运算符是if-else语句的简化语法。在这种情况下,如果success变量的值为 true,我们调用GetAllMusic()方法返回整个音乐列表,否则我们使用Enumerable.Empty<T>方法返回空列表。有关三元运算符和Enumerable.EmptyLINQ 扩展方法的更多信息,请参阅https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.empty 。
现在,要执行的最后一步是更新HomeController类中的Index()action 方法以使用GetAllMusicThenNotify()方法。以下是该方法的更新版本:
public IActionResult Index()
{
var songs = _musicManager.GetAllMusicThenNotify(new Notifier());
return View(songs);
}
请注意,在前面的代码中,我们现在正在传递 notifier 对象的具体实例。由于具体实例实现了INotifier接口,GetAllMusicThenNotify()方法会自动解决。
为了更好地理解点是如何连接到图片上的,下面是我们刚才所做工作的高级图形表示:

图 3.9–方法注入
上图中的重要框为接口框。这是因为通过接口抽象实现可以避免直接类访问,并将不同类中的各种实现解耦。例如,如果出现业务需求并要求您根据不同的事件实现不同形式的通知,您可以轻松创建实现INotifier接口的SMSNotifier、MessageQueueNotifier、EmailNotifier。然后,执行它需要的任何逻辑来分别满足业务需求。虽然您可能仍然能够在不使用接口的情况下完成方法注入,但它可能会使您的代码变得混乱,并且非常难以管理。如果不使用接口,最终将为每个通知类创建不同的方法,这将导致返回单元测试和代码维护问题。
财产注入
属性注入(或setter 注入允许您在类中将较低级别的依赖组件引用为property。只有在依赖关系确实是可选的情况下,您才会使用这种方法。换句话说,如果不提供这些依赖项,您的服务仍然可以正常工作。
让我们再举一个例子,使用我们现有的音乐列表示例。这次,我们将更新Notifier样本以使用属性注入而不是方法注入。为了实现这一点,我们需要做的第一件事是更新IMusicManager接口。继续并替换现有代码,使其看起来类似于:
using Chapter_03_QuickStart.Models;
using System.Collections.Generic;
namespace Chapter_03_QuickStart.DataManager
{
public interface IMusicManager
{
INotifier Notify { get; set; }
List<SongModel> GetAllMusic();
List<SongModel> GetAllMusicThenNotify();
}
}
在前面的代码中,我们添加了一个名为Notify的新属性,然后通过删除INotifier参数修改了GetAllMusicThenNotify()方法。
接下来,让我们更新MusicManager类以反映IMusicManager接口中的更改。更新后的类现在应如下所示:
using Chapter_03_QuickStart.Models;
using System.Collections.Generic;
using System.Linq;
namespace Chapter_03_QuickStart.DataManager
{
public class MusicManager : IMusicManager
{
public INotifier Notify { get; set; };
public List<SongModel> GetAllMusic()
{
//removed code for brevity
}
public List<SongModel> GetAllMusicThenNotify()
{
// Check if the Notify property has been set
if (Notify != default)
{
//invoke the notifier method
Notify.SendMessage("User viewed the music list page.");
}
//return list of music
return GetAllMusic();
}
}
}
在前面的代码中,我们实现了Notify属性,它使用 C#的自动实现属性特性返回一个INotifier接口类型。如果您不熟悉自动属性,那么在属性访问器中不需要附加逻辑的情况下,基本上会使属性声明更加简洁。这意味着以下代码行:
public INotifier Notify { get; set; }
仅相当于以下代码:
private INotifier _notifier;
public INotifier Notify
{
get { return _notifier };
set { _notifier = value };
}
前面的代码也可以使用 C#7.0 中引入的表达式 Bodied Property Accessors重写:
private INotifier _notifier;
public INotifier Notify
{
get => _notifier;
set => _notifier = value;
}
当您需要使用不同的实现设置属性时,可以使用前面的代码。然而,在我们的示例中,使用自动属性更有意义,因为它更干净。
回到我们的示例,我们需要实现Notify属性,以便HomeController类能够在调用GetAllMusicThenNotify()方法之前设置其值。
GetAllMusicThenNotify()方法非常简单。首先检查Notify属性是否已设置为null。任何引用类型的default关键字值均为null。换句话说,根据null或default进行验证在这里并不重要。如果没有null验证检查,当属性未设置时,您将最终得到一个NullReferenceException错误。因此,始终检查空值是最佳实践。现在,如果Notify属性不是null,那么我们调用SendMessage()方法。最后,我们将音乐列表返回给调用者。
我们需要修改的最后一步是HomeController的Index()方法。更新后的代码如下所示:
public IActionResult Index()
{
_musicManager.Notify = new Notifier();
var songs = _musicManager.GetAllMusicThenNotify();
return View(songs);
}
前面的代码使用Notifier类的新实例设置Notify属性。然后调用GetAllMusicThenNotify()方法,最终将结果返回给View。
下面是我们刚才所做工作的高级图形表示:

图 3.10–财产注入
在这种方法中需要注意的重要一点是,即使我们不设置Notify属性,Index()方法仍将工作,并将数据返回给View。总之,在代码中集成可选功能时,应该只使用属性注入。
视图注入
视图注入是 ASP.NET Core 支持的另一种 DI 方法。此功能是在 ASP.NET MVC 6 中引入的,它是 ASP.NET Core 的第一个版本(以前称为 ASP.NET 5),使用@inject指令。@inject指令允许您将类或服务中的一些方法调用直接注入View。这对于视图特定的服务非常有用,例如本地化或仅用于填充视图元素的数据。
让我们先举几个例子。现在,在MusicManager类中添加以下方法:
public async Task<int> GetMusicCount()
{
return await Task.FromResult(GetAllMusic().Count);
}
前面的代码是一个异步方法,返回int的Task。虽然本书没有深入介绍 C#异步编程,但提供一点关于它的背景知识可能会有用。方法中的逻辑只是返回GetAllMusic()结果中的项目计数。使用List集合的Count属性获取Count的值。由于该方法期望返回一个Task,而GetAllMusic()方法返回一个List类型,因此结果被包装在Task.FromResult()调用中。然后,它使用await操作符等待async方法完成任务,然后在流程完成时异步将结果返回给调用方。换句话说,await关键字是事物可以异步的地方。async关键字启用该方法中的await关键字,并更改处理方法结果的方式。换句话说,async关键字只启用await关键字。有关 C#的async和await关键字的更多信息,请查看本章末尾的参考链接。
为了让它工作,我们需要执行的下一步是在Startup.cs文件的ConfigureServices()方法中将MusicManager类注册为服务:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<MusicManager>();
//register other services here
}
在前面的代码中,我们已经将该服务注册为Transient。这意味着每次请求依赖项时,都会创建一个新的服务实例。我们将在本章的理解依赖生存期一节中进一步讨论服务生存期。
现在,您将如何将MusicManager类作为服务注入View:
@inject Chapter_03_QuickStart.DataManager.MusicManager MusicService
下面是我们前面添加的引用GetMusicCount()方法的代码:
Total Songs: <h2>@await MusicService.GetMusicCount()</h2>
@符号是一种隐含语法,允许您在View中使用 C#代码。我们将在下一章深入探讨 Razor。
以下是将服务注入View后输出的示例屏幕截图:

图 3.11–视图注入输出
请注意,4的值已经打印在页面上。这是从GetMusicCount()方法返回的值。请记住,在使用这种技术可能有用的时候,你应该考虑分离你的 Ty2 T2 和 AutoT3-逻辑来关注关注的分离。在实践中,建议从您的Controller生成数据;View不应该关心数据是如何以及在哪里处理的。
现在我们已经看到了不同类型的 DI,并了解了何时使用它们,我们将在下一节继续讨论 DI 容器。
理解依赖注入容器
依赖注入容器并不是应用 DI 技术的真正要求。然而,随着应用的增长和变得更加复杂,使用它可以简化对所有依赖项的管理,包括它们的生命周期。
.NET Core 附带了一个内置 DI/IoC 容器,简化了 DI 管理。事实上,默认的 ASP.NET Core 应用模板广泛使用 DI。您可以通过查看 ASP.NET Core 应用的Startup类来查看它:
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
// This method gets called by the runtime.
// Use this method to add services to the container.
}
public void Configure(IApplicationBuilder app,
IWebHostEnvironment env)
{
// This method gets called by the runtime.
// Use this method to configure the HTTP request
// and middleware pipeline.
}
}
在前面的代码中,IConfiguration接口已经使用构造函数注入方法传递给Startup类构造函数。这允许您访问appsettings.json文件中定义的配置值。您不需要自己注册IConfiguration,因为在配置Host时,框架会为您解决这一问题。您可以通过查看Program类的CreateHostBuilder()方法来了解这是如何实现的:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
前面代码中的CreateDefaultBuilder()方法使用预先配置的默认值初始化WebHostBuilder类的新实例,包括Hosting、Configurations和Logging。最后,ConfigureWebHostDefaults()方法添加了典型 ASP.NET Core 应用所需的所有其他内容,例如配置Kestrel和使用Startup类来配置 DI 容器和中间件管道。
请记住,您只能将某些服务注入Startup类构造函数,这些服务包括IWebHostEnvironment、IhostEnvironment和IConfiguration。
当应用启动时,其他服务必须注册到 DI 容器。此过程通过向IServiceCollection添加服务来完成:

图 3.12–DI 容器
在.NETCore 中,容器管理的依赖项称为服务。我们希望注入容器的任何服务都必须添加到IServiceCollection,以便服务提供商能够在运行时解析这些服务。在引擎盖下,微软内置 DI 容器实现IServiceProvider接口。构建自己的 IoC/DI 容器框架并不理想,但如果您这样做,IServiceProvider接口就是您应该看到的。
IServiceCollection有两种主要的服务类型:
- 框架提供的服务:表示上图中的紫色框,是.NET Core 框架的部分,默认注册。这些服务包括
Hosting、Configuration、Logging、HttpContext等。 - 应用服务:代表白框。此类服务指的是您在 ASP.NET Core 应用中创建和使用的服务,这些服务不属于框架本身。由于这些服务通常由您创建,因此您需要在 DI 容器中手动注册它们,以便在应用启动时解析它们。这类服务的一个例子是我们的
IMusicManager接口示例。
DI 容器管理已注册服务的实例化和配置。通常,此过程分三个步骤执行:
-
注册:需要先注册要注入应用不同区域的服务,以便 DI 容器框架知道将服务映射到哪个实现类型。一个很好的例子是当我们将
IMusicManager接口映射到名为MusicManager的具体类实现时。通常,服务注册是在Startup.cs文件的ConfigureServices()方法中配置的,如下代码所示:public void ConfigureServices(IServiceCollection services) { services.AddTransient<IMusicManager, MusicManager>(); } -
解析:DI 容器通过创建对象实例并将其注入类中,在应用启动时自动解析依赖关系。根据前面的示例,我们使用构造函数注入方法将
IMusicManager接口注入HomeController类构造函数,如下代码所示:private readonly IMusicManager _musicManager; public HomeController(IMusicManager musicManager) { _musicManager = musicManager; } -
处置:注册服务时,DI 容器框架还需要知道依赖项的生存期,以便能够正确管理它们。基于我们前面关于构造函数注入方法的示例,我们在
Startup.cs文件的ConfigureServices()方法中将接口映射注册为Transient服务。
有关 ASP.NET Core 基础知识以及默认 Microsoft DI 容器如何在后台工作的更多信息,请参阅此处的官方文档:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection 。
现在我们已经了解了 DI 是如何工作的,让我们继续下一节讨论服务生命周期。
理解依赖生存期
如果您完全不熟悉 ASP.NET Core,或者很长时间没有使用 ASP.NET Core,或者您是一名经验丰富的 ASP.NET 开发人员,但没有详细研究依赖项生命周期,在构建 ASP.NET Core 应用时,您可能只使用一种类型的依赖生存期来注册所有服务。这是因为您对使用哪个服务生命周期感到困惑,并且希望安全起见。这是可以理解的,因为选择使用哪种类型的服务生命周期有时会令人困惑。希望本节能让您更好地了解应用中可以使用的不同类型的生命周期,并决定何时使用每个选项。
ASP.NET Core DI 中主要有三种服务生命周期:
- 转瞬即逝的
- 范围
- 独生子女
临时服务
AddTransient()方法可能是您最常用的方法。如果是这样的话,那么这是一个很好的选择,因为这种类型是在有疑问时使用的最安全的选择。瞬时服务在每次请求时创建。换句话说,如果您注册了具有短暂生存期的服务,那么无论是新请求还是相同的请求,只要您将其作为依赖项调用,就会得到一个新对象。此生存期最适用于轻量级和无状态服务,因为它们是在请求结束时处理的。
让我们来看一个例子,让您更好地理解瞬态服务生命周期是如何工作的。为了便于参考,我们将使用现有的音乐列表示例。我们需要做的第一件事是将以下属性添加到IMusicManager接口:
Guid RequestId { get; set; }
前面的代码只是一个简单的属性,它返回一个全局唯一标识符(GUID。我们将使用此属性来确定每个依赖项的行为方式。
现在,让我们通过在现有代码中添加以下代码来实现MusicManager类中的RequestId属性:
public Guid RequestId { get; set; }
public MusicManager(): this(Guid.NewGuid()) {}
public MusicManager(Guid requestId)
{
RequestId = requestId;
}
在前面的代码中,我们已经从IMusicManager接口实现了RequestId属性,然后定义了两个新的构造函数。第一个构造函数设置一个新的GUID值,第二个构造函数通过应用构造函数注入方法将GUID值初始化为RequestId属性。如果没有第一个constructor,DI 容器将无法在应用启动时解析我们在HomeController类中配置的依赖关系。
为了演示多个依赖项引用,让我们创建一个名为InstrumentalMusicManager的新类,然后复制以下代码:
using System;
namespace Chapter_03_QuickStart.DataManager
{
public class InstrumentalMusicManager
{
private readonly IMusicManager _musicManager;
public Guid RequestId { get; set; }
public InstrumentalMusicManager(IMusicManager musicManager)
{
RequestId = musicManager.RequestId;
}
}
}
在前面的代码中,我们还应用了Constructor Injection方法,将IMusicManager接口作为对象依赖注入到类中。然后我们初始化了RequestId属性的值,就像我们在MusicManager类中所做的那样。InstrumentalMusicManager类和MusicManager类之间的唯一区别如下:
InstrumentalMusicManager类没有实现IMusicManager接口。这是有意的,因为我们只对RequestId属性感兴趣,并尽可能简化演示。InstrumentalMusicManager类没有 setter 构造函数。原因是我们将让MusicManager类设置值。通过将IMusicManager接口注入构造函数,我们将能够从中引用RequestId属性的值,因为MusicManager类实现了该接口,尽管属性的值将根据如何使用生存期类型注册而有所不同,我们将在后面的操作中看到。
现在,导航到Startup类并更新ConfigureServices()方法,使其看起来类似于以下代码:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IMusicManager, MusicManager>();
services.AddTransient<InstrumentalMusicManager>();
// Removed for brevity. Register other services here
}
在前面的代码中,我们已将这两个服务注册为临时服务。注意,我们选择了AddTransient()方法的第二个参数。这是因为InstrumentalMusicManager类没有实现任何接口。
我们需要执行的最后一步是更新HomeController类,将InstrumentalMusicManager具体类作为依赖项注入,并引用我们之前注册的每个服务的RequestId值。下面是HomeController类代码的样子:
public class HomeController : Controller
{
private readonly IMusicManager _musicManager;
private readonly InstrumentalMusicManager _insMusicManager;
public HomeController(IMusicManager musicManager,
InstrumentalMusicManager insMusicManager)
{
_musicManager = musicManager;
_insMusicManager = insMusicManager;
}
public IActionResult Index()
{
var musicManagerReqId = _musicManager.RequestId;
var insMusicManagerReqId = _insMusicManager.RequestId;
_musicManager.Notify = new Notifier();
var songs = _musicManager.GetAllMusicThenNotify();
return View(songs);
}
}
在前面的代码中,我们使用Constructor Injection方法将InstrumentalMusicManager类和IMusicManager接口的实例作为依赖项注入。然后我们从两个对象实例中获得每个RequestId值。
现在,当您运行应用并在Index()方法上设置断点时,我们应该看到musicManagerReqId和insMusicManagerReqId变量的不同值,如下面的屏幕截图所示:

图 3.13–IMusicManager 接口实例的 RequestId 值
在前面的屏幕截图中,我们可以看到musicManagerReqId变量保存了b50f0518-8649-47cb-9f22-59d3394d59a7的 GUID 值。让我们看一下在下面的屏幕截图中insMusicManagerReqId的值:

图 3.14–InstrumentalMusicManager 类实例的 RequestId 值
如您所见,每个变量都有不同的值,即使只在MusicManager类实现中设置了RequestId。这就是Transient服务的工作方式,DI 容器框架在每次请求它们时为每个依赖项创建一个新实例。这确保了每个请求的每个依赖对象实例的唯一性。虽然此服务生命周期有其自身的好处,但请注意,使用此类型的生命周期可能会潜在地影响应用的性能,特别是当您正在处理依赖项引用庞大而复杂的大型整体式应用时。
范围服务
范围服务生存期是在每个客户端请求的生存期内创建的服务。换句话说,每个 web 请求都会创建一个实例。使用Scoped生存期的常见示例是使用对象关系映射器(ORM)时,如微软的实体框架核心(EF)。默认情况下,EF 中的DbContext将为每个客户端 web 请求创建一次。这是为了确保处理数据的相关调用将包含在每个请求的相同对象实例中。让我们来看看这个方法是如何通过修改我们以前的例子来工作的。
让我们继续更新Startup类的ConfigureServices()方法,使其看起来类似于以下代码:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IMusicManager, MusicManager>();
services.AddTransient<InstrumentalMusicManager>();
}
我们在前面的代码中实际更改的只是作为作用域服务添加的MusicManager类注册。InstrumentalMusicManager接口仍然是瞬态的,因为该类依赖于实现IMusicManager接口的MusicManager类。这意味着 DI 容器将自动应用主组件中正在使用的任何服务生命周期。
现在,当您再次运行应用时,您应该看到musicManagerReqId和insMusicManagerReqId变量现在都持有相同的RequestId值,如以下屏幕截图所示:

图 3.15–IMusicManager 接口实例的 RequestId 值
在前面的屏幕截图中,我们可以看到,musicManagerReqId变量保存着50b6b498-f09d-4640-b5dc-c06d9e3c2cd1的 GUID 值。insMusicManagerReqId变量的值显示在以下屏幕截图中:

图 3.16–InstrumentalMusicManager 接口实例的 RequestId 值
请注意,在前面的屏幕截图中,musicManagerReqId和insMusicManagerReqId现在具有相同的值。这就是Scoped服务的工作方式;在整个客户端请求过程中,这些值将保持不变。
单件服务
单例服务生命周期是只创建一次的服务,所有依赖项将在应用的整个生命周期中共享同一对象的同一实例。对于实例化成本很高的服务,您将使用这种类型的生存期,因为对象将存储在内存中,并且可以在应用中的所有注入中重用。单例服务的一个典型示例是ILogger。某一类型的ILogger<T>实例T在应用运行期间一直保持不变。这意味着,当将一个ILogger<HomeController>实例注入到您的Controller中时,每次都会向其传递相同的记录器实例。
让我们看看另一个例子来更好地理解这种类型的服务生命周期。让我们更新Startup类中的ConfigureServices()方法,并将MusicManager添加为单例服务,如下代码所示:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IMusicManager, MusicManager>();
services.AddTransient<InstrumentalMusicManager>();
}
前面代码中的AddSingleton()方法只允许创建一次服务。当我们再次运行应用时,我们应该能够看到musicManagerReqId和insMusicManagerReqId变量现在都具有相同的RequestId值,如以下屏幕截图所示:

图 3.17–IMusicManager 接口实例的 RequestId 值
在前面的屏幕截图中,我们可以看到musicManagerReqId变量保存了6fd5c68a-6dba-4bac-becc-5fc92c91b4b0的 GUID 值。现在,让我们来看看下面的截图中的变量 T2 变量的值:

图 3.18–InstrumentalMusicManager 接口实例的 RequestId 值
正如您在前面的屏幕截图中所注意到的,每个变量的值也是相同的。与Scoped服务相比,这种方法的唯一区别在于,无论您向Index()操作方法发出多少次请求,您都应该得到相同的值。您可以通过刷新页面以模拟多个 HTTP 请求来验证这一点。在 web 上下文中,这意味着每个后续请求将使用与第一次创建时相同的对象实例。这也意味着它跨越了 web 请求,因此无论是哪个用户发出了请求,他们仍然会得到相同的实例。
请记住,由于在应用的整个生命周期中,单例实例都保存在内存中,因此您应该注意应用内存的使用情况。不过,好的方面是,内存只分配一次,因此垃圾收集器所需的工作更少,并可能为您提供一些性能增益。但是,我建议您只在有意义的时候使用单例,不要将事情变成单例,因为您认为这样可以节省性能。此外,不要将单例服务与其他服务生命周期类型(如瞬态或作用域)混合使用,因为它可能会影响应用行为的复杂场景。
有关更高级和更复杂的场景,请访问 ASP.NET Core 中与 DI 相关的官方文档https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection 。
学习和理解每个服务生命周期是如何工作的对于应用的正确运行非常重要。现在,让我们快速查看下一节中如何处理复杂场景的服务。
处理复杂场景
如果您已经做到了这一点,那么我们可以假设您现在已经更好地了解 DI 是如何工作的,以及如何根据需要在不同的场景中实现它们。在本节中,我们将介绍在编写应用时可能遇到的一些复杂情况。我们将看到如何应用默认 DI 容器提供的可用选项来解决复杂场景。最后,我们将研究在 DI 容器中注册服务时如何改进服务的组织
服务描述符
在我们深入研究各种复杂场景之前,了解服务描述符是什么很重要。
服务描述符包含有关已在 DI 容器中注册的已注册服务的信息,包括服务类型、实现和生存期。IServiceCollection和IServiceProvider都在内部使用。我们很少直接使用服务描述符,因为它们通常是通过IServiceCollection的各种扩展方法自动创建的。但是,可能会出现需要您直接使用服务描述符的情况。
让我们看看一些例子来理解这一点。在前面的示例中,我们使用AddSingleton()通用扩展方法将IMusicManager接口映射注册为服务:
services.AddSingleton<IMusicManager, MusicManager>();
在 DI 容器中注册服务时,使用前面代码中的泛型扩展方法非常方便。但是,在某些情况下,您可能希望使用服务描述符手动添加服务。让我们通过看一些例子来了解如何实现这一点。
创建服务描述符有四种可能的方法。第一个是使用ServiceDescriptor对象本身,并在构造函数中传递所需的参数,如下代码段所示:
var serviceDescriptor = new ServiceDescriptor
(
typeof(IMusicManager),
typeof(MusicManager),
ServiceLifetime.Singleton
);
services.Add(serviceDescriptor);
在前面的代码中,我们将第一个参数中的IMusicManager作为服务类型传递。然后将相应的实现类型设置为MusicManager,最后将服务生命周期设置为单例。ServiceDescriptor对象还有两个重载构造函数可供使用。您可以在上阅读更多关于他们的信息 https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicedescriptor 。
第二个选项是使用ServiceDescriptor对象的静态Describe()方法,如下面的代码片段所示:
var serviceDescriptor = ServiceDescriptor.Describe
(
typeof(IMusicManager),
typeof(MusicManager),
ServiceLifetime.Singleton
);
services.Add(serviceDescriptor);
在前面的代码中,我们将向方法传递相同的参数,这与我们之前使用ServiceDescriptor对象构造函数选项所做的几乎相同。有关Describe()方法及其可用重载方法的更多信息,请访问https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicedescriptor.describe 。
您可能已经注意到,前面示例中的两个选项都要求我们通过服务生命周期。在这种情况下,我们被迫传递ServiceLifetime.Singleton枚举值。为了简化它们,我们可以使用可用的static方法来创建具有生命周期的服务描述符。
以下代码演示了其余选项:
var serviceDescriptor = ServiceDescriptor.Singleton
(
typeof(IMusicManager),
typeof(MusicManager)
);
services.Add(serviceDescriptor);
前面的代码通过简单地传递服务类型和相应的实现类型,使用了Singleton()静态方法。虽然代码现在看起来更干净了,但您可以使用泛型方法进一步简化创建过程,使代码更简洁,如以下代码段所示:
var serviceDescriptor = ServiceDescriptor
.Singleton<IMusicManager,MusicManager>();
services.Add(serviceDescriptor);
添加对 TryAdd
在前面的示例中,我们学习了如何创建服务描述符。在这一节中,让我们看看在 DI 容器中登记它们的各种方式。
在本章前面,我们已经了解了如何使用通用的Add扩展方法,例如AddTransient、AddScoped和AddSingleton方法,在 DI 容器中注册具有指定生存期的服务。这些方法中的每一个都有不同的重载,它们根据您的需要接受不同的参数。但是,随着应用变得越来越复杂,并且需要处理大量服务,当您意外注册相同类型的服务时,使用这些通用方法可能会导致应用的行为有所不同。
例如,多次注册以下服务:
services.AddSingleton<IMusicManager, MusicManager>();
services.AddSingleton<IMusicManager, AwesomeMusicManager>();
前面的代码注册了两个引用IMusicManager接口的服务。第一个注册映射到MusicManager具体类实现,第二个注册映射到AwesomeMusicManager类。
如果运行应用,您会看到注入到HomeController类中的实现类型是AwesomeMusicManager类,如下图所示:

图 3.19–HomeController 类构造函数注入
这仅仅意味着 DI 容器将在您注册相同类型的多个服务的情况下使用最后注册的条目。因此,ConfigureServices()方法中服务注册的顺序可能非常重要。为了避免这种情况,我们可以使用各种可用于注册服务的TryAdd()通用扩展方法。
因此,如果您想注册同一服务的多个实现,只需执行以下操作:
services.AddSingleton<IMusicManager, MusicManager>();
services.TryAddSingleton<IMusicManager, AwesomeMusicManager>();
在前面的代码中,我们将第二次注册更改为使用TryAddSingleton()方法。当您再次运行应用时,您现在应该看到MusicManager类实现是被注入的,如下图所示:

图 3.20–HomeController 类构造函数注入
当使用TryAdd()方法时,DI 容器只会在没有为给定服务类型定义实现的情况下注册服务。这为您提供了便利,尤其是当您有复杂的应用时,因为您可以在注册服务时更清楚地表达您的意图,并且可以防止您意外地替换以前注册的服务。因此,如果您想安全地登记您的服务,那么请考虑使用 OutT1 方法。
处理多个服务实现
之前,我们已经看到了使用Add()方法在 DI 容器中注册相同服务类型的多个服务的效果。虽然 DI 容器使用为同一服务类型定义的最后一个实现类型,但您应该知道,定义的第一个服务仍然保留在服务集合条目中。换句话说,对同一接口多次调用Add()方法将在服务集合中创建多个条目。这意味着上一个示例中的最后一次注册不会取代第一次注册。
要利用同一接口的多个实现,必须首先更改使用相同服务类型定义服务的方式。这是为了在具有重复的实现实例时避免潜在的副作用。因此,在注册一个接口的多个实例时,建议使用TryAddEnumerable()扩展方法,如下例:
services.TryAddEnumerable(ServiceDescriptor
.Singleton<IMusicManager, MusicManager>());
services.TryAddEnumerable(ServiceDescriptor
.Singleton<IMusicManager, AwesomeMusicManager>());
在前面的代码中,我们已经将AddSingleton()和TryAddSingleton()调用替换为TryAddEnumerable()方法。TryAddEnumerable()方法接受ServiceDescriptor参数类型。此方法可防止相同实现的重复注册。更多信息,请参见https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.extensions.servicecollectiondescriptorextensions.tryaddenumerable 。
现在,下一步是修改HomeController类并将依赖项包含在IEnumerable泛型集合类型中,以允许对所有实现进行评估和解析。
下面是一个如何使用前面的示例来实现这一点的示例:
private readonly IEnumerable<IMusicManager> _musicManagers;
public HomeController(IEnumerable<IMusicManager> musicManagers)
{
_musicManagers = musicManagers;
}
在前面的代码中,我们已将HomeController构造函数参数更改为接受IEnumerable<IMusicManager>服务类型。当 DI 容器解析此类的服务时,它现在将尝试解析IMusicManager的所有实例,并将它们作为IEnumerable注入,如以下屏幕截图所示:

图 3.21–解析 IMusicManager 的所有实例
请记住,当类型为IEnumerable时,DI 容器将只解析服务实现的多个实例。
更换和删除服务注册
在本节中,我们将了解如何替换和删除服务注册。要替换服务注册,可以使用IServiceCollection接口的Replace()扩展方式,如图所示:
services.AddSingleton<IMusicManager, MusicManager>();
services.Replace(ServiceDescriptor
.Singleton<IMusicManager, AwesomeMusicManager>());
Replace()方法也接受ServiceDescriptor参数类型。此方法将查找IMusicManager服务类型的第一个服务注册,如果找到,则将其删除。然后,新的实现类型将用于在 DI 容器中创建新的注册。在这种情况下,MusicManager实现类型将替换为AwesomeMusicManager类实现。这里需要记住的一点是,Replace()方法只支持删除集合中的第一个服务类型条目。
在需要删除某个服务类型的所有以前的服务注册的情况下,可以使用RemoveAll()扩展方法传递要删除的服务类型。下面是一个例子:
services.AddSingleton<IMusicManager, MusicManager>();
services.AddSingleton<IMusicManager, AwesomeMusicManager>();
services.RemoveAll<IMusicManager>();
前面的代码删除服务集合中IMusicManager服务类型的两个注册。
替换或删除 DI 容器中的服务是一种非常罕见的场景,但是如果您想要为框架或其他第三方服务提供自己的实现,那么它可能很有用。
总结
DI 是一个巨大的主题,但我们已经解决了大多数主要主题,这些主题应该可以帮助初学者在学习 ASP.NET Core 的过程中取得进步。
我们已经介绍了 DI 的概念,它是如何在后台工作的,以及它在 ASP.NET Core 环境中的基本用法。这些概念对于创建设计良好且解耦良好的应用至关重要。我们了解到,DI 提供了一些好处,帮助我们构建健壮而强大的应用。通过下面的一些详细示例,我们了解了如何有效地使用 DI 解决各种场景中的潜在问题。
DI 是构建高度可扩展和可维护应用的一种非常强大的技术。通过利用抽象,我们可以轻松地交换依赖项,而不会影响代码的行为。这在轻松集成新功能方面为您提供了更大的灵活性,并使您的代码更易于测试,这对于构建精心设计的应用也至关重要。虽然 DI 容器并不是应用 DI 模式的真正要求,但随着应用的增长和变得更加复杂,使用它可以简化对所有依赖项的管理,包括它们的生命周期。
在下一章中,我们将探讨用于构建强大的 ASP.NET Core web 应用的 Razor 视图引擎。我们将通过从头开始构建应用来进行一些实际操作编码,以便您在开发过程中更好地理解这些主题。
问题
- DI 的类型是什么?
- 什么时候应该使用依赖项生存期?
Add和TryAdd扩展方法有什么区别?
进一步阅读
先决条件:
- 了解 ASP.NET Core 的基本原理:https://docs.microsoft.com/en-us/aspnet/core/fundamentals
- 理解坚实的原则:https://en.wikipedia.org/wiki/SOLID
- 了解 ASP.NET Core 中 DI 的基本原理:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection
- 理解 MVC 中的视图注入:https://docs.microsoft.com/en-us/aspnet/core/mvc/views/dependency-injection
基本:
- C#开启和关闭类型指南:https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/types#open-和封闭式
- C#构造类型指南:https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/types#constructed-类型
- 理解 LINQ 可枚举项
empty:https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.empty - 理解 C#三元条件算子:https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/conditional-operator
- 了解 C#自动实现的属性:https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/auto-implemented-properties
高级:
- 了解服务描述符:https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicedescriptor
- 理解
TryAddEnumerable方法:https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.extensions.servicecollectiondescriptorextensions.tryaddenumerable - 理解 C 中的
async和await:https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/await
四、Razor 视图引擎
构建动态和数据驱动的 web 应用非常容易;然而,事情有时会令人困惑,特别是如果你是这项技术的新手。作为初学者,您可能会发现自己很难理解 web 的无状态特性是如何工作的。这主要是因为您从未接触过如何应用框架,或者仅仅是因为您对 web 开发完全陌生,不知道从哪里开始。
尽管有很多教程可以作为学习的参考,但您可能仍然会发现很难连接各个部分,这可能会导致您失去兴趣。好消息是 ASP.NETCore 使您更容易学习如何进行 web 开发。只要您了解 C#、基本 HTML 和 CSS,您就应该能够很快学会 web 开发。如果您是新手,困惑不解,不知道如何开始构建 ASP.NET Core 应用,那么本章适合您。
本章主要针对初学者到中级.NET 开发人员,他们希望跳入 ASP.NET Core 5,了解不同的 web 框架,并通过编写示例来解决问题。
您可能知道,有很多技术可以选择将某些功能与 ASP.NET Core 集成,如图 4.1 所示:

图 4.1–ASP.NET Core 技术堆栈
在上图中,您可以看到 ASP.NET Core 提供了大多数可以与应用集成的通用功能。这使您可以灵活地选择在构建应用时要使用的任何框架和服务。事实上,您甚至可以组合这些框架中的任何一个来生成功能强大的应用。请记住,我们不会在本章中涵盖前一张图中显示的所有技术。
在本章中,我们将主要关注Web 应用堆栈,通过查看在 ASP.NET Core 中构建 Web 应用时可以选择的几种 Web 框架风格。我们将通过一些实际的编码练习来介绍 MVC 和 Razor 页面的基础知识,这样您就可以了解它们的工作原理并理解它们的差异。
以下是我们将在本章中讨论的主要主题列表:
- 了解 Razor 视图引擎
- 学习 Razor 语法的基础知识
- 用 MVC 构建待办应用
- 使用 Razor 页面构建待办应用
- MVC 和 Razor 页面之间的差异
在本章结束时,您应该了解 Razor 视图引擎的基本原理及其语法,并了解如何使用 ASP.NET Core 附带的两个流行 web 框架构建一个基本的、交互式的、数据驱动的 web 应用。然后,您应该能够权衡它们的优缺点,并决定哪种 web 框架最适合您。最后,您将了解在构建真实的 ASP.NET Core 应用时何时使用每个 web 框架。
技术要求
本章使用 Visual Studio 2019 演示各种示例,但如果您使用的是 Visual Studio 代码,则过程应该是相同的。
在深入阅读本章之前,请确保您对 ASP.NET Core 和 C#有基本的了解,并了解它们是如何分别工作以及如何一起工作的。虽然这不是必需的,但掌握 HTML 和 CSS 的基本知识有助于您轻松理解页面的构造方式。
请访问以下链接查看 CiA 视频:https://bit.ly/3qDiqYY
如果你准备好了,我们就开始吧。
了解 Razor view 引擎
在我们深入探讨 ASP.NET Core 环境下的 Razor 视图引擎之前,让我们先谈谈 ASP.NET 中各种视图引擎的历史。
以前版本的 ASP.NET 框架有自己的视图/标记引擎,用于呈现动态 web 页面。回到过去,活动服务器页面(经典 ASP)使用.ASP文件扩展名。ASP.NET Web 表单,通常称为Web 表单视图引擎,使用.ASPX文件扩展名。这些文件类型是包含服务器端代码(如 VBScript、VB.NET 或 C#)的标记引擎,这些代码由 web 服务器(IIS)处理以在浏览器中输出 HTML。几年后,在 ASP.NET Web 窗体变得流行之后,Microsoft 引入了 ASP.NET MVC 1.0 作为一种新的替代 Web 框架,用于在完整的.NET 框架中构建动态 Web 应用。将 MVC 引入.NET 为更广泛的开发人员打开了大门,因为它重视关注点的清晰分离和友好的 URL 路由,允许更深入的可扩展性,并遵循真实的 web 开发经验。
尽管早期版本的 MVC 解决了大多数 Web 表单的缺点,但它们仍然使用基于.ASPX的标记引擎来提供页面。许多人对 MVC 中的.ASPX标记引擎的集成并不满意,因为它太复杂,无法与 UI 一起工作。由于其处理开销,可能会影响应用的整体性能。当微软在 2011 年 1 月初发布 ASP.NET MVC 3.0 时,Razor 视图引擎作为一个新的视图引擎,在增强 ASP.NET MVC 视图的基础上诞生了。ASP.NET 完整.NET 框架中的 Razor 视图引擎支持 VB.NET(.vbhtml)和 C#(.cshtml)作为服务器端语言。
当 ASP.NET Core 被引入时,很多事情都发生了改进。由于该框架被重新设计为模块化、统一和跨平台,整个.NET 框架中的许多特性和功能都被中断,例如 Web 窗体和 VB.NET 支持。由于这些更改,Razor 视图引擎也放弃了对.vbhtml文件扩展名的支持,只支持 C#代码。
现在,您已经了解了不同 ASP.NET web 框架中各种视图引擎的一些背景知识,让我们进入下一节。在这里,您将更好地理解为什么 ASP.NET 团队决定使用Razor 视图引擎作为默认标记引擎来支持所有 ASP.NET Core web 框架。
查看 Razor view 引擎
随着 ASP.NET Core 框架的发展,ASP.NET Core 团队一直在努力提供一个更好的视图引擎,它提供了很多好处和生产力。新的 Razor 视图引擎是所有 ASP.NET Core web 框架的默认视图引擎,它经过优化,使用以代码为中心的模板方法为我们提供了更快的 HTML 生成速度。
Razor 视图引擎通常被称为Razor,是一种基于 C#的模板标记语法,用于生成包含动态内容的 HTML。视图引擎不仅支持 ASP.NET Core MVC,还支持所有其他 ASP.NET Core web 框架来生成动态页面(如图 4.2 所示)。

图 4.2–Razor 视图引擎
在上图中,我们可以看到,Blazor、Razor 页面和MVCweb 框架依赖 Razor 视图引擎生成内容页面和组件。Blazor 与 MVC 和 Razor Pages 稍有不同,因为它是一个单页应用(SPA)web 框架,使用基于组件的方法。Blazor 组件是使用.razor扩展名的文件,它仍然在引擎盖下使用 Razor 引擎。内容页通常被称为UI,只是具有.cshtml扩展名的 Razor 文件。Razor 文件主要由 HTML 和 Razor 语法组成,这使您能够在内容本身中嵌入 C#代码。因此,如果您请求一个页面,C 代码将在服务器上执行。然后,它处理所需的任何逻辑,从某处获取数据,然后将生成的数据以及组成页面的 HTML 返回到浏览器。
能够使用相同的模板语法来构建 UI 使您能够轻松地从一个 web 框架过渡到另一个 web 框架,而不需要太多的学习曲线。事实上,您可以组合任何 web 框架来构建 web 应用。但是,不建议这样做,因为事情可能会变得混乱,并且可能会导致应用代码难以维护。但有一个例外,即如果您要将整个应用从一个 web 框架迁移到另一个 web 框架,并且希望开始替换应用的某些部分以使用其他 web 框架;然后,将它们结合起来是很有意义的。
Razor 提供了很多好处,包括:
- 易学:只要你知道基本的 HTML 和一点 C#,那么学习 Razor 就很容易而且很有趣。Razor 的设计目的是让 C#开发人员在为其 ASP.NET Core 应用构建 UI 时能够充分利用他们的技能并提高生产效率。
- 干净流畅:Razor 设计紧凑简单,无需编写大量代码。与其他视图模板引擎不同,您需要在 HTML 中指定特定区域来表示服务器端代码块,Razor 引擎足够智能,可以检测 HTML 中的服务器代码,这使您能够编写干净且更易于管理的代码。
- 编辑不可知论:Razor 与 Visual Studio 这样的特定编辑无关。这使您能够在任何文本编辑器中编写代码,以提高生产率。
- IntelliSense 支持:虽然您可以在任何文本编辑器中编写基于 Razor 的代码,但由于内置了语句完成支持,使用 Visual Studio 可以进一步提高您的工作效率。
- 易于单元测试:基于 Razor 的页面/视图支持单元测试。
在 ASP.NET Core 中构建动态和交互式页面时,了解 Razor 视图引擎的工作原理非常重要。在下一节中,我们将讨论 Razor 的一些基本语法。
学习 Razor 语法基础
与其他模板化视图引擎相比,Razor 的优点在于,它最大限度地减少了构建视图或内容页时所需的代码。这使得在编写 UI 时,可以使用干净、快速、流畅的编码工作流来提高生产率。
要将 C 代码嵌入 Razor 文件(.cshtml,您需要告诉引擎您正在使用@符号注入服务器端代码块。通常,您的 C 代码块必须出现在@{…}表达式中。这意味着,只要您键入@,引擎就会足够智能,知道您正在开始编写 C#代码。在开始{符号之后的所有内容都假定为服务器端代码,直到它到达匹配的结束块}符号。
让我们看一些示例,以便更好地理解 Razor 语法基础。
呈现简单数据
在默认模板生成的典型 ASP.NET Core MVCweb 应用中,您将在主页的Index.cshtml文件中看到以下代码:
@{
ViewData[“Title”] = “Home Page”;
}
前面的代码是称为剃刀代码块。Razor 代码块通常以@符号开头,并用大括号{}括起。在前面的示例中,您将看到该行以@符号开头,它告诉 Razor 引擎您将要嵌入一些服务器代码。开放大括号和闭合大括号内的代码被假定为 C#代码。块中的代码将在服务器上计算和执行,允许您访问值并在视图中引用它。此示例与在Controller类中设置变量相同。
下面是创建一个新的ViewData变量并在HomeController类的Index()方法中为其赋值的另一个示例,如下代码块所示:
public IActionResult Index()
{
ViewData[“Message”] = “Razor is Awesome!”;
return View();
}
在前面的示例中,我们将ViewData[“Message”]值设置为“Razor is Awesome!”。ViewData不过是一本对象字典,可以通过string作为键访问。现在,让我们通过添加以下代码来显示每个ViewData对象的值:
<h1>@ViewData[“Title”]</h1>
<h2>@ViewData[“Message”]</h2>
前面的代码是隐式 Razor 表达式的示例。这些表达式通常以@符号开头,然后后跟 C#代码。与Razor 代码块不同,Razor 表达式代码呈现在浏览器中。
在前面的代码中,引用了ViewData[“Title”]和ViewData[“Message”]的值,然后将它们包含在<h1>和<h2>HTML 标记中。任何变量的值都与 HTML 一起呈现。图 4.3 显示了我们刚才所做工作的示例输出。

图 4.3–隐式 Razor 表达式输出
在前面的屏幕截图中,我们可以看到ViewData中的每个值都打印在页面上。这就是 Razor 的意义所在;它使您能够使用简化的语法将 HTML 与服务器端代码混合使用。
上一示例中描述的Razor 隐式表达式通常不应包含空格,但使用 C#await关键字除外:
<p>@await SomeService.GetSomethingAsync()</p>
前面代码中的await关键字表示通过调用SomeService类的GetSomethingAsync()方法对服务器进行异步调用。Razor 允许您使用视图注入将服务器端方法注入内容页面。有关依赖注入的更多信息,请查阅第三章、依赖注入。
隐式表达式也不允许使用 C#泛型,如下代码所示:
<p>@SomeGenericMethod<T>()</p>
前面的代码无法工作并且会抛出错误的原因是,<>括号内的数据类型T被解析为 HTML 标记。要在 Razor 中使用泛型,您需要使用Razor 代码块或显式表达式,就像下面的代码一样:
<p>@(SomeGenericMethod<T>())</p>
Razor 显式表达式以@符号开头,带平衡匹配括号。下面是显示从昨天开始的日期的显式表达式示例:
<p>@((DateTime.Now - TimeSpan.FromDays(1)). ToShortDateString())</p>
前面的代码获取昨天的日期,并使用ToShortDateString()扩展方法将值转换为短日期格式。Razor 将处理@()表达式中的代码,并将结果呈现给页面。
Razor 将忽略文本之间包含@符号的任何内容。例如,Razor 解析不会触及以下行:
<a href=”mailto:user@email.com”>user@email.com</a>
显式表达式对于字符串连接也很有用。例如,如果要将静态文本与动态数据结合起来并进行渲染,可以执行以下操作:
<p>Time@(DateTime.Now.Hour) AM</p>
前面的代码将呈现类似于<p>Time@10 AM</p>的内容。如果不使用显式的@()表达式,代码将改为<p>Time@DateTime.Now.Hour AM</p>。Razor 会将其评估为与电子邮件地址类似的纯文本。
如果您想显示在文本前包含一个@符号的静态内容,那么您可以简单地附加另一个@符号来转义它。例如,如果我们想在页面上显示文本@vmsdurano,那么您可以简单地执行以下操作:
<p>@@vmsdurano</p>
现在您已经了解了 Razor 的基本语法是如何工作的,让我们进入下一节,看看一些高级示例。
从视图模型渲染数据
在大多数情况下,当使用真实的应用时,您通常会处理真实数据以在页面上显示动态内容。这些数据通常来自ViewModel,其中包含一些与您感兴趣的内容相关的信息。
在本节中,我们将了解如何使用 Razor 语法在页面上显示来自服务器的数据。让我们首先在 MVC 应用的Models文件夹中创建以下类:
public class BeerModel
{
public int Id { get; set; }
public string Name { get; set; }
public string Type { get; set; }
}
前面的代码只是一个表示ViewModel的普通类。在本例中,ViewModel被称为BeerModel,其中包含视图所期望的一些属性。接下来,我们将创建一个新类来填充视图模型。新类的外观如下所示:
public class Beer
{
public List<BeerModel> GetAllBeer()
{
return new List<BeerModel>
{
new BeerModel { Id =1, Name=”Redhorse”, Type=”Lager” },
new BeerModel { Id =2, Name=”Furious”, Type=”IPA” },
new BeerModel { Id =3, Name=”Guinness”, Type=”Stout” },
new BeerModel { Id =4, Name=”Sierra”, Type=”Ale” },
new BeerModel { Id =5, Name=”Stella”, Type=”Pilsner” },
};
}
}
前面的代码只是一个表示模型的普通类。此类包含一个GetAllBeer()方法,该方法负责返回列表中的所有项目。在本例中,我们返回一个List<BeerModel>类型。根据您的数据存储和使用的数据访问框架,实现可能会有所不同。您可以从数据库或通过 API 调用提取数据。然而,在这个例子中,为了简单起见,我们只返回一个静态数据列表。
您可以将ViewModel视为占位符,用于保存仅视图需要的属性。另一方面,Model是一个实现应用的域逻辑的类。通常,检索这些类并将数据存储在数据库中。我们将在本章后面讨论这些概念。
现在我们已经对一些样本数据进行了建模,让我们修改HomeController类的Index()方法,使其看起来像这样:
public IActionResult Index()
{
var beer = new Beer();
var listOfBeers = beer.GetAllBeer();
return View(listOfBeers);
}
前面的代码初始化Beer类的一个实例,然后调用GetAllBeer()方法。然后,我们将结果设置为一个名为listOfBeers的变量,然后将其作为参数传递给视图以返回响应。
现在,让我们看看如何在页面上显示结果。继续并切换回位于Views/Home文件夹中的Index.cshtml文件。
要从视图模型访问数据,我们需要做的第一件事是使用@model指令声明一个类引用:
@model IEnumerable<Chapter_04_LearningRazorSyntax.Models. BeerModel>
前面的代码将对视图模型的引用声明为一种类型IEnumerable<BeerMode>,这使视图成为强类型视图。@model指令是保留关键字之一。此特定指令使您能够指定要在视图或页面中传递的类的类型。Razor 指令也通过使用@符号,后跟指令名称或 Razor 保留关键字,表示为隐式表达式。
现在,我们可以访问前面创建的视图模型。因为我们将视图模型声明为可枚举的,所以您可以轻松地迭代集合中的每个项,并以您想要的方式呈现数据。下面是一个仅显示BeerModel类的Name属性的示例:
<h1>My favorite beers are:</h1>
<ul>
@foreach (var item in Model)
{
<li>@item.Name</li>
}
</ul>
在前面的代码中,我们使用了<ul>HTML 标记以项目符号列表格式显示数据。在<ul>标记中,您应该注意到,我们已经使用@符号开始在 C#代码中操作数据。foreach关键字是C#保留关键字之一,用于迭代集合中的数据。在foreach块中,我们构建了要在<li>标签中显示的项目。在这种情况下,使用隐式表达式呈现Name属性。
请注意,在 HTML 中嵌入 C#逻辑是多么流畅和容易。它的工作方式是 Razor 将在表达式中查找任何 HTML 标记。如果它看到一个,它会跳出 C 代码,只有在看到匹配的结束标记时才会跳回。
以下是在浏览器中渲染时的输出:

图 4.4–隐式 Razor 表达式输出
前面只是一个示例,说明了如何轻松地在页面上显示格式化的数据列表。如果要根据某些条件筛选列表,可以执行以下操作:
<ul>
@foreach (var item in Model)
{
if (item.Id == 2)
{
<li>@item.Name</li>
}
}
</ul>
在前面的代码中,我们在foreach循环中使用了 C#if-statement来只过滤我们需要的项目。在本例中,我们检查了Id属性是否等于2,然后构造了一个<li>元素来显示满足条件时的值。
根据您的要求,在页面上显示信息的方式有很多种。在大多数情况下,您可能需要提供一个复杂的 UI 来显示信息。在这种情况下,HTML 和标记帮助程序就可以发挥作用。
HTML 帮助程序和标记帮助程序介绍
在标记助手被引入之前,HTML 助手被用于在 Razor 文件中呈现动态 HTML 内容。通常,您会在 MVC 应用视图中找到与此类似的代码:
<h1>List of beers:</h1>
<table class=”table”>
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Id)
</th>
@* Removed other headers for brevity *@
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Id)
</td>
@* Removed other rows for brevity *@
</tr>
}
</tbody>
</table>
前面的代码使用<table>标记以表格形式呈现数据。在<thead>部分中,我们使用DisplayNameForHTML 帮助程序显示视图模型中的每个属性名称。然后,我们使用 C#foreach迭代器对<tbody>部分中的每个项目进行迭代。这与我们在前面的示例中所做的几乎相同。现在的区别是,我们已经构建了以表格格式显示的数据。
<tr>元素表示行,<td>元素表示列。在每一列中,我们都使用了DisplayForHTML 助手在浏览器中显示实际数据。请记住,DisplayFor助手在呈现时不会生成任何 HTML 标记;相反,它将仅以纯文本显示值。所以,只有当你有理由使用DisplayFor时才使用它。理想情况下,上述代码中的foreach块可以替换为以下代码:
<tbody>
@foreach (var item in Model)
{
<tr>
<td>@item.Id</td>
@* Removed other rows for brevity *@
</tr>
}
</tbody>
前面的代码比更干净,渲染速度比使用DisplayForHTML 助手快得多。运行代码应该会产生如图 4.5 所示的输出。

图 4.5–HTML 帮助程序输出
虽然其他 HTML 帮助程序在处理集合、复杂对象、模板和其他情况时很有用,但在某些情况下,事情可能会变得很麻烦,特别是在处理 UI 自定义时。例如,如果我们想对 HTML 助手生成的元素应用一些 CSS 样式,那么我们必须使用重载方法,而不需要任何 IntelliSense 帮助。下面是一个简单的例子:
<h1>My most favorite beer:</h1>
@{ var first = Model.FirstOrDefault(); }
@* Removed other line for brevity *@
@Html.LabelFor(model => first.Name, new { @class = “font-weight-bold” })
: @first.Name
@* Removed other line for brevity *@
前面的代码使用LabelForHTML 助手显示信息。在本例中,我们仅使用 LINQFirstOrDefault扩展方法显示ViewModel集合中的第一个项集。LabelFor方法中的第二个参数表示htmlAttributes参数,在该参数中,我们被迫传递一个匿名对象来设置 CSS 类。在本例中,我们将 CSS 类属性应用于 label 元素的font-weight-bold。这是因为class关键字是 C#中的保留关键字,因此我们需要告诉 Razor 使用前面的@符号将@class=expression作为元素属性进行求值。这种情况使得页面变得越来越大,维护起来有点困难,阅读起来也不太友好,特别是对于不熟悉 C#的前端开发人员。为了解决这个问题,我们可以使用标签助手。
ASP.NET Core 提供了一系列内置的标记帮助程序,在 Razor 标记中创建和呈现 HTML 元素时,您可以使用它们来帮助提高工作效率。与作为 C#方法调用的HTML 帮助程序不同,标记帮助程序直接附加到 HTML 元素。这使得标记助手对前端开发人员的使用更加友好和有趣,因为他们可以完全控制 HTML。
虽然标签助手是一个需要涵盖的庞大主题,但我们将尝试看一个常见的示例,让您了解它们的用途和好处。
回到前面的示例,我们可以使用以下代码使用标记帮助程序重写代码:
<h1>My most favorite beer:</h1>
@* Removed other line for brevity *@
<label asp-for=”@first.Name” class=”font-weight-bold”></label>
: @first.Name
@* Removed other line for brevity *@
在前面的代码中,请注意,我们现在使用了一个标准的<label>HTML 标记,并使用asp-for标记助手来显示ViewModel中的Name属性。请注意,结束标记是必需的。如果使用自动关闭标记,例如<label asp-for=”@first.Id” />,则不会呈现该值。
如果您想更改要在 HTML 中呈现的属性名称,您可以使用[Display]属性。例如,如果我们想显示属性 ID 的值Beer Id,我们可以简单地执行如下代码:
Display(Name = “Beer Id”)]
public int Id { get; set; }
我们在前面的代码中所做的被称为数据注释。这使您能够定义要应用于模型/视图模型中属性的某些元数据,例如条件、验证、自定义格式等。有关数据注释的更多信息,请参见https://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations 。
图 4.6 显示了运行代码时的示例输出。

图 4.6–标记帮助器输出
您可以使用标记助手完成许多事情。ASP.NET Core 提供了大多数用于构建页面的常用标记帮助程序,如表单操作、输入控件、路由、验证、组件、脚本和许多其他。事实上,您甚至可以创建自己的或扩展标记帮助程序来定制您的需求。
标记助手在生成 HTML 元素时为您提供了很大的灵活性,提供了丰富的 IntelliSense 支持,并提供了 HTML 友好的开发体验,这有助于您在构建 UI 时节省一些开发时间。
有关 ASP.NET Core 中标记帮助程序的更多信息,请参阅https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers 。
学习Razor view engine的基本原理并理解语法的工作原理对于构建任何 ASP.NET Core web 应用至关重要。在以下部分中,我们将通过在各种 web 框架中构建一个待办应用来进行一些实践练习。这是为了让您更好地了解每个 web 框架是如何工作的,并帮助您决定在构建真实的 web 应用时选择哪种方法。
用 MVC 构建待办应用
待办事项应用是演示如何在网页上添加和修改信息的一个很好的示例。在构建真实世界、数据驱动的 web 应用时,了解这在 web 的无状态特性中是如何工作的非常有价值。
在我们开始之前,让我们先快速复习一下 MVC,以便您更好地理解它是什么。
了解 MVC 模式
为了更好地理解 MVC 模式方法,图 4.7 展示了一种以图形方式描述高级流程的尝试:

图 4.7–MVC 请求和响应流
通过查看请求流,前面的图表几乎是不言自明的。但为了验证您的理解,简要解释一下过程可能会有所帮助。术语 MVC 表示构成应用的三个组件:模型的M、视图视图的V和控制器的C。在上图中,您可以看到控制器是用户在浏览器中请求页面时调用的第一个条目。控制器负责处理任何用户交互和请求,处理完成请求所需的任何逻辑,并最终向用户返回响应。换句话说,控制器协调逻辑流。
模型是实际实现领域特定逻辑的组件。通常,模型包含实体对象、业务逻辑和检索和存储数据的数据访问代码。请记住,在实际应用中,您应该考虑分离业务逻辑和数据访问层,以评估关注点和单个责任的分离。ViewModel只是一个类,其中包含一些仅用于视图的属性。ViewModel是可选的,因为从技术上讲,您可以直接将模型返回到视图。事实上,它不是 MVC 术语的一部分。但是,值得将其包含在流程中,因为在构建实际应用时,它非常有用,值得推荐。添加此额外层使您能够仅公开所需的数据,而不是通过模型从实体对象返回所有数据。最后,视图是组成 UI 或页面的组件。通常,视图只是包含 HTML、CSS、JavaScript 和 C#嵌入代码的 Razor 文件(.cshtml。
现在您已经对 MVC 的工作原理有了一个概念,让我们从头开始构建一个 web 应用,以应用这些概念并了解框架。
创建 MVC 应用
让我们继续启动 Visual Studio 2019,然后选择创建新项目框,如图 4.8 所示。

图 4.8–创建新项目
此时会出现新建项目对话框。在对话框中选择Web作为项目类型,然后找到ASP.NET Core Web 应用项目模板,如图 4.9 所示。

图 4.9–创建新的 ASP.NET Core web 应用
要继续,请双击ASP.NET Core Web 应用模板或只需单击下一步按钮。此时会出现配置新项目对话框,如图 4.10 所示。

图 4.10–配置新项目
前面的对话框允许您配置项目名称和创建项目的位置路径。在实际应用中,您应该考虑给项目赋予一个有意义的名称,从而清楚地表明项目的全部内容。在本例中,我们将项目命名为ToDo.MVC。现在,点击创建,应该会弹出如图 4.11 所示的对话框。

图 4.11–创建新的 MVC 项目
前面的对话框允许您选择要创建的 web 框架类型。对于本例,只需选择Web 应用(模型视图控制器),然后单击创建即可让 Visual Studio 为您生成必要的文件。生成的默认文件如图 4.12 所示。

图 4.12–默认 MVC 项目结构
前面的屏幕截图显示了 MVC 应用的默认结构。您会注意到模板会自动生成Models、Views和Controllers文件夹。每个文件夹的名称对于应用的运行来说并不重要,但建议使用符合 MVC 模式的文件夹名称,这是一种良好的做法。在 MVC 应用中,功能分为多个功能。这意味着表示 MVC 的每个文件夹将包含其自己的专用逻辑函数。Models包含数据和验证;Views包含用于显示数据的 UI 相关元素,Controllers包含处理任何用户交互的操作。
如果您已经知道 ASP.NET Core 项目结构的重大变化,那么您可以跳过这一部分,但是如果您是 ASP.NET Core 的新手,那么有必要介绍一些生成的核心文件,以便更好地了解它们的用途。以下是除 MVC 文件夹外的核心文件的剖析:
Connected Services:允许您连接到应用所依赖的服务,如 Application Insights、Azure Storage、mobile 和其他 ASP.NET Core 服务,而无需手动配置它们的连接和配置。Dependencies:这是项目依赖项所在的位置,例如应用所需的 NuGet 包、外部程序集、SDK 和框架依赖项。Properties:此文件夹包含launchSettings.json文件,您可以在其中定义用于运行应用的应用变量和配置文件。wwwroot:此文件夹包含您的所有静态文件,这些文件将直接提供给客户端,包括 HTML、CSS、图像和 JavaScript 文件。appsettings.json:这是您配置应用特定设置的地方。请记住,不应将敏感数据添加到此文件中。你应该考虑将秘密和敏感信息存储在仓库或秘密管理器中。Program.cs:此文件是应用的主要入口点。这是您为应用构建主机的地方。默认情况下,ASP.NET Core 应用构建一个通用主机,封装运行应用所需的所有框架服务。Startup.cs:此文件是任何.NET 应用的核心。这是您配置应用所需的服务和依赖项的地方。
首次运行应用
让我们尝试构建并运行默认生成的模板,以确保一切正常。继续并按Ctrl+F5键盘键,或者只需单击 Visual Studio 菜单工具栏上的播放按钮,如图 4.13 所示。

图 4.13–运行应用
在前面的屏幕截图中,您将看到默认模板从 Visual Studio 内部自动配置两个 web 服务器配置文件,用于在localhost中运行应用:IIS Express和ToDo.MVC。使用的默认配置文件是 IIS Express,ToDo.MVC 配置文件在 Kestrel web 服务器上运行。通过查看launchSettings.json文件,您可以看到这是如何配置的。有关配置 ASP.NET Core 环境的更多信息,请参阅https://docs.microsoft.com/en-us/aspnet/core/fundamentals/environments 。
VisualStudio 将编译、构建并自动应用您为应用中的每个概要文件设置的任何配置。如果一切都成功构建,那么您应该看到如图 4.14 所示的输出。

图 4.14–首次运行输出
凉的现在,让我们继续下一步。
配置内存数据库
ASP.NET Core 的一个重要特性是,它允许您在内存中创建数据库。这使您能够轻松创建数据驱动的应用,而无需启动真正的服务器来存储数据。话虽如此,我们将在与实体框架(EF核心)的配合下利用这一功能,以便我们可以处理数据,并在不再需要时进行处理。
第 7 章、API 和数据访问将介绍如何使用真实数据库,因为它主要关注 API 和数据访问。现在,让我们仅将内存中的工作数据库用于此演示应用的唯一目的。
安装 EF 核心
我们需要做的第一件事是添加Microsoft.EntityFrameworkCore和Microsoft.EntityFrameworkCore.InMemoryNuGet 包作为项目引用,这样我们就能够使用 EF 作为我们的数据访问机制来查询内存数据存储中的数据。要执行此操作,请导航到 Visual Studio 菜单,然后转到工具NuGet Package ManagerPackage Manager 控制台。在 console 窗口中,通过运行以下命令安装每个软件包:
Install-Package Microsoft.EntityFrameworkCore -Version 5.0.0
Install-Package Microsoft.EntityFrameworkCore.InMemory -Version 5.0.0
前面代码中的每个命令都将提取应用所需的所有依赖项。
笔记
截至本文撰写时,Microsoft.EntityFrameworkCore的最新官方版本为5.0.0。将来的版本可能会更改,并可能影响本章中演示的示例代码。因此,在决定升级到新版本时,请确保始终检查是否有任何中断性更改。
在项目中安装 NuGet 依赖项的另一种方法是使用Manage NuGet Packages for Solution…选项,或者只需右键单击项目的dependencies文件夹,然后选择Manage NuGet Packages…选项。这两个选项都提供了一个 UI,您可以在其中轻松搜索和管理项目依赖关系。
成功安装两个包后,请确保检查您的项目依赖项文件夹,并验证它们是否已添加,如图 4.15 所示。

图 4.15–NuGet 包依赖关系
现在我们有了 EF 核心,让我们继续下一步。
创建视图模型
接下来,我们需要创建一个模型,该模型将包含我们的待办事项页面所需的一些属性。让我们继续在Models文件夹中创建一个名为Todo的新类,然后复制以下代码:
namespace ToDo.MVC.Models
{
public class Todo
{
public int Id { get; set; }
public string TaskName { get; set; }
public bool IsComplete { get; set; }
}
}
前面的代码只不过是一个包含一些属性的普通类。
定义 DbContext
EF Core要求我们查询数据存储。这通常是通过创建一个继承自DbContext类的类来完成的。现在,让我们将另一个类添加到Models文件夹中。将类别命名为TodoDbContext,然后复制以下代码:
using Microsoft.EntityFrameworkCore;
namespace ToDo.MVC.Models
{
public class TodoDbContext: DbContext
{
public TodoDbContext(DbContextOptions<TodoDbContext> options)
: base(options) { }
public DbSet<Todo> Todos { get; set; }
}
}
前面的代码定义了DbContext和将Model公开为DbSet的单个实体。DbContext需要DbContextOptions的实例。然后,我们可以重写OnConfiguring()方法来实现我们自己的代码,或者只将DbContextOptions传递给DbContext基构造函数,就像我们在前面的代码中所做的那样。
在内存中植入测试数据
现在,由于我们没有实际的数据库来提取一些数据,我们需要创建一个助手函数,在应用启动时初始化一些数据。让我们继续在Models文件夹中创建一个名为TodoDbSeeder的新类,然后复制以下代码:
public class TodoDbSeeder
{
public static void Seed(IServiceProvider serviceProvider)
{
using var context = new TodoDbContext(serviceProvider. GetRequiredService<DbContextOptions<TodoDbContext>>());
// Look for any todos.
if (context.Todos.Any())
{
//if we get here then the data already seeded
return;
}
context.Todos.AddRange(
new Todo
{
Id = 1,
TaskName = “Work on book chapter”,
IsComplete = false
},
new Todo
{
Id = 2,
TaskName = “Create video content”,
IsComplete = false
}
);
context.SaveChanges();
}
}
前面的代码从IServiceCollection中查找TodoDbContext服务并创建其实例。该方法负责在应用启动时生成两个测试Todo项。这是通过将数据添加到TodoDbContext的Todos实体来完成的。
现在,我们有了DbContext可以访问Todo项,还有一个将生成一些数据的助手类。我们接下来需要做的是将它们连接到Startup.cs和Program.cs文件中,以填充数据。
修改启动类
我们将Startup类的ConfigureServices()方法更新为以下代码:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoDbContext>(options => options. UseInMemoryDatabase(“Todos”));
services.AddControllersWithViews();
}
前面的代码将TodoDbContext注册到IServiceCollection中,并定义了一个名为Todos的内存数据库。我们需要这样做,以便我们可以通过依赖项注入引用Controller类中的DbContext实例或应用中代码的任何地方。
现在,让我们通过调用 seeder helper 函数来生成测试数据,从而进入下一步。
修改程序类
更新Program.cs文件的Main()方法,使其看起来类似于以下代码:
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
TodoDbSeeder.Seed(services);
}
host.Run();
}
前面的代码在Host生存期内创建一个作用域,并查找可从Host获得的服务提供者。最后,我们调用TodoDbSeeder类的Seed()方法,并将服务提供者作为参数传递给该方法。
此时,当应用启动并准备在应用中使用时,测试数据应该加载到内存“数据库”中。
创建待办事项控制器
现在,让我们为我们的Todo页面创建一个新Controller类。继续导航到Controllers文件夹并创建一个名为TodoController的新MVC 控制器空类。替换默认生成的代码,使其看起来类似于以下代码:
public class TodoController : Controller
{
private readonly TodoDbContext _dbContext;
public TodoController(TodoDbContext dbContext)
{
_dbContext = dbContext;
}
[HttpGet]
public IActionResult Index()
{
var todos = _dbContext.Todos.ToList();
return View(todos);
}
}
前面的代码首先定义了TodoDbContext的private和read-only字段。下一行代码定义了constructor类,使用构造函数注入方法初始化依赖对象。在这种情况下,TodoController类中的任何方法都将能够访问TodoDbContext的实例,并可以调用其所有可用的方法和属性。有关依赖注入的更多信息,请查看第 3 章、依赖注入。
Index()方法负责将内存数据存储中的所有Todo项返回到视图。您可以看到,该方法已使用[HttpGet]属性修饰,这表示该方法只能在 HTTPGET请求中调用。
现在,我们已经配置了TodoController,让我们进入下一步,创建用于显示页面上所有项目的视图。
创建视图
在创建视图之前,请确保首先构建应用以验证任何编译错误。成功构建后,右键单击索引()方法,然后选择添加视图…。在窗口对话框中,选择Razor View,它将弹出如图 4.16 所示的对话框。

图 4.16–添加新视图
在前面的对话框中,为模板选择列表,为模型类选择Todo(Todo.MVC.Models)。最后点击添加生成视图(如图 4.17 所示)。

图 4.17–生成的视图
在前面的屏幕截图中,请注意脚手架引擎以符合 MVC 模式的方式自动创建视图。在本例中,Index.cshtml文件是在Todo文件夹下创建的。
笔记
如果愿意,您可以手动添加视图文件。但是,使用脚手架模板生成与控制器操作方法匹配的简单视图要方便得多。
现在我们已经将模型、控制器和视图连接在一起,让我们运行应用以查看结果。
运行待办事项应用
按Ctrl+F5键在浏览器中启动应用,然后将/todo追加到 URL。您应该被重定向到待办事项页面,并显示如图 4.18 所示的输出。

图 4.18–待办事项列表页面
请注意,在前面的屏幕截图中,已经显示了我们之前配置的测试数据,并且脚手架模板基于ViewModel自动构建 HTML 标记。这非常方便,在应用中创建简单页面时,肯定可以节省一些开发时间。
要了解 MVC 路由是如何工作的以及它是如何配置的,只需导航到Startup类。您应该在Configure()方法中找到以下代码:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: “default”,
pattern: “{controller=Home}/{action=Index}/{id?}”);
});
前面的代码使用UseEndpoints()中间件为应用配置默认路由模式。默认模式设置一个值Home作为默认控制器,Index作为默认Action值,id作为任何路由的可选参数保持器。换句话说,/home/index路径是应用启动时的默认路径。MVC 模式遵循这种路由约定,将 URL 路径路由到Controller操作中。因此,如果您想为应用配置自定义路由规则,那么这就是您应该关注的中间件。有关 ASP.NET Core 路由的更多信息,请参阅https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing 。
此时,我们可以确认我们的待办事项页面已启动并使用测试数据运行。现在,让我们看看如何通过实现一些基本功能来扩展应用,例如添加、编辑和删除项目。
实现添加项功能
让我们修改我们的TodoController类,并为添加新项功能添加以下代码段:
[HttpGet]
public IActionResult Create()
{
return View();
}
[HttpPost]
public IActionResult Create(Todo todo)
{
var todoId = _dbContext.Todos.Select(x => x.Id).Max() + 1;
todo.Id = todoId;
_dbContext.Todos.Add(todo);
_dbContext.SaveChanges();
return RedirectToAction(“Index”);
}
正如您在前面的代码中所注意到的,有两个方法具有相同的名称。第一个Create()方法负责在用户请求页面时返回视图。我们将在下一步中创建此视图。第二个Create()方法是一个重载方法,它接受Todo视图模型作为参数,负责在内存数据库中创建一个新条目。您可以看到,这个方法已经被[HttpPost]属性修饰,这意味着该方法只能被POST请求调用。请记住,我们通过增加数据存储中现有的最大 ID 来手动生成 ID。在使用真实数据库的真实应用中,您可能不需要这样做,因为您可以让数据库自动为您生成 ID。
现在,让我们创建Create()方法的相应视图。要创建一个新视图,只需按照与我们对Index()方法相同的步骤进行操作,但这次选择创建作为脚手架模板。此过程应在View/Todo文件夹中生成名为Create.cshtml的 Razor 文件。
如果查看生成的视图,Todo视图模型的Id属性也已生成。这是正常的,因为脚手架模板将根据提供的视图模型生成 Razor 视图。我们不希望在代码中生成Id属性时将其包含在视图中。因此,请从视图中删除以下 HTML 标记:
<div class=”form-group”>
<label asp-for=”Id” class=”control-label”></label>
<input asp-for=”Id” class=”form-control” />
<span asp-validation-for=”Id” class=”text-danger”></span>
</div>
现在,再次运行应用并导航到/todo/create,您将看到一个类似于图 4.19 的页面。

图 4.19–待办事项添加页面
现在,在任务名文本框中输入值Write Tech Blog,并勾选IsComplete复选框。点击创建按钮,将向我们的内存数据库添加一个新条目,并将您重定向到索引页面,如图 4.20 所示。

图 4.20–待办事项列表页面
含糖的要添加更多项目,您可以单击列表顶部的新建链接,您应该被重定向回创建视图。请记住,为了简单起见,我们在这里没有实现任何输入验证。在实际应用中,您应该考虑使用 MultT3e 数据注释 To4 T4 或 Po.T5。您可以参考以下链接阅读更多关于这些的:
- https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation
- https://docs.fluentvalidation.net/en/latest/aspnet.html
现在,让我们进入下一步。
实现编辑功能
切换回到TodoController类,并为编辑功能添加以下代码段:
[HttpGet]
public IActionResult Edit(int id)
{
var todo = _dbContext.Todos.Find(id);
return View(todo);
}
[HttpPost]
public IActionResult Edit(Todo todo)
{
_dbContext.Todos.Update(todo);
_dbContext.SaveChanges();
return RedirectToAction(“Index”);
}
前面的代码也只有两个操作方法。第一个Edit()方法负责根据传递给路由的 ID 填充视图中的字段。第二个Edit()方法将在 HTTPPOST请求期间调用,该请求处理对数据存储的实际更新。
要为Edit操作创建相应的视图,只需按照与前面功能相同的步骤进行操作,但这次选择编辑作为脚手架模板。此过程应在View/Todo文件夹中生成名为Edit.cshtml的 Razor 文件。
下一步是更新我们的Index视图,以映射编辑和删除操作的路由。继续并将Action链接更新为以下内容:
@Html.ActionLink(“Edit”, “Edit”, new { id = item.Id }) |
@Html.ActionLink(“Delete”, “Delete”, new { id = item.Id })
前面的代码定义了两个用于在具有参数的视图之间导航的ActionLinkHTML 帮助程序。我们在前面的代码中所做的更改是将 ID 作为参数传递给每个路由,并删除 details 链接,因为我们在这里将不介绍这一点。无论如何,实现细节页面应该非常简单。您还可以查看本章的 GitHub 代码库,了解它是如何实现的。
现在,当您运行应用时,您应该能够通过点击编辑链接从待办事项Index页面导航到Edit页面。图 4.21 显示了编辑页面的示例屏幕截图。

图 4.21–待办事项编辑页面
在前面的屏幕截图中,请注意,ID 现在包含在路由中,页面将自动填充相应的数据。现在,让我们进入下一步。
实现删除功能
切换回类,并为删除功能添加以下代码段:
public IActionResult Delete(int? id)
{
var todo = _dbContext.Todos.Find(id);
if (todo == null)
{
return NotFound();
}
return View(todo);
}
[HttpPost]
public IActionResult Delete(int id)
{
var todo = _dbContext.Todos.Find(id);
_dbContext.Todos.Remove(todo);
_dbContext.SaveChanges();
return RedirectToAction(“Index”);
}
前面代码中的第一个Delete()方法负责根据 ID 向页面填充相应的数据。如果我们的内存数据存储中不存在 ID,那么我们只返回一个NotFound()结果。点击删除按钮会触发第二种Delete()方式。此方法执行从数据存储中删除项。图 4.22 显示了删除页面的示例屏幕截图。

图 4.22–待办事项删除页面
此时,您应该更好地了解 MVC 的工作原理,以及我们如何在页面上轻松实现创建、读取、更新和删除(CRUD操作。我们可以做很多事情来改进应用,所以花点时间添加缺少的功能作为额外练习。您可以尝试集成模型验证、日志记录或希望在应用中看到的任何功能。您还可以参考以下项目模板,以帮助您加快使用 MVC 与其他技术构建 web 应用的速度:
https://github.com/proudmonkey/MvcBoilerPlate
让我们继续到下一节,看看 Razor 页面。
使用 Razor 页面构建待办应用
Razor Pages 是另一个 web 框架,用于构建 ASP.NET Core web 应用。它最初是在 ASP.NET Core 2.0 发布时引入的,并成为 ASP.NET Core 的默认 web 应用模板。
查看 Razor 页面
为了更好地理解 Razor Pages 方法,图 4.23 提供了描述 HTTP 请求和响应流的流程的高级图表。

图 4.23–Razor 页面请求和响应流程
如果您以前使用过 ASP.NET Web 表单,或者使用过任何以页面为中心的 Web 框架,那么您应该会发现 Razor 页面很熟悉。与 MVC 不同,MVC 在控制器中处理请求,Razor 页面中的路由系统基于将 URL 与物理文件路径匹配。也就是说,所有请求都默认为根文件夹,默认名为Pages。
然后,将根据根文件夹中的文件和文件夹路径构建路由集合。例如,如果您有一个位于Pages/Something/MyPage.cshtml下的 Razor 文件,那么您可以使用/something/mypage路径在浏览器中导航到该页面。Razor 页面中的路由也很灵活,您可以根据需要自定义它。有关 Razor 页面路由的详细参考资料,请参阅以下资源:
https://www.learnrazorpages.com/razor-pages/routing
Razor 页面仍然使用Razor 视图引擎生成 HTML 标记,就像使用 MVC 一样。这两个 web 框架之间的主要区别之一是 Razor 页面不再使用控制器,而是使用单个页面。通常,Razor 页面由两个主要文件组成:一个.cshtml文件和一个.cshtml.cs文件。.cshtml文件是包含 Razor 标记的 Razor 文件,.cshtml.cs文件是定义页面功能的类。
为了让您更好地理解 Razor 页面与 MVC 的区别,让我们模拟一下我们之前用 MVC 构建的待办应用。请注意,我们将只讨论本示例中的显著差异,而不讨论诸如配置内存中的数据存储和运行应用以查看输出等常见问题。这是因为过程和实现与 MVC 几乎相同。此练习的源代码可在此处找到:
创建 Razor Pages 应用
继续启动 Visual Studio 2019,然后创建一个新项目。这次,从 ASP.NET Core Web 应用项目模板中选择Web 应用,如图 4.24 所示。

图 4.24–创建新的 Razor Pages web 应用
点击创建按钮生成默认文件。图 4.25 显示了 Razor Pages 项目结构的外观。

图 4.25–Razor Pages 项目结构
在前面的截图中,您会立即注意到不再有Controllers、Models和Views文件夹。相反,您只有Pages文件夹。Razor Pages 应用使用Startup类的ConfigureServices()方法中的AddRazorPages()服务进行配置:
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
}
现在,让我们看一看Index.cshtml文件,看看页面结构与 MVC 视图的区别。
了解 Razor 页面结构
以下是标记的外观:
@page
@model IndexModel
@{
ViewData[“Title”] = “Home page”;
}
<div class=”text-center”>
<h1 class=”display-4”>Welcome</h1>
</div>
我们可以看到,前面的标记与 MVC 视图非常相似,除了两件事:
- 它在文件的最开始使用
@page指令。此指令告诉 Razor 引擎将页面视为 Razor 页面,以便将任何页面交互正确地路由到正确的处理程序方法。换句话说,@page指令表示不应在Controllers中处理动作和路由。 - 与 MVC 中的
@model表示视图中要使用的ViewModel或Model类不同,Razor 页面中的@model指令表示 Razor 文件的“代码隐藏”类的名称。在这种情况下,Index.cshtml文件是指Index.cshtml.cs文件中定义的IndexModel类,如下代码所示:
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
public IndexModel(ILogger<IndexModel> logger)
{
_logger = logger;
}
public void OnGet() { }
}
前面的代码显示了 Razor 页面类结构背后的典型代码。表示页面模型的每个类都应该继承自PageModel基类。这个类封装了执行诸如ModelState、HttpContext、TempData、Routing等事物所需的几个特性和函数。
创建待办事项页面
让我们继续在Pages文件夹中创建一个名为Todos的新文件夹。我们将从显示待办事项列表的Index页面开始。
构建索引页
要创建新的 Razor 页面,只需右键单击Todos文件夹,然后选择添加****Razor 页面……。将页面名称设置为Index并点击添加按钮。此过程应在Todos文件夹中生成Index.cshtml(Razor 标记)和Index.cshtml.cs(代码隐藏类)文件。
现在,将以下代码段复制到代码隐藏类中:
public class IndexModel : PageModel
{
private TodoDbContext _dbContext;
public IndexModel(TodoDbContext dbContext)
{
_dbContext = dbContext;
}
public List<Todo> Todos { get; set; }
public void OnGet()
{
Todos = _dbContext.Todos.ToList();
}
}
前面的代码与 MVC 中的Controllers代码有些相似,除了以下几点:
- 它现在使用
OnGet()方法获取数据。PageModel公开了一些用于执行请求的处理程序方法,如OnGet()、OnPost()、OnPut()、OnDelete()等。Razor Pages 框架使用命名约定将适当的 HTTP 请求方法(HTTP 谓词)匹配到执行。这是通过在 handler 方法前面加上On前缀,后跟 HTTP 动词名称来完成的。换句话说,Razor 页面在执行请求时不使用 HTTP 谓词属性,如[HttpGet]、[HttpPost]等。 - 它将公共财产公开为
ViewModel。在这种情况下,当您在浏览器中请求Index页面时,Todos属性将填充来自数据存储的数据。然后,将在 Razor 标记中使用或使用此属性来表示数据。以以下为例:
<tbody>
@foreach (var item in Model.Todos)
{
<tr>
@*Removed other rows for brevity*@
<td>
<a asp-page=”Edit” asp-route-id=”@item. Id”>Edit</a> |
<a asp-page=”Details” asp-route-id=”@item. Id”>Details</a> |
<a asp-page=”Delete” asp-route-id=”@item. Id”>Delete</a>
</td>
</tr>
}
</tbody>
前面的标记使用了与我们在 MVC 中所做的相同的结构,只是我们现在引用了来自Model.Todos属性的数据。此外,我们现在使用asp-page和asp-route标记帮助器在具有路由参数的页面之间导航。
现在,当您运行应用并导航到/todos时,您应该看到以下输出,如图 4.26 所示。

图 4.26–Razor 页面待办事项列表页面
含糖的现在,让我们继续添加其余的功能。
添加项实现
以下代码片段相当于在 Razor 页面中添加一个新的Todo项:
public class CreateModel : PageModel
{
//removed constructor and private field for brevity
[BindProperty]
public Todo Todo { get; set; }
public IActionResult OnGet()
{
return Page();
}
public IActionResult OnPost()
{
_dbContext.Todos.Add(Todo);
_dbContext.SaveChanges();
return RedirectToPage(“./Index”);
}
}
前面的代码包含一个名为Todo的公共属性,它表示ViewModel和两个主处理程序方法。Todo属性用[BindProperty]属性修饰,以便服务器能够引用POST页面上的值。OnGet()方法只返回一个页面。OnPost()方法获取发布的Todo对象,将新记录插入数据存储,最后将您重定向回Index页面。有关 Razor 页面中模型绑定的更多信息,请参见https://www.learnrazorpages.com/razor-pages/model-binding 。
编辑项实现
以下是 Razor 页面中编辑功能的代码片段:
public class EditModel : PageModel
{
//removed constructor and private field for brevity
[BindProperty]
public Todo Todo { get; set; }
public void OnGet(int id)
{
Todo = _dbContext.Todos.Find(id);
}
public IActionResult OnPost()
{
_dbContext.Todos.Update(Todo);
_dbContext.SaveChanges();
return RedirectToPage(“./Index”);
}
}
前面的代码有点类似于Create页面,只是OnGet()方法现在接受 ID 作为参数。Id值用于从数据存储中查找相关数据,如果找到,则填充Todo对象。然后将Todo对象绑定到页面,并在提交页面时捕获相关属性的任何更改。OnPost()方法负责将数据更新到数据存储。
id值被添加到路线数据中。这是通过在@page指令中设置{id}模板保持架来完成的,如下代码所示:
@page “{id:int}”
前面的代码将创建/Edit/{id}路由,其中id表示一个值。:int表达式表示路由约束,表示id值必须是整数。
删除项目实现
以下是 Razor 页面中删除功能的代码片段:
[BindProperty]
public Todo Todo { get; set; }
public void OnGet(int id)
{
Todo = _dbContext.Todos.Find(id);
}
public IActionResult OnPost()
{
_dbContext.Todos.Remove(Todo);
_dbContext.SaveChanges();
return RedirectToPage(“./Index”);
}
前面的代码与Edit页面非常相似。唯一的区别是我们在OnPost()处理程序方法中删除项目的行。
既然您已经了解了 MVC 和 Razor 页面之间的核心区别,并且通过以下实践练习对这两种 web 框架都有了感觉,那么您应该能够决定在构建实际应用时使用哪种方法。
MVC 和 Razor 页面之间的差异
总之,这里是 MVC 和 Razor 页面之间的关键区别:
- 两者都是构建动态 web 应用的优秀 web 框架。他们有自己的好处。您只需使用哪种方法更适合某些情况。
- MVC 和 Razor 页面都重视关注点的分离。MVC 更严格,因为它遵循特定的模式。
- 由于 MVC 的复杂性,学习 MVC 可能需要更多的时间。你必须理解它背后的基本概念。
- 学习 Razor 页面更容易,因为它不那么神奇,更直接,更有条理。您不必为了构建页面而在文件夹之间切换。
- MVC 结构按功能分组。例如,视图中的所有操作都应该位于
Controller类中,以遵循约定。这使得 MVC 非常灵活,尤其是在处理复杂的 URL 路由时。 - Razor Pages 结构按功能和用途分组。例如,待办事项页面的任何逻辑都包含在单个位置中。这使您能够轻松地在应用中添加或删除功能,而无需修改代码中的不同区域。此外,代码维护更容易。
总结
这一章是巨大的!我们了解了 Razor 视图引擎的概念,以及它如何支持不同的 web 框架使用统一的标记语法生成 HTML 标记。这是 ASP.NET Core 功能强大的主要原因之一;它使您可以灵活地选择自己喜欢的 web 框架,而无需学习用于构建 UI 的不同标记语法。
我们已经介绍了 ASP.NET Core 到目前为止的两个热门 web 框架。MVC 和 Razor 页面可能都应该有专门的章节来详细介绍它们的特性。然而,我们仍然设法解决了这些问题,并通过使用内存数据库从头开始构建应用来探索它们的共同特性和差异。学习创建简单的数据驱动 web 应用的基础知识是成为一名成熟的 ASP.NET Core 开发人员的良好开端。
我们可以得出结论,Razor 页面非常适合初学者或构建简单的动态 web 应用,因为它将复杂性降至最低。另一方面,MVC 是构建大规模和更复杂应用的理想选择。
了解不同的 web 框架对于构建真实世界的应用至关重要,因为它有助于您了解优点和缺点,允许您根据项目范围和需求选择应采用的方法。
在下一章中,我们将探讨 Blazor 作为构建现代 web 应用的一种新的替代方法。
进一步阅读
- ASP.NET Core web 应用:https://dotnet.microsoft.com/apps/aspnet/web-apps
- Razor 语法:https://docs.microsoft.com/en-us/aspnet/core/mvc/views/razor
- 学习 ASP.NET Core:https://dotnet.microsoft.com/learn/aspnet
- ASP.NET Core 内置标记帮助程序:https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in
- ASP.NET Core 标记帮助程序:https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro
- C#
=>操作员:https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-operator - ASP.NET Core 基础知识:https://docs.microsoft.com/en-us/aspnet/core/fundamentals
- 页面模型类:https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel
- 学习 Razor 页面:https://www.learnrazorpages.com/razor-pages
- EF 核心:https://docs.microsoft.com/en-us/ef/core/
- EF 核心内存提供程序:https://docs.microsoft.com/en-us/ef/core/providers/in-memory
五、Blazor 入门
在上一章中,我们了解了 Razor 视图引擎的基本原理,并了解了它如何支持不同的 web 框架来呈现 web UI。我们介绍了实际的编码练习,以了解 MVC 和 Razor Pages web 框架,这些框架与 ASP.NET Core 一起提供,用于构建强大的 web 应用。在本章中,我们将了解 ASP.NET Core web 框架的最新添加—Blazor。
BlazorWeb 框架是一个巨大的主题;本书将主题分为两章,便于您轻松掌握开始使用框架所需的核心概念和基础知识。当您完成这两章时,您将了解 Blazor 应用如何与各种技术配合使用,以构建强大而动态的 web 应用。
以下是我们将在本章中介绍的主题:
- 了解 Blazor web 框架及其不同风格
- 了解我们将使用各种技术构建的目标
- 创建一个简单的 webapi
- 学习如何使用实体框架核心的内存数据库
- 学习如何使用 SignalR 执行实时更新
- 为旅游景点应用实现后端应用
本章主要针对具有 C#经验的初学者和中级.NET 开发人员,他们希望跳入 Blazor 并通过实际示例来解决问题。它将帮助您学习 Blazor 编程模型的基础知识,帮助您从头开始构建第一个 web 应用。
技术要求
本章使用 Visual Studio 2019 构建项目。您可以在查看本章的源代码 https://github.com/PacktPublishing/ASP.NET-Core-5-for-Beginners/tree/master/Chapter%2005%20and%2006/Chapter_05_and_06_Blazor_Examples/TouristSpot 。
在深入阅读本章之前,请确保您对 ASP.NET Core 和 C#有了基本的了解,因为本章将不介绍它们的基本原理。
请访问以下链接查看 CiA 视频:https://bit.ly/3qDiqYY
了解 Blazor web 框架
Blazor 于 2018 年初作为一个实验项目引入。它是基于单页应用(SPA)的 ASP.NET Core web 框架的最新添加。你可以把想象成与 React、Angular、Vue 和其他基于 SPA 的框架类似,但它由 C#和 Razor 标记语言提供支持,使你能够创建 web 应用,而无需编写 JavaScript。是的,你听对了——没有 JavaScript!虽然 Blazor 不要求您使用 JavaScript,但它提供了一个名为JavaScript 互操作性(JS 互操作性的功能),允许您从 C 代码调用 JavaScript 代码,反之亦然。相当整洁!
无论您是来自 Windows、Xamarin、Web 窗体还是传统的 ASP.NET MVC 开发背景,还是完全不熟悉 ASP.NET Core 并希望将您的技能提升到一个新的水平,Blazor 绝对是一个不错的选择,因为它可以让您使用现有的 C#技能来编写 Web UI。学习框架本身很容易,只要你知道基本的 HTML 和 CSS。它的设计目的是使 C#开发者能够利用他们的技能轻松过渡到 web 范例,以构建基于 SPA 的 web 应用。
回顾 Blazor 的不同口味
在我们讨论 Blazor 的不同口味之前,让我们先简要介绍一下 Razor 组件。
Razor 组件是 Blazor 应用的构建块。它们是 UI 的自包含块,由 HTML、CSS 和使用Razor标记的 C#代码组成。这些组件可以是整个页面、页面中的一个部分、表单或对话框。组件非常灵活、轻量级,并且易于在不同的应用(如 Razor 页面或 MVC 应用)之间重用、嵌套甚至共享。组件中发生的任何更改,例如影响应用状态的按钮单击,都将呈现图形并计算 UIdiff。此diff包含更新 UI 所需的一组 DOM 编辑,并由浏览器应用。
Blazor 已经获得了广泛的欢迎,即使该框架在市场上还是很新的。事实上,大型用户界面提供商,如 Telerik、Syncfusion 和 DevXpress,已经提供了一系列可以集成到应用中的 Razor 组件。还有其他开源项目提供现成的组件,您可以免费使用,例如 MatBlazor 和 RadZen。
Blazor 有两种主要的主机模型:
- Blazor 服务器
- BlazorWebAssembly(WASM)
让我们对每一个都做一个简短的概述。
Blazor 服务器
Blazor 服务器,通常被称为服务器端 Blazor,是在服务器上运行的一种 Blazor 应用。这是第一款正式在.NET Core 中发布的 Blazor 机型,已准备好投入生产使用。图 5.1 显示了 Blazor 服务器如何在引擎盖下工作。

图 5.1–Blazor 服务器
在上图中,我们可以看到基于服务器的 Blazor 应用被包装在 ASP.NET Core 应用中,允许它在服务器上运行和执行。它主要使用信号器管理和驱动实时服务器更新到 UI,反之亦然。这意味着在服务器中维护应用状态、DOM 交互和组件的呈现,Signal 将通过带有diff的集线器通知 UI 在应用状态更改时更新 DOM。
其优点如下:
- 无需编写 JavaScript 来运行应用。
- 应用代码保留在服务器上。
- 由于应用在服务器上运行,因此可以利用 ASP.NET 的核心功能,例如在共享项目中托管 Web API、集成其他中间件以及通过 DI 连接到数据库和其他外部依赖项。
- 支持快速加载时间和较小的下载大小,因为服务器需要处理繁重的工作负载。
- 在任何浏览器上运行。
- 强大的调试能力。
缺点如下:
- 它需要一台服务器来引导应用。
- 没有离线支持。信号器需要与服务器的开放连接。当服务器停机时,您的应用也会停机。
- 网络延迟更高,因为每个 UI 交互都需要调用服务器来重新呈现组件状态。如果您有一个在不同地区托管应用的地理复制服务器,则可以解决此问题。
- 维护和扩展成本高昂且困难。这是因为每次打开页面实例时,都会创建一个单独的信号器连接,这可能很难管理。在将应用部署到 Azure 时,使用 Azure 信号器服务可以解决此问题。对于非 Azure 云提供商,您可能需要依靠流量管理器来应对这一挑战。
Blazor WebAssembly
简单地说,WASM 是一种抽象,它使高级编程语言(如 C#)能够在浏览器中运行。此过程通过在浏览器中下载所有必需的基于 WASM 的.NET 程序集和应用 DLL 来完成,以便应用可以在客户端浏览器中独立运行。如今,大多数主流浏览器,如谷歌浏览器、微软 Edge、Mozilla Firefox、苹果 Safari 和 WebKit,都支持 WASM 技术。
Blazor WASM 最近被集成到 Blazor 中。在幕后,Blazor WASM 使用基于 WASM 的.NET 运行时来执行应用的.NET 程序集和 DLL。这种类型的应用可以在支持 WASM web 标准的浏览器上运行,无需插件。也就是说,Blazor WASM 并不是 Silverlight 的一种新形式。
图 5.2 显示了 Blazor WASM 应用如何在引擎盖下工作。

图 5.2-Blazor WASM
在上图中,我们可以看到 Blazor WASM 应用不依赖于 ASP.NET Core;应用直接在客户机上执行。客户端 Blazor 正在使用 WASM 技术运行。默认情况下,Blazor WASM 应用纯粹在客户端上运行;但是,您可以选择将其转换为 ASP.NET 托管的应用,以获得 Blazor 和全栈.NET web 开发的所有好处。
其优点如下:
- 无需编写 JavaScript 来运行应用。
- 没有服务器端依赖关系,这意味着没有延迟或可伸缩性问题,因为应用在客户端计算机上运行。
- 启用脱机支持,因为应用作为一个自包含的应用卸载到客户端。这意味着您仍然可以在与承载应用的服务器断开连接时运行应用。
- 支持渐进式 Web 应用(PWAs)。PWA 是使用现代浏览器 API 和功能的 web 应用,其行为与本机应用类似。
这些是缺点:
- 页面的初始加载速度很慢,下载量很大,因为所有必需的依赖项都需要提前拉出来,才能将应用卸载到客户端的浏览器中。在将来实现缓存以减少下载的大小和后续请求处理所需的时间时,可以对此进行优化。
- 由于 DLL 已下载到客户端,因此您的应用代码已公开。所以,你必须非常小心你放在那里的东西。
- 需要支持 WASM 的浏览器。请注意,大多数主流浏览器现在都支持 WASM。
- 这是一个不太成熟的运行时,因为它是新的。
- 与 Blazor 服务器相比,调试可能更加困难和有限。
有关 Blazor 托管模型的更多信息,请参见https://docs.microsoft.com/en-us/aspnet/core/blazor/hosting-models 。
移动 Blazor 绑定
Blazor 还提供了一个使用 C#和.NET 为 Android、iOS、Windows 和 macOS 构建本机和混合移动应用的框架。移动 Blazor 绑定使用相同的标记引擎构建 UI 组件。这意味着您可以使用 Razor 语法来定义 UI 组件及其行为。在幕后,UI 组件仍然基于Xamarin.Forms,因为它使用相同的基于 XAML 的结构来构建组件。与 Xamarin.Forms 相比,这个框架的突出之处在于它允许您混合使用 HTML,让开发人员可以选择使用他们喜欢的标记编写应用。使用混合应用,您可以混合使用 HTML 来构建组件,就像构建 web UI 组件一样。这使它成为 ASP.NET 开发人员使用现有技能进行跨平台本机移动应用开发的一个重要垫脚石。话虽如此,MobileBlazor 绑定仍处于实验阶段,在正式发布之前,没有任何保证。
在本章中,我们将不介绍移动 Blazor 绑定的开发。如果您想了解更多信息,可以参考这里的官方文档:https://docs.microsoft.com/en-us/mobile-blazor-bindings 。
五名球员,一个进球
正如我们从上一节了解到的,Blazor 只是构建 UI 的框架。为了使学习 Blazor 变得有趣有趣,我们将使用各种技术构建一个完整的 web 应用来实现一个目标。该目标是使用尖端技术构建一个简单的数据驱动 web 应用,具有实时性功能:Blazor 服务器、Blazor WASM、ASP.NET Core web API、信号器和实体框架核心。
图 5.3 展示了每种技术如何连接的高级过程。

图 5.3–五名球员,一个进球
根据上图,我们需要构建以下应用:
- 通过 API 调用显示和更新页面信息的 web 应用。该应用还将实现一个 signar 订阅,该订阅充当客户端,对 UI 执行实时数据更新。
- 公开
GET、PUT和POST面向公众的 API 端点的 Web API 应用。该应用还将配置内存中的数据存储以持久化数据,并实现 signar 以向中心广播消息,客户端可以在中心订阅并实时获取数据。 - 通过 API 调用提交新记录的 PWA。
既然您已经知道要构建什么以及使用哪些技术集,那么让我们开始着手编写代码。
构建旅游景点应用
为了在典型的数据驱动 web 应用中涵盖真实场景,我们将构建一个简单的旅游景点应用,该应用由不同的应用组成,以执行不同的任务。您可以将此应用视为旅游目的地的 wiki,用户可以在其中查看和编辑有关地点的信息。用户还可以根据评论查看排名靠前的位置,还可以实时查看其他类似应用提交的新位置。所谓实时,我们的意思是用户无需刷新页面即可查看新数据。
图 5.4 描述了我们的旅游景点应用示例所需的应用和流程的高级流程

图 5.4–要构建的应用
如果你准备好了,那我们开始吧。我们将从构建后端应用开始,该应用公开 API 端点以提供数据,以便其他应用可以使用它。
创建后端应用
对于旅游景点应用项目,我们将使用 ASP.NET Core Web API 作为后端应用。
让我们继续启动 Visual Studio 2019,然后选择创建新项目选项。在下一个屏幕上,选择ASP.NET Core Web 应用,然后单击下一步。配置新项目对话框应如图 5.5 所示出现。

图 5.5–配置您的新项目
此对话框允许您配置项目和解决方案名称,以及要创建项目的位置路径。对于这个特定的示例,我们只需将项目命名为PlaceApi,并将解决方案名称设置为TouristSpot。现在,点击创建,您将看到如图 5.6 所示的对话框。

图 5.6–创建新的 ASP.NET Core web 应用
此对话框允许您选择要创建的 web 框架的类型。对于本项目,只需选择API,然后点击创建即可让 Visual Studio 为您生成所需的文件。生成的默认文件应该与图 5.7 中的类似。

图 5.7–Web API 默认项目结构
前面的屏幕截图显示了 ASP.NET Core Web API 应用的默认结构。请注意,在本章中我们不会深入探讨 Web API 的细节,但为了让您快速了解,Web API 的工作方式与传统 ASP.NET MVC 相同,只是它是为构建可通过 HTTP 使用的 RESTful API 而设计的。换句话说,Web API 没有Razor 视图引擎,也不打算生成页面。我们将在第 7 章中深入探讨 Web API 的细节,API 和数据访问。
现在,让我们进入下一步。
配置内存中数据库
在上一章中,我们学习了如何使用具有实体框架核心的内存中数据库。如果您已经做到了这一点,您现在应该熟悉如何配置内存中的数据存储。在本演示中,我们将使用您现在熟悉的技术轻松创建数据驱动的应用,而无需启动真正的数据库服务器来存储数据。第 7 章、API 和数据访问将介绍如何在实体框架核心中使用真实数据库;现在,为了简化本练习,让我们只使用内存中的数据库。
安装实体框架核心
实体框架核心作为一个单独的 NuGet 包实现,以允许开发人员在需要时轻松集成它。有很多方法可以在应用中集成 NuGet package 依赖项。我们可以通过命令行(CLI)或通过集成到 Visual Studio 中的 NuGet 软件包管理界面(UI)来安装它。要使用 UI 安装依赖项,只需右键单击项目的Dependencies文件夹,然后选择Manage NuGet Packages…选项。图 5.8 显示了 UI 应该如何显示。

图 5.8–NuGet 软件包管理 UI
在浏览选项卡中,输入此处列出的软件包名称并安装:
Microsoft.EntityFrameworkCoreMicrosoft.EntityFrameworkCore.InMemory
成功安装两个软件包后,确保检查项目的Dependencies文件夹,并验证它们是否已添加(如图 5.9 所示)。

图 5.9–已安装的项目 NuGet 依赖项
注:
撰写本文时,Microsoft.EntityFrameworkCore的最新官方版本为5.0.0。将来的版本可能会更改,并可能影响本章中使用的示例代码。因此,在决定升级到新版本时,请确保始终检查是否有任何中断性更改。
既然我们已经准备好了实体框架核心,让我们继续下一步并配置一些测试数据。
实现数据访问层
在项目根目录中创建名为Db的新文件夹,然后创建名为Models的子文件夹。右键点击Models文件夹,选择添加>类。将类别命名为Places.cs,点击添加,然后粘贴以下代码:
using System;
namespace PlaceApi.Db.Models
{
public class Place
{
public int Id { get; set; }
public string Name { get; set; }
public string Location { get; set; }
public string About { get; set; }
public int Reviews { get; set; }
public string ImageData { get; set; }
public DateTime LastUpdated { get; set; }
}
}
前面的代码只是一个包含一些属性的普通类。稍后我们将使用该类用测试数据填充每个属性。
现在,在Db文件夹中创建一个名为PlaceDbContext.cs的新类,并复制以下代码:
using Microsoft.EntityFrameworkCore;
using PlaceApi.Db.Models;
namespace PlaceApi.Db
{
public class PlaceDbContext : DbContext
{
public PlaceDbContext(DbContextOptions<PlaceDbContext> options)
: base(options) { }
public DbSet<Place> Places { get; set; }
}
}
前面的代码定义了一个DbContext实例和一个将Places属性(实体)公开为DbSet实例的单个实体。DbSet<Place>表示内存中的数据集合,是执行数据库操作的网关。例如,在调用DbContext的SaveChanges()方法之后,对DbSet<Place>的任何更改都将提交到数据库。
让我们继续在Db文件夹中添加另一个名为PlaceDbSeeder.cs的新类。我们需要做的第一件事是声明以下命名空间引用:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using PlaceApi.Db.Models;
using System;
using System.IO;
using System.Linq;
前面的代码使我们能够访问每个命名空间中的方法和成员,这些方法和成员是我们在实现方法来为测试数据种子时所必需的。
现在,将以下方法粘贴到类中:
private static string GetImage(string fileName, string fileType)
{
var path = Path.Combine(Environment.CurrentDirectory, “Db/ Images”, fileName);
var imageBytes = File.ReadAllBytes(path);
return $”data:{fileType};base64,{Convert. ToBase64String(imageBytes)}”;
}
前面代码中的GetImage()方法获取Db/Images文件夹中存储的图像文件,并将图像转换为byte数组。然后,它将字节转换为base64字符串格式,并将格式化数据作为图像返回。我们将在下一步中引用此方法。
现在,将以下代码粘贴到类中:
public static void Seed(IServiceProvider serviceProvider)
{
using var context = new PlaceDbContext(serviceProvider. GetRequiredService<DbContextOptions<PlaceDbContext>>());
if (context.Places.Any()){ return; }
context.Places.AddRange(
new Place
{
Id = 1,
Name = “Coron Island”,
Location = “Palawan, Philippines”,
About = “Coron is one of the top destinations for tourists to add to their wish list.”,
Reviews = 10,
ImageData = GetImage(“coron_island.jpg”, “img/ jpeg”),
LastUpdated = DateTime.Now
},
new Place
{
Id = 2,
Name = “Olsob Cebu”,
Location = “Cebu, Philippines”,
About = “Whale shark watching is the most popular tourist attraction in Cebu.”,
Reviews = 3,
ImageData = GetImage(“oslob_whalesharks.png”, “img/png”),
LastUpdated = DateTime.Now
}
);
context.SaveChanges();
}
前面代码中的Seed()方法将在应用启动时初始化两个Place数据集。这是通过将数据添加到PlaceDbContext的Places实体中完成的。您可以看到,我们通过调用前面创建的GetImage()方法来设置ImageData属性的值。
现在我们已经实现了 seeder 类,接下来我们需要做的是创建一个新类,该类将包含两个扩展方法,用于注册内存中的数据库并将 seeder 类用作中间件。在Db文件夹中,继续添加一个名为PlaceDbServiceExtension.cs的新类,并粘贴以下代码:
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace PlaceApi.Db
{
public static class PlaceDbServiceExtension
{
public static void AddInMemoryDatabaseService(this IServiceCollection services, string dbName)
=> services.AddDbContext<PlaceDbContext>(options => options.UseInMemoryDatabase(dbName));
public static void InitializeSeededData (this IApplicationBuilder app)
{
using var serviceScope = app.ApplicationServices. GetRequiredService<IServiceScopeFactory>(). CreateScope();
var service = serviceScope.ServiceProvider;
PlaceDbSeeder.Seed(service);
}
}
}
前面的代码定义了两个主要的static方法。AddInMemoryDatabaseService()是将PlaceDbContext注册为依赖注入(DI容器中的服务的IServiceCollection扩展方法。请注意,我们正在将UseInMemoryDatabase``()扩展方法配置为AddDbContext()方法调用的参数。这告诉框架使用给定的数据库名称启动内存中的数据库。InitializeSeededData()扩展方法负责在应用运行时生成测试数据。它使用ApplicationServices类的GetRequiredService()方法引用用于从范围中解析依赖关系的服务提供者。然后它调用我们前面创建的PlaceDbSeeder.Seed()方法,并传递服务提供者来初始化测试数据。
this关键字位于每个方法参数中的对象类型之前,表示一个方法是一个扩展方法。扩展方法允许您向现有类型添加方法。对于这个特定的示例,我们将AddInMemoryDatabaseService()方法添加到IServiceCollection类型的对象中,并将InitializeSeededData()方法添加到IApplicationBuilder类型的对象中。有关扩展方法的更多信息,请参见https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods 。
现在,我们有了一个DbContext实例,它使我们能够访问我们的Places``DbSet、一个将生成一些数据的助手类,以及两个用于注册内存服务的扩展方法。我们接下来需要做的是将它们连接到Startup.cs中,以便在应用启动时填充我们的数据。
修改启动类
让我们将Startup类的ConfigureServices()方法更新为以下代码:
public void ConfigureServices(IServiceCollection services)
{
services.AddInMemoryDatabaseService(“PlacedDb”);
services.AddControllers();
}
在前面的代码中,我们调用了前面创建的AddInMemoryDatabaseService()扩展方法。同样,这个过程在IServiceCollection中注册PlaceDbContext,并定义一个名为PlacedDb的内存数据库。将DbContext注册为 DI 容器中的服务,使我们能够通过 DI 在应用中的任何类中引用此服务的实例。
现在,我们需要做的最后一步是调用Configure()方法中的InitializeSeededData()扩展方法,如下所示:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.InitializeSeededData();
//removed other middlewares for brevity
}
此时,我们的测试数据应该在应用启动时加载到内存中的数据库中,并准备好在应用中使用。
利用信号机实现实时功能
如今,将实时功能添加到任何 ASP.NET Core 服务器应用中都非常容易,因为 Signal 完全集成到框架中。这意味着不需要下载或引用单独的 NuGet 软件包就可以实现实时功能。
ASP.NET Signal 是一种技术,它提供了一套干净的 API,可为您的 web 应用实现实时行为,其中服务器将数据推送到客户端,而传统的方法是让客户端不断从服务器提取数据以进行更新。
要开始使用ASP.NET Core 信号器,我们需要先创建一个集线器。Hub是信号器中的一个特殊类,使我们能够从服务器对连接的客户端调用方法。本例中的服务器是我们的 Web API,我们将为其定义一个供客户端调用的方法。本例中的客户端是Blazor 服务器应用。
让我们在应用的根目录下创建一个名为PlaceApiHub的新类,然后粘贴以下代码:
using Microsoft.AspNetCore.SignalR;
namespace PlaceApi
{
public class PlaceApiHub : Hub
{
}
}
前面的代码只是从Hub类继承的一个类。我们将Hub类保留为空,因为我们没有从客户端调用任何方法。相反,API 将通过集线器发送事件。
接下来,我们将在 DI 容器中注册SignalR和ResponseCompression服务。在Startup类的ConfigureServices()方法中添加以下代码:
public void ConfigureServices(IServiceCollection services)
{
services.AddSignalR();
services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes. Concat(
new[] { “application/octet-stream” });
});
// Removed other services for brevity
}
接下来,我们需要在管道中添加ResponseCompression中间件,并映射我们的Hub。在Configure()方法中增加以下代码:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// Removed other code for brevity
app.UseResponseCompression();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<PlaceApiHub>(“/PlaceApiHub”);
});
}
前面的代码通过映射PlaceApiHub类定义信号集线器的路由。此使客户端应用能够连接到集线器并侦听从服务器发送的事件。
这很简单。我们将在下一节创建 Web API 端点时实现发送事件。
创建 API 端点
既然我们的内存数据库都设置好了,并且我们已经为我们的实时功能配置了 Signal,现在是我们创建 API 控制器并公开一些端点以向客户端提供数据的时候了。在这个特定示例中,我们需要以下 API 端点来处理数据的获取、创建和更新:
GET: api/placesPOST: api/placesPUT: api/places
继续,右键点击Controllers文件夹,然后选择添加>控制器>API 控制器清空,然后点击添加。
将类命名为PlacesController.cs,然后点击添加。现在,替换默认生成的代码,使您拥有的代码如下所示:
using Microsoft.AspNetCore.Mvc;
using PlaceApi.Db;
using PlaceApi.Db.Models;
using System;
using System.Linq;
namespace PlaceApi.Controllers
{
[ApiController]
[Route(“api/[controller]”)]
public class PlacesController : ControllerBase
{
private readonly PlaceDbContext _dbContext;
private readonly IHubContext<PlaceApiHub> _hubContext;
public PlacesController(PlaceDbContext dbContext,
IHubContext<PlaceApiHub> hubContext)
{
_dbContext = dbContext;
_hubContext = hubContext;
}
[HttpGet]
public IActionResult GetTopPlaces()
{
var places = _dbContext.Places.OrderByDescending(o => o.Reviews).Take(10);
return Ok(places);
}
}
}
前面的代码显示了 APIController类的典型结构。API 应该实现ControllerBase抽象类,以利用框架中内置的现有功能来构建 RESTful API。我们将在下一章更深入地讨论 API。现在,让我们来看看我们在前面的代码中做了什么。PlacesController类的前两行定义了PlaceDbContext和IHubContext<PlaceApiHub>的private和read-only字段。下一行定义类构造函数,并将PlaceDbContext和IHubContext<PlaceApiHub>作为依赖项注入类。在这种情况下,PlacesController类中的任何方法将能够访问PlaceDbContext和IHubContext的实例,允许我们调用其所有可用的方法和属性。
目前,我们在PlaceController中只定义了一种方法。GetTopPlaces()方法负责从内存数据存储返回前 10 行数据。我们已经使用了Enumerable类型的LINQOrderByDescending()和Take()扩展方法,根据Reviews值获取最上面的行。您可以看到,该方法已使用[HttpGet]属性修饰,这表示该方法只能由 HTTPGET请求调用。
现在,让我们添加另一个处理新记录创建的方法。在类中附加以下代码:
[HttpPost]
public IActionResult CreateNewPlace([FromBody] Place place)
{
var newId = _dbContext.Places.Select(x => x.Id).Max() + 1;
place.Id = newId;
place.LastUpdated = DateTime.Now;
_dbContext.Places.Add(place);
int rowsAffected = _dbContext.SaveChanges();
if (rowsAffected > 0)
{
_hubContext.Clients.All.SendAsync(“NotifyNewPlaceAdded”, place.Id, place.Name);
}
return Ok(“New place has been added successfully.”);
}
前面的代码负责在内存数据库中创建一个新的Place记录,同时向中心广播一个事件。在本例中,我们正在调用Hub类的Clients.All.SendAsync()方法,并将place.Id和place.Name传递给NotifyNewPlaceAdded事件。请注意,您也可以将对象传递给SendAsync()方法,而不是传递单个参数,就像我们在本例中所做的那样。您可以看到CreateNewPlace()方法已经用[HttpPost]属性修饰,这意味着方法只能通过 HTTPPOST请求调用。请记住,我们通过增加数据存储中现有的最大 ID 来手动生成Id。在使用真实数据库的真实应用中,您可能不需要这样做,因为您可以让数据库自动为您生成Id。
让我们创建应用所需的最后一个端点。将以下代码块添加到类中:
[HttpPut]
public IActionResult UpdatePlace([FromBody] Place place)
{
var placeUpdate = _dbContext.Places.Find(place.Id);
if (placeUpdate == null)
{
return NotFound();
}
placeUpdate.Name = place.Name;
placeUpdate.Location = place.Location;
placeUpdate.About = place.About;
placeUpdate.Reviews = place.Reviews;
placeUpdate.ImageDataUrl = place.ImageDataUrl;
placeUpdate.LastUpdated = DateTime.Now;
_dbContext.Update(placeUpdate);
_dbContext.SaveChanges();
return Ok(“Place has been updated successfully.”);
}
前面的代码负责更新内存数据库中现有的Place记录。UpdatePlace()方法将Place对象作为参数。首先根据 ID 检查记录是否存在,如果记录不在数据库中,则返回NotFound()响应。否则,我们更新数据库中的记录,然后返回带有消息的OK()响应。请注意,本例中的方法用[HttpPut]属性修饰,这表示此方法只能由 HTTPPUT请求调用。
启用 CORS
现在已经准备好了 API,下一步要做的就是启用跨源资源共享(CORS)。我们需要对此进行配置,以便托管在不同域/端口中的其他客户端应用可以访问 API 端点。要在 ASP.NET Core Web API 中启用 CORS,请在Startup类的ConfigureServices()方法中添加以下代码:
services.AddCors(options =>
{
options.AddPolicy(“AllowAll”,
builder =>
{
builder.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
});
});
前面的代码添加了 CORS 策略,以允许任何客户端应用访问我们的 API。在本例中,我们使用AllowAnyOrigin()、AllowAnyHeader()和AllowAnyMethod()配置设置了 CORS 策略。但是,请记住,在 To.T3 将真实 API 暴露在真实世界的 T4 应用之前,您应该考虑设置允许的来源、方法、标头和凭据。有关 CORS 的详细信息,请参见此处的官方文档:https://docs.microsoft.com/en-us/aspnet/core/security/cors 。
现在,在UseRouting()中间件后的Configure()方法中添加以下代码:
app.UseCors(“AllowAll”);
就这样。
测试端点
现在我们已经为我们的应用实现了所需的 API 端点,让我们做一个快速的测试,以确保我们的 API 端点工作正常。按Ctrl+F5在浏览器中启动应用,然后导航到https://localhost:44332/api/places端点。您应该看到如图 5.10 所示的输出。

图 5.10–API 的 HTTP GET 请求输出
前面的屏幕截图以 JSON 格式显示了我们的GetTopPlaces()``GET端点的结果。请注意 API 当前运行的 localhost 端口值,因为在调用 Blazor 应用中的端点时,我们将使用完全相同的端口号。在本例中,我们的 API 在 IIS Express 的本地端口44332上运行。通过查看Properties文件夹中的launchSettings.json文件,可以看到这是如何定义的,如下代码所示:
{
“$schema”: “http://json.schemastore.org/launchsettings.json”,
“iisSettings”: {
“windowsAuthentication”: false,
“anonymousAuthentication”: true,
“iisExpress”: {
“applicationUrl”: “http://localhost:60766”,
“sslPort”: 44332
}
},
//Removed other configuration for brevity
}
前面的代码显示了在本地运行应用(包括 IIS Express)时的配置文件配置。您可以更新配置并添加新的配置文件,以便在不同的环境中运行应用。在本例中,为了简单起见,我们将保留默认配置。默认 IIS Express 配置在http中运行时将applicationUrl端口设置为60766,在https中运行时将端口设置为44332。默认情况下,应用使用Startup类的Configure()方法中的UseHttpsRedirection()中间件。这意味着,当您尝试使用http://localhost:60766URL 时,应用将自动将您重定向到安全端口,在本例中为端口44332。
使用浏览器只允许我们测试 HTTPGET端点。要测试其余端点,例如POST和PUT,您可能需要安装浏览器应用扩展。在 Chrome 中,您可以安装高级 REST 客户端扩展。您还可以下载Postman来测试我们之前创建的 API 端点。Postman 是一个非常方便的测试 API 的工具,无需创建 UI,而且绝对免费。您可以在这里找到:https://www.getpostman.com/ 。
图 5.11 显示了在 Postman 中测试的 API 的示例屏幕截图。

图 5.11-邮递员测试
此时,我们有工作 API 端点,可以用来在页面上显示数据。学习创建 Web API 的基础知识对于整个项目的实现非常重要。
总结
在本章中,我们了解了不同类型 Blazor 托管模型背后的概念。在学习 Blazor 的同时,我们已经确定了我们将要构建的应用的目标,并确定了实现该目标所需的各种技术。我们开始使用 ASP.NET Core API 创建后端应用,并了解了如何使用 Entity Framework Core 的内存提供程序功能轻松配置测试数据,而不必设置真实的数据库。这使我们能够在执行概念验证(POC项目时轻松启动数据驱动的应用。我们还学习了如何创建简单的 RESTWebAPI 来服务数据,以及如何配置 SignalR 来执行实时更新。理解本章中使用的技术和框架的基本概念对于成功使用实际应用非常重要。
我们已经了解到,我们在本章中看到的两种 Blazor 模型都是不错的选择,尽管它们都有缺点。Blazor 背后的编程允许希望避免 JavaScript 障碍的 C#开发人员在不必学习新编程语言的情况下构建 SPA。尽管 Blazor 是相当新的,但很明显,Blazor 将成为其他著名 SPA 框架(如 Angular、React 和 Vue)中令人难以置信的热门和有力的竞争者,这是因为 WASM 在本质上是如何取代 JavaScript 的。当然,JavaScript 及其框架不会有任何进展,但是能够使用现有的 C#skillset 构建一个能够产生与 JavaScript web 应用相同输出的 web 应用是一个巨大的优势,因为它可以避免仅仅为了构建 web UI 而学习一种新的编程语言。除此之外,我们还了解到 Blazor 不仅限于 web 应用;mobileblazor 绑定正在为开发者提供一个框架来编写跨平台的本地移动应用。
在下一章中,我们将继续探索 Blazor,并构建其余部分,以完成我们的旅游景点应用。
问题
- Blazor 应用有哪些不同类型?
- 为什么使用 Blazor 而不是其他 SPA web 框架?
进一步阅读
- ASP.NET Core Blazor 简介:https://docs.microsoft.com/en-us/aspnet/core/blazor
- ASP.NET Core Blazor 托管模型配置:https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/additional-scenarios
- 在 ASP.NET Core 中创建 Web API:https://docs.microsoft.com/en-us/aspnet/core/web-api
- 开始使用 ASP.NET Core 信号器:https://docs.microsoft.com/en-us/aspnet/core/tutorials/signalr
- EF 核心内存数据库提供程序:https://docs.microsoft.com/en-us/ef/core/providers/in-memory
六、探索 Blazor Web 框架
在上一章中,我们了解了 Blazor 的全部内容,也了解了该框架提供的不同托管模型。我们开始使用 ASP.NET Core web API、EF Core 和 Signal 构建后端应用。在本章中,我们将构建其余部分以完成我们的目标。
以下是本章将涉及的主要主题列表:
- 学习服务器端和客户端 Blazor
- 学习如何创建 Razor 组件
- 学习路由、状态管理和数据绑定的基础知识
- 学习如何与后端应用交互以使用和传递数据
- 使用两个 Blazor 托管模型构建旅游景点应用
在本章结束时,您将学习如何构建一个旅游景点应用,借助实践示例结合各种技术学习 Blazor。
技术要求
本章是上一章的后续内容,因此在深入本章之前,请确保您已经阅读了第 5 章、Blazor 入门,并了解构建示例应用的目标。还建议查看第 4 章Razor 视图引擎,因为 Blazor 使用相同的标记引擎生成页面。虽然不是强制性的,但 HTML 和 CSS 的基本知识将有助于帮助您轻松理解页面的构造方式。
请访问以下链接查看 CiA 视频:http://bit.ly/3qDiqYY
创建 Blazor 服务器项目
在本项目中,我们将构建前端 web 应用,用于显示来自 web API 的数据。
让我们继续并在现有项目解决方案中添加一个新的Blazor 服务器项目。在 Visual Studio 菜单中,选择文件新建项目。或者,也可以右键单击解决方案以添加新项目。在新建项目对话框字段中,选择Blazor App,如下图所示:

图 6.1–创建新 Blazor 应用项目
点击下一步。在下一个屏幕中,您可以配置项目的名称和位置路径。在本例中,我们仅将项目命名为BlazorServer.Web。点击创建按钮,您将出现以下对话框:

图 6.2–创建新 Blazor 服务器应用项目
选择Blazor 服务器 App模板,保留默认配置不变,然后点击创建。Visual Studio 应构建构建 Blazor 服务器应用所需的必要文件,如以下屏幕截图所示:

图 6.3–Blazor 服务器应用默认项目结构
如果您阅读过第 4 章Razor 视图引擎,您会注意到 Blazor 服务器项目的结构与 Razor 页面非常相似,除了以下几点:
- 它使用
.razor文件扩展名而不是.cshtml,原因是 Blazor 应用主要基于组件。.razor文件是Razor 组件,通过可以使用 HTML 和 C#构建 UI。这与在.cshtml文件中构建 UI 基本相同。在 Blazor 中,组件本身就是页面,也可以是包含子组件的页面。Razor 组件也可以在 MVC 或 Razor 页面中使用,因为它们都使用相同的标记语言,称为Razor 视图引擎。 - Blazor 应用包含一个
App.razor组件。与任何其他 SPA web 框架一样,Blazor 使用一个主组件来加载应用 UI。App.razor组件作为应用的主组件,使您能够为组件配置路由。以下是App.razor文件的默认实现:
<Router AppAssembly=”@typeof(Program).Assembly”>
<Found Context=”routeData”>
<RouteView RouteData=”@routeData” DefaultLayout=”@typeof(MainLayout)” />
</Found>
<NotFound>
<LayoutView Layout=”@typeof(MainLayout)”>
<p>Sorry, there’s nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
前面的代码定义了Router组件,并配置了应用启动时在浏览器中呈现的默认布局。在这种情况下,默认布局将呈现MainLayout.razor组件。有关Blazor 路由的更多信息,请参考以下链接:https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/routing 。
Blazor 服务器项目还包含一个Host.cshtml文件,作为应用的主要入口点。在一个典型的基于客户端的 SPA 框架中,_Host.cshtml文件表示Index.html文件,其中主要的App组件正在被引用和引导。在这个文件中,您可以看到正在 HTML 文档的<body>部分中调用App.razor组件,如下代码块所示:
<body>
<app>
<component type=”typeof(App)” render-mode=”ServerPrerendered” />
</app>
@*Removed other code for brevity*@
</body>
前面的代码以ServerPrerendered作为默认呈现模式呈现App.razor组件。此模式告诉框架首先以静态 HTML 呈现组件,然后在浏览器启动时引导应用。
创建模型
在这个项目中,我们要做的第一件事是创建一个类,该类将包含一些与我们期望的 web API 响应相匹配的属性。让我们继续在Data文件夹下创建一个名为Place.cs的新类。类定义应如下所示:
using System;
using System.ComponentModel.DataAnnotations;
namespace BlazorServer.Web.Data
{
public class Place
{
public int Id { get; set; }
[Required] public string Name { get; set; }
[Required] public string Location { get; set; }
[Required] public string About { get; set; }
public int Reviews { get; set; }
public string ImageData { get; set; }
public DateTime LastUpdated { get; set; }
}
}
正如您所观察到的,前面的代码与我们在 web API 项目中创建的Place类相同,只是我们使用数据注释用[Required]属性装饰了一些属性。我们将用 web API 的结果填充这些属性,并在 Blazor 组件中使用它来显示信息。所需属性确保更新表单时这些字段不会为空。我们将在本章后面看到如何做到这一点。
实现 web API 通信服务
现在我们已经准备好了Model,让我们实现一个服务,用于调用两个 web API 端点来获取和更新数据。首先,安装Microsoft.AspNetCore.SignalR.ClientNuGet 软件包,以便 us 能够连接到Hub并监听事件。
安装 Signal 客户端包后,在Data文件夹下创建一个名为PlaceService.cs的新类,并复制以下代码:
public class PlaceService
{
private readonly HttpClient _httpClient;
private HubConnection _hubConnection;
public PlaceService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public string NewPlaceName { get; set; }
public int NewPlaceId { get; set; }
public event Action OnChange;
}
前面的代码为HttpClient和HubConnection定义了两个私有字段。稍后我们将使用这些字段来调用方法。PlaceService构造函数将HttpClient对象作为类的依赖项,并分配_httpClient字段。在运行时,HttpClient对象将由 DI 容器解析。
一旦应用从Hub接收到新添加的记录,NewPlaceName和NewPlaceId属性将被填充。OnChange事件是 C#中的一种特殊类型的委托,允许您在某个操作引发事件时订阅它。
现在我们来实现订阅Hub的SignalR配置。继续并在PlaceService类中附加以下代码:
public async Task InitializeSignalR()
{
_hubConnection = new HubConnectionBuilder()
.WithUrl($”{_httpClient.BaseAddress.AbsoluteUri} PlaceApiHub”)
.Build();
_hubConnection.On<int, string>(“NotifyNewPlaceAdded”, (placeId, placeName) =>
{
UpdateUIState(placeId, placeName);
});
await _hubConnection.StartAsync();
}
public void UpdateUIState(int placeId, string placeName)
{
NewPlaceId = placeId;
NewPlaceName = placeName;
NotifyStateChanged();
}
private void NotifyStateChanged() => OnChange?.Invoke();
InitializeSignalR()方法负责通过设置HubConnection.WithUrl()方法创建与Hub的连接。我们使用了_httpClient.BaseAddress.AbsoluteUri的值来避免对 web API 端点的基本 URL 进行硬编码。稍后,当我们将PlaceService类注册到类型化的HttpClient实例时,我们将配置基本 URL。WithUrl参数的值实际上相当于https://localhost:44332/PlaceApiHub。如果您还记得的话,/PlaceApiHubURL 段就是我们在创建 API 项目之前配置的Hub路由。在下一行中,我们使用了HubConnection的On方法来收听NotifyNewPlaceAdded事件。当服务器向该事件广播数据时,会调用UpdateUIState(),设置NewPlaceId和NewPlaceName属性,然后最终调用NotifyStateChanged()方法触发OnChange事件。
接下来,让我们实现连接到 web API 端点的方法。附加以下代码:
public async Task<IEnumerable<Place>> GetPlacesAsync()
{
var response = await _httpClient.GetAsync(“/api/places”);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var jsonOption = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
var data = JsonSerializer. Deserialize<IEnumerable<Place>>(json, jsonOption);
return data;
}
public async Task UpdatePlaceAsync(Place place)
{
var response = await _httpClient.PutAsJsonAsync( “/api/places”, place);
response.EnsureSuccessStatusCode();
}
GetPlacesAsync()方法调用/api/placesHTTPGET端点获取数据。请注意,在将结果反序列化到Place模型时,我们正在传递JsonSerializerOptions,并将PropertyNameCaseInsensitive设置为true。这是为了正确映射Place模型中的属性,因为来自 API 调用的默认 JSON 响应是驼峰大小写格式。如果不设置此选项,您将无法使用数据填充Place模型属性,因为格式为 Pascal 大小写。
UpdatePlaceAsync()方法非常简单。它将Place模型作为参数,然后调用 API 将更改保存到数据库中。如果 HTTP 响应不成功,EnsureSuccessStatusCode()方法调用将引发异常。
接下来,将以下条目添加到appSettings.json文件中:
“PlaceApiBaseUrl”: “https://localhost:44332”
在appSettings.json中定义公共配置值是一种很好的做法,可以避免在 C#代码中硬编码任何静态值。
注意:ASP.NET Core 项目模板将同时生成appSettings.json和appSettings.Development.json文件。如果要在不同的环境中部署应用,可以利用配置并针对每个环境创建特定的配置文件。对于本地开发,您可以将所有本地配置值放在appSettings.Development.json文件中,将常用配置放在appSettings.json文件中。在运行时,根据您的应用正在运行的环境,框架将自动用您在特定于环境的配置文件中配置的值覆盖您在appSettings.json文件中配置的任何值。有关更多信息,请参阅本章的进一步阅读部分。
这项工作的最后一步是在IServiceCollection中注册PlaceService。继续并将以下代码添加到Startup类的ConfigureServices()方法中:
services.AddHttpClient<PlaceService>(client =>
{
client.BaseAddress = new Uri(Configuration[“PlaceApiBaseUrl”]);
});
前面的代码在 DI 容器中注册了一个类型化的HttpClientFactory实例。请注意,BaseAddress值是通过Configuration对象从appSettings.json中提取的。
实现应用状态
Blazor 应用由组件组成,为了在依赖组件中发生的更改之间进行有效通信,我们需要实现某种状态容器来跟踪更改。在数据文件夹下创建一个名为AppState.cs的新类,并复制以下代码:
public class AppState
{
public Place Place { get; private set; }
public event Action OnChange;
public void SetAppState(Place place)
{
Place = place;
NotifyStateChanged();
}
private void NotifyStateChanged() => OnChange?.Invoke();
}
前面的代码由属性、事件和方法组成。Place属性用于保存已修改的当前Place模型。OnChange事件用于在应用状态发生变化时触发某些逻辑。SetAppState()方法处理组件的当前状态。在这里,我们设置属性来跟踪更改,并调用NotifyStateChanged()方法来调用OnChanged事件。
下一步是将AppState类注册为服务,以便我们可以将其注入任何组件。继续并将以下代码添加到Startup类的ConfigureServices()方法中:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<AppState>();
//removed other services for brevity
}
前面的代码将AppState类注册为 DI 容器中的作用域服务,因为我们希望为每个 web 请求创建此服务的实例。
现在,我们已经具备了构建 UI 所需的功能:一个用于消费数据的服务和一个用于跟踪组件状态的服务。现在,让我们进入下一步,开始为应用构建 UI。
创建 Razor 组件
我们将将页面实现拆分为组件。说到这里,我们现在将创建以下 Razor 组件:
Main.razorViewTouristSpot.razorEditTouristSpot.razor
下图显示了我们将如何布局网页的图形表示:

图 6.4–主要布局
Main.razor组件将包含三个主要部分,用于显示各种数据表示。这些部分只是组件中的<div>元素。在特色部分下,我们将把ViewTouristSpot.razor组件作为子组件呈现给Main.razor组件。ViewTouristSpot.razor将包含EditTouristSpot.razor作为子组件。
现在您已经了解了页面的外观,让我们开始构建所需的组件。
组成 EditTouristSpot 组件
让我们开始创建内部子组件。在页面文件夹下创建一个名为Spots的新文件夹。右键点击放置文件夹,然后选择添加|Razor 组件。应出现一个窗口对话框,供您命名组件。在本例中,只需将名称设置为EditTouristSpot.razor,然后单击添加。删除生成的代码,因为我们将用代码实现替换它。
Razor 组件通常分为三个主要部分:
- 第一部分用于声明调用方法和成员所需的类和服务引用。
- 第二部分是通过结合 HTML、CSS 和 C#,使用 Razor 语法构建实际的 UI。
- 第三部分用于处理
@code{}块中包含的任何用户交互逻辑。
下面是一个典型组件组成的快速总结:
@*Routing, Namespace, Class and Service references goes here*@
@*HTML generation and UI construction goes here*@
@*UI logic and C# code block goes here*@
让我们开始集成第一部分。添加以下代码:
@using BlazorServer.Web.Data
@inject PlaceService _placeService
@inject AppState _appState
前面的代码使用@using和@injectRazor 指令引用 Blazor 组件中的服务器端类和服务。这使我们能够访问可用的成员和方法。对于这个特定的示例,声明@using BlazorServer.Web.Data引用允许我们访问在该名称空间中定义的Place类。@inject指令也是如此。当注入AppState和PlaceService服务时,它允许我们访问它们在标记中公开的所有方法。
现在,让我们整合第二部分。附加以下代码:
@if (IsReadOnlyMode)
{
<ViewTouristSpot Place=”Place” />
}
else
{
<EditForm Model=”@Place” OnValidSubmit=”HandleValidSubmit”>
<div class=”card”>
<div class=”card-body”>
<DataAnnotationsValidator />
<ValidationSummary />
Name:
<InputText class=”form-control”
@bind-Value=”Place.Name” />
Location:
<InputText class=”form-control”
@bind-Value=”Place.Location” />
About:
<InputTextArea class=”form-control”
@bind-Value=”Place.About” />
<br />
<button type=”submit” class=”btn btn-outline- primary”>Save</button>
<button type=”button” class=”btn btn-outline- primary” @onclick=”UndoChanges”>Cancel </button>
</div>
</div>
</EditForm>
}
前面的代码称为剃刀代码块。Razor 代码块通常以@符号开头,并用大括号{}括起来。if-else语句根据@code部分中定义的IsReadOnlyMode布尔属性确定要在浏览器中呈现的 HTML 块。默认情况下,它被设置为false,因此else部分中的 HTML 块将被计算并显示编辑表单。否则,它将呈现ViewTouristSpot.razor组件,使显示器返回只读状态。
在只读状态下,我们将Place对象作为参数传递给ViewTouristSpot组件,这样它就可以在不重新调用 API 的情况下显示数据。请记住,ViewTouristSpot组件还不存在,我们将在下一节中创建它。在编辑状态下,我们使用了EditForm组件来利用的内置特性和表单验证。EditForm组件采用待验证的模型。在本例中,我们将Place对象作为模型传递,并将HandleValidSubmit()方法连接到OnValidSubmit事件处理程序。我们还使用了各种内置组件,如DataAnnotationsValidator、ValidationSummary、InputText和InputTextArea来处理输入验证和模型属性绑定。在本例中,我们使用双向数据绑定将Place属性绑定到使用@bind-Value属性的输入元素。当点击type=”submit”的 HTML<input>时,EditForm组件将在浏览器中呈现为 HTML<form>元素,并提交所有表单值。当点击Save按钮时,触发DataAnnotationsValidator组件并检查所有验证是否通过。如果您还记得,在本章的创建模型部分,我们只验证了需要的Name、Location和About属性,如果这些属性中的任何一个为空,则不会触发HandleValidSubmit()方法。
表单使用 Boostrap 4 CSS 类来定义组件的外观。引导是创建任何 ASP.NET Core web 框架时默认模板的部分,您可以看到 CSS 文件位于wwwroot/css/bootstrap文件夹下。
现在,让我们集成这个组件的最后一部分。附加以下代码:
@code {
[Parameter] public Place Place { get; set; }
private Place PlaceCopy { get; set; }
bool IsReadOnlyMode { get; set; } = false;
}
前面的代码称为C#代码块。@code指令是.razor文件所独有的,允许您向组件添加 C#方法、属性和字段。您可以将代码块视为 Razor Pages 中的代码隐藏文件(cshtml.cs)或 MVC 中的Controller类,您可以在其中基于 UI 交互实现 C#代码逻辑。
Place属性由[Parameter]属性修饰,该属性带有public访问修饰符,以允许父组件为此属性设置值。PlaceCopy属性是一个 holder 属性,包含从父组件传递的原始值。在这种情况下,父组件为ViewTouristSpot.razor。IsReadOnlyMode属性是一个布尔标志,用于确定要呈现的 HTML 块。
让我们继续实现该组件所需的方法。在@code{}块中追加以下代码:
protected override void OnInitialized()
{
PlaceCopy = new Place
{
Id = Place.Id,
Name = Place.Name,
Location = Place.Location,
About = Place.About,
Reviews = Place.Reviews,
ImageData = Place.ImageData,
LastUpdated = Place.LastUpdated
};
}
OnInitialized()方法是 Blazor 框架的一部分,它允许我们重写它来执行某些操作。此方法在组件初始化期间触发,是配置对象初始化和分配的理想场所。您会注意到,这是我们将原始Place模型中的属性值分配给名为PlaceCopy的新Place对象的地方。我们保持Place对象的原始状态的主要原因是我们想在取消编辑时将数据重置为其默认状态。我们可以将取消操作的IsReadOnlyMode标志设置为true。但是,在切换回只读状态时,仅此操作不会将值重置为原始状态。原因是我们在Place模型中使用了双向数据绑定,对表单所做的任何属性更改都将保留。
双向数据绑定的过程如下所示:
- 当
Place模型中的属性从服务器更新时,UI 中的输入元素会自动反映更改。 - 当 UI 元素更新时,更改也会传播回
Place模型。
如果您不想保持Place模型的原始状态,可以注入NavigationManager类,然后使用以下代码简单地重定向到Main.razor组件:
NavigationManager.NavigateTo(“/main”, true);
前面的代码是切换到只读状态的最快、最简单的方法。但是,这样做会导致页面重新加载并再次调用 API 以获取数据,这可能会很昂贵。
让我们继续并在@code{}块中附加以下代码:
private void NotifyStateChange(Place place)
{
_appState.SetAppState(place);
}
NotifyStateChange()方法将Place模型作为参数。这就是我们调用AppState的SetAppState()方法来通知变更的主要组件的地方。这样,当我们修改表单或执行更新时,主组件可以执行某些操作来对其进行操作;例如,刷新数据或更新主组件中的某些 UI。
接下来,在@code{}块中追加以下代码:
protected async Task HandleValidSubmit()
{
await _placeService.UpdatePlaceAsync(Place);
IsReadOnlyMode = true;
NotifyStateChange(Place);
}
点击Save按钮,在没有发生模型验证错误时,会触发前面代码中的HandleValidSubmit()方法。此方法调用PlaceService的UpdatePlaceAsync()方法,并调用 API 更新Place记录。
最后,在@code{}块中追加以下代码:
private void UndoChanges()
{
IsReadOnlyMode = true;
if (Place.Name.Trim() != PlaceCopy.Name.Trim() ||
Place.Location.Trim() != PlaceCopy.Location.Trim() ||
Place.About.Trim() != PlaceCopy.About.Trim())
{
Place = PlaceCopy;
NotifyStateChange(PlaceCopy);
}
}
点击Cancel按钮会触发前面代码中的UndoChanges()方法。这就是我们在修改任何Place属性后返回PlaceCopy对象的值的地方。
让我们转到下一步,创建用于显示数据只读状态的ViewTouristSpot组件。
组成 ViewTouristSpot 组件
继续并在Spots文件夹中创建一个新的 Razor 组件,并将其命名为ViewTouristSpot.razor。替换生成的代码,使其如下所示:
@using BlazorServer.Web.Data
@if (IsEdit)
{
<EditTouristSpot Place=”Place” />
}
else
{
<div class=”card”>
<img class=”card-img-top” src=”@Place.ImageData” alt=”Card image cap”>
<div class=”card-body”>
<h5 class=”card-title”>@Place.Name</h5>
<h6 class=”card-subtitle mb-2 text-muted”>
Location: <b>@Place.Location</b>
Reviews: @Place.Reviews
Last Updated: @Place.LastUpdated. ToShortDateString()
</h6>
<p class=”card-text”>@Place.About</p>
<button type=”button” class=”btn btn-outline- primary”
@onclick=”(() => IsEdit = true)”>
Edit
</button>
</div>
</div>
}
@code {
[Parameter] public Place Place { get; set; }
bool IsEdit { get; set; } = false;
}
前面的代码中确实没有太多内容。因为这个组件是只读的,所以这里没有复杂的逻辑。就像在EditTouristSpot.razor文件中一样,我们还实现了一个if-else语句来确定要呈现哪个 HTML 块。在@code部分,我们只有两个属性;Place属性用于将模型传递给EditTouristSpot组件。IsEdit布尔属性用作呈现 HTML 的标志。我们仅在单击Edit按钮时将此属性设置为true。
构成主要组成部分
现在我们已经熟悉了用于编辑和查看数据的组件,我们需要做的最后一件事是创建主组件,将它们包含在单个页面中。让我们继续在Pages文件夹下创建一个新的 Razor 组件,并将其命名为Main.razor。现在,将生成的代码替换为以下代码:
@page “/main”
@using BlazorServer.Web.Data
@using BlazorServer.Web.Pages.Spots
@inject PlaceService _placeService
@inject AppState _appState
@implements IDisposable
前面的代码使用@page指令定义了一个新路由。在运行时,/main路由将添加到路由数据收集中,使您能够导航到此路由并呈现其关联组件。我们使用了@using指令从服务器引用类,并使用@inject指令引用服务。我们还使用了@implements指令来实现一次性组件。稍后我们将了解如何使用此功能。
现在,让我们继续编写main组件。附加以下代码:
@if (Places == null)
{
<p><em>Loading...</em></p>
}
else
{
<div class=”container”>
<div class=”row”>
<div class=”col-8”>
<h3>Featured Tourist Spot</h3>
<ViewTouristSpot Place=”Place” />
</div>
<div class=”col-4”>
<div class=”row”>
<h3>What’s New?</h3>
<div class=”card” style=”width: 18rem;”>
<div class=”card-body”>
<h5 class=”card-title”>@_ placeService.NewPlaceName</h5>
</div>
</div>
</div>
<div class=”row”>
<h3>Top Places</h3>
<div class=”card” style=”width: 18rem;”>
<div class=”card-body”>
<ul>
@foreach (var place in Places)
{
<li>
<a href=” javascript:void(0)”
@onclick=”(() => ViewDetails( place.Id))”>
@place.Name
</a>
</li>
}
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
}
前面的代码负责呈现 HTML。我们再次使用引导CSS 来设置布局。布局基本上由两列的<div>元素组成。在第一列中,我们呈现ViewTouristSpot组件,并将Place模型作为参数传递给该组件。我们将在下一节中看到该模型是如何填充的。第二列呈现两行。第一行显示来自PlaceService的NewPlaceName属性,第二列显示使用<ul>HTML 元素显示的位置列表。在<ul>标记中,我们已经使用@符号开始处理 C#代码中的数据。foreach关键字是 C#保留关键字之一,用于迭代集合中的数据。在foreach块中,我们构建了要在<li>标签中显示的项目。在本例中,Place模型的Name属性使用隐式表达式呈现。
为了完成Main.razor组件,让我们实现服务器端逻辑来处理用户交互和应用状态。继续并附加以下代码:
@code {
private IEnumerable<Place> Places;
public Place Place { get; set; }
}
前面的代码定义了两个属性,用于存储要查看的位置列表和当前位置。
接下来,在@code{}块中追加以下代码:
protected override async Task OnInitializedAsync()
{
await _placeService.InitializeSignalR();
Places = await _placeService.GetPlacesAsync();
Place = Places.FirstOrDefault();
_placeService.NewPlaceName = Place.Name;
_placeService.NewPlaceId = Place.Id;
_placeService.OnChange += HandleNewPlaceAdded;
_appState.OnChange += HandleStateChange;
}
在OnInitializedAsync()方法中,我们调用了PlaceService的InitializeSignalR()方法来配置信号机和Hub连接。我们还填充了组件中的每个属性。Places属性包含来自GetPlacesAsync()方法调用的数据。在后台,此方法调用 API 调用以获取数据。Places属性用于显示顶部位置部分中的位置列表。另一方面,Place属性包含来自Places集合的第一个结果,用于显示ViewTouristSpot组件中的数据。我们还设置了PlaceService的NewPlaceName和NewPlaceId属性,以便新增内容部分有一个默认显示。我们还将PlaceService和AppState服务中的OnChange事件连接到每个相应的方法。
接下来,在@code{}块中追加以下代码:
private async void HandleNewPlaceAdded()
{
Places = await _placeService.GetPlacesAsync();
StateHasChanged();
}
当服务器向Hub发送事件时,将调用HandleNewPlaceAdded()方法。此过程在通过 APIPOST请求添加新记录时完成。此方法负责更新组件中的数据,以实时反映新记录。
接下来,在@code{}块中追加以下代码:
private async void HandleStateChange()
{
Places = await _placeService.GetPlacesAsync();
Place = _appState.Place;
if (_placeService.NewPlaceId == _appState.Place.Id)
{
_placeService.NewPlaceName = _appState.Place.Name;
}
StateHasChanged();
}
前面代码中的HandleStateChange()方法负责使Models状态保持最新。您可以在这个方法中看到,当状态发生更改时,我们正在重新填充Places、Place和NewPlaceName属性。请注意,只有当NewPlaceId与正在修改的Place记录匹配时,我们才更新NewPlaceName值。这是因为我们不想在编辑非新记录时更改此值。StateHasChanged()调用负责使用新状态重新呈现组件。
接下来,在@code{}块中追加以下代码:
private void ViewDetails(int id)
{
Place = Places.FirstOrDefault(o => o.Id.Equals(id));
}
前面代码中的ViewDetails()方法采用整数作为参数。此方法负责基于Id更新当前Place模型。
最后,在@code{}块中追加以下代码:
public void Dispose()
{
_appState.OnChange -= StateHasChanged;
_placeService.OnChange -= StateHasChanged;
}
在前面的代码中,我们将在调用Dispose()方法时取消订阅OnChange事件。当组件从 UI 中移除时,会自动调用Dispose()方法。务必将组件的StateHasChanged方法与OnChange事件解除挂钩,以避免潜在的内存泄漏,这一点非常重要。
更新导航菜单组件
现在,让我们将/main路线添加到现有的导航组件中。继续并打开文件,该文件位于Shared文件夹下。在<ul>元素中追加以下代码:
<li class=”nav-item px-3”>
<NavLink class=”nav-link” href=”main”>
<span class=”oi oi-list-rich” aria-hidden=”true”> </span> Tourist Spots
</NavLink>
</li>
前面的代码从现有菜单中添加了一个旅游景点链接。这使我们能够轻松地导航到主组件页面,而无需在浏览器中手动键入路线。
运行应用
VisualStudio 内置的许多强大功能之一是,它为我们提供了在本地机器上同时运行多个项目的能力。如果没有此功能,我们将不得不在 web 服务器中部署所有应用,其中每个应用都可以相互通信。否则,我们的 Blazor web 应用将无法连接到 web API。
要在 Visual Studio 中同时运行多个项目,请执行以下步骤:
-
右键点击解决方案项目,选择设置启动项目。
-
Select the Multiple startup projects radio button, as shown in the following screenshot:
![Figure 6.5 – Setting multiple startup projects]()
图 6.5–设置多个启动项目
-
选择开始作为两个项目的操作。
-
点击应用,然后点击确定。
现在,使用Ctrl+F5构建并运行应用。在导航侧栏菜单中,点击旅游景点链接,Main组件页面应显示如下截图所示:

图 6.6–主页面
点击编辑按钮将显示EditTouristSpot组件,如下图所示:

图 6.7–显示编辑模式的主页面
在前面的屏幕截图中,名称属性被修改。单击取消按钮将放弃更改并返回默认视图。点击保存将更新我们内存数据库中的记录,更新状态,并反映对主组件的更改,如下图所示:

图 6.8–显示只读模式的主页面
您还可以从顶部位置部分选择任何项目,这将在页面上显示相应的详细信息。例如,点击奥斯陆宿务项目将更新页面,如下所示:

图 6.9–显示只读模式的主页面
请注意,除了新增内容外,所有详细信息都已更新?节。这是有意的,因为我们只想在数据库中发布新记录时更新它。我们将在下一节中看到本节将如何更新。
如果你成功了,恭喜你!您刚刚让您的第一个 Blazor web 应用与连接到 API 的实时数据一起运行!现在,让我们继续玩下去,创建一个 Blazor WebAssembly WASM(应用),我们可以在其中提交新的旅游景点记录,并实时反映 Blazor 服务器应用中的变化。
创建 Blazor Web 组装项目
在上一个项目中,我们学习了如何创建具有基本功能的 web 应用,例如通过 web API 调用获取和更新记录。在本项目中,我们将构建前端渐进式 Web 应用(PWA,创造新记录。此过程通过调用 API 端点发布数据来执行,并向Hub发送事件,以便在提交新记录时实时自动更新 Blazor 服务器 UI。
下面是一个演示该过程如何工作的尝试:

图 6.10–实时数据更新流程
上图显示了实时功能如何工作的高级过程。这些步骤几乎是不言自明的,它应该让您更好地理解每个应用如何相互连接。不用再多说了,让我们开始构建最后一个项目来完成整个应用。
继续,在现有项目解决方案中添加一个新的 Blazor WebAssembly 项目。要做到这一点,只需右键点击解决方案,然后选择添加****新项目。在窗口对话框中,选择Blazor App,然后点击下一步。将项目名称设置为BlazorWasm.PWA,然后点击创建。
在下一个对话框中,选择Blazor WebAssembly App,然后选中Progressive Web Application复选框,如下图所示:

图 6.11–创建新的 Blazor WASM 项目
单击创建让 Visual Studio 生成默认模板。
Blazor WebAssembly 项目的项目结构与 Blazor Server 有点类似,除了以下几点:
- 它没有
Startup.cs文件。这是因为 Blazor WASM 项目的配置不同,并且使用自己的主机运行应用。 Progam.cs文件现在包含以下代码:
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>(“app”);
builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
}
在前面的代码中,我们可以看到它使用WebAssemblyHostBuilder而不是使用典型的 ASP.NET CoreIHostBuilder来配置 webHost。它还将HttpClient配置为BaseAddress设置为HostEnvironment.BaseAddress,这是应用本身正在运行的主机地址,例如localhost:<port>。
- 在页面文件夹中没有
_Host.chtml文件。如果您还记得,在 Blazor 服务器项目中,_Host.chtml文件是应用的主要入口点,它在其中引导App.razor组件。在 Blazor WASM 中,App.razor被添加到应用启动中,正如您在Program.cs文件中看到的那样。 - 它没有为默认的
Weatherforecast服务配置样本数据的数据文件夹。样本数据现在移动到wwwroot/sample data文件夹下的weather.json文件中。 - wwwroot中还添加了其他一些新文件,如
index.html、manifest.json和service-worker.js。index.html实际上取代了_Host.chtml文件,该文件包含应用的主 HTML 文档。您可以看到该文件包含<head>和<body>标记,以及呈现<app>组件、CSS 和 JavaScript 框架。manifest.json和service-worker.js文件使 Blazor WASM 应用能够变成 PWA。
我非常确定 Blazor 服务器和 WebAssembly 之间还有很多其他的区别,但是列表中突出显示的项目是关键的区别。
创建模型
现在,让我们开始添加此项目所需的功能。在项目根目录中创建一个名为Dto的新文件夹。在Dto文件夹中,添加一个名为CreatePlaceRequest.cs的新类,并复制以下代码:
using System.ComponentModel.DataAnnotations;
namespace BlazorWasm.PWA.Dto
{
public class CreatePlaceRequest
{
[Required]
public string Name { get; set; }
[Required]
public string Location { get; set; }
[Required]
public string About { get; set; }
[Required]
public int Reviews { get; set; }
public string ImageData { get; set; }
}
}
前面的代码定义了一个包含一些属性的类。请注意,该类类似于 web API 中的Place类,除了之外,我们使用了数据****注释,通过使用[Required]属性装饰一些属性。此属性确保如果属性为空,则不会将其发布到数据库。
让我们继续下一步,创建用于向数据库添加新记录的组件。
构成索引组件
现在,导航到Index.razor组件。删除其中的现有代码并添加以下代码:
@page “/”
@using Dto
@inject HttpClient client
前面的代码使用@page指令设置到根目录的路由。下一行使用@using指令声明对 C#命名空间的引用。我们将使用Dto名称空间访问一个类,并使用该类中属性的值填充该组件。最后一行注入了一个HttpClient对象,以便我们与 web API 进行通信。
接下来,附加以下代码块:
<h1>Submit a new Tourist Destination Spot</h1>
<EditForm Model=”@NewPlace” OnValidSubmit=”HandleValidSubmit”>
<div class=”card” style=”width: 30rem;”>
<div class=”card-body”>
<DataAnnotationsValidator />
<ValidationSummary />
Browse Image:
<InputFile OnChange=”HandleSelection” />
<p class=”alert-danger”>@errorMessage</p>
<p>@status</p>
<p>
<img src=”@imageData” style=”width:300px; height:200px;”>
</p>
Name:
<InputText class=”form-control” id=”name” @bind- Value=”NewPlace.Name” />
Location:
<InputText class=”form-control” id=”location” @ bind-Value=”NewPlace.Location” />
About:
<InputTextArea class=”form-control” id=”about” @ bind-Value=”NewPlace.About” />
Review:
<InputNumber class=”form-control” id=”review” @ bind-Value=”NewPlace.Reviews” />
<br/>
<button type=”submit” class=”btn btn-outline- primary oi-align-right”>Post</button>
</div>
</div>
</EditForm>
前面的代码是 HTML 代码,它使用输入元素和上载图像的按钮呈现表单。它还使用一个EditForm组件来处理表单提交和模型验证。我们不打算详细说明代码是如何工作的,因为在为 Blazor 服务器项目构建组件时,我们已经在上一节中介绍了这一点。
在本例中,我们使用InputFileBlazor 组件上传图像并配置连接到HandleSelection方法的OnChange事件。默认情况下,InputFile组件只允许选择单个文件。要支持多文件选择和上传,请设置multiple属性,如下代码段所示:
<InputFile OnChange=”HandleSelection” multiple />
有关InputFile组件的更多信息,请参阅本章进一步阅读部分。
让我们继续实现服务器端代码逻辑。附加以下代码:
@code {
string status;
string imageData;
string errorMessage;
}
前面的代码定义了组件 UI 中需要的几个私有字段。status字段是存储上传状态文本的变量。imageData用于存储encodedimage数据,errorMessage用于存储错误文本。
接下来,在@code{}块中追加以下代码:
async Task HandleSelection(InputFileChangeEventArgs e)
{
errorMessage = string.Empty;
int maxFileSize = 2 * 1024 * 1024;
var acceptedFileTypes = new List<string>() { “img/png”, “img/jpeg”, “img/gif” };
var file = e.File;
if (file != null)
{
if (!acceptedFileTypes.Contains(file.ContentType))
{
errorMessage = “File is invalid.”;
return;
}
if (file.Size > maxFileSize)
{
errorMessage = “File size exceeds 2MB”;
return;
}
var buffer = new byte[file.Size];
await file.OpenReadStream().ReadAsync(buffer);
status = $”Finished loading {file.Size} bytes from {file.Name}”;
imageData = $”data:{file.ContentType};base64,{Convert. ToBase64String(buffer)}”;
}
}
前面代码中的HandleSelection()方法以InputFileChangeEventArgs为参数。在这种方法中,通过读取e.File属性,我们只允许上传一个文件,而不允许上传多个文件。如果您接受多个文件,则使用e.GetMultipleFiles()方法。我们还为最大文件大小和文件类型定义了两个预验证值。在本例中,我们只允许最大文件大小为 2MB,并且只接受上传的.PNG、.JPEG 和.GIF 文件类型。然后,我们执行一些验证检查,如果不满足任何条件,则显示错误。如果所有条件都满足,我们将上传的文件复制到一个流中,并将结果字节转换为Base64String,这样我们就可以将图像数据设置为<img>HTML 元素。
现在,在@code{}块中追加以下代码:
private CreatePlaceRequest NewPlace = new CreatePlaceRequest();
async Task HandleValidSubmit()
{
NewPlace.ImageData = imageData;
var result = await client.PostAsJsonAsync( “https://localhost:44332/api/places”, NewPlace);
}
点击Post按钮时,如果没有发生模型验证错误,则会调用前面代码中的HandleValidSubmit()方法。此方法获取NewPlace对象并将 API 调用传递给它以执行HTTP POST。
就这样!现在,让我们试着运行应用。
运行应用
现在,将 Blazor WASM 项目作为启动项目,然后单击Ctrl+F5运行应用。您应该看到运行每个应用的三个浏览器选项卡。您可以最小化运行 web API 的选项卡,因为我们不需要对它做任何事情。现在,寻找 Blazor WASM 标签。
要将 Blazor WebAssembly 页面变成 PWA,只需单击浏览器导航栏中的+标志,如下图所示:

图 6.12–Blazor WASM
单击+标志将提示一个对话框,询问您是否要在桌面或移动设备上安装 Blazor 作为独立应用,如以下屏幕截图所示:

图 6.13–将 Blazor WASM 安装为 PWA
点击安装将在您的桌面或移动设备上创建一个图标,就好像它是一个已安装的常规本机应用,并将网页变成一个没有 URL 栏的窗口,如下所示:

图 6.14–Blazor WASM 作为 PWA
很酷!
现在,并排打开 Blazor 服务器应用和 Blazor PWA 应用,让您了解实时更新的工作原理:

图 6.15–Blazor 服务器和 PWA 并排
现在,浏览图像并输入所需字段以提交新的Place记录。当您点击提交时,您会注意到 Blazor 服务器应用(前面屏幕截图中的右侧窗口)中的有什么新功能?和顶部位置部分自动更新为新添加的Place名称,无需刷新页面。下面是它的一个示例:

图 6.16–Blazor 服务器和 PWA 实时通信
在前面的截图中,点击Post按钮后,大峡谷名称会实时自动出现在 Blazor 服务器的 web UI 中。您可以在此处实时查看:
卸载 PWA 应用
要从本地计算机或设备上完全卸载 PWA 应用,请确保退出 IIS Express 中运行的所有应用。您可以访问 Windows 计算机任务栏右下角的 IIS Express 管理器,如下所示:

图 6.17–IIS Express manager
退出所有应用后,您可以卸载 PWA 应用,就像您通常卸载机器上的应用一样。
总结
在本章中,我们通过进行一些实际编码,了解了 BlazorWeb 框架的不同风格。我们学习了如何在 Blazor 中轻松构建一个强大的 web 应用,只需应用我们的 C#技能,而无需编写 JavaScript,就可以与其他 ASP.NET Core 技术堆栈协同工作。我们看到了如何轻松地集成.NET 中已有的特性和功能,例如实时功能。我们还学习了如何执行基本表单数据绑定、状态管理、路由,以及如何与后端 REST API 交互以使用和传递数据。在构建实际应用时,必须学习这些基本概念和基础知识是至关重要的。
在下一章中,您将深入探讨使用真实数据库的 web API 和数据访问。
进一步阅读
- ASP.NET Core Blazor 简介:https://docs.microsoft.com/en-us/aspnet/core/blazor
- ASP.NET Core Blazor 托管模型配置:https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/additional-scenarios
- 在 ASP.NET Core 中使用多种环境:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/environments
- 可枚举类:https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable
- Razor 组件:https://docs.microsoft.com/en-us/aspnet/core/blazor/components
- Blazor 级联值和参数:https://docs.microsoft.com/en-us/aspnet/core/blazor/components/cascading-values-and-parameters
- Blazor 生命周期:https://docs.microsoft.com/en-us/aspnet/core/blazor/components/lifecycle
- Blazor 路由:https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/routing
- Blazor 调试:https://docs.microsoft.com/en-us/aspnet/core/blazor/debug
- WebAssembly:https://webassembly.org/
- 理解
InputFile组件:https://docs.microsoft.com/en-us/aspnet/core/blazor/file-uploads
七、API 和数据访问
在现实场景中,无论是移动应用、桌面应用、服务应用还是 web 应用,它们都严重依赖应用编程接口(API)与系统交互以提交或获取数据。API 通常充当客户端应用和数据库之间的网关,以执行系统之间的任何数据操作。通常,API 向客户端提供有关如何与系统交互以执行数据事务的说明和特定格式。因此,API 和数据访问共同实现两个主要目标:服务和获取数据。
以下是我们将在本章中讨论的主要主题列表:
- 了解什么是 ORM 和实体框架核心
- 审查 EF Core 支持的不同设计工作流
- 学习数据库优先开发
- 学习代码优先开发和迁移
- 学习 LINQ 的基础知识,根据概念模型查询数据
- 回顾 ASP.NET Core API 是什么
- 构建 Web API,实现最常用的 HTTP 数据服务方法
- 使用 Postman 测试 API
在本章中,我们将学习在实体框架(EF核心中使用真实数据库的不同方法。我们将了解如何在现有数据库中使用 EF 核心,以及如何使用 EF 核心代码优先方法实现与真实数据库对话的 API。我们将研究 ASP.NET Core Web API 与实体框架核心,以在 SQL Server 数据库中执行数据操作。我们还将学习如何实现用于公开某些 API 端点的最常用 HTTP 方法(谓词)。
了解 ASP.NET Core 不仅限于实体框架核心和 SQL Server 非常重要。您始终可以使用您喜欢的任何数据访问框架。例如,您可以始终使用 Dapper、NHibernate,甚至使用旧的普通 ADO.NET 作为数据访问机制。如果愿意,还可以使用 MySQL 或 Postgres 作为数据库提供程序。
技术要求
本章使用 Visual Studio 2019 演示如何构建不同的应用。为了简洁起见,省略了本章中演示的一些代码片段。确保在处检查源代码 https://github.com/PacktPublishing/ASP.NET-Core-5-for-Beginners/tree/master/Chapter%2007/Chapter_07_API_EFCore_Examples 。
请访问以下链接查看 CiA 视频:https://bit.ly/3qDiqYY
需要对数据库、ASP.NET Core 和 C#有基本的了解,因为在本章中我们将不介绍它们的基础知识。
理解实体框架核心
在软件工程领域,大多数应用都需要数据库来存储数据。因此,我们都需要代码来读取/写入存储在数据库中的数据。为数据库创建和维护代码是一项乏味的工作,对于开发者来说,这是一项真正的挑战。这就是像实体框架一样的对象关系映射器(ORMs)发挥作用的地方。
实体框架核心是一种 ORM 和一种数据访问技术,它使 C#开发者无需手动编写 SQL 脚本即可与数据库交互。像 EF Core 这样的 ORM 通过处理.NET 对象而不是直接与数据库模式交互,帮助您快速构建数据驱动的应用。这些.NET 对象只是类,它们通常被称为实体。有了 EF Core,C#开发者可以利用他们现有的技能,利用语言集成查询(LINQ的强大功能,根据概念实体模型操作数据集,否则简称为模型。从这里开始,我们将使用术语模型,如图 7.1 所示。

图 7.1-EF 核心高级流程
上图描述了使用 EF Core 与数据库交互的过程。在传统的 ADO.NET 中,通常手动编写 SQL 查询以执行数据库操作。虽然性能会因编写查询的方式而异,但 ADO.NET 方式带来了比 ORMs 更高的性能优势,因为您可以将 SQL 查询直接注入代码并在数据库中运行。但是,这会导致代码变得难以维护,因为任何 SQL 查询更改都会导致应用代码的更改;除使用存储过程外。此外,调试代码可能会很痛苦,因为编写 SQL 查询时需要处理一个简单的字符串,任何打字错误或语法错误都很容易被忽略。
使用 EF Core,您不必担心自己编写 SQL 脚本。相反,您将使用 LINQ 查询强类型对象,并让框架处理其余的事务,例如生成和执行 SQL 查询。
请记住,EF Core 不仅限于 SQL Server 数据库。该框架支持各种可以与应用集成的数据库提供程序,如 Postgres、MySQL、SQLite、Cosmos 和其他许多数据库提供程序。
审查 EF 核心设计工作流程
EF Core 支持两种主要设计工作流:数据库优先方法和代码优先方法。
以下图 7.2描述了两种设计工作流之间的差异:

图 7.2–EF 核心设计工作流程
在上图中,我们可以看到数据库第一工作流从现有数据库开始,EF Core 将根据数据库模式生成模型。另一方面,代码优先工作流从编写模型开始,EF Core 将通过 EF 迁移生成相应的数据库模式。迁移是一个保持模型和数据库模式同步而不丢失现有数据的过程。
下表概述了在构建应用时要考虑设计工作流的建议:

了解设计工作流之间的差异非常重要,以便您知道何时将它们应用于项目。
既然您已经了解了这两种设计工作流之间的区别,那么让我们继续下一节,学习如何通过动手编码练习实现每种方法。
学习数据库先行开发
在本节中,我们将构建一个.NET Core 控制台应用,探索数据库优先的方法,并了解如何从现有数据库创建实体模型(逆向工程)。
创建.NET Core 控制台应用
要创建新的.NET Core console 应用,请执行以下步骤:
- 打开Visual Studio 2019,选择新建项目。
- 选择控制台应用(.NET Core)项目模板。
- 点击下一步。在下一个屏幕上,将项目命名为
EFCore_DatabaseFirst。 - 单击创建让 Visual Studio 为您生成默认文件。
现在,我们将在应用中添加所需的实体框架核心包,以便使用数据库优先方法处理现有数据库。
集成实体框架核心
实体框架核心功能作为一个单独的 NuGet 包实现,以允许开发人员轻松集成应用所需的功能。
正如您可能已经从第 4 章Razor View Engine中了解到的;第五章Blazor 入门;以及第 6 章探索 Blazor Web 框架,在 Visual Studio 中添加 NuGet 包依赖项的方法有很多;您可以使用包管理器控制台(PMC)或NuGet 包管理器(NPM)。在这个练习中,我们将使用控制台。
默认情况下,PMC 窗口为enabled,您可以在 VisualStudio 的左下角找到它。
如果由于某种原因,您无法找到 PMC 窗口,您可以通过进入工具>NuGet Package Manager>Package Manager 控制台下的Visual Studio菜单手动导航到该窗口。
现在,让我们通过在控制台中分别运行以下命令来安装几个 NuGet 软件包:
PM> Install-Package Microsoft.EntityFrameworkCore.Tools
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer.Design -Pre
前面代码中的命令将在应用中作为依赖项安装 NuGet 软件包。-Pre命令指示安装实体框架核心包的最新预览版本。在本例中,截至撰写本文之时,SQL Server 和 Tools 包的当前版本为5.0.0,而SqlServer.Design包的当前版本为2.0.0-preview1-final。
现在我们已经安装了使用现有数据库所需的工具和依赖项,让我们进入下一步。
创建数据库
为了模拟使用现有数据库,我们需要从头创建一个数据库。在本例中,为了简单起见,我们将创建一个包含一些简单列的表。如果安装了 SQL Server Express,或者使用内置在 Visual Studio 中的本地数据库,则可以使用它。
要在 Visual Studio 中创建新数据库,请执行以下简单步骤:
-
进入查看>SQL Server 对象浏览器。
-
向下钻取到SQL Server>(localdb)\MSSQLLocalDB。
-
右键点击
Databases文件夹。 -
点击新增数据库。
-
命名为
DbFirstDemo并点击确定。 -
右键点击
DbFirstDemo数据库,选择新建查询。 -
复制以下 SQL 脚本:
CREATE TABLE [dbo].[Person] ( [Id] INT NOT NULL PRIMARY KEY IDENTITY(1,1), [FirstName] NVARCHAR(30) NOT NULL, [LastName] NVARCHAR(30) NOT NULL, [DateOfBirth] DATETIME NOT NULL ) -
运行脚本,它将在本地数据库中创建一个名为
Person的新表。
现在我们有了一个数据库,让我们进入下一节,创建.NET 类对象,以便使用 EF Core 处理数据。
从现有数据库生成模型
截至本文撰写之时,有两种方法可以从现有数据库生成模型。您可以使用 PMC 或.NET Core命令行界面(CLI命令)。让我们在下一节中看看如何做到这一点。
使用 Scaffold DbContext 命令
您需要做的第一件事是获取ConnectionString值,以便连接到数据库。您可以从 VisualStudio 中DbFirstDemo数据库的属性窗口获取此值。
现在导航回 PMC 并运行以下命令从现有数据库创建相应的Models:
PM> Scaffold-DbContext “INSERT THE VALUE OF CONNECTION STRING HERE” Microsoft.EntityFrameworkCore.SqlServer -o Db
前面代码中的Scaffold-DbContext命令是Microsoft.EntityFrameworkCore.Tools包的一部分,负责逆向工程过程。此过程将基于现有数据库创建一个DbContext和Model类。
我们在Scaffold-DbContext命令中传入了三个主要的参数:
- 连接字符串:第一个参数是指示如何连接到数据库的连接字符串。
- 提供程序:将使用对其执行连接字符串的数据库提供程序。在本例中,我们使用
Microsoft.EntityFrameworkCore.SqlServer作为提供者。 - 输出目录:
-o选项是–OutputDir的简写,可以指定要生成的文件的位置。在本例中,我们将其设置为Db。
使用 dotnet ef dbcontext scaffold 命令
从现有数据库生成Models的第二个选项是通过.NET Core CLI 使用 EF Core 工具。为了做到这一点,我们需要使用命令行提示符。在 Visual Studio 中,您可以转到工具>命令行>开发人员命令提示符。此过程将在解决方案文件(.sln所在的文件夹中启动一个命令提示窗口。由于我们需要在项目文件(.csproj所在的级别执行该命令,因此我们需要将目录向下移动一个文件夹。因此,在命令提示符中,执行以下操作:
cd EFCore_DatabaseFirst
前面的命令将设置项目文件所在的当前目录。
另一种方法是导航到 Visual Studio 外部的EFCore_DatabaseFirst文件夹,然后按Shift+右键单击并选择在此处打开命令窗口或在此处打开 PowerShell 窗口。此过程将直接打开项目文件目录中的命令提示符。
在命令提示符中,让我们首先运行以下命令来安装 EF Core CLI 工具:
Dotnet tool install-–global dotnet-ef
前面的代码将在您的机器上全局安装 EF Core tools。现在,运行以下命令:
dotnet ef dbcontext scaffold “INSERT THE VALUE OF CONNECTION STRING HERE” Microsoft.EntityFrameworkCore.SqlServer -o Db
前面的代码与使用Scaffold-DbContext命令非常相似,只是我们使用了dotnet ef dbcontext scaffold命令,这是特定于基于 CLI 的 EF 核心工具的。
两个选项都会给出相同的结果,并在Db文件夹中创建DbContext和Model类,如图 7.3所示:

图 7.3–EF 核心生成的文件
花点时间检查生成的每个文件,看看生成了什么代码。
当您打开DbFirstDemoContext.cs文件时,您可以看到该类被声明为partial class,并且它派生自DbContext类。DbContext是实体框架核心的主要需求。在本例中,DbFirstDemoContext类表示管理与数据库连接的DbContext,并提供各种功能,如构建模型、数据映射、更改跟踪、数据库连接、缓存、事务管理、查询和持久化数据。
您还将在DbFirstDemoContext类中看到以下代码:
public virtual DbSet<Person> People { get; set; }
前面的代码表示一个实体。实体被定义为表示模型的DbSet类型。EF Core 需要一个Entity,以便它可以读取、写入数据并将数据迁移到数据库。简单地说,DbSet<Person>表示名为Person的数据库表。现在,您不需要编写 SQL 脚本来执行数据库操作,如insert、update、fetch或delete,而只需对名为People的DbSet执行数据库操作,并利用 LINQ 的强大功能,使用强类型代码处理数据。作为一名开发人员,这有助于您提高生产率,方法是针对具有完全 IntelliSense 支持的概念应用模型进行编程,而不是直接针对关系存储模式进行编程。注意 EF 如何自动将DbSet属性名设置为复数形式。太棒了!
您将在DbFirstDemoContext类中看到的另一个内容是OnConfiguring()。此方法将应用配置为使用 Microsoft SQL Server 作为提供程序,使用UseSqlServer()扩展方法并传递ConnectionString值。在实际生成的代码中,您将看到该值直接传递给UseSqlServer()方法。
笔记
在实际应用中,为了安全起见,应该避免直接注入实际值,而是将ConnectionString值存储在密钥库或机密管理器中。
最后,您将在DbFirstDemoContext类中看到一个名为OnModelCreating()的方法。OnModelCreating()方法为您的Models配置一个ModelBuilder。该方法由DbContext类定义,并标记为virtual,允许我们覆盖其默认实现。您将使用此方法配置Model关系、数据注释、列映射、数据类型和验证。在这个特定的示例中,当 EF Core 生成模型时,它应用我们在dbo.Person数据库表中的相应配置。
笔记
再次运行 database first 命令时,您对DbContext类和Entity模型所做的任何更改都将丢失。
现在我们已经配置了一个DbContext,让我们进入下一节,运行一些测试来执行一些简单的数据库操作。
执行基本的数据库操作
由于这是一个控制台应用,为了简化本练习,我们将在Program.cs文件中执行简单的insert、update、select和delete数据库操作。
让我们从将新数据插入数据库开始。
添加记录
继续并在Program类中添加以下代码:
static readonly DbFirstDemoContext _dbContext = new DbFirstDemoContext();
static int GetRecordCount()
{
return _dbContext.People.ToList().Count;
}
static void AddRecord()
{
var person = new Person { FirstName = “Vjor”, LastName = “Durano”, DateOfBirth = Convert.ToDateTime(“06/19/2020”) };
_dbContext.Add(person);
_dbContext.SaveChanges();
}
前面的代码定义了DbFirstDemoContext类的static readonly实例。我们需要DbContext以便访问DbSet并对其执行数据库操作。
GetRecordCount()方法只是返回存储在数据库中的记录计数数。AddRecord()方法负责将新记录插入数据库。在本例中,为了简单起见,我们刚刚为Person``Model定义了一些静态值。_dbContext.Add()方法以Model为参数。在本例中,我们将person变量传递给它,然后调用DbContext类的SaveChanges()方法。您对DbContext所做的任何更改都不会反映在基础数据库中,除非您调用SaveChanges()方法。
现在,我们要做的是调用前面代码中的方法。继续,在Program类的Main方法中复制以下代码:
static void Main(string[] args)
{
AddRecord();
Console.WriteLine($”Record count: {GetRecordCount()}”);
}
运行上述代码将向数据库中插入一条新记录,并输出值1作为记录计数。
通过转到 Visual Studio 中的SQL Server 对象资源管理器窗格,可以验证记录是否已在数据库中创建。向下钻取到dbo.Person表,右键点击查看数据。应在数据库中显示新增记录,如图 7.4所示:

图 7.4–显示 dbo.Person 表中的数据
凉的现在,让我们继续并执行其他一些数据库操作。
更新记录
让我们对数据库中的现有记录执行一个简单更新。在Program类中追加以下代码:
static void UpdateRecord(int id)
{
var person = _dbContext.People.Find(id);
// removed null check validation for brevity
person.FirstName = “Vynn Markus”;
person.DateOfBirth = Convert.ToDateTime(“11/22/2016”);
_dbContext.Update(person);
_dbContext.SaveChanges();
}
前面的代码将id作为参数。然后使用DbContext的Find()方法查询数据库。然后我们检查传入的id在数据库中是否有相关记录。如果Find()方法返回null,我们什么也不做,直接返回给调用者。否则,如果数据库中存在给定的id,我们将执行数据库更新。在本例中,我们只是替换了FirstName和DateOfBirth属性的值。
现在,我们调用Program类的Main方法中的UpdateRecord()方法,如下所示:
static void Main(string[] args)
{
UpdateRecord(1);
}
在前面的代码中,我们手动将1的值传递为id。当我们在上一节中执行插入时,该值表示数据库中的现有记录。
运行代码应更新FirstName和DateOfBirth列的值,如图 7.5所示:

图 7.5–显示 dbo.Person 表中的更新数据
伟大的现在,让我们继续进行其他数据库操作。
查询记录
继续并在Program类中复制以下代码:
static Person GetRecord(int id)
{
return _dbContext.People.SingleOrDefault(p => p.Id.Equals(id));
}
前面的代码还将一个id作为参数,这样它就可以识别要获取的记录。它使用 LINQSingleOrDefault()扩展方法查询数据库,并使用lambda 表达式与给定的id值进行值比较。如果id与数据库中的记录匹配,那么我们将向调用者返回一个Person对象。
现在,让我们通过复制Program类的Main方法中的以下代码来调用GetRecord()方法:
static void Main(string[] args)
{
var p = GetRecord(1);
if (p != null)
{
Console.WriteLine($”FullName: {p.FirstName} {p.LastName}”);
Console.WriteLine($”Birth Date: {p.DateOfBirth. ToShortDateString()}”);
}
}
在前面的代码中,我们已经手动将1的值再次作为参数传递给GetRecord()方法。这是为了确保我们正在获取一条记录,因为目前数据库中只有一条记录。如果您传递一个数据库中不存在的id值,GetRecord()方法将返回null。这就是为什么我们实现了一个基本的验证来检查null,这样应用就不会崩溃。然后,我们将这些值打印到控制台窗口。
运行该代码将导致如下结果,如图 7.6所示:

图 7.6–获取记录控制台输出
就这么简单!使用 LINQ 可以做很多事情来查询数据,尤其是复杂的数据。在本例中,我们只是使用单个数据库进行基本查询,以便您更好地了解它的工作原理。
现在,让我们转到最后一个示例。
删除记录
现在,让我们看看如何使用 EF Core 轻松执行删除。在Program类中复制以下代码:
static void DeleteRecord(int id)
{
var person = _dbContext.People.Find(id);
// removed null check validation for brevity
_dbContext.Remove(person);
_dbContext.SaveChanges();
}
就像在数据库update操作中一样,前面的代码首先使用Find()方法检查现有记录。如果记录存在,我们调用DbContext的Remove()方法并保存更改以反映数据库中的删除。
现在,在Program类的Main方法中复制以下代码:
static void Main(string[] args)
{
DeleteRecord(1);
Console.WriteLine($”Record count: {GetRecordCount()}”);
}
运行该代码将删除数据库中id值等于1的记录。对GetRecordCount()方法的调用现在将返回0,因为数据库中没有任何其他记录。
现在,您已经了解了如何使用 EF Core 实现数据库优先的方法,让我们继续下一节,并结合 ASP.NET Core Web API 探索 EF Core 代码优先的方法。
学习代码优先开发
在本节中,我们将通过构建一个简单的 ASP.NET Core Web API 应用来执行基本的数据库操作,探索 EF 核心代码优先开发。
在编写代码之前,让我们先回顾一下 ASP.NET Core Web API 是什么。
回顾 ASP.NET Core Web API
有许多方法可以使各种系统从一个应用访问另一个应用的数据。一些通信示例包括基于 HTTP 的 API、web 服务、WCF 服务器、基于事件的通信、消息队列和许多其他通信。如今,基于 HTTP 的 API 是应用之间最常用的通信方式。使用 HTTP 作为构建 API 的传输协议有几种方式:OpenAPI、远程过程调用(gRPC)和表示状态转移(REST)。
ASP.NET CoreWeb API 是一个基于 HTTP 的框架,用于构建 RESTful API,允许不同平台上的其他应用通过 HTTP 消费和传递数据。在 ASP.NET Core 应用中,Web API 与 MVC 非常相似,只是它们将数据作为响应返回给客户机,而不是View。API 上下文中的术语客户端是指 web 应用、移动应用、桌面应用、其他 web API 或支持 HTTP 协议的任何其他类型的服务。
创建 Web API 项目
现在,您已经了解了 Web API 的全部内容,让我们看看如何构建一个简单但现实的 RESTFul API 应用,为来自真实数据库的数据提供服务。但请记住,我们不会涵盖 REST 的所有约束和指导原则,因为在一章中涵盖所有约束和指导原则将是一项艰巨的任务。相反,我们将只介绍一些基本的指导原则,以便您能够很好地掌握在 ASP.NET Core 中构建 API 并从中起步。
要创建新的 Web API 项目,请启动 Visual Studio 2019 并按照此处给出的步骤进行操作:
- 选择新建项目选项。
- 在下一个屏幕上,选择ASP.NET Core Web 应用,然后单击下一步。
- 在配置新项目对话框中,将项目名称设置为
EFCore_CodeFirst并选择要创建项目的位置。 - 点击创建。在下一个屏幕上,选择API项目模板,点击创建。
您应该看到 VisualStudio 为 Web API 模板生成的默认文件。默认生成的模板包括使用静态数据模拟简单的HTTP``GET请求的WeatherForecastController。为确保项目正常运行,按Ctrl+F5键运行应用,一切正常时应显示如下输出,如图 7.7所示:

图 7.7–天气预报 HTTP 获取响应输出
此时,我们可以得出结论,默认项目工作正常。现在让我们进入下一步,设置应用的数据访问部分。
配置数据访问
这里我们需要做的第一件事是为应用集成所需的 NuGet 包依赖项。就像我们在集成实体框架核心一节中所做的一样,安装以下 NuGet 软件包:
Microsoft.EntityFrameworkCoreMicrosoft.EntityFrameworkCore.DesignMicrosoft.EntityFrameworkCore.SqlServer
至少,我们需要添加这些依赖项,以便使用 EF Core,使用 SQL Server 作为数据库提供程序,最后,使用 EF Core 命令创建迁移和数据库同步。
成功安装所需的 NuGet 包依赖项后,让我们跳到下一步,创建我们的Models。
创建实体模型
正如我们在代码优先工作流程中所了解的,我们将开始创建表示实体的概念性Models。
在应用的根目录下创建名为Db的新文件夹,并创建名为Models的子文件夹。为了让这个练习更有趣,我们将定义一些包含关系的Models。我们将要建立一个 API,音乐播放器可以提交他们的信息以及他们演奏的乐器。为了达到这个要求,我们需要一些模型来保存不同的信息。
现在,在Models文件夹中创建以下类:
InstrumentType.csPlayerInstrument.csPlayer.cs
以下是InstrumentType.cs文件的类定义:
public class InstrumentType
{
public int InstrumentTypeId { get; set; }
public string Name { get; set; }
}
以下是PlayerInstrument.cs文件的类定义:
public class PlayerInstrument
{
public int PlayerInstrumentId { get; set; }
public int PlayerId { get; set; }
public int InstrumentTypeId { get; set; }
public string ModelName { get; set; }
public string Level { get; set; }
}
以下是Player.cs文件的类定义:
public class Player
{
public int PlayerId { get; set; }
public string NickName { get; set; }
public List<PlayerInstrument> Instruments { get; set; }
public DateTime JoinedDate { get; set; }
}
前面代码中的类只不过是普通类,包含了我们构建某些 API 端点所需的一些属性。这些类代表了我们的Models,我们稍后将作为数据库表迁移。请记住,为了简单起见,在本例中,我们使用int类型作为标识符。在一个实际应用中,您的 To.T3At 可能想考虑使用 AutoT4 全局唯一标识符 ORT T5(AutoT6G.GUID TY7TY)类型,这样当您在 API 端点中公开这些标识符时,就很难猜出它。
播种数据
接下来,我们将创建一个扩展方法来演示如何将数据预加载到名为InstrumentType的查找表中。继续,在Db文件夹中创建一个名为DbSeeder的新类,然后复制以下代码:
public static class DbSeeder
{
public static void Seed(this ModelBuilder modelBuilder)
{
modelBuilder.Entity<InstrumentType>().HasData(
new InstrumentType { InstrumentTypeId = 1, Name = “Acoustic Guitar” },
new InstrumentType { InstrumentTypeId = 2, Name = “Electric Guitar” },
new InstrumentType { InstrumentTypeId = 3, Name = “Drums” },
new InstrumentType { InstrumentTypeId = 4, Name = “Bass” },
new InstrumentType { InstrumentTypeId = 5, Name = “Keyboard” }
);
}
}
前面的代码使用EntityTypeBuilder<T>对象的HasData()方法初始化InstrumentType``Model的一些数据。我们将在下一步配置DbContext时调用Seed()扩展方法。
定义 DbContext
创建一个名为CodeFirstDemoContext.cs的新类并复制以下代码:
public class CodeFirstDemoContext : DbContext
{
public CodeFirstDemoContext(DbContextOptions<CodeFirstDemoContext> options)
: base(options) { }
public DbSet<Player> Players { get; set; }
public DbSet<PlayerInstrument> PlayerInstruments { get; set; }
public DbSet<InstrumentType> InstrumentTypes { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Player>()
.HasMany(p => p.Instruments)
.WithOne();
modelBuilder.Seed();
}
}
前面的代码为Player、PlayerInstrument和InstrumentType``Models定义了几个DbSet实体。在OnModelCreating()方法中,我们在Player和PlayerInstrument``Models之间配置了一对多关系。HasMany()方法指示框架Player实体可以包含一个或多个PlayerInstrument条目。对modelBuilder.Seed()方法的调用将在数据库中的InstrumentType表创建时用数据预填充该表。
请记住,DbContext使用扩展方法来执行数据库 CRUD 操作,并且已经管理了事务。因此,您实际上不需要创建通用存储库和工作单元模式,除非确实需要它来增加更多的价值。
将 DbContext 注册为服务
在Db文件夹中,继续创建一个名为DbServiceExtension.cs的新类,并复制以下代码:
public static class DbServiceExtension
{
public static void AddDatabaseService(this IServiceCollection services, string connectionString)
=> services.AddDbContext<CodeFirstDemoContext>(options => options.UseSqlServer(connectionString));
}
前面的代码定义了一个名为AddDatabaseService()的static方法,该方法负责在 DI 容器中注册使用 SQL Server 数据库提供程序的DbContext。
现在我们有了我们的DbContext,让我们继续下一步,并将剩余的部分连接起来,以使数据库迁移工作正常。
设置数据库连接字符串
在本练习中,我们还将使用内置在 Visual Studio 中的本地数据库。然而,这一次,我们不会在代码中注入ConnectionString值。相反,我们将使用一个配置文件来存储它。现在,打开appsettings.json文件并附加以下配置:
“ConnectionStrings”: {
“CodeFirstDemoDb”: “Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=*CodeFirstDemo*;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False”
}
前面的代码使用的ConnectionStrings值与我们在前面的示例中关于学习数据库第一次开发使用的ConnectionStrings值相同,只是我们将Initial Catalog值更改为CodeFirstDemo。在 SQL Server 中执行迁移后,此值将自动成为数据库名称。
笔记
作为提醒,在开发一个真正的应用时,一定要考虑在一个密钥库或秘密管理器中存储 Apple T0 值和其他敏感数据。这是为了防止在版本控制存储库中托管源代码时向恶意用户公开敏感的信息。
修改启动类
我们将Startup类的方法ConfigureServices()更新为以下代码:
public void ConfigureServices(IServiceCollection services)
{
services.AddDatabaseService(Configuration. GetConnectionString(“CodeFirstDemoDb”));
//Removed other code for brevity
}
在前面的代码中,我们调用了前面创建的AddDatabaseService()扩展方法。将DbContext注册为 DI 容器中的服务,使我们能够通过 DI 在应用中的任何类中引用该服务的实例。
管理数据库迁移
在现实世界的开发场景中,业务需求经常发生变化,您的Models也会发生变化。在这种情况下,EF Core 中的迁移功能非常方便,可以使您的概念Model与数据库保持同步。
总而言之,EF Core 中的迁移是通过使用 PMC 或通过.NET Core CLI 执行命令来管理的。在本节中,我们将学习如何执行迁移命令。
首先,让我们从创建迁移开始。
创建迁移
在 Visual Studio 中打开 PMC 并运行以下命令:
PM> Add-Migration InitialMigration -o Db/Migrations
或者,也可以使用.NET Core CLI 运行以下命令:
dotnet ef migrations add InitialMigration -o Db/Migrations
两个迁移命令都应该在Db/Migrations文件夹下生成迁移文件,如图 7.8所示:

图 7.8–生成的迁移文件
EF Core 将使用前面屏幕截图中生成的迁移文件在数据库中应用迁移。20200913063007_InitialMigration.cs文件包含接受MigrationBuilder作为参数的Up()和Down()方法。当您对数据库应用Model更改时,将执行Up()方法。Down()方法放弃任何更改,并基于上一次迁移恢复数据库状态。每次添加迁移时,CodeFirstDemoContextModelSnapshot文件都包含数据库的快照。
您可能已经注意到,迁移文件的命名约定以时间戳作为前缀。这是因为当您创建新迁移时,框架将使用这些文件将Models的当前状态与以前的数据库快照进行比较。
现在我们有了迁移文件,接下来我们需要做的是应用创建的迁移来反映数据库中的更改。
应用迁移
导航回 PMC 窗口并运行以下命令:
PM> Update-Database
与.NET Core CLI 等效的命令如下所示:
dotnet ef database update
前面的命令将基于Models生成一个名为CodeFirstDemo的数据库,其中包含相应的表以及一个名为_EFMigrationsHistory的特殊迁移历史表,如图 7.9所示:

图 7.9–生成的 CodeFirstDemo 数据库
dbo._EFMigrationsHistory表存储迁移文件的名称和用于执行迁移的 EF Core 版本。框架将使用此表根据新迁移自动应用更改。dbo.InstrumentTypes表也将预加载数据。
此时,您应该已经设置好数据访问,并准备好在应用中使用。
复习 DTO 课程
在我们深入研究实现细节之前。让我们首先回顾一下 DTO 是什么,因为我们将在本练习的后面创建它们。
数据传输对象(DTO是定义Model的类,有时对 HTTP 响应和请求进行预定义验证。您可以将 DTO 想象为 MVC 中的ViewModels,您只想将相关数据公开给View。拥有 DTO 的基本思想是将它们与数据访问层用于填充数据的实际Entity``Model类分离。这样,当需求发生变化或您的Entity``Model属性发生变化时,它们不会受到影响,也不会破坏您的 API。您的Entity``Model类只能用于与数据库相关的进程。DTO 应该只用于获取请求输入和响应输出,并且应该只公开希望客户端看到的属性。
现在,让我们进入下一步,创建几个用于服务和消费数据的 API 端点。
创建 Web API 端点
为了简单起见,internet 上的大多数示例都会教您如何通过直接在Controllers中实现逻辑来创建 Web API 端点。对于这个练习,我们不会这样做,而是通过应用一些推荐的指导方针和实践来创建 API。通过这种方式,您将能够使用这些技术并在构建实际应用时应用它们。
在本练习中,我们将介绍用于实现 Web API 端点的最常用的HTTP 方法(动词),例如GET、POST、PUT和DELETE。
实现 HTTP POST 端点
让我们从实现一个用于在数据库中添加新记录的POSTAPI 端点开始。
定义 DTO
首先,在应用的根目录下创建一个名为Dto的新文件夹。您希望构建项目文件的方式取决于首选项,您可以随意组织它们。对于这个演示,我们希望有一个清晰的关注点分离,这样我们就可以轻松地导航和修改代码,而不会影响其他代码。因此,在Dto文件夹中,创建一个名为PlayerInstruments的子文件夹,然后创建一个名为CreatePlayerInstrumentRequest的新类,代码如下:
public class CreatePlayerInstrumentRequest
{
public int InstrumentTypeId { get; set; }
public string ModelName { get; set; }
public string Level { get; set; }
}
前面的代码是一个表示DTO的类。记住,DTO 应该只包含我们需要从外部世界或消费者公开的属性。本质上,DTO 是轻量级的。
创建另一个子文件夹Players并复制以下代码:
public class CreatePlayerRequest
{
[Required]
public string NickName { get; set; }
[Required]
public List<CreatePlayerInstrumentRequest> PlayerInstruments { get; set; }
}
前面的代码包含两个属性。注意,我们在List类型表示中引用了CreatePlayerInstrumentRequest类。这是为了在创建具有多个乐器的新播放器时启用一对多关系。您可以看到,每个属性都使用了[Required]属性进行了修饰,以确保在提交请求时不会将属性保留为空。[Required]属性内置于框架中,位于System.ComponentModel.DataAnnotations名称空间下。对Models强制验证的过程称为数据注释。如果您希望有一个干净的Model定义并以流畅的方式执行复杂的预定义验证,那么您可以尝试使用FluentValidation代替。
定义接口
正如您在前一章的示例中所看到的,我们可以通过构造函数注入直接传递Controller中的DbContex``t实例。然而,在构建真正的应用时,您应该尽可能精简您的Controllers,并将业务逻辑和数据处理置于Controllers之外。您的Controllers应该只处理诸如路由、Model验证和将数据处理委托给单独的服务之类的事情。话虽如此,我们将创建一个服务来处理Controllers和DbContext之间的通信。
在一个单独的服务中实现代码逻辑是使您的Controller变得精简和简单的一种方法。然而,我们不希望Controller直接依赖于实际的服务实现,因为它可能导致紧密耦合的依赖关系。相反,我们将创建一个interface抽象来解耦实际的服务依赖性。这使您的代码更易于测试、扩展和管理。您可以查看第三章依赖注入,了解interface抽象的详细信息。
现在,在应用的根目录下创建一个名为interfaces的新文件夹。在文件夹中,创建一个名为IPlayerService的新接口,并复制以下代码:
public interface IPlayerService
{
Task CreatePlayerAsync(CreatePlayerRequest playerRequest);
}
前面的代码定义了一个方法,该方法接受我们前面创建的CreatePlayerRequest类。该方法返回一个Task,表示将异步调用该方法。
现在我们已经定义了一个interface,我们现在应该能够创建一个实现它的服务。让我们在下一步中看看如何做到这一点。
实施服务
在本节中,我们将实现前面定义的interface,为interface中定义的方法构建实际逻辑。
继续,在应用的根目录下创建一个名为Services的新文件夹,然后用以下代码替换默认生成的代码:
public class PlayerService : IPlayerService
{
private readonly CodeFirstDemoContext _dbContext;
public PlayerService(CodeFirstDemoContext dbContext)
{
_dbContext = dbContext;
}
}
在前面的代码中,我们定义了CodeFirstDemoContext的private和readonly字段,并添加了一个类构造函数,将CodeFirstDemoContext注入为PlayerService类的依赖项。通过在构造函数中应用依赖注入,类中的任何方法都将能够访问CodeFirstDemoContext的实例,允许我们调用其所有可用的方法和属性。
您可能还注意到该类实现了IPlayerService接口。既然interface定义了class应该遵循的契约,那么我们接下来要做的就是实现CreatePlayerAsync()方法。继续并在PlayerService类中附加以下代码:
public async Task CreatePlayerAsync(CreatePlayerRequest playerRequest)
{
using var transaction = await _dbContext.Database. BeginTransactionAsync();
try
{
var player = new Player
{
NickName = playerRequest.NickName,
JoinedDate = DateTime.Now
};
await _dbContext.Players.AddAsync(player);
await _dbContext.SaveChangesAsync();
var playerId = player.PlayerId;
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
在前面的代码中,通过使用async关键字将该方法标记为异步方法。代码的作用是首先在数据库中添加一个新的Player条目,并返回已生成的PlayerId。
完成的CreatePlayerAsync()方法。在var playerId = player.PlayerId;行后的try块内复制以下代码:
var playerInstruments = new List<PlayerInstrument>();
foreach (var instrument in playerRequest.PlayerInstruments)
{
playerInstruments.Add(new PlayerInstrument
{
PlayerId = playerId,
InstrumentTypeId = instrument.InstrumentTypeId,
ModelName = instrument.ModelName,
Level = instrument.Level
});
}
_dbContext.PlayerInstruments.AddRange(playerInstruments);
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
前面的代码遍历playerRequest.PlayerInstruments集合,并在数据库中与playerId一起创建关联的PlayerInstrument。
由于dbo.PlayerInstruments表依赖于dbo.Players表,因此我们使用了 EF Core 数据库事务特性来确保两个表中的记录仅在成功操作时创建。这是为了避免在一个数据库操作失败时损坏数据。当一切都成功运行时,您可以调用transaction.CommitAsync()方法,当发生错误时,您可以调用catch块中的transaction.RollbackAsync()方法来恢复任何更改。
让我们继续下一步并注册服务。
注册服务
我们需要将接口映射注册到 DI 容器中,以便将interface注入到应用中的任何其他类中。在Startup.cs文件的ConfigureServices()方法中添加以下代码:
services.AddTransient<IPlayerService, PlayerService>();
前面的代码将 DI 容器中的PlayerService类注册为具有瞬态作用域的IPlayerService接口类型。这告诉框架在运行时将接口依赖性注入到Controller类构造函数中来解决接口依赖性。
现在我们已经实现了服务并将该部分连接到 DI 容器中,我们现在可以将IPlayerService作为Controller类的依赖项注入,我们将在下一步中创建该类。
创建 API 控制器
继续并右键点击Controllers文件夹,然后选择添加>控制器>API 控制器清空,然后点击添加。
将类命名为PlayersController.cs,然后点击添加。现在,复制以下代码,使其看起来与此类似:
[Route(“api/[controller]”)]
[ApiController]
public class PlayersController : ControllerBase
{
private readonly IPlayerService _playerService;
public PlayersController(IPlayerService playerService)
{
_playerService = playerService;
}
}
前面的代码是 APIController类的典型结构。Web API 控制器使用与 MVC 相同的路由中间件,只是它使用属性路由来定义路由。[Route]属性允许您为 API 端点指定任何路由。ASP.NET Core API 默认约定使用格式api/[controller],其中[controller]段表示令牌占位符,以基于Controller类前缀名称自动构建路由。对于本例,路由api/[controller]将被翻译为api/players,其中players来自PlayersController类名。[ApiController]属性使Controller能够为您的 API 应用特定于 API 的行为,例如属性路由要求、HTTP404 和 405 响应的自动处理、错误的问题详细信息等。
Web API 应该派生自ControllerBase``abstract类,以利用框架中内置的现有功能来构建 RESTful API。在前面的代码中,您可以看到我们现在已经将IPlayerService作为依赖项注入,而不是DbContext本身。这将使您的数据访问实现与Controller类分离,在您决定更改服务的底层实现时允许更大的灵活性,并使您的Controller变得精简干净。
现在,为POST端点添加以下代码:
[HttpPost]
public async Task<IActionResult> PostPlayerAsync([FromBody] CreatePlayerRequest playerRequest)
{
if (!ModelState.IsValid) { return BadRequest(); }
await _playerService.CreatePlayerAsync(playerRequest);
return Ok(“Record has been added successfully.”);
}
前面的代码将CreatePlayerRequest类作为参数。通过使用[FromBody]属性标记参数,我们告诉框架只接受来自该端点请求主体的值。您还可以看到,PostPlayerAsync()方法已经被[HttpPost]属性修饰,这意味着方法只能被HTTP``POST请求调用。您可以看到,方法实现现在更干净了,因为它只验证DTO并将实际数据处理委托给服务。ModelState.IsValid()将检查CreatePlayerRequest``Model的任何预定义验证规则,并返回Boolean以指示验证是否失败或通过。在本例中,它仅通过检查为每个属性注释的[Required]属性来检查CreatePlayerRequest类中的两个属性是否都为空。
此时,您应该可以使用POST端点。让我们做一个快速测试,以确保端点按预期工作。
测试 POST 端点
我们将使用邮递员来测试我们的 API 端点。Postman 确实是一个测试 API 的便捷工具,无需创建 UI,而且绝对免费。继续并在此处下载:https://www.getpostman.com/ 。
下载 Postman 后,将其安装到您的计算机上,以便开始测试。现在,先运行应用,按Ctrl+F5键在浏览器中启动应用。
打开 Postman,然后使用以下 URL 发出POST请求:https://localhost:44306/api/players 。
请注意,端口44306在您的情况下可能不同,因此请确保将该值替换为本地应用正在运行的实际端口。您可以在项目的Properties文件夹下查看launchSettings.json,以了解有关如何配置启动 URL 配置文件的更多信息。
让我们继续测试。在 Postman 中,切换到主体选项卡,选择原始选项,选择JSON作为格式。参考以下图 7.10的视觉参考:

图 7.10–在 Postman 中配置 POST 请求
现在,在原始文本框中,复制以下 JSON 作为请求负载:
{
“nickName”:”Vianne”,
“playerInstruments” :[
{
“InstrumentTypeId”: 1,
“ModelName”: “Taylor 900 Series”,
“Level”: “Professional”
},
{
“InstrumentTypeId”: 2,
“ModelName”: “Gibson Les Paul Classic”,
“Level”: “Intermediate”
},
{
“InstrumentTypeId”: 3,
“ModelName”: “Pearl EXL705 Export”,
“Level”: “Novice”
}
]
}
前面的代码是/api/players端点期望的JSON请求主体。如果您还记得,POST端点期望CreatePlayerRequest作为参数。前面代码中的JSON有效载荷表示该情况。
现在,点击邮递员中的发送按钮,调用HTTP``POST端点,您会看到如下结果,如图 7.11所示:

图 7.11–在邮递员中发出 POST 请求
前面的屏幕截图返回 200 HTTP 状态,并显示一条响应消息,指示已在数据库中成功创建记录。您可以通过查看dbo.Players和dbo.PlayerInstruments数据库表来验证新插入的数据。
现在,让我们测试一下Model验证。下面的图 7.12显示了如果在请求体中省略playerInstruments属性并点击发送按钮的结果:

图 7.12–验证错误响应输出
前面的屏幕截图显示了一个带有 400 HTTP 状态代码的ProblemDetails格式的验证错误。当您注释一个Model属性是必需的,而您在调用 API 端点时不提供该属性时,响应就是这样的。
既然您已经了解了为POST请求创建 Web API 端点的基本知识,那么让我们继续通过探索其他示例来了解这些知识。
实现 HTTP GET 端点
在本节中,我们将为创建对HTTP``GET端点,让您了解从数据库获取数据的一些基本方法。
定义 DTO
就像我们对POST端点所做的一样,我们需要做的第一步是创建一个DTO类,以便我们定义需要公开的属性。在Dto/Players文件夹中创建一个名为GetPlayerResponse的新类,并复制以下代码:
public class GetPlayerResponse
{
public int PlayerId { get; set; }
public string NickName { get; set; }
public DateTime JoinedDate { get; set; }
public int InstrumentSubmittedCount { get; set; }
}
前面的代码只是一个普通类,它包含一些属性。这些属性将作为响应返回给客户机。
对于这个端点实现,我们不会将数据库中的所有记录返回给客户端,因为这样会非常低效。假设您的数据库中有数千条或数百万条记录,并且您的 API 端点试图一次返回所有记录。这肯定会降低应用的整体性能,更糟糕的是,它可能会使应用无法使用。
使用分页实现 GET
为了防止潜在的性能问题发生,我们将实现分页功能以评估性能。这将使我们能够限制返回到客户端的数据量,并在数据库中的数据增长时保持性能。
现在,继续在Dto文件夹中创建一个名为PagedResponse的新类。复制以下代码:
public class PagedResponse<T>
{
const int _maxPageSize = 100;
public int CurrentPageNumber { get; set; }
public int PageCount { get; set; }
public int PageSize
{
get => 20;
set => _ = (value > _maxPageSize) ? _maxPageSize : value;
}
public int TotalRecordCount { get; set; }
public IList<T> Result { get; set; }
public PagedResponse()
{
Result = new List<T>();
}
}
前面的代码定义了页面Model的一些基本元数据。注意,我们已经将常量_maxPageSize变量设置为100。这是 API GET 端点将返回给客户端的最大记录数的值。PageSize属性设置为20作为默认值,以防客户端在调用端点时不指定该值。另一件需要注意的事情是,我们已经定义了一个类型为IList<T>的泛型属性Result。T可以是任何您希望以分页方式返回的Model之一。
接下来,让我们在Dto文件夹中创建一个名为UrlQueryParameters的新类。复制以下代码:
public class UrlQueryParameters
{
public int PageNumber { get; set; };
public int PageSize { get; set; };
}
前面的代码将用作GET端点的方法参数,我们稍后将实现。这允许客户端在请求数据时设置页面大小和编号。
接下来,在应用的根目录下创建一个名为Extensions的新文件夹。在Extensions文件夹中,创建一个名为PagerExtension的新类,并复制以下代码:
public static class PagerExtension
{
public static async Task<PagedResponse<T>> PaginateAsync<T>(
this IQueryable<T> query,
int pageNumber,
int pageSize)
where T : class
{
var paged = new PagedResponse<T>();
pageNumber = (pageNumber < 0) ? 1 : pageNumber;
paged.CurrentPageNumber = pageNumber;
paged.PageSize = pageSize;
paged.TotalRecordCount = await query.CountAsync();
var pageCount = (double)paged.TotalRecordCount / pageSize;
paged.PageCount = (int)Math.Ceiling(pageCount);
var startRow = (pageNumber - 1) * pageSize;
paged.Result = await query.Skip(startRow). Take(pageSize).ToListAsync();
return paged;
}
}
前面的代码是实际分页和计算发生的。PaginateAsync()方法采用三个参数来执行分页,并返回一个PagedResponse<T>类型的Task。method 参数中的this关键字表示该方法是IQueryable<T>类型的扩展方法。请注意,代码使用 LINQSkip()和Take()方法对结果进行分页。
既然我们已经定义了DTO并实现了一个扩展方法来分页数据,那么让我们继续下一步,在IPlayerService接口中添加一个新的方法签名。
更新接口
继续并在IPlayerService界面中添加以下代码:
Task<PagedResponse<GetPlayerResponse>> GetPlayersAsync(UrlQueryParameters urlQueryParameters);
前面的代码定义了一个以UrlQueryParameters为参数并返回GetPlayerResponse``Model类型的PagedResponse的方法。接下来,我们将更新PlayerService来实现这个方法。
更新服务
在PlayerService类中添加以下代码:
public async Task<PagedResponse<GetPlayerResponse>> GetPlayersAsync(UrlQueryParameters parameters)
{
var query = await _dbContext.Players
.AsNoTracking()
.Include(p => p.Instruments)
.PaginateAsync(parameters.PageNumber, parameters.PageSize);
return new PagedResponse<GetPlayerResponse>
{
PageCount = query.PageCount,
CurrentPageNumber = query.CurrentPageNumber,
PageSize = query.PageSize,
TotalRecordCount = query.TotalRecordCount,
Result = query.Result.Select(p => new GetPlayerResponse
{
PlayerId = p.PlayerId,
NickName = p.NickName,
JoinedDate = p.JoinedDate,
InstrumentSubmittedCount = p.Instruments.Count
}).ToList()
};
}
前面的代码显示了从数据库查询数据的 EF 核心方法。因为我们只获取数据,所以我们使用了AsNoTracking()方法来提高查询性能。无跟踪查询速度更快,因为它们不需要为实体设置更改跟踪信息,因此可以更快地执行并提高只读数据的查询性能。Include()方法允许我们在查询结果中加载关联数据。然后我们调用前面实现的PaginateAsync()扩展方法,根据UrlQueryParameters属性值对数据进行分块。最后,我们使用基于LINQ 方法的查询构造返回响应。在本例中,我们返回一个带有GetPlayerResponse的PagedResponse对象。类型
要查看 EF Core 生成的实际 SQL 脚本,或者如果您更喜欢使用原始 SQL 脚本查询数据,请查看本章进一步阅读部分中的链接。
让我们继续下一步,更新Controller类以定义GET端点。
更新控制器
在PlayersController类中添加以下代码:
[HttpGet]
public async Task<IActionResult> GetPlayersAsync([FromQuery] UrlQueryParameters urlQueryParameters)
{
var player = await _playerService. GetPlayersAsync(urlQueryParameters);
//removed null validation check for brevity
return Ok(player);
}
前面的代码以UrlQueryParameters为请求参数。通过使用[FromQuery]属性修饰参数,我们告诉框架计算并从查询字符串中获取请求值。该方法从IPlayerService接口调用GetPlayersAsync()并将UrlQueryParameters作为参数传递。如果结果为null,则返回NotFound();否则,我们将返回Ok()以及结果。
现在,让我们测试端点,以确保得到预期的结果。
测试端点
现在运行应用并打开 Postman。使用以下端点发出HTTP``GET请求:
https://localhost:44306/api/players?pageNumber=1&pageSize=2
您可以将pageNumber和pageSize的值设置为任意值,然后点击发送按钮。以下图 7.13是响应输出的示例屏幕截图:

图 7.13–分页数据响应输出
含糖的现在,让我们尝试另一个端点示例。
实现按 ID 获取
在本节中,我们将学习如何通过传递记录的 ID 从数据库中获取数据。我们将看到如何从每个数据库表中查询相关数据,并向客户机返回一个响应,其中包含来自不同表的详细信息。
定义 DTO
不用多说,让我们继续并在Dto/PlayerInstrument文件夹中创建一个名为GetPlayerInstrumentResponse的新类。复制以下代码:
public class GetPlayerInstrumentResponse
{
public string InstrumentTypeName { get; set; }
public string ModelName { get; set; }
public string Level { get; set; }
}
使用Dto/Players文件夹创建另一个名为GetPlayerDetailResponse的新类,然后复制以下代码:
public class GetPlayerDetailResponse
{
public string NickName { get; set; }
public DateTime JoinedDate { get; set; }
public List<GetPlayerInstrumentResponse> PlayerInstruments { get; set; }
}
前面的类表示我们将向客户机公开的响应DTO或Model。让我们进入下一步,在IPlayerService界面中定义一个新方法。
更新接口
在IPlayerService界面中增加以下代码:
Task<GetPlayerDetailResponse> GetPlayerDetailAsync(int id);
前面的代码是我们将在服务中实现的方法签名。让我们继续做吧。
更新服务
在PlayerService类中添加以下代码:
public async Task<GetPlayerDetailResponse> GetPlayerDetailAsync(int id)
{
var player = await _dbContext.Players.FindAsync(id);
//removed null validation check for brevity
var instruments = await
(from pi in _dbContext.PlayerInstruments
join it in _dbContext.InstrumentTypes
on pi.InstrumentTypeId equals it.InstrumentTypeId
where pi.PlayerId.Equals(id)
select new GetPlayerInstrumentResponse
{
InstrumentTypeName = it.Name,
ModelName = pi.ModelName,
Level = pi.Level
}).ToListAsync();
return new GetPlayerDetailResponse
{
NickName = player.NickName,
JoinedDate = player.JoinedDate,
PlayerInstruments = instruments
};
}
前面的代码包含GetPlayerDetailAsync()方法的实际实现。异步模式中的方法,它以id作为参数并返回GetPlayerDetailResponse类型。代码首先使用FindAsync()方法检查给定的id在数据库中是否有相关记录。如果结果为null,则返回default或null;否则,我们使用LINQ 查询表达式连接相关表来查询数据库。如果您以前编写过 T-SQL,您会注意到查询语法与 SQL 非常相似,只是它处理概念性的Entity``Models代码,提供了丰富的IntelliSense支持的强类型代码。
现在我们已经准备好了方法实现,让我们继续下一步,更新Controller类以定义另一个GET端点。
更新控制器
在PlayersController类中添加以下代码:
[HttpGet(“{id:long}/detail”)]
public async Task<IActionResult> GetPlayerDetailAsync(int id)
{
var player = await _playerService.GetPlayerDetailAsync(id);
//removed null validation check for brevity
return Ok(player);
}
前面的代码定义了一个路由配置为“{id:long}/detail”的GET端点。路由中的id表示可以在 URL 中设置的参数。作为一个友好的提醒,考虑在将资源 ID 暴露给外部世界而不是标识种子时,使用 AuthT3 作为记录标识符。这是为了通过增加id值来降低将数据暴露给试图嗅探端点的恶意用户的风险。
让我们通过测试端点来了解输出的情况。
测试端点
运行应用,在 Postman 中使用以下端点发出GET请求:
https://localhost:44306/api/players/1/detail
以下图 7.14是响应输出的示例屏幕截图:

图 7.14–详细数据响应输出
现在,您已经了解了实现HTTP``GET端点的各种方法,让我们进入下一节,看看如何实现PUT端点。
实现 HTTP PUT 端点
在本节中,我们将学习如何使用HTTP``PUT方法更新数据库中的记录。
定义 DTO
为了简化这个示例,让我们只更新数据库中的一列。继续,在Dto/Players文件夹中创建一个名为UpdatePlayerRequest的新类。复制以下代码:
public class UpdatePlayerRequest
{
[Required]
public string NickName { get; set; }
}
接下来,我们将更新IPlayerService接口,以包括执行数据库更新的新方法。
更新接口
在IPlayerService界面增加以下代码:
Task<bool> UpdatePlayerAsync(int id, UpdatePlayerRequest playerRequest);
前面的代码是更新数据库中dbo.Players表的方法签名。让我们继续下一步,并在服务中实现此方法。
更新服务
在IPlayerService类中添加以下代码:
public async Task<bool> UpdatePlayerAsync(int id, UpdatePlayerRequest playerRequest)
{
var playerToUpdate = await _dbContext.Players. FindAsync(id);
//removed null validation check for brevity
playerToUpdate.NickName = playerRequest.NickName;
_dbContext.Update(playerToUpdate);
return await _dbContext.SaveChangesAsync() > 0;
}
前面的代码非常简单。首先检查id在数据库中是否有关联记录。如果结果为null,则返回false;否则,我们将使用NickName属性的新值更新数据库。现在,让我们进入下一步,更新Controller类以调用此方法。
更新控制器
在PlayersController类中添加以下代码:
[HttpPut(“{id:long}”)]
public async Task<IActionResult> PutPlayerAsync(int id, [FromBody] UpdatePlayerRequest playerRequest)
{
if (!ModelState.IsValid) { return BadRequest(); }
var isUpdated = await _playerService.UpdatePlayerAsync(id, playerRequest);
if (!isUpdated) {
return NotFound($”PlayerId { id } not found.”);
}
return Ok(“Record has been updated successfully.”);
}
前面的代码从请求主体获取一个id和一个UpdatePlayerRequest``Model。该方法用[HttpPut(“{id:long}”)]修饰,表示该方法只能在HTTP``PUT请求中调用。路由中的id表示 URL 中的一个参数。
测试 PUT 端点
运行应用,在 Postman 中使用以下端点发出PUT请求:
https://localhost:44306/api/players/1
现在,就像在POST请求中一样,在原始文本框中复制以下代码:
{
“nickName”:”Vynn”
}
前面的代码是PUT端点所需的参数。在这个特定的例子中,我们将把id等于1的NickName值更改为“Vynn”。点击发送按钮应更新数据库中的记录。
现在,当您通过/api/players/1/detail执行id的GET请求时,您应该看到持有1值的id的NickName已经更新。在这种情况下,值“Vjor”被更新为“Vynn”。
让我们继续看最后一个例子——实现一个HTTP``DELETE方法。
实现 HTTP 删除端点
在本节中,我们将学习如何实现执行数据库记录删除的 API 端点。对于这个例子,我们不需要创建一个DTO,因为我们只需要在delete端点的路由中通过id。那么,让我们通过更新IPlayerService接口来加入一个新的删除方法。
更新接口
在IPlayerService界面中增加以下代码:
Task<bool> DeletePlayerAsync(int id);
前面的代码是我们将在下一节中实现的方法签名。请注意,签名与update方法类似,只是我们没有将DTO或Model作为参数传递。
让我们继续下一步,并在服务中实现该方法。
更新服务
在PlayerService类中添加以下代码:
public async Task<bool> DeletePlayerAsync(int id)
{
var playerToDelete = await _dbContext.Players
.Include(p => p.Instruments)
.FirstAsync(p => p.PlayerId. Equals(id));
//removed null validation check for brevity
_dbContext.Remove(playerToDelete);
return await _dbContext.SaveChangesAsync() > 0;
}
前面的代码使用Include()方法对dbo.PlayerIntruments表中的相关记录执行级联删除。然后我们使用FirstAsync()方法根据id值过滤要删除的记录。如果结果为null,则返回false;否则,我们使用_dbContext.Remove()方法执行记录删除。现在,让我们更新Controller类以调用此方法。
更新控制器
在PlayersController类中添加以下代码:
[HttpDelete(“{id:long}”)]
public async Task<IActionResult> DeletePlayerAsync(int id)
{
var isDeleted = await _playerService.DeletePlayerAsync(id);
if (!isDeleted) {
return NotFound($”PlayerId { id } not found.”);
}
return Ok(“Record has been deleted successfully.”);
}
前面代码中的实现也类似于 update 方法,只是该方法现在用[HttpDelete]属性修饰。现在,让我们测试一下DELETEAPI 端点。
测试删除端点
再次运行应用,在 Postman 中使用以下端点发出DELETE请求:
https://localhost:44306/api/players/1
当id等于1的记录从数据库中删除后,点击发送按钮应显示成功的响应输出。
就这样!如果您已经做到了这一点,那么您现在应该熟悉在 ASP.NET Core 中构建 API,并且能够在构建自己的 API 时应用本章学到的知识。你可能知道,你可以做很多事情来改进这个项目。您可以尝试合并日志记录、缓存、HTTP 响应一致性、错误处理、验证、身份验证、授权、招摇过市文档等功能,并探索其他 HTTP 方法,如PATCH。
总结
在本章中,我们介绍了实现实体框架核心作为数据访问机制的概念和不同的设计工作流。在决定如何设计数据访问层时,了解“数据库优先”和“代码优先”工作流的工作方式非常重要。我们已经了解了 API 和数据访问如何协同工作来服务和使用来自不同客户机的数据。通过学习如何从头开始创建处理真实数据库的 API,您可以更好地了解底层后端应用的工作方式,特别是当您将使用相同技术堆栈的真实应用时。
我们已经学习了如何在 ASP.NET Core Web API 中通过实际动手编码练习实现常见的 HTTP 方法。我们还学习了如何通过利用接口抽象设计 API,使其更易于测试和维护,并学习了让 DTO 重视关注点分离的概念,以及如何使 API 控制器尽可能精简。学习这项技术使您能够轻松地管理代码,而不会在决定重构应用时影响大部分应用代码。最后,我们学习了如何使用 Postman 轻松测试 API 端点。
在下一章中,您将了解 ASP.NET Core 标识,用于保护 web 应用、API、管理用户帐户等。
进一步阅读
- 实体框架核心资源—https://entityframeworkcore.com/
- EF 核心概述–https://docs.microsoft.com/en-us/ef/core/
- EF 核心支持的数据库提供程序–https://docs.microsoft.com/en-us/ef/core/providers/
- Lambda 表达式–https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions
- LINQ 查询表达式–https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/linq/basic-linq-query-operations
- EF 核心查询数据–https://docs.microsoft.com/en-us/ef/core/querying/
- EF 岩芯测井命令–https://www.entityframeworktutorial.net/efcore/logging-in-entityframework-core.aspx
- EF 核心原始 SQL–https://docs.microsoft.com/en-us/ef/core/querying/raw-sql
- 迁移概述—https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations
- 使用 ASP.NET Core 创建 Web API–https://docs.microsoft.com/en-us/aspnet/core/web-api
- C#异步编程–https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/
- ASP.NET Core 路由—https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/routing
- 将 FluentValidation 与 ASP.NET Core 一起使用–https://docs.fluentvalidation.net/en/latest/aspnet.html
八、在 ASP.NET 中使用身份
现在几乎所有的网站都有登录功能。即使它们在匿名浏览时有效,通常也可以选择成为会员或类似的东西。这意味着这些网站有一些身份概念来区分访问者。换句话说,如果你的任务是建立一个网站,那么你很可能也需要处理身份问题。问题是,身份可能很难被正确对待,而错误对待的后果可能不那么有趣。在本章中,我们将深入了解 ASP.NET 5 中标识的基础知识。
本章将介绍以下主题:
- 理解身份验证概念
- 理解授权概念
- ASP.NET 中间件的作用与身份
- OAuth 和 OpenID 连接基础
- 与 Azure Active Directory 集成
- 使用联邦身份
技术要求
本章包括简短的代码片段,以演示所解释的概念。需要以下软件才能使其正常工作:
- Visual Studio 2019:Visual Studio 可从下载 https://visualstudio.microsoft.com/vs/community/ 。社区版是免费的,将用于本书的目的。
- 一些示例要求您拥有一个Azure Active Directory(AAD租户。如果您还没有,您可以通过转到 Azure 门户(来创建一个 https://portal.azure.com )并注册一个免费帐户,或者更好,注册一个免费的 Office 365 开发者帐户,其中包括 AAD 的付费版本以及 Office 365 服务: https://docs.microsoft.com/en-us/office/developer-program/microsoft-365-developer-program 。
- 关于联邦身份的部分使用 AAD B2C。这是需要单独创建的 AAD 的特殊版本:https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-tenant 。
出于实验室目的,本章中的所有样品都可以免费测试,但区域特定要求可能需要使用信用卡进行验证。
请访问以下链接查看 CiA 视频:https://bit.ly/3qDiqYY
本章代码见https://github.com/PacktPublishing/ASP.NET-Core-5-for-Beginners/tree/master/Chapter%2008
理解认证概念
当我们在日常用语中说“身份”时,我们大多数人都理解自己的意思。在.NET 和一般的编码中,在允许用户进入我们的应用之前,我们需要更加具体。在此上下文中,身份包含多个概念,在确定用户是谁以及允许他们在我们的系统中做什么的过程中,具有不同的操作和机制。
身份之谜的第一部分是身份验证。在文档和文献中,您会经常发现这个缩写为AuthN。身份验证就是回答你是谁的问题。与现实世界类似,这会带来不同程度的信任,这取决于这个问题的答案。
如果你在一个聚会上遇到一个你不认识的人,问他们叫什么名字,你可能会对他们的回答感到高兴,而不用进一步核实。然而,您很可能不喜欢在网站上实现登录功能,用户只需键入用户名即可登录。
现实生活中的一个例子是要求某人提供身份证件——可以是国民身份证、驾驶执照、护照或类似证件。在网站上,最常用的方法是提供用户名和只有你知道的秘密(例如密码)的组合。
在 web 应用中实现这一点的最简单形式是使用基本身份验证,这是 HTTP 规范的部分。这是通过客户端向 HTTP 请求附加一个头,并将凭据编码为 Base64 值来实现的。在控制台应用中,它将如下所示:
static void Main(string[] args){ var username = ''andreas''; var password = ''password''; var byteEncoding = System.Text.UTF8Encoding.UTF8.GetBytes(
$''{username}:{password}''); var credentials = Convert.ToBase64String(byteEncoding);
Console.WriteLine(credentials);
HttpClient client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new
AuthenticationHeaderValue(''Basic'', credentials); var response = client.GetAsync(''https://localhost:5001'');}
凭证将始终是YW5kcmVhczpwYXNzd29yZA==,没有随机元素,因此以这种方式传输凭证的主要好处是用于编码目的。在继续之前,让我们快速了解一下 Base64 是什么。
Base64 编码
我们大家都熟悉 Base10(通常称为十进制),因为这是我们在做普通算术时使用的——我们使用 0–9 来表示数字。在计算中,Base16 也经常以十六进制的名称使用。因为数字只升到 9,所以我们用字母加起来,所以 A=10,B=11,等等,直到 F=15。Base64 更进一步,使用了A-Z、a-z、0-9以及+和/字符,其中=作为特殊的填充字符(以确保字符串始终具有可预测的长度)。
我们将不深入讨论如何转换字符的算法,但正如前面的代码片段中所演示的,它将把一些人类可读的东西转换成一些虽然在技术上仍然可读,但仅通过查看就很难解释的东西。以这种方式编码数据的主要好处是,即使使用不可打印或不可读的字符,也可以传输纯文本和二进制数据而不会损坏。HTTP 协议本身并不能解释所有字符,因此对于带有特殊字符的密码,如果不进行编码传输,则可能无法在服务器端正确解释该密码。
Base64 不是加密的一种形式,因此您不能信任它本身的秘密,它可以被视为纯文本,即使您作为一个人类,无法动态解码它。这也意味着在没有 HTTPS 的情况下使用基本身份验证是一种不安全的身份验证机制。使用 TLS/SSL 来保护传输在这方面有了很大的改进,但它仍然依赖于通过网络发送密码。
在的脑海中,我们可以解码传输另一端的 Base64 字符串,相应的服务器部分如下所示:
public String Get(){ var authHeader = HttpContext.Request. Headers[''Authorization'']; var base64Creds = AuthenticationHeaderValue.Parse
(authHeader).Parameter; var byteEncoded = System.Convert.FromBase64String(base64Creds); var credentials = System.Text.Encoding.UTF8.GetString(
byteEncoded);
if (credentials == ''andreas:password'') { return ''Hello Andreas''; } else { return ''You didn't pass authentication!''; } }
首先运行服务器,然后运行客户端,您将获得一些输出:
dotnet run Base64 encoded: YW5kcmVhczpwYXNzd29yZA==Response: Hello Andreas
由于我们在身份验证代码中对用户名和密码进行了硬编码,所以您可能不会感到惊讶,因为这个实现是一个糟糕的实现。在这一点上,显而易见的选择是将其移动到数据库中并进行查找。这让我们想起了你可能犯下的最严重的身份实现错误之一——将密码直接存储在数据库中。永远不要在数据库中存储密码。时期您应该存储一个不可逆的密码散列,并计算输入的密码是否与数据库中存储的密码匹配。这样,如果攻击者掌握了数据库,他们就无法轻易提取密码。
这就引出了一个问题,在这个上下文中,散列是什么,下面我们来讨论这个问题。
散列是如何工作的
散列函数是一种将一个值转换为另一个值的算法,通常用于优化数据结构中的查找或验证初始值。例如,如果我们要创建一个非常基本的哈希算法,我们可以使用数字替换字符来创建给定字符串的哈希。比如说A=1、B=2等等。然后,Password字符串将是16 1 19 19 23 15 4(每个数字代表一个字符;为了可读性而添加空格)。然后我们将这些数字相加,除以字符数–(16 + 1 + 19 + 19 + 23 + 15 + 4) / 8 = 12.125。仅使用整数部分,我们以12结束。
我们将存储值12,而不是存储您的实际密码。当我们输入Password作为密码时,我们可以再次计算散列并将其与存储值进行比较。这也很好,因为它是不可逆的——即使已知算法,也不可能将数字12反向工程为Password,因此数据库的副本不会有助于确定密码。
即使你不是数学天才,你也可能会发现这个算法很弱。使用我们使用的简单替换方案,创建一个字符串相当容易,该字符串也将产生12作为值,因此是有效的。一个好的散列算法应该产生唯一的值,这样两个不同的密码就不可能有相同的散列。幸运的是,Microsoft 已经为.NET 实现了许多哈希算法,因此您不必推出自己的哈希算法。
如果我们要用伪代码来说明这一点(我们不会编译,因为我们没有实现数据库查找),它将如下所示:
var credentials = System.Text.Encoding.UTF8.GetString(byteEncoded);
//Split the credentials into separate parts var username = credentials.Split('':'')[0];var password = credentials.Split('':'')[1];
//Bad if (db.CheckUser == true && db.CheckPassword == true){ return $''Hello {username}'';}
//Good var myHash = System.Security.Cryptography.SHA256.Create();var hashEncoder = System.Text.UTF8Encoding.UTF8;var byteHashedPassword = myHash.ComputeHash(hashEncoder.GetBytes(password));
System.Text.StringBuilder sb = new System.Text.StringBuilder();foreach (Byte b in byteHashedPassword) sb.Append(b.ToString(''x2''));
var hashedPassword = sb;if (db.CheckUser == true && db.CheckHashedPassword == true){ return $''Hello {username}'';}
到现在为止,你可能会认为在身份验证中会有很多事情发生,而你是正确的。事实上,我们并不推荐使用基本身份验证,但希望它能让您了解什么是身份验证。在解释了身份验证的一个紧密伙伴,即授权之后,我们将展示一些更好的技术。
了解授权概念
身份拼图的第二块是授权,通常将缩短为AuthZ。其中AuthN是关于找出你是谁,AuthZ是关于你被允许做什么。
让我们回到现实世界和事物是如何运作的,让我们暂时考虑一下国际航空旅行。为了简单起见,假设所有国际旅行都需要您出示护照。如果您没有护照,这将等同于未经身份验证(未经身份验证),您将不被允许进入目的国。
如果您有护照,相关当局将通过询问以下问题对其进行审查:
- 它是由一个真实的国家发行的吗?(不幸的是,“.NET 土地”未得到联合国的承认。)
- 它看起来是真的,带有水印、生物特征标记等等,还是看起来像你在家里打印的东西?
- 签发护照的国家是否有良好的护照签发程序?
如果您通过这些检查,您将获得身份验证,但您可能还无法继续进行行李认领。新一轮的问题是:
- 您是目的地接受游客的国家的公民吗?
- 您是否来自需要签证的国家?如果是,您是否随身携带签证?
- 你是被判有罪的罪犯吗?
- 你是已知的恐怖分子吗?(航空公司在让你登机之前可能应该检查一下,但他们可能错过了。)
细节会因你想进入哪个国家而有所不同,但要点是一样的。当你的身份检查出来时,还有其他机制可以给你盖上批准章。
您可能已经在 web 应用中识别出类似的模式。例如,如果您以John作为用户名登录,则您拥有普通用户的权限,可以进行数据库查找、编辑等操作。然而,如果您以JohnAdmin作为用户名登录,您将获得管理权限,可以访问系统范围的服务器设置等。回顾上一节中的身份验证代码,我们将伪代码扩展为如下内容:
public String Get(){ var authHeader = HttpContext.Request.Headers[''Authorization'']; var base64Creds = AuthenticationHeaderValue.Parse(authHeader).Parameter; var byteEncoded = System.Convert.FromBase64String(base64Creds); var credentials =System.Text.Encoding.UTF8.GetString(byteEncoded);
//Split the credentials into separate parts var username = credentials.Split('':'')[0]; var password = credentials.Split('':'')[1];
//Password hashing magic omitted ... //Authentication code omitted ...
var userrole;
if (db.CheckRole == ''Admin'') { userrole = ''Admin''; } if (db.CheckRole == ''User'') { userrole = ''User'' } else { return ''You didn't pass authentication!''; }
return $''Hello {userrole}''; }
尽管这也是我们缺少角色查找的伪代码,但我们可以看到在引入授权时它是如何添加了一个附加层的。这可能是因为你的 web 应用可能不需要区分不同的角色,但我们在这里强调的一点是,我们已经构建了好几页了。
不要从头开始实现您自己的标识解决方案(或基于此示例代码)。
这并不是要抹黑本书读者的知识和能力;这是一个普遍的最佳实践,应该由那些全职工作的人来完成,他们有权与团队一起以批判的眼光审查和测试所有事情。
Microsoft 已在 Visual Studio 中为一个 SQL 支持的 web 应用添加了一个模板,该应用实现了类似的身份设置:
-
启动 Visual Studio 并选择新建项目。
-
选择ASP.NET Core Web 应用模板,点击下一步。
-
将解决方案命名为
Chapter_08_DB_Auth并为本书练习选择合适的位置(如C:\Code\Book\Chapter_08,然后点击创建。 -
Select the Web Application (Model-View-Controller) option and click Change under Authentication. Make sure you select Individual User Accounts and Store user accounts in-app before clicking OK, followed by Create:
![Figure 8.1 – Individual user accounts authentication]()
图 8.1–个人用户帐户身份验证
-
If you take a look at the Data folder, you will see the code that generates a database where the user accounts are stored as shown in Figure 8.2:
![Figure 8.2 – Migrations files in Visual Studio]()
图 8.2–Visual Studio 中的迁移文件
-
Open up
00000000000000_CreateIdentitySchema.cs. It should be 200+ lines of code, and theuserobject looks like this:migrationBuilder.CreateTable( name: ''AspNetUsers'', columns: table => new { Id = table.Column<string>(nullable: false), UserName = table.Column<string>(maxLength: 256, nullable: true), NormalizedUserName = table.Column<string>(maxLength: 256, nullable: true), Email = table.Column<string>(maxLength: 256, nullable: true), NormalizedEmail = table.Column<string>(maxLength: 256, nullable: true), EmailConfirmed = table.Column<bool>(nullable: false), PasswordHash = table.Column<string>(nullable: true), SecurityStamp = table.Column<string>(nullable: true), ConcurrencyStamp = table.Column<string>(nullable: true), PhoneNumber = table.Column<string>(nullable: true), PhoneNumberConfirmed = table.Column<bool>( nullable: false), TwoFactorEnabled = table.Column<bool>(nullable: false), LockoutEnd = table. Column<DateTimeOffset>(nullable: true), LockoutEnabled = table.Column<bool>(nullable: false), AccessFailedCount = table.Column<int>(nullable: false) }, constraints: table => { table.PrimaryKey(''PK_AspNetUsers'', x => x.Id); });这些名称应该是不言自明的,但正如您所看到的,除了用户名和散列密码之外,还有更多的含义。
-
通过快速查看
Startup.cs中的配置,我们可以看到数据库在哪里初始化,需要进行身份验证:public void ConfigureServices(IServiceCollection services){ services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( Configuration.GetConnectionString(''DefaultConnection''))); services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true) .AddEntityFrameworkStores<ApplicationDbContext>(); services.AddControllersWithViews();} -
在尝试运行应用之后,应该有一个用于注册电子邮件地址和定义密码的表单。图 8.3为报名示例:

图 8.3–注册单个用户帐户
如果你仔细查看构建的其他文件,你会注意到实际上有一些代码可以让它全部运行,然后在库中有你看不到的所有东西,巩固了为什么你不愿意自己做这些事情。
像这样的模板在几年前非常流行,因为它们耗费了大量的精力,用户习惯于在他们访问的每个网站上注册。虽然使用它本身并没有什么错——它是安全的,由微软维护——但现在有了其他选择,它变得不那么常见了。
我们将很快恢复常规编程,但前面的代码片段为我们提供了一个切入点,让我们进入一个在技术上与身份无关的主题,但对于理解不同身份片段如何在.NET 应用中发挥作用非常有用。
ASP.NET 中间件的作用与身份
很多技术和产品都是以一个代码名开始的,当微软提出Katana项目时,它肯定对这个名称有着强烈的兴趣。该项目于 2013 年推出,旨在解决.NET 当时的几个缺点。
我们不会拖拽旧的.NET 代码并指出设计中的缺陷,但即使不深入细节,您也可能会联想到替换代码中组件的挑战。比如说,你开始创建一个实用程序来控制家中的一些智能灯泡。在一天的故障排除过程中,您意识到如果捕获一些信息并将其记录下来会更容易。快速而肮脏的方法是在名为log.txt的文件中追加行。这很好地工作,直到您意识到您可以使用一些对非错误条件的洞察,例如在灯光打开和关闭时记录,为自己创建一些统计信息。
当你想在应用之外使用文本文件时,它不容易登录到文本文件中。因此,您意识到在数据库中使用它可能会很好。然后,您必须重写对文件的所有调用,以登录到数据库。你明白了。
如果有一个更通用的log.Info(''Lights out'')方法,而不关心细节,那就太好了。由于日志记录在许多应用中都是一个常见的问题,所以有很多日志记录框架,但每个应用都有一个安装仪式。
这一章是关于身份的,你说这两者之间有什么联系?认证和授权也是应用的常见用例。web 应用中的 URL 路由、缓存以及其他一些东西也是如此。
这些组件的另一个方面是,您最有可能希望在应用初始化期间尽早运行它们——当出现故障时加载日志组件可能太晚了。
这是一个精心设置的说法,微软已经构建了一个称为中间件的抽象。Katana 项目实际上涵盖了四个组件,这将延续到当前的实现中——主机、服务器、中间件和应用。
主机部分可以在Program.cs中找到,对于一个 web 应用,它如下所示:
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>(); });}
如果您将其与我们在第 2 章、跨平台设置中创建的工人服务进行比较,您会发现相似之处:
public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .UseWindowsService() .UseSystemd() .ConfigureServices((hostContext, services) => { services.AddHostedService<Worker>(); });}
你无法通过更改这些行将任何 web 应用转变为服务,但请注意模式是如何相同的。
我们已经提到并查看了Startup.cs文件,在该文件中可以找到服务器和中间件组件。
运行时使用以下代码调用服务器和服务:
public void ConfigureServices(IServiceCollection services){ … services.AddControllersWithViews(); …}
正如我们已经看到的,实际的运行时可能会有所不同,这取决于是在 IIS 中托管还是在 Kestrel 中托管(在本文中这并不重要)。
中间件可在文件的下一节中找到:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env){ if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler(''/Error''); app.UseHsts(); } …}
这被称为管道,它构建为一个序列——例如,身份验证先于授权,但并非所有中间件都对加载它的步骤敏感。
一些中间件有一个二进制行为–UseHttpsRedirection正好支持这种行为,如果您不想要它,只需删除它即可。
UseEndpoints允许您添加要收听的特定端点:
app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: ''default'', pattern: ''{controller=Home}/{action=Index}/{id?}''); });
中间件和身份的美妙之处在于,您可以将自定义中间件添加到组合中,并且由于使用是标准化的,因此之后进行更改是相当轻松的。我们没有将 basicauth 作为中间件实现,但是 visualstudio 中向导添加的用于使用本地数据库的样板文件实现了。
如果我们将身份实现升级为基于 OAuth,这将变得非常方便,下面将介绍 OAuth。
OAuth 和 OpenID 连接基础
基本身份验证易于实现,如果您需要使用遗留系统,很有可能会遇到它。但不建议使用基本身份验证启动新项目。
身份空间中协议的首字母缩略词并不短缺,.NET Framework 多年来一直依赖于不同的身份验证和授权协议。我们无法深入研究所有这些问题,也无法比较它们的优缺点。
目前用于AuthN和AuthZ目的的最流行的一组协议是OAuth和OpenID Connect(OIDC),因此我们将研究的部分理论和实际实现。OAuth 是基本协议,OIDC 构建在这个基础之上,因此有一些重叠的细节我们将继续讨论。
回顾基本身份验证,我们已经提到了一个缺点,即密码是通过网络传输的。客户端和服务器端都可以访问实际密码,在许多情况下,这超出了它们的需要。例如,在允许您访问管理设置之前,web 应用肯定会关心您是否具有管理员角色,但只要标识已建立,密码在执行此授权步骤时不会提供任何值。这只是您需要保护的额外数据。
OAuth 将这些部分解耦,以便服务器端不需要知道密码。对于客户机来说,这更像是一种“取决于”如何处理的情况——如果需要密码,您无法避免在某处键入密码。这一切都是从所谓的JSON Web 令牌(JWTs开始的,所以让我们先来讨论一下。
JSON web 令牌
对于 OAuth 和 OIDC,我们不依赖于将username:password作为王国的钥匙来传递,而是依赖于传递令牌。这些代币称为 JWTs,发音为jot/jots。
JWT 的格式为 JSON,包含三个部分——头、负载和签名。JWT 示例可能如下所示:
{ ''alg'': ''RS256'', ''kid'': ''4B92FBAE5D98B4D2AB43ACE4198026073012E17F'', ''x5t'': ''S5L7rl2YtNKrQ6zkGYAmBzAS4X8'', ''typ'': ''JWT''}.{ ''sub'': ''john.doe@contoso.com'', ''nbf'': 1596035128, ''exp'': 1596038728, ''iss'': ''contoso'', ''aud'': ''MyWebApp''}.[Signature]
如果您以前没有见过类似的情况,您可能(至少)有两个问题:
- 这一切意味着什么?
- 这到底有什么帮助?
此令牌中的信息称为索赔——因此,例如''sub''索赔是主体的缩写,其值为john.doe@contoso.com。此声明通常是用户/用户名(不必是电子邮件格式,但这是常见的)。
其余项权利要求如下。
标题如下:
''alg'':用于生成签名的算法''kid'':密钥标识符''x5t'':密钥标识符''typ'':令牌的类型
有效载荷如下:
''nbf'':之前没有。令牌生效的时间;通常与发行时间相同。''exp'':到期时间。令牌在之前的有效时间。通常从发行时算起一小时(但这取决于令牌发行人)。''iss'':发行人。代币的发行人。''aud'':观众。代币是给谁的;通常,令牌用于的应用。
这只是一个最小的示例标记–如果愿意,您可以拥有更多声明,并选择这些声明的格式。如果您想要一个值为''bar''的''foo''声明,而该声明仅对您的应用有意义,这是可以的。请注意,令牌的大小并不是无限的——在企业环境中,一些开发人员试图包括用户所属的所有组。当用户是 200 多个组的成员时,您会遇到所谓的令牌膨胀,这会导致令牌在通过网络传输时碎片化。在大多数情况下,这些数据包没有正确地重新组装,导致数据包崩溃。
将令牌传递给服务器类似于基本身份验证,因为我们添加了一个授权头,其中令牌是 Base64 编码的(为简洁起见,令牌被缩短):
Authorization: Bearer eyJhbGciOi...PDh4ck7Q
这是很好的,因为您可以发送比传递用户名和密码时更多的信息,同时仍然将凭据保留在数据传输之外。它被称为不记名代币,因为拥有它的任何人都可以使用它。这让我们回到第二个问题——这如何更好?你得到的第一印象是,任何客户都可以自己制作代币,这听起来不是一个好的机制。
OAuth/OIDC 事务中有两个重要操作:
- 发放代币:这是关于控制谁获得代币,这将受到一个或多个机制的保护。
- 验证令牌:此是关于检查令牌是否可信以及内容是什么。
这两种方法都主要基于使用证书——在颁发时签名,在验证时验证。(注意,这与基于证书的身份验证不同;这里我们只关注令牌本身。)
让我们来看看这在代码中是如何工作的。
如何生成/发行代币
在第 2 章跨平台设置中,我们展示了如何生成证书,在 Windows 和 Linux 上安装证书,以及随后的阅读。在此基础上,我们可以使用相同的证书对令牌进行签名。
要创建一个将生成令牌的应用,请执行以下操作:
-
打开命令行并创建一个新目录(
Chapter_08_BearerAuthClient。 -
运行
dotnet new console命令。 -
运行
dotnet add package System.IdentityModel.Tokens.Jwt命令。 -
We then need to add some code to
Program.cs. First, we create the token (based on a generic template):static void Main(string[] args){ jwt = new GenericToken { Audience = ''Chapter_08_BearerAuth'', IssuedAt = DateTime.UtcNow.ToString(), iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(). ToString(), Expiration = DateTime.UtcNow.AddMinutes(60).ToString(), exp = DateTimeOffset.UtcNow.AddMinutes(60). ToUnixTimeSeconds().ToString(), Issuer = ''Chapter 08'', Subject = ''john.doe@contoso.com'', };然后,我们设置/检索用于签名的证书:
SigningCredentials = new Lazy<X509SigningCredentials>(() => { X509Store certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser); certStore.Open(OpenFlags.ReadOnly); X509Certificate2Collection certCollection = certStore.Certificates.Find( X509FindType.FindByThumbprint, SigningCertThumbprint, false); // Get the first cert with the thumbprint if (certCollection.Count > 0) { return new X509SigningCredentials(certCollection[0]); } throw new Exception(''Certificate not found''); });最后一个部分是排列声明并创建实际签名的令牌:
IList<System.Security.Claims.Claim> claims = new List<System.Security.Claims.Claim>(); claims.Add(new System.Security.Claims.Claim(''sub'', jwt.Subject, System.Security.Claims.ClaimValueTypes.String, jwt.Issuer)); // Create the token JwtSecurityToken token = new JwtSecurityToken( jwt.Issuer, jwt.Audience, claims, DateTime.Parse(jwt.IssuedAt), DateTime.Parse(jwt.Expiration), SigningCredentials.Value); // Get the string representation of the signed token and // print it JwtSecurityTokenHandler jwtHandler = new JwtSecurityTokenHandler(); output = jwtHandler.WriteToken(token); Console.WriteLine($''Token: {output}'');}请注意,为了关注重要的部分,这不是完整的代码——请查看本章的 GitHub repo 以了解完整的代码。
-
Run the
dotnet runcommand.您的输出将类似于图 8.4:

图 8.4-A JWT
这不是供您阅读的,但它是可逆的,因为它只是 Base64 编码的。最重要的一点是,你真正的秘密没有包括在内,所以即使有人能读到,这也不是问题。
如何验证令牌
生成一个令牌很不错,但毫不奇怪,我们需要一个对应的令牌—检查令牌是否良好,并根据此评估允许或拒绝访问。为此,我们还将创建一个服务器端代码示例:
-
打开命令行并创建一个新目录(
Chapter_08_BearerAuthServer。 -
运行
dotnet new console命令。 -
运行
dotnet add package System.IdentityModel.Tokens.Jwt命令。 -
The following code goes into
EchoController.cs:[HttpGet]public String Get(){ var audience = ''Chapter_08_BearerAuth''; var issuer = ''Chapter 08''; var authHeader = HttpContext.Request.Headers [''Authorization'']; var base64Token = AuthenticationHeaderValue.Parse( authHeader).Parameter; JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler(); TokenValidationParameters validationParameters = null; validationParameters = new TokenValidationParameters { ValidIssuer = issuer, ValidAudience = audience, ValidateLifetime = true, ValidateAudience = true, ValidateIssuer = true, //Needed to force disabling signature validation SignatureValidator = delegate (string token, TokenValidationParameters parameters) { var jwt = new JwtSecurityToken(token); return jwt; }, ValidateIssuerSigningKey = false, }; try { SecurityToken validatedToken; var identity = handler.ValidateToken(base64Token, validationParameters, out validatedToken); return ''Token is valid!''; } catch (Exception e) { return $''Token failed to validate: {e.Message}''; }}与前面的代码示例一样,为了便于阅读,部分代码被省略了。
-
运行
dotnet run命令。 -
后退到客户端代码,添加以下代码:
HttpClient client = new HttpClient();client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(''Bearer'', output);var response = client.GetAsync(''https://localhost:5001/Echo'').Result; Console.WriteLine(response.Content.ReadAsStringAsync().Result. ToString()); -
Run the
dotnet runcommand in this folder while the server part is running.您应该看到一个输出,上面写着
Token is valid。
虽然服务器代码中有一些术语直观地具有某种意义,但可能需要对该过程进行一点解释。
最基本的是,我们为发行者(发行令牌的人)和受众(令牌的预期接收者)配置值。然后,我们配置用于验证令牌的参数;上述受众和发行人以及代币的时间戳。
如果令牌有效,我们将返回一条指示有效的消息,如果失败,我们将返回另一条消息。
在这段代码中,我们禁用了对签名的检查,这似乎违反了直觉。您应该始终验证签名–如果不验证,任何人都可以生成一个令牌,只要他们找到正确的插入值,该令牌将被视为有效。禁用这一难题的重要部分的原因是,如果我们想这样做,代码将变得更加复杂。我们需要先讨论一些额外的主题,然后再回到一种需要较少复杂性才能正确使用的方法。
OAuth 流
能够向 API 发送一个令牌并对其进行验证是非常好的,但是您可能想知道这在应用中实际是如何工作的。我们在这里使用的详细信息中不能有用户类型,即使我们只在服务器上这样做,也不涉及任何凭据。这听起来不像你在现实生活中实际使用的东西。
JWT 是 OAuth 的一个核心部分,但协议的内容不仅仅是令牌。OAuth 由我们所称的“流程”组成,这些流程规定了获取和使用所述令牌的过程中的步骤。这里我们无法涵盖这些流的所有变体,但我们将介绍一些与 ASP.NET Core 用例相关的变体。
我们需要整理几个适用于所有流的术语。
我们有一个称为身份提供者的中央服务,而不是处理代币发行的每个应用。此服务通常验证凭据(密码、证书等),并负责颁发令牌。虽然这在技术上是您可以自己实现的,但强烈建议您在市场上使用一个成熟的解决方案(我们将考虑为此使用Azure AD。
当获取令牌时,客户端请求它想要的权限。这些权限称为作用域,并作为声明嵌入到令牌中。
这里描述的流驱动了 Facebook、Google 和 Microsoft 的登录,因此您很可能已经尝试过了,即使您当时没有考虑太多。(这些提供程序支持多个流以支持不同的用例。)
OAuth 客户端凭据授予
最容易理解的流程可能是客户端凭据流程,因为这最接近于使用用户名和密码的。您将在 UI 中为正在使用的身份提供程序注册一个应用,并获得客户端 ID 和客户端机密。当您想要获取令牌时,可以将这些令牌发送给身份提供者,并指示您想要的权限。流程如图 8.5所示:

图 8.5–OAuth 客户端凭据流
需要注意的一件非常重要的事情是,此流仅适用于受信任的客户端。受信任的客户端通常运行在最终用户无法使用代码和配置的服务器上。这通常是一个服务帐户,或服务器端呈现的 web 应用。客户机 ID 不敏感,但与客户机机密配对后,它可能使任何拥有它的人都能够提取他们不应该拥有的信息。如果您有一个客户端应用(如下载到浏览器的 JavaScript)、一个移动应用或类似的应用,则永远不要使用客户端凭据流。
客户端密码通常太长、太复杂,用户无法记住和输入,因此对于密码,有不同的流程。
OAuth 资源所有者密码凭据
与客户端凭据类似但用于用户凭据的流是资源所有者密码凭据(ROPC流。当使用外部身份提供者时,通常会有预定义的登录体验外观,通常在浏览器中呈现为 HTML。即使有一个选项可以根据你自己的喜好来设计它,但使用用户体验的人会说,他们需要以某种方式调整某些元素,以使他们感到高兴,这并不罕见。
此时,您可能会想,如果您可以自己创建所有可视方面并像实现服务器端身份验证体验一样处理身份验证,那将是一件非常棒的事情。此流程中存在这样的选项,但您永远不应该向设计人员承认它的存在。Microsoft 和 identity 社区非常不鼓励使用此流程,因为它本质上不如直接在专门的产品上处理凭据交换以处理身份用例。该应用承担了更多的责任,因为它将知道用户的密码。
我们之所以在这里提到它,是因为意识到它是有用的,即使它没有出现。
OAuth 授权码授予
在本机应用中执行身份验证的推荐方法是一个称为授权代码流的流。当你第一次遇到它时,它可能会显得有点复杂,但它背后有一个逻辑。我们需要用户手动输入他们的凭据,但应用不应该知道他们。同时,我们希望应用在调用 API 时也是一个实体。图如图 8.6所示:

图 8.6–OAuth 授权代码流
授权端点和令牌端点都位于标识提供程序上。
这个图 Po.T0A.不覆盖低级细节,但在这种情况下可能的攻击向量是,例如,在移动设备上,恶意的 To1 T1 应用可能能够拦截 AUTH 代码并使用它作为其未经批准的目的。建议您实现一个名为代码交换验证密钥(PKCE–发音为pixie)的流扩展,以确保只有正确的应用才能使用特定的身份验证代码。
OAuth 隐式授权流
基本上很清楚什么是经典的 web 应用,什么是经典的原生应用,但基于 JavaScript 的单页应用(SPA)在哪里合适呢?从某种意义上说,它是一种混合,您可以通过本地执行的浏览器提供代码。这意味着你不能认为它是一个可信的客户端。您将看到许多指南提到为这些目的使用隐式授权流。看起来像图 8.7:

图 8.7–OAuth 隐式授权流
这里的片段的意思是,当重定向回 SPA 时,令牌将是 URL 的一部分,而不是在 HTTP 响应的主体中返回。这是因为大多数 SPA 不像非 SPA web 应用那样“在页面之间跳转”,需要通过 URL 使用数据。
虽然在某些用例中,隐式授权是合适的,并且在很多地方都在使用,但当前的建议是,使用 PKCE 的身份验证代码更适合大多数 SPA。隐式授权不太安全,因此虽然它在功能上是可以接受的,但它还有其他缺点。
注意,如果您使用库来提供此功能,您应该尝试找出它在幕后使用的两个流中的哪一个。
OpenID 连接
之前的所有流程都集中于获取表示“允许您访问此 API”的令牌。当然,这是一个非常重要的解决方案,但如果您尝试在不接触 API 的情况下登录 web 应用,您通常只想知道“谁登录了”。为此,我们有 OIDC 流程,或者更准确地说,如图 8.8所示,OAuth 顶部有一个单独的协议构建:

图 8.8–OIDC
OIDC 协议还包括一些其他功能,使开发人员更容易登录,我们将在代码示例中介绍这些功能。
还有其他 OAuth 流,它可以比我们在这里所展示的更详细,但是它超出了本书的范围,无法涵盖 AuthN 和 AuthZ 的所有细微差别。
如果没有身份提供者,这些流是不好的,因此在下一节中,我们将使用流行的提供者将所有内容放到上下文中。
与 Azure Active Directory 集成
如果您在过去 20 年中登录到公司计算机,那么无论您是否意识到,您都可能使用过 Active Directory。AD 是在 Windows Server 2000 中引入的,扩展了 Windows NT 4.0 中引入的域概念,以提供集中式身份的完整实现。当您登录到 Windows 桌面时,只要您坐在办公室,就可以轻松访问组织中的文件共享和服务器。
有了 AD,您至少需要在本地和配套的基础设施上安装两台服务器。这在当今的云世界是不可行的,但微软在必须提供Azure Active Directory(AAD作为云身份提供商的基础上,同时打破了物理位置的限制。
AD 基于旧的身份协议,因此 OAuth 流和 OIDC 本机不受支持,但需要使用Active Directory 联合服务(ADFS)作为额外服务来支持我们刚才描述的内容。与 Windows Server 许可证相比,这不会带来额外的成本,但建议为此服务配备专用服务器。
相反,AAD 是在考虑较新协议的情况下构建的,因此它不支持没有附加组件的较旧协议。
这意味着,如果您希望将现有的支持广告的本地应用迁移到 AAD,则可能需要对标识堆栈进行一些重写。我们将不讨论这一点,而是直接讨论较新的协议。AAD 是基于开放标准的,您可以很容易地用符合这些标准的其他身份提供程序替换它,因此这也不是微软的锁定。
AAD 的基本形式是免费的。有一些高级安全功能不是免费提供的,并且限制为 50000 个对象,但即使对于许多生产部署,这也应该足够了。根据本章开头列出的技术要求,我们假设您拥有这些样本的 AAD 租户,因此如果您尚未注册,则应立即注册。
使用 AAD 解锁 Azure 门户中的一系列选项。例如,您可以控制我们描述的所有流是可用的,还是只使用一个子集。此外,您可以指定哪些用户具有访问权限,应用可以访问哪些其他数据源,等等。
如果您有一个现有的 web 应用,可以在此基础上添加对 AAD 的支持,但为了简化问题,我们将从头开始创建 Blazor 应用,使用 Visual Studio 中的向导在 Azure 中为我们进行后端配置:
-
启动 Visual Studio 2019 并选择创建新项目。
-
选择Blazor App点击下一步。
-
将解决方案命名为
Chapter_08_AADAuth。 -
点击认证下的变更。
-
Select Work or School Accounts and select Cloud - Single Organization as shown in Figure 8.9:
![Figure 8.9 – Work or School Accounts]()
图 8.9——工作或学校账户
-
键入您将使用的 AAD 租户的域名。如果您以前没有登录,系统将提示您登录。
-
在点击创建之前,确保您选择了Blazor 服务器应用并且您已经选中了配置 HTTPS。
如果您尝试运行应用,首先会遇到的是 Microsoft 提供的登录表单,如图 8.10所示:

图 8.10–AAD 登录
输入用户名和密码后,下一步是请求权限,如图 8.11 所示:

图 8.11–同意通知
如果您点击接受按钮,应用将打开,在右上角,您将收到您的名字。看起来很简单,但是让我们先看看代码中的内容,然后再添加一些功能。
如果打开Startup.cs,您可能会注意到一些迄今为止尚未看到的代码:
public void ConfigureServices(IServiceCollection services){ services.AddMicrosoftIdentityWebAppAuthentication(Configuration, ''AzureAd''); services.AddControllersWithViews() .AddMicrosoftIdentityUI(); services.AddAuthorization(options => { // By default, all incoming requests will be authorized // according to the default policy options.FallbackPolicy = options.DefaultPolicy; }); services.AddRazorPages(); services.AddServerSideBlazor() .AddMicrosoftIdentityConsentHandler();}
在上一节中,我们提到了更换身份中间件是多么容易,我们可以在这里看到启动管道如何看到添加了中间件来处理身份和相关 UI。
如果我们看一下appsettings.json,我们可以看到我们的具体配置存储在哪里:
{ ''AzureAd'': { ''Instance'': ''https://login.microsoftonline.com/'', ''Domain'': ''contoso.com'', ''TenantId'': ''tenant-guid'', ''ClientId'': ''client-guid'', ''CallbackPath'': ''/signin-oidc'' },
您可能会发现,甚至在看到网页之前就被登录提示击中,这有点不友好。有很多页面在您未登录时提供默认体验,登录时功能解锁。
这由Startup.cs中的几行代码控制:
//Comment out the line below like this //services.AddRazorPages();
//And replace with this services.AddRazorPages(options =>{ options.Conventions.AllowAnonymousToPage(''/_Host''); });
请注意,这将有效地关闭 Blazor 应用中所有页面的授权,因此您需要在需要的页面上启用它。(关于如何更改默认行为的详细信息在不同的视图引擎(MVC、Razor 页面和 Blazor)之间有所不同。)
您可以将Index.razor的内容替换为以下代码:
@page ''/''
<AuthorizeView> <Authorized> Hello, @context.User.Identity.Name! <table class=''table''> <thead> <tr> <th scope=''col''>Claim Type</th> <th scope=''col''>Claim Value</th> </tr> </thead> <tbody> @foreach (var claim in context.User.Claims) { <tr> <td>@claim.Type</td> <td>@claim.Value</td> </tr> } </tbody> </table> </Authorized> <NotAuthorized> <p>For full functionality please log in</p> <a href=''MicrosoftIdentity/Account/SignIn''>Log in</a> </NotAuthorized></AuthorizeView>
这将打印您令牌中的所有声明,这仅在登录时才有意义,并在您尚未进行身份验证时为登录提供一个链接。这种方法适用于需要一个页面可供登录用户和匿名用户使用的情况。
如果要阻止页面的所有内容,可以通过添加[Authorize]属性(在Counter.razor中):
@page ''/counter''@attribute [Authorize]
<h1>Counter</h1>
未登录的用户只会看到一条消息,表明他们未经授权。
有多种方法可以配置此功能。您可以创建需要显示特定声明的策略,可以创建控制对视图的访问的角色,等等。我们不建议把它变得比必要的更复杂,尤其是在开始的时候。排除故障可能会很麻烦,因此首先要正确掌握基本知识。
理解单租赁与多租赁
在向导中,我们选择了云-单组织,但是如果您选中下拉列表,您可能会注意到云-多组织。我们或许应该解释一下。
这里的一个组织是 AAD 租户。这意味着,如果您的公司结构有多个租户,则这被视为多个组织,即使它可能只是一个合法组织。这是一个纯粹的技术定义。
当您创建单个组织应用时,这意味着只有一个特定 AAD 租户的用户才能登录,并且使用的数据主要是此租户的数据约束。如果您构建的应用只供您和您的同事使用,这是一个很好的选择,因为会有一个逻辑边界,并且您不会最终将数据泄漏到其他组织中。
对于多组织应用,您希望更改配置的原因有两个。假设我们有一家向企业销售电脑用品的网络商店。我们假设我们的大多数客户已经拥有 AAD–我们没有实现我们自己的用户数据库,而是提供客户租户的 AAD 登录。尽管我们有一个销售共享数据库,但我们可以强制执行,例如,只有从contoso.com登录的用户才能访问标有Contoso作为公司名称的订单。
另一个稍微不同的设置是,我们是一家向企业销售软件的 ISV。如果一家公司已经在使用 AAD,单点登录通常是他们的首选。该应用的架构可以让人产生一种为一个组织服务的错觉,但它可以在不同的公司间重用一套通用的用户管理。
多租户应用中的默认设置是允许 AAD 中的所有租户进行身份验证。如果需要,可以通过编辑令牌验证参数来限制这一点,但最重要的是,您还需要了解授权设置。
理解同意和许可
您在运行应用时被要求授予权限,但我们没有真正解释这一部分。基本概念应该很容易理解–如果您使用其他 Microsoft 服务,例如 Office 365,您的 AAD 帐户可能会解锁对大量数据的访问。我们不希望应用能够获取它想要的任何东西,因此作为一种保护措施,该应用必须请求访问,并且必须获得授权。
有两种类型的权限:
- 委托权限是在用户上下文中有效的权限。例如,如果应用想要读取您的日历,您作为用户必须授予此权限。您的同意仅适用于您–它不允许应用读取其他用户的日历。
- 应用权限是在更广泛的应用上下文中有效的权限,通常在后端。比如说,应用需要能够列出组织中的所有用户——这不是特定于您的数据。此权限需要由全局管理员授予。这意味着,如果您不是全局管理员,并且没有这些权限应用无法运行,则在组织中具有相应角色的人员同意之前,您不能使用该应用。
如前所述,代码中这些权限的技术术语为范围。默认的 OIDC 流请求offline_access和User.Read作用域,如果您想读取日历,您可以添加Calendars.Read。这可以在Startup.cs中找到:
public void ConfigureServices(IServiceCollection services){ services.AddAuthentication (OpenIdConnectDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApp(options => { Configuration.Bind(''AzureAD'', options); options.ResponseType = ''code''; options.SaveTokens = true; options.Scope.Add(''offline_access''); options.Scope.Add(''User.Read''); options.Scope.Add(''Calendars.Read''); });…
请注意,虽然不会再次提示您同意不同的登录之间的同一组权限,但如果应用请求的范围超过您最初同意的,则您需要重新同意。
您可能会想,我们如何确定作用域的名称?如果您在 Azure 门户中找到应用注册,您可以动态浏览列表,如图 8.12所示:

图 8.12–Azure 门户中的权限列表
当然,对于 Microsoft API,它也列在在线文档中,因此您不必猜测权限的名称。
拥有阅读日历的权限是有帮助的,但这并不意味着日历条目开始自己涌入。这需要更多的代码。不过,我们需要先阐述几个概念。
AAD 租户中的每个用户都可以进行身份验证并获取令牌。这是通过 AAD 端点完成的,在我们使用的代码中,这是通过Microsoft.Identity.Web库完成的。这是为了后端使用,例如运行服务器端的 web 应用(我们使用 Blazor 服务器)和受保护的 web API。
为了在客户机上获取令牌,我们使用一个不同的库,称为Microsoft 身份验证库(MSAL),它可以在 C#中的本机应用、基于 JavaScript 的 web 应用等上运行。它使用相同的端点,但实现不同的 OAuth 流。在互联网上搜索时,你可能会遇到一个名为ADAL的图书馆,这是一个旧的、不推荐使用的图书馆;你不应该再使用它了。
日历数据取决于是否拥有 Office 365 许可证。这些数据通过 Microsoft Graph 公开,Microsoft Graph 是许多 Microsoft 服务的网关,提供了一个一致的 API 界面。要与 Microsoft Graph 交互,可以在使用上述库之一获取令牌后使用 Microsoft Graph NuGet 包。
说到这里,我们可以回到如何阅读日历条目的问题上来。
客户机已经获得了一个令牌,因此第一种方法可能是认为可以相当容易地利用它。但应用无法直接访问令牌,因为它存储在浏览器会话中,因此您需要通过一些额外的步骤来检索它。幸运的是,微软已经通过Microsoft.Identity.Web库简化了这些步骤。
在幕后,库代表(海外建筑运营管理局)调用名为的 OAuth 流。这里我们并没有描绘流程的全貌,但高级视图是,应用首先允许用户进行身份验证,然后再使用令牌对身份提供商进行第二次调用,并对其本身进行身份验证。这使应用能够在拥有大量后端 API 时构建更复杂的场景。
为了实现这一目标,我们必须做几件事:
-
转到 Azure 门户并在 AAD 中找到应用注册。
-
进入API 权限刀片,点击添加权限。
-
选择微软图形,即委托权限权限类型,在列表中定位
Calendars.Read和Calendars.ReadWrite。 -
点击添加权限。
-
进入证书和机密刀片,点击新客户机密。在点击添加之前,给它一个名称,如
MySecret并选择其到期时间。 -
立即复制该秘密的副本,因为它在离开页面后将无法检索。
-
将新配置添加到
appsettings.json:''AzureAd'': { … ''ClientSecret'': ''copied from the portal'', ''CallbackPath'': ''/signin-oidc''},''Graph'': { ''BaseUrl'': ''https://graph.microsoft.com/v1.0'', ''Scopes'': ''user.read calendars.read calendars. readwrite''},''Logging'': { -
返回到
Startup.cs并将我们之前添加的代码更改为:string[] initialScopes = Configuration.GetValue<string>(''Graph:Scopes'')?.Split(' '); services.AddAuthentication(OpenIdConnectDefaults. AuthenticationScheme) .AddMicrosoftIdentityWebApp(Configuration.GetSection(''AzureAd'')) .EnableTokenAcquisitionToCallDownstreamApi (initialScopes) .AddInMemoryTokenCaches() .AddMicrosoftGraph(Configuration.GetSection(''Graph'')); -
Since this is a Blazor app, we will add a page called
Calendarto show the calendar entries. The first part is adding the following at the top:@page ''/Calendar''@using Microsoft.Graph @inject Microsoft.Graph.GraphServiceClient GraphClient注入的
GraphClient负责传递调用 Microsoft Graph 所需的令牌。 -
您需要一个代码段来实际调用图形:
```cs
@code{ private List<Event> eventList = new List<Event>();
protected override async Task OnInitializedAsync() { try { var events = await GraphClient.Me.Events.Request() .Select(''subject,body,organizer,start,end,location'') .GetAsync();
eventList = events.CurrentPage.ToList(); } catch (Exception ex) { var error = ex.Message; } }}
```
- Then, you need to print it all out, as shown in the following code block:
```cs
<AuthorizeView> <Authorized> <table class=''table''> <thead> <tr> <th scope=''col''>Subject</th> <th scope=''col''>Start</th> <th scope=''col''>Entry</th> </tr> </thead> <tbody> @foreach (var entry in eventList) { <tr> <td>@entry.Subject</td> <td>@entry.Start.DateTime.ToString()</td> <td>@entry.End.DateTime.ToString()</td> </tr> } </tbody> </table> </Authorized> <NotAuthorized> <p>For full functionality please log in</p> <a href=''MicrosoftIdentity/Account/SignIn''>Log in </a> </NotAuthorized></AuthorizeView>
```
我们将包装在`AuthorizeView`内,以避免因未登录而产生的任何错误–如果您未登录,您将无法获得任何数据,因此从这个意义上讲跳过它并不危险,但我们喜欢对用户有意义的消息,而不是不起作用的消息。
- 运行应用并手动将
/Calendar添加到 URL,您应该会看到一个条目列表,如图 8.13所示:

图 8.13–日历条目
请注意,在调试模式下运行时,您可能需要注销并再次登录才能在使用令牌时正常工作。这可能是由于在两次运行之间(使用内存缓存时)清空令牌缓存时,浏览器存储会话造成的。
我们已经走了很长一段路,但仍有一些事情需要考虑,例如扩展到您当前的 AAD 租户之外。
使用联邦身份
由于您与分配给您的特定 AAD 租户集成,因此很容易将其视为您的身份提供者。不过,微软的运营规模更大,而且在技术层面上,您正在与外部身份提供商联合。
那么,这到底意味着什么?
回到现实世界中我们最初的例子,你可以说护照是联邦身份的一个例子。即使您不是签发护照的实体,您也相信签发机关有一个良好的程序,并且您接受该程序作为身份证明。您可以选择不信任此身份,并构建自己的系统来验证人们是否是他们所说的人,但如果您能够提供相同级别的真实性,则很可能会耗费大量时间和成本。在不同国家订购护照的麻烦程度可能会有所不同,但试想一下,作为一名旅行者,在你旅行到的不同国家获得多张护照是多么不友好。
在过去的几年里,你很可能看到了在你访问过的网站上使用 Facebook 或 Google 登录的选项。您可以单击这些按钮,而不是创建新帐户。只要您接受网站能够读取您的某些身份属性,您就可以继续。当然,这些提供商的信任度可能低于你们国家的联邦实体,但很可能他们已经投入了相当大的努力来确保他们的用户帐户数据库是安全的,不太容易被黑客攻击。对你来说,作为一个用户,他们让你不用再去想另一个要记住的密码了。
护照和谷歌账户都是联邦身份的例子。虽然您的应用可能有一个用于访问和授权目的的用户数据库,但您只有一个对其身份的引用,因为该身份是由您信任的其他人提供的,您可以提供身份验证服务。
在高层次上,您需要在所选身份提供程序的控制窗格中为应用创建一个帐户,在该窗格中,您提供了两个相关属性,并且相应地,您可以像上一节中那样配置元数据,指向身份提供程序。
.NET5 和 ASP.NETCore5 提供了一些库,这些库可以帮助您完成这项工作,而单独完成并不一定很难。然而,在你的应用的生命周期中发生的事情是,你从谷歌和 Facebook 开始,它正在工作。然后,有人要求你添加苹果,让 iOS 用户更容易使用。然后添加一个使用“姓氏”而不是“姓氏”的提供程序,打破数据模型。即使您的回答是您喜欢挑战,也可能是由于您开始添加越来越多的逻辑来处理它,需要新的构建和发布,因此您的登录代码变得臃肿,从而导致了摩擦。
正如你可能猜到的,这导致不可避免的出现了一个 Azure 服务。AAD 的一个版本称为 AAD B2C,旨在处理此类场景。B2C部分代表企业对消费者,但它实际上是关于外部身份的。它的工作方式是,您建立一个嵌套的联盟,您的应用信任 AAD B2C,AAD B2C 反过来信任其他身份提供商。如果您需要添加新的提供商或自定义声明,您可以在 Azure 中这样做,而无需重新编译应用。
AAD B2C 中实际上有两种类型的用户帐户:本地帐户和社交帐户。在本文中,Social 是 federated 的另一个术语,因为它本身不必是社交网络上的帐户。美妙之处在于,有几个预先创建的提供者可以通过一个向导轻松添加,如图 8.14所示:

图 8.14–身份提供者选择
如果您的提供程序不在列表中,则可以添加通用 OIDC 提供程序。如果您想要非标准配置,您甚至可以添加一个非 B2C AAD 租户作为身份提供者。
本地帐户不与其他提供商联合,而是 AAD 的专用版本,用于添加具有任何电子邮件地址的个人帐户。一个普通的广告租户通常是一个组织,在这个组织中,用户可以查找其他用户的详细信息,成为组的一部分,等等,这是很正常的。在 B2C 租户中,每个用户都是一个孤岛,无法看到其他用户。如果您还记得我们以数据库的形式创建本地帐户的示例,您可以说这与此相竞争,但它的功能更强大,而且在大多数情况下,比维护自己的数据库更易于使用。
可以通过向导配置不同类型的用户旅程(注册、登录、密码重置),如果愿意,还可以替换样式。
如果您想更深入,还可以选择使用自定义策略,这需要深入 XML 文件以获得类似于编码的体验。它提供了极大的灵活性,可以在流程中调用后端 API,等等。请注意,这可能与用户友好相反,因此只有在向导驱动的策略没有涵盖您的用例时才使用它。
虽然 AAD B2C 具有与常规 AAD 不同的功能集,但用于获取令牌的端点也符合标准,因此调整代码相当容易。
在基本形式中,您实际上可以使用与我们使用常规 AAD 进行身份验证相同的代码,并将appsettings.json更改为指向 B2C 租户,该租户具有在该租户中创建的属性。如果您只定义了一个处理注册和登录的流,那么这将非常有效。如果您还想提供诸如密码重置和配置文件编辑等选项,则该选项将不起作用。
在全面了解 AAD B2C 服务之前,推荐的入门方法是让 Visual Studio 在 Visual Studio 中创建项目时选择使用 B2C 作为提供程序,从而为您生成内容。可在个人用户账户和连接到云中已有的用户存储下找到选择,如图 8.15所示:

图 8.15–AAD B2C 认证选项
乍一看,AAD B2C 似乎为不明确的好处增加了复杂性,因为这些事情可以直接在代码中实现。需要明确的是,就像许多其他事情一样,有好的用例,也有不太好的用例。最棒的是,如果你想使用 B2C,它只需要对代码进行很少的更改,而且 AAD B2C 中的大部分工作可以“外包”给身份认证专家。
关于身份信息系统的一点注记
无论您是从头开始编写自己的身份实现,还是依赖 AAD,如果用户要输入用户名和密码,您都需要一个 UI。一般来说,有三种不同的方法来实现这一点:
- 弹出:您可以打开一个单独的较小窗口,供用户输入凭证。一旦它们被验证,弹出窗口就会消失,而你又回到了 web 应用中。从技术角度来看,这种方法没有什么错,但许多用户在浏览器中阻止了弹出窗口,许多人认为它是一个恼人的 UI。
- 重定向:我们在与 AAD 集成时实现的方法是基于重定向的。您从
https://localhost开始,被发送到https://microsoftonline.com,然后再次返回https://localhost。这是一种非常常见的方法。它很容易实现,并且以安全的方式支持我们描述的流。 - Iframe:最圆滑的方法可能是将登录表单作为 web app 的一部分嵌入,并将用户保持在相同的上下文中。为了实现这一点,您需要在后端使用 cookie 和会话执行一些技巧。当您控制所有内容时,这不是一个问题,但是如果您想要使用联合身份,这将成为一个问题。单租户 AAD 理论上可以支持 Iframe,但在撰写本书时没有这样做。Facebook 和谷歌等提供商不支持该服务,原因是存在安全隐患——例如,创建用于获取密码的登录体验。此外,主要浏览器正在实施更多阻止第三方 cookie 的机制,以确保隐私,因此可能也会在那里被阻止。在尝试实现此 UI 之前,请确保您掌握了所有移动部件。
总结
本章带领我们从基本身份验证到联邦身份验证。它首先解释了身份验证和授权的含义。这里有一些细节,比如了解 Base64 编码和哈希的好处。AuthN 和 AuthZ 的示例实现旨在让您更好地了解正在发生的事情,尽管您可能不会实现或使用所有这些技术。OAuth 的演练和 AAD 的引入将使您能够在 web 应用中实现产品级标识。
并不是每个应用都需要超级安全,但这应该让你的网络应用比将所有访问者视为匿名用户更加个人化。
随着身份的讨论,下一章将深入到另一个热门话题,我们将介绍使用容器的细节。
问题
- 身份验证和授权之间有什么区别?
- 对于 web 应用中的前端用例,哪种 OAuth 流最常见且最推荐?
- 你为什么要使用 AAD B2C?
进一步阅读
- Microsoft identity platform 文档,可在获取 https://aka.ms/aaddev
- Microsoft Graph 登录页,可从获取 https://developer.microsoft.com/en-us/graph
九、Docker 入门
在上一章中,我们介绍了标识及其如何应用于 ASP.NET 5。身份是 web 应用开发的核心,因此我们介绍了几种形式的身份验证(您是谁)和授权(您可以做什么)。我们介绍了基本身份验证、OAuth、OIDC、Azure Active Directory 和联邦身份。
本章介绍容器和流行的 Docker 平台。容器是一个软件包,包含代码和运行所需的所有依赖项。这种软件打包技术源于在测试和生产环境中从开发人员的机器可靠地部署和运行软件的需要。通过使用容器,在每个环境中使用相同的包,这大大减少了可能出错的数量。
本章将介绍以下主题:
- Docker 化概述
- Docker 入门
- 在 Docker 上运行 Redis
- 访问容器中运行的服务
- 创建 Docker 映像
- Visual Studio 对 Docker 的支持
- 多容器支持
本章结束时,您将熟悉容器,并将获得在 Docker 中创建容器的实践经验。
技术要求
本章包括简短的代码片段,以演示所解释的概念。需要以下软件:
- Visual Studio 2019:Visual Studio 可从下载 https://visualstudio.microsoft.com/vs/community/ 。社区版是免费的,将用于本书的目的。
- .NET 5:可以从下载.NET frameworkhttps://dotnet.microsoft.com/download 。
确保下载 SDK,而不仅仅是运行时。您可以通过打开命令提示符并运行dotnet --infocmd 来验证安装,如图 9.1所示:

图 9.1–验证.NET 的安装
作为本章的一部分,我们将安装 Docker。这可能需要一些额外的设置,具体取决于您使用的是 Windows 10 还是 Mac。安装 Docker部分中的安装说明是为 Windows 10 编写的。除本章中提供的说明外,请使用以下资源:
- Mac 上的 Docker 桌面:https://docs.docker.com/docker-for-mac/install/
- Windows 上的 Docker 桌面:https://docs.docker.com/docker-for-windows/install/
本章的源代码位于 GitHub 存储库中的https://github.com/PacktPublishing/ASP.NET-Core-5-for-Beginners/tree/master/Chapter%2009 。
请访问以下链接查看 CiA 视频:https://bit.ly/3qDiqYY
硬件虚拟化
以下说明和相应的图像是针对 Windows 10 环境编写的。有关在 Mac 上安装的说明,请参阅 Docker 文档。
笔记
对于某些步骤,可能需要管理员权限。
在安装任何软件之前,让我们检查是否支持硬件虚拟化。使用任务管理器,查看性能选项卡。虚拟化支持如图 9.2所示:

图 9.2–启用了虚拟化
如果未启用硬件虚拟化,将显示如下错误消息:
Please enable the Virtual Machine Platform Windows feature and ensure virtualization is enabled in the BIOS.
在桌面 BIOS 中启用硬件虚拟化。请使用主板制造商提供的文档获取说明。
除硬件虚拟化外,还必须启用Hyper-V和容器Windows 功能,如图 9.3所示:

图 9.3–Windows 功能
这涵盖了安装的基础。如果您在虚拟机和/或 Windows Home 上运行,将添加以下两个部分以帮助您。
虚拟机安装
在虚拟机(虚拟机)上安装 Docker 与我们刚刚做的非常相似。必须启用容器和Hyper-VWindows 功能。此外,虚拟化确实必须向虚拟机公开。这可以通过运行以下命令(使用您自己的虚拟机名称)来完成:
set-vmprocessor -vmname vmname -exposevirtualizationextensions $true
WSL2 安装
如果您正在运行 Windows Home,您还需要安装 WSL2 来运行 Linux 容器。这需要启用针对 Linux 的虚拟机平台和Windows 子系统功能,如图 9.4所示:

图 9.4–Windows Home 功能
启用这些功能后,应安装最新的 WSL2 Linux 内核。这可以通过下载并运行包来完成。请使用指向适用于 x64 机器的Linux 内核更新包Microsoft 文档的链接:https://docs.microsoft.com/en-us/windows/wsl/install-win10 用于 WSL2。
在 Docker Desktop 安装过程中,如果 WSL 2 未按图 9.5所示安装,您将看到以下错误消息:

图 9.5–WSL 2 缺少错误消息
谢天谢地,Docker 桌面提供了关于如何安装内核的清晰说明。
Docker 化概述
将软件从开发机器转移到生产服务器的挑战比听起来要困难。环境的差异可能从硬件到软件不等。Docker 化是解决这一问题的一种方法。通过容器化,应用及其所有依赖项被捆绑到单个包或映像中。然后可以启动此映像,运行的映像或实例称为容器。
为了进一步解释,我们来看一个传统应用,如图 9.6 所示:

图 9.6–传统应用
上图演示了一个传统的应用,其中应用运行在基础架构上托管的操作系统上。当应用需要操作系统的不同功能时,这种方法可能会出现问题。不一定两个应用总是需要相反的特性,更重要的是,很难可靠地捕获一个应用的所有需求。在涉及开发团队和多个环境的组织中,如果没有清晰的文档或工具来帮助管理应用的依赖关系,这将变得难以控制。
VMs 抽象去掉底层基础设施,允许多个 VM 在一台物理机器上运行,如图 9.7所示:

图 9.7–虚拟机
上图显示了一个虚拟机监控程序用于托管多个虚拟机。每个 VM 都包含应用及其自己的操作系统副本,以运行应用。这种方法将用于运行虚拟机的硬件虚拟化。
Docker 化将虚拟化向前推进了一步,并将操作系统虚拟化,如图 9.8所示:

图 9.8–Docker
上图显示了Docker,一种流行的 Docker 技术,用于运行多个应用。注意,通过容器化,应用在共享主机操作系统上运行。一个优点是容器的大小比 VM 小得多。容器的启动速度也比 VM 快得多。容器化最显著的优点之一是软件的发布更具可预测性,因为应用及其所有依赖项都捆绑在一起,形成一个版本化的、不可更改的包。
与 Docker 一起开始
为了展示一个使用容器的实用示例,我们将使用流行的容器平台 Docker。Docker 之所以被选中,是因为它的受欢迎程度、易用性,以及它作为 Docker 运输行业领导者的地位。本章本节将提供 Docker 的概述和安装 Docker 的说明。
什么是码头工人?
Docker 是操作系统级虚拟化的平台,用于管理和执行称为容器的软件包。每个容器都是一个软件包以及运行容器所需的库和配置。该捆绑包称为映像,映像可以本地存储到运行 Docker 的机器或注册表中。Docker 注册表是图像的存储库。注册表可能需要身份验证;这称为私有注册表。不需要身份验证的 Docker 注册表称为公共存储库,Docker Hub 和 Docker Cloud 是两个流行的公共 Docker 注册表。让我们看一个常见的工作流程,以说明我们迄今为止讨论的内容,如图 9.9所示:

图 9.9-Docker 注册表
在 Docker 注册表中,存储图像的集合。在 Docker 环境中,比如说开发机器,pull命令用于将图像副本带入本地环境。然后,使用run命令创建名为容器的图像实例。可以停止和启动容器,并可以更改其状态。这意味着,如果容器包含数据库,并且数据库中的记录发生了更改,那么如果容器停止并启动,这些更改将存在。但是,一旦创建图像,就无法对其进行更改。但是,一个图像可以存在多个版本。当我们看实际例子时,这将更有意义。
让我们进一步讨论一个场景,在这个场景中,容器被开发、测试,然后发布到生产环境中。每项活动都将在不同的环境中进行。这是一个中央注册表可以帮助我们的例子,如图 9.10*所示:

图 9.10–Docker 工作流程
图像是在开发环境中创建的。在上图中,提交命令用于从正在运行的容器创建图像。有几种方法可以创建图像,我们将在本章后面介绍一些方法。然后将图像从开发环境推送到注册表。从测试环境中,使用pull命令从注册表引入图像,使用run命令启动容器。一旦图像经过测试和批准,就可以在生产环境中从注册表和注册表*中提取相同的图像。*
*既然我们已经对 Docker 有了一个高层次的理解,让我们花些时间讨论一下它的主要组成部分。
形象
理解 Docker 的第一步是区分图像和容器。图像是一个版本化的文件,无法更改,实际上什么都不做。它是应用的快照,一旦创建,就无法更改。容器是图像的一个实例。容器有一个状态,例如,正在运行或已停止,容器有自己的状态。在某些方面,您可以用类似于 C 中类和对象之间的关系的方式来思考图像和容器之间的关系。
一个图像可以被认为是由层组成的。每一层都建立在前一层的基础上。例如,第一层可以设置初始环境。为了举例说明,让我们使用 Ubuntu 映像,它是为流行的 Linux 操作系统提供的映像。随后将添加一个后续层,以包含一些必需的组件——比如说一个数据库引擎,如 Microsoft SQL Server。正如我们前面提到的,有几种创建新图像的方法。在上一节中,我们提到可以使用commit命令,但是让我们讨论一下使用 Dockerfile。
Dockerfile
Dockerfile 是一个文本文件,其中包含用于组装图像的命令。以官方的 Microsoft SQL Server 为例,用于创建 Microsoft SQL Server Linux 映像(mssql-server-linux的 Dockerfile 包含四个命令。
查看用于创建图像的 Dockerfile。这在位于的公共 GitHub 存储库中 https://github.com/microsoft/mssql-docker/blob/master/linux/mssql-server-linux/Dockerfile :
# mssql-server-linux
# Maintainers: Microsoft Corporation (LuisBosquez and twright-msft on GitHub)
# GitRepo: https://github.com/Microsoft/mssql-docker
# Base OS layer: Latest Ubuntu LTS.
FROM ubuntu:16.04
# Default SQL Server TCP/Port.
EXPOSE 1433
# Copy all SQL Server runtime files from build drop into
# image.
COPY ./install /
# Run SQL Server process.
CMD [ "/opt/mssql/bin/sqlservr" ]
第一个命令FROM ubuntu:1604是一个示例,其中第一层被指定为 Ubuntu Docker 官方图像。下一个命令EXPOSE 1433将使端口1433可用于主机操作系统。此命令后面跟着COPY ./install /,它将复制 SQL Server 运行时。最后一个命令启动 SQL Server 进程:CMD [ "/opt/mssql/bin/sqlservr" ]。
执行 Dockerfile 时,将创建一个新映像,该映像由文件中的命令组成。稍后我们将更详细地讨论不同的命令。本节的目的只是介绍 Dockerfile 的概念以及图像如何由层组成。
容器
映像的运行实例,即容器,是轻量级、安全和可移植的。容器是轻量级的,因为与 VM 不同,它可以访问底层操作系统公开的资源。例如,如果主机系统可以访问 internet,则默认情况下容器可以访问 internet。类似地,默认情况下,容器可以完全访问可用的 RAM 和 CPU 资源。容器还与主机系统上运行的其他容器和进程隔离。这就是为什么端口1433在Dockerfile部分的 Microsoft SQL Server 示例中显式公开。Docker 容器遵循行业标准,这意味着它可以在不同的平台和容器引擎上运行。
Docker 引擎
在本章中,我们将使用 Docker 引擎通过 Docker Desktop 运行容器。这一点值得注意,因为 Docker 遵循开放式 Docker 倡议(OCI标准),这意味着不同的发动机可以用于运行相同的图像。对于本地开发,我们可能使用 Docker Desktop,但我们的测试环境可能托管在云提供商中。在下一章中,我们将介绍如何使用 Azure 容器实例在 Azure 中运行容器。
Docker 引擎和 Azure 容器实例是用于管理隔离容器的强大引擎的示例。对于更高级的场景,需要一个编排引擎。Docker Swarm 和 Kubernetes 是支持扩展和负载平衡等附加功能以及身份验证和更高级监控功能的编排引擎的示例。
现在我们有了 Docker 的概述,让我们来安装它。
安装码头工人
Docker 桌面的安装可在 Docker 网站上找到。只需下载最新版本并安装即可。Docker 在上提供了 Mac、Windows 和 Linux 的全面安装说明 https://docs.docker.com/get-docker/ ,因此我们不会在这里重复说明和要求。
在本章中,出于几个原因,我们将使用 Linux 容器。首先,它们往往更小,因此下载和启动更快。第二个是说明.NET 能够将相同的源代码编译到 Linux 或 Windows 容器中的能力。
安装并启动 Docker Desktop 后,让我们运行一些命令以确保一切按预期进行。您可以使用 command、Bash 或 PowerShell 来运行本章中的 Docker CLI 命令。首先,通过运行docker version确保 Docker 已启动并运行。
答复分为两部分。第一个显示客户端,如图 9.11所示:

图 9.11-Docker 版本客户端
在这里,您可以看到撰写本文时 Docker Desktop 的版本以及客户端正在运行的操作系统 Windows。
第二部分是服务器,如图 9.12所示:

图 9.12-Docker 版本服务器
请注意 Docker Engine 的版本以及运行的体系结构linux,这表明 Linux 容器可以运行。
另一个确保所有工作正常的简单测试是docker hello-world命令。试一试,如果一切正常,没有错误,让我们在下一节中尝试一些更有趣的东西。
Windows 安全警报
根据您的特定桌面配置,您可能会收到一个警报,询问 Docker 后端是否可以访问网络,如图 9.13所示:

图 9.13–Windows 安全警报
要完成本章中的说明,Docker 需要能够访问 Docker Hub 检索图像。
在 Docker 上运行 Redis
在本节中,我们将运行流行的开源内存缓存Redis。Redis 是一种数据结构存储,这意味着它存储字符串、列表、集合、排序集和散列等内容,并支持对存储数据的查询。Redis 已经开发了十多年,拥有一个庞大的社区,如果您还没有这样做的话,它值得一看。
将 Redis 作为本地开发的容器运行非常有意义。通过使用容器,我们不必在机器上安装 Redis,也不必担心安全权限。对于容器,设置和安全性已经完成。不过,限制是我们只能访问一些 Redis 选项。如果存在基础 Redis 映像不支持的选项,则建议使用基础 Redis 映像创建自定义 Redis 映像。
启动 Redis
使用run命令启动 Redis 容器:
docker run --name myRedis -p 6379:6379 -d redis
使用此命令,我们将命名容器myRedis并指定要提取的redis图像。这将从 Docker Hub 中提取,我们可以看到正在下载的图像。由于我们将在下一节中从应用访问此端口,因此我们需要确保使用图 9.14中所示的-p选项公开默认的 Redis 端口6379:

图 9.14–Redis 的 docker 运行命令
一旦命令完成,Redis 将在容器中运行。您可以使用docker container ps命令查看正在运行的容器,如图 9.15所示:

图 9.15–docker ps 命令
另一个有用的命令是docker images,显示本地图像,如图 9.16所示:

图 9.16–docker 图像
上图显示了带有latest标签的redis图像。
在下一节中,我们将从.NET 应用访问 Redis,但现在,让我们连接到容器并四处看看。我们可以使用docker exec -it myRedis sh命令连接到容器。进入容器后,我们需要使用redis-cli命令进入 Redis 命令模式。Redis CLI 允许我们对缓存运行命令。
进入 Redis CLI 后,我们将发出一些命令来检查 Redis 是否按预期工作。第一个命令hset messageFromRedis "absexp" "-1" "sldexp" "-1" "data" "Hello from Redis!"将以允许.NET 应用检索的格式在 Redis 中创建一个字符串。好消息是使用 Redis SDK 进行设置和检索要简单得多。第二个命令set key1 value1将添加一个用key1标识的字符串和一个值value1。最后一个命令get key1显示可以检索key1的值,如图 9.17所示:

图 9.17–Redis CLI
然后您可以退出 Redis 和容器。
在本节中,我们启动了一个 Redis 容器,并检查它是否按预期运行。在下一节中,我们将从另一个应用访问 Redis。要做到这一点,我们需要确定 Redis 缓存地址。要确定 IP 地址,请使用ipconfig命令。如果您不是在虚拟机中运行,则应该看到属于 DockerNet 的网络。例如,您应该看到如下内容:
Ethernet adapter vEthernet (DockerNAT): Connection-specific DNS Suffix . : IPv4 Address. . . . . . . . . . . : 10.0.73.1 Subnet Mask . . . . . . . . . . . : 255.255.255.0 Default Gateway . . . . . . . . . :
在虚拟机上,查找属于WSL的网络:
Ethernet adapter vEthernet (WSL): Connection-specific DNS Suffix . : Link-local IPv6 Address . . . . . : fe80::8411:e43d:c978:9e70%32 IPv4 Address. . . . . . . . . . . : 172.23.160.1 Subnet Mask . . . . . . . . . . . : 255.255.240.0 Default Gateway . . . . . . . . . :
在下一节中,记录 IPv4 地址,因为我们需要它来连接 Docker。
在容器中运行 ASP.NET Core
在本节中,我们将创建一个简单的 ASP.NET Core 应用,用于访问我们的 Redis 容器。然后,我们将在容器中运行应用。我们将从命令行执行大部分操作,但我们将跳转到 Visual Studio,以展示一些可用的优秀工具:
-
The first step is to create a new directory and create a basic .NET web application. In the following Figure 9.18, we can see what ASP.NET projects are available by using the
dotnet new ASP.NET -lcommand:![Figure 9.18 – dotnet new ASP.NET -l]()
图 9.18–dotnet 新 ASP.NET-l
-
Next, we need to create a folder for our solution with the
mkdir Chap9command and create an empty solution with thedotnet new slncommand as shown in Figure 9.19:![Figure 9.19 – dotnet new sln]()
图 9.19–dotnet 新 sln
-
Then we create another folder within the previous one called
webwith themkdir webcommand. Remember to change directory, for example, usingcd web, into the created folder. Create a newASP.NET Core Emptyproject using thedotnet new webcommand as shown in Figure 9.20:![Figure 9.20 – dotnet new web]()
图 9.20–dotnet 新网站
-
最后一步是将项目添加到我们的解决方案中,如图 9.21所示:

图 9.21–dotnet sln 添加
笔记
在解决方案文件夹中创建 web 的额外步骤将在后面的章节中帮助我们。当以后添加容器编排支持时,VisualStudio 将以一种不那么混乱的方式显示与容器相关的文件。
现在我们已经创建了解决方案和项目,请继续使用dotnet run命令运行项目,确保一切正常。您将需要在 web 项目中执行此操作,如图 9.22所示:

图 9.22–dotnet 运行
在浏览器中,进入http://localhost:5000,您会收到一条熟悉的消息,如图 9.23所示:

图 9.23–你好,世界!
现在我们有了基本的 web 应用,我们将更改该应用,以便它从 Redis 检索一条自定义消息。
访问 Redis
让我们停止正在运行的应用—使用Ctrl+C可以停止 dotnet 应用—并编辑一些文件。第一个要编辑的文件是web.csproj;使用记事本是可以的。我们要插入以下行:
<ItemGroup> <PackageReference Include="Microsoft.Extensions.Caching.
StackExchangeRedis" Version="3.1.8" /> </ItemGroup>
编辑后的文件如图 9.24 所示:

图 9.24–web.csproj
下一个要编辑的文件是startup.cs文件。我刚刚用记事本添加了一个新的using语句:
using Microsoft.Extensions.Caching.Distributed;
在ConfigureServices方法中,我们将链接添加到 Redis。请务必输入您的 Redis IPv4 地址:
services.AddStackExchangeRedisCache(option =>
option.Configuration = "172.23.160.1");
Configure方法签名需要更新,以允许将缓存注入该方法:
public void Configure(IApplicationBuilder app,
IWebHostEnvironment env, IDistributedCache cache)
最后一步是将静态“Hello World!”替换为我们来自 Redis 的消息:
await context.Response.WriteAsync(cache.GetString
("messageFromRedis"));
下面的图 9.25显示了最终的startup.cs文件:

图 9.25–Startup.cs
再次运行应用并刷新浏览器以查看更新的消息,如图 9.26所示:

图 9.26–Redis 的您好!
在本节中,我们使用名为Hello World模板的空模板创建了一个新的 ASP.NET Core web 应用。然后,我们添加了一个用于从.NET 应用连接到 Redis 的流行包 StackExchangeRedis。这与大型站点(如堆栈溢出)使用的客户端相同。使用这个库,我们必须将缓存添加到 ASP.NET 的依赖项注入中。我们的最后一步是使用缓存从运行在 Docker 容器中的 Redis 缓存中检索字符串。
增加 Docker 支架
我们将从两个方面来研究将 ASP.NET Core 应用进行容器化。第一种方法将创建 Dockerfile 和命令,以创建映像并运行容器。第二种方法将使用 VisualStudio。
Dockerfile 方法
从项目的根文件夹开始,我们将使用dotnet publish -c Release命令发布发布版本。这将生成我们的应用的构建,以便它可以复制到我们的容器中,如图 9.27所示:

图 9.27–网络发布
在包含我们的应用的release文件夹中,我们将创建一个 Dockerfile。
笔记
默认情况下,Docker 将在当前文件夹中查找名为dockerfile且没有扩展名的文件。
为此,我使用记事本并输入以下语句:
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim WORKDIR /app COPY . .EXPOSE 80 ENTRYPOINT ["dotnet", "web.dll"]
还记得之前的洋葱比喻吗?我们将从 Microsoft 提供的已加载 ASP.NET 的层开始。下一个命令表示我们正在创建图像的app文件夹中工作。copy命令将当前文件夹的内容复制到图像的app文件夹中。然后,我们使端口80在我们的图像之外可用。最后一个命令指出.NET 应该在容器启动时运行web.dll。当我们的容器启动时,我们的 ASP.NET Core 应用应该启动并侦听端口80。
保存文件后,让我们构建映像:
docker build . -t myweb
如果您收到一个错误,说明找不到该文件,则可能是您将该文件命名为Dockerfile.txt。没问题–我们可以使用-f参数指定文件名:
docker build . -f Dockerfile.txt -t myweb
如果一切顺利,那么您将收到一条成功消息,说明图像已构建并标记为myweb:latest。您可以使用docker images命令查看图像,如图 9.28所示:

图 9.28–docker 图像
为了启动映像,我们使用docker run命令,将本地端口8080映射到 Docker 端口80:
docker run -p 8080:80 myweb
在浏览器中,我们可以导航到 web 应用,仍然可以看到来自 Redis 的消息,如图 9.29所示:

图 9.29–容器中的 ASP.NET Core
当然,我们只是在这里触及表面,但这是一个有力的例子,说明容器是多么容易。那么,VisualStudio 能让体验变得更简单吗?
VisualStudio 方法
在 VisualStudio 中,打开解决方案浏览器。继续并运行项目,如果系统提示您保存解决方案文件,请继续并将其保存在与项目文件相同的文件夹中。VisualStudio 有许多功能支持 Docker 容器开发人员。我们将看到的第一个特性是能够为我们的项目添加 Dockerfile。这位于添加子菜单下的解决方案资源管理器上下文菜单中,称为Docker Support…。如图 9.30所示:

图 9.30-Docker 支持…
通过选择此选项,Visual Studio 将准备将项目制作成图像。VisualStudio 将询问目标映像是针对Linux还是Windows操作系统,如图 9.31所示:

图 9.31–Docker 文件选项
由于我们的 Docker 桌面当前正在运行 Linux 容器,请选择默认的Linux选项。现在会发生几件事。首先,请注意,为项目创建了一个名为Dockerfile的新文件,如图 9.32所示:

图 9.32–Visual Studio Dockerfile
继续并打开文件,注意与我们在上一节中创建的 Dockerfile 有多么相似。主要区别在于此 Dockerfile 在将版本复制到映像之前执行dotnet build和dotnet release。
另外,请注意,运行选项已更改为显示 Docker 作为运行目标,如图 9.33 所示:

图 9.33–Visual Studio:Docker 运行目标
如果我们现在运行这个项目,将会发生几件事。Visual Studio 将显示一个名为容器的新窗口,如图 9.34所示:

图 9.34–Visual Studio 容器窗口
此窗口显示正在运行的容器和本地机器上的图像。在上图中,我们可以看到当前有三个容器正在运行。名为web的容器就是这个项目容器。您还可以看到名为myRedis的 Redis 容器正在运行,以及生成的名称,在本例中为keen_volhard。花点时间去探索。例如,如果选择myRedis容器,则可以看到端口6379已经映射,如图 9.35所示:

图 9.35–Visual Studio 容器窗口
哦,如果你想知道的话,我们对正在运行的容器有完全的调试支持。在Startup.cs文件中,将Configure方法中的断点放在我们从 Redis 检索字符串的行上,如图 9.36所示:

图 9.36–Visual Studio 调试支持
当项目再次运行时,点击调试,我们可以调查运行对象,如图 9.37所示:

图 9.37–Visual Studio 调试
我们将在后面的章节中更详细地讨论调试,但我们的目的是展示 VisualStudio 与 Docker 和正在运行的容器的紧密集成。
Docker 多 Docker 支持
在前面的部分中,我们有一个场景,其中一个容器调用另一个容器。我们使用主机网络实现了从 ASP.NET Core 应用到 Redis 缓存的调用。这是可行的,但有两个明显的缺点。首先,任何有权访问主机网络的人都可以调用 Redis 缓存。第二个缺点是没有任何迹象表明我们的 ASP.NET Core 应用需要 Redis。
在本节中,我们将通过使用 Docker Compose 来解决这两个缺点。Docker Compose 允许我们将多个容器合并到一个定义中。这将允许我们限制对 Redis 的访问,并表明 Redis 是我们的 ASP.NET Core 应用的一项要求。我们可以在没有 VisualStudio 的情况下完成这一部分,但我们将使用 VisualStudio 突出显示一些可用的优秀功能。
添加容器编排支持
在解决方案浏览器中,我们可以选择添加Container Orchestrator Support。位于添加子菜单下的项目上下文菜单中,如图 9.38所示:

图 9.38–容器编排支持…
系统将提示您输入所需的Container Orchestrator Support类型。有两个选项:库伯内特斯/赫尔姆和码头工人组合。这两个用例之间的主要区别在于您是需要一组引擎来承载容器,还是需要一个引擎。在大多数情况下,集群将指示单独的 VM 或物理机器。在我们的场景中,我们只想在单个 Docker 引擎实例上托管,所以我们将选择Docker Compose,如图 9.39所示:

图 9.39-Docker Compose
如果提示输入目标操作系统,请选择Linux。此外,Visual Studio 将检测到我们的项目中存在 Dockerfile,如图 9.40所示:

图 9.40–创建新的 Dockerfile
我们不介意覆盖当前的 Dockerfile,因此选择否。
现在来看解决方案,我们会注意到一些新的 YAML 文件,如图 9.41所示:

图 9.41–Visual Studio YAML
新的docker compose部分中的docker-compose.yml文件用于定义我们的编排。在这个文件中,我们将定义编排的容器、网络和其他需求。您还将注意到,docker-compose.override.yml折叠在文件下。不要担心这个文件中的细节,它提供了在 VisualStudio 中运行编排的细节。我们要做的是删除这个文件,因为如果我们只查看一个docker-compose.yml文件,它会使事情变得更简单。
笔记
一定要删除docker-compose.override.yml文件,以免以后混淆。
默认 Docker Compose 文件指定我们有一个名为web的服务,并给出其 Dockerfile 的位置:
version: "3.4"
services:
web:
image: ${DOCKER_REGISTRY-}web
build:
context: .
dockerfile: web/Dockerfile
文件中的版本号很重要,因为它指示支持的 Docker 引擎版本。例如,3.4 支持 Docker 引擎版本 17.09.0 及更新版本。版本可在找到 https://docs.docker.com/compose/compose-file/compose-versioning/ 。在services下,我们有一个名为web的服务。将用于web服务的图像指定为环境变量${DOCKER_REGISTRY}和单词web的组合。在新的环境中,不应设置环境变量,因此图像最终将仅为web。最后要指出的是,context是指向目录的路径,与dockerfile选项一起使用。在我们的 Docker Compose 文件中,这将导致 Dockerfile 位于web目录中。
将 Redis 添加到 Docker Compose 文件
我们需要做的第一件事就是将redis服务添加到此编排中。记住要小心缩进,因为 YAML 要求遵循缩进规则。在web服务的定义下,我们创建一个新的服务redis:
version: "3.4"
services:
web:
image: ${DOCKER_REGISTRY-}web
build:
context: .
dockerfile: web/Dockerfile
redis:
image: redis
ports:
- 6379:6379
请注意,我们使用的是默认端口。保存文件时,在Output窗口中查找中的Container Tools或Build。您应该会看到一个Bind for 0.0.0.0:6379 failed: port is already allocated错误,因为您仍然会运行上一个 Redis 容器。
添加隔离网络
我们想要做的是独立于其他示例运行新的编排。为此,我们需要在 Docker Compose 文件中定义一个网络。只需将网络定义添加到文件末尾并在两个服务上设置此网络即可:
version: "3.4"
services:
web:
image: ${DOCKER_REGISTRY-}web
build:
context: .
dockerfile: web/Dockerfile
networks:
- chap9
redis:
image: redis
networks:
- chap9
networks:
chap9:
这些更改将定义一个与主机隔离的新网络。这意味着我们必须进行一些额外的更改,以使我们的示例正常工作。第一个是我们需要公开一个从chap9网络到主机网络的端口,以便我们可以浏览站点:
web:
image: ${DOCKER_REGISTRY-}web
build:
context: .
dockerfile: web/Dockerfile
ports:
- 80
networks:
- chap9
在前面的代码块中,端口80是从chap9网络中暴露出来的。
修改启动
这也意味着我们在statup.cs文件中硬编码的端口将不正确。现在,让我们通过在新 Docker 网络中从使用 IP 地址改为使用服务名称来纠正这个问题。这是通过startup.cs文件中的ConfigureServices方法完成的:
public void ConfigureServices(IServiceCollection services)
{
services.AddStackExchangeRedisCache(option =>
option.Configuration = "redis");
}
我们需要做的另一件事是在 Redis 缓存中添加一条默认消息。这是以前在手动步骤中完成的,因此,如果消息尚未定义,我们将添加一些逻辑来执行此操作。
为简单起见,这是在Configure方法中通过在app.UserEndpoints命令之前添加以下行来完成的:
public void Configure(IApplicationBuilder app, IWebHostEnvironment
env, IDistributedCache cache)
{
…
if(string.IsNullOrEmpty(cache.GetString("messageFromRedis")))
{
cache.SetString("messageFromRedis", "Hello from Redis
running in an isolated network!");
}
…
}
前面的代码段仅在缺少messageFromRedis键时才使用该键设置字符串。这是一个简单的例子,但希望你能看到使用 Redis 缓存是多么简单。
潜在错误
如果事情进展不顺利,你可能会遇到一些事情。突出显示的第一个错误是,如果我们没有指定要向主机公开的端口,我们将看到如下对话框,如图 9.42所示:

图 9.42–缺少端口
这表示 Docker Compose 文件中的web服务下未指定任何端口。
第二件事是,如果 Redis 缓存的地址不匹配,我们在尝试建立与 Redis 的连接时会出现无法连接错误。让我们通过将网络位置作为环境变量传入来说明 Docker Compose 的另一个特性。这是通过在web服务部分的 Docker Compose 文件中定义变量来实现的。
添加 Environmentnt 变量
首先,在startup.cs文件中,编辑ConfigureServices方法以使用环境变量:
public void ConfigureServices(IServiceCollection services)
{
services.AddStackExchangeRedisCache(option =>
option.Configuration = Environment.
GetEnvironmentVariable("REDIS_ADDRESS"));
}
然后在 Docker Compose 文件中,编辑web服务部分以包括新的environment设置:
web:
image: ${DOCKER_REGISTRY-}web
build:
context: .
dockerfile: web/Dockerfile
environment:
- REDIS_ADDRESS=redis
ports:
- 80
networks:
- chap9
很可能,您不会遇到错误,但要在编排中突出显示的一个重要功能依赖于另一个容器。这可以通过使用depends_on设置在 Docker Compose 文件中完成:
web:
…
depends_on:
- redis
…
下面的显示了我们的已完成的docker-compose.yml文件:
version: "3.4"
services:
web:
image: ${DOCKER_REGISTRY-}web
build:
context: .
dockerfile: web/Dockerfile
depends_on:
- redis
environment:
- REDIS_ADDRESS=redis
ports:
- 80
networks:
- chap9
redis:
image: redis
networks:
- chap9
networks:
chap9:
在运行项目时,我们会看到新的更新消息,如图 9.43所示:

图 9.43–运行在隔离网络中的 Redis 的 Hello!
让我们从第二个角度来看这个问题,这样我们就可以更深入地了解正在发生的事情。
Docker 网络
让我们来看看当前定义的 OrthT2 网络,通过使用 Tyt T0 命令,如图 9.44 所示,图 4:

图 9.44-docker 网络 ls
您应该看到几个网络。我们将更详细地了解这两个版本的bridge驱动程序。使用docker network inspect bridge命令,让我们看看第一个名为bridge的网络。现在来看Containers部分,如图 9.45所示:

图 9.45-码头网络检查桥-Docker
通过查看容器的名称,我们可以看出这是默认网络,因为这些是我们在本章第一节中创建的容器。这在图 9.46所示的Options部分中表示:

图 9.46-docker 网络检查网桥-选项
注意,默认的bridge选项设置为true。当我们使用docker network inspect network id命令检查另一个网桥网络时,我们可以看到选项指示这是chap9``compose网络,如图 9.47所示:

图 9.47-docker 网络检查网络 id
花点时间检查网络中的容器,如图 9.48所示:

图 9.48-docker 网络检查 chap9 Docker
显示 ASP.NET Core 应用和 Redis 缓存容器及其内部地址。
在本节中,我们介绍了 Docker Compose。这允许我们定义一个包含两个容器的容器编排:ASP.NET 应用和 Redis 缓存。编排是定义的,以说明 Docker Compose 的几个特征。第一个是为两个 Docker 创建了一个隔离网络。我们还确保在 ASP.NET 应用上只公开端口80。我们使用depends_on设置在 ASP.NET 和 Redis 缓存之间包含了一个依赖项。此外,我们还演示了如何设置环境变量并使其可用于正在运行的容器。
总结
在本章中,我们介绍了容器和流行的 Docker 平台。我们概述了 Docker 化以及 Docker 与虚拟机的不同之处。我们研究了 Docker 及其一些主要组件,包括图像、容器、Docker 引擎和 DockerFile。
我们提供了三个运行容器的不同示例。第一个是运行流行的内存缓存 Redis。这表明启动一个新容器是多么简单。接下来,我们仅使用记事本创建了自己的 ASP.NET Core 容器。最后一个示例使用 Visual Studio 对现有 ASP.NET Core 应用进行容器化。这个例子突出了 IDE 在使用 Docker 时提供的一些不错的特性。
Docker 和码头工人是一个大课题。本章的目的是介绍这项强大技术的一些亮点和背景。由于.NET 对 Linux 和 Windows 的可移植性,它是构建容器的理想框架。
下一章将把 ASP.NET 带到云端!我们将看看亚马逊网络服务(AWS和 Azure 如何托管我们的 ASP.NET 解决方案。
问题
- 您希望应用在容器或 VM 中启动更快吗?
- Redis 是关系数据库吗?
- 能否在 Visual Studio 中查看正在运行的容器?
- 当创建涉及多个 Docker 引擎实例的业务流程时,应该使用什么业务流程类型?
- 这一章有趣吗?
进一步阅读
-
Docker 拥有大量文档,可在找到 https://docs.docker.com/ 。
-
Microsoft 在其文档中介绍了 Docker 和 Visual Studio 对容器的支持 https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/docker 。
-
学习 Docker–Docker 19.x 的基础知识,第二版由 Gabriel N.Schenker 编写,Packt 出版社,https://subscription.packtpub.com/book/cloud_and_networking/9781838827472 。
-
Docker for Developers by Richard Bullington-McGuire, Andrew K. Dennis, Michael Schwartz, Packt Publishing,
https://subscription.packtpub.com/book/cloud_and_networking/9781789536058 。**
十、部署到 AWS 和 Azure
在上一章中,我们研究了容器和 Docker 平台。通过简化开发生命周期并帮助减少部署过程中出错的机会,容器是提高生产率的一个好方法。我们研究了流行的 Docker 框架,并提供了一些实际示例。
在本章中,我们将提供一些在两个领先的云提供商Amazon Web Services(AWS)和 Azure 上托管 ASP.NET 解决方案的示例。这两个提供商都提供了一个复杂的服务器和基础设施网络,分布在全球各地,用于托管您的解决方案。这比听起来容易,因为两家提供商都提供工具、软件开发工具包(SDK和扩展来支持您。
我们的目的是支持那些不熟悉云提供商及其托管服务的人。但我们希望不仅仅重复现有的教程和文档。因此,对于某些步骤,我们将指导您使用云服务提供商自己编写和提供的文档。
本章将介绍以下主题:
- 云计算概述
- 负载平衡器与网站健康
- 使用 Visual Studio 发布到 AWS
- 使用 Visual Studio 发布到 Azure
对于许多刚接触 AWS 和 Azure 的用户来说,入门是一项挑战。这些门户网站旨在帮助新用户,并提供支持文档和教程。在本章末尾的进一步阅读一节中,我们将重点介绍一些我们认为特别有用的内容。
在本章结束时,您将对 AWS 和 Azure 有一些熟悉。您将有一些使用 Visual Studio 扩展部署 ASP.NET 应用的实际经验。您还将拥有在 AWS 控制台和 Azure 门户中查看已部署应用的经验。本章介绍云提供商,我们将在第 13 章、云原生中详细介绍云解决方案的开发。
技术要求
本章包括简短的代码片段,以演示所解释的概念。需要以下软件才能使其正常工作:
- Visual Studio 2019:Visual Studio 可从下载 https://visualstudio.microsoft.com/vs/community/ 。社区版是免费的,将用于本书的目的。
- .NET 5:可以从下载.NET frameworkhttps://dotnet.microsoft.com/download 。
确保下载 SDK,而不仅仅是运行时。您可以通过打开命令提示符并运行dotnet --info来验证安装,如图 10.1所示:

图 10.1–验证.NET 的安装
作为本章的一部分,我们将使用 VisualStudio 中的扩展来处理 AWS 和 Azure。
请访问以下链接查看 CiA 视频:https://bit.ly/3qDiqYY
与 AWS 合作
AWS 账户需要执行发布到 AWS部分中的步骤。本节中的步骤旨在通过使用免费层的服务,为新的 AWS 账户收取少量或不收取费用。如果使用指定服务以外的服务,可能会产生费用。
要创建新的 AWS 帐户,请使用 AWS 门户网站上的创建 AWS 帐户按钮:https://aws.amazon.com/ 。本章末尾的进一步阅读一节中引用了有关该过程的其他信息。
我们将使用 AWS 工具包扩展,在 Visual Studio 中使用管理扩展,如图 10.2所示:

图 10.2–管理扩展
可通过搜索短语AWS Toolkit找到 AWS 工具包,并可在图 10.3中看到:

图 10.3–AWS 工具包扩展
有关安装 AWS Visual Studio 工具包的其他信息,请访问https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/welcome.html 。
与 Azure 合作
需要 Azure 帐户来执行发布到 Azure部分中的步骤。本节中的步骤旨在确保每月 200 美元的信用额度涵盖使用费,从而使新 Azure 帐户不收取任何费用。此积分适用于所有新 Azure 帐户。如果使用规定以外的服务,可能会产生费用。
要创建新的 Azure 帐户,请使用 Azure 网站上的免费启动按钮:https://azure.microsoft.com/en-us/free/ 。本章末尾的进一步阅读一节中引用了有关该过程的其他信息。
Azure 扩展是作为 Visual Studio 2019 的一部分安装的。这可以使用 Visual Studio 安装程序通过选择修改选项来完成,如图 10.4所示:

图 10.4–Visual Studio 安装程序
应选择Azure 开发包在 Visual Studio 中添加 Azure 支持,如图 10.5所示:

图 10.5–Azure 开发
通过选择Azure 开发包,可以获得与 Azure 相关的 SDK、工具和示例项目。
GitHub 源代码
本章的源代码位于 GitHub 存储库中的https://github.com/PacktPublishing/ASP.NET-Core-5-for-Beginners/tree/master/Chapter%2010 。
云计算概述
本节仅提供云计算的简要概述,因为我们将在第 13 章、云原生中更详细地介绍内部部署和云计算模型。本节的目的是提供有关云计算的上下文以及两个选定云提供商的一些背景。您可能希望阅读发布到 AWS和发布到 Azure部分,但仅执行其中一个提供商的步骤。
云计算可以被认为是通过互联网提供计算基础设施和服务。在云计算普及之前,组织选择从自己运行的数据中心托管服务。我们将这些数据中心称为本地,因为它们通常托管在组织自身的本地。
在本章中,我们将所需的基础设施和托管服务作为资源。这些资源包括范围广泛的东西,包括虚拟机(虚拟机)、数据库、人工智能服务(AI)以及处理大量数据的服务。随着市场的不断发展,资源范围不断扩大。这些资源可供公众使用,但需要订阅才能访问。
云计算模型
这些资源被分为以下大类。我们在这里强调它们,因为您经常听到人们以这种方式引用资源组:
- 基础设施即服务(IaaS):此类别指解决方案所基于的 IT 基础设施。将这一类别视为您租用给应用的网络、计算和数据存储资源。IaaS 的一个例子是 VM 以及 VM 使用的磁盘和网络。
- 平台即服务(PaaS):这些资源通常是对 IaaS 资源的抽象,使开发和管理应用更加容易。这些资源消除了组织管理和提供底层资源的需要,这允许组织更轻松地构建和维护应用。这种平台的一个例子是托管数据库,其中托管的详细信息(例如,运行数据库所需的 VM 和磁盘)由云提供商处理。
- 软件即服务(SaaS):该类别包含由云提供商或第三方构建和管理的产品和服务。SaaS 的一个例子是电子邮件服务。
云计算提供商
有许多公司提供云计算服务,我们将关注两个领先的云提供商:AWS 和 Azure。我们选择它们有几个原因:
- 它们都为托管 ASP.NET Core 解决方案提供了强大的支持。
- 这两个云提供商都提供 IaaS 资源,包括 Linux 和 Windows 虚拟机的配置,可用于托管 web 应用。
- 它们还提供了几个 PaaS 产品,简化了 ASP.NET Core 解决方案的托管。
我们将在本章后面介绍 AWS Elastic Beanstalk 和 Azure 应用服务。这些 PaaS 产品是一个很好的例子,它简化了基础架构的细节,使您能够专注于构建解决方案。
亚马逊网络服务
AWS 成立于 2006 年,当时最大的零售公司之一亚马逊(Amazon)提供了供组织使用的 IT 基础设施。这一最初的产品已成长为全球最大的云服务提供商,从全球数据中心提供数百种不同的资源。2020 年 7 月,据估计 AWS 拥有 31%的云计算市场份额。
弹性豆茎
在发布到 AWS部分,我们将关注 AWS 弹性豆茎。此 PaaS 产品通过简化托管 web 应用的细节,使托管 ASP.NET Core web 应用变得简单。我们选择此产品是因为它非常常用于托管 web 应用,并且弹性 Beanstalk 的部署集成到 Visual Studio 中。
我们应该解释的一件事是应用和环境之间的区别。将应用视为环境的集合。这些环境是相关的,但它们有单独的配置。将它们视为同一网站的不同版本。每个环境都有自己的 URL。
一个常见的场景是有一个开发环境,其中新的更改由开发团队测试,以及一个客户使用的生产环境。开发环境可能被配置为使用不同的数据库,并且只运行一个实例。生产环境可能使用不同的数据库,并具有多个实例。
蔚蓝色的
Azure 于 2010 年发布,与 AWS 一样,它已经稳步增长,包括来自世界各地数据中心的数百种产品。2020 年 7 月,Azure 估计拥有 20%的云计算市场份额。
Azure 应用服务
在发布到 Azure部分中,我们将使用 Azure 应用服务托管我们发布到 AWS 的相同 ASP.NET Core web 应用。与 AWS Elastic Beanstalk 一样,此 PaaS 产品还简化了 ASP.NET Core web 应用的托管,Azure 应用服务的部署与 Visual Studio 集成。
创建 ASP.NET Core web 应用示例
在本章中,我们将使用一个简单的 ASP.NET web 应用来说明 AWS 和 Azure 的一些功能。示例应用一直保持简单,因为我们希望将重点放在部署到云上。我们将添加一个返回应用运行状况的新端点。这将被云平台使用,以确定应用是否健康。
我们建议您从 GitHub 存储库中的源代码开始,因为本章更多地是关于 Visual Studio 扩展,而不是 ASP.NET Core 应用。我们将为那些希望自己构建应用的人描述构建示例的步骤:
-
First, we created the sample application by using the
dotnet new mvccommand in a folder namedChapter 10 Final. This is shown in Figure 10.6:![Figure 10.6 – dotnet new mvc command]()
图 10.6–dotnet 新 mvc 命令
-
To make sure the application restored, we used the
dotnet runcommand as shown Figure 10.7:![Figure 10.7 – dotnet run command]()
图 10.7–dotnet 运行命令
-
然后我们使用浏览器验证应用返回的主页没有错误,如图 10.8所示:

图 10.8–示例应用
这表明基本应用已恢复,没有问题。现在,我们将添加检查应用运行状况的功能。
检查运行状况终结点
许多应用都是为支持健康端点而设计的。当应用实例按预期运行时,此端点被设计为返回健康状态。还记得我们谈到云计算的好处之一是可伸缩性吗?当应用有多个实例共同处理发送到网站的请求时,运行状况端点非常有用。使用健康端点,应用实例可以在其未处于能够成功处理请求的状态时进行报告。
让我们假设你有一个 Web 应用,有时发送给应用的消息太多,无法处理。我们有两个选择。我们可以增加运行 web 应用的资源的大小。这称为放大。我们还可以添加额外的资源(称为实例)来处理消息。这被称为向外扩展。在云中,添加应用的额外实例很容易,而且通常比增加资源的大小更具成本效益。
让我们用图 10.9更详细地讨论一下:

图 10.9-具有两个应用的负载平衡器
上图显示了两个 web 应用和一个负载平衡器。在本例中,我们有一个由两个应用组成的单一环境。负载平衡器用于将请求分发到两个应用之间的环境。在某些情况下,消息的数量可能会增加到两个应用无法处理的程度。当发生这种情况时,可以增加应用的数量,如图 10.10所示:

图 10.10–具有四种应用的负载平衡器
现在,我们有四个 web 应用位于负载平衡器后面。因为负载平衡器将请求分布到所有应用中,所以环境可以处理增加的请求数。
即使将应用添加到环境中,应用也可能需要一些时间来准备接收请求。可能应用需要先将信息加载到内存中,或者在准备就绪之前执行一些处理。或者,在某个时刻,应用可能检测到所需的资源不可用。虽然应用无法成功处理请求,但它可以通过返回不正常的响应让负载平衡器知道。这将使负载平衡器知道不向应用发送请求。如图 10.11所示:

图 10.11-具有不正常应用的负载平衡器
在上图中,App3正在向负载平衡器返回一个不正常的响应。然后,负载平衡器将停止向应用实例发送请求。
现在,让我们看看这是如何做到的。
响应状态代码
约定是创建一个通常称为“健康”的端点。这应该表明系统健康或不健康。这是通过返回状态代码为200 (OK)的响应或状态代码为5xx的响应来完成的。
笔记
5xx表示500-599范围内的任何状态代码。约定返回503(服务不可用)。
为了使这一点有意义,我们需要更详细地了解消息的外观。为此,让我们使用浏览器的开发工具。我将使用 Edge,但在 Firefox 或 Chrome 中的体验也将类似。在浏览器中,按F12启动开发者工具。Edge 的显影工具如图 10.12所示:

图 10.12–Edge 的开发者工具
您将看到几个选项卡,我们感兴趣的选项卡称为网络。继续并选择此选项卡。
笔记
我们将在第 11 章、调试和单元测试中详细讨论开发工具。
现在网络选项卡已打开,请刷新我们网站的主页。您应该看到类似于我们在图 10.13中看到的内容:

图 10.13–开发人员工具网络选项卡
将列出对服务器的每个请求,并包括请求类型、大小以及接收响应所用的时间等信息。我们感兴趣的栏目是状态。在上图中,您可以看到每个请求的状态为200。这意味着每个响应都包含一个状态代码200,表示响应处理无误。
现在,让我们尝试导航到一个不存在的端点。我们可以通过将/unknown放在 URL 的末尾来实现这一点。现在看看图 10.14中的响应代码:

图 10.14–响应失败的网络日志
服务器现在的响应状态为404,这意味着未找到请求的页面。在我们的示例应用中,我们将使用状态代码503进行响应,这意味着应用不健康。
添加健康端点
在本节中,我们将修改应用以支持健康端点。该终点将返回健康或不健康的反应。我们将随机地这样做;大多数情况下,响应是健康的,但偶尔端点会以不健康的响应进行响应。
在 ASP.NET Core 中,Microsoft.Extensions.Diagnostics.HealthChecks库中有健康检查中间件来支持这一点。有关此中间件的更多信息,请参阅进一步阅读部分。
首先,我们需要创建一个类来实现 IHealthCheck 接口。我们称之为健康检查。创建类后,添加: IHealthCheck,如图 10.15所示:

图 10.15–IHealthCheck 界面
IHealthCheck 下出现红色扭曲的原因是 VisualStudio 不知道该界面是什么。您通过为Microsoft.Extensions.Diagnostics.HealthChecks添加using Health Checks Middleware语句而离开 Visual Studio。如果您将鼠标悬停在 IHealthCheck 上,您可以选择添加此项,如图 10.16所示:

图 10.16–诊断健康检查
IHealthCheck 仍然会有一个红色的曲线,因为现在 VisualStudio 已经知道了接口,它告诉我们需要实现匹配的方法。同样,您可以通过选择实现接口进行添加,如图 10.17所示:

图 10.17–实施 IHealthCheck 接口
实现接口选项将为CheckHealthAsync生成一个方法。我们将用以下代码行替换throw语句:
var random = new Random();var isHealthy = random.Next(10) != 1;if (isHealthy){ return Task.FromResult(HealthCheckResult.Healthy());}else { return Task.FromResult(HealthCheckResult.Unhealthy());}
此代码段的第一部分使用Random类生成一个介于0和9之间的随机值。在1上,我们将布尔值isHealthy设置为false;否则设置为true。片段的第二部分将在IsHealthy为true时返回HealthCheckResult健康状态,或者在false时返回false不健康状态。
笔记
我们之所以使用Task.FromResult(),是因为接口方法是异步的,因此需要返回类型Task。
现在我们已经实现了HealthCheck,我们需要连接中间件。为此,我们将更新Startup类。在Status.cs的ConfigureServices方式中,增加以下行:
services.AddHealthChecks().AddCheck<HealthCheck>("web");
这将添加我们的HealthCheck实现作为HealthChecks中间件中的检查。图 10.18显示完成的ConfigureServices方法:

图 10.18–配置服务方法
下一步是添加HealthCheck作为端点。我们将把这张支票放在/health,因为这是惯例。为此,在Configure方法中添加以下端点:
endpoints.MapHealthChecks("health");
图 10.19显示了完成的方法,并突出显示了我们插入的行:

图 10.19–配置方法
此更改仅公开了/health处的运行状况检查。
继续并运行解决方案,以查看此操作。一旦应用启动,通过向 URL 添加/health导航到健康端点,如图 10.20所示:

图 10.20–健康终点
尝试按 refresh,您将看到大约 10 次中的 1 次Unhealthy响应。图 10.21显示了开发者工具中的响应:

图 10.21–响应不正常的网络日志
注意,第三个响应的状态为503。这表示有Unhealthy响应。
笔记
在开发人员工具中,使用保留日志选项将以前的响应保留在日志中。
现在我们已经准备好了示例应用,让我们将其发布到 AWS 和 Azure!
发布到 AWS
在本节中,我们将向 AWS Elastic Beanstalk 发布我们的应用。此时,您应该创建一个 AWS 帐户。有几种方法可以部署到 AWS Elastic Beanstalk。一种方法是直接在 AWS 控制台中。相反,我们将使用 AWS 工具包,因为它简化了部署过程。要使用 AWS 工具包进行部署,我们需要向 VisualStudio 添加所需的凭据。
创建用于从 Visual Studio 发布的用户
为了获得我们需要的凭证,我们将在 AWS 中创建一个用户。这是在 AWS 控制台中完成的。继续并登录:
-
The service we are interested in deals with identity and access. To find this service, use the
Servicesdropdown and typeiamas shown in Figure 10.22:![Figure 10.22 – IAM service]()
图 10.22–IAM 服务
-
After selecting this service, select Users under Access management as shown in Figure 10.23:
![Figure 10.23 – Identity and Access Management (IAM)]()
图 10.23–身份和访问管理(IAM)
-
We want to add a user, so select the Add user button. This will start a wizard. The first step sets the user's details. We will add a new user with the name
VisualStudioUser. This user will be getting programmatic access as shown here Figure 10.24:![Figure 10.24 – Add user – step 1]()
图 10.24–添加用户–步骤 1
-
Next, we want to add some permissions. We'll do this by adding the required permissions to a group and then adding the user to a group. This is a great way of configuring combinations of permissions so that they can be given to multiple users consistently. Select the Create group button as shown in Figure 10.25:
![Figure 10.25 – Add user – step 2]()
图 10.25–添加用户–步骤 2
-
We will now create a group named
VisualStudioPublisherGroup, and we will add two permissions. The first is access to IAM. This can be seen in Figure 10.26:![Figure 10.26 – Create group – IAMFullAccess]()
图 10.26–创建组–IAMFullAccess
所需的第二个权限是访问 AWS Elastic Beanstalk,如图 10.27所示:
![Figure 10.27 – Create group – AWSElasticBeanstalkFullAccess]()
图 10.27–创建组–AWSElasticBeanstalkFullAccess
-
After you have these permissions selected, proceed to the next step by pressing the Create Group button.
笔记
AWS 权限的详细信息不在本章的范围内。在进一步阅读部分,我们将提供与 AWS 相关的资源。
图 10.28显示用户将被添加到新组:
![Figure 10.28 – Add user – review]()
图 10.28–添加用户–审查
-
For our purposes, we do not need to define any tags, so we can skip the Tags step. Figure 10.29 shows a summary of the user:
![Figure 10.29 – Add user – step 4]()
图 10.29–添加用户–步骤 4
-
点击创建用户按钮后,我们会看到一个动作摘要,如图 10.30所示:

图 10.30–添加用户–步骤 5
继续并使用下载.csv按钮下载凭证。这些是我们将加载到 VisualStudio 中的凭据。
理解 AWS 中的区域
在这一点上,我们应该突出区域。云提供商将世界划分为多个区域。这些对应于地理位置相近的数据中心集合。AWS 资源既可以是区域性的,也可以是全球性的。例如,我们的用户是全球性的。我们将要部署的 web 应用将是区域性的。
判断资源是否为全局资源的一个简单方法是查看 AWS 控制台的右上角。选择 IAM 时,如图 10.31所示:

图 10.31–AWS 全球资源
现在,继续使用图 10.32所示的服务下拉列表查找AWS Elastic Beanstalk:

图 10.32–AWS 弹性豆茎
您将现在看到,默认情况下,与您最近的区域已被选中。在本例中,选择了悉尼地区,如图 10.33所示:

图 10.33–AWS 弹性豆茎区域
我们将将我们的应用部署到一个地区。您可以选择一个或另一个默认区域。
AWS 发布
在这一节中,我们将从 Visual Studio 发布到 AWS。让我们开始:
-
Back in Visual Studio, right-click on the project and select the Publish to AWS Elastic Beanstalk… option as seen in Figure 10.34:
![Figure 10.34 – Publish to AWS Elastic Beanstalk…]()
图 10.34–发布到 AWS Elastic Beanstalk…
-
Next, we need to add our credentials. You do this by clicking the image of the person with a plus symbol. This is indicated in Figure 10.35:
![Figure 10.35 – Adding a profile]()
图 10.35–添加配置文件
-
This will present you with a dialog where you can specify the profile name as well as loading the credentials we downloaded Figure 10.36:
![Figure 10.36 – Visual Studio AWS profile]()
图 10.36–Visual Studio AWS 配置文件
在上图中,我们提供了一个名称
VisualStudioPublisher并使用从 csv 文件导入…按钮导入了我们的凭证。我们保留了标准 AWS 账户的默认设置,点击确定。 -
Now that we have loaded our credentials, we can specify the Region we want to deploy to as shown in Figure 10.37:
![Figure 10.37 – AWS publish wizard step 1]()
图 10.37–AWS 发布向导步骤 1
由于这是一个新的应用环境,我们只能选择新建应用环境。继续并单击下一步。
-
In the next step, we specify the name of the application and environment. We also construct the URL of the website we are creating as shown in Figure 10.38:
![Figure 10.38 – AWS publish wizard step 2]()
图 10.38–AWS 发布向导步骤 2
在上图中,我们将应用命名为
Chatper10Final,并选择了开发环境。您可能会发现您需要更改 URL,直到找到一个免费名称,您可以使用检查可用性按钮查看 URL 是否免费。此 URL 将是全局的,因此它必须是唯一的。继续并按下下一步。
-
The next page provides some details about the environment. Elastic Beanstalk will be hosted on an EC2 VM. We don't have to worry about many of the details, but we do have to consider the type and size Figure 10.39:
![]()
图 10.39–AWS EC2 类型和尺寸
在上图中,我们选择了 Windows Server 核心版本,因为我们需要最新的.NET 版本。我们的应用不需要大型 VM,因此我们选择了t3a.micro,因为它位于 AWS 自由层。
笔记
并非所有 AWS Elastic Beanstalk 类型都支持 ASP.NET Core 5。要了解什么环境将支持部署,请使用 AWS Elastic Beanstalk 发行说明:https://docs.aws.amazon.com/elasticbeanstalk/latest/relnotes/relnotes.html 。
另一个必填字段是密钥对,它允许我们在部署后访问环境,如图10.40所示:
![Figure 10.40 – AWS key pair]()
图 10.40–AWS 密钥对
在前面的截图中,我们将密钥对命名为
vs_key_pair。 -
The next parameter to note is Single instance environment. When clicked, the application can only have one instance. But when unselected, the application will be provisioned with a load balancer and will allow more than one instance. It will be initially provisioned with one instance.
要显示如何设置健康端点,请取消选择单实例环境,如图 10.41所示:
![Figure 10.41 – Load balancer type]()
图 10.41–负载平衡器类型
默认的负载平衡器就是我们想要的。这将使用 HTTP 请求并使用响应状态代码来确定运行状况。继续下一页,如图 10.42所示:
![Figure 10.42 – AWS publish wizard permissions step]()
图 10.42–AWS 发布向导权限步骤
此页面允许您设置应用权限。出于我们的目的,默认值是合适的。继续下一页,如图 10.43所示:
![Figure 10.43 – AWS publish wizard options step]()
图 10.43–AWS 发布向导选项步骤
-
列出了几个选项,但为了简单起见,我们只对默认值进行两次更改。第一个是设置启用增强健康报告。这是一项免费服务,提供有关我们正在运行的服务的附加信息。第二个是健康检查 URL。只有在未启用单实例环境时,您才会看到这一点。我们将此设置为健康检查端点
/health。 -
After clicking Finish, your options are presented for review as shown in Figure 10.44:
![Figure 10.44 – Review]()
图 10.44–审查
-
When you click Deploy, the deployment will begin. This will take time, so be patient.
您可以在**输出**窗口中看到部署的状态,如*图 10.45*所示:

图 10.45——产出
显示 AWS 应用的窗口也将显示在图 10.46中:

图 10.46–环境窗口
这是了解应用的一个很好的工具。花点时间来探索它的一些特性。
由于我们已经启用了健康端点,请密切关注环境的健康:

图 10.47–事件
如上图所示,当健康端点返回不健康响应时,503状态代码作为警告,AWS 会进行报告。
AWS 的下一步
AWS Elastic Beanstalk 是托管 ASP.NET Core 应用的优秀 PaaS 服务,尤其是与其他 AWS 资源(如数据库和存储)结合使用时。我们提供了一个非常简单的示例来帮助您开始。接下来的步骤将是探索 AWS 提供的一些资源。这些可在 AWS 的以下位置找到:
- 与.NET合作:本系列指南位于https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/create_deploy_NET.html 。它们包括在 AWS Elastic Beanstalk 中使用.NET 的参考和指导。
- 部署到弹性豆茎:此系列导轨位于https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/deployment-beanstalk.html 。
我们研究了如何使用部署向导,但还有许多其他方法可以部署到 Elastic Beanstalk。例如在第 12 章与 CI/CD的集成中,我们将考虑直接从 GitHub 部署解决方案。为了找到最适合您和您的团队的方法,探索一种技术是很好的。
接下来,我们将了解 Azure 应用服务是如何工作的。
发布到 Azure
在部分中,我们将向 Azure 应用服务发布应用。此时,您应该创建了一个 Azure 帐户。有几种方法可以部署到 Azure web 应用,与我们的 AWS 示例一样,我们将使用 Visual Studio 中提供的功能。
在 Azure 中使用发布向导
在我们的解决方案中,我们将使用发布向导部署到 Azure 应用服务。
您可以右键单击项目启动向导,如图 10.48 所示:

图 10.48–发布…
此向导将引导您完成一系列步骤,它支持不同类型的发布,包括 Azure、Docker 容器注册表和 IIS。我们将这些步骤分解为指定要部署的内容,然后指定要部署到的位置。
发布到 Azure 应用服务
我们将发布到Azure,所以选择此选项,如图 10.49所示:

图 10.49–发布到 Azure
该向导支持发布到不同类型的 Azure 资源。我们将部署到运行在 Windows 上的 Azure 应用服务。我们也可以部署到运行 Linux 的应用服务。我们还可以将 Docker 映像部署到 Azure 容器注册表,并选择在 Azure 应用服务中运行 Docker 映像。还支持部署到 Azure VM 的选项。
我们将部署到运行在 Windows 上的 Azure 应用服务,因此在此页面上选择第一个选项,如图 10.50所示:

图 10.50–Azure 应用服务(Windows)
既然向导知道我们要部署什么,我们需要指定要部署到哪里。
笔记
根据与 Azure 帐户关联的电子邮件与与与 Visual Studio 关联的电子邮件是否匹配,以下页面可能会有所不同。以下截图来自帐户不匹配和/或您需要使用 Azure 进行身份验证时的截图。
创建新的 Azure 应用服务实例
接下来的系列步骤将使用您先前创建的 Azure 帐户创建一个新的 Azure 应用服务实例。第一步是使用登录链接对 Azure 帐户进行身份验证。图 10.51显示下的链接已经有账户了?标签:

图 10.51–登录
使用 Azure 帐户验证 Visual Studio 后,将向您显示一个与您正在创建的资源类型匹配的现有资源列表。由于这是我们的第一个资源,您将看到(未找到资源),如图 10.52所示:

图 10.52–资源组视图
当您在上图所示的页面上时,选择创建新的 Azure 应用服务链接以使用新的托管计划定义新的资源组。
让我们花些时间来定义这些术语。Azure 中的资源被分组为资源组。这允许将逻辑上相似的资源分组在一起,并提供一种同时管理资源组中所有资源的方法。例如当您准备删除一个网站时,您可以同时删除整个资源组及其所有资源。
在此页面,我们将创建一个新的资源组,如图 10.53所示:

图 10.53–新资源组名称
笔记
您使用的名称并不重要,但如果您希望遵循命名约定,我们建议您使用以下指南:https://docs.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/naming-and-tagging 。
下一步是创建托管计划。托管计划确定区域以及应用服务的所有实例使用的计算资源的大小。与 AWS Elastic Beanstalk 一样,选择您附近的区域。应用服务的大小将决定每月的费用,范围从免费到高级定价。如图 10.54 所示,如果您有免费托管计划,请选择该计划:

图 10.54–创建新的托管计划
在上图中,我们保留了默认名称并选择了悉尼数据中心。我们还选择了Standard 1的尺寸。在您的情况下,您应该可以访问自由大小。
定义了区域和大小之后,我们就可以创建托管计划了。使用创建按钮开始创建托管计划,如图 10.55所示:

图 10.55–创建新的资源组和托管计划
创建托管计划后,您将看到您的资源组和应用服务,如图 10.56所示:

图 10.56–已定义的应用服务
继续,点击完成进入下一步。
我们的发布配置文件现已完成。我们现在看到的页面如图 10.57所示:

图 10.57–准备发布的应用
这将向我们显示将使用的发布配置文件,包括将生成的 URL 和资源组。所有的默认值都是我们想要的,所以请继续并按发布。
在输出窗口中,您可以查看构建和发布的进度,如图 10.59所示:

图 10.58——产出
另一个需要注意的窗口是网络发布活动。这将提供有关发布活动的更多详细信息,如图 10.59所示:

图 10.59–网络发布活动
发布完成后,将使用网站 URL 启动默认浏览器。一旦网站加载到浏览器中,导航到健康端点,如图 10.60所示:

图 10.60–Azure 应用服务运行状况端点
上图中显示的端点表明我们的网站是健康的。多次按下刷新按钮。你应该会看到很多健康的反应,但也有一些不健康的反应。这表明我们的健康端点正在按预期工作。
笔记
与 AWS Elastic Beanstalk 类似,在处理这些示例时,不知道对 ASP.NET 的支持是什么。根据可用的版本,您可能必须针对较旧版本的框架。这是一个方便的地图,显示.NET 与 Azure 应用服务的兼容性:https://aspnetcoreon.azurewebsites.NET/#.NET%20Core%20SDK 。
现在我们已经部署了解决方案,让我们看看 Azure 如何支持健康端点。
健康检查
要查看运行中的健康端点,我们需要在 Azure 门户中查看它。与 AWS 一样,Azure 意识到第一次查看门户可能会让人望而生畏。有很多信息需要了解。与 AWS 一样,Azure 也有一项功能可以帮助您追踪资源—搜索。
在页面顶部,有一个搜索栏。使用此功能时,门户将过滤所有服务、资源和文档。我们输入了chapter,如图 10.61所示:

图 10.61–Azure 门户搜索
这将显示与输入值匹配的应用服务和应用服务计划的方式。选择您发布的应用服务。
在所选应用服务的左侧,您将看到菜单选项。我们想要的选项在监控部分,称为健康检查(预览)。您可以在图 10.62中看到:

图 10.62–健康检查菜单
上图显示了此选项,在撰写本文时,此功能处于预览状态,如图所示。
当您选择健康检查选项时,您将能够启用该功能,并且您可以定义到端点的路径,如图 10.63所示:

图 10.63–健康检查路径
上图显示了使用我们定义的/health路径启用的健康检查。另外,请注意顶部显示的信息,这些信息清楚地表明,如果实例不健康,Azure 将采取什么措施。在我们的例子中,我们只运行一个实例,因此 Azure 只会在实例不健康时提醒我们。如果我们有多个实例在运行,那么不健康的资源将被删除,一个新的资源将被联机以替换它。
启用健康检查后,导航到度量选项。这也在监控部分,如图 10.64所示:

图 10.64——指标
我们感兴趣的指标是健康检查状态。继续添加此指标,如图 10.65所示:

图 10.65–添加健康检查状态
让度量运行一段时间,以查看应用的运行状况随时间的变化。您应该得到一个应用基本正常的图形。图 10.66是我们的指标如何出现的示例:

图 10.66——指标
花一点时间探索其他可用的指标。这是一种简单而有效的监控应用服务的方法。
Azure 下一步行动
Azure 应用服务是托管 ASP.NET Core 应用的优秀 PaaS 服务。像 AWSElastic Beanstalk 一样,应用服务可以与 Azure 中托管的其他服务、其他云提供商甚至本地服务集成。我们提供了一个非常简单的示例来帮助您开始。接下来的步骤将是探索 Azure 提供的一些资源。这些可以在 Azure 中的以下位置找到:
- Azure 快速入门:这些快速入门为使用 Azure 应用服务提供了不同的语言和部署选项:https://docs.microsoft.com/en-us/azure/app-service/quickstart-dotnetcore?pivots=platform-linux。
- 托管和部署:这一系列部署文章提供了一个很好的资源来研究部署 ASP.NET Core 的不同方式:https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/azure-apps/?view=aspnetcore-3.1&选项卡=visual studio。
总结
在本章中,我们介绍了如何使用 AWS 和 Azure 托管我们的 ASP.NET Core 应用。我们简要介绍了云计算,包括如何将资源分类为 IaaS、PaaS 和 SaaS。使用这些分类有助于讨论 AWS 和 Azure 提供的不同产品和服务。我们还讨论了如何使用负载平衡器将流量定向到网站的多个实例。我们研究了网站如何使用健康端点对负载平衡器的健康状态做出响应。
然后,我们看到了将示例 ASP.NET Core 应用部署到 AWS 和 Azure 的两个实际示例。对于这两个示例,我们使用了 VisualStudio 中支持的功能来简化部署过程。我们鼓励您查看这两个云提供商的后续步骤以及进一步阅读部分中的链接。这将提供更多关于这些云提供商提供什么以及不同部署类型的上下文。
下一章将介绍调试和单元测试的基本主题。本文将介绍 ASP.NET Core 和 Visual Studio 用于记录应用活动的一些功能。我们还将重点介绍 VisualStudio 中调试的一些最有用的功能。本章还将介绍构建单元测试,包括 VisualStudio 提供的一些重要功能。
问题
- 虚拟网络允许您定义设备和其他网络之间的路径或路由。这个资源是什么云计算模型的一个例子?
- 运行状况端点是否仅适用于 AWS?
- Azure 是否仅在 Visual Studio 中受支持?
- 哪个云提供商更好:AWS 还是 Azure?
进一步阅读
- 有关 ASP.NET Core 中健康检查的信息,请访问https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks 。
- 有关创建新 AWS 帐户的信息,请访问https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/ 。
- 用于创建新 Azure 帐户和了解账单的模块:https://docs.microsoft.com/en-us/learn/modules/create-an-azure-account/ 。
- Kamil Mrzygłód 为开发者提供的 Azure 实践,来自 Packt Publishing,可在上获得 https://www.packtpub.com/product/hands-on-azure-for-developers/9781789340624 。
- Aurobindo Sarkar 和 Amit Shah 编写的学习 AWS–第二版,来自 Packt 出版社,可在上获得 https://www.packtpub.com/product/learning-aws-second-edition/9781787281066 。
十一、浏览器和 Visual Studio 调试
在上一章中,我们介绍了将 ASP.NET Core 应用部署到两个领先的云提供商:AWS 和 Azure。这两个云提供商都提供了从 VisualStudio 内部管理云的出色支持。本章是对云计算的介绍,我们将在第 13 章、云原生中更详细地介绍云计算。
在本章中,我们将了解浏览器和 Visual Studio 如何帮助我们理解并支持 ASP.NET Core 应用的开发。构建软件非常复杂,知道如何使用可用的工具对于生成高质量代码至关重要。幸运的是,所有主流浏览器都内置了对分析、调试和查看 web 应用的支持。由于 Visual Studio 是我们在大部分章节中使用的集成开发环境(IDE),因此我们将探讨在开发 ASP.NET Core 应用时您应该注意的功能。我们将使用渐进式 Web 应用(PWA来说明内置于浏览器和 Visual Studio 中的功能。
本章将介绍以下主题:
- 普华斯
- 使用浏览器工具进行调试
- 使用 VisualStudio 进行调试
在本章末尾,您将很好地理解如何有效地使用浏览器和 VisualStudio 进行调试。通过有效地使用可用的工具,我们可以深入了解正在创建的代码。这将提高您构建和理解 web 应用的能力。本章介绍如何使用浏览器开发人员工具和 Visual Studio 支持,巧妙地进行编码,以调试和分析我们的 ASP.NET Core 应用。
技术要求
本章包括简短的代码片段,以演示所解释的概念。需要以下软件:
- Visual Studio 2019:Visual Studio 可从下载 https://visualstudio.microsoft.com/vs/community/ 。社区版是免费的,将用于本书的目的。
- .NET 5:可以从下载.NET frameworkhttps://dotnet.microsoft.com/download 。
确保下载 SDK,而不仅仅是运行时。您可以通过打开命令提示符并运行dotnet --info命令来验证安装,如图 11.1所示:

图 11.1–dotnet--信息
前面的屏幕截图显示了编写本章时的版本。
请访问以下链接查看 CiA 视频:https://bit.ly/3qDiqYY
浏览器
在本章中,我们将使用 Chrome 展示浏览器的开发工具如何帮助调试 ASP.NET Core web 应用。Edge、Safari、Firefox 和其他浏览器也以同样的方式支持开发人员工具。我们鼓励您使用自己选择的浏览器探索开发人员工具。
GitHub 源
本章的源代码位于 GitHub 存储库中的https://github.com/PacktPublishing/ASP.NET-Core-5-for-Beginners/tree/master/Chapter%2010 。
设置示例应用
本章的申请将基于Blazor WebAssembly(WASM的样本申请。之所以选择此应用,是因为它提供了足够的复杂性,让人感兴趣,并且为实际应用提供了良好的基础。这个 ASP.NET Core web 应用示例向我们展示了一个很好的单页应用(SPA)示例。在许多方面,应用的行为更像一个桌面应用,而不是传统的网站。
当我们在第 6 章探索 Blazor Web 框架中构建 PWA 时,PWA 将消息发送到信号中心,信号中心将消息实时分发到服务器。在第 6 章**探索 Blazor Web 框架中,我们安装了应用,以显示应用作为本机应用运行,同时仍向服务器发送消息。
在本节中,我们将创建一个类似的 SPA,并使用浏览器中提供的工具,进一步探索 PWS 的含义。在本节结束时,您应该更加了解为什么这项技术令人兴奋。
创建渐进式 web 应用
我们将首先使用 Blazor WASM 模板创建 Blazor 应用。我更喜欢从命令行执行此操作,但您将从 Visual Studio 中获得相同的结果:
dotnet new blazorwasm
继续运行创建的应用,如以下命令所示:
dotnet run
应用启动后,导航到该站点以查看其显示是否正确。
笔记
在本章中,我们将使用 Chrome,但这些步骤中的大多数步骤在另一个浏览器中也同样适用。
本章我们要做的重点页面是计数器,如图 11.2所示:

图 11.2–计数器页面
首先要注意的是,如果您通过按单击我来增加计数器,导航到另一页,然后导航回该页,则当前计数将重置回0。这是因为当前计数存储在页面内存中。页面刷新后,当前计数的值将重置回0。
我们打开项目,导航到计数器页面,如图 11.3所示:

图 11.3–计数器。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++; }}
需要注意的是变量currentCount是一个私有成员变量。未使用变量初始化,且其值未存储在任何位置。这意味着当页面刷新时,它会重置回0。
保存应用的状态
当执行应用时,应用的内容和信息将发生变化。应用的状态是一组信息,可用于在某个时间点描述应用。这一点很重要,因为如果保存应用的状态,则可以将应用恢复到某个时间点。
计数器是应用存储每页刷新计数器状态的示例。这意味着计数器的状态仅持续到下一次加载页面。
对于 web 应用,我们有几个用于存储应用状态的选项。出于本讨论的目的,让我们只关注用户状态——换句话说,与单个用户相关的状态。
下表总结了一些存储状态的常用方法:

有比我们刚才列出的更多的选项,但即使只有表中的选项,我们也有一些选择。在第 7 章、API 和数据访问中,我们研究了在数据库中存储数据。我们还在第 9 章开始使用容器中提到了使用 Redis 缓存。这为我们提供了一个在服务器上存储状态的示例。
在本章中,我们将介绍如何访问浏览器的会话和本地存储以存储应用状态。为了解释为什么这很适合 PWA,让我们花些时间讨论一下这些现代 web 应用。
理解 PWAs
PWA 是使用通用 web 技术开发的应用,旨在使用符合标准的浏览器,包括 Edge、Chrome、Safari 和 Firefox。这些应用与网站的一些关键特性不同:
-
可安装
-
脱机工作
-
支持后台任务
-
Support for push notifications
笔记
在 web 应用开发的早期,通常在服务器上存储用户状态。这些是被称为有状态。有状态应用现在不太常见,因为无状态应用更具可扩展性,更适合 web 应用场景。
通过使用调试器工具,我们将能够更深入地了解 ASP.NET Core Blazor WASM 应用,并了解它如何支持构建 PWA。我们在第 6 章探索 Blazor Web 框架中查看了可安装功能。在本章中,我们将使用调试器工具更深入地了解 PWA 与其他 web 应用的区别。我们还将了解如何在浏览器中支持脱机测试。调试器工具的使用还将为如何设计 PWA 应用提供见解。
在进一步阅读一节中,我们将提供有关 PWAs 的更多信息。
在我们的示例应用中,我们希望存储计数器的状态。在更传统的网站中,每次计数器增加时,我们都会将应用的状态存储在数据库中,并在加载页面时检索值。在我们的示例 PWA 中,我们将使用浏览器在会话和本地存储中存储信息的能力。
让我们在下一节中添加这个。
访问浏览器会话和本地存储
JavaScript 支持访问浏览器会话和本地存储。这种访问采用字符串字典的形式。使用键检索字符串并将其放入存储器。在本例中,我们将获取一个 C#对象,并将其序列化为 JSON 并存储结果。
storageHandling.js
以下将创建一个 JavaScript 文件,用于访问会话和本地存储:
-
The first step is to add a JavaScript file named
storageHandling.jsin thewwwrootfolder. Figure 11.4 screenshot shows the location of the file:![Figure 11.4 – storageHandling.js]()
图 11.4–storageHandling.js
-
We will be creating four functions in this file, and the first function is shown in the following code block:
function SetLocalStorage(key, value) { if (key == null) { console.error("SetLocalStorage called without supplying a key value."); } if (localStorage.getItem(key) != null) { console.warn("Replacing local storage value with key:" + key); } localStorage.setItem(key, value);}SetLocalStorage功能将使用提供的键将给定值放入本地存储器。我们添加了两个检查,将使用两个不同的级别写入控制台:错误和警告。我们这样做主要是为了说明它们是如何反映在本章后面的浏览器工具中的。 -
The following code block retrieves the value stored at a given key:
function GetLocalStorage(key) { console.debug("GetLocalStorage called for key:" + 8key); return localStorage.getItem(key);}同样,我们向控制台添加了一个写操作,但这次我们是在调试级别进行日志记录。这样做的原因在以后会更有意义。
-
The following code block contains two functions for setting and retrieving a value from session storage.
function SetSessionStorage(key, value) { sessionStorage.setItem(key, value);}function GetSessionStorage(key) { return sessionStorage.getItem(key);}这四种方法将提供 Blazor 代码来访问本地和会话存储。
-
For the JavaScript file to be loaded, we will add it to our
Index.htmlfile located in thewwwrootfolder:装载。。。 发生未经处理的错误。
十二、与 CI/CD 集成
在上一章中,我们研究了浏览器和 Visual Studio 如何帮助我们开发 ASP.NET Core 应用。正如我们所看到的,优秀的工具和 IDE 帮助我们构建高质量的软件。
在本章中,我们将了解软件开发中的最佳实践如何有助于构建更好的软件。我们的最佳实践示例为持续集成(CI)和持续交付(CD)。
本章将介绍以下主题:
- CI/CD 综述
- GitHub 概述
- 使用 GitHub 操作的 CI/CD
在本章结束时,您将很好地理解 CI/CD 如何融入软件交付生命周期(SDLC)。您将了解 CI/CD 的好处,以及应用 CI/CD 可以解决哪些挑战。您将了解 GitHub 如何为构建 CI/CD 工作流提供支持。您还将有一个使用 CI/CD 部署 ASP.NET Core 项目的实际示例。
技术要求
在本章中,我们将仅使用 GitHub 来完成部署 ASP.NET Core 项目的实际示例,即使用 GitHub 操作。这意味着您只需要一个现代浏览器,如 Chrome、Edge、Firefox 或 Safari,以及一个 GitHub 帐户。GitHub 提供了一个适用于本章所述所有步骤的免费帐户。
您需要一个 GitHub 帐户来完成这些步骤。页面https://github.com/join 可用于创建账户。
GitHub 源代码
本章的源代码位于 GitHub 存储库中的https://github.com/PacktPublishing/ASP.NET-Core-5-for-Beginners/tree/master/Chapter%2012 。
请访问以下链接查看 CiA 视频:https://bit.ly/3qDiqYY
CI/CD 概述
CI 和 CD 是软件交付的现代方法,自动化用于提高质量和减少延迟。在本节中,我们将定义 CI/CD,并探讨此最佳实践解决的问题。
首先,让我们提供一些背景。
理解为什么 CI/CD
为了欣赏 CI/CD,我们来描述一个典型的开发过程图 12.1展示了开发过程:

图 12.1–开发过程
在前面的图中,我们展示了如何让一个开发团队在自己的设备上开发软件。然后,开发人员的变更被提升到系统集成(SIT环境中进行初始测试。一旦这些内容得到验证,更改将进入用户验收测试(UAT环境。同样,经过一轮测试,这些变化以合理的置信度进入生产(产品。
在某个时候,团队希望将其最新的更改部署到 SIT 环境中。其中一个开发人员可能负责将最新的更改添加到他们的开发机器上,并为 SIT 环境构建和生成一个包。这样做的一个问题是,当在不同的开发机器上进行更改时,一个更改可能会影响另一个更改。在创建 SIT 的生成之前,可能无法发现此问题。
这种方法的另一个问题是,因为构建是在开发机器上执行的,所以在另一台机器上执行构建之前,可能无法发现所需的构建依赖项。想象一下这样一个场景,通常执行构建的开发人员得到了应得的休息。当然,关于如何执行构建和创建包含最新更改的包的说明已经留下,但是缺少依赖项。寻找缺失的依赖关系可能是一项代价高昂的工作。
这些问题通过 CI 解决。
连续积分
CI 通过使用自动化增加了我们对正在生成的代码的信心。构建包的步骤各不相同,但以下是有关 CI 的关键点:
- 源代码保存在版本控制的存储库中。
- 源代码是在已知环境中构建的。
- 源的构建是自动化的。
例如,通过将源代码放在一个版本控制的存储库(如 GitHub)中,我们可以确信只有我们想要更改的内容发生了更改。这确保了我们的开发团队都合并到同一个位置,并且我们可以检查合并以确保它们是完整和准确的。
通过在已知环境中构建源代码,我们可以确保所有必需的依赖项都可用。在大多数情况下,一个已知的环境将是一个专用的构建机器或 VM。在使用 GitHub 操作构建 CI/CD部分,我们将使用 GitHub 提供的 Linux 虚拟机。这意味着我们有一个用于构建的一致平台,如果存在任何必需的依赖项,我们负责确保它们可用。例如,我们需要.NET5.0 来构建示例应用,这将作为一个单独的步骤添加到 Linux 虚拟机中。
源的构建将实现自动化。这既提高了效率又提高了可靠性。自动执行一系列步骤更有效,因为这样可以让个人自由活动,从而集中精力从事其他活动。这更可靠,因为我们排除了人类忘记执行步骤的可能性。
笔记
CI 中的一个常见步骤是运行单元测试。单元测试是为验证功能而设计的测试。这些测试可以手动运行,也可以作为构建过程的一部分运行。
通过自动化构建过程并在已知环境中执行解决方案的打包,我们能够提高效率并提高我们对开发团队正在进行的更改的信心。我们从 SDLC 的研究中了解到,问题越早被发现,修复成本就越低。通过在部署更改之前识别任何构建失败或功能中断,我们大大降低了修复这些问题的成本。在 CI 之前,可能只有在需要发布版本之前才检测到已损坏的版本,因此原始更改可能已经进行了几天。在创建 CI/CD 工作流部分,我们将设置每次检入存储库时要执行的 CI。
接下来,我们将了解如何使用 CD 来改进交付流程。
连续交付
现在,想象一下如果每个环境都由几个服务器组成。举一个负载平衡的例子,如图 12.2所示:

图 12.2–负载平衡应用
可能每个环境都有不同数量的服务器。例如,SIT 可能只需要两台服务器,而使用更频繁的 PROD 可能需要 10 台或更多服务器。这里重要的一点是,不同的环境可能会有所不同,对于单个版本,可能需要更新多个服务器。
此外,软件的每个版本可能需要多个步骤。例如,假设我们正在发布一个 ASP.NET Core 应用。对于每个版本,我们可能需要删除以前的版本,添加应用的新版本,然后执行一些自定义配置。细节并不重要。重要的是,我们必须准确地遵循一系列步骤,否则发布的软件可能无法正确运行。在自动化之前,这个过程应该是手工完成的。手动步骤会引入错误和遗漏步骤的可能性。
与 CI 一样,CD 使用自动化大大提高了交付过程的效率和信心。由于每个环境可能需要更新多个服务器,并且对于每个服务器,需要多个步骤,因此避免手动过程对于节省时间以及减少未正确遵循步骤或遗漏步骤时出错的机会是有意义的。
笔记
持续部署是指每次变更通过所有要求的检查后,一直进入最终环境。简言之,整个过程是自动化的,只有未通过自动化测试的更改才能被阻止发布。
简言之,CI/CD 通过使新软件更改的发布更高效、更可预测,使用自动化来极大地改进 SDLC。通过添加自动化测试,我们可以提高信心,即更改不会以意外的方式错误地改变行为。通过尽早发现问题,我们大大降低了解决问题的成本。自动化帮助我们的团队更有效地工作,因为他们不需要手动执行构建和部署步骤。自动化有助于减少由于人为错误导致的手动任务所产生的错误。
现在,我们已经对 CI/CD 有了很好的理解,接下来让我们看看 GitHub 对 CI/CD 有哪些支持。
介绍 GitHub
在本节中,我们将了解 GitHub 及其对 CI/CD 的支持。GitHub 是托管工具的提供商,支持软件开发所需的许多功能。GitHub 的主干是 Git,一个可靠的源代码版本控制系统。但是 GitHub 不仅仅是 Git,它还提供满足分布式软件开发的许多需求的在线实用程序。
笔记
Azure DevOps 是另一个用于构建 CI/CD 的 Microsoft 服务。在许多方面,构建 CI/CD 的经验是相同的,我们鼓励您花时间研究 Azure DevOps,因为它可能为您的需求提供更好的 CI/CD 平台。我们将在第 13 章云本机中讨论 Azure DevOps。
在下一节中,我们将研究 GitHub 支持的不同计划。
GitHub 是免费的吗?
是的,提供的基本服务是免费的。对于许多社区项目和/或涉及小型团队的项目,免费订阅效果良好。让我们简单地看一下不同的计划是如何比较的,如下表所示。

请注意,还有一个 GitHub One 计划,它提供了企业计划中的所有内容,同时为大型企业添加了更多功能,如全天候支持、更多指标和学习实验室课程。
最棒的是,您可以通过免费订阅加入,当您的情况发生变化,每月需要更多存储或操作时,您可以将您的计划升级到适当的计划。
在接下来的部分中,我们将在继续我们的 CI/CD 示例之前回顾 GitHub 的一些特性。
一些 Git 术语
正如我们在前几章中使用 GitHub 一样,我们假设对 Git 有些熟悉。到目前为止,不需要创建自己的代码分叉,就可以完成所有章节。fork 是存储库的副本,通常简称为回购协议,将存放在您的帐户中。这意味着您可以用它做任何事情,包括进行更改。例如,您可能会在将某些包升级到更高版本时发现问题。这将允许您修复变更,验证其是否有效,并将变更发布回原始回购协议,称为主协议。
本章还有几个其他术语,您应该熟悉,所以我们在下表中列出了它们:

上表在一定程度上简化了术语,但我们只需为 CI/CD 提供示例即可。我们将在本章末尾的进一步阅读一节中包括一些参考文献。
在下一节中,我们将执行 Packt 库的 fork。
复制回购协议
如果您尚未创建 fork,可以使用Packt 源页面上的fork按钮来创建 forkhttps://github.com/PacktPublishing/ASP.NET-Core-5-for-Beginners 如图 12.3所示:

图 12.3–叉子
这将在您的 GitHub 帐户中创建您自己的副本。从这里,我们将能够使用 GitHub 操作在 CI/CD 中完成 CI/CD 的设置。
在此之前,我们先介绍一下 GitHub 除了提供 Git 之外的一些其他特性。
GitHub 对 CI/CD 的支持
GitHub Actions 可用于所有免费订阅,并支持自动构建和部署应用。这些不必是基于 web 的应用,甚至根本不必是应用。例如,一些组织使用 GitHub 进行文档管理,使用 GitHub 操作在组织内分发文档。但我们对使用 GitHub 操作实现 CI/CD 感兴趣,本节将概述 GitHub 操作的功能。
GitHub Actions 允许我们定义一系列步骤,称为工作流,可由指定事件触发。事件可以基于另一个事件、计划事件或手动触发事件。例如,在 BuildingCI/CD using GitHub Actions部分中,我们将使用 Git push 事件触发我们的工作流。
每个工作流可以由一个或多个作业组成。作业是为在指定类型的转轮上运行而设计的一系列连续步骤。将跑步者视为虚拟机的一类。例如,您可能需要构建 Windows 软件包或使用特定类型的硬件。流道定义所需的机器类型。运行程序可以是 GitHub 托管的或自托管的。在我们的 CI/CD 示例中,我们将使用 GitHub 托管的 Linux 虚拟机构建 ASP.NET Core 应用。这是因为我们将在 Linux 中托管应用的目标环境。
在单个工作流中,可以使用多个运行程序的组合。例如,您可能有一个执行两个作业的工作流。第一个是构建 Windows 映像,第二个是构建 Linux 映像。一个工作流将在 Windows 运行程序上运行,而第二个工作流将在 Linux 运行程序上运行。默认情况下,每个作业将并行运行。在我们的 CI/CD 示例中,我们将展示如何在两个作业之间创建依赖关系。创建依赖项时,作业不会并行运行,而是按依赖项顺序运行。
如前所述,作业是一系列步骤。每个步骤都可以是一个操作或一个命令。动作是命令的组合。这些由 GitHub 或社区成员公开。您还可以编写自己的操作。例如,您的组织可能有一个您希望在多个工作流中使用的专有签名流程。然后,您可以编写私人操作并在工作流中引用它。我们将在下一节的 CI/CD 示例中使用操作和命令步骤。
工作流在 YAML 文件中定义。如果您还记得第 9 章容器中的内容,YAML 是一种设计为人类可读且语法最少的文件格式。这确实意味着空白,例如制表符和空格字符,是重要的。幸运的是,GitHub 有一个 YAML 编辑器,它既提供 IntelliSense 又提供可视提示,以帮助编写过程。我们将在下一节创建 CI/CD 工作流时使用编辑器。
现在我们已经了解了 CI/CD 和 GitHub 操作的背景,让我们继续下一节,创建 CI/CD 工作流以交付 ASP.NET Core 应用。
使用 GitHub 操作构建 CI/CD
我们已经讨论了 CI/CD,并了解了 GitHub 提供的一些功能,现在让我们看看如何使用 GitHub 操作部署 ASP.NET Core 应用。在上一节中,我们已经将 Packt master 分叉到我们自己的回购协议中,制作了回购协议的副本,我们准备开始了。
我们的计划是将 ASP.NET Core WASM 应用部署到 GitHub 页面。我们在上一章中介绍了 WASM 应用示例。下一节将概述 GitHub 页面。
什么是 GitHub 页面?
GitHub Pages 是一种方便而强大的方式,可以使用 GitHub 的所有功能(包括全球发行版)托管静态网站,而无需担心托管问题。在许多情况下,这是一种方便的方式,可以使用与之相关联的存储库的信息来托管网站。但没有理由不能在其他情况下使用。例如,在 Google 或 Bing 中,以搜索build a blog in GitHub Pages为例。而且,由于静态网站源于 GitHub,因此网站内容存储在私有或公共版本控制的存储库中。
静态网站的来源可以是主分支中名为/docs的特殊文件夹,也可以是单独的分支。在我们的示例中,我们将把项目的内容发布到一个单独的分支。GitHub 页面通常由静态网站生成器 Jekyll 提供动力。在我们的例子中,我们不需要静态来驱动我们的网站,所以我们需要禁用 Jekyll。
这只需创建一个名为.nojekyll的文件即可完成。在 GitHub 中,导航到Chapter 12 sample project中的wwwroot文件夹。记得在我们之前制作的分叉副本中这样做,如图 12.4所示:

图 12.4–创建一个新文件
在前面的屏幕截图中,我们可以看到wwwroot文件夹的位置和创建新文件的下拉列表。选择后,只需输入名称.nojekyll,并通过提交更改进行保存,如图 12.5所示:

图 12.5–提交新文件
通过将文件放置在wwwroot文件夹中,该文件将在我们稍后发布网站时包含在内。现在这将让 GitHub 知道我们不需要 Jekyll,让我们开始 GitHub 操作。
创建 CI/CD 工作流
GitHub Actions 允许我们构建 CI/CD 工作流。在其他管理 CI/CD 的工具中,比如 Azure DevOps,我们将在第 13 章、Cloud Native中讨论,您可以看到这被称为管道。术语 CI/CD 管道或工作流基本上是指一系列自动化操作。在我们的示例中,按此顺序我们将有两项主要工作:构建项目和部署项目。
这些操作包含在 YAML 文件中。让我们继续创建一个。在 GitHub 中点击菜单栏中的动作,如图 12.6所示:

图 12.6–操作菜单
由于我们的存储库没有任何现有的操作,我们有几个选项,包括几个帮助我们开始的模板。通读它们,了解不同支持的场景,准备好后,选择选项自行设置工作流,如图 12.7所示:

图 12。7–自己设置工作流程
这将创建一个起始 YAML 文件,但让我们将生成的文件替换为以下内容,以便我们在完成时解释不同部分:
name: Build and Deploy ASP .NET Core Chapter 12 to GitHub Pages on: # trigger the workflow only when a push happens in Chapter 12 jobs: build: steps: # steps to build the application deploy: steps: # steps to deploy the application
该名称用于描述工作流。好的名称有助于确定工作流的目的。假设您有一个用于部署到开发环境或生产环境的工作流。这应反映在名称中,以避免混淆。
接下来,我们定义触发工作流的因素。这可以从手动触发器到拉取或拉取到存储库,也可以按计划进行。不同的功能可以在本章末尾的进一步阅读一节中找到。
继续并替换现有注释,如下所示:
on: push: branches: [ master ] paths: - ‘Chapter 12/Chapter_12_GitHubActions_Examples/**’
在Chapter 12文件夹中的主分支上执行推送操作时,前面的代码段将触发工作流。这意味着无论何时将更改提交到Chapter 12下任何文件夹中的存储库,都将运行此工作流。
现在我们已经定义了触发器,让我们在下一节中完成构建工作。
创建持续集成作业
在本节中,我们将定义 CI 或构建作业。此作业将包括以下步骤:
- 从存储库中检索源代码。
- 设置.NET 环境。
- 发布 ASP.NET Core 应用。
- 将发布的应用另存为工件。
在你阅读了这个列表之后,你可能会想,为什么我们有前两个步骤?
答案将我们带到设置工作的第一部分。每个作业都在构建运行程序上运行。这些是预配置的 Windows 或 Linux 虚拟机。您可以使用自己的跑步者,称为自托管跑步者。出于我们的目的,我们将通过添加以下以粗体显示的代码段来使用 Linux VM:
jobs: build: runs-on: ubuntu-latest steps:
既然我们已经指定构建作业应该在 Linux VM 上运行,那么让我们添加第一步:
-
After the
steps:line, add the following code snippet:- uses: actions/checkout@v2步骤主要有两种类型:
run和uses。run步骤用于在运行程序上执行命令。uses命令将执行社区操作。将社区行动视为包含一组为完成任务而创建的run语句的存储库。在前面的代码片段中,我们正在执行 checkout 社区操作的版本 2。签出操作将签出存储库,以便工作流可以访问它。笔记
-
The next step sets up .NET on the runner. Unless we set up the .NET environment, the runner will not be able to run any required
dotnetcommands:- uses: actions/setup-dotnet@v1 with: dotnet-version: ‘5.0’在前面的代码片段中,我们将使用社区
setup-dotnet@v1操作,我们需要指定所需的.NET 版本。 -
The next step is to run the
publishcommand. This is shown in the following code block:- name: Publish app run: dotnet publish -c Release ‘./Chapter 12/Chapter_12_GitHubActions_Examples/Chapter12.csproj’前面的命令说明了如何将名称与步骤相关联,
uses步骤也支持此操作。发布命令指定了Release配置以及我们正在发布的项目文件。 -
In order to be able to reference the published application in the next job, we are going to publish or save the published application as an artifact. You have 500 MB of storage, so we are going to use some of that to store our published application:
- name: Save artifacts uses: actions/upload-artifact@v2 with: name: myWASM path: ‘./Chapter 12/Chapter_12_GitHubActions_Examples/bin/Release/net5.0/publish/wwwroot’前面的代码片段将上传
path参数中指定的内容,作为名为myWASM的工件。
这就完成了名为build的第一个作业。每当签入发布到Chapter 12时,此工作流将运行。源代码将被下载到 Linux 运行程序中并生成,输出将保存为工件。已完成的作业显示在下一个代码段中:
steps: - uses: actions/checkout@v2 - uses: actions/setup-dotnet@v1 with: dotnet-version: ‘5.0’ - name: Publish app run: dotnet publish -c Release ‘./Chapter 12/
Chapter_12_GitHubActions_Examples/Chapter12.csproj’ - name: Save artifacts uses: actions/upload-artifact@v2 with: name: myWASM path: ‘./Chapter 12/Chapter_12_GitHubActions_Examples/ bin/Release/net5.0/publish/wwwroot’
既然已经定义了工作流的 CI 部分,那么让我们继续 CD 部分。
创建连续部署作业
在本节中,我们将定义一个 CD 作业,以将发布的工件部署到名为pages的新存储库中。为此,我们需要设置pages存储库,下载工件,然后保存更改。
笔记
CD 作业是使用基本 Git 命令创建的。我们建议探索社区行动,而不是总是写自己的。GitHub 的好处之一是您是大型开发人员社区的一员。GitHub Marketplace 是一个很好的起点。
与 CI 作业一样,我们还必须指定用于运行作业的生成运行程序。我们还将使用 Linux 虚拟机,如以下代码段所示:
deploy: needs: build runs-on: ubuntu-latest steps:
另外,请注意前面的代码片段中显示的与 CI 作业相比的差异。我们已经指定,build作业需要在没有错误的情况下完成,deploy作业才能运行。如果我们没有这样做,build和deploy作业将并行运行。在我们的例子中,这不起作用,因为我们需要在build作业中发布工件,以便部署到 GitHub 页面:
-
与
build作业中的第一步一样,我们将首先执行签出,在 VM 上设置 GitHub 工作区:- uses: actions/checkout@v2 -
Next, we will create a new branch to contain our GitHub Pages WASM application:
- name: Create pages branch continue-on-error: true run: | git config --global user.name “GitHub Actions” git config --global user.email “your@email.com” git checkout -B pages前面的一系列命令首先设置有关当前用户的信息。这为 GitHub 提供了上下文,并将在执行签入时使用。下一步发出切换到
pages分支的命令。如果分支不存在,-B标志将创建一个新分支。 -
The next step in our job is to clear the branch of the existing files:
- name: Clear pages branch continue-on-error: true run: | git rm -rf . git commit --allow-empty -m “root commit” git push -f origin pages前面的代码将删除任何现有文件,将更改提交到存储库,然后将其推回存储库。如果存储库中已有以前部署的文件,则需要执行此步骤。
-
Now that we have cleaned the folder, we want to download the output that we created in the
buildjob:- name: Download build artifact uses: actions/download-artifact@v2 with: name: myWASM前面的命令使用社区操作下载名为
myWASM的工件。 -
The final step will commit the changes back to the
pagesbranch:- name: Commit changes run: | git add . git commit -m “publishing WASM” git push --set-upstream origin pages在前面的命令中,下载的工件中的文件被添加回存储库,提交,然后推回存储库。
这就完成了我们的工作流程。继续保存文件并继续下一节。
监测行动
既然我们的 CI/CD 工作流已经定义,现在是我们触发工作流的时候了。由于我们对Chapter 12文件夹所做的更改使用了路径过滤器,所以让我们编辑其中一个文件。
在Code页签中,导航到wwwroot文件夹,如图 12.8所示:

图 12.8–wwwroot 文件夹
在该文件夹中,选择index.html文件并使用铅笔图标编辑该文件,如图 12.9所示:

图 12.9–铅笔图标
继续并更改 title 元素中的文本,如图 12.10所示:

图 12.10–编辑标题
提交更改后,导航到操作选项卡。您应该会看到类似于图 12.11的内容:

图 12.11–所有工作流程
这表明工作流已被触发且当前正在运行。将提供以前运行的历史记录。让我们单击正在运行的工作流以查看正在发生的事情的详细信息。
这将更改视图以显示正在工作流中运行的作业。在图 12.12中,构建并部署 ASP.NET Core 第 12 章至 GitHub Pages工作流包括构建和部署两个作业,并已顺利完成工作流:

图 12.12–工作流详细信息
另外,请注意生成的工件myWASM是如何显示的。该工件是一个 ZIP 文件,它允许您下载该文件,以防您需要解决任何问题。
在查看 GitHub 页面之前,我们还需要做最后一步。
配置 GitHub 页面
在本节中,我们将设置 GitHub 页面。我们将使用 GitHub 页面托管 CI/CD 工作流的输出,幸运的是,GitHub 页面提供了一种灵活的方式来选择内容在存储库中的位置:
-
GitHub Pages can be configured under the Settings tab:
![Figure 12.13 – Settings]()
图 12.13–设置
-
In Settings, scroll down until you find the section about GitHub Pages, as shown in Figure 12.13:
![Figure 12.14 – GitHub Pages]()
图 12.14–GitHub 页面
上图显示 GitHub 页面当前已禁用。
-
To enable it, we select the pages branch as shown in Figure 12.15:
![Figure 12.15 – The pages branch]()
图 12.15–页面分支
-
After saving, the URL of your GitHub Pages site will be shown. It should be similar to Figure 12.16:
![Figure 12.16 – GitHub Pages published URL]()
图 12.16–发布的 GitHub 页面 URL
-
After clicking on the URL, we will encounter an issue as seen in Figure 12.17:
![Figure 12.17 – Loading issue]()
图 12.17–装载问题
-
If you review the errors in the browser’s developer tools (press F12 to access them), you will see several of the files are not able to be loaded as shown in Figure 12.18:
![Figure 12.18 – 404 errors]()
图 12.18–404 错误
-
继续并导航到网络选项卡,然后按刷新再次加载页面。您应该会看到相同的网络错误,但这次如果您单击其中一个失败的请求,您将获得一些附加信息,如图 12.19所示:

图 12.19–请求 URL
在上图中,请注意 URL 的构造不正确。正确的 URL 应该包含存储库的名称。在本例中,这将是https://chilberto.github.io/ASP.NET-Core-5-for-Beginners/css/bootstrap/bootstrap.min.css 。
幸运的是,解决方法很简单。
固定基准基准
在本节中,我们将为我们的网站设置基准参考。我们需要这样做,因为 GitHub 不是在网站的根目录下托管页面,而是在存储库名称下托管页面。这意味着我们需要将存储库名称插入 URL:
-
Back in the Code tab, navigate to the
wwwrootfolder and select theindex.htmlfile. In the file, locate thebaseelement as shown in Figure 12.20:![Figure 12.20 – Updating the base element]()
图 12.20–更新基本元素
-
将此行更新为以下内容:
<base href=”/ASP.NET-Core-5-for-Beginners/” /> -
提交更改后,将再次触发工作流。等待此操作完成。
-
完成后,刷新 GitHub 页面,您将看到 ASP.NET Core WASM 应用,如图 12.21 所示:

图 12.21–你好,世界!
根据您的浏览器和 GitHub 刷新更改的速度,您可能需要再等一分钟才能注意到更改。如果更改仍然没有反映出来,请尝试清除或禁用浏览器的缓存。
您可以在网络页签中选择禁用缓存,如图 12.22所示:

图 12.22–禁用缓存
在我们禁用了网络选项卡上的缓存之后,现在我们运行了一个基本的 CI/CD 工作流,让我们进一步了解一下发生了什么。
记录 CI/CD 工作流
不幸的是,有时事情并不顺利。自动化应用的构建和部署的原因之一是为了防止人为错误,但我们如何调查 CI/CD 工作流中出现的问题?本节将打断我们的 CI/CD 工作流,以说明如何在生成步骤中出现问题时进行调查:
-
To do this, let’s cause a syntax error in our code. In the code branch, navigate to the project file as shown in Figure 12.23:
![Figure 12.23 – Breaking the project]()
图 12.23——打破项目
-
Inside the project file, find the section that specifies the target framework as shown in Figure 12.24:
![Figure 12.24 – Target framework]()
图 12.24–目标框架
上一个屏幕截图显示项目文件指定.NET 5.0 作为目标框架。
-
Go ahead and change this value to
netcoreapp3.1,as shown in the next code snippet:<TargetFramework>netcoreapp3.1</TargetFramework>提交文件后,工作流将自动启动,但在发布 ASP.NET Core 项目时将失败。
-
Click on Actions and then running workflow and monitor the workflow until it fails as depicted in Figure 12.25:
![Figure 12.25 – Failure publishing app]()
图 12.25–发布应用失败
前面的屏幕截图显示了失败后工作流的状态。注意构建步骤如何指示发布应用步骤失败。还要注意,下面的步骤保存工件没有运行。而且,下面的作业部署也没有运行,因为我们已经指定它依赖于构建作业,并且没有错误地完成。
-
We can expand the Publish app step to view additional details. Have a look through the log to find where the error is reported. An example of this is given in Figure 12.26:
![Figure 12.26 – Error reported]()
图 12.26–报告的错误
-
花点时间找出失败的原因。您应该发现文本5.0.0 与 netcoreapp3.1不兼容,这表明我们尝试使用的包与.NET CoreApp3.1 框架不兼容。
我们想强调一个很好的特性。您会注意到日志中的每一行都有编号。如果你点击这个号码,你会注意到 URL 的变化。例如,我们单击第 32 行的第一个失败,我们的 URL 更改为https://github.com/chilberto/ASP.NET-Core-5-for-Beginners/runs/1409342784?check_suite_focus=true#step:4:32 。该 URL 可以与其他队友共享,而不是说构建被破坏,请调查,可以将该 URL 发送给队友,让他们立即了解报告的问题。
我们将在进一步阅读中包含更多关于 GitHub 操作的信息,因为我们只强调了一些基本功能和特性。
GitHub 操作的下一步
GITHUB 操作胡里有很多值得注意的特性,尤其是考虑企业场景时。在前面几节中使用的示例中,我们将部署到单个环境,在许多企业场景中,将有多个环境。每个环境可能需要不同的配置,例如连接字符串。解决此需求的一种方法是使用机密。
存储库秘密是一个加密变量,可以在 GitHub 操作中使用。在公共和私有存储库中,只有具有适当访问权限的用户才能查看和维护机密。在设置子菜单中定义了一个秘密,如图 12.27所示:

图 12.27–GitHub 机密
一旦定义了秘密,就可以通过 GitHub 操作访问它。例如,假设我们为每个环境的数据库访问定义了三个秘密,如图 12.27所示:

图 12.28–定义秘密
在 GitHub 操作中,可以使用以下语法访问该值:
${{ secrets.QA_DATABASE_KEY }}
在前面的代码中,QA_DATABASE_KEY秘密中的值将被替换到动作中。这不仅比存储在 YAML 文件中更安全,而且为在多个环境中重用同一脚本提供了一种方便的方法。
要了解为什么它更安全,我们需要查看我们的工作流。在存储库中,导航回存储库的根目录,如图 12.29所示:

图 12.29–.github/workflows 文件夹
在前面的截图中,我们可以看到一个文件夹.github/workflows已经创建。此位置是 GitHub 在存储库中存储工作流的位置。如果查看文件夹内部,您将看到我们先前创建的工作流:

图 12.30–main.yml
另一个需要强调的特性是 GithubAPI。GitHub API 提供了访问 GitHub 的编程方式。通过基于 GitHub API 事件设置要触发的工作流,可以与 GitHub 操作相结合。例如,假设一个场景,只有在测试负责人批准发布时,才会向生产发布。这可以在另一个设计用于管理测试用例的系统SystemX中完成。审批完成后,SystemX 使用 webhook 通过创建标记通知 GitHub。标签是标记发布的常用方式。
笔记
webhook 是一种轻量级 web 服务。参见https://docs.github.com/en/free-pro-team@最新/休息了解更多信息。
然后,我们使用以下方法创建一个在创建标记时触发的工作流:
on: create
这是一个示例,说明了如何将不同的功能结合使用,以构建符合您需求的 CI/CD 流程。
另一个需要提及的重要方面是 CI/CD 流程不必组合到单个工作流中。我们在示例中这样做了,但是我们可以有一个单独的 CI 和 CD 工作流。CI 工作流仍将发布包,而 CD 工作流将在将包添加到注册表时触发。以下代码段提供了所需的触发器:
on: registry_package: types: [published]
我们将在进一步阅读一节中包含其他链接。
总结
在本章中,我们讨论了 CI/CD,并提供了一个使用 GitHub 操作的实际示例。CI/CD 提供了一种更好的方式来交付我们的 ASP.NET Core 项目。它比手动部署更高效,也更不容易出错。即使是我们提供的简单示例应用也有多个部署步骤。对于较大的项目,步骤的数量可能会变得足够多,从而使部署到大型环境变得不切实际。
GitHub 使用 GitHub 操作对 CI/CD 提供了强大的支持。我们自动化了 ASP.NET Core WASM 应用的构建和部署。工作流同时使用了命令和社区操作。我们的示例工作流是由 Git 推送到存储库触发的,在 GitHub 操作的下一步部分中,我们强调了 GitHub API 如何通过其他 GitHub 事件触发工作流。
在下一章中,我们将介绍如何构建云本机应用。这不仅仅是选择一种伟大的技术,例如 ASP.NET Core,用于构建应用。我们将研究不同类别的云服务。我们将研究与传统应用相比,在构建云计算时需要做出的设计决策。
问题
- GitHub 行动是否需要付费计划?
- 您只能将 GitHub 用于 web 应用吗?
- GitHub 操作是否要求 CI 和 CD 位于同一工作流中?
- 在部署到云提供商时,您可以使用 CI/CD 吗?
进一步阅读
- Git 提供的 Git 概述,网址:https://git-scm.com/
- GitHub 团队提供的 GitHub 概览,网址:https://guides.github.com/activities/hello-world/
- GitHub 团队在提供的 GitHub 行动 https://docs.github.com/en/free-pro-team@最新/行动
- 关于 GitHub 团队触发工作流的信息,网址为:https://docs.github.com/en/free-pro-team@触发工作流的最新/操作/参考/事件
- Chris Love 的通过示例进行的渐进式 Web 应用开发,来自 Packt Publishing,可访问:https://subscription.packtpub.com/book/application_development/9781787125421
- GitHub Essentials:使用 GitHub 释放协作开发工作流的力量,Achilleas Pipinellis 的第二版,来自 Packt 出版社,网址:https://subscription.packtpub.com/book/web-development/9781789138337
- 实施 Azure DevOps 解决方案,作者:Henry Bee,Maik van der Gaag,来自 Packt 出版社,网址:https://subscription.packtpub.com/book/cloud_and_networking/9781789619690
十三、开发云原生应用
在过去几年中,没有一个流行语比云更为突出,对于开发者来说,这一术语已经扩展到云原生应用。看看 C#语言中较低级别的细节,你会认为无论代码在哪里执行,它们的工作原理都是一样的,因此你会怀疑它是否有什么意义,或者它是否只是炒作。
第 10 章部署到 AWS 和 Azure中,演示了大量云部署。在本章中,我们将深入研究并了解在构建云本地应用时需要了解和考虑的事项,以及审查云计算中心的概念。
本章将介绍以下方面:
- 什么使应用云成为本机的?
- DevOps 的作用
- 理解云中的成本
- 云存储与本地磁盘
- 作为代码的基础设施
- 监测和保健
本章介绍了支持云开发者角色的实用代码示例和云架构师领域的理论。目的不是让您在本章末尾成为一名成熟的架构师,而是让您了解一些云范例如何影响您作为一名.NET 开发人员。
技术要求
本章包括简短的代码片段,以演示所解释的概念。要使这些功能正常工作,需要以下软件:
- Visual Studio 2019:Visual Studio 可从下载 https://visualstudio.microsoft.com/vs/community/ 。社区版是免费的,将用于本书的目的。
- 有些示例要求您拥有 Azure 订阅。如果您还没有,您可以通过转到 Azure 门户(来创建一个 https://portal.azure.com 并注册一个免费帐户。
- DevOps 示例涉及 Azure DevOps。单个开发者可以在注册一个免费帐户 https://dev.azure.com 。
出于实验室目的,本章中的所有样品都可以免费测试,但区域特定要求可能要求使用信用卡进行验证。
本章的源代码位于 GitHub 存储库中的https://github.com/PacktPublishing/ASP.NET-Core-5-for-Beginners/tree/master/Chapter%2013 。
请访问以下链接查看 CiA 视频:https://bit.ly/3qDiqYY
是什么让应用云成为原生的?
在深入研究云应用是如何实现的,以及您需要考虑的问题之前,我们需要使用 To.t0 来研究云应用是什么。这包括云操作模型的一般定义、云与内部部署的区别以及对这些差异的更详细调查。对于术语云原生没有标准的教科书答案,也没有勾选复选框的列表,但本章试图阐明与该术语相关的常见实践。
有了云计算,你会听到各种即服务后缀,其中一些后缀比更有意义。如第 10 章部署到 AWS 和 Azure中所述,这里的支柱是以下三个首字母缩略词:
- IaaS–基础设施即服务
- PaaS–平台即服务
- SaaS–软件即服务
解释这些的经典类比是比萨饼服务。在光谱的一端,你有一个场景,你在厨房里开始用面粉、西红柿和所有其他需要的配料。你可以完全控制一切,可以根据自己的喜好定制东西。另一方面,你要去一家餐馆,在那里你指向菜单上的一个条目,等几分钟,然后把它送到你的桌子上。在这两者之间,你可以选择购买预先制作好的面团和酱汁,让它更像是家里的乐高积木体验。
如果你是一个好厨师,你可能会通过从头做起来获得好的效果。然而,即使你碰巧喜欢这项工作,也不可否认,它比外出就餐更重要。如果你碰巧是一个糟糕的厨师,那么最好在当地的比萨饼外卖店点一份。
为了避免偏离技术主题太远,您的责任与云提供商的责任可以在图 13.1中说明:

图 13.1–XaaS 责任
正如你所看到的,更多的控制意味着更多的责任。
我们都喜欢拥有内部硬件为我们提供的各种选择,但在实践中,餐厅体验也可以相当不错。早期的云服务通常过于狭窄,无法在严格定义的边界之外使用,但现在通常有很多方法可以根据您的喜好调整服务,因此这不是一个问题。
IaaS 与内部部署模型相比具有优势,但与关注基础设施的个人相比,开发人员对其中大多数模型的兴趣较小。拥有虚拟化的硬件是很好的,但是大多数开发人员已经将他们自己从中抽象出来,不去弄清楚哪根电缆在服务器机架的后面。由于这一事实,顶层五层可以捆绑到虚拟机(虚拟机)中,因此或多或少可以直接从内部部署到 IaaS。
如果您已经在本地运行虚拟机,那么此操作相当容易,因为只需重新配置一些操作系统设置,就可以按原样上传。如果您没有使用虚拟化并直接在服务器上运行,或者通常称之为裸机,那么有一些工具可以将工作负载迁移到虚拟机上,您可以随后迁移到云上。这种云迁移称为升降。虽然这是一种可能的、有时是推荐的重新托管模式,但它不被认为是云本机。云上运行的 VM 仍然意味着参加补丁周二。(微软每个月的第二个星期二发布一次补丁,因此得名。)
SaaS 对最终用户来说是非常好的——这些服务中的一些可以用很少的技术洞察力来购买,而且它们只起作用。当然,如果你想学习如何制作一个好的比萨饼,那么购买成品几乎没有什么帮助,因此,即使你享受这些服务,也可能无法帮助你为自己的目的构建新的应用。
作为开发人员,PaaS 通常是构建新服务的最佳选择。您可以选择最好的预制组件并在这些组件的基础上进行构建,而不是自己构建堆栈的每一部分。例如,您将需要某种 web 服务器,并且您希望确保它能够运行您的编程语言和相关框架,但您并不真正关心如何提供这种服务的底层细节。在安装应用之前,web 服务器本身只是一个空壳,没有任何价值。
可以说,Office 365 这样的服务既是 SaaS 又是 PaaS,因为它提供了丰富的 API 层,可以与您自己的服务进行集成,但这样的观察不会改变基本模型。
这将作为有用的背景信息,当我们深入研究下一个主题时,我们将比较经典的本地特性和云等效特性。
比较本地与云的特性
许多人听到云计算时首先说的是,它只是另一个数据中心,只要你安装了正确的软件,它实际上与你自己搭建几个机架没有什么不同。从某种意义上说,这是正确的。虽然大多数公司无法承受 Azure 和 Amazon 的规模,但您可以选择在本地安装各种云解决方案,从而复制体验。
尽管如此,仍然存在一些差异,你可以谈论不同的心态。在图 13.2中,您可以看到经典本地软件与云本地模型的特征比较:

图 13.2–本地特性与云特性
可能处于两个极端之间的不同阶段,而这些只是一般特征。如果您没有使用公共云服务提供商,则完全可以使用云服务甚至。
让我们在下面的章节中详细阐述这些特性。
单片与微服务架构
软件架构是一个大话题,一旦你从简单的应用转向构建系统,有许多设计时决策可能会让你绊倒。当 compute 的 boundary 在机架中购买新服务器时,默认情况下,您通常会得到一个整体。如果你告诉运营部门,你希望后端和前端使用不同的服务器,他们会笑着拒绝你的请求。只有当产生的负载需要更多的计算机时,将两者分离才可以。
在云端,这应该不再是一个担忧。如果您为不同的任务设计了一个基于容器的解决方案,并使用微服务,那么您需要为总计算量付费。单片服务器不再是边界,您不应该受到它的约束。这并不是说构建的每一个软件都应该分解成微服务,但通常在规划体系结构时,您会希望对其进行研究。
可扩展性规划
知道你需要多少计算能力是很难的。是的,你可以做出有根据的猜测,但仍然存在一些你无法控制的随机性。你是否应该超额供应并购买超过你需要的硬件?如果在负载到达时您没有足够的硬件怎么办?您能以多快的速度将更多服务器放入机架?
亚马逊进入云提供商业务的原因之一是可伸缩性问题。与许多在线零售商一样,亚马逊在圣诞节前的最后一个半年销售的商品比今年剩余时间多,他们需要大量的马力来处理这一问题,因此他们建立了多个数据中心来处理负载。问题是——在 1 月份的大甩卖之后,计算能力闲置了,只不过花了他们的钱。商业机会是,这种过剩的产能肯定可以提供给其他公司,以及数十亿美元的收入,我们可以说这是一个好主意。
如果您有一个云本机应用,那么您可以针对这种规模进行设计。订购并交付服务器不需要几周的时间。您可以将创建虚拟机的时间缩短为几分钟,根据您的工作负载,甚至可能只需几秒钟。
请注意,云也有一些限制,因此,如果你知道黑色星期五即将到来,你不应该计划在前一天晚上分配数百台服务器——如果没有云提供商的指导,你可能无法做到这一点。
有两种缩放机制–放大和缩小。
放大还是缩小
当增加更多的计算单位时,就可以进行放大。除了有两台服务器处理负载外,您还添加了另一台服务器,这意味着您有三台。
向外扩展是向计算单元添加更多资源。增加更多的内存和存储就是一个很好的例子。服务器的数量保持不变。
要决定哪一种模式适合你,就必须弄清楚是什么驱动了你的资源消耗。如果 CPU 的负载介于 20%到 30%之间,但内存在 90+范围内,请添加更多内存(放大)。如果 CPU 达到最大负载,请添加更多服务器实例(向外扩展)。
具体细节取决于您使用的服务,具体操作方式取决于您使用的服务,但大多数可用服务都可以选择设置某种自动缩放机制,以便在需要时添加更多功能。对于完全动态处理,您通常还可以自动缩小比例,并且您还可以在知道负载很少时将事情安排为关闭。
使用不同的数据库类型
当您想到一个本地数据库时,通常它们是 SQL 的一些实现。(这可能是 MS SQL、Oracle、MySQL 或其他数据库)这些是关系数据库,依赖于表的数据库模型以及它们之间的关系。
例如,Person实体的表可以类似于 Visual Studio designer 中的图 13.3:

图 13.3–人员表
相应的 SQL 代码如下:
CREATE TABLE [dbo].[Person]( [Id] INT NOT NULL PRIMARY KEY, [FirstName] VARCHAR(50) NULL, [LastName] VARCHAR(50) NULL, [Address] VARCHAR(50) NULL, [ZipCode] VARCHAR(10) NULL, [State] NCHAR(2) NULL )
在 C#代码中,您更可能使用一种语法,比如 Linq,因为它对开发人员更友好,但在这两种情况下,原则是相同的。
这是一个示例,数据在写入时被采用到模式。当您写入数据库时,SQL 引擎将验证您的数据是否正确–如果您尝试将字符串写入整数字段,则它将无法工作。
通常还有一些锁定机制来确保应用 A 和应用 B 不能同时写入同一属性。考虑到数据库服务器上良好的吞吐量,这一点可能不明显,但对于多用户场景,这一点不容忽视。
这对于许多用例来说都很好,因为它确保了高度的完整性和一致性。对于诸如银行帐户之类的用例,这是您想要的。对于资金如何进出账户来说,草率的机制对每个人都是有害的。
缺点是,它要求写入数据库的代码更加复杂,而且即使使用强大的硬件,它在短时间内接收大量数据的能力也会降低。
作为一种选择,更像云的产品将是文档数据库(如 Azure Cosmos DB 和 MongoDB)。
如果你有数千个物联网传感器捕获数据,你的重点是将数据输入数据库。如果一个温度读数下降了一度,这可能不是一个大问题。您需要吞吐量,因此不必在写入时强制使用模式,只需插入一个包含您喜欢的内容的 JSON 文档。
当您需要提取并呈现数据时,您可能需要为数据类型制定一些规则–这称为模式读取。这为您提供了处理日期时间值的选项,例如仅用于显示目的的字符串表示形式,以及需要将其作为实际日期时间类型处理的选项。
个人的 JSON 实例可以如下所示:
{ "FirstName": "John", "LastName" : "Doe", "Address" : "One Microsoft Way", "ZipCode" : "98052", "State" : "WA"}
在本例中,我们看到我们没有遵守为 SQL 记录定义的模式约束。我们只是将属性视为纯文本。
使用 SDK 将其插入 Cosmos DB 数据库中,看起来如下所示:
ItemResponse<Person> personResponse = await this.container.CreateItemAsync<Person>(johndoe, new PartitionKey(person.LastName));
Console.WriteLine("Created item in database with id: {0} ", personResponse.Resource.Id);
注意,这段代码省略了类定义和到数据库的连接,但它说明了如何提供一个 JSON 文档来创建一个新项。
当您拥有一个全球分布的文档数据库时,这种模式使得始终保持同步变得更加困难,这就是为什么我们经常提到最终一致性。回到银行账户的例子,这可能不是你想要的,但如果有人在欧洲观看统计仪表盘,与美国相比,延迟了几秒钟,这可能不是问题。
存储数据的延迟不仅仅与数据库技术有关;它还涉及获得正确的同步和多重处理。
同步性和多处理任务
并行处理和异步请求不是云所独有的。在硬件方面,多年来有很多抽象,因为真正的并行性很难实现,但作为最终用户,您总是希望体验同时发生的事情,而不依赖于后台发生的其他事情。
云计算就是基于这一点构建的。当您的服务每天需要处理数十亿个请求时,通过一次整洁有序地处理一个请求是不可能的。作为云计算的用户,您可能不必处理大量的请求,但您仍应将其视为默认值。
异步行为在创建 web 应用时非常重要,因为您很可能希望为用户提供快速的体验。当您在等待 API 调用超时时阻塞 UI 数秒时,您将遇到不满意的用户。
幸运的是,.NET 模板在这方面可以帮助您生成异步代码(如果适用)。
例如,您可以在 web 应用的控制器中使用以下同步代码:
[HttpGet]public string HelloWorld(){ return "Hello World";}
如果要将其重写为异步代码,则可能会改为如下所示:
[HttpGet]public async Task<string> HelloWorld(){ return "Hello World";}
重要的是要意识到,您会遇到更难排除的 bug。例如,如果您在实际接收响应之前实例化 HTTP 客户端调用并处理连接,,那么步进调试器找出答案可能会很有趣。
和往常一样,您需要了解您正在尝试做什么,但一般来说,这是云本机的首选方法。
避免失败与预期失败
我们在代码中所做的最困难的事情之一是处理意外情况,这适用于您是在云中运行还是在本地安装的硬件中运行。这两个托管选项的处理方式可能有所不同。
如果您有 10 台企业级服务器,那么除非它们在前几周出现故障,否则它们很可能会工作很长时间。如果其中一个出现故障,请致电硬件供应商,让技术人员到现场维修。
当然,您在代码中内置了冗余,但它可能仅限于假设两台服务器可用,并且如果其中一台服务器出现故障,则需要在它们之间进行手动切换。
如果你有 100000 台服务器,那么仅仅通过概率计算,一年中发生故障的几率就不止一台。云提供商已经将这个问题从开发人员身上抽象出来。他们按容器购买服务器,而运营规模意味着无法保证技术人员能够在足够短的时间内更换出故障的硬件,使其不会产生影响,这就是为什么云计算被设计为保持运行,即使单个部件开始出现故障。
即使硬件没有出现故障,也存在操作系统需要更新的风险。如果发布了重要的安全更新,云提供商不会等到您方便时再应用它——他们会尽快进行。
我们中的许多人都有经验的软件,这些软件假设您始终能够以预期和正确的方式关闭它。可能是它希望在运行时锁定在关机时释放的资源,或者其他假设系统稳定可用的事情。当事情出了问题,你又重新启动它时,你会收到一条信息,说上次关机失败了,所以你会被要求跳过这些障碍,重新启动并运行。
这不是云计算的方式。您应该预计进程可能会以非计划的方式终止,重要的是确保新实例能够尽快启动,而无需手动干预。
请注意,备份是一个单独的考虑事项。您应该始终确保有备份和恢复重要数据的策略,无论系统如何以及何时发生故障。
了解云更新计划
由于扩展选项有限,并且需要规划资源的可用性,内部部署世界通常会实施计划内停机。许多公司仍然有维护窗口,您必须点击这些窗口才能更新软件。这通常涉及到开发人员必须随时执行更新,或者在夜间或周末发生不好的事情时随时待命。
有了好的云本地代码和工具,这应该是过去的事了。云提供了部署到暂存站点的机制,在那里您可以进行基本测试,如果测试通过,还可以单击切换将其转换为生产版本。或者,您可以将两个版本部署到生产环境中,并配置所谓的 A/B 测试,其中只有部分用户暴露于新版本中,以查看他们如何响应。
这一切归结为商业需要。如果你以谷歌、Facebook 或 Netflix 的规模运作,那么永远都不会有一个好时机离线。一年中的任何一天都可以 24 小时访问这些服务。这也不是一个每季度只进行一次大爆炸式更新的选项——如果你已经准备好对网站进行改进,它应该尽快上线。
使用源代码管理工具,我们已经学会了尽早签入和经常签入。Cloud native 还意味着提前发布和经常发布。
服务器和服务的管理
作为开发人员,您不太可能认为管理是为管理员保留的。在某些情况下,这是非常正确的——如果有人在本地或云中为您维护 Windows 服务器,您就不必担心它的管理方式。
不幸的是,在现实生活中,开发人员并不总是能够避免所有的管理任务。为了将风险降至最低,您应该创建需要尽可能少的管理的应用。如果在重新启动时,有一页说明要按照正确的顺序启动和运行服务,那么当云自动扩展并创建 10 个新实例时,您将如何处理?(提示:您最好学习脚本语言。)
宠物对牛
一个经常使用的关于云资源与房屋资源的类比是宠物与牛。有了内部硬件,它是一种物理的、可关联的东西。一个频繁的管理员活动是为服务器制定一个命名方案——可能是希腊神、山、超级英雄或福特汽车制造厂的名称。(所有这些都是在实际的服务器环境中观察到的。)也可能观察到某些特性—服务器的硬盘驱动器/电源/网卡略有不同……换句话说,服务器是宠物。
在云中,您无法命名硬件资源,坦率地说,您可能也不想知道如何使用单个名称命名一百万台服务器。你也不在乎硬盘或记忆棒的品牌。您希望,当您订购 100 GB 的存储和 8 GB 的 RAM 时,每次订购的存储和 8 GB 的 RAM 基本相同。这是把资源当作牛来对待。当你在杂货店买牛奶时,你真的不在乎是 143 号奶牛还是 517 号奶牛生产的。
这种心态只是其中的一部分。你也需要工具。
当你养宠物时,你可以一个接一个地处理事情。例如,如果我们为您提供在 Azure 中创建 web 应用以运行本书中的代码的说明,则说明可能如下所示:
- 登录 Azure 门户。
- 点击创建资源。
- 从列表中选择网络应用。
- 在下拉列表中创建一个新的资源组。
- 选择一个名字和离你最近的地区。
- 选择.NET Core 5作为运行时堆栈,选择Windows作为操作系统。
- 跳过监控和标签,进入审核+创建。
- 如果没有错误,点击创建。
您将看到类似于图 13.4的内容:

图 13.4–Azure 门户中的 Web 应用创建向导示例(.NET 5 在撰写本文时不可用)
当你被告知要创建 20 个 web 应用时,你如何处理这个问题?你如何确保每次都是一致的,并且总是正确的?如果你想要牛,你需要一个可重复的标准化程序。
您可以从手动方式开始,仍然可以生成云原生的应用,但如果您想全力以赴,您可能会希望将基础设施作为代码(IaC进行调查。(在引入基础设施代码(IaC)一节中将有更多关于这方面的内容。)
如前所述,这些都是共同的特征,您可以对您的环境进行自己的触摸,无论是在公共云中还是在您自己的数据中心中。当作为单独的检查表项目处理时,您可以修复,但它或多或少会导致一个更广泛的术语,称为DevOps。
了解 DevOps 的作用
DevOps 的使用通常没有进一步区分它的确切含义,除了它是您为了更加敏捷而需要的东西之外。大多数人都会同意,这是通过使用产品、合适的人员和流程的组合来实现持续价值。
我们不会深入探讨 DevOps 的人员和流程部分,因为这毕竟是一本技术书籍。这里重要的一点是,如果您想提高敏捷性,您需要有反映这一点的流程。例如,您可以准备好工具,每天多次发布新的更新。如果你有一个程序,说每个版本都必须由不同的 QA 和测试团队手动批准,那么这根本不起作用。它非常适合少量和大型更新,但不适合频繁但小型的更新。
在技术方面,您想要的术语是持续集成(CI)和持续部署/交付(CD)。在第 10 章将 ASP.NET 带入云中,我们展示了如何将您的代码从 VisualStudio 导入 Azure 和 AWS。然而,在 VisualStudio 中有一句话经常被使用,朋友们不会让朋友们右键点击发布。第 12 章与 CI/CD的集成,注意到了这一点,并展示了如何通过GitHub 动作实现这一点。
GitHub 多年来一直是开发人员最受欢迎的服务之一,但 GitHub Actions 的加入是在 GitHub 被微软收购后的一个相当新的发展。微软生态系统中的尝试和测试的*解决方案将是 Azure DevOps。这两项服务都在开发和改进中,但在撰写本文时,Azure DevOps 针对企业场景提供了稍微成熟一点的产品,并提供了更广泛的功能集。
Azure DevOps 并非云本机应用的专有。它也可以用于内部部署,甚至还有一个演示,它被用于为 Commodore 64 构建软件(对于那些已经听说过这台计算机的人来说),以说明它决不局限于 Microsoft 语言或框架。
Azure DevOps 有多种功能可用于帮助您在云中构建软件开发生命周期:

图 13.5–Azure DevOps 功能
以下是这些特性的用例:
- Azure Boards用于管理工作项和开发任务的一般流程。
- Azure Repos用于存储您的代码和版本历史记录。
- Azure 管道用于设置构建和发布(CI/CD)。
- Azure 测试计划用于设置代码的测试和 QA。
- Azure 工件用于管理库和模块。这可以用于设置您自己的 NuGet 提要。
在Azure 管道下,您有管道用于设置构建(命名约定最多是混淆)。您拥有所谓的经典向导,它使您能够以用户友好的方式为一系列解决方案设置构建。此向导允许您从模板列表中进行选择,如图 13.6所示:

图 13.6–Azure Pipelines 经典向导
这会让你行动迅速,对探索性工作很有帮助,但从长远来看,这不是推荐的方法。推荐的方法是使用 YAML 文件(基于文本的文件)定义管道。YAML 也是 GitHub 操作的方式,但这两种实现目前并不相同,因此不能来回复制文件的内容。如果您选择 YAML 而不是 classic,您将进入一个文本定义,如图 13.7所示:

图 13.7–Azure 管道 YAML 定义
YAML 是一种标记语言,用于 Kubernetes 配置文件和许多其他服务,因此这也不是特定于 Microsoft 的。一般来说,编写 XML 和 JSON 比编写 XML 和 JSON 更方便用户,但另一方面,它对空格和缩进等内容非常挑剔,因此在掌握格式之前,您还需要了解一些内容。(缩进为两个字符:三个字符将中断。)
使用这种方法,您可以将构建定义视为代码的一部分(您可以将其签入与应用代码相同的存储库)。
此外,在Azure 管道下,您会发现版本,它们与构建紧密相连。这是关于获取管道的输出并部署它。让我们看看 Azure 管道向导。
与“生成向导”类似,您有多个选项可供选择代码的存放位置:

图 13.8–Azure Pipelines 发布经典向导
有更多的选项,我们无法在这些截图中捕捉到,因此,如果您需要其他内容,请务必查看。构建和发布基于容器的应用不同于非容器化的 C#web 应用。Java、Python 和 PHP 也都有各自的特点,无论是如何生成可执行文件还是将其推送到服务器。
发布定义也可以定义为 YAML 文件并签入存储库。
与部署软件时经常涉及的手动步骤相比,这是一个很好的改进。在传统设置中,发布新版本的过程涉及开发人员在本地计算机上构建,然后将结果复制到文件共享,然后登录到从共享复制并部署文件的其他计算机,这并非闻所未闻。试图在这样一个体制下进行完全的 DevOps 是很困难的,但本节中给出的示例表明,不再需要这样做了。代码可以在云中构建、部署和运行,而无需传统方法。
所以,有很多好东西,但也没有免费的云端午餐;一切都有代价。
了解云中的成本
计算机比以往任何时候都能为你的钱带来更多的价值,但计算机总是会带来成本,在商业中,成本通常需要一个理由。许多人错误地认为,默认情况下,云计算中的服务比本地运行的服务更便宜,但情况比你第一眼看到的要复杂,因此我们应该解释一下这幅图的部分内容。
为大型解决方案创建评估并成为一名 Excel 忍者超出了本书的范围,但在云计算中,当有人问资金流向何处时,开发人员往往是第一行。
大多数公司都能负担得起购买服务器的费用,这些服务器可以安装在您的办公室中,其规格可以运行一些 web 应用或几台虚拟机。与云中的虚拟机相比,您可能会认为这只是支付这些服务器的另一种方式。
在云计算中,有两种主要的客户计费机制——固定定价和基于消费的定价:
- 固定定价是指某物每时间单位有成本,可以是小时/天/月;例如,一个 VM 是根据每月的小时数计费的。如果 CPU 被加载到最大值或几乎不做任何事情,成本保持不变。为了省钱,您可以关闭它或缩小它的硬件规模。一个简单的动作,比如在晚上和周末关闭测试环境,可以让你的账单减少 50%。
- 基于消费的定价是指您为使用资源的多少付费。这可以是存储系统,您可以按 GB 付费,也可以是消息传递系统,您可以为发生的事件付费。这些资源可以全天候使用,无需任何额外费用——如果你不在夜间使用,也不需要任何费用。
在构建解决方案时,通常需要将它们结合起来。例如,在 Azure 中,您可以拥有一个按时间计费的 Azure 应用服务,并 24 小时不间断地运行,而您可以拥有一个 Cosmos DB 实例,用于存储数据,并根据吞吐量付费。
本地服务的成本通常也比物理服务器的成本更复杂。你有一些基本的东西,比如电费和网络连接,但还有很多。你需要网络设备。你需要存储。为了实现高可用性和冗余,您需要复制所有内容。您需要配置所述冗余的知识。如果您是一家小型企业,您甚至可能无法构建与大型企业相媲美的基础设施。所以,确保你在比较苹果和苹果,而不是抱怨香蕉看起来不一样。
如果你做得对,你会在云计算中省钱,如果你做得不对,它的成本可能会比在本地更高。
存储成本是一个考虑因素,但存储在云中的工作方式也不同。
云存储与本地磁盘
开发人员计算机上的存储很容易理解。如今,即使是一台廉价笔记本电脑也有一个 SSD,尽管它可能无法与现有的高级选项相比,但对于一个简单的 web 应用来说,它通常已经足够了。你把你的东西储存在C:\foo里,除非 Windows 崩溃或类似的事情,否则你不会有什么大的担心。
将代码移动到生产环境会改变一些事情。您的代码仍然可以保留在虚拟机的C:\foo中,但下面的硬盘可能配置不同。然而,这仍然不是一个问题。
现在存储是便宜的,至少在你考虑到其他东西之前是这样。笔记本电脑中的一个 SSD 可能不会花那么多钱,但是如果你想部署一个在本地运行的 web 应用,你可以使用计算器来增加额外的成本。由于一个硬盘驱动器可能会出现故障,因此您需要将两个硬盘驱动器对折并放置在镜像中。但由于这只处理冗余,您需要另外两个驱动器来处理备份(备份还必须能够容忍驱动器故障)。理想情况下,您需要将它们放在不同的计算机中,中间有高速网络,更不用说大楼可能会烧毁,因此您需要更多的物理位置。这是不断给予的礼物。
有一个老笑话说:你需要多少程序员来更换一个灯泡?没有,这是硬件问题。
关于存储,我们可以说同样的话,这是一件好事。
如果你是硬件专家,你会喜欢云存储,因为你可以将答案更改为无—这是其他人的问题。
云存储的强大之处在于,云提供商已经拥有数千个磁盘、高速网络和多个位置。
我们将不在这里深入讨论细节,但您需要查看可供提供商选择的选项,以便做出正确的选择。在这个范围的低端,您有价格低廉的存档存储,但它只适用于非活动使用的文件(因此命名为存档,不能用于运行的 web 应用)。在价格较高的一端,您可以在世界多个地区自动复制高速 NVMe 驱动器。
让硬件成为硬件,作为一名开发人员,您还需要了解,在您的终端,情况也在发生轻微变化。
短暂储存与持久储存
通常,在云设置中,不能将本地驱动器视为持久驱动器。如果你在基于 Windows 的主机上运行 web 应用,你通常会有一个本地驱动器,因此将临时文件写入c:\foo文件夹是可行的。当主机重新启动时,您可以预期它会消失,如果它确实是临时的,那么这很好;如果您希望它在重新启动后出现,那么这很糟糕。(请记住,您可能无法控制主机何时在云中重新启动。)
如果您在容器中运行应用,则同样适用。每个容器都有一些本地空间来存储应用本身,但是容器可以在任何时间点被杀死,所以你需要相应地处理这个事实。
为了避免这种现象,云服务中的一项基本服务是存储。在 Azure 中,最常用的服务是Azure Blob 存储。
在 Azure Blob 存储中存储和读取文件
如果您跳过了避免覆盖现有文件、检查当前文件夹以及其他一切方面的所有复杂性,您可以通过以下代码片段将字符串输出到文件中,并通过输出到控制台将其读回:
Using System;using System.IO;
namespace Chapter_13_FileStorage { class Program { static void Main(string[] args) { File.WriteAllText("foo.txt", "Hello World"); Console.WriteLine(File.ReadAllText("foo.txt")); } }}
这段代码也将在云中运行,但需要注意的是,它可能随时消失。
如果我们对 Azure Blob 存储执行相同的操作,步骤将略有不同:
-
Use the Azure portal to create a new storage account. To do so, you need to provide the desired configuration for what kind of storage you want, whether to replicate the data geographically, and the location you want it in:
![Figure 13.9 – Azure storage account creation]()
图 13.9–Azure 存储帐户创建
-
有很多设置可以查看,但是在本练习中,只需直接跳到创建。
-
Go to the resource you just created and step into the Storage Explorer option as shown in Figure 13.9:
![Figure 13.10 – Storage account blade in the Azure portal]()
图 13.10–Azure 门户中的存储帐户刀片
-
右键点击Blob 容器并选择创建 Blob 容器。命名为
foo并确保访问级别设置为私有。 -
转到访问键刀片,复制键 1的连接字符串,因为您将需要它作为您的代码。
-
打开命令行窗口,转到解决方案的根目录,然后键入以下命令:
dotnet add package Azure.Storage.Blobs -
修改并添加现有代码,如图所示:
using System;using System.IO;using Azure.Storage.Blobs;using Azure.Storage.Blobs.Models; namespace Chapter_13_FileStorage { class Program { static async System.Threading.Tasks.Task Main(string[] args) { File.WriteAllText("foo.txt", "Hello World"); Console.WriteLine(File.ReadAllText("foo.txt")); //Set up the connection and a blob reference string connString = "copied-from-Azure-Portal"; BlobServiceClient blobServiceClient = new BlobServiceClient(connString); BlobContainerClient blobContainerClient = BlobServiceClient.GetBlobContainerClient("foo"); BlobClient blobClient = BlobContainerClient.GetBlobClient("foo.txt"); //Upload to Blob Storage using FileStream uploadFileStream = File.OpenRead ("foo.txt"); await blobClient.UploadAsync(uploadFileStream, true); uploadFileStream.Close(); //Download from Blob Storage BlobDownloadInfo dl = await blobClient. DownloadAsync(); using (FileStream dlfs = File.OpenWrite( "fooBlob.txt")) { await dl.Content.CopyToAsync(dlfs); dlfs.Close(); } Console.WriteLine(File.ReadAllText("fooBlob.txt")); } }} -
如果一切正常,控制台应该打印两次相同的字符串值
Hello World。
乍一看,这个可能看起来很复杂——一旦你习惯了,它就会变得更容易。从这样一个小例子中看不出这一点,但一旦您开始扩展需要访问文件的组件数量,您就会体会到这一点的好处。
请注意,它确实对性能有影响,因为事情需要经过仔细考虑。
处理存储延迟
无论您是在办公桌上的计算机上运行代码,还是在云中运行代码,在存储器之间传输数据都不是即时的。对于少量的数据,你可能不会注意到,但这里和那里加起来只有一毫秒。
如果您的应用需要缓存层,您应该研究解决方案,例如Azure cache for Redis,它将数据存储在内存中,并减少涉及磁盘的需要。在第 9 章容器中,我们考虑了使用 Redis 的预构建图像,这将是一个很好的解决方案。
我们不会在门户中创建下一个 web 应用或存储帐户,但我们将在下一步查看 IaC 时,研究如何使用牛方法。
引入基础设施代码(IaC)
当提到通过 Azure 门户创建 web 应用时,我们提到了更好的大规模解决方案是研究 IaC,但我们没有进一步解释。那么,IaC 到底是什么意思?
通过 Azure 门户创建 web 应用并不是那么糟糕。你会得到一个向导来引导你通过它,它会在你前进的过程中捕捉到一些错误;如果您试图创建一个对 DNS 无效的 web 应用,它会这样说。
如果您曾经使用过内部软件安装,或者为此创建了供他人安装的软件,那么您可能会遇到不太友好的过程。可能需要严格遵守安装指南,而且由于您没有研究必备条件列表,因此在向导的第三页上,您发现需要取消安装 SQL server,然后才能返回安装。
这两种方法的共同点是,它们容易出现不一致和不正确的部署,并且如果您想创建更多的安装和实例,它根本无法扩展。
这是 IaC 要解决的主要问题。正如我们在构建和发布定义中看到的,您可以将其签入代码,这同样适用于 IaC 定义。
IaC 有两种基本形式——命令式和声明式。
命令式 IaC
使用这种方法,您可以精确地指定您想要的内容以及顺序。这对于自动化来说是很好的,但是你需要自己处理依赖关系。如果您尝试创建一个 web 应用而不先创建资源组,则会失败。命令式 IaC 的示例包括 Azure PowerShell 和 Azure CLI。以创建 web 应用为例,它在 PowerShell 中的外观如下所示:
$location = "North Europe"
# Creating Resource group New-AzResourceGroup -Name rg-webapp -Location $location
# Creating App Service Plan New-AzAppServicePlan -Name webapp -Location $location -ResourceGroupName rg-webapp -Tier Free
# Creating web app New-AzWebApp -Name webapp -Location $location -AppServicePlan webapp -ResourceGroupName rg-webapp
在 Azure CLI 中,它将如下所示:
# Creating Resource group az group create -l northeurope -n rg-webapp
# Creating App Service Plan az appservice plan create -g rg-webapp-n webapp
# Creating web app az webapp create -g rg-webapp -p webapp -n webapp
对于这两种方法中的哪一种是最好的,没有一个明确的答案,您将看到语法有相似之处,但对于这两种方法,您可以看到它是如何遵循菜谱式的方法的。
声明性 IaC
使用声明式 IaC,您将更少地关注如何,而更多地关注什么。您可以定义您想要一个具有给定属性集的 web 应用,指定依赖项,并让资源调配引擎处理其余的内容,而不是一步一步的方法。这意味着在 Azure 为云的情况下,您让工具在创建 web 应用之前确定应用服务计划已经到位。
声明性 IaC 的 Azure 本机版本是 ARM 模板。语法过于冗长,无法包含完整的示例,但它基于 JSON,这将是部署的应用服务部分所需的代码:
"resources": [ { "type": "Microsoft.Web/serverfarms", "apiVersion": "2020-06-01", "name": "[variables('appServicePlanPortalName')]", "location": "[parameters('location')]", "sku": { "name": "[parameters('sku')]" }, "kind": "linux", "properties": { "reserved": true } },
由于 ARM 可能变得非常复杂,因此对它的感觉复杂多样,但它有两个主要方面:
- 由于它是 Azure 本机的,因此在手动创建资源时,门户中通常会提供一个示例,因此可以将该向导用作生成自定义代码的帮助器。
- 与 Azure 集成后,它会自动跟踪资源的状态。例如,如果在以前的模板上部署模板构建,引擎将知道资源组已经存在,并且不会再次尝试创建它。
另一个支持 Azure 和 Amazon(以及其他一些提供商)的流行工具是 HashiCorp 的Terraform。
再一次,在创建 web 应用时,一个基本示例如下所示:
provider "azurerm" { version = "~>2.0" features {}}
resource "azurerm_resource_group" "rg" { name = "rg-webapp" location = "northeurope"}
resource "azurerm_app_service_plan" "appserviceplan" { name = "webapp" location = azurerm_resource_group.rg.location resource_group_name = azurerm_resource_group.rg.name
sku { tier = "free" }}
resource "azurerm_app_service" "webapp" { name = "webapp" location = azurerm_resource_group.rg.location resource_group_name = azurerm_resource_group.rg.name app_service_plan_id = azurerm_app_service_plan.appserviceplan.id }
如果您不熟悉 ARM 模板,从表面上看,这看起来更方便用户使用,但它仍然是一种需要学习的新格式。它还有一个缺点,就是必须通过将状态存储在单独的文件中来手动跟踪状态处理。它仍然是一个非常有用的工具,微软也为 Azure 文档的部分内容提供了地形示例。
更进一步,还有代码为的IaC(请注意,这不是官方术语)。一个名为Pulumi的工具在 Terraform 上提供了一个编码层,使您能够编写 C#代码,用常规编程中熟悉的一切创建基础设施。
这个主题很大,对于一个专注于构建应用而不是周边基础设施的程序员来说,学习它的所有细微差别可能太多了。在较小的组织和单人团队中,您可能也会被指派负责云的这一部分,因此,如果您是那个人,深入研究这一点可能会很有价值。
在本章的最后,我们将快速了解监控和健康。
了解监测与健康
关于云计算的工作原理,有一个误解是云计算提供商处理应用的运行状况。在本章的第一部分中,我们看到了从 IaaS 到 SaaS 的责任划分,在您向右移动时,提供商承担了更大的责任。如果您一直使用 SaaS,供应商确实必须处理几乎所有不是用户错误的事情,但正如前面所述,开发人员的最佳选择通常是 PaaS,您仍然需要承担一些责任。
这意味着,如果用户体验到的 web 应用的响应时间是不可接受的,那么您需要意识到这一点并找出如何处理它。如果云中的存储出现故障,您需要了解如何补救。这里的您部分可能会被以不同的方式处理,这取决于您的组织,但在大多数情况下,不是云提供商负责,即使他们有机制来帮助您。
Azure 中的 Web 应用有一些内置工具供您使用,如图 13.11所示:

图 13.11–Azure web app 监控刀片
名称中带有log的是跟踪错误条件的不同方法,对于调试非常有用。日志流将让您实时查看日志,因此如果您将错误输出到应用中的控制台,并且能够在用户界面中复制问题,这是非常有用的。
指标可用于规划和实时决策。您可以看到请求的数量、响应时间、抛出多少基于 HTTP 的错误,等等:

图 13.12–Azure web 应用的 Azure 指标
警报还有多种用途。例如,如果错误计数太高,它可以发送一条文本消息和/或电子邮件,有人需要查看日志。根据条件列表,还可以向其他 Azure 服务发送详细信息以调用操作。
与监控相关,但不在同一菜单区,您可以找到放大和缩小(在设置下)。我们在前一节中解释了两者之间的区别,这是本机支持的。您可以配置自动缩放–这意味着,当某个指标在某个时间范围内高于指定阈值时(以避免短峰值触发),Azure 将自动向您的 web 应用添加更多资源。
为了跟踪应用的运行状况,如果您在代码中使用更容易在云中正确设置内容的机制,这会有所帮助。我们在第 10 章中展示了如何将 ASP.NET 带入云中,向您的应用添加健康端点。应将此端点添加到 Azure(或 AWS)中设置的监视和相应的警报机制中。公平地说,也应该在本地考虑添加这样的端点,但用于监视的机制可能会有所不同。
记住我们前面所说的——在构建一些东西的时候,我们期望它们会失败,并构建一个健康和监控策略来帮助您处理这个问题。
总结
构建云本地应用不仅仅是简单地将比特从一个数据中心迁移到另一个数据中心。
我们在这里并没有深入探讨所有内容,但我们涵盖了广泛的主题,首先了解了 cloud native 是什么,以及内部部署与云的一般特征。我们介绍了数据库和存储选项之间的技术差异,这是一篇简短的 DevOps 简介,并且,在开发人员角色之外,我们简要地深入研究了诸如 IaC 之类的主题,最后给出了一些关于监控和运行状况的指点。
现在您应该了解为什么要使用云应用模型,以及在使用云之前需要考虑哪些东西。您还了解了与内部部署相比,在云思维方式方面的差异,并了解了 DevOps 工具和服务。此外,您已经了解了为什么 IaC 可以使您在云中构建所需服务的工作变得更轻松。
即使你还没有完全投入到云计算中,这里也应该有一些东西可以应用到你的老式软件中。
问题
- 云计算的三种基本模型是什么?
- 写模式和读模式有什么区别?
- 你为什么要调查 IaC?
进一步阅读
- 微软提供的 AZ-900 学习路径,可在上获得 https://docs.microsoft.com/en-us/learn/paths/az-900-describe-cloud-concepts/
- Microsoft Azure 架构良好的框架简介,由 Microsoft 提供,网址为https://docs.microsoft.com/nb-no/learn/modules/azure-well-architected-introduction/*
十四、答案
本节包含所有章节中问题的答案。
第一章——ASP.NET Core 5 简介
- .NETClassic 与 Windows 操作系统紧密结合。这阻止了任何跨平台的野心,而且对于云使用和微服务来说也不太理想。NET Core 消除了其中一些障碍;它提供了更干净的 API 表面和更精简的封装外形。
- 每年 11 月发布。每两年发布一次,都是长期支持。
- 基于 MVC 模式的 Web 应用主要由三个组件组成:M(如模型中所示)是应用的数据结构;V(如视图中所示)为用户界面;C(如控制器中所示)表示位于模型和视图之间的组件,并在它们之间洗牌数据。
- 这些属性仅用于在创建对象时设置,以后不能更改。
- 是的,从技术上讲这是可能的,但这很困难,而且非常令人沮丧。考虑执行 RESTORY API 或 GRPC 代替。
第二章跨平台设置
- Windows、Linux、MacOS、iOS 和 Android。
- 这是 Windows 的一个组件,允许您在 Windows 中运行 Linux,但它是以本机方式运行的,而不是作为仿真层。
- 独立的.NET 应用包含运行所需的一切,因此不需要单独安装.NET framework。这意味着它也可以在未安装.NET 的系统上运行,或者在安装了不同版本的 framework 的系统上运行。
- 编译跨平台应用会使应用在不同的平台上运行,但不能确保所有代码都适用于编译应用的平台。这意味着,作为一名开发人员,您必须使代码本身与平台兼容,而不仅仅是与可执行文件兼容。
第三章依赖注入
-
依赖注入(DI)有四种类型:构造函数、方法、属性和视图注入。构造函数注入是构建 ASP.NET Core 应用最常用的方法。
-
There are three types of DI lifetimes: transient, scoped, and singleton.
当您不确定应该如何注册服务时,请使用暂时生存期。这是使用最安全的选项,而且可能是最常用的,因为每次请求服务时都会创建服务。此生存期最适用于轻量级和无状态服务,因为它们在请求结束时被释放。但是,请注意,短暂的生命周期可能会影响应用的性能,特别是当您正在处理一个庞大的整体式应用时,其中依赖项引用是巨大而复杂的。
如果希望每个客户端 web 请求创建一次对象,请使用作用域生存期。这是为了确保每个请求的相关调用(处理相关操作)都包含在相同的对象实例中。(使用作用域生存期的)一个很好的例子是注册数据库服务或上下文,例如 EntityFrameworkCore。
对实例化成本很高的服务使用单例生存期,因为对象将存储在内存中(并且可以在应用中的所有注入中重用)。注册为单例的服务将只创建一次,并且在应用的整个生命周期内,所有依赖项将共享同一对象的同一实例。使用单例的一个好例子是注册记录器或应用配置。
-
Add()方法是在 DI 容器中注册服务最常用的方法。Add()方法为服务创建注册,并且可能会创建重复注册,这可能会影响应用的行为。TryAdd()方法仅在没有为给定服务类型定义实现时注册服务。这可以防止您意外地替换以前注册的服务。所以,如果你想安全登记你的服务,那么考虑使用 AUTYT3ED 方法。
第五章——Blazor 入门
- 您可以使用 Blazor 服务器或 Blazor WebAssembly 创建 web 应用。Blazor 还支持构建本机和混合移动应用,称为 Blazor 移动绑定。
- Blazor 的最大卖点是不必学习硬核 JavaScript 来构建 SPA web 应用。学习框架本身很容易,只要你知道基本的 HTML 和 CSS。它旨在帮助 C#开发者利用他们的技能,在构建基于 SPA 的 web 应用时轻松过渡到 web 模式。
第 8 章-在 ASP.NET 中使用身份
- 认证是关于你是谁,授权是关于你能做什么。
- 对于这些用例中的大多数,推荐的流程是授权代码流(使用 PKCE)。
- Azure AD B2C 使与外部身份提供商的集成变得更容易,这既因为它将实现从代码中抽象出来,也因为它允许对注册和登录体验进行细粒度控制。
第 9 章-Docker 入门
- 与虚拟机相比,容器的存储空间更小,启动速度更快。这是因为容器的抽象在操作系统级别,而虚拟机的抽象在硬件级别。
- 尽管 Redis 可以支持持久卷,但它并不打算取代 RDBMS。
- 对您可以查看图像和容器,也可以轻松查看日志、端口和其他设置。
- 希望你喜欢这一章,就像我们喜欢写它一样。
第 10 章-部署到 AWS 和 Azure
- 虚拟网络(VNET)构成了一个基础设施即服务产品,允许您定义路由。这使得(设备和网络之间的)连接可以被授予或拒绝。例如,VNET 可能有一个规则,只允许特定的 IP 地址或端口接收来自 internet 的请求。
- 定义健康端点是一种常见做法,大多数本地和云负载平衡器都支持这种做法。AWS Elastic Beanstalk 和 Azure 应用服务都支持运行状况端点监视。
- AWS 和 Azure 都在 VisualStudio 中提供了优秀的工具。我们认为展示 ASP.NET Core 和 Visual Studio 如何在 Azure 以外的平台上得到广泛支持是很重要的。
- 我们故意忽略了关于哪个云提供商更好的判断。这两个云提供商都为托管 ASP.NET Core 应用提供了强大的支持,范围从小型组织到大型企业。
第 11 章-浏览器和 Visual Studio 调试
- PWA 是从服务器交付的,但它们仅在浏览器中运行。
- 会话和本地存储仅对正在运行的浏览器可见。在大多数情况下,最好的选择是建立一个数据库,以便向大量用户共享信息。
- 不,所有主要的浏览器都支持开发者工具。
- 是的,VisualStudio 可以调试在同一个项目中运行的 JavaScript 和 C。
第 12 章——与 CI/CD 集成
- 否,免费计划中提供 GitHub 操作。但是,您可以存储的数量和运行工作流的次数是有限制的。
- GitHub 可用于存储源代码、文档或任何文件集合。
- GitHub 操作提供了几种类型的触发器,允许将 CI/CD 进程拆分为多个文件。
- 在部署到 Azure 和 AWS 等云提供商时,CI/CD 非常有意义。在许多方面,云是 CI/CD 的理想选择,我们将在下一章中详细介绍这一点。
第 13 章——开发云原生应用
- IaaS——基础设施即服务,PaaS——平台即服务,SaaS——软件即服务。
- 写模式是经典的 SQL 模型,在这里输入新数据时需要遵守规则。写模式是输入动态数据和在使用数据时定义结构的更灵活的方法。
- “基础架构即代码”可帮助您以可重复且一致的方式大规模自动化资源的创建。
第一部分:爬行
在本节中,您将学习.NET Core 5 的基础知识,包括概述、目标/价值观、新功能及其历史。我们还将帮助您更新 C#技能,我们将介绍如何设置跨平台环境,以及如何使用 CSHTML、MVC、Razor 页面和 Blazor(通过使用统一的标记引擎 Razor)构建应用和页面。最后,我们将解释依赖注入软件设计模式。
本节包括以下章节:
第二部分:步行
既然你能爬了,让我们学走路吧!在演示 Blazor web 框架之后,我们将在本节中探讨如何创建 web API 项目、访问数据、身份验证和授权您的解决方案,以及如何利用容器。
本节包括以下章节:
第三部分:跑步
祝贺你可以走路。现在让我们学习如何跑步!在本节中,我们将探讨构建云本机应用意味着什么,我们还将介绍联邦身份、调试、单元测试以及与 CI/CD 管道的集成。
本节包括以下章节:







































































浙公网安备 33010602011771号