ASP-NET-Core3-学习手册-全-
ASP.NET Core3 学习手册(全)
零、前言
每天,软件开发人员、应用架构师和 IT 项目经理都在尽可能快地构建应用,以成为各自市场的领导者:上市时间(TTM)至关重要。不幸的是,这些应用的质量和性能往往不如预期,因为它们没有经过充分的测试、优化和保护。
在过去几年中,ASP.NET 已发展成为市场上用于 web 应用开发的最一致、最稳定、功能最丰富的框架之一。它提供了所有您可以想到的有关性能、稳定性和安全性的现成特性。
一段时间以来,IT 市场一直在变化。现在需要遵守不同的标准,客户期望实现工业化、高性能和可扩展的应用,而开发人员则要求使用能够提高生产率和可扩展性的框架来适应特定的业务需求。因此,这导致微软彻底反思其网络技术。
因此,Microsoft 构建了 ASP.NET Core,使开发人员能够执行以下操作:
- 创建应用并在特定环境中编译它们,然后在任何环境(如 Linux、Windows 或 macOS)中运行它们。
- 使用具有附加功能的第三方库。
- 使用各种工具、框架和库。
- 为前端开发采用最新的最佳实践。
- 开发灵活、响应迅速的 web 应用。
ASP.NET Core 3 与 Microsoft Visual Studio 2019 一起提供了一些功能,使您作为 web 开发人员的生活更轻松、更高效。例如,VisualStudio 提供了可用于开发 web 应用的项目模板。Visual Studio 还支持多种开发模式,包括使用 MicrosoftInternet Information Services(IIS)在开发期间直接测试 web 应用,以及使用内置 web 服务器通过 FTP 开发 web 应用。
使用 VisualStudio 中的调试器,您可以运行应用并逐步查找代码的关键区域以发现问题。使用 VisualStudio 编辑器,您可以有效地开发自己的自定义用户界面。
当您准备部署应用时,VisualStudio 可以轻松创建部署包,以便在 Azure、Amazon Web Services 和 Docker 或任何其他平台(包括 Linux 和 macOS)上部署。这些只是 ASP.NET Core 框架与 Visual Studio 结合时内置的一些功能。
这本书提供了最新的最佳实践和 ASP.NET Core 指导,让您快速了解最新情况。本书的每一部分都以易于阅读的格式提供了特定的 ASP.NET Core 3 功能,并附有详细的示例。循序渐进的说明会立即产生工作结果。ASP.NET Core 的大部分关键功能都使用简洁、易于理解和可重用的示例进行了说明。这些例子是深入的,以便在不傲慢的情况下说明特征。
除了通过示例展示 ASP.NET Core 功能外,本书还包含每个功能的实际应用,以便您可以在现实世界中应用这些技术。阅读本书并应用这些练习后,您将在构建高效的 web 应用方面有一个良好的开端,这些应用包括现代功能,如 MVC 体系结构、web API、自定义视图组件和标记帮助器。
我们希望这本书能帮助你作为一名开发人员的日常工作,阅读这本书会给你带来和写作一样的快乐。
曾几何时——NGWS 和.NET 框架
下面是一段历史来解释.NETFramework 是如何在过去几年中发展的,以及为什么您今天必须考虑.NET Core 框架:

微软在 20 世纪 90 年代末开始开发我们现在所知的.NET Framework,并在 2001 年底发布了.NET Framework 1.0 的第一个测试版。
最初,该框架被命名为下一代 Windows 服务(内部代码为 Lightning/Project 42)的NGWS。起初,开发人员只能使用 VB.NET 作为编程语言。在 10 多个框架版本之后,已经取得了很多成果。今天,您可以在大量语言、框架和技术之间进行选择。
起初,InterDev 是开发 ASP 页面的主要开发环境,您必须使用命令行 VBC 编译器工具来编译代码。
我们钟爱的 Visual Studio 开发环境的第一个版本于 2002 年 2 月发布,为 Windows 客户端和 Windows 服务器系列(NT 4、Windows 98、Windows ME、Windows XP,然后是 Windows 2000)提供了一个通用的运行时环境。
大约在同一时间,微软提供了一个名为 Compact framework 的轻量级框架,用于在 Windows Mobile 上执行 Windows CE。最后一个版本在 2008 年 1 月发布为 3.5 版 RTM,之后被更新的移动技术取代。
第一个.NET SDK 于 2003 年 4 月作为.NET Framework 1.1 发布,并包含在 Visual Studio 2003 中。它是 Windows Server 操作系统中包含的第一个版本,与 Windows 2003 一起提供。
.NET Framework 2.0 于 2006 年 1 月在 Windows 98 和 Windows Me 时代发布。它对公共语言运行库(CLR进行了重大升级。它是第一个完全支持 64 位计算并与 Microsoft SQL Server 完全集成的版本。它还引入了一个新的 Web 页面框架,提供了皮肤、模板、母版页和样式表等功能。
.NET Framework 3(WinFX)于 2006 年 11 月发布。它包括一组新的托管代码 API。这个版本增加了一些新的技术来构建新的应用,如 Po.T0. Windows 演示基金会 To1 T1(Po.T2,WPF PosiT3),AutoT4. Windows 通信基金会 Ty5 T5(AutoT66WCF OutT7T),AutoT8. Windows 工作流基金会 Ty9 T9(Windows CardSpace,TW10,WWF),T11A.(后来集成到 Windows Identity Foundation 中)。
一年后的 2007 年,.NET Framework 3.5 扩展了 WinFX 功能。此版本包括关键功能,如 LINQ、ADO.NET、ADO.NET 实体框架和 ADO.NET 数据服务。此外,它还附带了两个新的程序集,这些程序集后来成为 MVC 框架的基础:
.NET Framework 4.0 于 2009 年 5 月发布;它对 CLR 进行了一些重大升级,并添加了一个并行扩展,以改进对并行计算、动态调度、命名参数和可选参数以及代码契约和BigIntegerComplex数字格式的支持。
在.NET Framework 4.0 发布后,Microsoft 发布了一系列改进,以 Windows Server AppFabric 框架的形式构建微服务。本质上,它提供了内存中的分布式缓存和应用服务器场。
.NET Framework 4.5 于 2012 年 8 月发布;它添加了所谓的 Metro 风格的应用(后来演变为通用 Windows 平台应用)、核心功能和Microsoft 扩展框架(MEF)。
关于 ASP.NET,该版本更兼容 HTML5 和 jQuery,并提供了捆绑和缩小功能以提高网页性能。它也是第一个支持 WebSocket 和异步 HTTP 请求和响应的。
.NET Framework 4.6.1 于 2015 年 11 月发布;它需要 Windows7SP1 或更高版本,是一个重要的版本。包括的一些新功能和 API 支持 AlwaysOn 的 SQL 连接、始终加密,以及在使用 Azure SQL 数据库时改进的连接弹性。它还使用更新的System.TransactionsAPI 为分布式事务添加了 Azure SQL 数据库支持,并在 RyuJIT、GC 和 WPF 中提供了许多其他与性能、稳定性和可靠性相关的修复。
.NET Framework 4.6.2 于 2016 年 3 月发布;它增加了对超过 260 个字符的路径、X.509 证书中的 FIPS 186-3 DSA 以及数据注释本地化的支持,并且资源文件被移动到了App_LocalResources文件夹中。此外,ASP.NET 会话提供程序和本地缓存管理器与异步框架兼容。
.NET Framework 4.7 于 2017 年 4 月发布;它包含在 Windows 10 Creators 更新中。一些新功能包括增强的椭圆曲线加密和改进的传输层安全性(TLS支持,尤其是 1.2 版。它还引入了对象缓存存储,通过实现ICacheStoreProvider接口,开发人员可以轻松地提供定制的提供者。
应用和内存监视器以及著名的内存限制反应之间也有更好的集成,这使开发人员能够在 CLR 截断内存中缓存的对象并重写默认行为时观察 CLR。
然后,微软开发了一个全新的.NET 框架,从一开始就考虑了开源多平台。它被命名为 ASP.NET 5,后来更名为 ASP.NET Core 框架。
Richard Lander(MSFT)于 2016 年 6 月发布了第一个版本 1.0;ASP.NET MVC 和 web API 框架被合并到一个框架包中,您可以通过 NuGet 轻松地将其添加到项目中。
第二个版本,.NET Core 框架 1.1 于 2017 年 11 月发布;它运行在更多的 Linux 发行版上,性能得到了提高,与 Kestrel 一起发布,在 Azure 上的部署得到了简化,生产效率也得到了提高。实体框架核心开始支持 SQL Server 2016。
在撰写本书时,.NET Core 框架的最新版本是 3,于 2019 年 9 月发布。2018 年末发布了第一个预览版,自年初(2019 年)以来,随后发布了多个预览版。
微软已经大大改进了.NET Core 框架。这些改进和扩展是.NET Core 3 愿景的结果;它使您能够在更多的地方使用更多的代码。
值得注意的是,GitHub 上提供了大多数常规库。任何想要扩展或改变任何标准行为的人都可以分叉和重建它们。
这本书是给谁的
本书面向希望使用 ASP.NETCore3 构建现代 web 应用的开发人员。不需要具备 ASP.NET 或.NET Core 的先验知识。然而,基本的编程知识是假定的。此外,以前的 VisualStudio 经验会有所帮助,但不是必需的,因为详细的说明将指导您阅读本书的示例。本书还可以帮助从事基础设施工程和操作的人员在 ASP.NET Core 3 web 应用运行期间监控和诊断问题。
这本书涵盖的内容
本书分为多个章节,以简单易懂的格式,通过实际示例解释 ASP.NET Core 3 的功能。ASP.NET Core 3 的大多数关键功能都使用简洁、高效的示例和分步说明进行了说明,以产生即时的工作结果。
你不必为了发现这本书有用而阅读章节。除第一章外,每章都有自己的内容。第一章详细介绍了 ASP.NET Core 的基本原理。如果您从未尝试过桌面应用开发,您可能希望先阅读这一章。
以下主题将贯穿全书。
第一章、什么是 ASP.NET Core 3?介绍了 ASP.NET Core 3 的特性和功能,以及技术限制,让您了解在哪些情况下它可以很好地满足您自己的需求,以及您的期望。
第 2 章设置环境详细说明了如何设置您的开发环境以及如何创建您的第一个 ASP.NET Core 3 应用。您将学习如何使用 Visual Studio 2019 或 Visual Studio 代码,如何安装运行时,以及如何使用 NuGet 检索所有必要的 ASP.NET Core 3 依赖项。
第 3 章Azure DevOps中的持续集成管道,演示了如何建立完整的 Azure DevOps 持续集成管道。您将学习如何在云中使用 Azure DevOps 完全自动化应用的构建、测试和部署。
第 4 章、通过自定义应用实现 ASP.NET Core 3 的基本概念:第 1 部分解释了 ASP.NET Core 3 应用的基本结构和概念。它展示了一切在内部是如何工作的,以及可以使用哪些类和方法来覆盖基本行为。它还为所有其他章节提供了理论背景。
第 5 章、通过自定义应用实现 ASP.NET Core 3 的基本概念:第 2 部分,继第 4 章中所述的概念之后,通过自定义应用实现 ASP.NET Core 3 的基本概念:第 1 部分深入探讨了 ASP.NET Core 3 的基本概念。您将了解 ASP.NET Core 为构建响应性 web 应用提供的组件和功能。
第 6 章介绍了 Razor 组件和 SignalR,介绍了 Blazor,这是微软推出的一款新产品,旨在满足使用 C#作为语言进行前端开发的需要。它为您准备了使用服务器端 Blazor 所需的基本知识。
第 7 章创建 ASP.NET Core MVC 应用提供了创建第一个 ASP.NET Core 3 MVC 应用所需的所有概念和一切。您将学习 MVC 应用的细节以及如何高效地实现它们。此外,您将看到单元测试和集成测试将如何帮助您以更少的 bug 构建更好的应用,从而降低维护成本。
第 8 章创建 Web API 应用涵盖了 Web API 框架,并提供了创建第一个 ASP.NET Core 3 Web API 所需的一切。您将看到不同的 web API 样式,如 RPC、REST 和 HATEOAS,并了解何时使用它们以及如何以有效的方式实现它们。
第 9 章使用实体框架核心 3访问数据,展示了如何使用实体框架核心 3 访问数据库,同时使用其提供的所有高级功能(代码优先、流畅 API、数据迁移、内存数据库等)。
第 10 章保护 ASP.NET Core 3 应用说明了如何使用内置的 ASP.NET Core 3 功能进行用户身份验证,以及如何通过添加外部提供者来扩展这些功能。如果您需要保护您的应用,那么本章就是您想要去的地方。
第 11 章、保护 ASP.NET 应用-漏洞为我们提供了构建应用时需要注意的事项,包括可利用的领域,因此需要更多关注。
第 12 章托管 ASP.NET Core 3 应用是关于在本地和云中托管和部署 ASP.NET Core 3 web 应用的各种选项。您将学习如何为给定的用例选择合适的解决方案,这将允许您为自己的应用做出更好的决策。
第 13 章管理 ASP.NET Core 3 应用最后一章将介绍如何在部署后管理和监督生产就绪的应用。它将极大地帮助您在运行时诊断 ASP.NET Core 3 web 应用的问题,并减少理解和修复错误的时间。
充分利用这本书
您需要 Visual Studio 2019 Community Edition 或 Visual Studio 代码(出于测试和学习目的都是免费的),以便能够遵循本书中的代码示例。您也可以使用您选择的任何其他文本编辑器,然后使用dotnet命令行工具,但建议使用前面提到的开发环境之一,以提高生产率。
在本书的后面,我们将使用数据库,因此您还需要一个版本的 SQL Server(任何版本中的任何版本都可以使用)。我们建议使用 SQL Server 2019 Express Edition,出于测试目的,该版本也是免费的。
在接下来的章节中可能会介绍其他工具或框架。我们将解释如何在使用它们时检索它们。
如果您需要为 Linux 开发,那么 Visual Studio 代码和 SQL Server 2016 或 2019 是您的主要选择,因为它们是唯一在 Linux 上运行的。
此外,对于书中所示的一些示例,您将需要 Azure 订阅和 Amazon Web 服务订阅。有多个章节专门介绍如何利用云。
下载示例代码文件
您可以从您的账户www.packt.com下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packt.com/support并注册,将文件通过电子邮件直接发送给您。
您可以通过以下步骤下载代码文件:
- 登录或注册www.packt.com。
- 选择“支持”选项卡。
- 点击代码下载。
- 在搜索框中输入图书名称,然后按照屏幕上的说明进行操作。
下载文件后,请确保使用以下最新版本解压或解压缩文件夹:
- WinRAR/7-Zip for Windows
- 适用于 macOS 的 Zipeg/iZip/UnRarX
- 适用于 Linux 的 7-Zip/PeaZip
该书的代码包也托管在 GitHub 上的以下存储库中:https://github.com/PacktPublishing/Learn-ASP.NET-Core-3-Second-Edition 。
我们的丰富书籍和视频目录中还有其他代码包,请访问https://github.com/PacktPublishing/ 。看看他们!
下载彩色图像
我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在这里下载:https://static.packt-cdn.com/downloads/9781789610130_ColorImages.pdf 。
行动中的代码
请访问以下链接查看行动视频中的代码:http://bit.ly/39ecHAf 。
使用的惯例
本书中使用了许多文本约定。。
CodeInText:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。“启动 Visual Studio 2019,打开您创建的 Tic Tac Toe ASP.NET Core 3 项目,创建三个名为Controllers、Services和Views的新文件夹,然后在Views文件夹中创建一个名为Shared的子文件夹。”
代码块设置如下:
[HttpGet]
public IActionResult EmailConfirmation (string email)
{
ViewBag.Email = email;
return View();
}
当我们希望提请您注意代码块的特定部分时,相关行或项目以粗体显示:
public class Student
{
public long Id { get; set; }
public string Name { get; set; }
public StudentDetails StudentDetails { get; set; }
public ICollection<StudentSubject> StudentSubjects { get; set; }
// Added after constructed table
}
任何命令行输入或输出的编写方式如下:
sudo apt-get install code
粗体:表示一个新术语、一个重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词出现在文本中,如下所示。下面是一个示例:“打开 VisualStudio2019,转到团队资源管理器选项卡,然后单击分支按钮。”
Warnings or important notes appear like this. Tips and tricks appear like this.
联系
我们欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中注明书名,并发送电子邮件至customercare@packtpub.com。
勘误表:尽管我们已尽一切努力确保内容的准确性,但还是会出现错误。如果您在本书中发现错误,如果您能向我们报告,我们将不胜感激。请访问www.packt.com/submit-errata,选择您的书籍,点击 errata 提交表单链接,并输入详细信息。
盗版:如果您在互联网上发现我们作品的任何形式的非法复制品,请您提供我们的位置地址或网站名称,我们将不胜感激。请通过copyright@packt.com与我们联系,并提供该材料的链接。
如果您有兴趣成为一名作家:如果您对某个主题有专业知识,并且您有兴趣撰写或贡献一本书,请访问authors.packtpub.com。
评论
请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?然后,潜在读者可以看到并使用您的无偏见意见做出购买决定,我们 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们书籍的反馈。非常感谢。
有关 Packt 的更多信息,请访问Packt.com。
一、什么是 ASP.NET Core 3?
世界上最早的 web 服务器形式是在 1990 年左右出现的。它被称为CERN httpd,由 Tim Berners Lee 开发,这个名字与万维网(WWW的起源相当接近。
最初,web 服务器的基本形式是用来处理只希望将文件内容作为响应的请求,但随着时间的推移,人们的期望也有所增加,最初对静态文件的需求有所改变,而今天的动态 web 应用要求更高。
如今,关于 web 服务器与 web 应用的职责,存在着模糊的界限,随着我们对 ASP.NET Core 3 作为 web 应用框架的更多了解,这些界限变得更加清晰。值得注意的是,这个框架和其他并非源于 Microsoft 的框架(如 RubyonRails)充当开发人员和 web 服务器之间的缓冲区。
与以前的其他框架相比,ASP.NET Core 3 具有许多优点,当我们进一步了解其功能以及此版本的新增功能时,将在后续章节中详细阐述这些优点。
在本章中,我们将介绍以下主题,其中明显偏向于 ASP.NET Core 3:
- ASP.NET 的历史
- ASP.NET Core 3 功能
- ASP.NET Core 3 的新增功能是什么?
- 跨平台支撑
- 微服务体系结构
- 性能和可扩展性
- 技术限制
- 何时选择 ASP.NET Core 3
ASP.NET 的历史
这一切都始于 90 年代中期的活动服务器页面,当时微软试图跟上通过网络提供动态内容的热潮,这显然影响了活动服务器页面的名称,即今天的 ASP。
与任何有价值的技术一样,ASP.NET 也随着时间的推移而不断发展,其中一个主要的变化是在 2002 年前后引入了ASP.NET Web 表单,这受到了微软另一个应用框架(称为Windows 表单)的成功的严重影响,或更常见的称为WinForms。
随着在 WinForms 中轻松创建 HTML 表单和控件,随之而来的是大量不必要的 HTML 和 JavaScript,它们降低了页面加载速度,还有其他因素,如视图状态和页面生命周期,它们进一步降低了业务应用的速度。这导致了一系列 ASP.NET MVC 版本的引入,这些版本试图解决 ASP.NET Web 表单存在的一些问题。
ASP.NET MVC 也有助于迎合良好编程实践的一个主要原则,即优先选择关注点分离(SoC),而不是 ASP.NET Web 表单及其文件后面的代码中明显存在的紧密耦合。这本身就带来了连锁反应的好处,允许测试驱动的开发,并总体上提高了可测试性。
2016 年,随着第一个版本 1.0 中的ASP.NET Core的发布,另一个重大转变发生了。在撰写本书(2019 年)时,该版本一直发展到第 3 版。在这一转变中,微软几乎完全重写了 ASP.NET,主要是去除了对System.Web命名空间的依赖,这就需要依赖互联网信息服务(IIS)。由于 IIS 仅与 Windows 操作系统兼容,独立于它允许 ASP.NET Core 真正跨平台。
必须提到的是,2014 年左右,随着业务动态的变化,微软明显地接受了开源,尽管 ASP.NET Core 的最大卖点之一是它是开源的,但我们需要注意的是,即使是以前版本的 ASP.NET,包括 MVC 和 web API,最终也以开源的形式发布,任何人都可以为其持续发展做出贡献:

在版本 3 之前,ASP.NET Core 应用运行在.NET Core framework 和完整的.NET framework 上,但 Microsoft 决定,从版本 3 开始,ASP.NET Core 将只运行在.NET Core 上,以便更好地利用新的开发,而不受旧功能的束缚。
在下图中,您可以看到不同的.NET Framework 版本和组件是如何协同工作的:

这本书是关于 ASP.NET Core 的,更具体地说,是关于它的最新版本 3(在撰写本文时)。因此,前面简要提到的先前版本足以为我们提供上下文,但从现在起,我们将更多地关注 ASP.NET Core。
本书的重点仍然是 ASP.NETCore3,它不同于.NETCore3;前者是应用框架,后者是运行时。传统上,ASP.NET Core 应用可以在.NET Core 以及其他.NET Framework 版本上运行,这突出表明了它们是不同的。
很容易理解为什么有些人会混淆两者,因为 ASP.NET Core 应用也可以是.NET Core 应用,就像它可以是.NET Framework 4.8 应用一样。
在决定使用什么框架开发新的应用时,需要注意的是,Microsoft 计划在 ASP.NET Core 的未来版本中只在.NET Core 上运行,而不在其他.NET framework 版本上运行。
在简要介绍了 ASP.NET Core 3 的历史之后,让我们在下一节中了解一下定义应用框架的功能。
ASP.NET Core 3 功能
ASP.NET Core 2.0 中的Microsoft.AspNet.Core.All包包含单个库中的所有功能。它包括身份验证、模型视图控制器(MVC)、Razor、监控、红隼支持等等。
自 ASP.NET Core 版本 2.1 以来,不鼓励将Microsoft.ASP.Net.Core.All作为包引用,这适用于当前版本 3。
我们仍然可以通过使用补丁来使用名称空间,但首选的替代品是Microsoft.AspNetCore.App共享框架,详细说明见第 4 章、通过自定义应用实现 ASP.NET Core 3 的基本概念:第 1 部分,当我们阐述 ASP.NET Core 3 的基本概念时。
为了使 ASP.NET Core 尽可能轻量级,或许为了更好地控制,Microsoft 决定只让内部开发和维护的程序集位于共享框架中,并排除使用Microsoft.AspNet.Core.All命名空间的第三方程序集。
值得注意的是,从该框架中删除的非完全由微软所有,因此也不完全由微软控制的人员伤亡包括 Json.NET。然而,我们仍然可以通过添加它们的引用来使用它们。
ASP.NET Core 3 还允许我们使用现成的模板创建遵循 MVC 体系结构风格的应用,该模板可供使用,我们在本书后面的章节中专门介绍了这一主题。
此外,我们可以构建基于 HTTP 的 web 服务以及 RESTful 服务。下一节将介绍 ASP.NET Core 3 的新增功能,即 ASP.NET Core 3 附带的gRPC模板。
ASP.NET Core 3 完全支持 Razor,它包含用于创建视图和标记帮助程序的高效语言,允许从服务器端编写逻辑以生成可在 Razor 视图中使用的 HTML。
在客户端开发方面,ASP.NET Core 3 与 Microsoft 外部的多个框架(包括 Angular、React 和 React-Redux)集成并协同工作,但必须注意的是,这些框架将变得越来越不突出,Microsoft 显然试图通过内部 Razor 组件处理类似的功能,另称为Blazor。
此外,ASP.NET Core 还提供了以下基本改进:
- ASP.NET MVC 和 web API 已组合到一个框架中。
- 基于环境的配置系统已准备好进行云托管。
- 依赖项注入功能是默认的。
- 您可以在 IIS、Docker、云中甚至在您自己的进程中托管相同的应用,也可以自行托管。
- 有新的工具简化了现代 web 开发。
- 有一个简化的
csproj文件,使它更容易与 Visual Studio 以外的开发环境(例如,在 Linux 和 macOS 上)一起工作。 Startup.cs已通过将日志记录和配置移动到主机生成器初始化中来简化。- ASP.NET Core 应用现在可以在 Visual Studio for Mac 上开发。
我们看到的功能也可以应用于 ASP.NET Core 3 之前的其他版本,但其他功能仅适用于 3 版,将来可能更高版本。我们将在下一节中介绍它们。
ASP.NET Core 3 的新增功能是什么?
ASP.NET Core 3 存在于一个生态系统中,其他一切都在发生变化,包括.NET Core 运行时(目前也是版本 3)和 C 语言本身(版本 8)。有了所有这些变化,ASP.NET Core 也适应了生态系统的变化,不仅适应了与 Microsoft 相关的变化,还考虑了开发人员社区的总体情况,例如,将 Angular 模板更改为 Angular 7 就是明证。
ASP.NET Core 3 共享框架已经变得更加轻量级,与实体框架核心、Roslyn 代码分析和 Json.NET 等其他非核心组件分离。
这样的更改不可避免地会影响到连锁反应中的其他更改,例如,强制删除运行时编译,这显然是由 Roslyn 实现的,因此,ASP.NET Core 3 比以前的版本要轻得多。
ASP.NET Core 从 1.0 中作为包引用演变为 2.1 中的共享框架。然而,在 3 中,我们不再通过<PackageReference>元素引用Microsoft.AspNetCore.App,该元素自然被替换为<FrameworkReference>。
如果您的项目引用了Microsoft.NET.Sdk.WebSDK,那么它将自动访问共享框架。通常引用的 API,如 MVC、Razor 和 Kestrel 等,不再被引用为NuGet包,但作为开发人员,我们仍然可以通过<FrameworkReference>元素访问Microsoft.AspNetCore.App。
ASP.NET Core 3 试图改进与OpenAPI的集成,并引入了一个生成 API 客户端的系统,该系统可轻松与NSwag和其他代码生成器集成。
以下屏幕截图显示了从 ASP.NET Core 版本 3 引入的一些新模板:

对 ASP.NET Core 3 最大的介绍之一是C#Razor 组件,迄今为止,它被称为 Blazor。它是作为一个独立的实验框架单独开发的,并且伴随着一个新的.razor扩展,它帮助编译器识别带有 Razor 组件的文件。
通常,JavaScript 代码是大多数浏览器能够理解和执行的代码,但 Razor 组件带来的是能够在浏览器中运行 C#的能力。我们将在第 6 章介绍 Razor 组件和 SignalR中进一步讨论 Razor 组件。
ASP.NET Core 3 默认附带一个工作者服务模板。如果您有桌面开发背景,您将熟悉 Windows 服务,对于那些有 Linux 经验的人来说,您也会熟悉类似的守护进程。作为 web 环境的一个答案,ASP.NET Core 3 为我们引入了一个模板,以便我们能够开发辅助服务,以满足长期运行的服务。
ASP.NET Core 3 添加的另一个令人兴奋的新功能是gRPC 服务模板,它将受到经常使用微服务的开发人员的欢迎。gRPC源于 Google,在 HTTP/2 上的服务对服务通信中,与普通的 XML/JSON 序列化相比,它使用了更轻量级的协议缓冲区序列化。这方面的演示将包含在第 8 章创建 Web API 应用中。
正如 ASP.NET MVC 以前的用户所知,路由模型已经有了显著的改进,主要是它如何使用中间件进行操作,恰当地称为端点路由;它在 2.2 中介绍,但在 3 中具体介绍的是信号机和 Razor 组件与端点路由的集成。更多信息将在第 6 章中介绍 Razor 组件和 SignalR。
现在我们已经看到了与 ASP.NET Core 3 相关的许多重要功能,有一个功能值得特别提及和具体介绍,因为它的重要性。在当今多样化的技术平台中,支持不同的平台非常重要,因此我们将在下一节中研究 ASP.NET Core 3 如何为跨平台支持做好准备。
跨平台支撑
如前所述,ASP.NET Core 3 框架从一开始就考虑到了跨平台支持。它支持多种操作系统和技术,如 Windows、Linux、macOS、Docker、Azure 等。
ASP.NET Core 3 目前支持以下 Linux 发行版:
- Ubuntu 14、16
- Linux Mint 17、18
- Debian 8
- 软呢帽
- CentOS 7.1 和 Oracle 7.1
- SUSE 企业服务器 64 位
- openSUSE 64 位
关于 macOS,它目前只支持以下内容(以后可能会添加其他版本):
- macOS 10.11
- macOS 10.12
对于应用开发,您可以使用 Visual Studio 或 Visual Studio 代码在 Windows 上进行开发,然后将 ASP.NET Core 3 应用部署到目标系统。
Note that the target system can use a completely different underlying operating system. For instance, you can develop and test on Windows and then deploy your applications to a Linux server for performance, stability, or cost reduction reasons.
如果您选择这样做,当然可以使用几个特定于系统的源代码编辑器在 Linux 和 macOS 上直接开发。例如,在 Linux 上,您可以使用 Visual Studio 代码、Vim/Vi、Sublime 或 Emacs。在 macOS 上,您可以使用 Visual Studio for Mac、Visual Studio 代码或任何其他特定于 macOS 的文本编辑器。
不过,Visual Studio 2019 或 Visual Studio Code developer 环境将是首选,因为它们提供了高效、能够调试和理解代码以及在其中轻松导航所需的一切。这就是为什么我们将在本书的其余部分使用这些 IDE。
构建应用后,可以使用多个 web 服务器来运行它。以下是一些例子:
- 阿帕奇
- 非法移民
- 红隼自寄主
- NGINX
跨平台是一个巨大的因素,我们已经看到 ASP.NET Core 3 是如何满足它的,但在软件工程界还有一个流行词叫微服务。在下一节中,让我们看看 ASP.NET Core 3。
微服务体系结构
微服务架构(Microservice architecture)最常被称为微服务,是当前一种以模块化方式设计和构建软件应用的常用方法,考虑到单一责任原则。它强调在实现面向服务的业务解决方案时,具有与其他服务不紧密耦合的服务模块。微服务可用于构建电子商务系统、商业应用和物联网。您会发现它们是非常流行的实现,尤其是在使用分布式应用时。
当您想要采用此系统体系结构时,ASP.NET Core 3 是最佳候选。ASP.NET Core 3 框架是轻量级的,它的 API 表面可以最小化到特定微服务的范围内。微服务体系结构还允许您跨服务边界混合各种技术,从而逐步过渡到 ASP.NET Core。
请注意,使用 ASP.NET Core 3 构建的微服务可以与使用其他技术(如完整的经典.NET Framework、Java、Ruby,甚至其他更传统的技术)的服务协同工作。当您需要逐步将单片应用转换为更多(微型)面向服务的应用时,这是一个很大的优势。
您不受特定基础设施的约束;相反,您有很多选择,因为 ASP.NET Core 3 支持您现在可以想到的几乎所有技术。此外,您可以在需要时修改基础结构,这样就不会对基于基础结构开发的应用进行技术锁定。
高效、大规模、本地和云中编排和管理用 C#编写的微服务的主要选择应该是 Microsoft Service Fabric,也称为 Azure Service Fabric。它正是为此而构思的,并且已经被微软用于各种 Azure 服务(如 SQL 数据库)很多年了。
microservicesdocker 容器方法也可能适合您的需要,我们将在下一节中解释它的用例。总之,ASP.NET Core 3 是在任何技术环境中实现和托管微服务的理想选择。
使用容器
容器目前很流行,因为它们提供了一种高效、轻量级和自包含的方法,可以在重用底层操作系统文件和资源的同时打包应用及其依赖项。
它们非常适合微服务体系结构,但也可以用于任何其他应用原型。它们与 ASP.NET Core 3 应用配合得非常好,因为这两个应用都考虑到了模块化、性能、可扩展性、轻量级和效率。
我们必须注意,目前有不同的容器可供开发人员社区使用,如 CoreOS rkt、Apache Mesos 容器器和LXC(简称Linux 容器),但目前最流行的是 Docker 容器。
Note that Docker container images including ASP.NET Core 3 applications are much smaller than images with classic ASP.NET applications, meaning that they are faster to deploy and to start up.
Docker 容器和 ASP.NET Core 3 框架都提供了完整的跨平台支持(Windows、Linux 和 macOS)。此外,您可以在本地和云中托管容器。例如,您可以通过基础设施即服务(IaaS部署或通过Azure 容器服务使用 Azure,后者正被弃用以支持Azure Kubernetes 服务,它还允许混合和匹配不同的操作系统和技术。
微服务体系结构、跨平台支持和其他功能可能会使 ASP.NET Core 3 成为一个很好的框架,但如果它拥有如此强大的功能而没有相应的强大性能,它又有多好呢?ASP.NET Core 3 如何处理需要增长的应用?在下一节中,我们将研究 ASP.NET Core 3 的性能和可伸缩性。
性能和可扩展性
如果您需要为高可扩展性场景提供尽可能好的性能和支持,那么您绝对需要使用 ASP.NET Core 3 和当前版本为.NET Core 3 的底层.NET Core Framework。
ASP.NET Core 3 是为高性能和高可扩展性场景而从头构建的。它在这些领域非常出色,可以被认为是最佳选择。
它比经典的 ASP.NET 快很多倍,可以被认为是目前可用的.NET 世界中最快的 web 应用运行时!
如果我们要通过 TechEmpower 所做的测试,测试不同 web 框架的性能,可以在这里找到:https://www.techempower.com/benchmarks 您会注意到,ASP.NET Core 肯定会在其.NET 同行中名列前茅,当然,与其他提供商的竞争对手框架相比,它的表现也相当出色:

您可以使用 Microsoft 的 ASP.NET Core 基准测试项目中的详细信息来运行 ASP.NET Core 基准测试,请参见:https://github.com/aspnet/benchmarks 。
此外,它还为性能和可扩展性极其重要的微服务体系结构提供了最佳解决方案。在消耗如此低的系统资源的同时,没有任何其他技术比这更高效,这也降低了基础设施和云托管成本。
到目前为止,我们已经看到了使用 ASP.NET Core 3 作为一个平台是多么好,具有前面提到的所有功能,但不幸的是,该平台及其运行时不支持其他技术。我们将在下一节中介绍它们。
技术限制
请仔细阅读本节中显示的技术。如果您在当前应用中使用此处列出的技术或框架,但该技术或框架(尚未)受支持,则您可能会发现很难甚至不可能迁移到 ASP.NET Core 3。
并非所有当前的.NET Framework 技术都可以在 ASP.NET Core 3 中使用,有些技术可能永远无法移植,因为它们不符合新的.NET Core 特定范例和模式。
ASP.NET Core 和.NET Core 中未直接找到的常用技术
下面的列表显示了 ASP.NET Core 和.NET Core 中未直接找到的最常用技术,尽管有些技术可以通过多目标功能使用:
- ASP.NET Web 表单应用:传统 Web 表单技术仅在使用完整的经典.NET 框架时可用;不能对这些类型的应用使用 ASP.NET Core 和.NET Core。
- ASP.NET 网页应用:它们本身不包括在 ASP.NET Core 3 中,但可以使用 Razor 网页引擎提供相同的功能。
- WCF 服务:ASP.NET Core 3 包含用于访问 WCF 服务的 WCF 客户端,但不支持创建 WCF 服务。
并非所有可用于 ASP.NET Core 3 的模板都支持所有主要的.NET 语言;例如,VB.NET 唯一可用的模板是 GtkSharp,F#还有一些模板,包括 ASP.NET Core web API 和 F#TypeProvider 模板。更全面的模板列表可在以下链接中找到,该模板适用于何种语言:https://github.com/dotnet/templating/wiki/Available-templates-for-dotnet-new 。
何时选择 ASP.NET Core 3
ASP.NET Core 3 和底层的.NET Core Framework 运行时确实提供了一些主要的增强和性能改进,但仍有一些特定的场景,在这些场景中,新的应用模式不适用,而完整的.NET Framework 将是最好的,有时甚至是唯一的选择。
从一开始就将整个现有应用迁移到 ASP.NET Core 可能很困难,甚至不可能做到。您应该考虑如何逐步转换应用,以降低失败或过于复杂的风险,并给自己时间真正理解新的模式和范例。
例如,您可以先在所有新开发中只使用 ASP.NET Core 3,然后再看看如何在以后迁移遗留代码,有时甚至可以不迁移,因为迁移它不会带来任何实际好处。如果你真的对移民话题感兴趣,请考虑一下 To.T0.附录 AUTYT1T.因为我们有一个完整的篇章致力于这个重要的话题。
ASP.NET Core 和.NET Core 框架每天都得到越来越多的框架和客户端库支持。Microsoft、工具和框架供应商以及不同的开发人员社区努力提供大量功能,以支持功能丰富且高性能的 web 应用。每个人都想致力于这项有前途的技术,它可以可持续地塑造未来。
在使用.NET 标准 2.0 时同时使用.NET Core 和.NET Framework 库的可能性进一步扩展了这种可能性,并为开发人员提供了一个临时解决方案,直到.NET Core 中的每个重要功能和每个主要框架都可用为止。
要重述本节中讨论的内容,如果满足以下条件,则应在服务器应用中使用 ASP.NET Core 3:
- 你有跨平台的需求。
- 您专门针对微服务。
- 您想使用 Docker 容器。
- 您需要高性能和高度可扩展的应用。
- 所提供的技术限制不适用于您的应用要求。
总结
在本章中,您了解了 ASP.NET Core 3 框架及其功能。您已经看到,它包含了在使用微服务体系结构和容器技术(如 Docker)的同时在跨平台环境中高效工作所必需的一切。
此外,您还了解到它为您的 web 应用提供了非常好的性能和优异的可伸缩性。
在本章的最后,我们讨论了技术限制以及何时建议使用 ASP.NET Core 3 框架。
在下一章中,我们将讨论如何设置开发环境,包括 Visual Studio 2019 或 Visual Studio 代码作为 IDE。
二、建立环境
您已经决定学习 ASP.NET Core 3,这是当今市场上最先进、最高效的跨平台 web 应用框架。一个很好的选择!您当然渴望立即开始编程,但在我们开始之前,我们必须设置所需的技术先决条件和工具。
在本章中,我们将介绍 VisualStudio2019 社区版和 VisualStudio 代码,然后将其中任何一个安装为开发环境。然后,我们将基于 ASP.NET Core 3 框架构建一个简单的示例应用。
阅读本章内容后,您将能够在 Windows 操作系统、macOS 和 Linux 上安装不同类型的 ASP.NET Core 3 开发环境。您还将学习对大多数基于 ASP.NET Core 的应用进行故障排除所需的基本调试技能。
总而言之,在本章中,我们将介绍以下主题:
- Visual Studio 2019 作为开发环境
- 如何安装 Visual Studio 2019 社区版
- 在 Visual Studio 中并通过命令行创建第一个 ASP.NET Core 3 应用
- 使用 Visual Studio 2019 社区版进行基本调试
- Visual Studio 代码作为开发环境
- 如何在 Linux 上安装 Visual Studio 代码
- 在 Visual Studio 代码中创建第一个 ASP.NET Core 3 应用
- 在 Linux 中创建第一个 ASP.NET Core 3 应用
- C#交互式和 LINQPad 工具简介
Visual Studio 2019 作为开发环境
作为开发人员,您需要一个用于日常开发任务的环境,而 Microsoft Visual Studio 2019 正是如此。
不同编程语言的开发人员还可以使用许多其他 IDE,其中一些著名的 IDE 包括 NetBeans、PyCharm、IntelliJ IDEA、Eclipse、Code::Blocks 和 XCode 等。虽然您可以使用其中许多工具来使用 C#进行编程,这是我们将在本书中使用的基础语言,但必须注意,前面提到的 IDE 更适合于其他语言,如 Python 和 Java。
其他更适合 C#编程语言的 IDE 包括 Visual Studio 代码、MonoDevelop、SharpDevelop(#develop)、JetBrains Rider、CodeMaid 和.NET Fiddle。
这本书将利用微软技术堆栈上开发人员最常用的两个 IDE:VisualStudio 系列和 VisualStudio 代码。
Visual Studio 2019 提供了一个非常高效和高效的集成开发环境(IDE),用于创建新的软件项目以及开发、调试和测试它们。它将帮助您以非常快速和直观的方式构建高质量的应用。它的许多特性都是围绕着常见的开发任务以及如何在单个工具中简化和优化这些任务而构建的。通过使用此工具,您可以创建 web 应用、web 服务、桌面应用、移动应用以及本书中未介绍的许多其他类型的应用。
此外,您还可以使用多种编程语言,如 C#、Visual Basic、F#、JavaScript,甚至 Java 或其他不由 Microsoft 维护的语言。
Visual Studio 2019 有不同的版本,每个版本都有自己独特的功能和许可证。例如,VisualStudio2019 社区版是免费的,但与专业版和企业版相比功能更少,我们将在后面解释。社区版的预期用途是用于私人用途和学习目的。
Visual Studio 2019 Professional 和 Enterprise 版本包含更多功能,包括在生产环境中构建和运行应用所需的许可证。
Visual Studio 2019 专业版包含企业版中提供的所有功能的子集。通常从这个版本开始,然后在必要时升级到企业版就足够了。
Visual Studio 2019 Enterprise Edition 包含许多附加功能,我们可以使用这些功能进一步提高开发人员的工作效率,例如时间旅行调试、实时依赖项验证、实时测试、体系结构图、体系结构验证、代码克隆等。如果您需要这些功能,则需要使用此版本。
可在以下链接中找到每个版本可用功能的完整比较:https://visualstudio.microsoft.com/vs/compare/ 。
Note that multiple versions of Visual Studio (2013, 2015, 2017, 2019, and more) can be installed side by side on a developer machine that has earlier versions of the Visual Studio IDE installed.
传统上,VisualStudio 只针对 Windows 发布,但自 2016 年以来,macOS 版本一直存在,称为 VisualStudio for macOS。您可以使用它在此操作系统上开发.NET 应用。Visual Studio for macOS 支持开发.NET Core、ASP.NET Core、Mono 库(包括.NET 标准)和 Xamarin 应用。您可以在 Visual Studio 中构建相同类型的 ASP.NET Core 应用,但工具还不够丰富。
VisualStudio2019 社区版正是我们尝试和理解本书中示例所需要的。这正是我们将在下一节中安装它的原因。
如何安装 Visual Studio 2019 社区版
Visual Studio 2019 Community Edition 可以像任何其他 Windows 应用一样安装。
Note that you need administrator rights during the installation process. These rights will not be required when developing with Visual Studio later.
要安装 Visual Studio 2019 社区版,您可以在以下三种不同的 Visual Studio 2019 安装模式中进行选择:
- 快速安装以简单快捷的方式安装所有被微软视为默认组件的组件。这些组件位于“默认工作负载”选项卡上,可以方便地分组为 Windows、web 和云、移动和游戏以及其他工具集,您只需选中相应的复选框即可安装这些工具集。如果需要此列表中没有的特定 Visual Studio 功能,则需要使用自定义安装。
- 自定义安装选项为您提供了可安装的所有 Visual Studio 2019 功能的完整选择。例如,您可以安装互补的特性,例如 VisualC++、F、SQL Server 数据工具、移动平台以及其他几个 SDK,以及特定的语言包,通过各个组件、语言包和安装位置标签。VS 安装程序中的安装组称为工作负载**。
- 使用离线安装时,您可以安装 Visual Studio 2019 而无需网络连接。当您无法连接到 internet 并且仍然希望准备一台开发人员机器时,这非常方便。在这种情况下,您必须准备外部支持,例如移动硬盘或 USB 密钥,并事先将 Visual Studio 2019 安装程序文件放在其上。
准备此类外部支持的一种方法是从 Visual Studio 网站下载必要的 Visual Studio 安装程序(社区版、专业版或企业版)https://www.visualstudio.com/downloads/ ,并将其内容解压缩到文件夹中。然后,您可以通过在命令行窗口中执行<executable name> --layout命令来检索各种安装包。一段时间后,所有内容都将被下载,您将获得可用于脱机安装的外部支持。
Note that you can use the same procedure to download all of the installation files to your central network storage and then create a shared folder so that you can install Visual Studio 2019 from within your own network to optimize installation times and lower network bandwidth needs.
现在,让我们了解如何使用从前面提到的 Microsoft Visual Studio 网站下载的安装程序手动安装 Visual Studio 2019 Community Edition:
- 启动 Visual Studio 2019 社区版安装程序。您将看到各种可安装工作负载的列表。默认情况下,您将看到 Windows、Web 和云、移动和游戏以及其他工具集:

-
选择所需的组件–它们将在以下步骤中安装。如果这就是你所需要的,那么你不需要做任何其他事情。如前所述,这是快速安装过程。
-
如果需要自定义已安装的组件或添加或删除单个组件,则必须单击单个组件。从这里开始,您将进行自定义安装:

- 根据指定语言包的可用性,您可能需要选择自己的语言。此选项卡当前有中文、捷克文、英文、法文、德文、意大利文、日文、韩文、波兰文、葡萄牙文、俄文、西班牙文和土耳其文作为可用选项。您可能还需要指定自定义安装路径,这可以通过“安装位置”选项卡完成。
- 选择完所需的工作负载和组件后,安装将开始。安装时间取决于您选择的工作负载和组件的数量,以及您的 internet 连接速度(如果您不使用我们前面介绍的脱机安装方法)。
对于更高级的方案,例如自动化 Visual Studio 2019 安装并编写脚本,可以通过命令提示符启动安装程序。有各种各样的命令行参数可以帮助我们定义需要安装的内容和位置。
下面列出了一些可用的命令行参数,并简要介绍了它们的作用。请转到https://docs.microsoft.com/en-us/visualstudio/install/use-command-line-parameters-to-install-visual-studio 了解更多信息,包括除以下所述参数外的所有现有命令行参数的完整列表:
| 参数 | 说明 |
| /AddRemoveFeatures | 这将添加已选择的特征 |
| /AdminFile | 这将指定要静默安装的文件 |
| /CreateAdminFile | 这指定您希望在安装后生成静默响应文件 |
| /CustomInstallPath | 这将指定目标路径 |
| /ForceRestart | 这将迫使您的电脑重新启动 |
| /Full | 这将安装所有必要的功能 |
| /noweb | 这将禁用 internet 搜索功能和下载 |
| /ProductKey | 这指定要使用的密钥 |
Visual Studio 2019 的第一步
安装 Visual Studio 2019 后,您现在可以探索它在提高开发人员生产率方面提供的一切。以下是所提供的一些功能的列表。
VisualStudio 最重要的功能之一是 IntelliSense。它通过提供列表成员、参数信息、快速信息和完整 Word 等功能,帮助开发人员提高工作效率。它在 VisualStudio2019 中得到了改进,具有一些非常有趣的新功能,例如 IntelliCode,现在您可以按类型(类、名称空间或关键字)和 camelCase 搜索进行过滤。
您可以使用编程语言、平台和项目类型作为搜索筛选器。以下选项将为您提供:
- java 语言,C++,java,f*,javascript,python,查询语言,类型化脚本,Visual Basic
- 平台:所有平台,Android、Azure、iOS、Linux、macOS、tvOS、Windows 和 Xbox
- 项目类型:所有项目类型,云、控制台、机器学习、桌面、扩展、游戏、物联网、图书馆、移动、办公、服务、测试、UWP、网络
建议从结果列表中选择最佳匹配项,而不是只选择最匹配项,因为它不一定总是正确的:

Visual Studio 2019 的代码重构和实时代码分析功能加快了开发速度,并确保您拥有可读和可维护的代码。例如,以下是一个实例,您可以在其中自动添加缺少的名称空间或删除不必要的名称空间:

下面是代码重构建议的一个示例,在本例中它显示为一个灯泡:

使用 VisualStudio2019,您会发现它包含了以前仅作为外部插件(如 ReSharper)的增强功能提供的功能。这方面的一个例子是,您现在可以将foreach循环转换为更简洁、更高性能的 LINQ 语句。
顾名思义,“查找所有引用”功能允许开发人员轻松快速地查找方法或对象的所有引用。着色、分组和查看预览功能可以直观地帮助您在代码中导航并真正理解代码:

Peek Definition 和 Go To Definition 功能允许您在弹出窗口中检查方法、接口或类的定义,而无需更改当前窗口,或直接打开包含所请求定义的源代码的文件。Go To Implementation(转到实现)功能也会执行相同的操作,但会导航到实现:

另一个重要特性是实时单元测试。您需要 Visual Studio 2019 Enterprise Edition 才能使用它。它允许您在每次修改或编译代码后在后台自动运行单元测试。它可以在测试设置菜单中配置和激活。从这里,您可以设置测试进程的数量、每个测试的最大持续时间和最大内存消耗:

Visual Studio 2019 中有许多更有趣和激动人心的功能,我们邀请您访问 Visual Studio 官方网页https://docs.microsoft.com/en-us/visualstudio/welcome-to-visual-studio 如果你想了解更多。对于开发人员来说,关键是尽可能了解他们的开发人员 IDE,并熟悉其许多特性,以便他们能够更好更快地完成工作。因此,在开始开发应用之前,一定要花一些时间来了解这一点。
在 Visual Studio 2019 中创建第一个 ASP.NET Core 3 应用
您已经耐心地阅读了前面的章节,理解了通过阅读本书将要学习的内容,并准备好了开发人员机器。现在,您已经准备好创建第一个示例应用。
按照以下说明创建第一个 ASP.NET Core 3 示例 web 应用:
-
如果您尚未安装.NET Core 3 SDK,请从下载并安装.NET Core 3https://dotnet.microsoft.com/download/dotnet-core/3.0 。
-
启动 VisualStudio2019。您将看到一个页面,允许您克隆或签出代码或打开现有项目。在此,我们有兴趣创建一个新项目:

- 如果单击“创建新项目”或“不使用代码继续”,然后单击“文件|新建|项目”,则会弹出相同的“创建新项目”页面:

选择 ASP.NET Core Web 应用并单击下一步。
- 选择项目模板,即 Visual C#|.NET Core | ASP.NET Core Web 应用(.NET Core)并单击下一步后,您可以命名项目并配置其位置:

- 现在,您可以选择特定的 web 应用类型,即 ASP.NET Core 版本。请注意,如果我们选择 ASP.NET Core 3.0,则左侧只有.NET Core 可用,但对于所有其他版本,我们也有.NET Framework 可供选择。向下滚动并选择 Web 应用,保留启用 Docker 支持,并取消选中 Configure for HTTPS 选项。另外,将“身份验证”设置为“无身份验证”:

- 生成示例应用项目后,将显示项目起始页。在这里,您可以配置其他选项,例如连接服务(应用洞察等)和发布目标(Microsoft Azure 应用服务、IIS、FTP、文件夹等)。保持一切不变:

- 现在,您可以通过按F5或单击调试|开始调试来启动应用。
通过命令行创建第一个 ASP.NET Core 3 应用
在上一节中,您学习了如何使用 Visual Studio 2019 创建第一个 ASP.NET Core 3 web 应用。这应该是大多数开发人员的首选方法。
但是,如果您更喜欢使用命令行或 Visual Studio 代码(我们将在本书稍后介绍),那么使用 Visual Studio 2019 并不是一个真正的选项。幸运的是,.NET Core 和 ASP.NET Core 3 提供了对命令行的完全支持。这甚至可能是您在其他操作系统(如 Linux 或 macOS)上的唯一选项。相同的命令行指令适用于所有不同的操作系统,因此一旦您习惯了它们,就可以在任何环境下工作。
现在,让我们学习如何使用 Windows 命令行创建第一个示例应用:
- 如果您尚未安装.NET Core 3 SDK,请从下载并安装.NET Core 3https://dotnet.microsoft.com/download/dotnet-core/3.0 。
- 为示例应用创建一个名为
mkdir aspnetcoresample的文件夹。 - 移动到新文件夹:
cd aspnetcoresample。 - 基于名为
dotnet new web的空 ASP.NET Core 3 web 应用模板创建新的 web 应用。
Previous versions of .NET Core required an additional -t parameter for choosing a template (dotnet new -t web). If you get an error when executing dotnet new web, it is a good indication that you need to install .NET Core 2.0.
请注意,如果您不确定您的环境,您可以通过输入dotnet(无参数)来验证您的.NET 版本,因为它将显示当前的.NET Core 版本。
- 通过执行
dotnet run运行示例应用:

- 打开浏览器并转到
http://localhost:5000。如果一切正常,你应该看到一个你好的世界!第页:

在本节中,您学习了如何使用 Visual Studio 2019 或命令行创建第一个示例应用,还学习了如何使用 Visual Studio 代码,以及它如何帮助您在 Linux 或 macOS 上构建 ASP.NET Core 3 应用。
现在,您已经安装了 Visual Studio 2019,并且已经启动并运行了第一个应用,您需要知道在应用中遇到错误时该怎么办。这是时间的问题,不是如果。如果在开发应用时遇到错误,不要绝望——即使是最有经验的人也会遇到这种情况。幸运的是,VisualStudio 帮助我们诊断错误。在下一节中,我们将介绍如何使用 Visual Studio 2019 进行调试。
Visual Studio 2019 的基本调试
每当我们为软件应用编写逻辑时,有时我们会设法完全实现预期的功能,而不会出现任何问题和错误。虽然第一次就把它做好通常是可取的,但对于大多数软件开发人员来说,情况肯定不总是这样。
我们可能会遇到这样一种情况:我们的代码已经成功编译,但是我们发现没有我们想要的输出,或者我们可能会得到编译时或运行时错误。在这种情况下,开发人员在软件发布后发现任何错误之前发现错误是非常有帮助的。
应用中的错误是语法错误或语义错误,作为开发人员,您要么没有遵循规定的语言语法,要么没有逻辑意义。这类错误更容易发现。VisualStudioIDE 将帮助您捕获这些开发错误中的大部分,同时在您以调试模式运行应用后拒绝编译或引发异常。
还有一组应用错误是由于无法产生预期的行为而导致的,即使我们已经尽了最大努力根据规定的功能编写代码。最好通过编写单元测试并根据我们的代码运行它们来进行计数器检查。软件开发行业中的一些专业人员提倡使用 To.T0.测试驱动开发 To1 T1(Po.T2AdTDD AutoT3),其中测试是在编写任何功能之前编写的,而其他人可能认为这是一种重复的努力,并不是那么乐观。不管是什么情况,单元测试在应用中都是非常重要的,我们将在本书后面的部分花一些时间,当我们提供实际示例时,展示它们的价值。
以下屏幕截图是 Visual Studio 2019 中提供的调试功能的快照:

我们将不讨论通过前面屏幕截图中显示的调试菜单提供的所有调试功能,因为这需要一本完整的书,但作为一名开发人员,它非常值得探索。了解调试的基本知识将为开发人员节省大量时间。在下一节中,我们将解释 Visual Studio 2019 中提供给您的几个最重要的调试项。
断点
断点是调试中最重要的工具之一,在前面的屏幕截图中用红点表示。当您通过“调试”菜单以调试模式启动应用时,应用将按顺序在代码中运行,直到它到达您在代码库中任何一点上放置的断点,从该断点开始,您可以选择单步执行代码语句并检查它、单步执行它或从一组语句中单步执行。
断点将使您能够访问特定实例中对象的实际值,这将有助于您在关注点检查程序的行为,从而帮助您排除可能出现的任何问题。
一个程序可以有任意多的断点,您可以批量启用或禁用这些断点。
有时,您可能需要 Visual Studio 仅在特定条件下中断,例如属性更改或某些条件变为 true 时。幸运的是,VisualStudio 为此场景提供了一个现成的解决方案。您可以通过右键单击任何正常断点,然后选择条件来设置通常称为条件断点。单击它时,将出现一个弹出窗口,您可以在其中设置条件,如下所示:

前面的屏幕截图只是一个假设的例子,它展示了我们如何在用户模型中查看LastName,并检查它是否等于我们选择的字符串"FUKIZI"。在前面的示例中,我们设置了另一个名为Hit Count >= 2的条件,这意味着我们的条件断点只有在被命中两次或更多次后才会触发。
我们还可以设置一个操作来输出消息,或者选择是否要继续执行。有时,我们可能需要回顾在达到某一点之前发生的事情。为此,调用堆栈很方便。
调用堆栈
调用堆栈将为您提供程序调用的快照历史记录,以使您达到调试模式:

如果您希望检查代码在其直接历史记录中所做的事情,这将非常有用,并且可以帮助您定位代码中的问题可能起源于何处。
自动、本地和监视窗格
除了将鼠标悬停在断点上以查看变量中包含的内容外,Autos 窗口是调试时的下一个最重要的工具,因为它以更持久的方式在屏幕上为您提供变量内容的快照。您可以将手表添加到感兴趣的特定变量,它将显示在“手表”窗格中。您可以操纵和修改变量的值,作为测试变量或对象中应该包含或传递的内容的一种方法,以尝试获得预期的行为,然后再决定在故障排除时在何处更改有问题的代码:

除了上面提到的工具和其他超出本书范围的工具外,必须指出的是,调试是一项随着时间和经验而强化的技能。您编写和调试的应用越多,您发现应用哪里出了问题就越直观和自然。
到目前为止,我们已经介绍了使用 VisualStudio2019 作为集成开发环境的一些方面。这是一个非常好的工具,深受许多人的喜爱,但也有其他一些功能强大的工具可用于开发 ASP.NET Core 3 应用。许多人喜欢并使用的一种工具是 VisualStudio 代码,我们将在下一节介绍它。
Visual Studio 代码作为开发环境
Visual Studio 代码是针对 Windows、Linux 和 macOS 的轻量级、功能强大的跨平台开发环境。
您可以使用各种编程语言如 JavaScript、Type Script 和 NoDE.js,以及 C++、Cython、Python、PHP、Go 和.Net Cype 和 Unity 运行库,通过语言和运行时扩展。
它有一个流线型的、干净的、非常高效的用户界面。左侧有一个文件和文件夹资源管理器,右侧有一个源代码编辑器,其中显示了您已打开且当前正在处理的文件的内容:

用户界面由以下区域组成:
- 活动栏:提供几种不同的视图和额外的上下文特定指标,例如启用 Git 时传出代码的更改。
- 侧边栏:包含用于处理项目的文件和文件夹资源管理器。
- 编辑组:这是使用代码并在其中导航的主要区域。最多可以同时打开三个源代码编辑器窗口。
- 面板:显示带有输出或调试信息、错误和警告的面板,或集成终端。
- 状态栏:提供有关您编辑的项目和文件的附加信息。
Please go to https://code.visualstudio.com/docs for additional information on Visual Studio Code and its capacities and functionalities. It will be our primary choice for illustrating how to build ASP.NET Core 3 applications on Linux.
如何在 Linux 上安装 Visual Studio 代码
在本节中,我们将解释在 Linux 上安装 VisualStudio 代码是多么容易和快速。最流行的 Linux 发行版之一 Ubuntu16.04 就是一个例子。
如果您没有可用的 Linux Ubuntu 的物理或虚拟安装,您可以轻松地在 Azure 中安装它,以试用 Visual Studio 代码并了解各种 ASP.NET Core 3 示例。通过执行此操作,您可以通过 Microsoft 远程桌面应用进行连接。
在本例中,从 Azure Marketplace 中选择 Linux Ubuntu 18.04 LTS 映像,并在 Azure 中创建一个新的 Linux Ubuntu VM。保留所有默认选项,并将其配置为允许远程桌面连接(安装兼容的桌面、安装xrdp、打开端口3389等):

让我们了解如何在 Linux Ubuntu 上安装 Visual Studio 代码:

- 在 Ubuntu 中打开一个新的终端窗口。
- 通过
sudo dpkg -i <file>.deb安装下载的软件包。 - 然后,输入
sudo apt-get install -f。 - 通过键入
xdg-mime default code.desktop text/plain命令,将 Visual Studio 代码设置为默认文本文件编辑器。
安装将开始并自动安装 APT 存储库和签名密钥,以启用自动软件包更新,以及 Visual Studio 代码:

您还可以手动安装存储库和签名密钥,更新包缓存,并启动 Visual Studio 代码包安装,如下所示:
- 在 Ubuntu 中打开一个新的终端窗口:
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --
dearmor>microsoft.gpg sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg
sudo sh -c 'echo "deb [arch=amd64]
https://packages.microsoft.com/repos/vscode stable main" >
/etc/apt/sources.list.d/vscode.list'
sudo apt-get update
sudo apt-get install code
- 通过键入
xdg-mime default code.desktop text/plain命令,将 Visual Studio 代码设置为默认文本文件编辑器。
For more information on how to install Visual Studio Code on other Linux distributions, such as RHEL, Fedora, CentOS, openSUSE, SLE, and others, please go to https://code.visualstudio.com/docs/setup/linux.
现在,我们已经准备好了环境,可以在新的 Linux 环境中创建第一个 ASP.NET Core 应用。
在 Visual Studio 代码中创建第一个 ASP.NET Core 3 应用
现在,您将学习如何使用内置的 Visual Studio 代码终端窗口初始化第一个 ASP.NET Core 3 应用。然后,您将安装所有必要的扩展,以便可以运行和调试它:
- 启动 VisualStudio 代码。
- 单击打开文件夹,然后单击创建文件夹。将文件夹命名为
aspnetcoremvcsample并单击“确定”:

- 通过查看|集成终端显示集成终端窗口,并通过输入
dotnet new mvc初始化一个新的 ASP.NET Core 3 MVC 项目:

- 打开 C#文件时,会要求您安装其他项目依赖项和 Visual Studio 代码扩展。您需要这样做,才能按照以下步骤构建、运行和调试应用:

- 修改
.vscode文件夹中的launch.json文件,并将调试器设置为.NET Core Launch(web):

- 在代码中的任意位置设置断点,然后通过按F5或单击 debugging viewlet 中的绿色闪光灯开始调试。尝试点击断点;一切都应该正常工作。
在 Linux 中创建第一个 ASP.NET Core 3 应用
要在 Linux 中仅使用终端窗口创建和运行第一个示例应用,请执行以下步骤:
- 如果您尚未安装.NET Core 3 SDK,请从下载并安装 https://dotnet.microsoft.com/download/dotnet-core/3.0 用于您的 Linux 发行版。下面是一个如何为 Ubuntu 执行此操作的示例:
sudosh -c 'echo "deb [arch=amd64]
https://apt-mo.trafficmanager.net/repos/dotnet-release/
xenial main" > /etc/apt/sources.list.d/dotnetdev.list'
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80
--recv-keys 417A0893
sudo apt-get update
sudo apt-get install dotnet-sdk-2.0.0-preview2-006497
- 为示例应用创建一个名为
aspnetcoremvcsample:mkdir ~/Documents/aspnetcoremvcsample的文件夹。 - 移动到新文件夹,即
cd ~/Documents/aspnetcoremvcsample。 - 基于 ASP.NET Core 3 MVC web 应用模板创建一个名为
dotnet new mvc的新 web 应用:

- 通过执行
dotnet run运行示例应用:

- 打开浏览器并转到
http://localhost:5000
在本节中,我们看到了第一个在不同操作系统上运行的应用,并简要探讨了集成开发环境中可用的工具。我们将在下一节中介绍 C#Interactive。还有其他一些外部工具可以帮助尽可能轻松地进行开发。LINQPad 就是这样一个工具,我们也将在下一节介绍它。
C#交互式和 LINQPad 工具简介
C#Interactive 是一个读取-评估打印循环(REPL工具,许多使用 Visual Studio 的开发人员通常会忽略它,但它具有惊人的功能,可以帮助您在实现概念之前快速体验它。你可以用它来玩弄 C 语言功能和任何.NET 技术。例如,如果您不太确定某个 ASP.NET Core 3 功能是如何工作的,则可以在窗格中引用它,并与之交互,然后立即查看输出。这使得它比使用完整的应用进行实验更容易、更快。
在下面的简单示例中,我们将声明一个名为SquareNumber的函数,它接受一个整数x,将其自身相乘,并在下一行调用时给出答案:

由于它的高交互性,您可以编写和测试脚本,并立即获得输出,然后您只需将代码复制到窗格中,并将其粘贴到以.csx扩展名命名的脚本文件中即可使用;然后,您可以使用csi关键字通过开发者命令提示符运行它。
您还可以使用 C#Interactive 来学习可以交互使用的外部 API,但这超出了本书的范围。
在第 9 章中使用实体框架核心 3访问数据,我们将使用 LINQPad 演示如何处理和调试 LINQ 查询。现在是介绍 LINQPad 的适当时机,可从下载 https://www.linqpad.net/ 。
Please note that LINQPad 5 supports .NET Framework and that LINQPad 6 supports the .NET Core 3.0 SDK.
LINQPad 还可以用于许多其他方面,包括作为微软主要语言(C#、F#和 VB.NET)的语句、表达式和脚本的测试场,但我们的兴趣在于,当我们在第 9 章中讨论该主题时,使用它帮助我们理解语言集成查询(LINQs、使用实体框架核心 3访问数据。
总结
在本章中,您学习了如何设置开发环境,以便通过安装 Visual Studio 2019 或 Visual Studio 代码来使用 ASP.NET Core 3。
然后,您在这两种开发环境中创建了第一个 ASP.NET Core 3 web 应用,并在 Linux 中构建了一个项目来展示它们的跨平台功能。
在下一章中,我们将讨论如何使用 VisualStudioAzure DevOps 建立持续集成管道,包括工作项、Git 分支以及构建和发布管道。
三、Azure DevOps 中的持续集成管道
构建优秀的应用不是一项简单的任务。相反,这是一项困难而复杂的工作,许多专业人员需要高效地合作,以创建符合高端用户期望的应用。
如今,一切都进展得很快,上市时间对成功非常重要。本章将介绍一些方法、过程和工具,以帮助您优化开发过程,从而构建具有短发布周期的高质量软件。
传统上,构建软件涉及从头到尾规划整个项目、编写详细的规范、开发和测试(通常是匆忙进行的),同时希望一切都能按预期工作(如 V-model 方法所示)。
这种方法有时有效,有时无效。当它不起作用时,开发人员只在手动测试时实现特性,目的是在以后添加单元测试。然后,在项目结束时,他们必须加快速度以确保按时交付,因此经常会出现时间不足的情况。
这导致项目存在重大的技术、功能和质量缺陷,存在大量的 bug 和巨大的维护工作量,导致发布周期过长。在最坏的情况下,最终用户不会喜欢交付的功能;因此,最终产品可视为完全失效。有一种更好的方法做事情,这是人们已经谈论了一段时间的事情,你肯定已经听说过敏捷方法了!
敏捷方法与持续集成(CI)和持续部署(CD相结合,为构建更好的软件提供了解决方案,具有更快的上市时间、更低的维护成本、更好的整体质量和更高的客户满意度。
虽然这本书并不是关于敏捷方法论,但我们建议您熟悉这个主题,并且我们将解释所有与之相关的工具和过程。
在本章中,我们将介绍以下主题:
- CI、CD 以及构建和发布管道
- 将 Azure DevOps 用于 CI 和 CD
- 创建免费的 Azure DevOps 订阅和您的第一个 Azure DevOps 项目
- 通过工作项组织您的工作
- 使用 Git 作为版本控制系统(VCS)
- 创建 Azure DevOps 构建管道
- 创建 Azure DevOps 发布管道
技术要求
为了阅读本章,需要以下内容:
- Microsoft 帐户
- Azure DevOps 订阅
- GitHub 帐户
CI、CD 以及构建和发布管道
使用 CI 时,开发团队编写代码。在代码审查之后,它被集成到 VCS 中,并从那里自动构建和测试。这种情况通常一天发生多次。因此,开发团队可以快速发现问题和 bug,并尽早修复它们,从而实现通常所说的快速失效。
CD 是 CI 的自然扩展,因为它确保在构建和测试之后,每次应用修改都是可发布的。开发、测试、暂存和生产系统通过 CD 自动升级。
管道定义了完整的开发和发布工作流。它包含概念、开发、质量保证和测试所需的所有步骤,直到最终产品交付。它包括以工业化方式构建高质量应用的 CI 和 CD 过程。
Note that you can separate your development process into two different pipelines—a build pipeline and a release pipeline—or have only one single pipeline that does it all, depending on your specific needs.
有各种技术和工具可以帮助您实现基于 CI 和 CD 的高效、高效、全自动化和工业化的软件开发过程。我们将在以下示例中使用 Microsoft Azure DevOps:
将 Azure DevOps 用于 CI 和 CD
如果您需要在敏捷环境中协同工作、共享代码、规划和管理您的用户故事和开发任务、跟踪功能和 bug 的进度,那么 Azure DevOps 可能是您在云中可以找到的最佳解决方案之一。
它支持多种不同的编程语言(C#、Java、JavaScript 等)、各种开发工具(VisualStudio、Eclipse 等),并可扩展到任何团队规模。
此外,它对私人团队项目中最多五个用户是免费的,这对于尝试本书中所示的示例非常有帮助。
Azure DevOps 提供以下主要功能:
- 工作项目和看板:计划和分配工作和任务。
- 源代码管理:在 VCS 中共享代码。
- 测试:创建并执行包含测试用例的测试计划。
- Azure 工件:共享您自己的 NuGet 软件包或任何其他软件包。
- 构建管道:用于创建应用包的构建代码。
- 发布管道:将应用包部署到不同的发布目标。
For further information on Azure DevOps and all of its features, please go to https://azure.microsoft.com/en-us/services/devops/.
创建免费的 Azure DevOps 订阅和您的第一个 Azure DevOps 项目
我们现在将解释如何创建您自己的免费 Azure DevOps 订阅和您的第一个 Azure DevOps 项目。稍后,您将使用这些信息来尝试和理解本书中的示例:

- 使用您的工作、学校或个人 Microsoft 帐户登录:

-
如果您是第一次连接,请输入其他信息,如您的姓名、国家/地区和电子邮件地址,然后单击“继续”。
-
现在您的帐户已创建,让我们创建一个新项目。对于我们的示例,选择 Git 作为版本控制,单击 Change Details,然后选择 Work item process as Scrum:

- 您的新项目已经生成,现在您已经准备好创建第一个工作项和 Git 存储库,如本章所示。
在您执行任何应用之前,建议您为其制定计划。Azure DevOps 允许您创建和管理工作项,从而在这方面为您提供帮助,我们将在下一节中介绍此功能。
通过工作项组织您的工作
工作项用于在软件开发项目期间计划、分配、跟踪和组织您的工作。它们有助于更好地理解需要做什么,并提供关于项目状态的见解。
一些常见的工作项用法如下所示:
- 为应用功能创建、优先排序和跟踪用户情景。
- 创建和跟踪实现用户情景所需的开发任务。
- 创建、优先排序和跟踪应用错误。
- 确定应用质量和应用发布日期。
- 在单个看板中显示用户故事、任务和 bug 的进度。
如前所述,您可以在 Azure DevOps 项目创建期间选择工作项过程。此选项定义了可用的标准工作项类型(WITs)(敏捷、基本、CMMI 和 scrum 工作项流程)。
默认情况下,WIT 超过 14 个,您可以为高级场景创建自己的自定义 WIT。大多数情况下,您不需要创建自己的自定义智慧。
可能的工作项过程选择如下:
- Scrum,如果您的团队使用 Scrum 方法,如果您想在看板上跟踪您的产品积压项目(PBI)。
- 敏捷,如果您的团队实践敏捷方法,但不想遵守特定的 scrum 约束和术语。
- CMMI,如果您的团队需要更正式的开发任务跟进。有了它,您可以跟踪请求、更改、风险和审查。
以下是 WIT 列表,具体取决于工作项流程:
| 域 | Scrum | 敏捷 | CMMI |
| 产品规划 | PBI 缺陷 | 用户故事缺陷 | 要求改变缺陷 |
| 文件夹 | 叙事诗特色 | 叙事诗特色 | 叙事诗特色 |
| 任务和冲刺计划 | 任务 | 任务 | 任务 |
| Bug 积压管理 | 缺陷 | 缺陷 | 缺陷 |
| 问题和风险管理 | 阻碍 | 问题 | 问题危险回顾 |
在我们的示例中,我们选择使用 scrum 过程。这种方法是世界上最常用的管理和跟踪工作项的方法之一,如果我们了解 scrum,将更容易将知识应用到其他场景中。因此,我们将在下一节中介绍 scrum 的实际过程。
理解 scrum 过程
在 scrum 过程中,产品所有者创建史诗、特性和产品待办事项(相当于用户故事)。在 sprint 计划开发过程中,任务被定义并链接到产品待办事项。通过云中的看板,整个团队都可以看到:

测试人员使用 Azure DevOps web 门户或 Microsoft 测试管理器创建和执行测试用例。它们创建并分配 bug,可以跟踪代码缺陷和阻塞问题,如以下屏幕截图所示:

Azure DevOps 允许您按层次结构组织工作。您可以向上钻取、向下钻取、重新排序和修改父项,以及在层次视图中使用过滤器。
For even more information, go to https://www.visualstudio.com/en-us/docs/work/backlogs/create-your-backlog.
现在让我们更详细地了解不同的元素。史诗可以描述为一个有大量工作的大型用户故事。必须将其分解为功能和较小的产品待办事项,以便能够完全理解其需求,然后在多个 sprint 期间高效地实施:

特征将史诗分解成更小的可理解部分。它们由一组产品待办事项组成,对应于详细的预期功能:

产品待办事项是一个具有业务价值的工作单元,它足够小,可以在一次冲刺中完成。如果您不能在一个 sprint 中完成它,那么它必须被视为一个特性,并且必须进一步分解:

任务描述了在 sprint 期间实现预期产品积压项功能所需的开发或测试工作。它们链接到产品待办事项以实现可跟踪性,并且能够自动计算项目进度。
在冲刺过程中,有时完成的任务没有以正确的方式完全完成它应该做的事情,或者可能导致系统的其他部分行为不正确。这些被称为bug,包含测试人员和/或系统用户在 sprint 期间提出的问题,sprint 通常以两周为一个周期进行组织。Bug 可能会被分配到 sprint 期间解决,并链接到相应的产品待办事项:

在定义了 EPIC、特性和产品待办事项之后,您可以进行 sprint 规划,并决定在哪个迭代中需要做什么。此外,看板提供了良好的视觉表现,以便更好地理解:

可以为每个 sprint 定义每个团队成员的工作能力,工作详细信息报告允许您实时跟踪他们的工作成果:

此外,每个工作项都有一个随时间变化的状态。状态允许您跟踪工作成果并过滤工作项,以便更好地理解和检测问题。
下表显示了各种默认工作项状态,具体取决于工作项流程:
| | Scrum | 敏捷 | CMMI |
| 工作项状态 | 刚出现的经核准的坚信的多恩远离的 | 刚出现的忙碌的断然的关闭远离的 | 提出忙碌的断然的关闭 |
Please note that you do not have to follow each status, as defined for scrum, agile, or CMMI. You can customize and add in different statuses as you see fit in your specific organization. For example, there are other enterprises that decide to add in custom statuses to complement the existing steps, as follows:
- 承诺开发(开发完成,准备 QA)
- 承诺测试(QA 已完成,准备好供产品所有者演示和签字)。
您可以查询工作项、创建图表并将其发布到 Azure DevOps 项目主页。如果您需要检索特定的工作项或需要获得项目的整体视图,这是一个非常有用的特性。
现在,让我们看一下以下屏幕截图:

前面的屏幕截图显示了对其标题包含单词游戏的工作项的查询,相应的结果显示在下方窗格的同一窗口中。
使用 Git 作为 VCS
在过去几年中,Git 取得了相当大的成功,现在是开发人员社区中首选的分布式 VCS。
Azure DevOps 和 Git 之间有很好的集成,您可以使用一些强大且高效的功能(https://www.visualstudio.com/en-us/docs/work/backlogs/connect-work-items-to-git-dev-ops ),包括以下内容:
- Git 分支可以从 backlog 或看板中创建。
- 可以直接从 Azure DevOps 网站为多个工作项轻松创建 Git 功能分支。
- 拉取请求和提交自动链接到相应的工作项。
- 生成摘要页面将链接到提交的工作项显示为关联的工作项。
让我们看看如何创建新的 Git 存储库,在本地克隆它,在 Visual Studio 2019 中使用它,以及如何创建第一次提交:
- 在 Azure DevOps 项目中,单击 Repos 左侧菜单中的,然后单击 Visual Studio 中的克隆按钮:

- 将显示一个新窗口;选择 Microsoft Visual Studio Web 协议处理程序选择器:

- Visual Studio 2019 将自动启动,您可以使用您的工作、学校或个人 Microsoft 帐户进行身份验证:

- 选择本地 Git 存储库的目标文件夹,然后单击克隆按钮开始下载:

- 转到团队资源管理器-主页并单击设置:

- 在团队资源管理器-设置中,单击存储库设置:

- 在“忽略和属性文件”部分中,单击忽略文件和属性文件的“添加”:

- 返回 Team Explorer-Home,这次单击更改,为第一次提交输入注释,然后单击提交阶段按钮:

- 第一次提交是在单击提交阶段按钮时在本地创建的;单击同步链接将其推送到服务器:

- 转到 Azure DevOps 网站并单击上方菜单中的代码;您可以看到您创建的文件已上载:

就这样!您已经创建并初始化了 Git 存储库。就这么简单!从这里开始,您可以选择多条路径。例如,将所有内容保留在同一分支中并不是一个好主意,尤其是当您必须维护应用的多个版本时。
For guidance on different branching strategies, see https://docs.microsoft.com/en-us/azure/devops/repos/git/git-branching-guidance?view=azure-devops.
使用特征分支
功能分支背后的理念是,每次开始使用新的 Azure DevOps 功能(甚至 Azure DevOps 产品积压项目)时,您必须做的第一件事就是创建一个新的所谓功能分支。
然后,您将完全独立地处理这个分支,直到您准备将经过测试和验证的修改推送到主分支(或者,在更复杂的环境中,推送到开发分支)。在推送之前,它不会干扰您的其他功能,也不会导致错误或降低总体质量。
如果项目截止日期临近,而您还没有及时完成所有计划的功能,您就不需要再强调了!为什么?因为您只能集成准备发布的功能。您将拥有一个功能较少的产品,但您可以确信这些功能将按预期工作,不会有任何风险。
让我们看看如何使用 Visual Studio 2019 和 Git 创建功能分支:
- 打开 Visual Studio 2019,转到团队资源管理器-主页选项卡,然后单击分支按钮:

- 在团队资源管理器-分支中,单击“新建分支”链接:

- 输入新的要素分支名称(使用
FEA-前缀),然后单击“创建分支”按钮:

必须注意的是,我们正在使用FEA-前缀作为良好实践,以使团队的其他成员能够识别这是一个特性分支。不强制输入FEA-前缀。不同的团队对分支的命名约定不同。
合并更改和解决冲突
有时,团队成员同时处理相同的文件,导致冲突。让我们看看在这种情况下如何合并更改和解决冲突:
- 创建一个名为
HelloWorld.txt的文本文件,并将其添加到本地存储库中。将文件推送到服务器,并在服务器和本地存储库中更新文件。 - 如果您尝试推送在本地和远程存储库中都已修改的
HelloWorld.txt文件,则会收到错误消息,推送失败:

-
在输出窗口中查看时,您将获得有关推送失败的可能原因的其他信息,如下所示:错误:提示:更新被拒绝,因为远程包含本地没有的工作。这通常是由另一个存储库推送到同一个引用引起的。在再次按下之前,您可能希望首先集成远程更改(例如,
git pull)。 -
单击拉链接,您将获得远程更改,这将导致本地副本和远程副本之间发生冲突。单击“解决冲突”或“冲突”链接:

- 您现在将看到冲突文件的列表。单击要解决的冲突,然后单击“合并”按钮:

- 您将看到相互冲突的修改。选择要保留的内容(左侧、右侧或两者),然后单击“接受合并”按钮:

- 返回“团队资源管理器-解决冲突”窗口,单击“提交合并”按钮:

- 输入注释,然后单击“全部提交”按钮以在本地完成并提交合并:

- 本地创建提交后,单击同步链接,然后单击推送链接:

- 您现在应该看到更改已上载到远程存储库:

在本节中,我们已经了解了 Git 的基本用法,主要使用 visualstudio 开发环境,但还必须注意,您也可以使用命令行执行相同的命令。还有其他一些软件应用,如 GitHub Desktop、Git Extensions 等,它们被设计为帮助您与存储库进行交互,作为一种抽象,但它们都使用相同的git命令作为 VCS 的底层指令,并且它们对大多数命令都使用类似的术语。
创建 Azure DevOps 构建管道
在规划和组织您的工作并创建 Git 存储库之后,您现在应该配置 Azure DevOps 构建管道,它将允许您为您的应用执行 CI:
- 打开 Visual Studio 2019,转到团队资源管理器-主页选项卡,然后单击构建按钮:

- 接下来,单击新建生成定义链接:

- Azure DevOps 网站将打开,当您单击“新建管道”按钮,然后选择源作为 Azure Repos 时,您将看到一个构建定义模板的选择。选择 ASP.NET Core 模板:

- 在新生成定义中,输入名称,然后选择默认代理池。我们建议在 VS2019 中使用托管 Windows 2019 选项:

- 要选择源存储库,请单击“获取源”。对于我们的示例,我们使用默认值(此项目,分支:master):

- 要启用 CI,请单击生成定义菜单中的触发器,然后勾选启用持续集成复选框:

- 在验证是否正确选择了 Git 存储库和主分支后,单击 Save 或 Save&queue 按钮。配置已完成,每次代码提交到存储库时都会自动触发生成:

创建构建管道就这么简单。在构建之后,我们很自然地想要发布代码,因此我们将在下一节中研究如何创建发布管道。
创建 Azure DevOps 发布管道
随着应用的不断集成,您也看到了一些巨大的好处,例如更快地检测和修复 bug 以及其他问题。我们不要到此为止;进一步改进您的开发过程比您想象的要容易得多!
现在,我们将了解如何通过创建 Azure DevOps 发布管道来采用应用的 CD:
- 打开 Azure DevOps 网站,单击菜单中的管道,单击发布,然后单击新建定义按钮,然后选择空作业定义模板:

- 现在,您可以选择项目和源(生成管道),启用 CD,然后单击“创建”按钮:

- 将创建发布定义,您可以在列表中看到它。
所示的示例发布定义目前实际上没有太多用处。我们将在相应的 Azure 章节中看到部署到 Azure 的更高级版本。
总结
在本章中,我们了解了 CI、CD 以及构建和发布管道,包括它们的好处以及如何使用 Azure DevOps 实现它们。
我们已经创建了一个新的 Azure DevOps 订阅并初始化了一个新项目。然后,我们探讨了一些基本概念,例如用于源代码控制的工作项和 Git。最后,我们通过一个实际示例演示了如何配置 Azure DevOps 构建管道以及 Azure DevOps 发布管道。
在接下来的两章中,我们将解释 ASP.NET Core 3 的基本概念,包括启动类、使用中间件、路由、错误处理等。
四、通过自定义应用实现 ASP.NET Core 3 的基本概念:第 1 部分
在最后三章中,您已经从全局角度了解了 ASP.NET Core 3 的内容,以及如何设置开发环境,包括 Visual Studio 2019(或 Visual Studio 代码)。我们还看到了如何使用 Git 存储库在 Azure DevOps 中设置CI(简称持续集成)和CD(简称持续交付)管道。
这真的很有趣,但非常理论化。现在是时候做一些实际的事情了,是时候做正确的事情了,是时候自己动手做了!
在本章中,我们将构建一个应用来展示 ASP.NET Core 3 框架的基本概念。在接下来的章节中,我们将不断改进此应用,同时使用并演示 ASP.NET Core 3 的各种功能及其相关技术。
读完本章内容后,您将掌握使用 ASP.NET Core 3 启动类、针对不同的.NET 框架、使用中间件以及在 ASP.NET Core 3 中执行错误处理的技能。
在本章中,我们将介绍以下主题:
Startup和Program类- 创建页面和服务
- 使用节点包管理器(NPM和布局页面
- 应用依赖注入
- 使用内置中间件
- 创建自己的中间件
- 使用静态文件
- 使用路由、URL 重定向和 URL 重写
- 错误处理和模型验证
Tic Tac Toe 演示应用预览
让我们做点有趣的事吧!我们将构建一个 Tic-Tac-Toe 游戏,也称为零和叉,或 Xs 和 Os。
在我们将构建的这个游戏中,玩家将选择谁使用 Xs,谁使用 Os。然后,他们将轮流在 3×3 的网格中标记空间,每圈一个标记。在水平、垂直或对角行中成功放置三个标记的玩家将赢得游戏,如图所示:
| x | | 0 |
| 0 | x | 0 |
| x | 0 | x |
在上图中,使用 Xs 的玩家将赢得比赛,因为从左上角到右下角,对角线行中有三个十字架。
玩家必须输入他们的电子邮件和姓名进行注册,以创建一个帐户,然后才能开始游戏。他们将在每场比赛后获得一个比赛分数,该分数将被加到他们的总分中。
我们将有一个排行榜,旨在提供一系列游戏的玩家排名和最高分信息。
创建游戏时,一名玩家必须向另一名玩家发送邀请,然后会为他们显示一个特定的等待页面,直到另一名玩家做出响应。
收到邀请电子邮件后,其他玩家可以确认请求并加入游戏。当两名玩家在线时,游戏开始。
这是我们将要构建的演示应用的预览,但是 talk 很便宜。让我们从下一节开始构建它。
构建 Tic-Tac-Toe 游戏
正如在上一章中所解释的,我们可以使用 Azure DevOps 及其工作项来组织和安排 Tic-Tac-Toe 游戏应用的实现。为此,我们必须创建 epics、features 和 ProductBacklog 项,然后进行 sprint 规划,以确定优先顺序并决定首先要实现的内容。
正如您在下面的屏幕截图中所看到的,我们决定在第一个 sprint 中处理五个产品待办事项,并将它们添加到 sprint 待办事项中:

在实现任何新功能之前,您还记得接下来需要做什么吗?你不记得了?也许是树枝敲响了警钟?
在上一章中,我们展示了创建开发分支的最佳实践,这些分支是独立的,并且更易于维护和发布。它们包括在 Git 存储库中为要添加到应用中的每个新功能创建一个功能分支。
因此,每个开发人员都可以在其特定的特性分支中处理其特定的特性,直到他们决定该特性可以发布为止。
最后,所有准备发布的特性都合并到一个开发(发布或主)分支中。然后,进行集成测试,如果一切正常,则交付新的应用版本。
我们选择首先处理的功能是用户注册,因此我们要做的第一件事是创建一个名为FEA-UserRegistration的功能分支。如果您不知道如何做到这一点,您可以转到第 3 章、Azure DevOps中的持续集成管道,并获得一个完整的分步过程和详细的解释。
在 Azure DevOps 中为用户注册创建功能分支后,它将如以下屏幕截图所示:

此时,这仍然是一个空项目,只有以前提交的历史记录,以及我们用来演示如何解决冲突的HelloWorld文本文件。
我们将从以下几节开始,逐步共同创建和构建 ASP.NET Core 3 解决方案。
构思并实现您的第一个 Tic-Tac-Toe 功能
在实现用户注册功能之前,我们必须了解它,并决定一切应该如何工作。我们必须定义用户故事和工作流。为此,我们需要在预览部分更详细地分析前面提到的 Tic-Tac-Toe 游戏描述。
如前所述,只有拥有用户帐户的用户才能创建和加入游戏。要创建此帐户,用户必须输入其名字、姓氏、电子邮件地址和新密码。然后,系统将验证输入的电子邮件地址是否已注册。给定的电子邮件地址只能注册一次。如果电子邮件地址是新的,将生成用户帐户;如果已知电子邮件地址,将显示错误。
让我们看一下用户注册过程以及为了实现它而必须交互的不同组件:
- 将有一个主页,其中有一个用户注册链接,新用户必须点击注册才能创建其玩家帐户。单击用户注册链接可将用户重定向到专用注册页面。
- 注册页面将包含一份注册表格,用户必须在其中输入个人信息,然后进行确认。
- JavaScript 客户端将验证表单,提交数据并将其发送到通信中间件,然后等待结果。
- 通信中间件将接收请求并将其路由到注册服务。
- 注册服务将接收请求,验证数据的完整性,检查电子邮件是否已用于注册,并注册用户或返回错误消息。
- 通信中间件将接收结果并将其路由到等待的 JavaScript 客户端。
- 如果结果成功,JavaScript 客户端将重定向用户开始玩游戏,如果结果失败,它将显示错误消息。
下面的序列图显示了用户注册过程。通常更容易、更快速地理解具有更直观表示的流程:

为了开始,我们需要创建一个新的空 ASP.NET Core 3 web 应用,该应用将用于在本章和本书其余部分中添加各种组件和包。然后,我们将逐步添加新的概念和功能,这将使您真正了解正在发生的事情以及一切是如何工作的:
- 启动 Visual Studio 2019 并单击文件|新建|项目。
- 在.NET Core 部分中,选择 ASP.NET Core Web 应用,输入应用名称、存储库位置、解决方案名称,然后单击创建:

Note that if you have not created a Git repository for your application code yet, you can do it here by ticking the Create new Git repository checkbox.
- 选择空模板:

- 将生成一个新的空 ASP.NET Core 3 web 应用项目,其中仅包含
Program.cs和Startup.cs文件:

伟大的我们已经创建了我们的项目,现在准备实施我们的第一个功能!但是,在这样做之前,让我们花点时间看看 VisualStudio2019 在幕后为我们做了些什么。
在项目的.csproj 文件中针对不同的.NET Core 版本
对于 Visual Studio 2019 生成的每个项目,它都会创建一个相应的.csproj文件,其中包括多个项目范围的设置,例如引用的程序集、.NET Framework 目标版本、包含的文件和文件夹,以及多个其他设置。
例如,打开先前创建的 ASP.NET Core 3 项目时,可以看到以下结构:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
</Project>
您可以看到TargetFramework设置,该设置允许您定义应包含哪些.NET Framework 版本,并用于构建和执行源代码。
在我们的示例中,它被设置为netcoreapp3.0,即使用.NET Core 3 Framework 的具体值:
<TargetFramework>netcoreapp3.0</TargetFramework>
Note that you can refer to multiple .NET Framework versions within your library projects. In this case, you have to replace the TargetFramework element with the TargetFrameworks element.
例如,如果要跨越目标.NET Core 3 和.NET Core 2,则必须使用以下设置:<TargetFrameworks>netcoreapp3.0;netcoreapp2.0</TargetFrameworks>
在调试模式下点击F5键执行应用时,您可以看到应用的Debug文件夹(/bin/Debug中创建了多个文件夹和文件):

如果您更改.csproj文件并添加其他目标框架,您将看到将生成其他文件夹。然后将每个特定.NET Framework 版本的 DLL 放入相应的文件夹中:

前面的示例使用.NET Core 2 和.NET Core 3 的TargetFrameworks设置。
使用 Microsoft.AspNetCore.App 元包
在“依赖项 SDK”部分中查看解决方案资源管理器时,您可以看到一些非常有趣的内容,特别是 ASP.NET Core 3 项目:Microsoft.AspNetCore.App元包:

当您创建 ASP.NET Core 3 web 应用时,Microsoft.AspNetCore.App项目依赖项自动添加。对于这种类型的项目,默认情况下会执行此操作。
但是,Microsoft.AspNetCore.App不是标准的 NuGet 包,因为它不包含任何代码或 DLL。相反,它充当元包,引用它所依赖的其他包。更具体地说,它包括 ASP.NET Core 和 Entity Framework Core 的大多数重要包,以及它们的内部和外部依赖项,并利用了.NET Core 运行时存储。
针对.NET Core 3 SDK 或更高版本的新Microsoft.AspNetCore.App元包与针对.NET Core 早期版本的旧Microsoft.AspNetCore.All元包之间的主要区别在于App元包不再包含微软主要不支持的第三方依赖项。
如果您的项目以Microsoft.NET.Sdk.Web为目标,那么您可以自动访问共享框架,其中包括应用洞察、身份验证、授权、Azure 应用服务和许多其他的包。在旧版本的.NETCore(版本 1.0 和 1.1)中,您必须自己添加大量 NuGet 软件包。
现在,Microsoft 已经创建了 ASP.NET Core 元包的概念,您可以在一个地方找到所有内容。此外,包修剪排除了未使用的二进制文件,因此在部署应用时不会发布它们。
除了共享框架已经提供给您的功能外,当您使用 ASP.NET Core 3 框架时,还有几个类已经安装,我们将在下一节中介绍它们。
默认 ASP.NET Core 3 类简介
当您从任何模板创建 ASP.NET Core 3 应用时,无论是 MVC 应用还是空模板,默认情况下,它将始终具有Program类和Startup类。现在我们来看一下这些默认类中最重要的特性。
ASP.NET Core 3 启动类
对于您将要构建的每个 ASP.NET Core 3 应用,无论模板是 MVC 还是空模板,都会有最小的类管道,用于确保您有一个正常工作的应用。要启动 ASP.NET Core 3 应用,有两个非常重要的主要类:Program类和Startup类;以下部分将对这两个方面进行解释。
使用程序类
Program类是 ASP.NET Core 3 应用的主要入口点。事实上,ASP.NET Core 3 应用在这方面与标准的.NET Framework 控制台应用非常相似。两者都有一个在运行应用时执行的Main方法。
即使是接受字符串数组作为参数的Main方法的基本签名也是一样的,正如您在下面的代码中所看到的。毫不奇怪,这是因为 ASP.NET Core 应用实际上是托管 web 应用的控制台应用:
namespace TicTacToe
{
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>();
});
}
}
通常,您不需要以任何方式触摸Program类。默认情况下,运行应用所需的一切都已存在并已预先配置。
但是,您可能需要激活一些更高级的功能。
例如,您可以启用在服务器启动期间捕获错误并显示错误页面。在这种情况下,您只需使用以下说明:
webBuilder.CaptureStartupErrors(true);
默认情况下,此设置未启用,这意味着如果出现错误,主机将退出。这可能不是所需的行为,我们建议相应地更改此参数。
另外两个共同工作的有用参数是PreferHostingUrls和UseUrls。您可以指示主机是否应侦听由您提供的特定于Microsoft.AspNetCore.Hosting.Server.IServeror的 URL 定义的标准 URL。根据您的需要,URL 可以有不同的格式,例如:
- 带有主机和端口的 IPv4 地址(例如,
https://192.168.57.12:5000 - 带有端口的 IPv6 地址(例如,
https://[0:0:0:0:0:ffff:4137:270a]:5500 - 主机名(例如,
https://mycomputer:90) - 本地主机(例如,
https://localhost:443) - Unix 套接字(例如,
http://unix:/run/dan-live.sock)
以下是如何设置这些参数的示例:
webBuilder.PreferHostingUrls(true);
webBuilder.UseUrls("http://localhost:5000");
下面是一个Program类的示例,它包括前面显示的所有概念:
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>();
webBuilder.CaptureStartupErrors(true);
webBuilder.PreferHostingUrls(true);
webBuilder.UseUrls("http://localhost:5000");
});
}
Program类中的默认代码来自 ASP.NET Core 版本 2.1,包括版本 3。以前的版本使用了WebHostBuilder,而不是我们将在下一节介绍的通用 web 主机。
使用.NET 通用主机而不是 WebHostBuilder
在 ASP.NET Core 2.1 之前,Main方法将主机定义为WebHostBuilder,如下所示:
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseStartup<Startup>()
.Build();
host.Run();
}
这意味着每个应用都被绑定为 web 应用。ASP.NET Core 2.1 引入了通用主机,它允许应用不必处理基于 web 的 HTTP 请求。
还有其他一些应用,如消息传递和后台应用,在这些应用中,像以前一样与WebHostBuilder抽象绑定是没有意义的,因此,引入了一种更通用的HostBuilder程序初始化抽象。其原始形式的示例如下所示:
public static Task Main(string[] args)
{
var host = new HostBuilder()
.Build();
host.Run();
}
以前版本的 ASP.NET 在Main方法中有一个CreateWebHostBuilder()方法,该方法正在被 ASP.NET Core 3 中的CreateHostBuilder()方法所取代,我们现在没有使用WebHost.CreateDefaultBuilder(args)方法,而是使用Host.CreateDefaultBuilder(args)。
使用 Startup 类
另一个自动生成的元素是Startup类,它存在于所有类型的 ASP.NET Core 3 项目中。如前所述,Program类主要处理与托管环境相关的所有内容。Startup类是关于服务和中间件的预加载和配置。这两个类是所有 ASP.NET Core 3 应用的基础。
现在让我们看一下Startup类的基本结构,以便更好地了解所提供的内容以及如何充分利用其功能:
public class Startup
{
public void ConfigureServices(IServiceCollection services) { }
public void Configure(IApplicationBuilder app,
IHostingEnvironment env)
{
if (env.IsDevelopment())
{ app.UseDeveloperExceptionPage(); }
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
}
有两种方法需要您注意,因为您将定期使用它们:
ConfigureServices方法,由运行时调用,用于向容器添加服务- 用于配置 HTTP 管道的
Configure方法
我们在本章开始时说过,我们需要更多的实际工作,所以让我们回到我们的 Tic-Tac-Toe 游戏,看看如何在一个真实的例子中使用Startup类!
我们将使用 MVC 实现该应用,但是,由于您使用了空的 ASP.NET Core 3.0 Web 应用模板,因此 Visual Studio 2019 在项目生成期间未添加任何内容。你必须自己添加所有内容;这是一个很好的机会,可以更好地了解一切是如何运作的!
首先要做的是将 MVC 添加到服务配置中。通过使用ConfigureServices方法,只需添加 MVC 中间件即可:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
}
你可能会说这太容易了;那么,有什么问题吗?没有陷阱!ASP.NET Core 3 中的所有内容都是围绕着简单性、清晰性和开发人员生产力而开发的。
在配置 MVC 中间件和设置路由路径时,您可以再次看到这一点(稍后我们将更详细地解释路由):
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
同样,我们这里有非常清晰和简短的说明,使我们作为开发人员的生活更加轻松和高效。现在是做开发人员的好时机!
在下一步中,您需要在 ASP.NET Core 3 应用中启用静态内容的使用,以便使用 HTML、CSS、JavaScript 和图像。
你知道怎么做吗?是的,你是对的;您需要添加另一个中间件。您可以像以前一样通过调用相应的app方法:
app.UseStaticFiles();
下面是一个Startup.cs类的示例,在配置了前面看到的各种服务设置后,您可以将其用于 Tic Tac Toe 游戏:
public class Startup {
public void ConfigureServices(IServiceCollection services)
{ services.AddControllersWithViews(); }
public void Configure(IApplicationBuilder app,
IHostingEnvironment env){
if (env.IsDevelopment())
app.UseDeveloperExceptionPage();
else
app.UseExceptionHandler("/Home/Error");
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
请注意,有许多其他服务可以添加到ConfigureServices方法中,例如 ASP.NET Core 框架已经提供的services.AddAuthorization()或services.AddAspnetCoreIdentity(),这些服务确实可以由您自己创建。这里配置的任何内容都可以通过依赖注入(DI)在您的整个应用中访问,我们在第 5 章、ASP.NET Core 3 的基本概念中通过自定义应用专门介绍了这一部分:第 2 部分。
现在我们已经了解了默认情况下作为 ASP.NET Core 应用启动类存在的类,现在是了解基本项目结构的好时机,如下一节所述。
准备基本项目结构
在构建了 Tic-Tac-Toe 游戏应用之后,您肯定希望看到一些东西在运行。现在,我们已经从功能的角度定义了一切应该如何工作,我们需要从创建应用的基本项目结构开始。
对于 ASP.NET Core 3 web 应用,最佳做法是为项目提供以下结构:
- 一个
Controllers文件夹,包含应用的所有控制器。 - 一个
Services文件夹,包含应用的所有服务(例如,外部通信服务)。 - 一个
Views文件夹,包含应用的所有视图。此文件夹应包含单个Shared子文件夹以及每个控制器一个文件夹。 - 一个
_ViewImports.cshtml文件,用于定义在所有视图中可用的某些名称空间。 - 一个
_ViewStart.cshtml文件,用于定义在每次视图渲染开始时要执行的一些代码(例如,为所有视图设置布局页面)。 - 一个
_Layout.cshtml文件,用于为所有视图定义通用布局。
让我们创建项目结构:
- 启动 Visual Studio 2019,打开您创建的 Tic Tac Toe ASP.NET Core 3 项目,创建三个名为
Controllers、Services和Views的新文件夹,然后在Views文件夹中创建一个名为Shared的子文件夹:

- 在
Views文件夹中创建名为_ViewImports.cshtml的新视图页面:
@using TicTacToe
@addTagHelper*, Microsoft.AspNetCore.Mvc.TagHelpers
- 在
Views文件夹中创建名为_ViewStart.cshtml的新视图页面:
@{ Layout = "~/Views/Shared/_Layout.cshtml"; }
- 右键点击
Views/Shared文件夹,选择添加新项目,在搜索框中输入Layout,选择 MVC 视图布局页面,然后点击添加:

Note that the layout page concept will be detailed a little bit later in this chapter, but don't worry too much; it is not a very complicated concept.
创建 Tic-Tac-Toe 主页
由于基本的项目结构已经到位,我们需要实现需要协同工作的不同组件,以提供 Tic Tac Toe 游戏 web 应用:
-
如前所述,更新
Program.cs和Startup.cs文件。 -
添加新控制器,在
Controllers文件夹的解决方案资源管理器中单击鼠标右键,然后选择“添加|控制器…”:

- 在添加脚手架弹出窗口中,选择 MVC 控制器-空,并将新控制器命名为
HomeController:

- MVC 主控制器自动生成,包含一个方法。您现在需要通过右键单击
Index方法名称并选择添加视图来添加相应的视图。。。从菜单中:

- “添加视图”窗口有助于定义需要生成的内容。保留默认的空模板,并启用我们将在本章下一节中修改的布局页面:

- 祝贺您的视图将自动生成,您可以通过按F**5或单击 Visual Studio 2019 菜单上的调试来测试您的应用,然后开始调试。我们将在本章后面通过添加更多相关内容来最终确定该视图。
前面生成的视图看起来很简单,可能无法在 web 应用上提供最佳的用户体验。为了帮助确保我们创建更有意义的内容,并且系统性地创建内容,我们将使用布局页面和 NPM 来引入包,这些包将有助于提升使用 ASP.NET Core 3 构建的应用的外观和感觉。让我们在下一节中看看如何做到这一点。
通过使用 NPM 和布局页面,为您的网页提供更现代的外观
我们刚刚了解了如何创建基本网页。知道如何在技术上做到这一点是一回事,但创建成功的 web 应用则完全是另一回事。它不仅涉及技术实现,还涉及如何使应用在视觉上具有吸引力和用户友好性。虽然这本书不是关于网页设计和用户体验的,但我们想为您提供一些快速简便的方法,以便在这方面构建更好的 web 应用。
为此,我们建议使用 NPM(https://www.npmjs.com/ ),web 上最常用的包管理器,与 ASP.NET Core 布局页面结合使用。
在过去几年中,NPM 在 web 开发社区取得了一些显著的成功。它有助于安装包含静态内容(如 HTML、CSS、JavaScript、字体和图像,包括它们的依赖项)的客户端软件包。
Visual Studio 2019 中的 NPM 提供了一些强大的集成和支持;为了有效地使用它,您只需正确地配置它。让我们看看如何做到这一点:
- 右键点击 Tic Tac Toe 项目,选择添加新项目,在搜索框中输入
NPM,选择 npm 配置文件,点击添加:

- 添加 npm 配置文件时应添加一个
package.json文件。使用以下内容更新此文件:
{
"version": "1.0.0",
"name": "asp.net",
"private": true,
"devDependencies": {
"bootstrap": "4.3.1",
"jquery": "3.3.1",
"jquery-validation": "1.17.0",
"jquery-validation-unobtrusive": "3.2.11",
"popper.js": "1.14.7"
}
}
- 构建项目,成功构建后,将有一个名为
npm的文件夹,该文件夹将在依赖项下创建。右键单击依赖项,然后单击还原包:

- 然后将客户端软件包(
bootstrap、jquery等)下载到您定义的文件夹中,默认情况下,该文件夹将为(wwwroot/lib。现在可以在应用中使用静态内容:

- 在
wwwroot文件夹中,创建一个名为css的文件夹。在此文件夹中添加名为site.css的新样式表:
body {
padding-top: 50px;
padding-bottom: 20px;
}
.body-content {
padding-left: 15px;
padding-right: 15px;
}
/* Set width on the form input elements since they're
100% wide by default */
input,
select,
textarea {
max-width: 280px;
}
前面的 CSS 样式代码通过设置填充以防止内容触及边缘,并设置我们将用于输入区域的自定义宽度(例如,用户将在其中键入他们的姓名和其他详细信息以进行注册),使我们的视图更具表现力。
对于验证样式,将以下代码添加到相同的site.css文件中:
.field-validation-error {
color: #b94a48;
}
.field-validation-valid {
display: none;
}
input.input-validation-error {
border: 1px solid #b94a48; }
input[type="checkbox"].input-validation-error {
border: 0 none; }
.validation-summary-errors {
color: #b94a48; }
.validation-summary-valid {
display: none;
}
这将有助于我们获得具有正确外观和感觉的验证消息,并使用自定义颜色,使用户将自动识别出发生了错误。您可能听说过术语UX 设计,这是UX(简称用户体验)的一个简单示例,您将不得不为您将要构建的大多数应用考虑这些因素。
一个成功的 web 应用在从一页导航到另一页时应该有一个具有一致用户体验的通用布局。这是用户采用和用户满意度的关键。ASP.NET Core 布局页是解决这一问题的正确解决方案。
它们可用于为 web 应用中的视图定义模板。所有视图都可以使用相同的模板,也可以使用不同的模板,具体取决于您的具体需要。
更新布局页面
转到_Layout.cshtml中的head部分,添加以下代码段:
<meta name="viewport" content="width=device-width,
initial-scale=1.0" />
<title>@ViewData["Title"] - TicTacToe</title>
<environment include="Development">
<link rel="stylesheet"
href="~/lib/bootstrap/dist/css/bootstrap.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn
.com/bootstrap/4.3.1/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css
/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-
property="position" asp-fallback-test-value="absolute"
crossorigin="anonymous"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784
/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"/></environment>
<link rel="stylesheet" href="~/css/site.css" />
创建一个带有以下标记的body部分<body></body>,并在主体内创建一个带有以下代码段的header导航栏:
<header><nav class="navbar navbar-expand-sm navbar-toggleable-sm
navbar-light bg-white border-bottom
box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-
action="Index">TicTacToe</a>
<button class="navbar-toggler" type="button" data-toggle=
"collapse" data-target=".navbar-collapse" aria-controls=
"navbarSupportedContent" aria-expanded="false" aria-label=
"Toggle navigation"> <span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-
reverse">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-
controller="Home" asp- action="Index">Home</a> </li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-
controller="Home" asp-action="Privacy">
Privacy</a></li></ul></div></div></nav></header>
在同一body区段内,在导航区段外,在关闭</header>标签后,添加集装箱主体内容,如下所示:
<div class="container body-content">
<main role="main" class="pb-3">
@RenderBody()
</main>
<footer class="border-top footer text-muted">
<div class="container">
© 2019 - TicTacToe - <a asp-area="" asp-
controller="Home" asp-action="Privacy">Privacy</a>
</div>
</footer>
</div>
然后,在body内容之后,在底部添加以下将在开发环境中使用的引用:
<environment include="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<script src="~/js/site.js" asp-append-version="true"></script>
@RenderSection("Scripts", required: false)
此外,对于生产环境,我们将不在本书的范围内使用,我们将有类似于以下代码段的内容:
<environment exclude="Development">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery
/3.3.1/jquery.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o
/NMZxltwRo8QtmkMRdAu8=">
</script>
<script src="https://stackpath.bootstrapcdn.com
/bootstrap/4.3.1/js/bootstrap.bundle.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js
/bootstrap.bundle.min.js"
asp-fallback-test="window.jQuery &&
window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-xrRywqdh3PHs8keKZN+8zzc5TX0GRT
LCcmivcbNJWm2rs5C8PRhcEn3czEjhAO9o">
</script>
</environment>
对于我们的示例应用,我们将使用前面一系列代码片段更新的布局页面。
在下一节中创建用户注册页面之前,让我们先更新之前创建的主页,以显示有关 Tic Tac Toe 游戏的一些基本信息,同时使用前面显示的布局页面:
@{ ViewData["Title"] = "Home Page";
Layout = "~/Views/Shared/_Layout.cshtml"; }
<div class="row">
<div class="col-lg-12">
<h2>Tic-Tac-Toe</h2>
<div class="alert alert-info">
<p>Tic-Tac-Toe is a two-player turn-based game.</p>
<p>Two players will choose who takes the Xs and who takes the Os.
They will then be taking turns and mark spaces in a
3×3 grid by putting their marks, one mark per turn.</p>
<p>A player who succeeds in placing three of his arks in a
horizontal, vertical, or diagonal row wins the
game.</p>
</div>
<p><h3>Register by clicking <a asp-controller="User
Registration"asp-view="Index">here</a></h3</p>
</div>
</div>
启动应用时,您将看到一个新的主页设计,其中包含先前添加的文本,如下所示:

您可以在屏幕截图中看到,我们有一个链接,允许我们的应用用户注册,以便他们可以玩我们的游戏。因此,就如何创建注册页面而言,我们现在正处于一个良好的阶段。我们将在下一节中这样做。
创建 Tic Tac Toe 用户注册页面
现在,您将集成第二个组件,即用户注册页面及其表单,这将允许新用户注册以玩 Tic Tac Toe 游戏:
- 将名为
Models的新文件夹添加到项目中。 - 通过右键单击项目中的
Models文件夹并选择 Add | Class 并将其命名为UserModel来添加新模型:
public class UserModel
{
public Guid Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public bool IsEmailConfirmed { get; set; }
public System.DateTime? EmailConfirmationDate { get;
set; }
public int Score { get; set; }
}
- 添加一个新控制器并将其命名为
UserRegistrationController(如果您不知道如何操作,请参阅创建 Tic Tac Toe 主页部分)。 - 右键单击名为
Index的方法并选择添加视图。这一次,选择创建模板,选择 UserModel 作为模型类,如前一点所述,并启用布局页面的使用:

Note that you can leave the layout page empty if you want to use the _ViewStart.cshtml file in the Shared folder to define a unified common layout for all your views.
_ViewStart.cshtml文件用于在视图之间共享设置,_ViewImports文件用于共享using名称空间和注入 DI 实例。Visual Studio 2019 为这些文件提供了两个模板。
- 从视图中删除自动生成的
Id、IsEmailConfirmed、EmailConfirmationDate和Score元素;我们不需要他们的用户注册表。 - 视图现在已准备就绪;按F5键,点击首页注册链接显示:

您将看到一个表单,该表单可用于填写用户详细信息,如姓名、姓氏、电子邮件和密码。请注意,输入字段较短;准确地说,它们有 280 像素长,这是因为我们在上一节中使用了 CSS 样式;否则,它们将跨越屏幕的整个长度。
我们已经完成了用户注册页面,但是您会很快注意到用户是应用的核心部分。用户将必须登录和注销,以不同的方式进行验证,并且除了注册之外,还将具有其他功能。毫不奇怪,我们将需要一个用户服务来协调用户正在发生的一切与应用的其余部分。我们将在下一节中创建一个用户服务。
创建 Tic-Tac-Toe 用户服务
开发人员在开发应用时面临的最大问题之一是组件间的依赖关系。这些依赖关系使得单独维护和发展组件变得困难,因为修改可能会对其他依赖组件产生不利影响。对于我们的演示应用,我们希望确保能够更新和修改单个组件或服务,而不需要去更改其他依赖组件。
但是,请放心,有一些机制允许分解这些依赖关系,其中之一就是 DI。
在提供松耦合的同时,DI 允许组件一起工作。一个组件只需要知道由另一个组件实现的契约就可以使用它。对于 DI 容器,组件不会直接实例化,静态引用也不会用于查找另一个组件的实例。相反,DI 容器负责在运行时检索正确的实例。
当一个组件的设计考虑到 DI 时,默认情况下它是非常进化的,并且不依赖于任何其他组件或行为。例如,身份验证服务可以使用提供程序进行使用 DI 的身份验证,如果添加了新的提供程序,现有的提供程序将不会受到影响。
使用 DI 鼓励松耦合
ASP.NET Core 3 包含一个非常简单的内置 DI 容器,它支持构造函数注入。要使服务可用于容器,必须将其添加到Startup类的ConfigureService方法中。在不知情的情况下,您以前已经为 MVC 做过:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
}
事实上,您必须为自己的定制服务做同样的事情;您必须在此方法中声明它们。当你知道自己在做什么时,这真的很容易做到!
但是,有多种注入服务的方法,您需要选择最适合您需要的方法:
- 瞬时注入:每次调用该方法时创建一个实例(例如,无状态服务):
services.AddTransient<IExampleService, ExampleService>();
- 作用域注入:每个请求管道创建一个实例(例如,有状态服务):
services.AddScoped<IExampleService, ExampleService>();
- 单例注入:为整个应用创建一个实例:
services.AddSingleton<IExampleService, ExampleService>();
Note that you should add the instances for your services by yourself if you do not want the container to automatically dispose of them. The container will call the Dispose method of each service instance it creates by itself.
下面是一个如何自己实例化服务的示例:
services.AddSingleton(new ExampleService());
现在您已经了解了如何使用 DI,让我们应用您的知识,为示例应用创建下一个组件。
创建用户服务
我们已经创建了一个主页以及一个用户注册页面。用户可以点击注册链接填写注册表格,但表格数据尚未以任何方式处理。我们将添加一个用户服务,该服务将负责处理与用户相关的任务,例如用户注册请求。此外,您还将应用刚才学习的一些 ASP.NET Core 3 DI 机制:
-
将名为
UserService.cs的新类添加到Services文件夹中。 -
添加新的用户注册方法,并检查用户是否在线,将上一节中创建的模型作为参数:
using TicTacToe.Models;
public class UserService
{
public Task<bool>RegisterUser(UserModel userModel)
{
return Task.FromResult(true);
}
public Task<bool> IsOnline(string name)
{
return Task.FromResult(true);
}
}
- 右键单击该类并选择快速操作和重构,然后单击提取接口…:

- 将所有默认值保留在弹出窗口中,然后单击“确定”:

- Visual Studio 2019 将生成一个名为
IUserService.cs的新文件,其中包含提取的接口定义,如下所示:
public interface IUserService
{
Task<bool>RegisterUser(UserModeluserModel);
Task<bool> IsOnline(string name);
}
- 更新之前创建的
UserRegistrationController并应用构造函数注入机制:
using TicTacToe.Services;
public class UserRegistrationController : Controller
{
private IUserService _userService;
public UserRegistrationController(IUserService
userService)
{
_userService = userService;
}
public IActionResult Index()
{
return View();
}
}
- 在
UserRegistrationController中添加一些处理用户注册的简单代码(我们将在本章后面添加验证):
[HttpPost]
public async Task<IActionResult> Index(UserModel
userModel)
{
await _userService.RegisterUser(userModel);
return Content
($"User {userModel.FirstName} {userModel.LastName}
has been registered successfully");
}
- 转到
Startup类并在ConfigureServices方法中声明UserService以使其可供应用使用:
using TicTacToe.Services;
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddSingleton<IUserService, UserService>();
}
- 按F5,填写注册页面,然后单击“确定”,测试您的应用。你应该得到一个
User has been registered successfully输出。
至此,您已经创建了 Tic Tac Toe 应用的多个组件,这是一个非常好的进展!请保持关注,因为下一节非常重要,因为它详细解释了中间件。
为 Tic-Tac-Toe 应用创建基本通信中间件
如前所述,Startup类负责在 ASP.NET Core 3 应用中添加和配置中间件。但什么是中间件?何时、如何使用它,以及如何创建自己的中间件?这些都是我们现在要讨论的问题。
实际上,多个中间件构成了 ASP.NET Core 应用的功能。正如您可能已经注意到的,即使是最基本的功能,例如提供静态内容,也由它们执行。
使用中间件
中间件是 ASP.NET Core 3 请求管道的一部分,用于处理请求和响应。当它们链接在一起时,它们可以将传入的请求从一个传递到另一个,并在管道中调用下一个中间件之前和之后执行操作:

使用中间件可以使您的应用更加灵活和进化,因为您可以在Startup类的Configure方法中轻松添加和删除中间件。
此外,您在Configure方法中调用中间件的顺序就是调用它们的顺序。建议按以下顺序调用中间件,以确保更好的性能、功能和安全性:
- 异常处理中间件
- 静态文件中间件
- 认证中间件
- MVC 中间件
如果不按此顺序调用它们,可能会出现一些意外行为甚至错误,因为中间件操作可能在请求管道中应用得太迟或太早。
例如,如果不首先调用异常处理中间件,则可能无法捕获其调用之前发生的所有异常。另一个例子是在静态文件中间件之后调用响应压缩中间件。在这种情况下,静态文件将不会被压缩,这可能不是期望的行为。因此,注意中间件调用的顺序;它可以带来巨大的不同。
以下是一些可以在应用中使用的内置中间件(列表并不详尽;还有更多):
| 认证 | OAuth 2 和 OpenID 身份验证,基于最新版本的 IdentityModel |
| 科尔斯 | 基于 HTTP 头的跨源资源共享保护 |
| 响应缓存 | HTTP 响应缓存 |
| 响应压缩 | HTTP 响应 gzip 压缩 |
| 路由 | HTTP 请求路由框架 |
| 一场 | 基本本地和分布式会话对象管理 |
| 静态文件 | HTML、CSS、JavaScript 和图像支持,包括目录浏览 |
| URL 重写 | URL SEO 优化和重写 |
内置中间件将足以满足最基本的需求和标准用例,但您肯定需要创建自己的中间件。有两种方法可以做到这一点:在Startup类中内联创建它们,或者在自包含类中创建它们。
让我们先看看如何定义内联中间件。以下是可用的方法:
RunMapMapWhenUse
Run方法用于添加中间件并立即返回响应,从而使请求管道短路。它不调用以下任何中间件,并结束请求管道。因此,建议将其放在中间件调用的末尾(参考前面讨论的中间件排序)。
Map方法允许在请求路径以特定路径开始时执行某个分支并添加相应的中间件,这意味着您可以有效地对请求管道进行分支。
MapWhen方法提供的分支请求管道和添加特定中间件的概念基本相同,但可以控制分支条件,因为它基于Func<HttpContext, bool>谓词的结果。
Use方法添加了中间件,并允许在线调用下一个中间件或短接请求管道。但是,如果您希望在执行特定操作后传递请求,则必须使用next.Invoke和当前上下文作为参数手动调用下一个中间件。
下面是一些如何使用这些扩展方法的示例,首先是使用ApiPipeline和WebPipeline:
private static void ApiPipeline(IApplicationBuilder app) {
app.Run(async context =>
{
await context.Response.WriteAsync("Branched to Api
Pipeline.");
}); }
private static void WebPipeline(IApplicationBuilder app) {
app.MapWhen(context =>
{
return context.Request.Query.ContainsKey("usr");
}, UserPipeline);
app.Run(async context =>
{
await context.Response.WriteAsync("Branched to Web
Pipeline.");
}); }
然后,有UserPipeline和Configure方法,它们利用创建的管道:
private static void UserPipeline(IApplicationBuilder app) {
app.Run(async context =>
{
await context.Response.WriteAsync("Branched to User
Pipeline.");
}); }
public void Configure(IApplicationBuilder app,
IHostingEnvironmentenv) {
app.Map("/api", ApiPipeline); app.Map("/web", WebPipeline);
app.Use(next =>async context =>
{
await context.Response.WriteAsync("Called Use.");
await next.Invoke(context); });
app.Run(async context =>
{
await context.Response.WriteAsync("Finished with Run.");
}); }
如前所示,您可以内联创建中间件,但对于更高级的场景,不建议这样做。在这种情况下,我们建议您将中间件放在自包含类中,这样做的过程非常简单。中间件只是通过扩展方法公开的具有特定结构的类。
创建通信中间件
让我们执行以下步骤:
- 在项目中创建一个名为
Middleware的新文件夹,然后添加一个名为CommunicationMiddleware.cs的新类,代码如下:
using Microsoft.AspNetCore.Http;
using TicTacToe.Services;
public class CommunicationMiddleware
{
private readonly RequestDelegate _next;
private readonly IUserService _userService;
public CommunicationMiddleware(RequestDelegate next,
IUserService userService)
{
_next = next;
_userService = userService;
}
public async Task Invoke(HttpContext context)
{
await _next.Invoke(context);
}
}
- 在项目中创建一个名为
Extensions的新文件夹,然后添加一个名为CommunicationMiddlewareExtension.cs的新类,代码如下:
using Microsoft.AspNetCore.Builder;
using TicTacToe.Middleware;
public static class CommunicationMiddlewareExtension
{
public static IApplicationBuilder
UseCommunicationMiddleware(this IApplicationBuilder app)
{
return app.UseMiddleware<CommunicationMiddleware>();
}
}
- 在
Startup类中为TicTacToe.Extensions添加using指令,然后将通信中间件添加到Configure方法中:
using TicTacToe.Extensions;
...
public void Configure(IApplicationBuilder app,
IHostingEnvironment env)
{
...
app.UseCommunicationMiddleware();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}
/{id?}");
endpoints.MapRazorPages();
});
}
- 在通信中间件实现中设置一些断点,按F5启动应用。您将看到,如果一切正常,断点将被命中:

这只是如何创建自己的中间件的一个基本示例;此部分与其他部分之间没有可见的功能更改。在接下来的章节中,您将进一步实现各种功能,以完成 Tic-Tac-Toe 应用,本章中介绍的通信中间件不久将做一些实际工作。
使用静态文件
在使用 web 应用时,大多数情况下,您必须使用 HTML、CSS、JavaScript 和图像,ASP.NET Core 3 将这些文件视为静态文件。
默认情况下,无法访问这些文件,但在本章开头,您看到了允许在应用中使用静态文件需要做些什么。事实上,您必须将相应的中间件添加并配置到Startup类中,才能为静态文件提供服务:
app.UseStaticFiles();
Note that, by default, all static files served by this middleware are public and anyone can access them. If you need to protect some of your files, you need to either store them outside the wwwroot folder or you need to use the FileResult controller action, which supports the authorization middleware.
此外,出于安全原因,默认情况下禁用目录浏览。但是,如果需要允许用户查看文件夹和文件,则可以轻松激活它:
- 调用
AddControllersWithViews()方法后,立即将DirectoryBrowsingMiddleware添加到Startup类的ConfigureService方法中:
services.AddDirectoryBrowser();
- 在
Startup类的Configure方法中,调用UseDirectoryBrowser方法(调用UseCommunicationMiddleware方法后)激活目录浏览:
app.UseDirectoryBrowser();
前面的代码允许我们从浏览器查看以下根文件夹:

- 从
Startup类中删除对UseDirectoryBrowser方法的调用;对于示例应用,我们不需要它。
使用路由、URL 重定向和 URL 重写
在构建应用时,路由用于将传入请求映射到路由处理程序(URL 匹配)并为响应生成 URL(URL 生成)。
ASP.NET Core 3 的路由功能结合并统一了以前存在的 MVC 和 web API 的路由功能。它们已经从头开始重建,以创建一个通用的路由框架,在一个地方具有所有不同的功能,可用于所有类型的 ASP.NET Core 3 项目。
现在,让我们看看路由在内部是如何工作的,以便更好地理解它在您的应用中是如何有用的,以及如何将它应用到我们的 Tic-Tac-Toe 应用示例中。
对于收到的每个请求,将根据请求 URL 检索匹配的路由。管线将按照它们在管线集合中出现的顺序进行处理。
更具体地说,传入的请求被分派到相应的处理程序。大多数情况下,这是基于 URL 中的数据完成的,但是您也可以使用请求中的任何数据来实现更高级的场景。
如果您使用的是 MVC 中间件,您可以在Startup类中定义和创建路由,如本章开头所示。这是开始 URL 匹配和 URL 生成的最简单方法:
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
还有一个专用的路由中间件,您可以使用它在应用中处理路由,这在前面关于中间件的部分中已经看到。您只需将其添加到Startup类:
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting();
}
下面是一个如何使用它调用Startup类中的UserRegistration服务的示例。
首先,我们将UserService和路由添加到ServiceCollection:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddSingleton<IUserService, UserService>();
services.AddRouting();
}
然后我们在Configure方法中使用它们如下:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{ app.UseStaticFiles();
var routeBuilder = new RouteBuilder(app);
routeBuilder.MapGet("CreateUser", context =>
{ var firstName = context.Request.Query["firstName"];
var lastName = context.Request.Query["lastName"];
var email = context.Request.Query["email"];
var password = context.Request.Query["password"];
var userService = context.RequestServices.
GetService<IUserService>();
userService.RegisterUser(new UserModel { FirstName =
firstName, LastName = lastName, Email = email,
Password = password });
return context.Response.WriteAsync($"User {firstName} {lastName}
has been successfully created.");
});
var newUserRoutes = routeBuilder.Build();
app.UseRouter(newUserRoutes);
app.UseCommunicationMiddleware();
app.UseStatusCodePages("text/plain", "HTTP Error - Status Code:
{0}"); }
如果使用一些查询字符串参数调用它,将得到以下结果:

另一个重要的中间件是URL 重写中间件。它提供 URL 重定向和 URL 重写功能。然而,这两者之间有一个关键的区别,你需要理解。
URL 重定向需要往返到服务器,并在客户端完成。客户端首先收到一个永久移动的301或临时移动的302HTTP 状态码,表示要使用的新重定向 URL。然后,客户机调用新的 URL 来检索请求的资源,因此它将对客户机可见。
另一方面,URL 重写纯粹是服务器端的。服务器将从不同的资源地址内部检索请求的资源。客户端将不知道该资源是从另一个 URL 提供的,因为它对客户端不可见。
回到 Tic Tac Toe 应用,我们可以使用 URL 重写为注册新用户提供更有意义的 URL。我们可以使用更短的 URL,而不是使用UserRegistration/Index,例如/NewUser:
var options = new RewriteOptions()
.AddRewrite("NewUser", "/UserRegistration/Index", false);
app.UseRewriter(options);
这里,用户认为该页面是从/NewUser开始服务的,而实际上是从/UserRegistration/Index开始服务的,用户没有注意到:

当您希望 URL 有意义时,这对于应用上的用户体验非常有用,并且可以在搜索引擎优化中发挥作用,在搜索引擎优化中,web 爬虫匹配 URL 和页面内容非常重要。
ASP.NET Core 3 的终结点路由
端点路由,在其早期概念中也称为调度程序,在 ASP.NET Core 的 2.2 版中引入,默认情况下,推荐用于 ASP.NET Core 3。
如果您使用过 ASP.NET Core 2.0 及更早版本,您会发现大多数应用要么使用RouteBuilder,如前面的示例所示,要么使用路由属性(如果您正在开发 API),我们将在后面的一章中讨论。您将熟悉UseMVC()和/或UseRouter()方法,这些方法在 ASP.NET Core 3 中仍然有效,但端点路由的设计允许开发人员使用不打算使用 MVC 的应用,并且仍然使用路由来处理请求。
以下是在 ASP.NET Core 之前的应用中的Startup类Configure方法中通常可以找到的示例:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "
{controller=Home}/{action=Index}/{id?}");
});
我们必须注意,在使用app.UseMvc或Configure方法中的app.UseRouting之前,我们必须在ConfigureServices方法中定义services.AddMvc()。
将其与 ASP.NET Core 3 应用中的默认实现进行比较,如下所示:
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
endpoints.MapControllers();
});
在这个实现中,我们使用端点而不是 MVC,因此我们不需要专门将 MVC 添加到ConfigureServices方法中,这本身就使得这个实现更轻量级,减少了 MVC 带来的开销,当我们构建不一定需要遵循 MVC 架构的应用时,这一点非常重要。
我们的应用开始增长,遇到错误的机会也在增加。在下一节中,让我们看看如何将错误处理添加到应用中。
向 Tic-Tac-Toe 应用添加错误处理
在开发应用时,问题不是是否会发生错误和 bug,而是何时会发生。构建应用是一项非常复杂的任务,几乎不可能考虑运行时可能发生的所有情况。即使你认为你已经考虑了所有的事情,那么环境并没有按照预期的那样运行;例如,服务不可用,或者处理请求的时间比预期的要长得多。
这个问题有两种解决方案,需要同时应用单元测试和错误处理。从应用的角度来看,单元测试将确保开发期间的正确行为,而错误处理将帮助您在运行时为环境问题做好准备。在本节中,我们将研究如何向 ASP.NET Core 3 应用添加有效的错误处理。
默认情况下,如果根本没有错误处理,并且发生异常,则应用将停止,用户将无法再使用它,并且在最坏的情况下,服务将中断。
开发期间要做的第一件事是激活默认的开发异常页面;它显示发生的异常的详细信息。您已经在本章开头了解了如何执行此操作:
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
在默认开发异常页面上,您可以深入了解原始异常详细信息以分析堆栈跟踪。您有多个选项卡,可以查看查询字符串参数、客户端 cookie 和请求头。
这些都是一些强有力的指标,可以让你更好地理解发生了什么以及为什么会发生。它们应该可以帮助您在开发期间更快速地发现问题并解决问题。
以下是发生异常时发生的情况的示例:

但是,不建议在生产环境中使用默认的“开发异常”页面,因为它包含了太多有关系统的信息,这些信息可能会危害系统。
对于生产环境,建议配置带有静态内容的专用错误页。在以下示例中,您可以看到默认的开发异常页面在开发期间使用,如果应用配置为在非开发环境中运行,则会显示特定的错误页面:
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
默认情况下,400和599之间的 HTTP 错误代码不显示信息。例如,这包括404(未找到)和500(内部服务器错误)。用户只会看到一个空白页面,这不是很友好。
您应该激活Startup类中特定的UseStatusCodePages中间件。这将帮助您自定义在这种情况下需要显示的内容。有意义的信息将帮助用户更好地理解应用中发生的事情,并将带来更好的客户满意度。
最基本的配置可能是只显示一条文本消息:
app.UseStatusCodePages("text/plain", "HTTP Error - Status Code:
{0}");
前面的代码生成以下内容:

但是,你可以走得更远。例如,您可以重定向到特定 HTTP 错误状态代码的特定错误页。
以下示例显示如何将移动的临时302(已找到)HTTP 状态代码发送到客户端,然后将其重定向到特定的错误页面:
app.UseStatusCodePagesWithRedirects("/error/{0}");
此示例显示如何将原始 HTTP 状态代码返回到客户端,然后将其重定向到特定的错误页:
app.UseStatusCodePagesWithReExecute("/error/{0}");
You can disable HTTP status code pages for specific requests as shown here:
var statusCodePagesFeature =
context.Features.Get<IStatusCodePagesFeature>();
if (statusCodePagesFeature != null)
{
statusCodePagesFeature.Enabled = false;
}
现在我们已经了解了如何在外部处理错误,让我们看看如何在应用内部处理错误。
如果我们回到UserRegisterController实现,我们可以看到它有多个缺陷。如果字段填写不正确或根本不正确怎么办?如果模型定义未得到遵守怎么办?目前,我们不需要任何东西,也不验证任何东西。
让我们解决这个问题,看看如何构建更健壮的应用:
- 更新
UserModel,并使用修饰符(也称为属性)来设置一些属性,如Required和DataType。Required属性表示以下字段必须有一个值,如果没有提供值,则会导致错误。DataType属性指定字段需要特定的数据类型:
public class UserModel
{
public Guid Id { get; set; }
[Required()]
public string FirstName { get; set; }
[Required()]
public string LastName { get; set; }
[Required(), DataType(DataType.EmailAddress)]
public string Email { get; set; }
[Required(), DataType(DataType.Password)]
public string Password { get; set; }
public bool IsEmailConfirmed { get; set; }
public System.DateTime? EmailConfirmationDate { get;
set; }
public int Score { get; set; }
}
- 更新
UserRegistrationController中的具体Index方法,然后添加ModelState验证码:
[HttpPost]
public async Task<IActionResult> Index(UserModel userModel)
{
if (ModelState.IsValid)
{
await _userService.RegisterUser(userModel);
return Content($"User {userModel.FirstName}
{userModel.LastName} has been registered
successfully");
}
return View(userModel);
}
- 如果您没有填写必填字段,或者您提供了无效的电子邮件地址,然后单击“确定”,您现在将收到相应的错误消息:

为了达到这个阶段,我们已经经历了创建一个M模型(如UserModel)的过程,一个V视图(如前一个)和一个C控制器(如UserRegistrationController)的过程。换句话说,简而言之,我们已经成功创建了一个功能正常的MVC应用!赞扬自己走到了这一步,并期待更多令人兴奋的东西,因为我们将在后面的章节中详细阐述 ASP.NET Core MVC 应用。
总结
在本章中,您已经了解了 ASP.NET 3 的一些基本概念。有很多东西需要理解,也有很多东西需要看,我们希望你自己尝试每一件事时都会感到有趣。你确实取得了巨大的进步!
一开始,您创建了 Tic-Tac-Toe 项目,然后开始实现它的不同组件。我们探索了Program和Startup类,了解了如何使用 NPM 和布局页面,学习了如何应用 DI,并使用了静态文件。
此外,我们还为更高级的场景引入了中间件和路由。最后,我们通过一个实例说明了如何为应用添加有效的错误处理。
在下一章中,我们将继续介绍其他概念,如WebSockets、全球化、本地化和配置。我们还将学习如何一次构建应用,并使用相同的构建在多个环境中运行。
五、通过自定义应用实现 ASP.NET Core 3 的基本概念:第 2 部分
上一章让您深入了解了在使用 ASP.NET Core 3 构建高效且更易于维护的 web 应用时可以使用的各种功能和特性。我们已经解释了一些基本概念,您已经看到了如何将它们应用于名为Tic Tac Toe的实际应用的多个示例。
自从您了解了 ASP.NET Core 3 应用的内部结构、如何正确配置它们以及如何使用自定义行为对它们进行扩展(这是将来构建自己的应用的关键)以来,您已经取得了相当好的进展。
但我们不要就此止步!在本章中,您将发现如何最好地实现缺失的组件,进一步改进现有组件,并添加客户端代码,以便在本章末尾拥有一个完全运行的端到端 Tic-Tac-Toe 应用。
在本章中,我们将介绍以下主题:
- 使用 JavaScript、捆绑和缩小优化客户端开发
- 使用 WebSocket 实现实时通信场景
- 利用会话和用户缓存管理
- 为多语言用户界面应用全球化和本地化
- 配置应用和服务
- 实现高级依赖注入概念
- 一次构建并在多个环境中运行
使用 JavaScript 进行客户端开发
在上一章中,您使用 MVC 模式创建了主页和用户注册页。您实现了一个控制器(UserRegistrationController)以及一个用于处理用户注册请求的相应视图。然后,您添加了一个服务(UserService)和中间件(CommunicationMiddleware),但是我们才刚刚开始,所以还没有完成:

与 Tic-Tac-Toe 应用的初始工作流相比,我们可以看到仍然缺少很多东西,例如使用整个客户端部分,实际使用通信中间件,以及我们仍然需要实现的多个其他功能。
让我们从客户端部分开始,学习如何应用更先进的技术。然后,我们将学习如何尽可能地优化一切。
您可能还记得,上一次,我们在用户将其数据提交到注册表后停止了,注册表已发送到UserService。在这里,我们刚刚显示了一条纯文本消息,如下所示:

然而,处理并不是到此为止。我们需要使用客户端开发和 JavaScript 添加整个电子邮件确认过程,这就是我们下一步要做的。
初步电子邮件确认功能
在本节中,我们将构建一个暂定的电子邮件确认功能,以演示客户端开发。这一功能将随着本书的发展而发展,并将在后面的章节中完善。但现在,让我们创建电子邮件确认功能,如下所示:
- 启动 Visual Studio 2019 并打开 Tic-Tac-Toe 项目。将名为
EmailConfirmation的新方法添加到UserRegistrationController:
[HttpGet]
public IActionResult EmailConfirmation (string email)
{
ViewBag.Email = email;
return View();
}
- 右键点击
EmailConfirmation方法,生成相应的视图,并用一些有意义的信息进行更新:
@{
ViewData["Title"] = "EmailConfirmation";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>EmailConfirmation</h2>
An email has been sent to @ViewBag.Email, please
confirm your email address by clicking on the
provided link.
- 转到
UserRegistrationController并修改Index方法,从上一步重定向到EmailConfirmation方法,而不是返回文本消息:
[HttpPost]
public async Task<IActionResult> Index(UserModel userModel)
{
if (ModelState.IsValid)
{
await _userService.RegisterUser(userModel);
return RedirectToAction(nameof(EmailConfirmation),
new { userModel.Email });
}
else
{
return View(userModel);
}
}
- 按F5启动应用,注册新用户,并验证新电子邮件确认页面是否正确显示:

这里,您已经实现了第一组修改,以完成用户注册过程。在下一节中,我们需要检查用户是否已确认其电子邮件地址。
我们用户的电子邮件确认
以下步骤将帮助我们检查电子邮件确认:
- 在
IUserService界面增加两个新方法GetUserByEmail和UpdateUser。这些将用于处理电子邮件确认更新:
public interface IUserService
{
Task<bool> RegisterUser(UserModel userModel);
Task<UserModel> GetUserByEmail(string email);
Task UpdateUser(UserModel user);
}
- 实现这些新方法,使用静态的
ConcurrentBag来持久化UserModel,并修改UserService中的RegisterUser方法,如下所示:
public class UserService : IUserService
{
private static ConcurrentBag<UserModel> _userStore;
static UserService() { _userStore = new ConcurrentBag
<UserModel>(); }
public Task<bool> RegisterUser(UserModel userModel) {
_userStore.Add(userModel);
return Task.FromResult(true); }
public Task<UserModel> GetUserByEmail(string email) {
return Task.FromResult(_userStore.FirstOrDefault
(u => u.Email == email)); }
public Task UpdateUser(UserModel userModel) {
_userStore = new ConcurrentBag<UserModel>(_userStore.Where
(u => u.Email != userModel.Email))
{ userModel };
return Task.CompletedTask;
}
}
- 将名为
GameInvitationModel的新模型添加到Models文件夹中。用户注册成功后,将用于游戏邀请:
public class GameInvitationModel
{
public Guid Id { get; set; }
public string EmailTo { get; set; }
public string InvitedBy { get; set; }
public bool IsConfirmed { get; set; }
public DateTime ConfirmationDate { get; set; }
}
- 添加一个名为
GameInvitationController的新控制器,并更新其Index方法以自动设置InvitedBy属性:
using TicTacToe.Services;
public class GameInvitationController : Controller
{
private IUserService _userService;
public GameInvitationController(IUserService userService)
{
_userService = userService;
}
[HttpGet]
public async Task<IActionResult> Index(string email)
{
var gameInvitationModel = new GameInvitationModel
{InvitedBy = email };
return View(gameInvitationModel);
}
}
- 右键点击
Index方法,选择创建模板,选择GameInvitationModel(TicTacToe.Models)作为模型类,生成相应的视图,如下图所示:

- 修改自动生成的视图,删除除
EmailTo输入控件以外的所有不必要的输入控件:
@model TicTacToe.Models.GameInvitationModel
<h4>GameInvitationModel</h4>
<div class="row">
<div class="col-md-4">
<form asp-action="Index">
<div asp-validation-summary="ModelOnly" class="text
-danger"></div>
<div class="form-group">
<label asp-for="EmailTo" class="control-label"></label>
<input asp-for="EmailTo" class="form-control" />
<span asp-validation-for="EmailTo" class="text
-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-
primary" />
</div>
</form>
</div>
</div>
- 现在更新
UserRegistrationController中的EmailConfirmation方法。在确认用户的电子邮件后,必须将其重定向到GameInvitationController。如您所见,我们现在将在代码中模拟此确认:
[HttpGet]
public async Task<IActionResult> EmailConfirmation(
string email)
{
var user = await _userService.GetUserByEmail(email);
if (user?.IsEmailConfirmed == true)
return RedirectToAction("Index", "GameInvitation",
new { email = email });
ViewBag.Email = email;
user.IsEmailConfirmed = true;
user.EmailConfirmationDate = DateTime.Now;
await _userService.UpdateUser(user);
return View();
}
- 按F5启动应用,注册新用户,并验证是否显示 EmailConfirmation 页面。在 Microsoft Edge 中,按F5重新加载页面,如果一切正常,应重定向到游戏邀请页面,如下图所示:

很好,还有一些进步!在游戏邀请之前,一切都在进行中,但不幸的是,用户干预仍然是必要的。用户必须手动刷新邮件确认页面,按F5键,直到其邮件被确认;只有这样,他们才会被重定向到游戏邀请页面。
整个刷新过程必须在下一步实现自动化和优化。你的选择如下:
- 在页面的
head部分放置一个 HTMLmeta刷新标记。 - 使用简单的 JavaScript,它以编程方式进行刷新。
- 使用 jQuery 实现XMLHttpRequest(XHR)。
HTML5 引入了 meta refresh 标签,用于在一定时间后自动刷新页面。但是,不建议使用此方法,因为它会造成较高的服务器负载。此外,Microsoft Edge 中的安全设置可能会完全停用它,一些广告拦截器会阻止它工作。因此,如果您使用它,您无法确定它是否能够正常工作。
使用简单的 JavaScript 可以很好地以编程方式自动化页面刷新,但它主要有相同的缺陷,因此也不推荐使用。
使用 XMLHttpRequest
我们刚刚提到,不建议在刷新过程中使用简单的 JavaScript 和meta刷新标记,所以让我们介绍一下 XHR,这正是我们真正想要的。它提供了我们的 Tic Tac Toe 应用所需的功能,因为它允许以下功能:
- 在不重新加载网页的情况下更新网页
- 从服务器请求和接收数据,即使在页面加载之后也是如此
- 在后台向服务器发送数据
这可以在下图中看到:

现在,您将使用 XHR 来自动化和优化用户注册电子邮件确认过程的客户端实现。操作步骤如下所示:
- 在
wwwroot文件夹中创建一个名为app的新文件夹(该文件夹将包含以下步骤中显示的所有客户端代码),并在该文件夹中创建一个名为js的子文件夹。 - 将名为
scripts1.js的新 JavaScript 文件添加到包含以下内容的wwwroot/app/js文件夹中:
var interval;
function EmailConfirmation(email) {
interval = setInterval(() => {
CheckEmailConfirmationStatus(email);
}, 5000);
}
- 将名为
scripts2.js的新 JavaScript 文件添加到包含以下内容的wwwroot/app/js文件夹中:
function CheckEmailConfirmationStatus(email) {
$.get("/CheckEmailConfirmationStatus?email=" + email,
function (data) {
if (data === "OK") {
if (interval !== null)
clearInterval(interval);
alert("ok");
}
});
}
- 打开
Views\Shared\_Layout.cshtml文件中的布局页面,在关闭body标签之前添加一个新的开发环境元素(最好放在这里):
<environment include="Development">
<script src="~/app/js/scripts1.js"></script>
<script src="~/app/js/scripts2.js"></script>
</environment>
- 更新通信中间件中的
Invoke方法,并添加一个新的等待来访问一个名为ProcessEmailConfirmation的ProcessEmailConfirmation方法:
public async Task Invoke(HttpContext context)
{
if (context.Request.Path.Equals(
"/CheckEmailConfirmationStatus"))
{
await ProcessEmailConfirmation(context);
}
else
{
await _next?.Invoke(context);
}
}
ProcessEmailConfirmation方法将模拟电子邮件确认功能。我们对该方法的定义如下:
private async Task ProcessEmailConfirmation( HttpContext
context)
{
var email = context.Request.Query["email"];
var user = await _userService.GetUserByEmail(email);
if (string.IsNullOrEmpty(email))
{ await context.Response.WriteAsync("BadRequest:Email is
required"); }
else if ((await _userService.GetUserByEmail
(email)).IsEmailConfirmed)
{ await context.Response.WriteAsync("OK"); }
else
{
await context.Response.WriteAsync(
"WaitingForEmailConfirmation");
user.IsEmailConfirmed = true;
user.EmailConfirmationDate = DateTime.Now;
_userService.UpdateUser(user).Wait();
}
}
- 通过在页面底部添加对 JavaScript
EmailConfirmation函数的调用(来自上一步),更新UserRegistration文件夹下的EmailConfirmation视图,如下所示:
@section Scripts
{
<script>
$(document).ready(function () {
EmailConfirmation('@ViewBag.Email');
});
</script>
}
- 更新
UserRegistrationController中的EmailConfirmation方法。由于通信中间件将模拟有效的电子邮件确认,请删除以下行:
user.IsEmailConfirmed = true;
user.EmailConfirmationDate = DateTime.Now;
await _userService.UpdateUser(user);
- 按F5启动应用并注册新用户。您将看到一个 JavaScript 警报框返回
WaitingForEmailConfirmation,一段时间后,另一个框返回 ok:

- 现在,您必须更新
scripts2.js文件中的CheckEmailConfirmationStatus方法,以将我们的用户重定向到游戏邀请页面,以防收到确认的电子邮件。为此,请删除alert("OK");指令并在其位置添加以下指令:
window.location.href = "/GameInvitation?email=" + email;
- 按F5启动应用并注册新用户。所有内容都应该是自动的,并且您应该在最后自动重定向到游戏邀请页面:

Note that, if you still see the alert box, even though you have updated the project in Visual Studio, you might have to delete the cached data in your browser to have the JavaScript refreshed correctly in your browser and see the new behavior.
优化 web 应用并使用捆绑和缩小
正如您在第 4 章中通过自定义应用了解到的 ASP.NET Core 3 的基本概念:第 1 部分,我们选择了经过社区验证的节点包管理器(NPM作为客户端包管理器。我们没有触及appsettings.json文件,这意味着我们已经恢复了四个默认包,并在 ASP.NET Core 3 布局页面中添加了一些引用以使用它们:

在当今的现代 web 应用开发世界中,在开发过程中将客户端 JavaScript 代码和 CSS 样式表分离到多个文件中是一种很好的做法。但是,在生产环境中运行时,拥有如此多的文件可能会导致性能和带宽问题。
这就是为什么在构建过程中,在生成最终发布包之前必须对所有内容进行优化,这意味着 JavaScript 和 CSS 文件必须捆绑并缩小。TypeScript 和 CoffeeScript 文件必须转换成 JavaScript。
捆绑和缩小是两种可以用来提高 web 应用整体页面加载性能的技术。捆绑允许您将多个文件合并到一个文件中,而缩小优化了 JavaScript 和 CSS 文件的代码以获得更小的有效负载。它们一起工作以减少服务器请求的数量以及总体请求大小。
ASP.NET Core 3 支持不同的捆绑和缩小解决方案:
- Visual Studio Bundler 和 Minifier 扩展
- 吞咽
- 咕哝
捆绑和缩小
让我们学习如何通过使用 Visual Studio Bundler&Minifier 扩展名和bundleconfig.json文件来捆绑和缩小 Tic Tac Toe 项目中的多个 JavaScript 文件:
- 在顶部菜单中,选择扩展,点击在线,在搜索框中输入
Bundler,选择 Bundler&Minifier,点击下载:

- 关闭 Visual Studio;安装将继续进行。接下来,单击修改:

- 重新启动 VisualStudio。现在,您将通过捆绑和缩小来优化打开连接的数量以及带宽使用。为此,向项目中添加一个名为
bundleconfig.json的新 JSON 文件。 - 更新
bundleconfig.json文件,以便将两个 JavaScript 文件捆绑成一个名为site.js的文件,并缩小site.css和site.js文件:
[
{"outputFileName": "wwwroot/css/site.min.css",
"inputFiles": [
"wwwroot/css/site.css" ]},
{"outputFileName": "wwwroot/js/site.js",
"inputFiles": [
"wwwroot/app/js/scripts1.js",
"wwwroot/app/js/scripts2.js" ],
"sourceMap": true,
"includeInProject": true },
{"outputFileName": "wwwroot/js/site.min.js",
"inputFiles": ["wwwroot/js/site.js"],
"minify": {
"enabled": true,
"renameLocals": true },
"sourceMap": false }
]
- 右键单击项目并选择 Bundler&Minifier |更新捆绑包:

- 在解决方案资源管理器中查看时,您将看到已生成两个名为
site.min.css和site.min.js的新文件:

- 在 Task Runner Explorer 中查看时,您将看到为项目配置的绑定和缩小过程:

- 右键单击“更新所有文件”,然后选择“运行”。现在,您可以更详细地查看和理解流程所做的工作:

- 通过右键单击 Update all files 并选择 Bindings | after build,在每次生成后安排流程执行。将生成一个名为
bundleconfig.json.bindings的新文件,如果您删除wwwroot/js文件夹并重建项目,这些文件将自动生成。 - 要查看新生成的文件,请转到项目设置中的 Debug*选项卡,并将
ASPNETCORE_ENVIRONMENT变量设置为Staging。然后,单击保存:

- 按F5启动应用,在 Microsoft Edge 中按F12打开开发者工具,重新进行注册过程。您将看到,仅加载了捆绑和缩小的
site.min.css和site.min.js文件,并且加载时间更快:

好了,现在我们知道了如何实现客户端并从现代 web 应用开发中的捆绑和缩小中获益,让我们回到 Tic-Tac-Toe 游戏,进一步优化它,并添加缺少的组件。
首先,我们将研究如何使用 Web 套接字进行实时通信。
使用 WebSocket 实现实时通信场景
在上一节结束时,一切都是完全自动化的,正如预期的那样。然而,仍有一些改进的余地。
实际上,客户端定期向服务器端发送请求,以查看电子邮件确认状态是否已更改。这可能会导致大量请求,以查看是否有状态更改。
此外,服务器端无法在确认电子邮件后立即通知客户端,因为它必须等待客户端请求响应。
在本节中,您将了解 WebSocket(的概念 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/websockets )以及它们将如何让您进一步优化客户端实现。
WebSocket 通过 TCP 实现持久的双向通信通道,这对于需要运行实时通信场景(聊天、股票行情、游戏等)的应用来说尤其有趣。碰巧我们的示例应用是一个游戏,它是一种主要的应用类型,主要得益于直接使用套接字连接。
Note that you could also consider SignalR as an alternative. SignalR provides a better solution for real-time communication scenarios and encapsulates some of the functionalities that are missing from WebSockets that we may have implemented manually.
我们将在下一章介绍信号机,即第 6 章介绍 Razor 组件和信号机。
正在运行的 WebSockets
让我们通过使用 WebSocket 进行实时通信来优化 Tic Tac Toe 应用的客户端实现:
- 转到
Configure方法中的 Tic Tac ToeStartup类,在通信中间件和 MVC 中间件之前添加 WebSockets 中间件(请记住,中间件调用顺序对于确保正确的行为非常重要):
app.UseWebSockets();
app.UseCommunicationMiddleware();
...
- 更新通信中间件,新增两种方式,第一种为
SendStringAsync,如下:
private static Task SendStringAsync(WebSocket socket,
string data, CancellationToken ct =
default(CancellationToken))
{
var buffer = Encoding.UTF8.GetBytes(data);
var segment = new ArraySegment<byte>(buffer);
return socket.SendAsync(segment, WebSocketMessageType.Text,
true, ct);
}
第二个为ReceiveStringAsync,用于 WebSocket 通信:
private static async Task<string> ReceiveStringAsync(
WebSocket socket, CancellationToken ct =
default(CancellationToken))
{
var buffer = new ArraySegment<byte>(new byte[8192]);
using (var ms = new MemoryStream()) {
WebSocketReceiveResult result;
do
{ ct.ThrowIfCancellationRequested();
result = await socket.ReceiveAsync(buffer, ct);
ms.Write(buffer.Array, buffer.Offset, result.Count); }
while (!result.EndOfMessage);
ms.Seek(0, SeekOrigin.Begin);
if (result.MessageType != WebSocketMessageType.Text)
throw new Exception("Unexpected message");
using (var reader = new StreamReader(ms, Encoding.UTF8))
{ return await reader.ReadToEndAsync(); }
}
}
- 更新通信中间件,增加一个新的方法
ProcessEmailConfirmation用于通过 WebSocket 进行电子邮件确认处理:
public async Task ProcessEmailConfirmation(HttpContext context, WebSocket currentSocket, CancellationToken ct, string email)
{
UserModel user = await _userService.GetUserByEmail(email);
while (!ct.IsCancellationRequested &&
!currentSocket.CloseStatus.HasValue &&
user?.IsEmailConfirmed == false) {
if (user.IsEmailConfirmed)
await SendStringAsync(currentSocket, "OK", ct);
else
{ user.IsEmailConfirmed = true;
user.EmailConfirmationDate = DateTime.Now;
await _userService.UpdateUser(user);
await SendStringAsync(currentSocket, "OK", ct); }
Task.Delay(500).Wait();
user = await _userService.GetUserByEmail(email);
}
}
- 更新通信中间件中的
Invoke方法,并从上一步添加对 WebSocket 特定方法的调用,同时仍保留不支持 WebSocket 的浏览器的标准实现:
public async Task Invoke(HttpContext context) {
if (context.WebSockets.IsWebSocketRequest)
{
var webSocket = await context.WebSockets.
AcceptWebSocketAsync();
var ct = context.RequestAborted;
var json = await ReceiveStringAsync(webSocket, ct);
var command = JsonConvert.DeserializeObject<dynamic>(json);
switch (command.Operation.ToString()) {
case "CheckEmailConfirmationStatus":
{await ProcessEmailConfirmation(context, webSocket,
ct, command.Parameters.ToString());
break; }
}
}
else if (context.Request.Path.Equals("/CheckEmailConfirmationStatus"))
//... keep the rest of the method as it was
}
- 修改
scripts1.js文件并添加一些特定于 WebSocket 的代码,用于打开和使用套接字:
var interval;
function EmailConfirmation(email) {
if (window.WebSocket) {
alert("Websockets are enabled");
openSocket(email, "Email");
}
else {
alert("Websockets are not enabled");
interval = setInterval(() => {
CheckEmailConfirmationStatus(email);
}, 5000);
}
}
- 修改
scripts2.js文件,但保持CheckEmailConfirmationStatus功能不变。添加一些特定于 WebSocket 的代码,用于打开和使用套接字,如果用户的电子邮件已被确认,还可以将用户重定向到游戏邀请页面:
var openSocket = function (parameter, strAction) {
if (interval !== null) clearInterval(interval);
var protocol = location.protocol === "https:" ? "wss:" : "ws:";
var operation = ""; var wsUri = "";
if (strAction == "Email") {
wsUri = protocol + "//" + window.location.host
+ "/CheckEmailConfirmationStatus";
operation = "CheckEmailConfirmationStatus"; }
var socket = new WebSocket(wsUri);
socket.onmessage = function (response) {
console.log(response);
if (strAction == "Email" && response.data == "OK") {
window.location.href = "/GameInvitation?email=" +
parameter; } };
socket.onopen = function () {
var json = JSON.stringify({ "Operation": operation,
"Parameters": parameter });
socket.send(json); };
socket.onclose = function (event) { };
};
- 当您启动应用并继续进行用户注册时,如果支持 WebSocket,您将获得必要的信息。如果是,您将像以前一样被重定向到游戏邀请页面,但处理时间要快得多:

现在,我们在 ASP.NET Core 3 下对客户端开发和优化的研究到此结束。现在,您将学习如何使用其他 ASP.NET Core 概念进一步扩展和完成 Tic Tac Toe 应用,这些概念将帮助您在日常工作中构建多语言、支持生产的 web 应用。
随着 web 应用变得越来越繁忙,我们可能希望尽量减少不必要的往返,这些往返请求的数据可以保留一段时间用于检索和使用。让我们通过介绍会话和用户缓存管理来了解如何做到这一点。
利用会话和用户缓存管理
作为一名 web 开发人员,您可能知道 HTTP 是一种无状态协议,这意味着,默认情况下,不存在会话的概念。每个请求都是独立处理的,不同的请求之间不保留任何值。
尽管如此,处理数据有不同的方法。您可以使用查询字符串、提交表单数据,也可以使用 cookie 在客户端上存储数据。然而,所有这些机制或多或少都是手动的,需要自己管理。
如果您是经验丰富的 ASP.NET 开发人员,您将熟悉会话状态和会话变量的概念。这些变量存储在 web 服务器上,您可以在不同的用户请求期间访问它们,以便有一个中心位置来存储和接收数据。会话状态非常适合于存储特定于会话的用户数据,而不需要永久持久性。
Note that it is good practice to not store any sensitive data in session variables due to security reasons. Users might not close their browsers; thus, session cookies might not be cleared (also, some browsers keep session cookies alive).
Also, a session might not be restricted to a single user, so other users might continue with the same session, which could cause security risks.
ASP.NET Core 3 通过使用专用的会话中间件提供会话状态和会话变量。基本上,有两种不同类型的会话提供程序:
- 内存中会话提供程序(本地到单个服务器)
- 分布式会话提供程序(在多台服务器之间共享)
内存会话提供程序
让我们了解如何在 Tic Tac Toe 应用中激活内存会话提供程序,以存储用户界面的区域性和语言:
- 打开
Views\Shared\_Layout.cshtml文件中的布局页面,并在主导航菜单中添加新的用户界面语言下拉列表。这将放置在其他菜单项之后。这将允许用户在英语和法语之间进行选择:
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown"
href="#">Settings<span class="caret"></span></a>
<ul class="dropdown-menu multi-level">
<li class="dropdown-submenu">
<a class="dropdown-toggle" data-toggle="dropdown"
href="#">Select your language (@ViewBag.Language)
<span class="caret"></span></a>
<ul class="dropdown-menu">
<li @(ViewBag.Language == "EN" ? "active" : "")>
<a asp-controller="Home" asp-action="SetCulture"
asp-route-culture="EN">English</a></li>
<li @(ViewBag.Language == "FR" ? "active" : "")>
<a asp-controller="Home" asp-action="SetCulture"
asp-route-culture="FR">French</a></li>
</ul>
</li>
</ul>
</li>
- 打开
HomeController并添加一个名为SetCulture的新方法。这将包含用于在会话变量中存储用户区域性设置的代码:
using Microsoft.AspNetCore.Http;
public IActionResult SetCulture(string culture)
{
Request.HttpContext.Session.SetString("culture", culture);
return RedirectToAction("Index");
}
- 更新
HomeController的Index方法,以便从文化会话变量中检索文化:
public IActionResult Index()
{
var culture =
Request.HttpContext.Session.GetString("culture");
ViewBag.Language = culture;
return View();
}
- 转到
wwwroot/css/site.css文件,添加一些新的 CSS 类,以更现代的方式查看用户界面语言下拉列表,首先查看相对位置,然后查看不同的浏览器和鼠标悬停:
.dropdown-submenu {
position: relative;
}
.dropdown-submenu > .dropdown-menu {
top: 0;
left: 100%;
margin-top: -6px;
margin-left: -1px;
-webkit-border-radius: 0 6px 6px 6px;
-moz-border-radius: 0 6px 6px;
border-radius: 0 6px 6px 6px;
}
.dropdown-submenu:hover > .dropdown-menu {
display: block;
}
在site.css中通过添加以下样式执行相同操作:
.dropdown-submenu > a:after {
display: block;
content: " ";
float: right;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 5px 0 5px 5px;
border-left-color: #ccc;
margin-top: 5px;
margin-right: -10px;
}
.dropdown-submenu:hover > a:after {
border-left-color: #fff;
}
最后,添加以下代码段:
.dropdown-submenu.pull-left {
float: none;
}
.dropdown-submenu.pull-left > .dropdown-menu {
left: -100%;
margin-left: 10px;
-webkit-border-radius: 6px 0 6px 6px;
-moz-border-radius: 6px 0 6px 6px;
border-radius: 6px 0 6px 6px;
}
- 将 ASP.NET Core 3 内置的会话中间件添加到
Startup类的ConfigureServices方法中:
services.AddSession(o =>
{
o.IdleTimeout = TimeSpan.FromMinutes(30);
});
- 在
Startup类的Configure方法中激活会话中间件,将其添加到静态文件中间件之后:
app.UseStaticFiles();
app.UseSession();
- 通过设置 email session 变量更新
GameInvitationController中的Index方法,如下所示:
[HttpGet]
public async Task<IActionResult> Index(string email)
{
var gameInvitationModel = new GameInvitationModel {
InvitedBy = email };
HttpContext.Session.SetString("email", email);
return View(gameInvitationModel);
}
- 按F5 启动应用。您应该看到新的用户界面语言下拉列表,其中包含英语和法语选项:

在这里,我们使用了内存中的会话提供程序,但有一种不同类型的会话提供程序在其他场景中工作良好,称为分布式会话提供程序。我们将在下一节中介绍这一点。
分布式会话提供程序
到目前为止,您已经了解了如何激活和使用会话状态。然而,在大多数情况下,您将拥有多个 web 服务器,而不仅仅是一个,特别是在今天的云环境中。那么,如何将会话状态从内存中存储到分布式缓存中呢?
好吧,这很简单——你只需要在Startup类中注册额外的服务。这些附加服务将提供此功能。以下是一些例子:
- 分布式内存缓存:
services.AddDistributedMemoryCache();
- 分布式 SQL Server 缓存:
services.AddDistributedSqlServerCache(o =>
{
o.ConnectionString = _configuration["DatabaseConnection"];
o.SchemaName = "dbo";
o.TableName = "sessions";
});
- 分布式 Redis 缓存:
services.AddDistributedRedisCache(o =>
{
o.Configuration = _configuration["CacheRedis:Connection"];
o.InstanceName = _configuration
["CacheRedis:InstanceName"];
});
我们在本节中添加了一个新的用户界面语言下拉列表,但您还没有学会如何在应用中处理多种语言。没有时间可以浪费;让我们学习如何执行此操作,并使用下拉列表和会话变量动态更改用户界面语言。
在多语言用户界面中应用全球化和本地化
有时,您的应用取得了成功,有时甚至取得了相当大的成功,因此您希望在国际上向更广泛的受众提供这些应用,并在更大范围内部署它们。但是,您不可能轻松做到这一点,因为您从一开始就没有考虑过本地化应用,现在您必须修改已经运行的应用,这可能会带来回归和不稳定的风险。
不要落入这个陷阱!从一开始就考虑您的目标受众和未来的部署策略!
本地化应用应该从项目一开始就考虑,特别是因为使用 ASP.NET Core 3 框架时,本地化非常容易和直接。它为此提供了现有的服务和中间件。
构建支持不同语言和文化的应用以进行显示、输入和输出称为全球化,而将全球化应用适应特定文化则称为本地化。
有三种不同的方法可以本地化 ASP.NET Core 3 web 应用:
- 字符串定位器
- 视图定位器
- 本地化数据注释
让我们更详细地看一下这些概念。
全球化和本地化概念
在本节中,您将了解全球化和本地化的概念,以及它们如何让您进一步优化网站以实现国际化。
For additional information on globalization and localization, please visit https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization.
那么,你是如何开始的呢?首先,让我们看看如何使用字符串定位器使 Tic-Tac-Toe 应用可本地化:
- 转到
Services文件夹并添加名为CultureProviderResolverService的新服务。这将通过查看Culture查询字符串、Culturecookie 和Culture会话变量(在本章上一节中创建)来检索区域性集。 - 通过继承
RequestCultureProvider并覆盖其具体方法来实现CultureProviderResolverService:
public class CultureProviderResolverService :
RequestCultureProvider
{
private static readonly char[] _cookieSeparator =
new[] {'|' };
private static readonly string _culturePrefix = "c=";
private static readonly string _uiCulturePrefix = "uic=";
//...
}
- 将
DetermineProviderCultureResult方法添加到CultureProviderResolverService类中:
public override async Task<ProviderCultureResult>
DetermineProviderCultureResult(HttpContext httpContext)
{
if (GetCultureFromQueryString(httpContext,
out string culture))
return new ProviderCultureResult(culture, culture);
else if (GetCultureFromCookie(httpContext, out culture))
return new ProviderCultureResult(culture, culture);
else if (GetCultureFromSession(httpContext, out
culture))
return new ProviderCultureResult(culture, culture);
return await NullProviderCultureResult;
}
- 添加以下方法,可以从查询字符串中获取
Culture:
private bool GetCultureFromQueryString( HttpContext httpContext, out string culture)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
var request = httpContext.Request;
if (!request.QueryString.HasValue)
{
culture = null;
return false;
}
culture = request.Query["culture"];
return true;
}
- 添加以下方法,允许我们从 Cookie 中获取
Culture:
private bool GetCultureFromCookie(HttpContext httpContext, out
string culture)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
var cookie = httpContext.Request.Cookies["culture"];
if (string.IsNullOrEmpty(cookie))
{
culture = null;
return false;
}
culture = ParseCookieValue(cookie);
return !string.IsNullOrEmpty(culture);
}
- 下面是我们将用来解析 cookie 值的方法:
public static string ParseCookieValue(string value)
{
if (string.IsNullOrWhiteSpace(value)) return null;
var parts = value.Split(_cookieSeparator,
StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 2) return null;
var potentialCultureName = parts[0];
var potentialUICultureName = parts[1];
if (!potentialCultureName.StartsWith(_culturePrefix) ||
!potentialUICultureName.StartsWith(_uiCulturePrefix)) return null;
var cultureName = potentialCultureName.
Substring(_culturePrefix.Length);
var uiCultureName = potentialUICultureName.Substring
(_uiCulturePrefix.Length);
if (cultureName == null && uiCultureName == null) return null;
if (cultureName != null && uiCultureName == null) uiCultureName =
cultureName;
if (cultureName == null && uiCultureName != null) cultureName = uiCultureName;
return cultureName;
}
- 现在,添加以下方法,允许我们从会话中获取
Culture:
private bool GetCultureFromSession(HttpContext httpContext,
out string culture)
{
culture = httpContext.Session.GetString("culture");
return !string.IsNullOrEmpty(culture);
}
- 在
Startup类中ConfigureServices方法顶部增加本地化服务:
public void ConfigureServices(IServiceCollection services)
{
services.AddLocalization(options => options.
ResourcesPath = "Localization");
//...
}
- 将本地化中间件添加到
Startup类中的Configure方法中,并定义支持的区域性。
请注意,正如您已经看到的,添加中间件的顺序很重要。您必须在 MVC 中间件之前添加本地化中间件:
...
var supportedCultures =
CultureInfo.GetCultures(CultureTypes.AllCultures);
var localizationOptions = new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture("en-US"),
SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures
};
localizationOptions.RequestCultureProviders.Clear();
localizationOptions.RequestCultureProviders.Add(new
CultureProviderResolverService());
app.UseRequestLocalization(localizationOptions);
app.UseMvc(...);
请注意,您可以使用不同的方法更改应用的区域性:
- 查询字符串:在 URI 中提供区域性
*** 曲奇:将培养物储存在曲奇中* 浏览器:浏览器页面语言设置* 定制:实现您自己的提供者(如本例所示)**
**10. 在解决方案资源管理器中,添加名为Localization的新文件夹(将用于存储资源文件)并创建名为Controllers的子文件夹。然后,在此文件夹中添加一个名为GameInvitationController.resx的新资源文件:
Note that you can put your resource files either into subfolders (for example, Controllers, Views, and so on) or directly name your files accordingly (for example, Controllers.GameInvitationController.resx, Views.Home.Index.resx, and so on). However, we advise that you use the folder approach for clarity, readability, and better organization of your files.

If you see errors while using your resource files with .NET Core, right-click on each file and select Properties. Then, check each file to ensure that the Build Action is set to Content instead of Embedded Resource. These are bugs that should have been fixed by the final release, but if they haven't you can use this handy workaround to make everything work as expected.
- 打开
GameInvitationController.resx资源文件,新增英文GameInvitationConfirmationMessage:

- 在同一个
Controllers文件夹中,为法语翻译添加一个名为GameInvitationController.fr.resx的新资源文件:

- 转至
GameInvitationController,添加stringLocalizer,并更新构造函数实现:
private IStringLocalizer<GameInvitationController>
_stringLocalizer;
private IUserService _userService;
public GameInvitationController(IUserService userService,
IStringLocalizer<GameInvitationController> stringLocalizer)
{
_userService = userService;
_stringLocalizer = stringLocalizer;
}
- 在
GameInvitationController中增加一个新的Index方法。这将返回本地化消息,具体取决于应用区域设置:
[HttpPost]
public IActionResult Index(
GameInvitationModel gameInvitationModel)
{
return Content(_stringLocalizer[
"GameInvitationConfirmationMessage",
gameInvitationModel.EmailTo]);
}
- 以英语(默认区域性)启动应用并注册新用户,直到您收到以下文本消息,该文本消息应为英语:

- 使用“用户界面语言”下拉列表将应用语言更改为法语。然后,注册一个新用户,直到您收到以下文本消息(现在应该是法语),如以下屏幕截图所示:

在本节中,您学习了如何在应用中本地化任何类型的字符串,这对于某些特定的应用用例都很有用。但是,在使用视图时,这不是推荐的方法。
使用视图定位器
ASP.NET Core 3 框架提供了一些用于本地化视图的强大功能。您将在以下示例中使用视图定位器方法:
- 更新
Startup类中的ConfigureServices方法,将视图本地化服务添加到 MVC 服务声明中:
services.AddMvc().AddViewLocalization(
LanguageViewLocationExpanderFormat.Suffix,
options => options.ResourcesPath = "Localization");
- 修改
Views/ViewImports.cshtml文件并添加视图定位器功能,使其可用于所有视图:
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
- 打开主页视图并添加新标题,该标题将进一步本地化,如下所示:
<h2>@Localizer["Title"]</h2>
- 在解决方案资源管理器中,转到
Localization文件夹并创建名为Views的子文件夹。然后,将两个名为Home.Index.resx和Home.Index.fr.resx的新资源文件添加到此文件夹:

- 打开
Home.Index.resx文件,为英文标题添加条目:

- 打开
Home.Index.fr.resx文件,为法文标题添加条目:

- 启动应用并将用户界面语言下拉列表设置为英语:

- 使用用户界面e La语言下拉列表将应用语言更改为法语。标题现在应以法语显示:

在本节中,您已经了解了如何轻松地本地化视图,但是如何本地化视图中使用数据注释的表单?让我们更详细地看一下这一点;您会惊讶于 ASP.NET Core 3 框架所提供的功能!
本地化数据注释
我们将在以下示例中完全本地化用户注册表单:
- 在解决方案资源管理器中,转到
Localization/Views文件夹,添加两个名为UserRegistration.Index.resx和UserRegistration.Index.fr.resx的新资源文件。 - 打开
UserRegistration.Index.resx文件,添加一个Title和一个SubTitle元素,并提供英文翻译:

- 打开
UserRegistration.Index.fr.resx文件,添加一个具有法语翻译的Title和SubTitle元素:

- 更新用户注册索引视图,使其使用视图定位器:
@model TicTacToe.Models.UserModel
@{
ViewData["Title"] = Localizer["Title"];
}
<h2>@ViewData["Title"]</h2>
<h4>@Localizer["SubTitle"]</h4>
<hr />
<div class="row">
...
- 启动应用,使用“用户界面语言”下拉列表将语言设置为法语,然后转到“用户注册”页面。标题应以法语显示。单击“创建”,而不在输入字段中输入任何内容,然后查看发生了什么:

这里少了一些东西。您已经为页面标题以及用户注册页面的副标题添加了本地化,但是我们仍然缺少表单的一些本地化。但我们错过了什么?
您应该亲眼看到错误消息尚未本地化和翻译。我们使用数据注释框架进行错误处理和表单验证,那么如何本地化数据注释验证错误消息呢?这就是我们现在要看的:
- 将数据注释本地化服务添加到
Startup类的ConfigureServices方法中的 MVC 服务声明中:
services.AddMvc().AddViewLocalization(
LanguageViewLocationExpanderFormat.Suffix, options =>
options.ResourcesPath = "Localization")
.AddDataAnnotationsLocalization();
- 转到
Localization文件夹并创建名为Models的子文件夹。然后,添加两个名为UserModel.resx和UserModel.fr.resx的新资源文件。 - 用英文翻译更新
UserModel.resx文件:

- 接下来,用法语翻译更新
UserModel.fr.resx文件:

- 转到 Models 文件夹并更新
FirstName、LastName、Email和Password字段的UserModel实现,以便您可以使用前面的资源文件:
...
[Display(Name = "FirstName")]
[Required(ErrorMessage = "FirstNameRequired")]
public string FirstName { get; set; }
[Display(Name = "LastName")]
[Required(ErrorMessage = "LastNameRequired")]
public string LastName { get; set; }
[Display(Name = "Email")]
[Required(ErrorMessage = "EmailRequired"),
DataType(DataType.EmailAddress)]
[EmailAddress]
public string Email { get; set; }
[Display(Name = "Password")]
[Required(ErrorMessage = "PasswordRequired"),
DataType(DataType.Password)]
public string Password { get; set; }
...
- 重新生成解决方案并启动应用。当我们将用户界面语言更改为法语时,您将看到整个用户注册页面(包括错误消息)被完全翻译:

在本节中,您学习了如何使用数据注释本地化字符串、视图甚至错误消息。为此,您使用了 ASP.NET Core 3 的内置功能,因为它们包含用于开发多语言本地化 web 应用的所有内容。下一节将向您介绍如何配置应用和服务。
配置应用和服务
在前面的部分中,您已经通过向用户注册过程中添加缺少的组件,甚至对 Tic-Tac-Toe 应用的某些部分进行了本地化而取得了进展。但是,您总是通过在代码中以编程方式设置用户确认来模拟确认电子邮件。在本节中,我们将修改此部分,以便真正向新注册的用户发送电子邮件,并使所有内容完全可配置。
添加电子邮件服务
首先,您将添加一个新的电子邮件服务,用于向刚在网站上注册的用户发送电子邮件。让我们开始:
- 在
Services文件夹中,添加一个名为EmailService的新服务,并实现一个默认的SendEmail方法,我们稍后会更新:
public class EmailService
{
public Task SendEmail(string emailTo, string subject,
string message)
{
return Task.CompletedTask;
}
}
- 提取
IEmailService接口:

- 将新的电子邮件服务添加到
Startup类的ConfigureServices方法中(我们需要一个应用实例,所以将其添加为一个单实例):
services.AddSingleton<IEmailService, EmailService>();
- 更新
UserRegistrationController使其能够访问我们在上一步中创建的EmailService:
readonly IUserService _userService;
readonly IEmailService _emailService;
public UserRegistrationController(IUserService userService,
IEmailService emailService)
{
_userService = userService;
_emailService = emailService;
}
- 更新
UserRegistrationController中的EmailConfirmation方法,在var user=await _userService.GetUserByEmail(email);和user?.IsEmailConfirmed条件语句检查之间插入以下代码,调用EmailService的SendEmail方法:
var user = await _userService.GetUserByEmail(email);
var urlAction = new UrlActionContext
{
Action = "ConfirmEmail",
Controller = "UserRegistration",
Values = new { email },
Protocol = Request.Scheme,
Host = Request.Host.ToString()
};
var message = $"Thank you for your registration on
our website, please click here to confirm your
email " + $"
{Url.Action(urlAction)}";
try
{
_emailService.SendEmail(email,
"Tic-Tac-Toe Email Confirmation", message).Wait();
}
catch (Exception e) { }
很好–您现在有一个电子邮件服务,但您还没有完成。您需要能够配置服务,以便可以设置特定于环境的参数(SMTP 服务器名称、端口、SSL 等),然后发送电子邮件。
配置电子邮件服务
您将来创建的几乎所有服务都将具有某种配置,应该可以从代码外部进行配置。
为此,ASP.NET Core 3 有一个内置的配置 API。它提供了在应用运行时从多个源读取配置数据的各种功能。名称-值对用于配置数据持久化,可分为多级层次结构。此外,配置数据可以自动反序列化为包含私有成员和属性的普通旧 CLR 对象(POCO。
ASP.NET Core 3 支持以下配置源:
- 配置文件(JSON、XML 甚至经典 INI 文件)
- 环境变量
- 命令行参数
- 内存中的.NET 对象
- 加密用户存储
- Azure 密钥保险库
- 自定义提供者
For more information about the Configuration API, please visit https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration?tabs=basicconfiguration.
让我们了解如何通过使用带有 JSON 配置文件的 ASP.NET Core 3 配置 API 快速配置电子邮件服务:
- 将新的
appsettings.json配置文件添加到项目中,并添加以下自定义部分。这将用于配置电子邮件服务:
"Email": {
"MailType": "SMTP",
"MailServer": "localhost",
"MailPort": 25,
"UseSSL": false,
"UserId": "",
"Password": "",
"RemoteServerAPI": "",
"RemoteServerKey": ""
}
- 在解决方案资源管理器中,在项目根目录下创建一个名为
Options的新文件夹。在此文件夹中添加一个名为EmailServiceOptions的新 POCO 类,并为我们之前看到的选项实现一些私有成员和公共属性:
public class EmailServiceOptions
{
private string MailType { get; set; }
private string MailServer { get; set; }
private string MailPort { get; set; }
private string UseSSL { get; set; }
private string UserId { get; set; }
private string Password { get; set; }
private string RemoteServerAPI { get; set; }
private string RemoteServerKey { get; set; }
public EmailServiceOptions() { }
public EmailServiceOptions(string mailType, string mailServer,
string mailPort, string useSSL,
string userId, string password, string
remoteServerAPI,string remoteServerKey) {
MailType = mailType;
MailServer = mailServer;
MailPort = mailPort;
UseSSL = useSSL;
UserId = userId;
Password = password;
RemoteServerAPI = remoteServerAPI;
RemoteServerKey = remoteServerKey; }
}
- 更新
EmailService实现,添加EmailServiceOptions,并在类中添加参数化构造函数:
private EmailServiceOptions _emailServiceOptions;
public EmailService(IOptions<EmailServiceOptions>
emailServiceOptions)
{
_emailServiceOptions = emailServiceOptions.Value;
}
- 将新构造函数添加到
Startup类中,以便您可以配置电子邮件服务:
public IConfiguration _configuration { get; }
public Startup(IConfiguration configuration)
{
_configuration = configuration;
}
- 更新
Startup类的ConfigureServices方法:
services.Configure<EmailServiceOptions>
(_configuration.GetSection("Email"));
services.AddSingleton<IEmailService, EmailService>();
- 更新
EmailService中的SendEmail方法。使用电子邮件服务选项从配置文件中检索设置,如下所示:
public Task SendEmail(string emailTo, string
subject, string message)
{
using (var client =
new SmtpClient(_emailServiceOptions.MailServer,
int.Parse(_emailServiceOptions.MailPort)))
{
if (bool.Parse(_emailServiceOptions.UseSSL) ==
true)client.EnableSsl = true;
if (!string.IsNullOrEmpty(_emailServiceOptions.UserId))
client.Credentials =
new NetworkCredential(_emailServiceOptions.UserId,
_emailServiceOptions.Password);
client.Send(new MailMessage("example@example.com",
emailTo, subject, message));
}
return Task.CompletedTask;
}
- 将断点放入
EmailService构造函数,按F5以调试模式启动应用。现在,验证是否已从配置文件中正确检索电子邮件服务选项值。如果您有 SMTP 服务器,还可以验证电子邮件是否确实已发送:

在本节中,您学习了如何通过使用 ASP.NET Core 3 的内置配置 API 来配置应用和服务,该 API 允许您编写更少的代码并提高生产效率,同时提供了一个更优雅、更易于维护的解决方案。
ASP.NET Core 3 帮助我们拥有可维护代码的另一个特性是其固有的依赖注入(DI功能。在其他优点中,DI 确保类之间没有太多耦合。在下一节中,我们将在 ASP.NETCore3 的上下文中研究 DI。
实现高级依赖注入概念
在上一章中,您了解了 DI 是如何工作的,以及如何使用构造函数注入方法。然而,如果您需要在运行时注入许多实例,此方法可能会非常麻烦,并且会使理解和维护代码变得复杂。
因此,您可以使用一种更先进的 DI 技术,称为方法注入。这允许您直接从代码中访问实例。
方法注入
在下面的示例中,您将添加一个新的服务,用于处理游戏邀请和更新 Tic-Tac-Toe 应用。这有助于电子邮件通信,用于联系其他用户加入游戏,同时使用方法注入:
- 在
Services文件夹中添加名为GameInvitationService的新服务,用于管理游戏邀请(添加、更新、删除等):
public class GameInvitationService
{
private static ConcurrentBag<GameInvitationModel>
_gameInvitations;
public GameInvitationService(){ _gameInvitations = new ConcurrentBag<GameInvitationModel>();}
public Task<GameInvitationModel> Add(GameInvitationModel
gameInvitationModel)
{ gameInvitationModel.Id = Guid.NewGuid();
_gameInvitations.Add(gameInvitationModel);
return Task.FromResult(gameInvitationModel); }
public Task Update(GameInvitationModel gameInvitationModel)
{ _gameInvitations = new ConcurrentBag<GameInvitationModel>
(_gameInvitations.Where(x => x.Id != gameInvitationModel.Id))
{ gameInvitationModel };
return Task.CompletedTask; }
public Task<GameInvitationModel> Get(Guid id)
{ return Task.FromResult(_gameInvitations.FirstOrDefault(x =>
x.Id == id)); }
}
- 提取
IGameInvitationService接口:

- 将新的游戏邀请服务添加到
Startup类的ConfigureServices方法中(我们需要一个应用实例,所以将其添加为一个单实例):
services.AddSingleton<IGameInvitationService,
GameInvitationService>();
- 更新
GameInvitationController中的Index方法,并使用RequestServices提供者通过方法注入注入游戏邀请服务实例:
public IActionResult Index(GameInvitationModel gameInvitationModel, [FromServices]IEmailService emailService)
{
var gameInvitationService = Request.HttpContext.RequestServices.GetService <IGameInvitationService>();
if (ModelState.IsValid) {
emailService.SendEmail(gameInvitationModel.EmailTo,
_stringLocalizer["Invitation for playing a Tic-Tac-Toe game"],
_stringLocalizer[$"Hello, you have been invited to play the
Tic-Tac-Toe game by {0}. For joining the game, please
click here {1}", gameInvitationModel.InvitedBy,
Url.Action("GameInvitationConfirmation", GameInvitation",
new { gameInvitationModel.InvitedBy,
gameInvitationModel.EmailTo }, Request.Scheme,
Request.Host.ToString())]);
var invitation = gameInvitationService.Add
(gameInvitationModel).Result;
return RedirectToAction("GameInvitationConfirmation",
new { id = invitation.Id }); }
return View(gameInvitationModel);
}
Don't forget to add the following using statement at the beginning of the class: using Microsoft.Extensions.DependencyInjection;, If you don't, the .GetService<IGameInvitationService>(); method can't be used and you will get build errors.
- 将名为
GameInvitationConfirmation的新方法添加到GameInvitationController:
[HttpGet]
public IActionResult GameInvitationConfirmation(Guid id,
[FromServices]IGameInvitationService gameInvitationService)
{
var gameInvitation = gameInvitationService.Get(id).Result;
return View(gameInvitation);
}
- 为之前添加的
GameInvitationConfirmation方法创建一个新视图。这将向用户显示一条等待消息:
@model TicTacToe.Models.GameInvitationModel
@{
ViewData["Title"] = "GameInvitationConfirmation";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h1>@Localizer["You have invited {0} to play
a Tic-Tac-Toe game
with you, please wait until the user is connected",
Model.EmailTo]</h1>
@section Scripts{
<script>
$(document).ready(function () {
GameInvitationConfirmation('@Model.Id');
});
</script>
}
- 在
scripts1.js文件中添加一个名为GameInvitationConfirmation的新方法。您可以使用与我们用于现有EmailConfirmation方法相同的基本结构:
function GameInvitationConfirmation(id) {
if (window.WebSocket) {
alert("Websockets are enabled");
openSocket(id, "GameInvitation");
}
else {
alert("Websockets are not enabled");
interval = setInterval(() => {
CheckGameInvitationConfirmationStatus(id);
}, 5000);
}
}
- 将名为
CheckGameInvitationConfirmationStatus的方法添加到scripts2.js文件中。您可以使用与我们用于现有CheckEmailConfirmationStatus方法相同的基本结构:
function CheckGameInvitationConfirmationStatus(id) {
$.get("/GameInvitationConfirmation?id=" + id,
function (data) {
if (data.result === "OK") {
if (interval !== null)
clearInterval(interval);
window.location.href = "/GameSession/Index/" + id;
}
});
}
- 更新
scripts2.js文件中的openSocket方法,增加具体的游戏邀请案例:
...
if (strAction == "Email") {
wsUri = protocol + "//" + window.location.host + "/CheckEmailConfirmationStatus";
operation = "CheckEmailConfirmationStatus";
}
else if (strAction == "GameInvitation") {
wsUri = protocol + "//" + window.location.host + "/GameInvitationConfirmation";
operation = "CheckGameInvitationConfirmationStatus";
}
var socket = new WebSocket(wsUri);
socket.onmessage = function (response) { console.log(response);
if (strAction == "Email" && response.data == "OK") {
window.location.href = "/GameInvitation?email=" + parameter;
}else if (strAction == "GameInvitation") {
var data = $.parseJSON(response.data);
if (data.Result == "OK") window.location.href = "/GameSession/Index/" + data.Id; } };
...
- 在通信中间件中增加一个名为
ProcessGameInvitationConfirmation的新方法。对于不支持此功能的浏览器,这将在不使用 WebSocket 的情况下处理游戏邀请请求:
private async Task ProcessGameInvitationConfirmation(HttpContext context)
{
var id = context.Request.Query["id"];
if (string.IsNullOrEmpty(id))await context.
Response.WriteAsync("BadRequest:Id is required");
var gameInvitationService = context.RequestServices.GetService
<IGameInvitationService>();
var gameInvitationModel = await
gameInvitationService.Get(Guid.Parse(id));
if (gameInvitationModel.IsConfirmed) await
context.Response.WriteAsync(
JsonConvert.SerializeObject(new
{
Result = "OK",
Email = gameInvitationModel.InvitedBy,
gameInvitationModel.EmailTo
}));
else {
await context.Response.WriteAsync(
"WaitGameInvitationConfirmation");
}
}
Don't forget to add the following using statement at the beginning of the class:
using Microsoft.Extensions.DependencyInjection;.
- 向通信中间件添加一个名为
ProcessGameInvitationConfirmation的新方法,该方法带有附加参数。这将处理游戏邀请请求,同时将 WebSocket 用于支持以下内容的浏览器:
private async Task ProcessGameInvitationConfirmation(HttpContext context,
WebSocket webSocket, CancellationToken ct,
string parameters)
{
var gameInvitationService = context.RequestServices.GetService
<IGameInvitationService>();
var id = Guid.Parse(parameters);
var gameInvitationModel = await gameInvitationService.Get(id);
while (!ct.IsCancellationRequested && !webSocket.
CloseStatus.HasValue &&
gameInvitationModel?.IsConfirmed == false) {
await SendStringAsync(webSocket, JsonConvert.
SerializeObject(new
{ Result = "OK",
Email = gameInvitationModel.InvitedBy,
gameInvitationModel.EmailTo,
gameInvitationModel.Id }), ct);
Task.Delay(500).Wait();
gameInvitationModel = await gameInvitationService.Get(id);
}
}
- 更新通信中间件中的
Invoke方法。从现在起,无论是否使用 WebSocket,这将适用于电子邮件确认和游戏邀请确认:
public async Task Invoke(HttpContext context)
{
if (context.WebSockets.IsWebSocketRequest)
{
...
switch (command.Operation.ToString())
{
...
case "CheckGameInvitationConfirmationStatus":
{ await
ProcessGameInvitationConfirmation(context,webSocket, ct,
command.Parameters.ToString());
break; }
}
}
else if (context.Request.Path.Equals
("/CheckEmailConfirmationStatus"))
{ await ProcessEmailConfirmation(context); }
else if (context.Request.Path.Equals
("/CheckGameInvitationConfirmationStatus"))
{ await ProcessGameInvitationConfirmation(context); }
else { await _next?.Invoke(context); }
}
在本节中,您学习了如何在 ASP.NET Core 3 web 应用中使用方法注入。这是注入服务的首选方法,您应该在适当的时候使用它。
您在 Tic-Tac-Toe 游戏的实现方面取得了很好的进展。几乎所有关于用户注册、电子邮件确认、游戏邀请和游戏邀请确认的内容都已经实现。
总结
在本章中,您了解了 ASP.NET Core 3 的一些更高级的概念,并实现了 Tic-Tac-Toe 应用中缺少的一些组件。
首先,您使用 JavaScript 创建了 Tic-Tac-Toe web 应用的客户端部分。我们探讨了如何通过使用捆绑和缩小以及用于实时通信场景的 WebSocket 来优化我们的 web 应用。
此外,您已经了解了如何从集成的用户和会话处理中获益,这在一个易于理解的示例中得到了展示。
然后,我们介绍了多语言用户界面、应用和服务配置的全球化和本地化,以及日志记录,以更好地了解运行时应用中发生的事情。
最后,通过一个实际示例,我们说明了如何一次性构建应用,然后根据部署目标,使用多个ConfigureServices和Configure方法以及多个Startup类的概念使其适应不同的环境。
在下一章中,我们将介绍使用 Razor 组件或 Blazor 的客户端开发,并将处理演示应用的日志记录。**
六、Razor 组件和 SignalR 简介
到目前为止,我们已经看到了 ASP.NET Core 3 中引入的许多更改,这些更改与以前版本的框架相比,包括.NET Core 的早期版本。在前面的章节中,我们已经通过使用我们的演示应用提到了这些,但现在是时候介绍一下 ASP.NET Core 3:服务器端 Blazor,以前称为Razor 组件的重要介绍内容了。
在第 5 章ASP.NET Core 3 的基本概念:第 2 部分的一节中,我们探讨了使用 JavaScript 进行客户端开发。对于许多习惯于使用 Microsoft 技术堆栈带来的强类型和其他语法优势的开发人员来说,Blazor 起到了解救作用。Blazor 是 JavaScript 的替代品。
Blazor 与.NET Core 集成为服务器端 Blazor,与 WebAssembly 集成为客户端 Blazor,使直接在浏览器上运行 C#并完全取代 JavaScript 成为可能。
WebAssembly (abbreviated to Wasm), as defined on its official page, https://webassembly.org/, is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable target for the compilation of high-level languages such as C/C++/Rust (and, in our case, C#), enabling deployment on the web for client and server applications.
客户端 Blazor 只在客户端工作。目前,在预览版中,它有望与更高版本的 ASP.NET Core 一起发布。
服务器端 Blazor 在这一点上依赖于现有的技术,Signal。一个简短的介绍是值得的,我们将在后面的章节中适当地介绍您需要了解的内容。
您将学习如何构建一个简单的 Blazor 应用,以及它与普通 ASP.NET Core 3 的区别。您将了解构成基本 Blazor 页面的组件。我们通过解释日志记录和遥测来完成本章,以帮助在生产环境中进行调试。您将了解在记录应用的重要信息时使用的不同选项,以及如何配置应用以使用文件记录器进行记录。
您将学习如何配置您的应用,以便可以在不同的托管环境中运行它,无论是在开发、暂存还是生产环境中。
本章将介绍以下主题:
- 使用 C#Razor 组件进行客户端开发
- 与信号员合作
- 使用测井和遥测进行监测和监督
- 一次构建并在多个环境中运行
使用 C#Razor 组件进行客户端开发
为了在.NET Core 框架上提供真正的完整堆栈体验,微软一直在尝试使用 C#Razor 组件进行客户端开发;在撰写本文时,这被称为服务器端 Blazor。将来有计划发布直接在 WebAssembly 上运行的客户端 Blazor,但这超出了本书的范围。
微软最初为 ASP.NETCore3 发布了一个 C#Razor 组件模板,但现在已经改名为 Blazor(服务器端)模板。
Note that C# Razor and Blazor (server-side) components are essentially the same. Agreement was reached on using the name server-side Blazor, influenced by the long-running previously experimental Blazor project: https://dotnet.microsoft.com/apps/aspnet/web-apps/client
服务器端 Blazor的名称可能会产生误导,因为 Blazor 主要是为了通过动态丰富的用户界面增强客户端开发。
让我们从 Tic Tac Toe 演示应用中休息一下,使用服务器端 BLAZOR 模板创建一个简单的 Web 应用。首先使用 ASP.NET Core 应用框架,然后使用 Blazor 模板创建一个新项目,如下所示:

创建 web 应用时,您会立即注意到它的项目结构通常类似于任何 ASP.NET Core 模板,具有以下Startup和Program类:

您将立即注意到的一个区别是新的.razor文件扩展名,用于标识任何 Razor 组件,例如App.razor,如前面的屏幕截图所示。我们将AddServerSideBlazor()方法调用到服务集合中的另一个差异是在ConfigureServices方法中的Startup类中发现的:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddServerSideBlazor();
...
}
在前面的代码块中,我们可以看到服务器端 Blazor 被添加为服务,接下来在Configure方法中,Blazor 的端点被配置,主要是为了通过MapBlazorHub()方法接受交互组件的传入连接:
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
按原样运行应用而不做任何更改,您应该能够在浏览器中看到以下内容:

主页显示 Hello,world!文本,它是在Index.razor组件中定义的,同样,我们还有Counter.razor和FetchData.razor组件,它们确定单击相应选项卡时显示的内容。
作为整个应用的共享组件,我们有MainLayout.razor,负责应用的布局,NavMenu.razor在左侧定义导航项。
在此示例模板中,Counter组件自行执行其所有功能,但FetchData组件依赖外部 C#service 类为其提供所需的数据。
如果您仔细查看计数器功能,大多数开发人员都希望使用 JavaScript,但您会注意到,Counter组件中没有 JavaScript。仔细查看 Razor 组件,您将发现没有 JavaScript 或任何对它的引用!事实上,在wwwroot部分,对于任何传统的 ASP.NET Core 模板,都会有一个js文件夹,这是我们BlazorDemo中明显的遗漏!这一切都是有意的;如果您像许多后端 C#开发人员一样不太擅长使用 JavaScript,Blazor 将让您如释重负!
Blazor 的设计方式是,作为开发人员,您将能够在客户端使用 C#而不是 JavaScript 的。必须注意的是,Blazor 还可以与 JavaScript 并行工作。
现在,让我们看一下以下代码块:
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" onclick="@IncrementCount">Click me</button>
@functions {
int currentCount = 0;
void IncrementCount()
{
currentCount++;
}
}
前面的代码片段表示 Blazor 应用中的基本页面组件。这一切都从路由开始,在路由中,页面的确切 URL 在@page指令之后指定。在前面的示例中,您可以通过向 URL 添加/counter来访问此页面,并将能够访问此特定页面。
在我们给body一些内容之前,就在@page指令下,如果您需要访问应用的另一部分,您可以有@using指令,例如@using BlazorDemo.Data。
您还可以使用 DI,如果需要,可以在页面中注入@inject WeatherForecastService ForecastService之类的服务。
最后,我们有@functions部分,您可以根据需要添加任意多的 C#函数,这通常是您放置 JavaScript 函数的地方。
服务器端 Blazor 之所以如此命名,是因为为了让前面的所有代码正常工作,它实际上是在服务器上执行的。它使用什么通信通道来确保实时渲染?我们将在下一节回答这个问题:
与信号员合作
SignalR 是一种为服务器端 Blazor 提供实时功能的技术。但首先,让我们试着了解这项技术到底是什么,以及所有东西是如何结合在一起的。
什么是信号员
SignalR 在引入 ASP.NET Core 框架系列版本之前就已经存在,它只是被设计为一个库,以满足服务器与其客户端之间的实时通信。
它利用并改进了 WebSockets 技术的使用,我们在上一章中已经介绍过。如果您阅读万维网(WWW),您会注意到,Signal 的大多数示例都解释了它在实时聊天应用上的用法,这是正确的,但也有很多应用场合可以使用它,包括仪表板、实时股票交易应用等。
SignalR 作为一种技术一直在发展和成熟,因此它与 Blazor 或 Razor 组件一起工作,后者的客户端特性依赖于与服务器的强大通信。我们将在下一节中解释这项技术如何为服务器端 Blazor 提供动力。
带有服务器端 Blazor 或 Razor 组件的 SignalR
我们已经简要介绍了服务器端 Blazor,但我们需要始终牢记的一点是,服务器端 Blazor 使用 SignalR 将内容从服务器端即时推送到客户端。SignalR 是一个库,用于处理 web 应用中需要实时、客户端和服务器交互的情况,使用集线器。
对于聊天应用,signar 的使用已经有很好的文档记录,但是关于何时使用 signar 的一个简单指南是查看需要从服务器进行大量更新的场景。在服务器端 Blazor 的情况下,客户端和服务器之间有很多更新,所有更新都是通过 Signal 进行的。下图说明了这一点:

对于这本涉及 ASP.NET Core 3 的书来说,只需了解围绕 Signal 使用的生态系统就足够了。您将遇到的大多数 signar 实现实际上都是在后台工作的(如服务器端 Blazor),是从您那里抽象出来的,因此我们将不深入研究如何使用它。
使用测井和遥测进行监测和监督
在开发应用时,您将使用一种著名的集成开发环境,如 Visual Studio 2019 或 Visual Studio 代码,如本书最初几章所述。你每天都这样做,你做的大部分事情都会变成第二天性,一段时间后你会自动完成它们。
例如,通过使用 Visual Studio 2019 的高级调试功能,您自然能够调试应用并了解运行时发生的事情。查找变量值、查看以何种顺序调用哪些方法、了解注入了哪些实例以及捕获异常,这些都是构建健壮且响应业务需求的应用的关键。
然后,在将应用部署到生产环境时,您突然错过了所有这些功能。您很少会发现安装了 VisualStudio 的生产环境,但会发生错误和意外行为,您需要能够尽快理解和修复它们。
这就是日志记录和遥测技术发挥作用的地方。通过在输入和离开方法中检测应用和日志记录,以及重要的变量值或在运行时认为重要的任何类型的信息,您将能够在应用日志中查看在发生问题时在生产环境中发生的情况。
在本节中,我们将回到我们的 Tic-Tac-Toe 演示应用,在这里我们将向您展示如何使用日志记录和异常处理来提供一个工业化的解决方案,以解决我们只在生产中遇到异常的问题,而没有更精细的细节来帮助您调试问题。
ASP.NET Core 3 为登录到以下目标提供内置支持:
- Azure 应用服务
- 安慰
- Windows 事件源
- 查出
- 调试器输出
- 应用见解
但是,默认情况下不支持文件、数据库和日志记录服务。如果要将日志发送到这些目标,则需要使用第三方记录器解决方案,如 Log4net、Serilog、NLog、Apache、ELMAH 或 Loggr。
您还可以通过实现ILoggerProvider接口轻松创建自己的提供者,如下所示:
- 向解决方案中添加一个新的类库(.NET Core)项目,并将其命名为
TicTacToe.Logging(删除自动生成的Class1.cs文件):

- 通过 NuGet 软件包管理器添加
Microsoft.Extensions.Logging和Microsoft.Extensions.Logging.ConfigurationNuGet 软件包:

- 添加来自
TicTacToeweb 应用项目的项目引用,以便我们可以使用TicTacToe.Logging类库中的资产:

- 将名为
LogEntry的新POCO(简称普通旧 CLR 对象)类添加到 TicTacToe.Logging 项目中。这将包含日志数据、事件id、实际日志的消息、日志级别、级别(信息、警告或关键),以及创建日志时的时间戳:
public class LogEntry
{
public int EventId { get; internal set; }
public string Message { get; internal set; }
public string LogLevel { get; internal set; }
public DateTime CreatedTime { get; internal set; }
}
- 添加一个名为
FileLoggerHelper的新类,该类将用于文件操作。然后我们添加字段定义和构造函数。构造函数确保每次实例化FileLoggerHelper时,都会强制接受文件名,并使其可用于InsertLog等内部方法,如下所示:
public class FileLoggerHelper
{
private string fileName;
public FileLoggerHelper(string fileName)
{
this.fileName = fileName;
}
static ReaderWriterLock locker = new ReaderWriterLock();
//....
}
}
然后,让我们向FileLoggerHelper类添加一个InsertLog方法。如果文件目录不存在,该方法将创建一个文件目录,在获取锁后将事件记录到文件中,然后在使用后释放它们。InsertLog的实施方式如下:
public void InsertLog(LogEntry logEntry)
{
var directory = System.IO.Path.GetDirectoryName(fileName);
if (!System.IO.Directory.Exists(directory))
System.IO.Directory.CreateDirectory(directory);
try
{
locker.AcquireWriterLock(int.MaxValue);
System.IO.File.AppendAllText(fileName,
$"{logEntry.CreatedTime} {logEntry.EventId} {logEntry.LogLevel}
{
logEntry.Message}" + Environment.NewLine);
}
finally
{
locker.ReleaseWriterLock();
}
添加一个名为FileLogger的新类并实现ILogger接口。file logger concrete 类将允许我们使用 Microsoft 在.NET Core framework 中提供的ILogger接口模板中提供的日志功能:
public sealed class FileLogger : ILogger
{
public IDisposable BeginScope<TState>(TState state)
{
return null;
}
public bool IsEnabled(LogLevel logLevel)
{
return (_filter == null || _filter(_categoryName, logLevel));
}
public void Log<TState>(LogLevel logLevel, EventId eventId,
TState state, Exception exception, Func<TState, Exception, string> formatter)
{
throw new NotImplementedException();
}
}
在实现Log方法之前,让我们先创建构造函数和字段定义。我们确保提供了类别名称、日志级别和文件名,并创建了一个新的FileLoggerHelper实例,如下所示:
private string _categoryName;
private Func<string, LogLevel, bool> _filter;
private string _fileName;
private FileLoggerHelper _helper;
public FileLogger(string categoryName, Func<string,
LogLevel,
bool> filter, string fileName)
{
_categoryName = categoryName;
_filter = filter;
_fileName = fileName;
_helper = new FileLoggerHelper(fileName);
}
然后是我们的主要Log方法,现在实现如下:
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (!IsEnabled(logLevel)) return;
if (formatter == null) throw new
ArgumentNullException(nameof(formatter));
var message = formatter(state, exception);
if (string.IsNullOrEmpty(message)) return;
if (exception != null) message += "\n" + exception.ToString();
var logEntry = new LogEntry
{
Message = message,
EventId = eventId.Id,
LogLevel = logLevel.ToString(),
CreatedTime = DateTime.UtcNow
};
_helper.InsertLog(logEntry);
}
- 添加一个名为
FileLoggerProvider的新类并实现ILoggerProvider接口。用于在 ASP.NET Core 需要时提供ILogger的FileLogger实例,稍后注入:
public class FileLoggerProvider : ILoggerProvider
{
private readonly Func<string, LogLevel, bool> _filter;
private string _fileName;
public FileLoggerProvider(Func<string, LogLevel, bool>
filter, string fileName)
{
_filter = filter;
_fileName = fileName;
}
public ILogger CreateLogger(string categoryName)
{
return new FileLogger(categoryName, _filter, _fileName);
}
public void Dispose() { }
}
- 为了简化从 web 应用调用文件日志提供程序,我们需要添加一个名为
FileLoggerExtensions的静态类(配置部分、文件名和日志详细程度作为参数):
public static class FileLoggerExtensions
{
const long DefaultFileSizeLimitBytes = 1024 * 1024 *
1024;
const int DefaultRetainedFileCountLimit = 31;
}
我们的FileLoggerExtensions类在AddFile方法上有三种不同的重载。现在,让我们添加AddFile方法的第一个实现:
public static ILoggingBuilder AddFile(this ILoggingBuilder loggerBuilder, IConfigurationSection configuration)
{
if (loggerBuilder == null) throw new
ArgumentNullException(nameof(loggerBuilder))
if (configuration == null) throw new
ArgumentNullException(nameof(configuration));
var minimumLevel = LogLevel.Information;
var levelSection = configuration["Logging:LogLevel"];
if (!string.IsNullOrWhiteSpace(levelSection))
{
if (!Enum.TryParse(levelSection, out minimumLevel))
{
System.Diagnostics.Debug.WriteLine("The minimum level setting
`{0}` is invalid", levelSection);
minimumLevel = LogLevel.Information;
}
}
return loggerBuilder.AddFile(configuration[
"Logging:FilePath"], (category, logLevel) => (logLevel >=
minimumLevel), minimumLevel);
}
然后是AddFile方法的第二个过载:
public static ILoggingBuilder AddFile(this ILoggingBuilder
loggerBuilder, string filePath, Func<string, LogLevel,
bool> filter, LogLevel minimumLevel =
LogLevel.Information)
{
if (String.IsNullOrEmpty(filePath)) throw
new ArgumentNullException(nameof(filePath));
var fileInfo = new System.IO.FileInfo(filePath);
if (!fileInfo.Directory.Exists)
fileInfo.Directory.Create();
loggerBuilder.AddProvider(new FileLoggerProvider
(filter, filePath));
return loggerBuilder;
}
然后,AddFile方法有第三个重载实现:
public static ILoggingBuilder AddFile(this ILoggingBuilder
loggerBuilder, string filePath, LogLevel minimumLevel =
LogLevel.Information)
{
if (String.IsNullOrEmpty(filePath)) throw
new ArgumentNullException(nameof(filePath));
var fileInfo = new System.IO.FileInfo(filePath);
if (!fileInfo.Directory.Exists)
fileInfo.Directory.Create();
loggerBuilder.AddProvider(new FileLoggerProvider
((category,
logLevel) => (logLevel >= minimumLevel), filePath));
return loggerBuilder;
}
- 在
TicTacToeweb 项目中,向Options文件夹中添加两个名为LoggingProviderOption和LoggingOptions的新选项:
public class LoggingProviderOption
{
public string Name { get; set; }
public string Parameters { get; set; }
public int LogLevel { get; set; }
}
public class LoggingOptions
{
public LoggingProviderOption[] Providers { get; set; }
}
- 在
TicTacToeweb 项目中,将名为ConfigureLoggingExtension的新扩展添加到Extensions文件夹中:
public static class ConfigureLoggingExtension
{
public static ILoggingBuilder AddLoggingConfiguration(this
ILoggingBuilder loggingBuilder, IConfiguration
configuration)
{
var loggingOptions = new LoggingOptions();
configuration.GetSection("Logging").
Bind(loggingOptions);
foreach (var provider in loggingOptions.Providers)
{
switch (provider.Name.ToLower())
{
case "console": { loggingBuilder.AddConsole();
break;
}
case "file": {
string filePath = System.IO.Path.Combine(
System.IO.Directory.GetCurrentDirectory(),
"logs",
$"TicTacToe_{System.DateTime.Now.ToString(
"ddMMyyHHmm")}.log");
loggingBuilder.AddFile(filePath,
(LogLevel)provider.LogLevel);
break;
}
default: { break; }
}
}
return loggingBuilder;
}
}
- 进入
TicTacToeweb 应用项目的Program类,更新BuildWebHost方法,调用扩展:
public static IHostBuilder CreateHostBuilder(string[] args)
=>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.CaptureStartupErrors(true);
webBuilder.PreferHostingUrls(true);
webBuilder.UseUrls("http://localhost:5000");
webBuilder.ConfigureLogging((hostingcontext,
logging) =>
{
logging.AddLoggingConfiguration(
hostingcontext.Configuration);
});
});
Don't forget to add the following using statement at the beginning of the class:
using TicTacToe.Extensions;.
- 在
appsettings.json文件中添加一个名为Logging的新节:
"Logging": {
"Providers": [
{
"Name": "Console",
"LogLevel": "1"
},
{
"Name": "File",
"LogLevel": "2"
}
],
"MinimumLevel": 1
}
- 启动应用并验证是否已在应用文件夹中名为
logs的文件夹中创建了新的日志文件:

这是第一步,简单易懂,很快就能完成。现在您有了一个日志文件,可以将日志写入其中。您将看到,使用集成的日志功能从 ASP.NET Core 3 应用中的任何位置创建日志(包括Controllers、Services等)都同样容易。
让我们快速向 Tic-Tac-Toe 应用添加一些日志:
- 更新
UserRegistrationController构造函数实现,以便我们为整个控制器提供一个记录器实例:
readonly IUserService _userService;
readonly IEmailService _emailService;
readonly ILogger<UserRegistrationController> _logger;
public UserRegistrationController(IUserService userService,
IEmailService emailService,
ILogger<UserRegistrationController>
logger)
{
_userService = userService;
_emailService = emailService;
_logger = logger;
}
- 更新
UserRegistrationController中的EmailConfirmation方法,并在方法开始处添加日志:
_logger.LogInformation($"##Start## Email confirmation
process for {email}");
- 更新
EmailService实现,并将记录器添加到其构造函数中,以使其可用于电子邮件服务:
public class EmailService : IEmailService
{
private EmailServiceOptions _emailServiceOptions;
readonly ILogger<EmailService> _logger;
public EmailService(IOptions<EmailServiceOptions>
emailServiceOptions, ILogger<EmailService> logger)
{
_emailServiceOptions = emailServiceOptions.Value;
_logger = logger;
}
}
然后将EmailService中的SendMail方法替换为:
public Task SendEmail(string emailTo, string subject,
string message)
{
try
{
_logger.LogInformation($"##Start sendEmail## Start
sending Email to {emailTo}");
using (var client = new
SmtpClient(_emailServiceOptions.MailServer,
int.Parse(_emailServiceOptions.MailPort)))
{
if (bool.Parse(_emailServiceOptions.UseSSL)
== true)
client.EnableSsl = true;
if (!string.IsNullOrEmpty
(_emailServiceOptions.UserId))
client.Credentials = new NetworkCredential
(_emailServiceOptions.UserId,
_emailServiceOptions.Password);
client.Send(new MailMessage
("ken@afrikancoder.co.za", emailTo, subject,
message));
}
}
catch (Exception ex) { _logger.LogError($"Cannot
send email {ex}"); }
return Task.CompletedTask;
}
- 然后,在运行应用并注册新用户后,打开生成的日志文件并分析其内容:

您会注意到,电子邮件确认过程的开始和发送电子邮件的开始都已正式记录在日志中。发送电子邮件本身的失败也被记录为异常及其堆栈跟踪。
一次构建并在多个环境中运行
在构建应用之后,您必须考虑将它们部署到不同的环境中。正如您在前面关于配置的部分中所看到的,您可以使用配置文件来更改服务甚至应用的配置。
在多个环境的情况下,您必须为每个环境复制appsettings.json文件,并相应地将其命名为:appsettings.{EnvironmentName}.json。
ASP.NET Core 3 将按照分层顺序自动检索配置设置,首先从公共appsettings.json文件,然后从相应的appsettings.{EnvironmentName}.json文件,同时在必要时添加或替换值。
然而,开发基于不同部署环境和配置使用不同组件的条件代码一开始似乎很复杂。在传统的应用中,您必须创建大量代码来自行处理所有不同的操作,然后对其进行维护。
在 ASP.NET Core 3 中,您可以使用大量的内部功能来实现此目标。然后,您可以简单地使用环境变量(开发、登台、生产等)来指示特定的运行时环境,从而为该环境配置应用。
正如您将在本节中看到的,您可以使用特定的方法名甚至类名来使用 ASP.NET Core 3 现成提供的现有注入和重写机制来配置应用。
在下面的示例中,我们将向应用添加特定于环境的组件(SendGrid),只有当应用部署到特定的生产环境(Azure)时,才需要使用该组件:
- 将 Sendgrid NuGet 包添加到项目中。这将用于 Tic Tac Toe 应用的未来 Azure 生产部署:

- 在
Services文件夹中添加名为SendGridEmailService的新服务。这将用于通过SendGrid发送电子邮件。让它继承IEmailService接口并实现具体的SendEmail方法。首先,建造商:
public class SendGridEmailService : IEmailService
{
private EmailServiceOptions _emailServiceOptions;
private ILogger<EmailService> _logger;
public SendGridEmailService(IOptions<EmailServiceOptions>
emailServiceOptions, ILogger<EmailService> logger)
{
_emailServiceOptions = emailServiceOptions.Value;
_logger = logger;
}
//....
}
- 然后在同一
SendGridEmailService类中添加SendMail方法:
public Task SendEmail(string emailTo, string subject, string
message)
{
_logger.LogInformation($"##Start## Sending email via
SendGrid to :{emailTo} subject:{subject} message:
{message}");
var client = new SendGrid.SendGridClient(_emailServiceOptions.
RemoteServerAPI);
var sendGridMessage = new SendGrid.Helpers.Mail.SendGridMessage
{
From = new SendGrid.Helpers.Mail.EmailAddress(
_emailServiceOptions.UserId)
};
sendGridMessage.AddTo(emailTo);
sendGridMessage.Subject = subject;
sendGridMessage.HtmlContent = message;
client.SendEmailAsync(sendGridMessage);
return Task.CompletedTask;
}
- 添加新的扩展方法,以便更轻松地为特定环境声明特定的电子邮件服务。为此,请转到
Extensions文件夹并添加一个新的EmailServiceExtension类:
public static class EmailServiceExtension
{
public static IServiceCollection AddEmailService(
this IServiceCollection services, IHostingEnvironment
hostingEnvironment, IConfiguration configuration)
{
services.Configure<EmailServiceOptions>
(configuration.GetSection("Email"));
if (hostingEnvironment.IsDevelopment() ||
hostingEnvironment.IsStaging())
{
services.AddSingleton<IEmailService, EmailService>();
}
else
{
services.AddSingleton<IEmailService,
SendGridEmailService>();
}
return services;
}
}
- 更新
Startup类以使用创建的资产。为了更好的可读性和可维护性,我们将更进一步,为我们必须支持的每个环境创建一个专用的ConfigureServices方法,删除现有的ConfigureServices方法,并添加以下特定于环境的ConfigureServices方法。首先,我们配置定义和构造函数:
public IConfiguration _configuration { get; }
public IHostingEnvironment _hostingEnvironment { get; }
public Startup(IConfiguration configuration,
IHostingEnvironment hostingEnvironment)
{
_configuration = configuration;
_hostingEnvironment = hostingEnvironment;
}
其次,我们配置公共服务:
public void ConfigureCommonServices(IServiceCollection services)
{
services.AddLocalization(options => options.ResourcesPath =
"Localization");
services.AddMvc().AddViewLocalization(
LanguageViewLocationExpanderFormat.Suffix, options =>
options.ResourcesPath = "Localization").AddDataAnnotationsLocalization();
services.AddSingleton<IUserService, UserService>();
services.AddSingleton<IGameInvitationService,
GameInvitationService>();
services.Configure<EmailServiceOptions>
(_configuration.GetSection("Email"));
services.AddEmailService(_hostingEnvironment, _configuration);
services.AddRouting();
services.AddSession(o =>
{
o.IdleTimeout = TimeSpan.FromMinutes(30);
});
}
最后,我们配置了一些特定的服务:
public void ConfigureDevelopmentServices(
IServiceCollection services)
{
ConfigureCommonServices(services);
}
public void ConfigureStagingServices(
IServiceCollection services)
{
ConfigureCommonServices(services);
}
public void ConfigureProductionServices(
IServiceCollection services)
{
ConfigureCommonServices(services);
}
Note that you could also apply the same approach to the Configure method in the Startup class. For that, you just remove the existing Configure method and add new methods for the environments you would like to support, such as ConfigureDevelopment, ConfigureStaging, and ConfigureProduction. The best practice would be to combine all common code into a ConfigureCommon method and call it from the other methods, as shown here for specific ConfigureServices methods.
- 按F5启动应用,并确认所有程序仍在正确运行。您应该看到添加的方法将自动使用,并且应用功能齐全。
那很简单,很直接!没有特定的环境条件代码,没有复杂的进化和维护;只是非常清晰和容易理解的方法,其中包含了开发它们的环境名称。这是一个非常干净的解决方案,可以解决一次构建并在多个环境中运行的问题。
但这还不是全部!如果我们告诉你,你不需要一节Startup课怎么办?如果您可以为每个环境提供一个专用的Startup类,其中只包含适用于其上下文的代码,该怎么办?那太好了,对吧?这正是 ASP.NETCore3 提供的。
为了能够为每个环境使用专用的Startup类,您只需更新Program类,它是 ASP.NET Core 3 应用的主要入口点。您可以在BuildWebHost方法中更改一行来传递程序集名称.UseStartup("TicTacToe")而不是.UseStartup<Startup>(),然后您就可以使用这个奇妙的功能:
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.CaptureStartupErrors(true)
.UseStartup("TicTacToe")
.PreferHostingUrls(true)
.UseUrls("http://localhost:5000")
.UseApplicationInsights()
.Build();
}
}
现在,您可以为不同的环境添加专用的Startup类,如StartupDevelopment、StartupStaging和StartupProduction。与之前的方法一样,它们将自动使用;在你这方面不需要做任何其他事情。只需更新Program类,实现特定于环境的Startup类,就可以了。ASP.NET Core 3 提供了这些有用的功能,使我们的生活变得更加轻松。
总结
在本章中,我们介绍了服务器端 Blazor,它在各个方面都值得一本书,如果您想熟练使用 ASP.NET Core 中的新添加内容,我们鼓励您查找有关 Blazor 的额外阅读材料。我们研究了如何使用服务器端 Blazor 创建基本应用,以及每个 Blazor 应用最重要的组件。
然后,我们介绍了 signar 作为使服务器端 Blazor 工作的底层技术之一,并且再次建议更高级的读者阅读更多内容。对于大多数开发人员来说,现在只需了解 signar 如何适应 Blazor 生态系统就可以了。
我们研究了遥测和日志技术,以帮助我们在生产中发布应用时解决问题。我们详细了解了如何配置和使用文件日志记录。
最后,我们向您介绍了一个事实,即您可以根据环境更改设置,甚至配置要运行的服务。
从第一章开始,我们已经了解了 ASP.NET Core 3 最重要的构建块的演练和高级视图,现在可以开始更详细的讨论,在下一章中从 ASP.NET Core MVC 开始。
七、创建 ASP.NET Core MVC 应用
当今大多数现代 web 应用都基于模型-视图-控制器模式,也称为MVC。您可能已经注意到,我们在前面的章节中也使用了它来构建 Tic-Tac-Toe 演示应用的基础。因此,您已经在多个地方使用了 MVC 体系结构,甚至不知道背景中发生了什么,也不知道应用此特定模式的重要性。
自 2007 年第一次发布以来,ASP.NET MVC 框架多年来一直在证明自己,直到有效地成为市场标准。微软已经成功地将它发展成为一个工业化的、高效的、具有高开发效率的框架。有许多 web 应用的例子充分利用了 MVC 提供的多种功能。一个很好的例子是堆栈溢出。它为开发人员提供信息,拥有非常高的用户基础,需要同时扩展到数千甚至数百万用户。
在本章中,您将获得创建 MVC 应用的技能,并确定什么样的设备正在访问您的应用。您还将学习如何使用标记帮助器、创建视图组件和局部视图,并将掌握创建单元测试和集成测试所需的技能。
首先,我们将从一个高级视图开始剖析 MVC 应用由什么组成,然后进一步深入到更精细的细节,例如视图页面和组件。然后,我们将研究视图引擎、如何构建项目以及如何分层项目。
在本章中,我们将介绍以下主题:
- 理解模型-视图-控制器模式
- 为多个设备创建专用布局
- 了解 ASP.NET Core 状态管理
- 使用视图页面、局部视图、视图组件和标记帮助器
- 将 web 应用划分为多个区域
- 应用视图引擎、单元测试和集成测试等高级概念
- 分层 ASP.NET Core 3 应用
理解模型-视图-控制器模式
MVC模式将应用分为三个主要层:模型、视图和控制器。这种模式的好处之一是关注点分离(SoC),也可以被描述为单一责任原则(SRP),这使得我们能够独立开发、调试和测试应用特性。
In software engineering, it is considered good practice to keep similar functionality as a unit. Mixing different responsibilities in a unit is considered an anti-pattern. There are other terms, such as heavy coupling, that describe a similar scenario where, for example, changing one aspect of a class requires changes in others to create a ripple effect. To avoid this effect, concepts of SoC and SRP were coined and are encouraged as they greatly help in unit testing as well, making sure the required functionality does what it purports to do.
当使用MVC模式时,用户请求被路由到控制器,该控制器将使用模型检索数据并执行操作。控制器为用户选择相应的视图,同时提供模型中的必要数据。如果某个层(例如,视图层)发生更改,则影响较小,因为它现在与应用的其他层(例如,控制器和模型)松散耦合。测试应用的不同层也容易得多。最终,通过使用此模式,您将获得更好的可维护性和更健壮的代码:

必须提到的是,这是一个简单的图表,显示了单元测试和集成测试的主要集中区域。以下章节将提供对每个组件及其与其他组件的关系的更好理解。
模型
模型包含逻辑数据结构以及应用的数据,与它们的视觉表示无关。在 ASP.NET Core 3 的上下文中,它还支持本地化和验证,如前几章所述。
可以使用视图和控制器在同一项目中创建模型,也可以在专用项目中创建模型,以便更好地组织我们的解决方案项目。
在 ASP.NET Core 3 中,脚手架使用模型自动生成视图。此外,可以使用模型将表单自动绑定到实体对象。在数据持久性方面,可以使用各种数据存储目标。对于数据库,您应该使用实体框架核心的对象关系映射器(ORM),这将在第 9 章、中介绍,使用实体框架核心 3访问数据。当我们使用 web API 时,模型是序列化的。
意见
视图提供应用的可视表示和用户界面元素。使用 ASP.NET Core 3 时,视图是使用 HTML、Razor 标记和 Razor 组件编写的。视图通常具有.cshtml文件扩展名,在使用第 6 章中介绍的Blazor 模板的情况下,引入 Razor 组件和 SignalR时,它们具有.razor文件扩展名。
视图包含完整的网页、网页部分(称为局部视图)或布局。在 ASP.NET Core 3 中,一个视图可以被划分为具有自己行为的逻辑子部分,称为视图组件。
此外,标记助手允许您将 HTML 代码集中并封装在一个标记中,并在所有应用中使用它。
ASP.NET Core 3 已经包含许多现有的标记帮助程序,可以提高开发人员的工作效率。
控制器
控制器管理模型和视图之间的交互。它提供应用的逻辑行为和业务逻辑。它为特定的用户请求选择必须呈现的视图。
一般来说,由于控制器提供主应用入口点,这意味着它们控制应用应如何响应用户请求。
单元测试
单元测试的主要目标是验证业务逻辑。通常情况下,单元测试被放入它们自己的外部单元测试项目中。有多种测试框架可供您使用,主要有 xUnit、NUnit 和 MSTest。
正如我们前面提到的,由于在使用 MVC 模式时,一切都是完全解耦的,因此您可以通过使用单元测试在任何时候独立于应用的其他部分测试控制器。
集成测试
应用功能的端到端验证通过集成测试完成。
集成测试检查从应用用户的角度来看,一切都按预期工作。因此,控制器及其相应视图将一起测试。
与单元测试一样,集成测试通常被放在自己的测试项目中,您可以使用各种测试框架(xUnit、NUnit 或 MSTest)。但是,对于这种类型的测试,您可能还需要使用 web 服务器自动化工具包,例如 Selenium。必须注意的是,在集成测试和功能测试之间有一条细线,这是其他开发人员可以互换使用的术语,但是,它们是不同的。功能测试比集成测试更复杂,因为它们用于端到端测试,涵盖了应用中的功能。集成测试主要是为了查看某些组件如何与不同的组件一起实际工作。换句话说,它们如何整合?
现在,我们已经了解了 MVC 应用的一般和高级组成,让我们获得一些使用 MVC 的实际经验。最好从风景开始。这是我们的用户将看到的;毕竟,我们都知道“顾客至上”这个词,对吗?我们的用户实际上可能更喜欢通过手机浏览我们的应用,而不是使用台式 PC。我们如何知道哪个设备正在访问我们的应用?我们如何使视图具有响应性,以便能够适应不同大小的浏览器屏幕?我们将在下一节中回答这些问题。
为多个设备创建专用布局
现代 web 应用使用 web 页面布局来提供一致和连贯的样式。将 HTML 与 CSS 结合使用来定义此布局是一种很好的做法。在 ASP.NET Core 3 中,通用网页布局定义集中在布局页面上。
布局页面通常称为_Layout.cshtml,包括所有常见的用户界面元素,如页眉、菜单、侧边栏和页脚。此外,常见的 CSS 和 JavaScript 文件在布局页面中被引用,以便它们可以在整个应用中使用。这允许您减少视图中的代码,从而帮助您应用不要重复自己(干燥原则。
从 Tic Tac Toe 演示应用的早期版本开始,我们就一直在使用布局页面,也就是说,当我们第一次在第 4 章中添加布局页面时,我们通过自定义应用添加了 ASP.NET Core 3 的基本概念:第 1 部分,以使我们的应用具有现代感,如您所见:

到目前为止,我们的应用或多或少都是为默认浏览器(即 PC)设置的。我们有没有办法根据各自的设备来区分代码?我们将在下一节中介绍实现这一点的步骤。
更详细地查看布局页面
在本节中,我们将更详细地查看布局页面,了解它是什么,以及如何利用其功能,以便我们可以为具有不同外形因素的多个设备(PC、手机、平板电脑等)创建专用布局。
在第 4 章、ASP.NET Core 3 的基本概念中,通过自定义应用:第 1 部分,我们在Views/Shared文件夹中添加了一个名为_Layout.cshtml的布局页面。打开此页面并分析其内容时,您可以看到它包含适用于应用中所有页面的通用元素(页眉、菜单、页脚、CSS、JavaScript 等):

版面页面中的公共标题部分包含 CSS 链接,但也包含s****搜索引擎优化(SEO)标签,如标题、描述和关键字。正如您已经看到的,ASP.NET Core 3 提供了一个简洁的功能,允许您通过环境标记(开发、登台、生产等)自动包含特定于环境的内容。
Bootstrap已经成为呈现菜单和导航栏组件的准标准,这就是为什么我们也将其用于 Tic-Tac-Toe 应用。
将公共 JavaScript 文件放在布局页面的底部是一种很好的做法;根据 ASP.NET Coreenvironment标记,它们也可以包含在内。
我们将使用Views/_ViewStart.cshtml文件在中心位置为所有页面定义布局页面。或者,如果要手动设置特定布局页面,可以在页面顶部进行设置:
@{
Layout = "_Layout";
}
要构造布局页面,可以定义节,以便组织某些页面元素(包括公共脚本节)的放置位置。一个例子是可以在布局页面中看到的脚本部分,我们在 Tic-Tac-Toe 应用的第一个示例中添加了该部分。默认情况下,它被放在页面底部,这是通过添加一个专用的元标记完成的:
@RenderSection("Scripts", required: false)
您还可以在视图中定义节,以便添加文件或客户端脚本。我们已经在 email confirmation 视图中这样做了,我们在其中添加了一个用于调用客户端 JavaScriptEmailConfirmation方法的部分:
@section Scripts{
<script>
$(document).ready(function () {
EmailConfirmation('@ViewBag.Email');
});
</script>
}
我们也可以使用NuGet 软件包。有一个名为DeviceDetector.NET.NetCore的软件包,它不仅能够检测到移动设备,而且能够检测到其他设备,包括台式机、电视机,甚至汽车。
我们可以使用以下命令通过 package Manager 控制台安装和使用前面的软件包:
Install-Package DeviceDetector.NET.NetCore
现在,让我们自己动手做移动检测功能吧!让我们学习如何优化移动设备的 Tic-Tac-Toe 应用。
移动设备的优化
遵循以下步骤,使我们的 Tic-Tac-Toe 演示应用对移动设备的响应能力越来越强,最终目标是拥有更方便的界面:
- 我们希望更改显示器,使其专门用于移动设备。为此,启动 Visual Studio 2019,转到解决方案资源管理器,创建一个名为
Filters的新文件夹,并添加一个名为DetectMobileFilter的新类,该类继承自IActionFilter接口。然后我们将创建MobileCheck和MobileVersionCheck正则表达式(正则表达式),如下所示:
static Regex MobileCheck = new Regex(@"android|(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer
|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle
|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian
|treo|up\.
(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled);
static Regex MobileVersionCheck = new Regex(@"1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)
|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled);
- 现在,让我们创建一个方法来检查用户代理是否是移动的:
public static bool IsMobileUserAgent( ActionExecuted
Context context)
{
var userAgent = context.HttpContext.
Request.Headers["User-Agent"].ToString();
if (context.HttpContext != null && userAgent != null)
{
if (userAgent.Length < 4)
return false;
if (MobileCheck.IsMatch(userAgent) ||
MobileVersionCheck.IsMatch(userAgent.
Substring(0, 4)))
return true;
}
return false;
}
- 我们从
IActionFilter开始执行OnActionExecuted方法,如下所示:
public void OnActionExecuted(ActionExecutedContext context)
{
var viewResult = (context.Result as ViewResult);
if (viewResult == null)
return;
if (IsMobileUserAgent(context))
{
viewResult.ViewData["Layout"] =
"~/Views/Shared/_LayoutMobile.cshtml";
}
else
{
viewResult.ViewData["Layout"] =
"~/Views/Shared/_Layout.cshtml";
}
}
- 复制现有的
Views/Shared/_Layout.cshtml文件,并将结果副本重命名为_LayoutMobile.cshtml。 - 通过添加两个名为
Desktop和Mobile的专用部分,更新主页索引视图,删除现有布局定义,并根据设备显示不同的标题:
@{
ViewData["Title"] = "Home Page";
}
<div class="row">
<div class="col-lg-12">
@section Desktop {<h2>@Localizer["DesktopTitle"]</h2>}
@section Mobile {<h2>@Localizer["MobileTitle"]</h2>}
<div class="alert alert-info">
- 当使用相应的设备时,将专门使用这些部分。这意味着,如果用户正在使用手机进行浏览,那么屏幕上将只显示
Mobile部分。
Note that you must also update all the other views of the application (GameInvitation/GameInvitationConfirmation, GameInvitation/Index, Home/Index, UserRegistration/EmailConfirmation, UserRegistration/Index) with the section tags from the preceding code for now:
@section Desktop{<h2>@Localizer["DesktopTitle"]</h2>}
@section Mobile {<h2>@Localizer["MobileTitle"]</h2>}
If you do not add them to your other views, you will get errors when you complete the steps that follow. However, this is only a temporary solution; later in this chapter, you will learn how to address this problem more effectively by using conditional statements.
- 更新资源文件。下面是英文主页索引资源文件的示例;您还应添加法语翻译:

- 修改
Views/Shared/_LayoutMobile.cshtml文件,用以下说明替换@RenderBody()元素;应显示Desktop部分,忽略Mobile部分:
@RenderSection("Desktop", required: false)
@{IgnoreSection("Mobile");}
@RenderBody()
- 修改
Views/Shared/_Layout.cshtml文件,用以下说明替换@RenderBody()元素;应显示Mobile部分,忽略Desktop部分:
@RenderSection("Mobile", required: false)
@{IgnoreSection("Desktop");}
@RenderBody()
- 转到
Views/_ViewStart.cshtml文件并更改所有网页的布局分配,以便能够使用前面代码中的布局定义:
@{Layout = Convert.ToString(ViewData["Layout"]);}
- 更新
Startup类,并将DetectMobileFilter作为参数添加到 MVC 服务注册中:
services.AddControllersWithViews(o =>
o.Filters.Add(typeof(DetectMobileFilter)))
- 在 Microsoft Edge 浏览器中正常启动 Tic Tac Toe 应用。请注意,本地化标题已将桌面显示为检测到的浏览器:

按F12打开 Microsoft Edge 中的开发者工具,进入仿真选项卡,选择移动设备:

现在,重新加载 Tic Tac Toe 应用;它将显示为您已在模拟设备上打开:

在本节中,您学习了如何为特定设备提供特定布局。现在,您将学习如何应用其他高级 ASP.NET Core 3 MVC 功能,以获得更好的生产效率和应用。但首先,让我们看看在状态管理中,在不同的请求、控制器和视图之间,我们可以使用什么。我们将不得不在某个时候处理所有这些问题,即使是在最基本的应用中。
了解 ASP.NET Core 状态管理
ASP.NET Core 3 web 应用通常是无状态的。每当服务器请求页面时,就会实例化一个全新的 Razor web 页面。因此,每次来回请求时,Razor 页面中的任何数据都会丢失。
为了更好地理解问题,我们已经查看了UserRegistration页面,如果我们使用开发工具检查浏览器,我们会注意到,当我们填写表单并提交时,我们的数据会发送到服务器,但数据不会在响应标题中返回到浏览器。
这是为 web 生成有意义的交互式应用的自然障碍,我们很幸运,ASP.NET Core 3 通过内置功能为我们解决了这一障碍。
我们可以通过提出以下一些问题来决定使用什么功能:
- 我们是否需要在请求之间保留大量数据?
- 用户客户端接受不同类型 cookie 的可能性有多大?
- 我们希望数据由服务器或客户端存储吗?
- 我们需要确保数据的安全性,因此所涉及的数据是否微妙?
- 我们是否在为无法承受高带宽使用的用户或客户端浏览器编写应用?
- 究竟什么样的设备将访问我们的应用?它们有限制吗?
- 每个应用用户都需要个性化数据吗?
- 我们需要应用数据保持多长时间?
- 我们是要在具有多个实例的分布式环境中托管应用,还是在具有单个实例的普通环境中托管应用?
让我们来看看我们管理国家的各种选择,以及它们的优势。
客户端状态管理选项
对于客户端状态管理,服务器根本不需要为应用中的任何来回请求存储任何数据。让我们更详细地看一下这些选项。
Please note that we're referring to either the Razor pages or the user's device when we talk about the client.
隐藏字段
隐藏字段是一种标准的 HTML 功能,不需要复杂的编程逻辑。隐藏字段广泛支持大多数 internet 浏览器。由于一个隐藏字段被持久化并从 Razor 视图页面读取,因此不需要服务器资源。
ASP.NET Core 3 允许我们使用隐藏字段。在隐藏字段中放置的任何内容都将始终以提交的形式与来自其他 HTML 元素的输入数据一起发送。这将从定义它们的位置发送。
您可以使用Hidden Field将数据保存在.cshtml页面上,并检测在回发之间存储在隐藏字段中的数据何时发生了更改。
It is recommended that you don't use a hidden field to keep sensitive data since the data can easily be revealed by right-clicking a web page and selecting View Page Source.
曲奇饼
Cookie 是应用通过各自的 web 浏览器存储在用户计算机上的微小数据。它们可用于保存自定义的客户端数据、用户在应用上的会话或应用数据。我们可以在 Cookie 上设置一个经过深思熟虑的持续时间,从毫秒到分钟、小时、天,甚至更长,这取决于您希望保留数据的时间。
在第 10 章保护 ASP.NET Core 3 应用中稍后讨论身份验证时,我们将了解 Cookie 与 ASP.NET Core Identity 协作的最重要用途之一。
We will deal with issues of security from Chapter 10, Securing ASP.NET Core 3 Applications, onward, but it is worth noting that cookies can present a vulnerability point to your application as they are often the targets of hackers. The best advice is to never store valuable information in your cookies but in tokens, which you use to locate your valuable data.
查询字符串
有时,我们可能会注意到在问号标记: ?之后的url中嵌入了一个键/值对。这表示正在使用的查询字符串。在网页之间导航时,查询字符串非常方便,您需要将一些数据传递到下一页。例如,可以传递游戏会话id或游戏的其他参数,我们可以为演示应用这样做:
https://example.net/gamesessions?id=002e6431-3eb5-4d98-b3d9-3263490ce7c0
我们必须记住始终保持url长度相对较短,即使现代浏览器最多有 2048 个字符。
查询字符串是一个很好的工具,但有些情况下我们不应该使用它们。我们将在下面的小节中更详细地讨论这一点。
查询字符串用法
当我们需要使用GET方法从 web 服务器请求数据时,应该使用查询字符串。请注意,它们不能用于使用POST方法向 web 服务器发送数据。
我们将在下一章学习更多关于GET和POST超文本传输协议(HTTP的方法,即第 8 章、创建 Web API 应用的方法。
Information that is passed in a query string is susceptible and can be tampered with by hackers. Please make sure you're not using query strings to pass important data. It must also be noted that an unsuspecting user can bookmark the URL or send the URL to other users, thereby passing that sensitive data in the URL along with it.
查询字符串、cookie 和隐藏字段是客户端状态管理选项的示例,但大多数 web 应用需要从服务器端管理状态。下一节将介绍用于从服务器管理应用状态的选项。
基于服务器的状态管理选项
ASP.NET Core 3 提供了多种方法来维护服务器上的状态信息,而不是在客户端保存数据。从服务器端管理状态时,服务器客户端调用会减少。这在资源方面也可能是昂贵的。以下部分将描述两个基于服务器的状态管理功能:应用状态和会话状态。
应用状态
应用状态在 ASP.NET Core 之前的早期框架中使用,例如在 ASP.NET MVC 4 中。它现在已经转变为 ASP.NET Core 3 中的应用状态。最简单的说,它只是应用在快照中的状态在时间上的表示。当我们引用应用状态时,在指定的时间,所有应用用户的状态都是相同的。这允许我们在所有客户机用户之间保持数据不变。
如果系统中的此类数据需要跨会话访问,并且每隔一段时间才更改一次,建议使用缓存。ASP.NET Core 3 更倾向于使用缓存而不是应用状态。
Caching in ASP.NET Core 3 is achieved by using IMemoryCache and adding services.AddMemoryCache() to the ConfigureServices method, but it's out of the scope of this book. You can find more information at the following link: https://docs.microsoft.com/en-us/aspnet/core/performance/caching/memory?view=aspnetcore-3.0.
会话状态
与应用状态相反,会话状态在用户使用应用期间保留特定于用户的数据。ASP.NET Core 3 使用会话中间件,在Startup类的ConfigureServices方法中通过添加services.AddSession()进行配置。
在应用中使用会话对象之前,您需要将app.UseSession()添加到Startup类中的Configure方法中。
通过这样做,您可以通过HttpContext.Session访问会话对象,您可以利用其方法获取或设置会话变量或属性。
我们已经提到,会话状态是一个基于服务器的状态管理选项,这意味着您设置的所有内容都将由服务器处理。请注意,您实际上可以将会话数据存储在 cookie 中,作为AddSession(option)方法中的一个选项,如下所示:
services.AddSession(options => {options.Cookie.Name = "yourCustomSessionName";});
在本书后面部分,我们将使用以下代码在游戏会话中使用会话状态:
services.AddSession(o =>
{
o.IdleTimeout = TimeSpan.FromMinutes(30);
});
前面的代码片段在ConfigureServices方法中向服务集合添加了一个 30 分钟超时的会话。为了能够使用这个会话,我们需要配置app.UseSession(),可以在Startup类的Configure方法中找到。当我们在第 8 章、创建 Web API 应用中配置游戏会话时,将介绍更多关于会话的内容,特别是在构建 RPC 风格的 Web API部分。
现在我们已经了解了状态管理的概念,让我们看看可以使用哪些不同类型的视图来为应用用户提供适当的用户界面(UI)和良好的用户体验(UX)。
使用视图页面、局部视图、视图组件和标记帮助器
ASP.NET Core 3 和 Razor 与 Visual Studio 2019 结合使用时,提供了一些可用于创建 MVC 视图的功能。在本节中,您将了解这些功能如何帮助您提高工作效率。例如,您可以使用 VisualStudio2019 的集成支架功能创建视图,您在前面的章节中已经多次使用了该功能。这允许您自动生成以下类型的视图:
- 查看页面
- 局部视图
除了查看页面和局部视图之外,有时还需要更高级的功能,这可以通过使用视图组件和标记帮助器来实现。
您想了解它们是什么,以及如何使用 Visual Studio 2019 高效地与它们协作吗?保持专注–我们将在本节中详细解释所有内容。
使用查看页面
视图页面用于根据操作呈现结果,并对 HTTP 请求做出响应。在 MVC 方法中,它们定义并封装应用的可见部分—表示层。此外,它们使用.cshtml文件扩展名,默认情况下存储在应用的Views文件夹中。
Visual Studio 2019 的脚手架功能提供了不同的查看页面模板,如您所见:
- 创建:生成插入数据的表单
- 编辑:生成数据更新表单
- 删除:生成一张显示记录的表单,表单上有一个确认删除记录数据的按钮
- 详细信息:生成一个表格,用于显示一条记录,有两个按钮,一个用于编辑表格,另一个用于删除显示的记录页面
- 列表:生成一个显示对象列表的 HTML 表
- 空:不使用任何模型生成空页面
如果您不想使用 Visual Studio 2019 生成页面视图,您可以通过自己将其添加到Views文件夹来手动实现。在这种情况下,建议您遵守 MVC 约定。因此,在匹配操作名称的同时,将它们添加到相应的子文件夹中。这有助于 ASP.NET 引擎查找手动创建的视图。
让我们为 Tic-Tac-Toe 游戏创建排行榜,看看所有这些都在起作用:
- 打开解决方案资源管理器,转到“视图”文件夹,并创建一个名为
Leaderboard的新子文件夹。然后,右键单击文件夹,从向导中选择添加|新建项目| Razor 视图页面,然后单击添加按钮:

- 打开创建的文件并清除其内容,通过在页面顶部添加以下说明将排行榜视图与用户模型关联:
@model IEnumerable<UserModel>。 - 很好的做法是设置其标题变量,使其显示在 SEO 标签中:
@{ViewData["Title"] = "Index";}。 - 使用
@section元标记添加新的两个部分Desktop和Mobile。然后,使用@()元标记添加上次更新的时间:
<div class="row">
<div class="col-lg-12">
@section Desktop {<h2>
@Localizer["DesktopTitle"] (
Last updated @(System.DateTime.Now))
</h2>}
@section Mobile {<h2>
@Localizer["MobileTitle"] (
Last updated @(System.DateTime.Now))
</h2>}
</div>
</div>
- 为排行榜视图添加英文和法文资源文件,并为
DesktopTitle和MobileTitle定义本地化。 - 右键点击
Controllers文件夹,选择添加|控制器,选择 MVC 控制器-空,点击添加按钮。命名为LeaderboardController:

- 将自动生成以下代码段:
public class LeaderboardController : Controller
{
public IActionResult Index()
{
return View();
}
}
Note that Razor matches views with actions with <actionname>.cshtml or <actionname>.<culture>.cshtml in the Views/<controllername> folder.
- 更新
Views/Shared文件夹中的_Layout.cshtml和_LayoutMobile.cshtml文件,并在Home元素之后添加一个 ASP.NET 标记帮助程序,用于调用navbar菜单中的新排行榜视图:
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-
controller="Leaderboard" asp-action="Index">Leaderboard</a>
</li>
- 启动应用并显示新的排行榜视图:

现在我们已经了解了基础知识,让我们看看使用 Razor 时的一些更高级的技术,例如代码块、控制结构和条件语句。
代码块@{}用于设置或计算变量和格式化数据。您已经在_ViewStart.cshtml文件中使用了它们来定义应该使用哪个特定布局页面:
@{
Layout = Convert.ToString(ViewData["Layout"]);
}
控制结构提供处理循环所需的一切。例如,您可以使用@for、@foreach、@while或@do来重复元素。它们的行为与 C#等价物完全相同。让我们使用它们来实现排行榜视图:
- 使用上述控制结构时,向排行榜视图添加新的 HTML 表:
<table class="table table-striped">
<thead> <tr>
<th>Name</th>
<th>Email</th>
<th>Score</th> </tr>
</thead>
<tbody>
@foreach (var user in Model)
{ <tr>
<td>@user.FirstName @user.LastName</td>
<td>@user.Email</td>
<td>@user.Score.ToString()</td> </tr>
}
</tbody>
</table>
- 在
IUserService界面添加一个新的GetTopUsers方法,用于检索将在排行榜视图中显示的顶级用户:
Task<IEnumerable<UserModel>> GetTopUsers(int numberOfUsers);
- 在
UserService内实施新的GetTopUsers方法:
public Task<IEnumerable<UserModel>> GetTopUsers(int
numberOfUsers)
{
return Task.Run(() =>
(IEnumerable<UserModel>)_userStore.OrderBy(x =>
x.Score).Take(numberOfUsers).ToList());
}
- 更新排行榜控制器,以便您可以调用新方法:
private IUserService _userService;
public LeaderboardController(IUserService userService)
{
_userService = userService;
}
public async Task<IActionResult> Index()
{
var users = await _userService.GetTopUsers(10);
return View(users);
}
- 按F5启动应用,注册多个用户,显示排行榜:

@if、@else if、@else和@switch等条件语句允许您有条件地呈现元素。它们的工作原理也与 C#同行完全相同。
As we mentioned previously, you need to define the Desktop and Mobile sections in all of your views, that is, @section Desktop { } and @section Mobile { }.
例如,如果您从排行榜索引视图中临时删除它们,并尝试在ASPNETCORE_ENVIRONMENT变量设置为'Development'时显示它们,以便激活开发者异常页面,您将收到以下错误消息:

这是因为我们更改了应用的Layout和Mobile布局页面,并使用了IgnoreSection指令。不幸的是,在使用IgnoreSection指令时,必须始终声明节。
但是现在您知道了条件语句的存在,您已经可以看到更好的解决方案了,对吗?是的,没错;我们必须在两个布局页面中用条件语句if包装IgnoreSection指令。
以下是您需要如何使用IsSectionDefined方法更新布局页面:
@RenderSection("Desktop", required: false)
@if(IsSectionDefined("Mobile")){IgnoreSection("Mobile");}
@RenderBody()
以下是您需要如何更新Mobile布局页面:
@RenderSection("Mobile", required: false)
@if(IsSectionDefined("Desktop")){IgnoreSection("Desktop");}
@RenderBody()
如果启动该应用,您将看到现在一切都按预期工作,但这次使用的是更干净、更优雅、更易于理解的解决方案。这是因为我们正在使用 ASP.NET Core 3 和 Razor 的内置功能。
使用局部视图
到目前为止,我们已经学习了如何使用 Razor 创建视图页面,但有时,我们必须在所有或部分视图页面中重复元素。如果我们可以在视图中创建可重用的组件,这不是很有帮助吗?毫不奇怪,ASP.NET Core 3 默认通过提供所谓的局部视图来实现此功能。
部分视图在调用视图页中呈现。与标准视图页面一样,它们也具有.cshtml文件扩展名。我们可以定义它们一次,然后在所有视图页面中使用它们。这是一个通过减少代码重复来优化代码的好方法,可以提高质量,减少维护!
现在让我们看看如何从中获益。为此,我们将优化布局和移动布局页面,使其仅使用一个菜单:
- 转到视图/共享文件夹并添加一个名为
_Menu.cshtml的新 MVC 视图页面。它将用作菜单“局部视图”:

- 从其中一个布局页面复制
nav bar并粘贴到菜单部分视图:
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-
controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-
controller="Leaderboard" asp-
action="Index">Leaderboard</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-
controller="Home" asp-action="Privacy">Privacy</a>
</li>
</ul>
</div>
- 将两个版面中的
nav bar替换为<partial name="_Menu" />。 - 启动应用并验证一切是否仍在工作。你不应该看到任何差异,但这是一件好事;现在,您已将菜单封装并集中在局部视图中。
使用视图组件
到目前为止,您已经学习了如何通过使用局部视图来创建可重用组件,可以从应用中的任何视图页面调用局部视图,并将此概念应用于 Tic-Tac-Toe 应用的顶部菜单。但有时,即使是这个功能也不够。
有时,您需要一些更强大、更灵活的功能,可以在整个 web 应用中使用,甚至可以在多个 web 应用中使用。这就是视图组件发挥作用的地方。
视图组件用于复杂的用例,这些用例需要在服务器上运行一些代码(例如,登录面板、标记云和购物车),其中部分视图太有限,无法使用,并且您需要能够广泛测试功能。
在下面的示例中,我们将添加一个用于管理游戏会话的视图组件。您将看到它与标准控制器实现非常相似:
- 将名为
TurnModel的新模型添加到Models文件夹:
public class TurnModel
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public UserModel User { get; set; }
public int X { get; set; }
public int Y { get; set; }
}
- 将名为
GameSessionModel的新模型添加到Models文件夹:
public class GameSessionModel
{
public Guid Id { get; set; }
public Guid UserId1 { get; set; }
public Guid UserId2 { get; set; }
public UserModel User1 { get; set; }
public UserModel User2 { get; set; }
public IEnumerable<TurnModel> Turns { get; set; }
public UserModel Winner { get; set; }
public UserModel ActiveUser { get; set; }
public Guid WinnerId { get; set; }
public Guid ActiveUserId { get; set; }
public bool TurnFinished { get; set; }
}
- 在
Services文件夹中添加一个名为GameSessionService的新服务,并实现,提取IGameSessionService接口:
public class GameSessionService : IGameSessionService
{
private static ConcurrentBag<GameSessionModel> _sessions;
static GameSessionService()
{
_sessions = new ConcurrentBag<GameSessionModel>();
}
public Task<GameSessionModel> GetGameSession(Guid
gameSessionId)
{
return Task.Run(() => _sessions.FirstOrDefault(
x => x.Id == gameSessionId));
}
}
- 在
Startup类中注册GameSessionService,就像您在所有其他服务中一样:
services.AddSingleton<IGameSessionService, GameSessionService>();
- 转到解决方案资源管理器,创建一个名为
Components的新文件夹,并向其中添加一个名为GameSessionViewComponent.cs的新类:
[ViewComponent(Name = "GameSession")]
public class GameSessionViewComponent : ViewComponent
{
IGameSessionService _gameSessionService;
public GameSessionViewComponent(IGameSessionService
gameSessionService)
{
_gameSessionService = gameSessionService;
}
public async Task<IViewComponentResult> InvokeAsync(Guid
gameSessionId)
{
var session = await _gameSessionService.
GetGameSession(gameSessionId);
return View(session);
}
}
- 转到解决方案资源管理器,在
Views/Shared文件夹中创建一个名为Components的新文件夹。在此文件夹中,为GameSessionViewComponent创建一个名为GameSession的新文件夹。然后,手动添加一个名为default.cshtml的新视图:
@model TicTacToe.Models.GameSessionModel
@{ var email = Context.Session.GetString("email"); }
@if (Model.ActiveUser?.Email == email)
{<table>
@for (int rows = 0; rows < 3; rows++) {<tr style="height:150px;">
@for (int columns = 0; columns < 3; columns++)
{<td style="width:150px; border:1px solid #808080">
@{var position = Model.Turns?.FirstOrDefault(turn => turn.X ==
columns && turn.Y == rows);
if (position != null) { if (position.User?.Email == "Player1")
{<i class="glyphicon glyphicon-unchecked"
style="width:100%;height:100%"></i> }
else{<i class="glyphicon glyphicon-remove-circle"
style="width:100%;height:100%"></i> }
} else{ <a asp-action="SetPosition"
asp-controller="GameSession"
asp-route-id="@Model.Id" asp-route-email="@email"
class="btn btn-default" style="width:150px;
min-height:150px;">
</a> } } </td> } </tr> }
</table>
}else{
<div class="alert">
<i class="glyphicon glyphicon-alert">Please wait until the other user has finished his turn.</i> </div> }
如果你不是活跃用户,这个视图会告诉你等待轮到你;否则,它会给你一张桌子,你可以在那里玩TicTacToe游戏。
We advise using the following syntax to put all partial views for your View Components in their corresponding folders:
Views\Shared\Components\<ViewComponentName>\<ViewName>.
- 使用
@addTagHelper *, TicTacToe命令更新_ViewImports.cshtml文件以使用视图组件。 - 在
Views文件夹中创建一个名为GameSession的新文件夹。然后,为Desktop部分添加一个名为Index的新视图,如下所示:
@model TicTacToe.Models.GameSessionModel
@section Desktop
{<h1>Game Session @Model.Id</h1>
<h2>Started at @(DateTime.Now.ToShortTimeString())</h2>
<div class="alert alert-info">
<table class="table">
<tr>
<td>User 1:</td>
<td>@Model.User1?.Email (<i class="glyphicon
glyphicon-unchecked"></i>) </td>
</tr>
<tr>
<td>User 2:</td>
<td>@Model.User2?.Email (<i class=" glyphicon
glyphicon-remove-circle"></i></td>
</tr>
</table>
</div>}
现在,对Mobile部分执行同样的操作,如下所示:
@section Mobile{
<h1>Game Session @Model.Id</h1>
<h2>Started at @(DateTime.Now.ToShortTimeString())</h2>
User 1: @Model.User1?.Email <i class="glyphicon glyphicon-
unchecked"></i><br />
User 2: @Model.User2?.Email (<i class="glyphicon glyphicon-
remove-circle"></i>)
}
<vc:game-session game-session-id="@Model.Id"></vc:game-session>
- 在
GameSessionService中添加一个公共构造函数,以便获得用户服务的实例:
private IUserService _UserService;
public GameSessionService(IUserService userService)
{
_UserService = userService;
}
- 在
GameSessionService中增加创建游戏会话的方法,更新游戏会话服务界面:
public async Task<GameSessionModel> CreateGameSession( Guid invitationId, string invitedByEmail, string invitedPlayerEmail)
{
var invitedBy =
await _UserService.GetUserByEmail(invitedByEmail);
var invitedPlayer =
await _UserService.GetUserByEmail(invitedPlayerEmail);
GameSessionModel session = new GameSessionModel
{
User1 = invitedBy,
User2 = invitedPlayer,
Id = invitationId,
ActiveUser = invitedBy
};
_sessions.Add(session);
return session;
}
- 在
Controllers文件夹中添加一个名为GameSessionController的新控制器,并实现一个新的Index方法:
private IGameSessionService _gameSessionService;
public GameSessionController(IGameSessionService
gameSessionService)
{ _gameSessionService = gameSessionService; }
public async Task<IActionResult> Index(Guid id)
{
var session = await _gameSessionService.
GetGameSession(id);
if (session == null)
{
var gameInvitationService =
Request.HttpContext.
RequestServices.GetService
<IGameInvitationService>();
var invitation = await gameInvitationService.
Get(id);
session = await _gameSessionService.
CreateGameSession(
invitation.Id, invitation.InvitedBy,
invitation.EmailTo);
}
return View(session);
}
- 启动应用,注册新用户,并邀请其他用户玩游戏。等待新游戏会话页面显示,如下所示:

在本节中,我们学习了如何实现名为 View Components 的高级功能。在下一节中,我们将看一看另一个高级且令人兴奋的特性,称为标记帮助器。保持专注。
使用标记助手
自 ASP.NET Core 2+以来,标记帮助器是一项相对较新的功能,允许在创建和呈现 HTML 元素时使用服务器端代码。可以将它们与用于呈现 HTML 内容的著名 HTML 帮助程序进行比较。
ASP.NET Core 3 提供了许多内置的标记帮助程序,如ImageTagHelper和LabelTagHelper,您可以在应用中使用它们。创建自己的标记帮助程序时,可以基于元素名称、属性名称或父标记将 HTML 元素作为目标。然后,您可以在视图中使用标准 HTML 标记,同时在 web 服务器上应用用 C#编写的表示逻辑。
此外,您甚至可以创建自定义标记。您可以在 Tic-Tac-Toe 演示应用中使用这些工具。让我们了解如何创建自定义标记:
- 打开解决方案资源管理器并创建一个名为
TagHelpers的新文件夹。然后,添加一个名为GravatarTagHelper.cs的新类,该类实现TagHelper基类。 - 实现
GravatarTagHelper.cs类;它将用于连接到 Gravatar 在线服务,以便为用户检索帐户照片。让我们从课程的管道开始:
[HtmlTargetElement("Gravatar")]
public class GravatarTagHelper : TagHelper
{
private ILogger<GravatarTagHelper> _logger;
public GravatarTagHelper(ILogger<GravatarTagHelper> logger)
{
_logger = logger;
}
public string Email { get; set; }
...
}
现在我们可以实现Process方法,如下所示:
public override void Process(TagHelperContext context, TagHelperOutput output)
{
byte[] photo = null;
if (CheckIsConnected())
{
photo = GetPhoto(Email);
}
else
{
photo = File.ReadAllBytes(Path.Combine(
Directory.GetCurrentDirectory(),
"wwwroot", "images", "no-photo.jpg"));
}
string base64String = Convert.ToBase64String(photo);
output.TagName = "img";
output.Attributes.SetAttribute("src",
$"data:img/jpeg;base64,{base64String}");
}
Process方法需要一个名为CheckIsConnected的方法来检查连接,该方法可以实现如下:
private bool CheckIsConnected()
{
try
{
using (var httpClient = new HttpClient())
{
var gravatarResponse = httpClient.GetAsync(
"http://www.gravatar.com/avatar/").Result;
return (gravatarResponse.IsSuccessStatusCode);
}
}
catch (Exception ex)
{
_logger?.LogError($"Cannot check the gravatar
service status: { ex} ");
return false;
}
}
我们还需要一个GetPhoto方法,如下所示:
private byte[] GetPhoto(string email)
{
var httpClient = new HttpClient();
return httpClient.GetByteArrayAsync(
new Uri($"http://www.gravatar.com/avatar/ {
HashEmailForGravatar(email) }")).Result;
}
最后,我们需要一个HashEmailForGravatar方法,如下所示:
private static string HashEmailForGravatar(string email)
{
var md5Hasher = MD5.Create();
byte[] data = md5Hasher.ComputeHash(
Encoding.ASCII.GetBytes(email.ToLower()));
var stringBuilder = new StringBuilder();
for (int i = 0; i < data.Length; i++)
{
stringBuilder.Append(data[i].ToString("x2"));
}
return stringBuilder.ToString();
}
- 打开
Views/_ViewImports.cshtml文件,确认addTagHelper指令存在。如果没有使用@addTagHelper *, TicTacToe命令将其添加到文件中。 - 更新
GameInvitationController中的Index方法,存储用户的电子邮件,并在会话变量中显示其姓名(名字和姓氏):
[HttpGet]
public async Task<IActionResult> Index(string email)
{
var gameInvitationModel = new GameInvitationModel
{
InvitedBy = email,
Id = Guid.NewGuid()
};
Request.HttpContext.Session.SetString("email", email);
var user = await _userService.GetUserByEmail(email);
Request.HttpContext.Session.SetString("displayName",
$"{user.FirstName} {user.LastName}");
return View(gameInvitationModel);
}
- 将名为
AccountModel的新模型添加到Models文件夹:
public class AccountModel
{
public string Email { get; set; }
public string DisplayName { get; set; }
}
- 将名为
_Account.cshtml的新局部视图添加到Views/Shared文件夹:
@model TicTacToe.Models.AccountModel
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<span class="glyphicon glyphicon-user"></span>
<strong>@Model.DisplayName</strong>
<span class="glyphicon glyphicon-chevron-down"></span>
</a>
<ul class="dropdown-menu" id="connected-dp">
<li>
<div class="navbar-login">
<div class="row">
<div class="col-lg-4">
<p class="text-center">
<Gravatar email="@Model.Email">
</Gravatar>
</p>
</div>
<div class="col-lg-8">
<p class="text-left"><strong>@Model.
DisplayName</strong></p>
<p class="text-left small">
<a asp-action="Index" asp-
controller="Account">
@Model.Email</a>
</p>
</div>
</div>
</div>
</li>
<li class="divider"></li>
<li>
<div class="navbar-login navbar-login-session">
<div class="row">
<div class="col-lg-12">
<p>
<a href="#" class="btn btn-danger btn-
block">Log off</a>
</p>
</div>
</div>
</div>
</li>
</ul>
</li>
- 在
wwwroot/css/site.css文件中添加一个新的 CSS 类:
#connected-dp {
min-width: 350px;
}
Note that you might need to empty your browser cache or force a refresh for the application so that you can update the site.css file within your browser.
- 更新菜单部分视图,并在页面顶部检索用户显示名称和电子邮件:
@using Microsoft.AspNetCore.Http;
@{
var email = Context.Session.GetString("email");
var displayName = Context.Session.GetString("displayName");
}
- 更新菜单部分视图并添加我们先前创建的新帐户部分视图。可在菜单中的隐私元素后找到:
<li>
@if (!string.IsNullOrEmpty(email))
{
Html.RenderPartial("_Account",
new TicTacToe.Models.AccountModel
{
Email = email,
DisplayName = displayName
});
}
</li>
- 使用电子邮件在 Gravatar 上创建帐户并上传照片。启动 Tic Tac Toe 应用并使用相同的电子邮件注册。现在,您将在顶部菜单中看到一个新的下拉列表,其中包含照片和显示名称:

请注意,您必须联机才能使用此功能。如果你想离线测试你的代码,你应该在名为no-photo.jpg的wwwroot/images文件夹中放一张照片;否则,将出现错误,因为找不到脱机照片。
这应该易于理解和使用,但是什么时候应该使用视图组件,什么时候应该使用标记帮助器?以下简单规则应帮助您决定何时使用其中一种:
- 每当我们需要视图模板、需要呈现一组元素以及需要将服务器代码与之关联时,都会使用视图组件。
- 标记帮助器用于将行为附加到单个 HTML 元素,而不是一组元素。
我们的应用正在增长。对于较大的应用,从逻辑上遵循应用可能会成为一场噩梦,特别是如果您是一名新开发人员,并且被安排在现有项目上,那么您可能需要一些时间来适应代码库。幸运的是,ASP.NETCore3 允许我们划分类似的功能。我们将在下一节中了解如何做到这一点。
将 web 应用划分为多个区域
有时,在处理较大的 web 应用时,在逻辑上将它们划分为较小的功能单元可能会很有趣。然后,每个单元都可以拥有自己的控制器、视图和模型,这使得随着时间的推移更容易理解、管理、发展和维护它们。
ASP.NET Core 3 提供了一些基于文件夹结构的简单机制,用于将 web 应用划分为多个功能单元,也称为Areas;例如,将标准Area与应用中更高级的管理Area分开。标准Area甚至可以在某些页面上启用匿名访问,同时在其他页面上请求身份验证和授权,而管理Area将始终要求在所有页面上进行身份验证和授权。
以下公约和限制适用于Areas:
-
Area是Areas文件夹中的一个子目录。 -
Area至少包含两个子文件夹:Controllers和Views。 -
Area可以包含特定的布局页面,以及专用的_ViewImport.cshtml和_ViewStart.cshtml文件。 -
您必须注册一个特定的路由,该路由在其路由定义中启用
Areas,以便能够在您的应用中使用Areas。 -
建议对
AreaURL 使用以下格式:
http://<Host>/<AreaName>/<ControllerName>/<ActionName>。 -
asp-area标记帮助器可用于将Area附加到 URL。
让我们看看如何为帐户管理创建一个特定的管理Area:
- 打开解决方案资源管理器并创建一个名为 Areas 的新文件夹。右键点击文件夹,选择添加|区域…,输入
Account作为Area名称,点击添加按钮:

- 脚手架将为
Account Area创建一个专用文件夹结构,如下所示:

- 在
Startup类的Configure方法中的UseEndpoints声明中增加Areas的新路由:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "
{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
endpoints.MapAreaControllerRoute(
name: "areas",
areaName: "Account",
pattern : "
{area:exists}/{controller=Home}
/{action=Index}/{id?}"
);
});
- 右键点击
Account Area中的Controllers文件夹,添加一个名为HomeController的新控制器:
[Area("Account")]
public class HomeController : Controller
{
private IUserService _userService;
public HomeController(IUserService userService)
{
_userService = userService;
}
public async Task<IActionResult> Index()
{
var email = HttpContext.Session.GetString("email");
var user = await _userService.GetUserByEmail(email);
return View(user);
}
}
- 在
Account/Views文件夹中添加一个名为Home的新文件夹。然后,在此新文件夹中添加名为Index的视图:
@model TicTacToe.Models.UserModel
<h3>Account Details</h3>
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-6 col-md-6">
<div class="well well-sm">
<div class="row">
<div class="col-sm-6 col-md-4">
<Gravatar email="@Model.Email"></Gravatar>
</div>
<div class="col-sm-6 col-md-8">
<h4>@($"{Model.FirstName}
{Model.LastName}")</h4>
<p>
<i class="glyphicon glyphicon
-envelope"></i>
<a href="mailto:@Model.Email">@Model.
Email</a>
</p>
<p>
<i class="glyphicon glyphicon
-calendar">
</i> @Model.EmailConfirmationDate
</p>
</div>
</div>
</div>
</div>
</div>
</div>
- 更新帐户部分视图并添加链接以显示前面的视图(就在现有注销链接之后):
<a class="btn btn-default btn-block" asp-action="Index"
asp-controller="Account">View Details</a>
- 启动应用,注册用户,点击下拉菜单上的查看详情链接,调用新的
Area:

我们将在此处停止管理区域的实施,并在第 10 章保护 ASP.NET Core 3 应用中回到它,在这里您将学习如何保护对它的访问。现在,让我们通过查看一个名为视图引擎的激动人心的功能来进一步了解它。我们得到的越高级,我们的代码库就会变得越复杂,确保我们始终获得预期功能的最佳方法之一就是编写单元测试和集成测试。我们也将在下一节中介绍这些。
应用视图引擎、单元测试和集成测试等高级概念
现在,我们已经了解了 ASP.NET Core 3 MVC 的所有基本功能,让我们来看看一些更高级的功能,这些功能可以在您作为开发人员的日常工作中帮助您。
您还将学习如何使用 Visual Studio 2019 测试您的应用,从而为您的用户提供更好的质量。
使用视图引擎
当 ASP.NET Core 3 使用服务器端代码呈现 HTML 时,它使用视图引擎。默认情况下,当使用相关的.cshtml文件构建标准视图时,我们使用 Razor 视图引擎和 Razor 语法。
按照惯例,此引擎能够处理位于Views文件夹中的视图。由于它是内置的,并且是默认引擎,因此它会自动绑定到 HTTP 请求管道,而无需我们为它的工作做任何事情。
如果我们需要使用 Razor 来呈现位于Views文件夹之外并且不是直接来自 HTTP 请求管道的文件,例如电子邮件模板,我们不能使用默认的 Razor 视图引擎。相反,我们需要定义自己的视图引擎,并让它负责生成 HTML 代码。
在以下示例中,我们将解释如何使用 Razor 根据非来自 HTTP 请求管道的电子邮件模板呈现电子邮件:
- 打开解决方案资源管理器并创建一个名为
ViewEngines的新文件夹。然后,添加一个名为EmailViewEngine.cs的新类,该类具有以下构造函数:
public class EmailViewEngine
{
private readonly IRazorViewEngine _viewEngine;
private readonly ITempDataProvider _tempDataProvider;
private readonly IServiceProvider _serviceProvider;
public EmailViewEngine( IRazorViewEngine viewEngine,
ITempDataProvider tempDataProvider, IServiceProvider
serviceProvider)
{
_viewEngine = viewEngine;
_tempDataProvider = tempDataProvider;
_serviceProvider = serviceProvider;
}
...
}
在同一个EmailViewEngine中,我们创建一个FindView方法,如下所示:
private IView FindView(ActionContext actionContext, string viewName)
{
var getViewResult = _viewEngine.GetView(executingFilePath:
null, viewPath: viewName, isMainPage: true);
if (getViewResult.Success)
return getViewResult.View;
var findViewResult = _viewEngine.FindView(actionContext,
viewName, isMainPage: true);
if (findViewResult.Success)
return findViewResult.View;
var searchedLocations = getViewResult.
SearchedLocations.Concat(findViewResult.SearchedLocations);
var errorMessage = string.Join
( Environment.NewLine, new[] { $"Unable to
find view '{viewName}'. The following locations
were searched:" }.Concat(searchedLocations));
throw new InvalidOperationException(errorMessage);
}
让我们在同一个EmailViewEngine类中创建一个GetActionContext方法:
private ActionContext GetActionContext()
{
var httpContext = new DefaultHttpContext
{
RequestServices = _serviceProvider
};
return new ActionContext(httpContext, new RouteData(),
new ActionDescriptor());
}
我们将在以下RenderEmailToString方法中使用上述方法,如下所示:
public async Task<string> RenderEmailToString<TModel>(string viewName, TModel model)
{
var actionContext = GetActionContext();
var view = FindView(actionContext, viewName);
if (view == null)
throw new InvalidOperationException(string.Format
("Couldn't find view '{0}'", viewName));
using var output = new StringWriter();
var viewContext = new ViewContext(actionContext,
view,
new ViewDataDictionary<TModel>(metadataProvider:
new
EmptyModelMetadataProvider(), modelState: new
ModelStateDictionary())
{
Model = model
},
new TempDataDictionary(actionContext.HttpContext,
_tempDataProvider), output, new HtmlHelperOptions());
await view.RenderAsync(viewContext);
return output.ToString();
}
创建EmailViewEngine类后,提取其接口IEmailViewEngine,如下所示:
public interface IEmailViewEngine
{
Task<string> RenderEmailToString<TModel>(string
viewName, TModel model);
}
- 创建一个名为
Helpers的新文件夹,并向其中添加一个名为EmailViewRenderHelper.cs的新类:
public class EmailViewRenderHelper
{
IWebHostEnvironment _hostingEnvironment;
IConfiguration _configurationRoot;
IHttpContextAccessor _httpContextAccessor;
public async Task<string> RenderTemplate<T>(string
template, IWebHostEnvironment hostingEnvironment,
IConfiguration configurationRoot,
IHttpContextAccessor httpContextAccessor, T model
) where T : class
{
_hostingEnvironment = hostingEnvironment;
_configurationRoot = configurationRoot;
_httpContextAccessor = httpContextAccessor;
var renderer = httpContextAccessor.HttpContext.
RequestServices
.GetRequiredService<IEmailViewEngine>();
return await renderer.RenderEmailToString<T>(template,
model);
}
}
- 在
Services文件夹中添加名为EmailTemplateRenderService的新服务。它将具有以下构造函数:
public class EmailTemplateRenderService
{
private IWebHostEnvironment _hostingEnvironment;
private IConfiguration _configuration;
private IHttpContextAccessor _httpContextAccessor;
public EmailTemplateRenderService(IWebHostEnvironment
hostingEnvironment, IConfiguration configuration,
IHttpContextAccessor httpContextAccessor)
{
_hostingEnvironment = hostingEnvironment;
_configuration = configuration;
_httpContextAccessor = httpContextAccessor;
}
}
现在,创建一个RenderTemplate方法,如下所示:
public async Task<string> RenderTemplate<T>(string templateName, T model, string host) where T : class
{
var html = await new EmailViewRenderHelper().
RenderTemplate(templateName, _hostingEnvironment,
_configuration, _httpContextAccessor, model);
var targetDir = Path.Combine(Directory.
GetCurrentDirectory(), "wwwroot", "Emails");
if (!Directory.Exists(targetDir))
Directory.CreateDirectory(targetDir);
string dateTime = DateTime.Now.
ToString("ddMMHHyyHHmmss");
var targetFileName = Path.Combine(targetDir,
templateName.Replace("/", "_").Replace("\\", "_")
+ "." + dateTime + ".html");
html = html.Replace("{ViewOnLine}", $"
{host.TrimEnd('/')}/Emails/{Path.GetFileName
(targetFileName)}");
html = html.Replace("{ServerUrl}", host);
File.WriteAllText(targetFileName, html);
return html;
}
提取其接口并将其命名为IEmailTemplateRenderService。
- 在
Startup类中注册EmailViewEngine和EmailTemplateRenderService:
services.AddTransient<IEmailTemplateRenderService,
EmailTemplateRenderService>();
services.AddTransient<IEmailViewEngine,
EmailViewEngine>
();
Note that you need to register EmailViewEngine and EmailTemplateRenderService as transient because of HTTPContextAccessor injection.
- 在名为
_LayoutEmail.cshtml的Views/Shared文件夹中添加一个新的布局页面。首先,我们将创建head部分,如下所示:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-
scale=1.0" />
<title>@ViewData["Title"] - TicTacToe</title>
<environment include="Development">
<link rel="stylesheet"
href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet"
href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7
/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css
/bootstrap.min.css"
asp-fallback-test-class="sr-only"
asp-fallback-test-property="position"
asp-fallback-test-value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css"
asp-append-version="true" />
</environment>
</head>
现在,我们将创建body部分,如下所示:
<body>
<div class="container body-content">
@RenderBody()
<hr />
<footer> <p>© 2019 - TicTacToe</p> </footer>
</div>
<environment include="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js">
</script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
@RenderSection("Scripts", required: false)
</body>
- 将名为
UserRegistrationEmailModel的新模型添加到Models文件夹:
public class UserRegistrationEmailModel
{
public string Email { get; set; }
public string DisplayName { get; set; }
public string ActionUrl { get; set; }
}
- 在
Views文件夹中新建名为EmailTemplates的子文件夹,并添加名为UserRegistrationEmail的新视图:
@model TicTacToe.Models.UserRegistrationEmailModel
@{
ViewData["Title"] = "View";
Layout = "_LayoutEmail";
}
<h1>Welcome @Model.DisplayName</h1>
Thank you for registering on our website. Please click <a href="@Model.ActionUrl">here</a> to confirm your email.
- 更新
UserRegistrationController中的EmailConfirmation方法,以便我们可以在发送任何电子邮件之前使用新的电子邮件查看引擎:
var userRegistrationEmail = new UserRegistrationEmailModel
{
DisplayName = $"{user.FirstName} {user.LastName}",
Email = email,
ActionUrl = Url.Action(urlAction)
};
var emailRenderService = HttpContext.RequestServices.
GetService<IEmailTemplateRenderService>();
var message = await emailRenderService.RenderTemplate
("EmailTemplates/UserRegistrationEmail",
userRegistrationEmail, Request.Host.ToString());
- 启动应用并注册新用户。打开
UserRegistrationEmail并分析其内容(查看wwwroot/Emails文件夹):

If you see the InvalidOperationException: Unable to resolve service for type 'Microsoft.AspNetCore.Http.IHttpContextAccessor error, you will need to register IHttpContextAccessor manually in the Startup class by adding services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); in the ConfigureServices method or by adding the built-in services.AddHttpContextAccessor(); method.
在本书中,您已经了解了各种概念和代码示例,但我们仍然没有讨论如何确保我们的应用具有优异的质量和可维护性。下一节将介绍这个主题,它致力于应用测试。
通过创建单元测试和集成测试提供更好的质量
构建高质量的应用并满足应用用户是一项艰巨的任务。装运有技术和功能缺陷的产品可能会在应用的维护阶段导致巨大的问题。
最坏的情况是,由于维护对时间和资源的要求很高,您将无法尽快开发应用以缩短上市时间,并且无法提供令人兴奋的新功能。不要以为你的竞争对手没有等待!他们将超越你,你将失去市场份额和市场领导地位。
但是你怎么能成功呢?如何减少检测 bug 和功能问题的时间?您必须测试您的代码和应用,而且您必须尽可能多地、尽快地这样做。众所周知,在开发过程中修复 bug 更便宜、更快,而在生产过程中修复 bug 则需要更多的时间和金钱。
对于 bug,拥有一个较低的平均修复时间(是平均修复时间的缩写)对于成为特定市场中的未来市场领导者会产生很大的影响。
让我们稍微转移一下注意力,做一些最佳实践内务管理,这将需要在应用中使用。在 C#中,我们可以使用String.IsNullOrEmpty()方法检查字符串是否为 null 或空。我们需要这样做,因为有一个空字符串和一个空字符串是两种完全不同的情况。空字符串不一定为 null。
在处理集合时,也有一些情况,我们需要检查集合是否为 null 或空。不幸的是,我们没有像用于字符串的那种现成的实现,这意味着我们将自己创建它。
让我们转到 extensions 文件夹,创建一个名为CollectionsEtensionMethods的静态类,它包含两个方法,如下所示:
public static class CollectionsExtensionMethods
{
public static bool IsNullOrEmpty<T>(this IEnumerable<T>
genericEnumerable)
{
return (genericEnumerable == null) ||
(!genericEnumerable.Any());
}
public static bool IsNullOrEmpty<T>(this ICollection<T>
genericCollection)
{
if (genericCollection == null)
{
return true;
}
return genericCollection.Count < 1;
}
}
现在,只要我们从应用中的任何位置引用TicTacToe.Extensions名称空间,我们就可以对任何集合实现IsNullOrEmpty()检查。我们将在下面的代码片段中看到这一点,在这里我们将查看游戏会话结果,并找出它们是空的还是空的。
让我们继续开发 Tic-Tac-Toe 应用,并学习如何更详细地仔细测试它:
- 在
GameSessionService中新增AddTurn方法,更新游戏会话服务界面:
public async Task<GameSessionModel> AddTurn(Guid id, string email, int x, int y)
{
var gameSession = _sessions.FirstOrDefault(session => session.Id
== id);
List<TurnModel> turns;
if (!gameSession.Turns.IsNullOrEmpty())
turns = new List<TurnModel>(gameSession.Turns);
else turns = new List<TurnModel>();
turns.Add(new TurnModel {User = await _UserService.GetUserByEmail
(email), X = x, Y = y });
if (gameSession.User1?.Email == email) gameSession.ActiveUser =
gameSession.User2;
else gameSession.ActiveUser = gameSession.User1;
gameSession.TurnFinished = true;
_sessions = new ConcurrentBag<GameSessionModel>(_sessions.Where(u
=> u.Id != id))
{ gameSession };
return gameSession;
}
- 将名为
SetPosition的新方法添加到GameSessionController:
public async Task<IActionResult> SetPosition(Guid id,
string email, int x, int y)
{
var gameSession =
await _gameSessionService.GetGameSession(id);
await _gameSessionService.AddTurn(gameSession.Id,
email, x, y);
return View("Index", gameSession);
}
- 将名为
InvitationEmailModel的新模型添加到Models文件夹:
public class InvitationEmailModel
{
public string DisplayName { get; set; }
public UserModel InvitedBy { get; set; }
public DateTime InvitedDate { get; set; }
public string ConfirmationUrl { get; set; }
}
- 将名为
InvitationEmail的新视图添加到Views/EmailTemplates文件夹:
@model TicTacToe.Models.InvitationEmailModel
@{
ViewData["Title"] = "View";
Layout = "_LayoutEmail";
}
<h1>Welcome @Model.DisplayName</h1>
You have been invited by @($"{Model.InvitedBy.FirstName} { Model.InvitedBy.LastName} ") to play the Tic-Tac-Toe game.
Please click <a href="@Model.ConfirmationUrl">here</a> to join the game.
- 更新
GameInvitationController中的Index方法,以便能够使用我们前面提到的邀请电子邮件模板:
[HttpPost]
public async Task<IActionResult> Index( GameInvitationModel
gameInvitationModel, [FromServices]IEmailService
emailService)
{
var gameInvitationService = Request.HttpContext.
RequestServices.GetService<IGameInvitationService>();
if (ModelState.IsValid)
{
try
{
var invitationModel = new InvitationEmailModel
{
DisplayName = $"{gameInvitationModel.
EmailTo}",
InvitedBy = await
_userService.GetUserByEmail
( gameInvitationModel.InvitedBy),
ConfirmationUrl =
Url.Action("ConfirmGameInvitation",
"GameInvitation",
new { id = gameInvitationModel.Id },
Request.Scheme, Request.Host.ToString()),
InvitedDate = gameInvitationModel.
ConfirmationDate
};
var emailRenderService = HttpContext.
RequestServices.GetService
<IEmailTemplateRenderService>();
var message = await emailRenderService.
RenderTemplate<InvitationEmailModel>
("EmailTemplates/InvitationEmail",
invitationModel, Request.Host.ToString());
await emailService.SendEmail(
gameInvitationModel.EmailTo,
_stringLocalizer
["Invitation for playing a Tic-Tac-Toe
game"], message);
}
catch
{
}
var invitation = gameInvitationService.Add
(gameInvitationModel).Result;
return RedirectToAction
("GameInvitationConfirmation", new { id =
gameInvitationModel.Id });
}
return View(gameInvitationModel);
}
- 将名为
ConfirmGameInvitation的新方法添加到GameInvitationController:
[HttpGet]
public IActionResult ConfirmGameInvitation(Guid id,
[FromServices]IGameInvitationService
gameInvitationService)
{
var gameInvitation = gameInvitationService.
Get(id).Result;
gameInvitation.IsConfirmed = true;
gameInvitation.ConfirmationDate = DateTime.Now;
gameInvitationService.Update(gameInvitation);
return RedirectToAction("Index", "GameSession", new
{ id
= id });
}
- 启动应用并验证所有内容是否按预期工作,包括启动新游戏的各种电子邮件和步骤。
既然我们已经实现了所有这些新代码,我们如何测试它呢?我们如何确保它按预期工作?我们可以在调试模式下启动应用,并验证所有变量设置是否正确以及应用流是否正确,但这将非常繁琐,效率也不高。
有什么比这样做更好?使用单元测试和集成测试。我们将在接下来的部分中介绍这些测试。
添加单元测试
单元测试允许您单独验证各种技术组件的行为,并确保它们按预期工作。它们还可以帮助您快速识别回归并分析新发展的总体影响。Visual Studio 2019 包含用于单元测试的强大功能。
测试资源管理器帮助您运行单元测试以及查看和分析测试结果。为此,您可以使用内置的 Microsoft 测试框架或其他框架,如 NUnit 或 xUnit。
此外,您可以在每次构建之后自动执行单元测试,以便开发人员可以在某些东西不能按预期工作时快速做出反应。
重构代码可以不用担心回归,因为单元测试确保一切都像以前一样工作。没有更多的借口,没有最好的代码质量可能!
您甚至可以更进一步,应用测试驱动开发(TDD),也就是在编写实现之前编写单元测试的地方。此外,单元测试成为某种设计文档和功能规范。进一步的步骤是应用行为驱动开发(BDD)并根据规范创建测试。
This book is about ASP.NET Core 3, so we won't go into too much detail about unit tests. It is, however, advised to dig deeper and familiarize yourself with all the different unit test concepts so that you can build better applications.
让我们了解使用 xUnit 是多么容易,它是 ASP.NET Core 3 的首选单元测试框架:
- 将名为
TicTacToe.UnitTests的 xUnit 测试项目(.NET Core)类型的新项目添加到 Tictaoe 解决方案中:

- 使用 NuGet 软件包管理器将 xunit 和 Microsoft.NET.Test.SDK NuGet 软件包更新至其最新版本:

- 添加对 TictaToe 和 TictaToe 的引用。日志记录项目:

- 删除自动生成的类,添加一个名为
FileLoggerTests.cs的新类来测试一个常规类,并实现一个名为ShouldCreateALogFileAndAddEntry的新方法:
public class FileLoggerTests
{
[Fact]
public void ShouldCreateALogFileAndAddEntry()
{
var fileLogger = new FileLogger(
"Test", (category, level) => true,
Path.Combine(Directory.GetCurrentDirectory(),
"testlog.log"));
var isEnabled = fileLogger.IsEnabled
(LogLevel.Information);
Assert.True(isEnabled);
}
}
- 为测试服务添加另一个名为
UserServiceTests.cs的新类,并实现一个名为ShouldAddUser的新方法:
public class UserServiceTests
{
[Theory]
[InlineData("test@test.com", "test", "test", "test123!")]
[InlineData("test1@test.com", "test1", "test1", "test123!")]
[InlineData("test2@test.com", "test2", "test2", "test123!")]
public async Task ShouldAddUser(string email, string firstName,
string lastName, string password)
{ var userModel = new UserModel
{ Email = email,
FirstName = firstName,
LastName = lastName,
Password = password };
var userService = new UserService();
var userAdded = await userService.RegisterUser
(userModel);
Assert.True(userAdded); }
}
- 通过测试| Windows |测试资源管理器打开测试资源管理器,并选择“全部运行”,以确保所有测试都成功执行:

单元测试非常重要,但也有一定的局限性。他们只单独测试每个技术组件,这是此类测试的主要目标。
单元测试背后的思想是在不减慢持续集成过程的情况下,一个接一个地快速了解所有技术组件的当前状态。他们不会在实际生产条件下测试应用,因为外部依赖关系是模拟的。相反,它们旨在快速运行,并确保被测试的每个方法不会在其他方法或类中产生意外的副作用。
如果您停在这里,您将无法像通常在开发阶段那样找到很多 bug。您必须更进一步,在真实环境中一起测试所有组件;这就是集成测试发挥作用的地方。
添加集成测试
集成测试是单元测试的逻辑扩展。它们在真实环境中测试应用中多个技术组件之间的集成,以访问外部数据源(如数据库、web 服务和缓存)。
这类测试的目标是确保在组合各种技术组件以创建应用行为时,一切都能正常工作,并提供预期的功能。
此外,集成测试应该始终具有清理步骤,以便它们可以重复运行而不会出错,并且不会在数据库或文件系统中留下任何工件。
在下面的示例中,您将了解如何将集成测试应用于 Tic Tac Toe 演示应用:
- 将名为
TicTacToe.IntegrationTests的 xUnit 测试项目(.NET Core)类型的新项目添加到 TictaToe 解决方案中,更新 NuGet 包,并添加对 TictaToe 和 TictaToe.Logging 项目的引用,如前面的单元测试项目所示。 - 将
Microsoft.AspNetCore.TestHostNuGet 包添加到IntegrationTests项目中,如下图所示。这允许我们使用 xUnit 创建全自动集成测试:

- 删除自动生成的类,添加一个名为
IntegrationTests.cs的新类,实现一个名为ShouldGetHomePageAsync的新方法:
[Fact]
public async Task ShouldGetHomePageAsync()
{
var response = await _httpClient.GetAsync("/");
response.EnsureSuccessStatusCode();
var responseString = await response.Content.
ReadAsStringAsync();
Assert.Contains("Welcome to the Tic-Tac-Toe Desktop
Game!", responseString);
}
- 在测试资源管理器中运行测试并确保它们成功执行。
现在您已经了解了如何测试应用,可以继续添加其他单元和集成测试,以充分理解这些概念,并构建测试覆盖率,从而提供高质量的应用。
分层 ASP.NET Core 3 应用
你们中的一些人可能已经注意到,将许多功能塞进一个项目并不总是一个好主意。我们的项目结构目前如下所示:

我们一直在添加一个又一个文件夹和文件夹内的文件夹,对于大型项目,它可能很快失控,并成为维护方面的噩梦。本节只为让您意识到在使用分层架构设计我们的解决方案时要考虑的最佳实践,其目的是实现以下几点:
- 定义的 SoC。
- 由于各层之间的耦合度较低,且各层之间的内聚力较高,因此维护的难度较小。
- 能够交换和切换层接口的不同实现的能力。
- 其他解决方案应该能够重用各个层公开的功能。
确定所需的图层
对于您将要编写的大多数应用,它们都具有通用功能。建议将此通用功能按层分组到不同的项目中。
在我们的TicTacToe演示应用中,我们将所有视图分组到一个项目中作为表示层,将UserService、EmailService等服务分组到服务层,将UserModel等所有模型分组到一个项目中作为域层,以及包含数据库上下文的数据访问层。这将在第 9 章中解释,使用实体框架核心 3访问数据。
我们的演示应用的假设分层应用如下所示:

It's nice to have a layered application, but don't go on a wanton spree of randomly creating layers. The key is to look for functionality that makes sense for it to be grouped together so that you can make your application more maintainable and more scalable.
在本书中,我们将不使用这种分层体系结构。我们提到它只是为了让您了解,我们可以在许多方面改进TicTacToe应用的当前设计。
决定图层和组件的分布
层和组件应分布在单独的物理层上,但仅限于必要时。实现分布式部署有几个原因,包括安全策略、物理约束、共享业务逻辑和可伸缩性。
为了简化本书的内容,为了满足TicTacToe应用的需要,我们只将应用部署为单个实例,而不是分布式的(我们将在第 12 章、托管 ASP.NET Core 3 应用中介绍)。因此,只需说,还有其他方式和方法可以将具有多个实例的应用作为分布式应用托管。
In web applications, you should deploy the business layer and presentation layer components on the same physical tier to maximize performance and ease operational management, unless security restrictions need a trust boundary between them.
确定层间交互的规则
当涉及到分层策略时,必须为各层如何相互交互定义规则。指定交互规则的主要原因是最小化依赖关系和消除循环引用。例如,如果两个层都依赖于另一层中的组件,那么将引入循环依赖。
Only implement top-down interaction. Higher-level layers can interact with the layers below them, but a lower level layer should never interact with the layers above.
实施一条规则,帮助您避免层之间的循环依赖关系。事件可用于使较高层中的组件了解较低层中的更改,而无需引入依赖项。
确定交叉关注点
当您将项目分为多个层时,您会注意到在每个层中重复执行的一些功能。例如,您会发现必须在每个层中执行验证功能。另一个例子是,您每次都必须在不同的层中进行身份验证。最好的选择是识别这些类型的功能,并将它们作为交叉关注点分组到一个项目中。
横切关注点项目(层)的名称不必与此完全相同。您可以根据自己的意愿选择将项目命名为自己的名字。可以放在横切层中的其他候选功能包括如何管理应用的异常、如何缓存常用对象以及记录器功能。
将这些横切功能放在一个层中的好处是,您可以促进重用,并使我们的应用更易于维护。
Avoid mixing the cross-cutting code with code in the components of each layer so that the layers and their components only make calls to the cross-cutting components when they must carry out an action such as logging, caching, or authentication.
总结
在本章中,您了解了 MVC 模式、它的不同组件和层,以及它对于构建优秀的 ASP.NET Core 3 web 应用的重要性。
您学习了如何使用布局页面及其周围的功能来创建特定于设备的布局,从而使您的用户界面适应它们将运行的设备。此外,在了解了 ASP.NET Core 3 中不同类型的状态管理之后,我们使用查看页面构建 web 应用的可见部分,即表示层。
然后,我们讨论了部分视图、视图组件和标记帮助器,以便在应用的不同视图中封装和重用表示逻辑。最后,我们介绍了视图引擎等高级概念,以及用于创建高质量应用的单元测试和集成测试,这些应用的 bug MTTR 较低。
最后,我们了解了使用分层架构和我们需要考虑的基础结构来构造更复杂的应用是多么重要。
通过阅读本章,您现在可以创建视图、模型和控制器;检测移动设备;使用视图组件;将应用划分为多个区域;并决定为应用创建哪些层。
在下一章中,我们将讨论 ASP.NET Core 3 web API 框架以及如何构建、测试和部署 web API 应用。
八、创建 Web API 应用
你可能还不知道,但这一章是你一直在等待的一章!由于多种原因,它非常特殊。
首先,我们将完成游戏部分,您将能够开始玩井字游戏。是–最终,整个应用将启动并运行,您将能够与其他用户竞争。非常激动人心!
其次,您将学习如何将应用与其他系统和服务集成。这一点非常重要,因为现代应用不再是孤立的筒仓。相反,他们相互沟通,不断交换数据,为客户提供更多价值。我们怎样才能做到这一点?我们可以提供可互操作的web 应用编程接口(web API),允许用户插入组件,有时基于完全不同的技术!
第三,使用 web API 不仅允许您与其他系统集成;它还将帮助您构建更灵活和可重用的应用组件,然后您可以将这些组件组合起来创建新的应用,以响应更高级的用例。
我们将在本章中创建的 API 不仅可用于我们一直在开发的 MVC web 前端,还可用于您将来可能构建的任何新的移动前端。这将使您能够接触到更多的客户。您将能够为您的客户提供全渠道体验,让他们从一台设备开始使用,到另一台设备结束。
在本章中,我们将介绍以下主题:
- 应用 web API 概念和最佳实践
- 构建 RPC、REST 和 HATEOAS 样式的 web API
- Web API 安全
- 带有 Swagger/OpenAPI 的 ASP.NET Core web API 帮助页
技术要求
本章的源代码见https://github.com/PacktPublishing/Learn-ASP.NET-Core-3-Second-Edition/tree/master/Chapter08 。
应用 web API 概念和最佳实践
ASP.NET Core 3 将 ASP.NET MVC 和 web API 的最佳功能组合到一个框架中。这是完全有意义的,因为它们提供了许多类似的功能。
在这次合并之前,当开发人员需要通过 MVC 和 WebAPI 以不同格式公开数据时,他们必须重写代码。他们必须同时使用多个框架和概念。幸运的是,整个过程在 ASP.NET Core 3 中已经完全优化,您将在本章中看到。
下图说明了 ASP.NET Core 3 如何根据 web API 和 MVC 处理客户端 HTTP 请求:

Web API 通常使用 JSON 或 XML 作为响应格式。JSON 是首选格式,因为它已成为市场上的准标准,并且由于其简单高效,大多数现代应用都使用它。
此外,过滤器和中间件可以与 web API 一起使用,因为 ASP.NET Core 3 管理 web API 的方式与管理标准 MVC 控制器的方式相同。这在某些用例中非常方便,开发人员可以更广泛地应用他们的技能。
通常,在使用 ASP.NET Core 3 时,有三种不同的样式可用于创建 web API:
- RPC 样式
- 休息方式
- 哈提奥斯风格
Note that it is also possible to use the Simple Object Access Protocol (SOAP) to create web APIs, but it is not recommended. Instead, SOAP should be used in the context of standard web services, which is why it is not shown in the following examples.
我们将更详细地介绍每种样式,并提供一些实际示例,这些示例将帮助您决定自己的集成策略。
构建 RPC 风格的 web API
RPC样式基于远程过程调用范式,这种范式已经存在了很长时间(自 20 世纪 80 年代初以来)。它基于在 URL 中包含一个动作名称,这使得它与标准 MVC 动作非常相似。
ASP.NET Core 3 的一大优点是不需要将 MVC 部件与 web API 部件分开。相反,您可以在控制器实现中使用这两种方法。
控制器现在能够呈现视图结果以及 JSON/XMLAPI 响应,从而实现从一个到另一个的轻松迁移。此外,您可以为 MVC 操作使用特定的路由路径或相同的路由路径。
在以下示例中,您将把控制器操作从 MVC 视图结果转换为 RPC 样式的 web API:
- 在
UserRegistrationController中增加一个名为ConfirmEmail的新方法;它将用于确认用户注册电子邮件。该方法接受电子邮件作为参数,通过提供的电子邮件获取用户,如果找到用户,则更新用户已确认其电子邮件的事实,并设置确认时间戳:
[HttpGet]
public async Task<IActionResult> ConfirmEmail(string email)
{
var user = await _userService.GetUserByEmail(email);
if (user != null)
{
user.IsEmailConfirmed = true;
user.EmailConfirmationDate = DateTime.Now;
await _userService.UpdateUser(user);
return RedirectToAction("Index", "Home");
}
return BadRequest();
}
- 更新
GameInvitationController中的ConfirmGameInvitation方法,将邀请用户的邮件存储在会话变量中,通过用户服务注册新用户:
[HttpGet]
public async Task<IActionResult> ConfirmGameInvitation
(Guid id,
[FromServices]IGameInvitationService
gameInvitationService)
{
var gameInvitation = await gameInvitationService.Get(id);
gameInvitation.IsConfirmed = true;
gameInvitation.ConfirmationDate = DateTime.Now;
await gameInvitationService.Update(gameInvitation);
Request.HttpContext.Session.SetString("email",
gameInvitation.EmailTo);
await _userService.RegisterUser(new UserModel
{
Email = gameInvitation.EmailTo, EmailConfirmationDate =
DateTime.Now, IsEmailConfirmed =true
});
return RedirectToAction("Index", "GameSession", new { id });
}
- 通过删除
@if (Model.ActiveUser?.Email == email)包装,更新GameSessionViewComponent中的表格元素,该元素可以在Views/Shared/Components/GameSession/default.cshtml文件中找到。接下来,不使用gameBoarddiv 元素包装 table 元素(如下代码所示),而是更新 wait turndiv元素,该元素有一个名为"divAlertWaitTurn"的id,如下所示:
<div id="gameBoard">
<table>
...
</table>
</div>
<div class="alert" id="divAlertWaitTurn">
<i class="glyphicon glyphicon-alert">Please wait until
the other user has finished his turn.</i>
</div>
- 在名为
GameSession.js的wwwroot\app\js文件夹中添加一个新的 JavaScript 文件。这将用于调用 web API。SetGameSession方法接受一个会话 ID,用于设置游戏会话:
function SetGameSession(gdSessionId, strEmail) {
window.GameSessionId = gdSessionId;
window.EmailPlayer = strEmail;
}
$(document).ready(function () {
$(".btn-SetPosition").click(function () {
var intX = $(this).attr("data-X");
var intY = $(this).attr("data-Y");
SendPosition(window.GameSessionId, window.EmailPlayer,
intX, intY);
})
})
然后,发送位置,如下所示:
function SendPosition(gdSession, strEmail, intX, intY) {
var port = document.location.port ? (":" +
document.location.port) : "";
var url = document.location.protocol + "//" +
document.location.hostname + port +
"/restApi/v1/SetGamePosition/" + gdSession;
var obj = {
"Email": strEmail, "x": intX, "y": intY
};
为测试目的添加临时警报框:
var json = JSON.stringify(obj);
$.ajax({
'url': url,
'accepts': "application/json; charset=utf-8",
'contentType': "application/json",
'data': json,
'dataType': "json",
'type': "POST",
'success': function (data) {
alert(data);
}
});
}
- 将前面的 JavaScript 文件添加到
bundleconfig.json文件中,以便您可以将其与其他文件捆绑到site.js文件中:
{
"outputFileName": "wwwroot/js/site.js",
"inputFiles": [
"wwwroot/app/js/scripts1.js",
"wwwroot/app/js/scripts2.js",
"wwwroot/app/js/GameSession.js"
],
"sourceMap": true,
"includeInProject": true
},
- 将名为
Email的新属性添加到TurnModel模型中:
public string Email { get; set; }
- 更新
GameSessionController中的SetPosition方法。在这里,将其公开为 web API,以便您可以从我们之前实现的 JavaScriptSendPosition函数接收 AJAX 调用:
[Produces("application/json")]
[HttpPost("/restapi/v1/SetGamePosition/{sessionId}")]
public async Task<IActionResult> SetPosition([FromRoute]Guid sessionId)
{
if (sessionId != Guid.Empty)
{
using (var reader = new StreamReader(Request.Body,
Encoding.UTF8, true, 1024, true))
{
...
}
}
return BadRequest("Id is empty");
}
然后,在StreamReader主体中添加以下代码:
var bodyString = reader.ReadToEnd();
if (string.IsNullOrEmpty(bodyString))
return BadRequest("Body is empty");
var turn = JsonConvert.DeserializeObject<TurnModel>(bodyString);
turn.User = await HttpContext.RequestServices.
xGetService<IUserService>().GetUserByEmail(turn.Email);
turn.UserId = turn.User.Id;
if (turn == null) return BadRequest("You must pass a TurnModel
object in your body");
var gameSession = await _gameSessionService.
GetGameSession(sessionId);
if (gameSession == null)
return BadRequest($"Cannot find Game Session {sessionId}");
if (gameSession.ActiveUser.Email != turn.User.Email)
return BadRequest($"{turn.User.Email} cannot play this turn");
gameSession = await _gameSessionService.
AddTurn(gameSession.Id, turn.User.Email, turn.X, turn.Y);
if (gameSession != null && gameSession.ActiveUser.Email !=
turn.User.Email)
return Ok(gameSession);
else
return BadRequest("Cannot save turn");
Note that it is good practice to prefix web APIs with a meaningful name and a version number (for example, /restapi/v1), as well as support for JSON and XML.
- 更新
Views文件夹中的游戏会话索引视图,并使用相应参数调用 JavaScriptSetGameSession函数:
@using Microsoft.AspNetCore.Http
@model TicTacToe.Models.GameSessionModel
@{
var email = Context.Session.GetString("email");
}
@section Desktop {
...
}
@section Mobile{
...
}
<h3>User Email @email</h3>
<h3>Active User <span id="activeUser">
@Model.ActiveUser?.Email</span></h3>
<vc:game-session game-session-id="@Model.Id"></vc:game-
session>
@section Scripts{
<script> SetGameSession("@Model.Id", "@email");
</script>
}
- 更新通信中间件中 WebSocket 的
ProcessEmailConfirmation方法:
public async Task ProcessEmailConfirmation(HttpContext
context,
WebSocket currentSocket, CancellationToken ct, string
email)
{
var user = await _userService.GetUserByEmail(email);
while (!ct.IsCancellationRequested &&
!currentSocket.CloseStatus.HasValue &&
user?.IsEmailConfirmed == false)
{
await SendStringAsync(currentSocket,
"WaitEmailConfirmation", ct);
await Task.Delay(500);
user = await _userService.GetUserByEmail(email);
}
if (user.IsEmailConfirmed)
await SendStringAsync(currentSocket, "OK", ct);
}
- 更新通信中间件中 WebSocket 的
ProcessGameInvitationConfirmation方法:
public async Task ProcessEmailConfirmation(HttpContext
context, WebSocket currentSocket, CancellationToken ct,
string email)
{
var user = await _userService.GetUserByEmail(email);
while (!ct.IsCancellationRequested &&
!currentSocket.CloseStatus.HasValue && user?
.IsEmailConfirmed == false)
{
await SendStringAsync(currentSocket,
"WaitEmailConfirmation", ct);
await Task.Delay(500);
user = await _userService.GetUserByEmail(email);
}
if (user.IsEmailConfirmed)
await SendStringAsync(currentSocket, "OK", ct);
}
- 更新
scripts2.jsJavaScript 文件中的CheckGameInvitationConfirmationStatus方法。它必须验证返回的数据:
function CheckGameInvitationConfirmationStatus(id) {
$.get("/GameInvitationConfirmation?id=" + id, function
(data) {
if (data.result === "OK") {
if (interval !== null) {
clearInterval(interval);
}
window.location.href = "/GameSession/Index/" + id;
}
});
}
- 更新 Gravatar Tag Helper 中的
Process方法,并正确处理没有照片的情况:
public override void Process(TagHelperContext context,
TagHelperOutput output)
{
byte[] photo = null;
if (CheckIsConnected()) photo = GetPhoto(Email);
else
{
string filePath = Path.Combine(Directory.
GetCurrentDirectory(),"wwwroot", "images",
"no-photo.jpg");
if (File.Exists(filePath)) photo =
File.ReadAllBytes(filePath);
}
if (photo != null && photo.Length > 0)
{
output.TagName = "img";
output.Attributes.SetAttribute("src",
$"data:img/jpeg;base64,
{Convert.ToBase64String(photo)}");
}
}
- 更新
GameInvitationService中的Add方法:
public Task<GameInvitationModel> Add(GameInvitation
Model gameInvitationModel)
{
_gameInvitations.Add(gameInvitationModel);
return Task.FromResult(gameInvitationModel);
}
-
更新桌面布局页面和移动布局页面。通过移除两页底部包含
script1.js和script2.js的显影environment标签来清理。 -
更新
scripts1.jsJavaScript 文件,并通过删除所有显示是否启用 WebSocket 的警告框来清除以前不必要的代码。 -
启动应用,注册新用户,通过邀请其他用户启动游戏会话,然后单击单元格。现在,您将看到一个 JavaScript 警报框:

到目前为止,您已经了解了如何将现有的GameSessionController操作转换为 RPC 样式的 web API。由于所有不同的 ASP.NET web 框架都集中在 ASP.NET Core 3 中的单个框架中,因此无需重写任何代码或过度更改现有代码,即可轻松快速地完成此操作。
在下一步中,我们将学习如何向 RPC 样式的 web API 添加新方法,以检查当前用户的回合是否已完成,这意味着下一个用户可以开始他们的回合:
- 在
GameSessionModel中添加一个名为TurnNumber的新属性,以便跟踪当前的匝数:
public int TurnNumber { get; set; }
- 在
TurnModel中添加一个名为IconNumber的新属性,以便您可以定义稍后需要用于显示的图标(X或O:
public string IconNumber { get; set; }
- 在
GameSessionController中增加一个名为GetGameSession的新方法,使用游戏会话服务获取游戏会话;它将仅限于 web API 调用:
[Produces("application/json")]
[HttpGet("/restapi/v1/GetGameSession/{sessionId}")]
public async Task<IActionResult> GetGameSession(Guid
sessionId)
{
if (sessionId != Guid.Empty)
{
var session = await _gameSessionService.
GetGameSession(sessionId);
if (session != null)
return Ok(session);
else
return NotFound($"cannot found session
{sessionId}");
}
else
return BadRequest("session id is null");
}
- 更新
GameSessionService中的AddTurn方法,以计算IconNumber和TurnNumber。为此,请替换以下代码行:
turns.Add(new TurnModel {
User = await _UserService.GetUserByEmail(email), X = x,
Y = y });
编写以下代码,允许设置图标编号:
public async Task<GameSessionModel> AddTurn(Guid id,
string email, int x, int y)
{
...
turns.Add(new TurnModel
{
User = await _UserService.GetUserByEmail(email),
X = x,
Y = y,
IconNumber = email == gameSession.User1?.
Email ? "1" : "2"
});
gameSession.Turns = turns;
gameSession.TurnNumber = gameSession.TurnNumber + 1;
...
}
- 更新游戏会话索引视图、用户图像,并通过将底部的脚本部分替换为以下代码片段来添加启用和禁用 gameboard 的可能性。这将启用或禁用电路板,具体取决于用户是否处于活动状态:
@section Scripts{
<script>
SetGameSession("@Model.Id", "@email");
EnableCheckTurnIsFinished();
@if(email != Model.ActiveUser?.Email)
{
<text>DisableBoard(@Model.TurnNumber);</text>
}
else
{
<text>EnableBoard(@Model.TurnNumber);</text>
}
</script>
}
- 使用以下
EnableCheckTurnIsFinished()函数将名为CheckTurnIsFinished.js的新 JavaScript 文件添加到wwwroot\app\js文件夹中。这将检查播放回合是否已完成:
function EnableCheckTurnIsFinished() {
interval = setInterval(() => {CheckTurnIsFinished();},
2000);
}
function CheckTurnIsFinished() {
var port = document.location.port ? (":" +
document.location.port) : "";
var url = document.location.protocol + "//" +
document.location.hostname + port +
"/restapi/v1/GetGameSession/" + window.GameSessionId;
$.get(url, function (data) {
if (data.turnFinished === true && data.turnNumber >=
window.TurnNumber) {
CheckGameSessionIsFinished();
ChangeTurn(data);
}
});
}
在同一CheckTurnIsFinished.js文件中,增加ChangeTurn()函数。这会改变玩家的回合数,并相应地禁用或启用棋盘:
function ChangeTurn(data) {
var turn = data.turns[data.turnNumber-1];
DisplayImageTurn(turn);
$("#activeUser").text(data.activeUser.email);
if (data.activeUser.email !== window.EmailPlayer) {
DisableBoard(data.turnNumber);
}
else {
EnableBoard(data.turnNumber);
}
}
添加禁用和启用电路板的实际功能,如下所示:
function DisableBoard(turnNumber) {
var divBoard = $("#gameBoard");
divBoard.hide();
$("#divAlertWaitTurn").show();
window.TurnNumber = turnNumber;
}
function EnableBoard(turnNumber) {
var divBoard = $("#gameBoard");
divBoard.show();
$("#divAlertWaitTurn").hide();
window.TurnNumber = turnNumber;
}
最后,添加一个DisplayImageTurn函数,该函数根据相应的回合操作级联样式表,如下所示:
function DisplayImageTurn(turn) {
var c = $("#c_" + turn.y + "_" + turn.x);
var css;
if (turn.iconNumber === "1") {
css = 'glyphicon glyphicon-unchecked';
}
else {
css = 'glyphicon glyphicon-remove-circle';
}
c.html('<i class="' + css + '"></i>');
}
更新bundleconfig.json使其包含新的CheckTurnIsFinished.js文件:
{
"outputFileName": "wwwroot/js/site.js",
"inputFiles": [
"wwwroot/app/js/scripts1.js",
"wwwroot/app/js/scripts2.js",
"wwwroot/app/js/GameSession.js",
"wwwroot/app/js/CheckTurnIsFinished.js"
],
"sourceMap": true,
"includeInProject": true
},
- 更新
GameSession.jsJavaScript 文件中的SetGameSession方法。现在,将TurnNumber默认设置为0:
function SetGameSession(gdSessionId, strEmail) {
window.GameSessionId = gdSessionId;
window.EmailPlayer = strEmail;
window.TurnNumber = 0;
}
- 更新
GameSession.jsJavaScript 文件中的SendPosition函数,移除我们之前添加的临时测试警报框。本节结束时,游戏将完全正常运行:
// Remove this alert
'success': function (data) {
alert(data);
}
- 现在,我们需要在
GameSessionController中添加两个新方法。第一个名为CheckGameSessionIsFinished,使用游戏会话服务获取会话,并决定游戏是平局还是被用户1或2赢得。因此,系统将知道游戏会话是否已完成。为此,请使用以下代码:
[Produces("application/json")]
[HttpGet("/restapi/v1/CheckGameSessionIsFinished/{sessionId}")]
public async Task<IActionResult> CheckGameSessionIsFinished(Guid sessionId)
{ if (sessionId != Guid.Empty)
{
var session = await
_gameSessionService.GetGameSession(sessionId);
if (session != null)
{
if (session.Turns.Count() == 9) return Ok("The
game was a draw.");
var userTurns = session.Turns.Where(x => x.User ==
session.User1).ToList();
var user1Won = CheckIfUserHasWon(session.User1?.Email,
userTurns);
if (user1Won) return Ok($"{session.User1.Email} has
won the game.");
else
{
userTurns = session.Turns.Where(x => x.User ==
session.User2).ToList();
var user2Won = CheckIfUserHasWon(session.User2?.
Email, userTurns);
if (user2Won)return Ok($"{session.User2.Email}
has won the game.");
else return Ok("");
}
}
else
return NotFound($"Cannot find session {sessionId}.");
}
else
return BadRequest("SessionId is null.");
}
现在,我们需要实现第二种方法,即CheckIfUserHasWon,它确定用户是否赢得了游戏,并将此信息发送给GameSessionController:
private bool CheckIfUserHasWon(string email,
List<TurnModel> userTurns)
{
if (userTurns.Any(x => x.X == 0 && x.Y == 0) &&
userTurns.Any(x => x.X == 1 && x.Y == 0) &&
userTurns.Any(x => x.X == 2 && x.Y == 0))
return true;
else if (userTurns.Any(x => x.X == 0 && x.Y == 1) &&
userTurns.Any(x => x.X == 1 && x.Y == 1) &&
userTurns.Any(x => x.X == 2 && x.Y == 1))
return true;
else if (userTurns.Any(x => x.X == 0 && x.Y == 2) &&
userTurns.Any(x => x.X == 1 && x.Y == 2) &&
userTurns.Any(x => x.X == 2 && x.Y == 2))
return true;
else if (userTurns.Any(x => x.X == 0 && x.Y == 0) &&
userTurns.Any(x => x.X == 0 && x.Y == 1) &&
userTurns.Any(x => x.X == 0 && x.Y == 2))
return true;
else if (userTurns.Any(x => x.X == 1 && x.Y == 0) &&
userTurns.Any(x => x.X == 1 && x.Y == 1) &&
userTurns.Any(x => x.X == 1 && x.Y == 2))
return true;
else if (userTurns.Any(x => x.X == 2 && x.Y == 0) &&
userTurns.Any(x => x.X == 2 && x.Y == 1) &&
userTurns.Any(x => x.X == 2 && x.Y == 2))
return true;
else if (userTurns.Any(x => x.X == 0 && x.Y == 0) &&
userTurns.Any(x => x.X == 1 && x.Y == 1) &&
userTurns.Any(x => x.X == 2 && x.Y == 2))
return true;
else if (userTurns.Any(x => x.X == 2 && x.Y == 0) &&
userTurns.Any(x => x.X == 1 && x.Y == 1) &&
userTurns.Any(x => x.X == 0 && x.Y == 2))
return true;
else
return false;
}
- 将名为
CheckGameSessionIsFinished.js的新 JavaScript 文件添加到wwwroot\app\js文件夹中,并相应地更新bundleconfig.json文件:
function CheckGameSessionIsFinished() {
var port = document.location.port ? (":" +
document.location.port) : "";
var url = document.location.protocol + "//" +
document.location.hostname + port +
"/restapi/v1/CheckGameSessionIsFinished/" +
window.GameSessionId;
$.get(url, function (data) {
debugger;
if (data.indexOf("won") > 0 || data == "The game
was a draw.") {
alert(data);
window.location.href = document.location.protocol +
"//" + document.location.hostname + port;
}
});
}
- 启动游戏,注册新帐户,打开确认电子邮件,确认,发送游戏邀请电子邮件,确认游戏邀请,然后开始游戏。现在一切正常,您应该能够玩游戏,直到用户获胜或游戏以平局结束:

在本节中,我们将介绍 RPC 样式,它非常接近标准 MVC 控制器操作。在以下部分中,您将了解一种完全不同的方法,它基于资源和资源管理。
祝贺您已经完成了 RPC 风格的实现,并创建了一个漂亮、现代、基于浏览器的游戏,其中两个用户可以互相玩。
做好准备–在以下各节中,您将了解更高级的技术,并了解如何使用两种最著名的 API 通信样式(REST 和 HATEOAS)为互操作性提供 web API。
要玩游戏,您可以使用两个单独的私有浏览器窗口,也可以使用两个不同的浏览器,如 Chrome、Edge 或 Firefox。为了测试您的 web API,建议您安装并使用 Postman(https://www.getpostman.com/ ),但您也可以使用任何其他与 HTTP REST 兼容的客户端,如 Fiddler(https://www.telerik.com/fiddler 、SoapUI(https://www.soapui.org/downloads/soapui.html ,甚至 Firefox 的高级功能。
构建 REST 风格的 web API
REST 风格是 Roy Fielding 在 2000 年代发明的,是提供基于多种技术的系统之间互操作性的最佳方法之一,无论是在您的网络中还是在 internet 上。
此外,REST 方法本身并不是一种技术,而是用于高效使用 HTTP 协议的一些最佳实践。
REST 没有像 SOAP 或 XML-RPC 那样添加新层,而是使用 HTTP 协议的不同元素来提供服务:
- URI 标识资源。
- HTTP 谓词标识一个操作。
- 响应不是资源,而是资源的表示。
- 客户端身份验证作为请求头中的参数传递。
与 RPC 样式不同,它的主要用途不再是提供操作,而是管理和操作资源。
To find out even more about the concepts and ideas behind REST, you should read Roy Fielding's dissertation on this subject, which you can find at http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm.
如下图所示,Tic Tac Toe 应用中主要有三种类型的资源:
- 用户
- 游戏邀请函
- 游戏环节:

让我们学习如何使用 REST 样式使用 REST API 构建游戏邀请:
- 添加两个新方法,一个名为
All,返回所有游戏邀请,另一个名为Delete,根据指定的游戏邀请 ID 删除游戏邀请。您需要将这两个方法添加到GameInvitationService并相应更新游戏邀请服务界面:
public Task<IEnumerable<GameInvitationModel>> All()
{
return Task.FromResult<IEnumerable<GameInvitationModel>>
(_gameInvitations.ToList());
}
public Task Delete(Guid id)
{
_gameInvitations = new ConcurrentBag<GameInvitationModel>
(_gameInvitations.Where(x => x.Id != id));
return Task.CompletedTask;
}
- 添加一个名为
GameInvitationApiController的新 API 控制器,右键点击Controllers文件夹,选择添加控制器。然后,选择具有读/写操作模板的 API 控制器:

- 删除自动生成的代码,并将其替换为以下 REST API 实现:
- 首先,插入以下代码作为游戏邀请 API 控制器的支架,其中包含预期输出和实际端点路由的装饰器。然后,我们有一个构造函数,它将游戏邀请服务和用户服务注入控制器,如下所示:
[Produces("application/json")]
[Route("restapi/v1/GameInvitation")]
public class GameInvitationApiController : Controller
{
private IGameInvitationService
_gameInvitationService;
private IUserService _userService;
public GameInvitationApiController
(IGameInvitationService
gameInvitationService, IUserService userService)
{
_gameInvitationService = gameInvitationService;
_userService = userService;
}
...
}
[HttpGet]
public async Task<IEnumerable<GameInvitationModel>> Get()
{
return await _gameInvitationService.All();
}
[HttpGet("{id}", Name = "Get")]
public async Task<GameInvitationModel> Get(Guid id)
{
return await _gameInvitationService.Get(id);
}
[HttpPost]
public IActionResult Post([FromBody]GameInvitationModel
invitation)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
var invitedPlayer =
_userService.GetUserByEmail(invitation.EmailTo);
if (invitedPlayer == null) return BadRequest();
_gameInvitationService.Add(invitation);
return Ok();
}
[HttpPut("{id}")]
public IActionResult Put(Guid id,
[FromBody]GameInvitationModel invitation)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
var invitedPlayer =
_userService.GetUserByEmail(invitation.EmailTo);
if (invitedPlayer == null) return BadRequest();
_gameInvitationService.Update(invitation);
return Ok();
}
[HttpDelete("{id}")]
public void Delete(Guid id)
{
_gameInvitationService.Delete(id);
}
- 启动应用,安装并启动 Postman,以便您可以对您提供的新 REST API 进行一些手动测试,并向
http://<yourhost>/restapi/v1/GameInvitation发送 HTTPGET请求。由于您尚未创建任何游戏邀请,因此将不会有游戏邀请:

- 新建游戏邀请,向
http://<yourhost>/restapi/v1/GameInvitation发送 HTTPPOST请求,点击 Body,选择 raw 和 JSON,使用"id":"7223160d-6243-498b-9d35-81b8c947b5ca"、"EmailTo":"example@example.com"和"InvitedBy":"test@test.com"作为参数:

Basic Concepts of ASP.NET Core 3 via a Custom Application: Part 1
- 您可以通过向
http://<yourhost>/restapi/v1/GameInvitation发送 HTTPGET请求,或者更具体地说,通过向http://<yourhost>/restapi/v1/GameInvitation/7223160d-6243-498b-9d35-81b8c947b5ca发送 HTTPGET请求来检索游戏邀请:

- 更新游戏邀请,向
http://<yourhost>/restapi/v1/GameInvitation/7223160d-6243-498b-9d35-81b8c947b5ca发送 HTTPPUT请求,点击 Body,选择 raw 和 JSON,使用"id":"7223160d-6243-498b-9d35-81b8c947b5ca"、"EmailTo":"updated@updated.com"、"InvitedBy":"test@test.com"作为参数:

- 查看更新后的游戏邀请,向
http://<yourhost>/restapi/v1/GameInvitation/7223160d-6243-498b-9d35-81b8c947b5ca发送 HTTPGET请求:

- 删除游戏邀请并向
http://<yourhost>/restapi/v1/GameInvitation/7223160d-6243-498b-9d35-81b8c947b5ca发送 HTTPDELETE请求:

- 验证游戏邀请的删除并向
http://<yourhost>/restapi/v1/GameInvitation发送 HTTPGET请求:

REST 样式是目前市场上最常见的 web API 样式。它很容易理解,并且已经适应了互操作性用例。
在下一节中,您将了解一种称为 HATEOAS 的更高级样式,它特别适合于不断发展的 web api。
构建 HATEOAS 风格的 web API
作为应用状态(HATEOS风格)引擎的超媒体是提供高效 web API 的另一种方法。然而,它与我们介绍的其他两种风格完全不同。使用这种方法,客户端可以通过遍历 HTTP 响应中提供的各种超媒体链接来动态导航到资源。
这种风格的优点是服务器不再驱动应用状态;相反,是服务器返回的超媒体链接监督了这一过程。
此外,与其他样式相比,由于客户端不再将 URI 硬编码为动作(RPC 样式)或资源(REST 样式),因此 API 更改的处理效果要好得多。相反,它们可以处理服务器为发出请求后收到的每个响应返回的超媒体链接。这是一个有趣的概念,因为它允许更灵活和更可进化的 web API。
下图显示了如何将 HATEOAS 样式应用于 Tic Tac Toe 应用的示例:

此图的 JSON 表示示例如下:
{
"_links": {
"self": { "href": "/gameinvitations" },
"next": { "href": "/gameinvitations?page=2" },
"find": {
"href": "/gameinvitations{?Id}",
"templated": "true"
}
},
"_embedded": {
"gameinvitations": [
{
"_links": {
"self": { "href": "/gameinvitations/f1eaf6ac-c998-40da-
8eb5-198eaa2cc96f" },
"confirm": { "href": "/gameinvitations/f1eaf6ac-c998-
40da-8eb5-198eaa2cc96f/confirm" }
},
"isConfirmed": "false",
"confirmDate": "null",
"emailTo": {
"self": { "href": "/user/1" }
},
"invitedBy": { "self": "\"{\"href\":\"/user/2\"}" }
}
]
}
}
HATEOAS 提供了一些强大的功能,所有这些功能都允许我们独立地开发组件。客户机可以与服务器上运行的业务工作流完全解耦,后者通过使用链接和其他超媒体工件(如表单)来管理交互。
无论您使用什么样式,无论是 RPC、RESTful 还是 HATEOAS,根据最适合于什么场景的样式以及它作为解决方案的优雅程度,除非您的 api 是安全的,否则它都不会非常有用。在下一节中,您将了解 web API 的基本安全性。
保护您的 web API
在这一点上,我们已经成功地创建了一些 API 端点,但有一个问题是,任何人都可以从任何浏览器点击端点,甚至可以修改/删除我们的游戏邀请,只要他们知道传递什么参数。这是一种安全威胁,您可以想象处理高级别敏感功能的应用所带来的影响。
我们将在第 10 章、保护 ASP.NET Core 3 应用和第 11 章、保护 ASP.NET 应用–漏洞中处理 ASP.NET Core 3 的安全问题,但值得注意的是我们的 web API 端点的可用安全措施。让我们看一下下面的屏幕截图,它显示了 Postman 的授权选项卡:

注意邮递员期望的不同类型的授权,包括无授权,这意味着根本没有授权。
第 11 章保护 ASP.NET 应用的安全–漏洞将让我们深入了解我们必须注意的常见安全漏洞。考虑到这一点,使用以下任何授权选项保护我们的 web API 端点始终很重要:
- API 密钥
- 不记名代币
- 基本授权
- 摘要作者
- OAuth 1.0
- OAuth2.0
- 霍克认证
- AWS 签名
- NTLM 身份验证
下面的文档将进一步解释这些身份验证选项,其中讨论了 Postman 中的授权:https://learning.getpostman.com/docs/postman/sending-api-requests/authorization/ 。
除了确保我们的 API 不受不想要的用户的攻击外,我们还需要确保合法用户拥有良好的 API 使用体验。帮助我们的用户做到这一点的方法之一是让他们使用我们的 API 规范访问文档。我们将在下一节学习如何做到这一点。
带有 Swagger/OpenAPI 的 ASP.NET Core web API 帮助页
随着任何应用的大小和 web API 端点数量的增长,通常最好有关于 API 本身、可用端点、它们期望作为参数的内容以及所做的任何相应 API 调用的正常响应的文档。
手动记录每个 API 端点可能会很乏味,但幸运的是,Swagger/OpenAPI 在这里起到了解救作用。让我们来看一看:
- 转到我们的 Tic-Tac-Toe 演示应用,右键单击
TicTacToe项目,转到 NuGet 软件包管理器,搜索Swashbuckle.AspnetCore。现在,单击安装按钮:

您也可以通过转到 Package Manager 控制台并在 Package Manager 的命令提示符下键入Install-Package Swashbuckle.AspNetCore -Version 5.0.0-rc4来安装 Swashback。
- 接下来,将以下代码片段添加到
Startup类中的ConfigureServices方法中:
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo {
Title = "Learning ASP.Net Core 3.0 Rest-API",
Version = "v1",
Description = "Demonstrating auto-generated
API documentation",
Contact = new OpenApiContact
{
Name = "Kenneth Fukizi",
Email = "example@example.com",
},
License = new OpenApiLicense
{
Name = "MIT",
}
});
using Microsoft.OpenApi.Models;
OpenApiInfo
- 最后,我们需要在同一
Startup.cs类的Configure方法中添加以下代码:
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json",
"LEARNING ASP.CORE 3.0 V1");
});
- 启动
TicTacToe演示应用,并将 Swagger 添加到根 URL。您将看到到目前为止我们创建的所有 API 端点,所有这些端点都记录在 Swagger 索引页面上:

- Swagger 还可以用于测试任何 API 端点的预期功能,而不是其他最常用的工具,如 Postman 和 Fiddler。当然,测试 API 端点的另一种常见方法是将它们作为浏览器 URL 手动输入。您可以使用 Swagger 测试端点,方法是单击端点(展开端点),单击“尝试”按钮(如以下屏幕截图右侧所示),然后输入预期值:

- 我们可能希望在主索引页上有 API 文档,特别是在整个应用是我们为其他用户开发的 API 的情况下。在这种情况下,我们只需添加一个空的
RoutePrefix SwaggerUIOption,如下所示:
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json",
"LEARNING ASP.CORE 3.0 V1");
c.RoutePrefix = string.Empty;
});
对于您希望添加的每一个附加 API 端点,Swagger 都会自动获取并记录它,让您只专注于生成代码而不是文档,这不是很解放吗?
总结
在本章中,您学习了如何为应用构建 web API,以实现集成和松耦合应用体系结构。
我们探讨了 web API 的三种不同样式,即 RPC、REST 和 HATEOAS。这些样式中的每一种都有特定的优点和用例。您必须根据具体的应用需要仔细选择,因为没有一种样式比其他样式更优秀。
在本章中,我们介绍了如何将现有控制器操作转换为 RPC 样式的 web API 以及如何从头构建 REST 样式和 HATEOAS 样式的 web API 的示例。然后,我们使用 Postman 手动测试我们的 web API,您已经掌握了足够的知识,可以将所有这些新概念应用到您自己的环境中。最后,我们使用 OpenAPI 的招摇过市功能为我们的 API 端点自动生成文档。
总之,我们学习了如何构建 RESTAPI,掌握了如何将控制器操作转换为 RPC 样式的 web API 的技能,并从头开始构建它们。我们还学习了如何构建 HATEOAS 风格的 web API,以及如何配置我们的 API,以便它们有一个包含 API 规范文档的帮助页面。
在下一章中,我们将讨论如何在 ASP.NET Core 3 应用中使用 Entity Framework Core 3 访问数据。
九、使用实体框架核心 3 访问数据
我们在 Tic-Tac-Toe 演示 web 应用的实现方面取得了长足的进步,但是当我们重新启动应用时,我们的用户注册和应用数据都不会被记住。这是因为我们尚未保存或保留任何数据。
要持久化数据并在应用启动时重新加载数据,我们必须将其放入某种持久性存储中,例如文件(XML、JSON、CSV)或数据库。
数据库将是最佳选择,因为与简单的文件存储相比,它提供了更好的性能和更高的安全性,这就是为什么我们将在本章的示例中使用这种方法。
从旧的 ASP.NET 3 天开始,我们已经能够使用名为实体框架的对象关系映射(ORM框架)以更高效、更简单的方式访问数据库中的数据。ASP.NET Core 3 可与此框架的专用版本实体框架 Core 3(实体框架 6.3 的一部分)无缝配合,也可与以前的版本配合使用。
我们将在本章开始介绍 Entity Framework Core 3.0 以及如何安装它。然后,我们将了解使用代码优先方法创建数据库所需的所有类,然后我们将向您展示如何执行迁移。接下来,我们将探讨正常的 CRUD 操作,并解释最常见和最重要的数据关系。最后,我们将更深入地解释查询并介绍事务。
本章结束时,您将能够使用 Entity Framework Core 连接到数据库,使用带更新的迁移,执行基本 CRUD 操作,使用 Fluent API,对数据库执行复杂查询,以及使用事务。
在本章中,我们将介绍以下主题:
- 实体框架核心 3 入门
- 使用实体框架核心 3 数据注释
- 使用实体框架核心 3 迁移
- 创建、读取、更新和删除数据
- 理解数据关系
- 处理查询
- 使用事务
实体框架核心 3 入门
Microsoft.AspNetCore.App元包包含 Entity Framework Core 3,包括您需要使用Microsoft SQL Server和SQLite的所有包。
Note that, if you need to work with other databases such as MySQL, you have to download additional packages from NuGet.
You can find a list of all the currently available Entity Framework Core 3 NuGet packages here: https://www.nuget.org/packages?page=2&q=Tags%3A%22entity-framework-core%22.
Entity Framework 是 Microsoft 版本的 ORM,并不是唯一可以在 ASP.NET Core 上使用的 ORM。其他与.NETCore 无缝协作的 ORM 包括 NHibernate、LINQtoSQL 和 Dapper。
ORM 是访问数据库的推荐方式,尤其是关系数据库管理系统(关系数据库管理系统),以抵消记录充分的阻抗失配。作为一名开发人员,ORMs 将您从 SQL 操作和实现的本质中抽象出来。
Entity Framework Core 3.0 是 Entity Framework Core 1.0 的更高版本,以及自 Entity Framework 版本专门为.NET Framework 设计以来一直在发展的后续版本。
通过在 Package Manager 控制台上运行以下命令,可以安装 Entity Framework Core 3.0:
Install-package Microsoft.EntityFrameworkCore
通过运行上述命令,您将收到以下输出:

您还需要在 Package Manager 控制台上使用以下命令安装 SQL Server 提供程序,因为它们可以协同工作:
Install-package Microsoft.EntityFrameworkCore.SqlServer
在实际开始使用 EF Core 3.0 之前,首先尝试建立到数据库的连接是很自然的,我们将在下一节中介绍。
建立联系
要打开数据库会话并查询和更新实体实例,您需要使用DbContext,它基于工作单元和存储库模式的组合。
让我们学习如何准备 Tic-Tac-Toe 应用,以便从头开始使用 Entity Framework Core 3 通过DbContext和连接字符串连接到 SQL 数据库:
- 转到解决方案资源管理器,添加一个名为
Data的新文件夹,添加一个名为GameDbContext的新类,并为每个模型实现一个DbSet属性(UserModel、TurnModel等等):
public class GameDbContext : DbContext
{
public DbSet<GameInvitationModel> GameInvitationModels
{get; set; }
public DbSet<GameSessionModel> GameSessionModels { get;
set; }
public DbSet<TurnModel> TurnModels { get; set; }
public DbSet<UserModel> UserModels { get; set; }
public GameDbContext(DbContextOptions<GameDbContext>
dbContextOptions) : base(dbContextOptions) { }
}
- 在
Startup类中注册GameDbContext。然后,将连接字符串和数据库提供程序作为参数传递给构造函数。目前我们只需要一个实例,所以我们将使用AddSingleton:
var connectionString =
_configuration.GetConnectionString("DefaultConnection");
services.AddEntityFrameworkSqlServer()
.AddDbContext<GameDbContext>((serviceProvider,
options) => options.UseSqlServer(connectionString).
UseInternalServiceProvider(serviceProvider)
);
var dbContextOptionsbuilder =
new DbContextOptionsBuilder<GameDbContext>()
.UseSqlServer(connectionString);
services.AddSingleton(dbContextOptionsbuilder.Options);
Please note that you will need to add the following using statements for the code to compile: using TicTacToe.Data; and
using Microsoft.EntityFrameworkCore;.
- 更新名为
UserService.cs的用户服务类,以便您可以使用游戏数据库上下文:GameDbContext.cs。为游戏数据库上下文添加新的公共构造函数和私有成员:
private DbContextOptions<GameDbContext> _dbContextOptions;
public UserService(DbContextOptions<GameDbContext>
dbContextOptions)
{
_dbContextOptions = dbContextOptions;
}
- 更新
UserService中的RegisterUser方法,以便您可以使用游戏数据库上下文:GameDbContext:
public async Task<bool> RegisterUser(UserModel userModel)
{
using(var Database = new GameDbContext
(_dbContextOptions))
{
Database.UserModels.Add(userModel);
await Database.SaveChangesAsync();
return true;
}
}
- 将名为
ModelBuilderExtensions的新扩展添加到Extensions文件夹。这将用于定义表名约定:
public static class ModelBuilderExtensions
{
public static void RemovePluralizingTableNameConvention(
this ModelBuilder modelBuilder)
{
foreach (IMutableEntityType entity in
modelBuilder.Model.GetEntityTypes())
{
entity.SetTableName(entity.DisplayName());
}
}
}
- 更新游戏数据库上下文
GameDbContext中的OnModelCreating方法,将模型配置为配置从DbSet属性中公开的实体类型中发现的模型,例如:public DbSet<UserModel> UserModels { get; set; }。然后,我们调用ModelBuilderExtensions扩展类来应用表名约定:
protected override void OnModelCreating(ModelBuilder
modelBuilder)
{
modelBuilder.RemovePluralizingTableNameConvention();
}
Note that we could also use another method called OnConfiguring in the database context in order to configure the database context without using DbContextOptions.
- 将名为
GameDbContextFactory的新类添加到Data文件夹中。这将用于实例化游戏数据库上下文GameDbContext,具体选项如下:
public class GameDbContextFactory :
IDesignTimeDbContextFactory<GameDbContext>
{
public GameDbContext CreateDbContext(string[] args)
{
var optionsBuilder = new
DbContextOptionsBuilder<GameDbContext>();
optionsBuilder.UseSqlServer(@"Server=
(localdb)\MSSQLLocalDB;Database=TicTacToe;
Trusted_Connection=True;
MultipleActiveResultSets=true");
return new GameDbContext(optionsBuilder.Options);
}
}
Note that you will have to add the following using statements so that the code compiles: using Microsoft.EntityFrameworkCore; and using Microsoft.EntityFrameworkCore.Design;.
如果您以前使用过数据库,那么应该熟悉连接字符串的概念。它们包含连接数据库所需的配置(地址、用户名、密码等)和设置(加密、协议等)。
在 ASP.NET Core 3 中,您还可以使用appSettings.<env>.json文件来配置连接字符串。当我们使用此文件中的ConnectionStrings部分时,会自动加载连接字符串:
"ConnectionStrings": {
"DefaultConnection":
"Server=(localdb)\\MSSQLLocalDB;Database=TicTacToe;
Trusted_Connection=True;MultipleActiveResultSets=true"
},
如您所见,您可以使用GetConnectionString方法在运行时检索连接字符串:
var databaseConnectionString =
_configuration.GetConnectionString("DefaultConnection");
这是使用游戏数据库上下文GameDbContext以及存储在 Tic-Tac-Toe 应用的appsettings.json配置文件中的相应默认连接字符串所需知道的一切。
在我们开始在我们的模型上使用游戏数据库上下文之前,重要的是确保我们已经掌握了所有必需的基础知识,以便我们可以使用游戏数据库上下文来创建和访问数据库表。建议每个数据库表都有一个主键,如果它是一个关系表的话;如果它与另一个表相关,则需要有外键。在下一节中,我们将了解如何通过数据注释在代码中定义主键和外键。
通过数据注释定义主键和外键
现在,我们需要修改现有的模型,以便能够在 SQL 数据库中持久化它们。为了允许 EntityFrameworkCore3.0 创建、读取、更新和删除记录,我们需要为每个模型指定一个主键。我们可以通过使用数据注释来实现这一点,数据注释允许我们使用[Key]装饰器装饰属性。
以下是如何使用UserModel的数据注释的示例:
public class UserModel
{
[Key]
public long Id { get; set; }
...
}
您应该将此应用于 Tic Tac Toe 应用中的UserModel、GameInvitationModel、GameSessionModel和TurnModel。您可以重用现有的Id属性,并使用[Key]装饰器对其进行装饰,或者在模型尚未包含Id属性的情况下添加新属性。
Note that it is sometimes required to use composite keys as the identity for your rows in a table. In this case, decorate each property with the [Key] decorator. Furthermore, you can use Column[Order=] to define the position of the property if you need to order a composite key.
在使用SQL Server(或任何其他 SQL 92 DBMS)时,首先要考虑的是表之间的关系。在 Entity Framework Core 3 中,您可以使用[ForeignKey]装饰器在模型中指定外键。
关于 Tic Tac Toe 应用,这意味着您必须更新GameInvitationModel并向用户模型 ID 添加外键关系。执行以下步骤:
- 更新
GameInvitationModel并向InvitedByUser属性添加外键属性:
public class GameInvitationModel
{
[Key]
public Guid Id { get; set; }
public string EmailTo { get; set; }
public string InvitedBy { get; set; }
public UserModel InvitedByUser {get; set;}
[ForeignKey(nameof(InvitedByUserId))]
public Guid InvitedByUserId { get; set; }
public bool IsConfirmed { get; set; }
public DateTime ConfirmationDate { get; set; }
}
这是一个已经存在的GameInvitationalModel类,我们只是用[Key]属性装饰属性Id,以便 Entity Framework Core 3.0 能够将其识别为主键。外键属性[ForeignKey(nameof(InvitedByUserId)]修饰名为InvitedUserId的 GUID,以便 EF Core 3.0 能够将此属性视为另一个表的外键。
- 更新
GameSessionModel并在UserId1中添加外键:
public class GameSessionModel
{
[Key]
public Guid Id { get; set; }
...
[ForeignKey(nameof(UserId1))]
public UserModel User1 { get; set; }
...
}
这里,我们有一个GameSessionModelPOCO 类,它在Id属性上有一个主键属性,在名为User 1的用户模型上有一个辅助键属性。这将允许 EF Core 3.0 创建一个GameSessionModel表,分别使用名为Id的主键和名为User1的外键。
- 更新
TurnModel并在UserId中添加外键:
public class TurnModel
{
[Key]
public Guid Id { get; set; }
[ForeignKey(nameof(UserId))]
public Guid UserId { get; set; }
public UserModel User { get; set; }
public int X { get; set; }
public int Y { get; set; }
public string Email { get; set; }
public string IconNumber { get; set; }
}
默认情况下,EntityFrameworkCore3 使用模式表示映射模型中的所有属性。但是一些更复杂的属性类型不兼容,这就是为什么我们应该将它们从自动映射中排除。但我们如何做到这一点?嗯,通过使用[NotMapped]装饰器。这有多简单和直接?
- 对于 Tic-Tac-Toe 应用,将活动用户保留一段时间是没有意义的,因此您应该使用
GameSessionModel中的[NotMapped]装饰器将其从自动映射过程中排除:
public class GameSessionModel
{
[Key]
public Guid Id { get; set; }
...
[NotMapped]
public UserModel Winner { get; set; }
[NotMapped]
public UserModel ActiveUser { get; set; }
public Guid WinnerId { get; set; }
public Guid ActiveUserId { get; set; }
public bool TurnFinished { get; set; }
public int TurnNumber { get; set; }
}
现在您已经使用 Entity Framework Core 3 数据注释装饰了所有模型,您将注意到在GameSessionModel中有两个属性User1和User2,它们指向相同的UserModel实体。这会导致循环关系,这会给我们(在处理关系数据库时)执行诸如级联更新或级联删除之类的操作带来问题。
For more information on Entity Framework Data Annotations, please visit https://msdn.microsoft.com/en-us/library/jj591583(v=vs.113).aspx.
- 为了避免循环关系,您需要使用
[ForeignKey]装饰器装饰User1,并更新游戏数据库上下文GameDbContext中的OnModelCreating方法,为User2定义外键。这两个修改将允许您定义两个外键,同时避免自动级联操作,这将导致问题:
protected override void OnModelCreating(ModelBuilder
modelBuilder)
{
modelBuilder.RemovePluralizingTableNameConvention();
modelBuilder.Entity(typeof(GameSessionModel))
.HasOne(typeof(UserModel), "User2")
.WithMany()
.HasForeignKey("User2Id").OnDelete(DeleteBehavior.Restrict);
}
- 现在,您需要修复单元测试。您可能已经注意到,如果您尝试编译解决方案,单元测试项目将不再构建。在这里,您需要更新单元测试,因为
UserService现在需要DbContextOptions的实例,如下所示:
var dbContextOptionsBuilder =
new DbContextOptionsBuilder<GameDbContext>()
.UseSqlServer(@"Server=
(localdb)\MSSQLLocalDB;Database=TicTacToe;
Trusted_Connection=True;MultipleActiveResultSets=true");
var userService = new
UserService(dbContextOptionsBuilder.Options);
Please note that, while the preceding code snippet fixes the tests, it is not good practice to work with real database connections inside unit tests. Ideally, the data connection should be mocked or abstracted in some way. If you need to use real data for integration tests, the connection information should come from a config file instead of being hardcoded.
现在,单元测试为UserService的构造函数提供了选项生成器的新重载。现在我们已经在模型中定义了主键和外键,我们可以创建初始数据库模式,并为迁移准备应用。在下一节中,我们将介绍 EF Core 3 迁移。
使用实体框架核心 3 迁移
正如您已经看到的,在开发应用时,当您重构和完成项目时,您的模型可能会频繁更改。这可能导致数据库架构不同步,需要手动更新。您可以通过创建升级的脚本来实现这一点,但这不是一个理想的解决方案。
幸运的是,EntityFrameworkCore3 包含了一个名为迁移的特性来帮助您完成这项繁琐的任务。它会自动使您的模型及其相应的数据库模式保持同步。
在您更新了模型、服务和控制器,使其符合前面的约束条件并修改了游戏数据库上下文GameDbContext之后,相应地,您就可以使用 Entity Framework Core 3 迁移了。以下步骤将向您展示如何使用 Entity Framework Core 3 迁移:
- 添加名为
InitialDbSchema的数据库模式的第一个版本。要执行此操作,请通过单击工具| NuGet Package Manager | Package Manager 控制台打开 NuGet Package Manager,并执行Add-Migration InitialDbSchema命令:

- Visual Studio 将自动添加名为
Migrations的新文件夹。它将包含两个自动生成的文件,这些文件将帮助您在将来管理和升级数据库架构:

如果可以从开发环境访问数据库,则可以直接从 Visual Studio 2019 中更新数据库。以下步骤将引导您完成更新过程:
- 进入 Package Manager 控制台,执行
Update-Database命令。这将在首次使用数据库时创建数据库,或在更改模型时自动更新数据库:

- 然后,转到 SQL Server 对象资源管理器,分析 Entity Framework 3 迁移在 SQL Server 中自动生成的数据库架构:

- 之后,右键单击
__EFMigrationsHistory表并选择查看数据,查看实体框架迁移如何跟踪数据库架构版本:

如果无法从开发环境(例如,在暂存或生产环境中)访问数据库,则必须生成 SQL 脚本文件。
- 转到 Package Manager 控制台,执行
Script-Migration命令自动生成 SQL 脚本文件,该文件可用于创建 Tic Tac Toe 应用的数据库:

- 然后,在特定的环境中执行生成的 SQL 脚本文件,如使用首选的数据库工具(例如,SQL Server Management Studio 等)进行登台和生产,以创建 Tic Tac Toe 应用的数据库。
您还可以直接从代码中使用 Entity Framework Core 3 迁移,以确保数据库始终与模型同步。为此,需要在Startup类的Configure方法中调用GameDbContext实例的Migrate方法。执行以下步骤以执行此操作:
- 更新
Startup类中的Configure方法,并在方法底部添加以下说明:
using (var scope =
app.ApplicationServices.GetService<IServiceScopeFactory>()
.CreateScope())
{
scope.ServiceProvider.GetRequiredService<GameDbContext>()
.Database.Migrate();
}
这将游戏数据库上下文的Migrate方法作为一个作用域服务,可以在应用运行时解析。
- 按F5启动 Tic Tac Toe 应用
Note that if a table or property doesn't exist in the database and if the connection string provides enough access rights, Entity Framework Core 3 will automatically create the missing table or the property/column that does not exist.
现在我们已经更新了模型和相应的应用数据库,所有模型数据都将被持久化,应用状态将可用,即使在应用重新启动之后也是如此。这意味着你不能注册已经存在的电子邮件,你必须手动添加新的电子邮件,所以现在就截断数据库并删除它们。
在下一节中,我们将重点介绍创建、读取、更新和删除数据。
创建、读取、更新和删除数据
到目前为止,我们已经定义了我们的模型,并以一致和连贯的方式启动和运行了数据库。在本节中,我们将学习如何处理数据并执行创建、读取、更新和删除操作。
让我们学习如何使用GameDbContext处理数据:
- 首先更新
UserService,删除ConcurrencyBag和静态构造函数,更新GetUserByEmail方法:
public async Task<UserModel> GetUserByEmail(string email)
{
using (var Database = new
GameDbContext(_dbContextOptions))
{
return await Database.UserModels.FirstOrDefaultAsync(
x => x.Email == email);
}
}
- 更新
UserService中的UpdateUser方法,学习如何使用数据库上下文更新数据:
public async Task UpdateUser(UserModel userModel)
{
using (var gameDbContext =
new GameDbContext(_dbContextOptions))
{
gameDbContext.Update(userModel);
await gameDbContext.SaveChangesAsync();
}
}
- 更新
UserService中的GetTopUsers方法,学习如何使用数据库上下文通过排序和过滤数据构建高级查询:
public async Task<IEnumerable<UserModel>> GetTopUsers(
int numberOfUsers)
{
using (var gameDbContext =
new GameDbContext(_dbContextOptions))
{
return await gameDbContext.UserModels.OrderByDescending(
x => x.Score).ToListAsync();
}
}
- 在
UserService中添加一个名为IsUserExisting的新方法。这将用于检查用户是否存在。更新IUserService界面:
public async Task<bool> IsUserExisting(string email)
{
using (var gameDbContext =
new GameDbContext(_dbContextOptions))
{
return await gameDbContext.UserModels.AnyAsync(
user => user.Email == email);
}
}
在本节中,您学习了如何配置应用,以便它们可以使用 EntityFrameworkCore3 及其所有有用和有趣的功能。这是一种从开发人员的日常生活中抽象复杂性和消除耗时任务的好方法。
您不需要学习任何其他语言(如 SQL),也不需要更改环境来创建、读取、更新和删除数据库中的记录。一切都可以在您的代码和 VisualStudio 中完成,以确保开发人员的高生产率和效率。
理解数据关系
让我们从我们的正常演示 Tic Tac Toe 游戏应用休息一下,看看一些实体框架核心 3 概念更详细。
在理解和进行任何高级查询之前,了解两个或多个实体之间可能存在的数据关系是很重要的。
我们已经从主键和外键的角度看了基础知识,但是让我们看一下它们的定义,这将帮助您更好地理解这些术语。
主键
主键用于唯一标识表中的每条记录。例如,在学生表中,它将是学生 ID 列。我们需要注意的是,主键可以是复合键,其中两个不同的列可以组合成表中记录的唯一标识符。
在任何表中,每个记录的主键列中都必须有一个非空值,并且它应该始终是唯一的,并且从不重复。
对于关系表,必须定义主键。定义主键后,不能在同一个表中定义另一个主键。
索引用于存储主键,使其保持唯一,并确保外键可以引用它。
我们在本章前面的示例中使用了[Key]属性来定义模型上的主键。
外键
表中的外键是用来引用其他表中主键的列。与主键一样,外键也可以是多个列的组合。
当您有两个不同的表或实体相互关联时,有几种方法可以将它们关联起来。例如,您可以有一对一、一对多和多对多的关系。
在本章的示例中,我们主要使用一个[ForeignKey]属性来修饰我们想要用作外键的字段,但我们也可以使用 Fluent API 来定义外键。我们可以将.HasForeignKey()属性用于此:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity(typeof(GameSessionModel))
.HasOne(typeof(UserModel), "User2")
.WithMany()
.HasForeignKey("User2Id")
.OnDelete(DeleteBehavior.Restrict);
}
前面的代码片段还有几个其他属性,我们可以使用这些属性定义表之间的关系,示例包括.HasOne()和.WithMany()。我们将在本章后面解释这些。
一对一关系
当我们有一对一的关系时,我们可以说一个表中的一条记录只能与另一个表中的一条记录有关系。
例如,如果我们有一个User类和一个UserAvatar类,并且一个用户有一个且只有一个化身,那么我们可以使用 Fluent API 表示这一点,如下所示:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasOne(u => u.UserAvatar)
.WithOne(a => a.User)
.HasForeignKey<UserAvatar>(u => u.UserForeignKey);
}
在前面的代码片段中,我们在用户和他们的化身之间有一对一的关系。这由粗体代码表示,即.HasOne(u => u.UserAvatar).WithOne(a => a.User)。这两方面都适用。
一对多关系
当我们有一对多关系时,我们可以说一个表中的一条记录与另一个表中的多条记录相关。
一对多关系的一个例子是用户和游戏会话之间的关系。假设我们有一个User类和一个GameSession类,一对多关系可以用 Fluent API 表示,如下所示:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasOne(u => u.GameSession)
.WithMany(g => g.User)
.HasForeignKey<GameSession>(u => u.UserForeignKey);
}
前面的代码片段仍然使用.HasOne()属性,但区别在于它链接到.HasMany()属性,使其成为一对多关系。
多对多关系
当我们有多对多关系时,我们可以说一个表中的一条记录与另一个表中指定关系中的多条记录相关,反之亦然。
学生与课程之间的关系就是一个例子。
使用 Entity Framework Core 3.0,您不能让 Fluent API 直接创建多对多关系,但您可以使用一种变通方法。为此,我们可以构造一个联接表并映射这两个类。让我们来看看如何做到这一点。
假设我们有一个名为Student的类和一个名为Course的类。通过这些类,我们可以创建一个StudentCourse表,如下所示:
public class Student
{
public long Id { get; set; }
public string Name { get; set; }
public StudentDetails StudentDetails { get; set; }
public ICollection<StudentSubject> StudentSubjects { get; set; }
// Added after constructed table
}
然后,我们可以有一个Course表和一个StudentCourse连接表,如下所示:
public class Course
{
public long Id { get; set; }
public string CourseName { get; set; }
public ICollection<StudentCourse> StudentCourses { get; set; }
// Added after constructed table
}
public class StudentCourse
{
public long StudentId { get; set; }
public Student Student { get; set; }
public long CourseId { get; set; }
public Course Course { get; set; }
}
现在,我们可以使用 Fluent API 来表示我们的多对多关系,如下所示:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasKey(s => new { s.StudentId, s.SubjectId });
modelBuilder.HasOne(ss => ss.Student)
.WithMany(s => s.StudentSubjects)
.HasForeignKey(ss => ss.StudentId);
modelBuilder.HasOne(ss => ss.Subject)
.WithMany(s => s.StudentSubjects)
.HasForeignKey(ss => ss.SubjectId);
}
在这里,我们做了一个变通方法,这样我们可以在学生和课程实体之间建立多对多关系。
有了这些关系,Entity Framework 将在执行迁移和更新数据库时创建必要的表,但数据库中的数据有什么好处呢?我们需要能够查询和利用它。在下一节中,我们将讨论如何处理查询。
处理查询
有几种方法可以检索保存在数据库中的数据,包括原始 SQL 语句,但到目前为止,最方便、最安全的选择是使用 LINQ。
在本节中,我们将介绍两个使用 LINQ 查询数据库的最典型示例。
查询一个项目
如果我们必须获得游戏会话,我们将使用以下代码:
using (var context = new GameDbContext())
{
var gameSession = context.GameSessions
.SingleOrDefault
(g => g.GameSessionId == Guid.Parse("002e6431-3eb5-
4d98-b3d9-3263490ce7c0"));
}
查询所有项目
如果我们必须返回到目前为止玩过的所有游戏会话,我们将使用以下代码:
using (var context = new GameDbContext())
{
var gameSessions = context.GameSessions.ToList();
}
查询筛选的项目
假设我们有以下代码:
using System.Linq;
using (var db = new GameDbContext())
{
var users = db.Users
.Where(u => u.GamesPlayed > 5)
.OrderBy(u => u.FirstName)
.ToList();
}
在前面的代码中,我们试图假设返回所有使用 LINQ 玩了超过5个游戏的用户。您还可以使用 LINQ 中可用的任何其他方法和属性,包括GroupBy、OrderByDescending等。LINQ 本身就是一个非常强大的库,建议您熟悉它。如果您完全是 LINQ 的初学者,它可能会帮助您完成这里的基础知识:https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/linq/basic-linq-query-operations 。
You will find that a tool called LINQPad, especially version 6, which can be found at https://www.linqpad.net/LINQPad6.aspx, is a great resource for working with LINQ in .NET Core 3.
在更复杂的场景中,可能会有多个查询为了一个目的一起运行,在这些情况下,如果组中的一个查询失败,则可能需要回滚所有其他查询。这是交易开始发挥作用的时候。我们将在下一节中查看事务。
使用事务
一个好的数据库实现将具有原子性、一致性、隔离性和耐久性(ACID属性。
事务对于维护数据库中的数据完整性至关重要。它们有助于确保逻辑分组在一起的数据被视为操作单元中的一项。这对确保酸的性质得到保留有很大的帮助。
当触发一个操作单元并将其保存到数据库时,所有组成逻辑分组的操作都会成功保存。这就是所谓的交易。如果部分事务失败,则所有事务都将回滚。事务在全有或全无的场景中运行。
Microsoft SQL Server 数据库是许多实际支持事务的数据库之一。这意味着,如果我们使用 Entity Framework Core 调用SaveChanges()方法,那么每个更改都会被视为事务的一部分,因此所有更改都会被保存,或者在出现错误时不会保存任何更改。
您不一定需要以自己的自定义方式实现事务,尤其是在可能需要开发的最基本的应用中。
对于大多数应用,此默认行为已足够。只有当应用需求认为有必要时,才应该手动控制事务。但如果需要,您可以将流程放入事务中,如下所示:
using (var gameContext = new GameDbContext())
{
using (var gameTransaction =
gameContext.Database.BeginTransaction())
{
try
{
gameContext.GameInvitation.Add(new GameInvitation { ... });
gameContext.SaveChanges();
gameContext.GameSession.Add(new GameSession { ... });
gameContext.SaveChanges();
gameTransaction.Commit(); // Both the above
operations will be in this transaction
}
catch (Exception Ex)
{
Console.WriteLine(Ex.Message)
}
}
}
在前面的代码片段中,我们实例化了一个新的游戏数据库上下文,并通过添加游戏邀请和游戏会话的两个进程在其上启动了一个事务。这些都是一次性完成的,如果其中任何一个单独失败,那么它们都不会被保存——要么全有,要么什么都没有。
为了本书的目的,这是结束我们对 EntityFrameworkCore3.0 的讨论的一个很好的地方,本书主要关注 ASP.NETCore3。
总结
在本章中,我们学习了如何将 Entity Framework Core 3 与 ASP.NET Core 3 结合使用,以便使用 SQL Server 数据库。
我们已经了解了如何使用数据库上下文和连接字符串连接到 SQLServer 数据库。然后,我们使用 Entity Framework Core 3 数据注释,并在数据库上下文中重写OnModelCreating方法,用主键和外键定义更新了 Tic Tac Toe 应用中的模型。
我们使用 Entity Framework Core 3 迁移,以使代码中的模型与其相应的数据库表示保持一致。
此外,我们还学习了如何以简单、高效的方式插入、更新和查询数据。我们还学习了如何使用 Fluent API 查询数据库以及如何使用事务。
在下一章中,我们将讨论如何使用 ASP.NET Core 3 的集成授权功能保护对 ASP.NET Core 3 应用的访问。
十、保护 ASP.NET Core 3 应用的安全
在数字犯罪和互联网欺诈日益增多的今天,所有现代网络应用都需要实施强大的安全机制,以防止攻击和用户身份盗用。
到目前为止,我们主要集中在了解如何构建高效的 ASP.NET Core 3 web 应用,而根本不考虑用户身份验证、授权或任何数据保护,但由于 Tic Tac Toe 应用越来越复杂,在最终向公众部署之前,我们必须解决安全问题。
构建一个 web 应用而不考虑安全性将是一个巨大的失败,甚至可能会毁掉最伟大和最著名的网站。在安全漏洞和个人数据被盗的情况下,负面声誉和用户信心的影响可能是巨大的,没有人愿意再与这些应用和更麻烦的公司合作。
这是一个需要认真对待的话题。您应该与安全公司合作执行代码验证和入侵测试,以确保您遵守最佳实践和高安全标准(例如,OWASP 前 10 名可在此处找到:https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project )。
幸运的是,ASP.NET Core 3 包含了帮助您解决这个复杂但重要的主题所需的一切。大多数内置功能甚至不需要高级编程或安全技能。您将看到,使用 ASP.NET Core 3 Identity framework 很容易理解和实现安全应用。
本章将学习的主要技能包括如何验证应用的用户身份,以及如何授权用户在应用中执行不同的任务。您将学习如何使用不同类型的身份验证,包括如何实现双因素身份验证。
我们自然会从实现身份验证开始,然后实现授权。在身份验证中,我们将首先查看基本形式的身份验证,然后再查看添加外部身份验证、使用双因素身份验证,最后添加忘记密码的机制和重置机制,然后再整体处理授权。
在本章中,我们将介绍以下主题:
- 实施身份验证:
- 添加基本用户表单身份验证
- 添加外部提供程序身份验证
- 添加忘记密码和密码重置机制
- 使用双因素身份验证
- 实施授权
实现身份验证
身份验证允许应用识别特定用户。它不用于管理用户访问权限(授权角色),也不用于保护数据(数据保护角色)。
有几种用于验证应用用户的方法,例如:
- 基本用户表单身份验证,使用带有登录和密码框的登录表单
- 单点登录(SSO)身份验证,用户在其公司上下文中对其所有应用只进行一次身份验证
- 社交网络外部提供商身份验证(如 Facebook 和 LinkedIn)
- 证书或公钥基础设施(PKI认证
ASP.NET Core 3 支持所有这些方法,但在本章中,我们将重点介绍使用用户登录名和密码的表单身份验证,以及通过 Facebook 的外部提供商身份验证。
在以下示例中,您将看到如何使用这些方法对应用用户进行身份验证,以及一些更高级的功能,如电子邮件确认和密码重置机制。
最后,您将看到如何使用内置的 ASP.NET Core 3 身份验证功能为最关键的应用实现双因素身份验证。
让我们为 Tic-Tac-Toe 应用准备不同身份验证机制的实现:
- 更新
Startup类中UserService、GameInvitationService和GameSessionService的生存期:
services.AddTransient<IUserService, UserService>();
services.AddScoped<IGameInvitationService,
GameInvitationService>();
services.AddScoped<IGameSessionService, GameSessionService>
();
- 更新
Startup类中的Configure方法,在静态文件中间件之后直接调用认证中间件:
app.UseStaticFiles();
app.UseAuthentication();
- 更新
UserModel使用内置 ASP.NET Core 身份认证功能,删除IdentityUser类已经提供的Id和Email属性:
public class UserModel : IdentityUser<Guid>
{
[Display(Name = "FirstName")]
[Required(ErrorMessage = "FirstNameRequired")]
public string FirstName { get; set; }
[Display(Name = "LastName")]
[Required(ErrorMessage = "LastNameRequired")]
public string LastName { get; set; }
[Display(Name = "Password")]
[Required(ErrorMessage = "PasswordRequired"),
DataType(DataType.Password)]
public string Password { get; set; }
[NotMapped]
public bool IsEmailConfirmed{ get {
return EmailConfirmed; } }
public System.DateTime? EmailConfirmationDate { get; set;
}
public int Score { get; set; }
}
Note that in the real world, we would advise also removing the Password property. However, we will keep it in the example for clarity and learning purposes.
- 添加一个名为
Managers的新文件夹,在名为ApplicationUserManager的文件夹中添加一个新的管理器,然后添加以下构造函数:
public class ApplicationUserManager : UserManager<UserModel>
{
private IUserStore<UserModel> _store;
DbContextOptions<GameDbContext> _dbContextOptions;
public ApplicationUserManager(DbContextOptions<GameDbContext> dbContextOptions,
IUserStore<UserModel> store, IOptions<IdentityOptions> optionsAccessor, IPasswordHasher<UserModel> passwordHasher, IEnumerable<IUserValidator<UserModel>> userValidators,IEnumerable<IPasswordValidator<UserModel>> passwordValidators, ILookupNormalizer Normalizer,IdentityErrorDescriber errors, IServiceProvider services,
ILogger<UserManager<UserModel>> logger) :
base(store, optionsAccessor, passwordHasher, userValidators,
passwordValidators, keyNormalizer, errors, services, logger)
{
_store = store;
_dbContextOptions = dbContextOptions;
}
...
}
-
让我们看一下创建一个功能全面的
ApplicationUserManager类的步骤:- 增加
FindByEmailAsync方法如下:
- 增加
public override async Task<UserModel> FindByEmailAsync
(string email)
{
using (var dbContext = new GameDbContext
(_dbContextOptions))
{
return await dbContext.Set<UserModel>
().FirstOrDefaultAsync(
x => x.Email == email);
}
}
public override async Task<UserModel> FindByIdAsync(string
userId)
{
using (var dbContext = new GameDbContext
(_dbContextOptions))
{
Guid id = Guid.Parse(userId);
return await dbContext.Set<UserModel>
().FirstOrDefaultAsync(
x => x.Id == id);
}
}
public override async Task<IdentityResult> UpdateAsync
(UserModel user)
{
using (var dbContext = new GameDbContext(_dbContextOptions))
{
var current = await dbContext.Set<UserModel>
().FirstOrDefaultAsync(x => x.Id == user.Id);
current.AccessFailedCount = user.AccessFailedCount;
current.ConcurrencyStamp = user.ConcurrencyStamp;
current.Email = user.Email;
current.EmailConfirmationDate = user.EmailConfirmationDate;
current.EmailConfirmed = user.EmailConfirmed;
current.FirstName = user.FirstName;
current.LastName = user.LastName;
current.LockoutEnabled = user.LockoutEnabled;
current.NormalizedEmail = user.NormalizedEmail;
current.NormalizedUserName = user.NormalizedUserName;
current.PhoneNumber = user.PhoneNumber;
current.PhoneNumberConfirmed = user.PhoneNumberConfirmed;
current.Score = user.Score;
current.SecurityStamp = user.SecurityStamp;
current.TwoFactorEnabled = user.TwoFactorEnabled;
current.UserName = user.UserName;
await dbContext.SaveChangesAsync();
return IdentityResult.Success;
}
}
public override async Task<IdentityResult> ConfirmEmailAsync(UserModel user, string token)
{
var isValid = await base.VerifyUserTokenAsync(user,
Options.Tokens.EmailConfirmationTokenProvider,
ConfirmEmailToken
Purpose, token);
if (isValid)
{
using (var dbContext = new GameDbContext
(_dbContextOptions))
{
var current = await dbContext.UserModels.
FindAsync(user.Id);
current.EmailConfirmationDate = DateTime.Now;
current.EmailConfirmed = true;
await dbContext.SaveChangesAsync();
return IdentityResult.Success;
}
}
return IdentityResult.Failed();
}
}
- 更新
Startup类,注册ApplicationUserManager类:
services.AddTransient<ApplicationUserManager>();
- 更新
UserService以使用ApplicationUserManager类,构造函数如下:
public class UserService : IUserService
{
private ILogger<UserService> _logger;
private ApplicationUserManager _userManager;
public UserService(ApplicationUserManager userManager,
ILogger<UserService> logger)
{
_userManager = userManager;
_logger = logger;
var emailTokenProvider = new EmailTokenProvider<UserModel>();
_userManager.RegisterTokenProvider("Default",
emailTokenProvider);
}
...
}
-
为了利用
ApplicationUserManager类,注册认证中间件,然后准备数据库,我们做了如下添加:- 添加两个新方法,第一个称为
GetEmailConfirmationCode,如下所示:
- 添加两个新方法,第一个称为
public async Task<string> GetEmailConfirmationCode
(UserModel user)
{
return await _userManager.
GenerateEmailConfirmationTokenAsync(user);
}
public async Task<bool> ConfirmEmail(string email, string code)
{
var start = DateTime.Now;
_logger.LogTrace($"Confirm email for user {email}");
var stopwatch = new Stopwatch(); stopwatch.Start();
try
{
var user = await _userManager.FindByEmailAsync(email);
if (user == null) return false;
var result = await _userManager.ConfirmEmailAsync(user,
code);
return result.Succeeded;
}
catch (Exception ex)
{
_logger.LogError($"Cannot confirm email for user
{email} - {ex}");
return false;
}
finally
{
stopwatch.Stop();
_logger.LogTrace($"Confirm email for user finished in
{stopwatch.Elapsed}");
}
}
public async Task<bool> RegisterUser(UserModel userModel)
{
var start = DateTime.Now;
_logger.LogTrace($"Start register user {userModel.Email} -
{start}");
var stopwatch = new Stopwatch(); stopwatch.Start();
try
{
userModel.UserName = userModel.Email;
var result = await _userManager.CreateAsync
(userModel,userModel.Password);
return result == IdentityResult.Success;
}
catch (Exception ex)
{
_logger.LogError($"Cannot register user
{userModel.Email} -
{ex}");
return false;
}
finally
{
stopwatch.Stop();
_logger.LogTrace($"Start register user {userModel.Email}
finished at {DateTime.Now} - elapsed
{stopwatch.Elapsed.
TotalSeconds} second(s)");
}
}
public async Task<UserModel> GetUserByEmail(string
email)
{
return await _userManager.FindByEmailAsync(email);
}
public async Task<bool> IsUserExisting(string email)
{
return (await _userManager.FindByEmailAsync(email)) !=
null;
}
public async Task<IEnumerable<UserModel>> GetTopUsers(
int numberOfUsers)
{
return await _userManager.Users.OrderByDescending( x =>
x.Score).ToListAsync();
}
public async Task UpdateUser(UserModel userModel)
{
await _userManager.UpdateAsync(userModel);
}
Note that you should also update the UserServiceTest class to work with the new constructor. For that, you will also have to create a mock for the UserManager class and pass it to the constructor. For the moment, you can just disable the unit test by commenting it out and updating it later. But don't forget to do it!
- 更新
UserRegistrationController中的EmailConfirmation方法,并使用您之前添加的GetEmailConfirmationCode方法检索邮件代码:
var urlAction = new UrlActionContext
{
Action = "ConfirmEmail",
Controller = "UserRegistration",
Values = new { email, code =
await _userService.GetEmailConfirmationCode(user) },
Protocol = Request.Scheme,
Host = Request.Host.ToString()
};
- 更新
UserRegistrationController中的ConfirmEmail方法;必须调用UserService中的ConfirmEmail方法完成邮件确认:
[HttpGet]
public async Task<IActionResult> ConfirmEmail(string email,
string code)
{
var confirmed = await _userService.ConfirmEmail(email,
code);
if (!confirmed)
return BadRequest();
return RedirectToAction("Index", "Home");
}
- 在
Models文件夹中添加一个名为RoleModel的新类,并使其从IdentityRole<long>继承,因为内置 ASP.NET Core 身份验证功能将使用该类:
public class RoleModel : IdentityRole<Guid>
{
public RoleModel()
{
}
public RoleModel(string roleName) : base(roleName)
{
}
}
- 更新
GameDbContext,并为榜样添加新的DbSet:
public DbSet<RoleModel> RoleModels { get; set; }
- 在
Startup类中注册认证服务和身份服务,然后使用之前添加的新角色模型:
services.AddIdentity<UserModel, RoleModel>(options =>
{
options.Password.RequiredLength = 1;
options.Password.RequiredUniqueChars = 0;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.SignIn.RequireConfirmedEmail = false;
}).AddEntityFrameworkStores<GameDbContext>
().AddDefaultTokenProviders();
services.AddAuthentication(options => {
options.DefaultScheme = CookieAuthenticationDefaults.
AuthenticationScheme;
options.DefaultSignInScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
}).AddCookie();
- 更新通信中间件,从类中删除
_userService私有成员,并相应更新构造函数:
public CommunicationMiddleware(RequestDelegate next)
{
_next = next;
}
- 更新通信中间件中的两个
ProcessEmailConfirmation方法,因为它们必须是异步的,才能使用 ASP.NET Core 标识。停止使用私人定义的private readonly IUserService _userService;用户服务,在以下两种方法中,优先使用本地定义的用户服务:
private async Task ProcessEmailConfirmation(HttpContext
context,
WebSocket currentSocket, CancellationToken ct, string
email)
{
var userService = context.RequestServices.
GetRequiredService<IUserService>();
...
}
private async Task ProcessEmailConfirmation(HttpContext
context)
{
var userService = context.RequestServices.
GetRequiredService<IUserService>();
...
}
- 更新
GameInvitationService,将公共构造函数设置为static。 - 从
Startup类中删除以下DbContextOptions注册;这将在下一步中被另一个替换:
var dbContextOptionsbuilder =
new DbContextOptionsBuilder<GameDbContext>()
.UseSqlServer(connectionString);
services.AddSingleton(dbContextOptionsbuilder.Options);
- 更新
Startup类,并添加新的DbContextOptions注册:
var connectionString = Configuration.
GetConnectionString("DefaultConnection");
services.AddScoped(typeof(DbContextOptions<GameDbContext>),
(serviceProvider) =>
{
return new DbContextOptionsBuilder<GameDbContext>()
.UseSqlServer(connectionString).Options;
});
- 更新
Startup类中的Configure方法,然后替换方法末尾执行数据库迁移的代码:
var provider = app.ApplicationServices;
var scopeFactory = provider.
GetRequiredService<IServiceScopeFactory>();
using (var scope = scopeFactory.CreateScope())
using (var context = scope.ServiceProvider.
GetRequiredService<GameDbContext>())
{
context.Database.Migrate();
}
- 更新
GameInvitationController中的Index方法:
...
var invitation =
gameInvitationService.Add(gameInvitationModel).Result;
return RedirectToAction("GameInvitationConfirmation",
new { id = invitation.Id });
...
- 更新
GameInvitationController中的ConfirmGameInvitation方法,并在现有用户注册中添加额外字段:
await _userService.RegisterUser(new UserModel
{
Email = gameInvitation.EmailTo,
EmailConfirmationDate = DateTime.Now,
EmailConfirmed = true,
FirstName = "",
LastName = "",
Password = "Qwerty123!",
UserName = gameInvitation.EmailTo
});
Note that the automatic creation and registration of the invited user is only a temporary workaround that we have added to simplify the example application. In the real world, you will need to handle this case differently and replace the temporary workaround with a real solution.
- 更新
GameSessionService中的CreateGameSession方法,传入invitedBy和invitedPlayer用户模型,而不是像以前那样在内部定义它们:
public async Task<GameSessionModel> CreateGameSession(
Guid invitationId, UserModel invitedBy, UserModel
invitedPlayer)
{
var session = new GameSessionModel
{
User1 = invitedBy,
User2 = invitedPlayer,
Id = invitationId,
ActiveUser = invitedBy
};
_sessions.Add(session);
return session;
}
更新GameSessionService中的AddTurn方法,通过传入用户而不是像以前那样通过电子邮件获取用户,然后重新提取GameSessionService界面:
public async Task<GameSessionModel> AddTurn(Guid id, UserModel
user, int x, int y)
{
...
turns.Add(new TurnModel
{
User = user,
X = x,
Y = y,
IconNumber = user.Email == gameSession.User1?
.Email ? "1" : "2"
});
gameSession.Turns = turns;
gameSession.TurnNumber = gameSession.TurnNumber + 1;
if (gameSession.User1?.Email == user.Email)
gameSession.ActiveUser = gameSession.User2;
...
}
- 更新
GameSessionController中的Index方法:
public async Task<IActionResult> Index(Guid id)
{
var session = await _gameSessionService.GetGameSession(id);
var userService = HttpContext.RequestServices.
GetService<IUserService>();
if (session == null)
{
var gameInvitationService = quest.HttpContext.RequestServices.
GetService<IGameInvitationService>();
var invitation = await gameInvitationService.Get(id);
var invitedPlayer = await userService.GetUserByEmail
(invitation.EmailTo);
var invitedBy = await userService.GetUserByEmail
(invitation.InvitedBy);
session = await _gameSessionService.CreateGameSession(
invitation.Id, invitedBy, invitedPlayer);
}
return View(session);
}
- 更新
GameSessionController中的SetPosition方法,并通过turn.User而不是turn.User.Email(确保IGameSessionService具有以下定义:Task<GameSessionModel> AddTurn(Guid id, UserModel user, int x, int y);:
gameSession = await _gameSessionService.AddTurn(gameSession.Id,
turn.User, turn.X, turn.Y);
- 更新
GameDbContext中的OnModelCreating方法,增加WinnerId外键:
...
modelBuilder.Entity(typeof(GameSessionModel))
.HasOne(typeof(UserModel), "Winner")
.WithMany()
.HasForeignKey("WinnerId")
.OnDelete(DeleteBehavior.Restrict);
...
- 更新
GameInvitationController中的GameInvitationConfirmation方法,使其异步。要使用 ASP.NET Core 标识,控制器操作必须是异步的:
[HttpGet]
public async Task<IActionResult>
GameInvitationConfirmation(
Guid id, [FromServices]IGameInvitationService
gameInvitationService)
{
return await Task.Run(() =>
{
var gameInvitation = gameInvitationService.Get(id).
Result;
return View(gameInvitation);
});
}
- 更新
HomeController中的Index和SetCulture方法,使其异步,以便使用 ASP.NET Core 标识:
public async Task<IActionResult> Index()
{
return await Task.Run(() =>
{
var culture = Request.HttpContext.Session.
GetString("culture");
ViewBag.Language = culture; return View();
});
}
public async Task<IActionResult> SetCulture(string culture)
{
return await Task.Run(() =>
{
Request.HttpContext.Session.SetString("culture",
culture);
return RedirectToAction("Index");
});
}
- 更新
UserRegistrationController中的Index方法,使其与 ASP.NET Core 标识异步工作:
public async Task<IActionResult> Index()
{
return await Task.Run(() =>
{
return View();
});
}
- 打开 Package Manager 控制台,执行
Add-Migration IdentityDb命令。 - 通过在 Package Manager 控制台中执行
Update-Database命令来更新数据库。 - 启动应用并注册一个新用户,然后验证一切是否仍按预期工作。
Note that you have to use a complex password, such as Azerty123!, to be able to finish the user registration successfully now, since you have implemented the integrated features of ASP.NET Core Identity in this section, which requires complex passwords.
到目前为止做得很好,因为我们的应用现在已经可以使用 ASP.NET Core 标识,并且在完成了前面部分中的所有准备工作之后,一般来说,它现在可以处理不同类型的身份验证。现在,我们可以开始学习如何添加不同类型的身份验证,我们将在下一节中开始学习基本的用户表单身份验证。
添加基本用户表单身份验证
伟大的您已经注册了身份验证中间件并准备好了数据库。在下一步中,您将为 Tic-Tac-Toe 应用实现基本的用户身份验证。
下面的示例演示如何修改用户注册并添加一个简单的登录表单,其中包含用于验证用户的用户登录和密码文本框:
- 将名为
LoginModel的新模型添加到Models文件夹:
public class LoginModel
{
[Required]
public string UserName { get; set; }
[Required]
public string Password { get; set; }
public string ReturnUrl { get; set; }
}
- 将名为
Account的新文件夹添加到Views文件夹中,然后在此新文件夹中添加名为Login.cshtml的新文件。它将包含登录视图:
@model TicTacToe.Models.LoginModel
<div class="container">
<div id="loginbox" style="margin-top:50px;"
class="mainbox
col-md-6 col-md-offset-3 col-sm-8 col-sm-offset-2">
<div class="panel panel-info">
<div class="panel-heading">
<div class="panel-title">Sign In</div>
</div>
<div style="padding-top:30px" class="panel-body">
<div style="display:none" id="login-alert"
class="alert alert-danger col-sm-12"></div>
<form id="loginform" class="form-horizontal"
role="form" asp-action="Login" asp-
controller="Account">
<input type="hidden" asp-for="ReturnUrl" />
<div asp-validation-summary="ModelOnly"
class="text-danger"></div>
<div style="margin-bottom: 25px" class="input-
group">
<span class="input-group-addon"><i
class="glyphicon
glyphicon-user"></i></span>
<input type="text" class="form-control"
asp-for="UserName" value=""
placeholder="username
or email">
</div>
<div style="margin-bottom: 25px" class="input-
group">
<span class="input-group-addon"><i
class="glyphicon
glyphicon-lock"></i></span>
<input type="password" class="form-control"
asp-for="Password" placeholder="password">
</div>
<div style="margin-top:10px" class="form-group">
<div class="col-sm-12 controls">
<button type="submit" id="btn-login" href="#"
class="btn btn-success">Login</button>
</div>
</div>
<div class="form-group">
<div class="col-md-12 control">
<div style="border-top: 1px solid#888;
padding-top:15px; font-size:85%">
Don't have an account?
<a asp-action="Index"
asp-controller="UserRegistration">Sign Up
Here
</a>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
- 更新
UserService,增加SignInManager私有字段,然后更新构造函数:
...
private SignInManager<UserModel> _signInManager;
public UserService(ApplicationUserManager userManager,
ILogger<UserService> logger, SignInManager<UserModel>
signInManager)
{
...
_signInManager = signInManager;
...
}
...
- 将名为
SignInUser的新方法添加到UserService:
public async Task<SignInResult> SignInUser( LoginModel loginModel, HttpContext httpContext)
{
_logger.LogTrace($"signin user {loginModel.UserName}");
var stopwatch = new Stopwatch(); stopwatch.Start();
try
{
var user = await _userManager.FindByNameAsync
(loginModel.UserName);
var isValid = await _signInManager.CheckPasswordSignInAsync
(user, loginModel.Password, true);
if (!isValid.Succeeded) return SignInResult.Failed;
if (!await _userManager.IsEmailConfirmedAsync(user))
return SignInResult.NotAllowed;
var identity = new ClaimsIdentity
(CookieAuthenticationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(ClaimTypes.Name,
loginModel.UserName));
identity.AddClaim(new Claim(ClaimTypes.GivenName,
user.FirstName));
identity.AddClaim(new Claim(ClaimTypes.Surname,
user.LastName));
identity.AddClaim(new Claim("displayName", $"
{user.FirstName} {user.LastName}"));
if (!string.IsNullOrEmpty(user.PhoneNumber))
identity.AddClaim(new Claim(ClaimTypes.HomePhone,
user.PhoneNumber));
identity.AddClaim(new Claim("Score", user.Score.
ToString()));
await httpContext.SignInAsync(CookieAuthenticationDefaults.
AuthenticationScheme,
new ClaimsPrincipal(identity), new AuthenticationProperties {
IsPersistent = false });
return isValid;
}
catch (Exception ex)
{
_logger.LogError($"cannot sign in user{ loginModel.UserName} - {
ex} ");
throw ex;
}
finally
{
stopwatch.Stop();
_logger.LogTrace($"sign in user {loginModel.UserName} finished
in
{ stopwatch.Elapsed} ");
}
}
在UserService中增加另一种方式SignOutUser并更新用户服务界面:
public async Task SignOutUser(HttpContext httpContext)
{
await _signInManager.SignOutAsync();
await httpContext.SignOutAsync(new
AuthenticationProperties {
IsPersistent = false });
return;
}
- 将名为
AccountController的新控制器添加到Controllers文件夹:
public class AccountController : Controller
{
private IUserService _userService;
public AccountController(IUserService userService)
{
_userService = userService;
}
}
让我们执行以下步骤:
public async Task<IActionResult> Login(string returnUrl)
{
return await Task.Run(() =>
{
var loginModel = new LoginModel { ReturnUrl =
returnUrl };
return View(loginModel);
});
}
...
[HttpPost]
public async Task<IActionResult> Login(LoginModel loginModel)
{
if (ModelState.IsValid)
{
var result = await _userService.SignInUser(loginModel,
HttpContext);
if (result.Succeeded)
{
if (!string.IsNullOrEmpty(loginModel.ReturnUrl))
return Redirect(loginModel.ReturnUrl);
else return RedirectToAction("Index", "Home");
}
else ModelState.AddModelError("", result.IsLockedOut ?"User
is locked" : "User is not allowed");
}
return View();
}
public IActionResult Logout()
{
_userService.SignOutUser(HttpContext).Wait();
HttpContext.Session.Clear();
return RedirectToAction("Index", "Home");
}
- 更新
Views/Shared/_Menu.cshtml文件,替换方法顶部已有的代码块:
@using Microsoft.AspNetCore.Http;
@{
var email = User?.Identity?.Name ??
Context.Session.GetString("email");
var displayName = User.Claims.FirstOrDefault(
x => x.Type == "displayName")?.Value ??
Context.Session.GetString("displayName");
}
- 更新
Views/Shared/_Menu.cshtml文件,以显示已认证用户的显示名称元素或已认证用户的登录元素;为此,请更换最后的<li>元素:
<li>
@if (!string.IsNullOrEmpty(email))
{
Html.RenderPartial("_Account",
new TicTacToe.Models.AccountModel { Email = email,
DisplayName = displayName });
}
else
{
<a asp-area="" asp-controller="Account"
asp-action="Login">Login</a>
}
</li>
- 更新
Views/Shared/_Account.cshtml文件,并替换注销和查看详细信息链接:
<a class="btn btn-danger btn-block" asp-controller="Account"
asp-action="Logout" asp-area="">Log Off</a>
<a class="btn btn-default btn-block" asp-action="Index"
asp-controller="Home" asp-area="Account">View Details</a>
- 进入
Views\Shared\Components\GameSession文件夹,更新default.cshtml文件,通过如下表格改善视觉表现:
...
<table>
@for (int rows = 0; rows < 3; rows++)
{
<tr style="height:150px;">
@for (int columns = 0; columns < 3; columns++)
{
<td style="width:150px; border:1px solid #808080;text-
align:center; vertical-align:middle"
id="@($"c_{rows}_{columns}")">
@{
var position = Model.Turns?.FirstOrDefault(turn =>
turn.X == columns && turn.Y == rows);
if (position != null)
{
if (position.User == Model.User1)
<i class="glyphicon glyphicon-unchecked"></i>
else
<i class="glyphicon glyphicon-remove-circle"></i>
}
else
{
<a class="btn btn-default btn-SetPosition"style=
"width:150px; min-height:150px;"
data-X="@columns" data-Y="@rows"> </a>
}
}
</td>
}
</tr>
}
</table>
...
- 启动应用,单击顶部菜单中的 Login 元素,并以现有用户身份登录(如果以前未注册用户,则注册为用户):

- 单击注销按钮。您应该注销并重定向回主页:

这基本上构成了我们的表单身份验证,我们可以使用登录表单登录和注销用户。在下一节中,我们将研究如何将外部提供者添加到应用中作为身份验证的手段。
添加外部提供程序身份验证
在下一节中,我们将通过使用 Facebook 作为身份验证提供商来展示外部提供商身份验证。
以下是本例中控制流的概述:
- 用户单击专用的外部提供者登录按钮。
- 相应的控制器接收指示需要哪个提供者的请求,然后向外部提供者发起质询。
- 外部提供者发送一个 HTTP 回调(
POST或GET),其中包含提供者名称、密钥和应用的一些用户声明。 - 声明与内部应用用户匹配。
- 如果没有内部用户可以与声明匹配,则该用户将被重定向到特定的注册表或被拒绝。
Note that the implementation steps are the same for all external providers if they support OWIN and ASP.NET Core Identity, and that you may even create your own providers and integrate them in the same way.
我们现在将通过 Facebook 实施外部提供商身份验证:
- 更新登录表单,并在标准登录按钮后直接添加一个名为“使用 Facebook 登录”的按钮:
<a id="btn-fblogin" asp-action="ExternalLogin"
asp-controller="Account" asp-route-Provider="Facebook"
class="btn btn-primary">Login with Facebook</a>
- 更新
UserService类和用户服务界面,增加GetExternalAuthenticationProperties和GetExternalLoginInfoAsync两个新方法:
public async Task<AuthenticationProperties>
GetExternalAuthenticationProperties(string provider,
string redirectUrl)
{
return await Task.FromResult(
_signInManager.ConfigureExternalAuthentication
Properties(
provider, redirectUrl));
}
public async Task<ExternalLoginInfo>
GetExternalLoginInfoAsync()
{
return await _signInManager.GetExternalLoginInfoAsync();
}
添加另一个名为ExternalLoginSignInAsync的新方法:
public async Task<SignInResult> ExternalLoginSignInAsync(
string loginProvider, string providerKey, bool
isPersistent)
{
_logger.LogInformation($"Sign in user with external login
{loginProvider} - {providerKey}");
return await _signInManager.ExternalLoginSignInAsync(
loginProvider, providerKey, isPersistent);
}
- 更新
AccountController,增加ExternalLogin方法:
[AllowAnonymous]
public async Task<ActionResult> ExternalLogin(string provider, string ReturnUrl)
{
var redirectUrl = Url.Action(nameof(ExternalLoginCallBack),
"Account", new { ReturnUrl = ReturnUrl }, Request.Scheme,
Request.Host.ToString());
var properties = await _userService.
GetExternalAuthenticationProperties(provider, redirectUrl);
ViewBag.ReturnUrl = redirectUrl;
return Challenge(properties, provider);
}
在同一AccountController类中,添加另一个名为ExternalLoginCallBack的方法:
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallBack(string returnUrl, string remoteError = null)
{
if (remoteError != null)
{
ModelState.AddModelError(string.Empty, $"Error from external
provider: {remoteError}");
ViewBag.ReturnUrl = returnUrl;
return View("Login");
}
var info = await _userService.GetExternalLoginInfoAsync();
if (info == null)
return RedirectToAction("Login", new { ReturnUrl = returnUrl });
var result = await _userService.ExternalLoginSignInAsync(
info.LoginProvider, info.ProviderKey, isPersistent: false);
if (result.Succeeded)
{
if (!string.IsNullOrEmpty(returnUrl)) return
Redirect(returnUrl);
else return RedirectToAction("Index", "Home");
}
if (result.IsLockedOut) return View("Lockout");
else return View("NotFound");
}
}
- 在
Startup类中注册 Facebook 中间件:
services.AddAuthentication(options => {
options.DefaultScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
}).AddCookie().AddFacebook(facebook =>
{
facebook.AppId = "123";
facebook.AppSecret = "123";
facebook.ClientId = "123";
facebook.ClientSecret = "123";
});
Note that you must update the Facebook middleware configuration and register your application with the Facebook developer portal before being able perform authenticated logins with a Facebook account.
请转到https://developer.facebook.com 了解更多信息。
- 启动应用,单击“使用 Facebook 登录”按钮,使用您的 Facebook 凭据登录,并验证一切是否正常工作:

祝贺你达到了这一步,通过与以前类似的步骤,你将能够使用其他外部提供商,如谷歌,或者微软,进行身份验证。现在,让我们看看如何在下一节中实现双因素身份验证。
使用双因素身份验证
您以前见过的标准安全机制只需要简单的用户名和密码,这使得网络罪犯越来越容易通过破解密码或拦截用户凭据(电子邮件、网络嗅探等)来访问机密数据,如个人和财务详细信息。然后,这些数据可用于实施金融欺诈和身份盗窃。
双因素身份验证增加了一层额外的安全性,因为它不仅需要用户名和密码,还需要只有用户才能提供的双因素代码(物理设备、生成的软件等)。这使得潜在入侵者更难获得访问权限,从而有助于防止身份和数据被盗。
所有主要网站都提供双因素身份验证作为一个选项,所以让我们将其添加到 Tic-Tac-Toe 应用中。
双因素身份验证-逐步
以下步骤将使您的应用具有完整的双因素身份验证:
- 将名为
TwoFactorCodeModel的新模型添加到Models文件夹:
public class TwoFactorCodeModel
{
[Key]
public long Id { get; set; }
public Guid UserId { get; set; }
[ForeignKey("UserId")]
public UserModel User { get; set; }
public string TokenProvider { get; set; }
public string TokenCode { get; set; }
}
- 将名为
TwoFactorEmailModel的新模型添加到Models文件夹:
public class TwoFactorEmailModel
{
public string DisplayName { get; set; }
public string Email { get; set; }
public string ActionUrl { get; set; }
}
- 在
GameDbContext内注册TwoFactorCodeModel,增加相应的DbSet:
public DbSet<TwoFactorCodeModel> TwoFactorCodeModels { get;
set; }
- 打开 NuGet Package Manager 控制台,执行
Add-Migration AddTwoFactorCode命令。然后,通过执行Update-Database命令更新数据库。 - 更新
ApplicationUserManager,然后添加一个名为SetTwoFactorEnabledAsync的新方法:
public override async Task<IdentityResult> SetTwoFactorEnabledAsync(UserModel user, bool enabled)
{
try
{
using (var db = new GameDbContext(_dbContextOptions))
{
var current = await db.UserModels.FindAsync(user.Id);
current.TwoFactorEnabled = enabled; await
db.SaveChangesAsync();
return IdentityResult.Success;
}
}
catch (Exception ex)
{ return IdentityResult.Failed(new IdentityError {Description = ex.ToString() });}
}
然后,我们执行以下步骤:
public override async Task<string>GenerateTwoFactorTokenAsync
(UserModel user, string tokenProvider)
{
using (var dbContext = new GameDbContext(_dbContextOptions))
{
var emailTokenProvider = new EmailTokenProvider
<UserModel>();
var token = await emailTokenProvider.GenerateAsync
("TwoFactor", this, user);
dbContext.TwoFactorCodeModels.Add(new TwoFactorCodeModel
{ TokenCode = token,TokenProvider = tokenProvider,UserId =
user.Id });
if (dbContext.ChangeTracker.HasChanges())
await dbContext.SaveChangesAsync();
return token;
}
}
public override async Task<bool>
VerifyTwoFactorTokenAsync(UserModel user, string
tokenProvider, string token)
{
using (var dbContext = new
GameDbContext(_dbContextOptions))
{
return await dbContext.TwoFactorCodeModels.AnyAsync(
x => x.TokenProvider == tokenProvider &&
x.TokenCode == token && x.UserId == user.Id);
}
}
- 进入
Areas/Account/Views/Home文件夹,更新索引视图:
@inject UserManager<TicTacToe.Models.UserModel> UserManager
@{ var isTwoFactor =UserManager.GetTwoFactorEnabledAsync(Model).Result; ... }
<h3>Account Details</h3>
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-6 col-md-6">
<div class="well well-sm">
<div class="row">
...
<i class="glyphicon glyphicon-check"></i><text>Two
Factor Authentication </text>
@if (Model.TwoFactorEnabled)<a asp-
action="DisableTwoFactor">Disable</a>
else <a asp-action="EnableTwoFactor">Enable</a>
</div>
...
- 将名为
_ViewImports.cshtml的新文件添加到Areas/Account/Views文件夹:
@using TicTacToe
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
@addTagHelper *, TicTacToe
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
- 更新
UserService类和用户服务界面,然后添加一个名为EnableTwoFactor的新方法:
public async Task<IdentityResult> EnableTwoFactor(string name, bool enabled)
{
try
{
var user = await _userManager.FindByEmailAsync(name);
user.TwoFactorEnabled = true;
await _userManager.SetTwoFactorEnabledAsync(user, enabled);
return IdentityResult.Success;
}
catch (Exception ex)
{
throw;
}
}
增加另一种方法GetTwoFactorCode:
public async Task<string> GetTwoFactorCode(string userName, string tokenProvider)
{
var user = await GetUserByEmail(userName);
return await _userManager.GenerateTwoFactorTokenAsync(user,tokenProvider);
}
- 更新
UserService中支持双因素认证的SignInUser方法,如果启用:
public async Task<SignInResult> SignInUser(LoginModel
loginModel, HttpContext httpContext)
{
...
if (await _userManager.GetTwoFactorEnabledAsync(user))
return SignInResult.TwoFactorRequired;
...
}
...
}
- 进入
Areas/Account/Controllers文件夹,更新HomeController。更新Index方法,增加EnableTwoFactor和DisableTwoFactor两种新方法:
[Authorize]
public async Task<IActionResult> Index()
{ var user = await _userService.GetUserByEmail(User.Identity.Name);
return View(user); }
[Authorize]
public IActionResult EnableTwoFactor()
{
_userService.EnableTwoFactor(User.Identity.Name, true);
return RedirectToAction("Index");
}
[Authorize]
public IActionResult DisableTwoFactor()
{
_userService.EnableTwoFactor(User.Identity.Name, false);
return RedirectToAction("Index");
}
Note that we will explain the [Authorize] decorator/attribute later in this chapter. It is used to add access restrictions to resources.
- 将名为
ValidateTwoFactorModel的新模型添加到Models文件夹:
public class ValidateTwoFactorModel
{
public string UserName { get; set; }
public string Code { get; set; }
}
- 更新
AccountController,新增SendEmailTwoFactor方法:
private async Task SendEmailTwoFactor(string UserName)
{
var user = await _userService.GetUserByEmail(UserName);
var urlAction = new UrlActionContext
{ Action = "ValidateTwoFactor", Controller = "Account",Values =
new { email = UserName,
code = await _userService.GetTwoFactorCode(user.UserName,
"Email") },
Protocol = Request.Scheme, Host = Request.Host.ToString() };
var TwoFactorEmailModel = new TwoFactorEmailModel
{ DisplayName = $"{user.FirstName} {user.LastName}", Email =
UserName, ActionUrl = Url.Action(urlAction) };
var emailRenderService = HttpContext.RequestServices.
GetService<IEmailTemplateRenderService>();
var emailService = HttpContext.RequestServices.
GetService<IEmailService>();
var message = await emailRenderService.RenderTemplate(
"EmailTemplates/TwoFactorEmail", TwoFactorEmailModel,
Request.Host.ToString());
try{ emailService.SendEmail(UserName, "Tic-Tac-Toe Two Factor
Code", message).Wait(); }
catch { }
}
Note that in order to call RequestServices.GetService<T>();, you must also add using Microsoft.Extensions.DependencyInjection; as you have done previously in other examples.
- 更新
AccountController中的Login方法:
[HttpPost]
public async Task<IActionResult> Login(LoginModel loginModel)
{
if (ModelState.IsValid)
{
var result = await _userService.SignInUser(loginModel,
HttpContext);
if (result.Succeeded)
{
if (!string.IsNullOrEmpty(loginModel.ReturnUrl)) return
Redirect(loginModel.ReturnUrl);
else return RedirectToAction("Index", "Home");
}
else if (result.RequiresTwoFactor) await SendEmailTwoFactor
(loginModel.UserName);
return RedirectToAction("ValidateTwoFactor");
else
ModelState.AddModelError("", result.IsLockedOut ? "User is
locked" : "User is not allowed");
}
return View();
}
- 将名为
ValidateTwoFactor的新视图添加到Views/Account文件夹:
@model TicTacToe.Models.ValidateTwoFactorModel
@{ ViewData["Title"] = "Validate Two Factor";Layout = "~/Views/Shared/_Layout.cshtml"; }
<div class="container">
<div id="loginbox" style="margin-top:50px;" class="mainbox
col-md-6 col-md-offset-3 col-sm-8 col-sm-offset-2">
<div class="panel panel-info">
<div class="panel-heading">
<div class="panel-title">Validate Two Factor Code</div>
</div>
<div style="padding-top:30px" class="panel-body">
<div class="text-center">
<form asp-controller="Account"asp-
action="ValidateTwoFactor" method="post">
<div asp-validation-summary="All"></div>
<div style="margin-bottom: 25px" class="input-group">
<span class="input-group-addon"><i class="glyphicon
glyphicon-envelope
color-blue"></i></span>
<input id="email" asp-for="UserName"
placeholder="email address"
class="form-control" type="email">
</div>
<div style="margin-bottom: 25px" class="input-group">
<span class="input-group-addon"><i class="glyphicon
glyphicon-lock
color-blue"></i></span>
<input id="Code" asp-for="Code"
placeholder="Enter your code" class="form-
control"> </div>
<div style="margin-bottom: 25px" class="input-group">
<input name="submit"class="btn btn-lg btn-primary
btn-block"
value="Validate your code" type="submit">
</div>
</form>
</div>
</div>
</div>
</div>
</div>
- 将名为
TwoFactorEmail的新视图添加到Views/EmailTemplates文件夹:
@model TicTacToe.Models.TwoFactorEmailModel
@{
ViewData["Title"] = "View";
Layout = "_LayoutEmail";
}
<h1>Welcome @Model.DisplayName</h1>
You have requested a two factor code, please click <a
href="@Model.ActionUrl">here</a> to continue.
- 更新
UserService类和用户服务界面,然后添加一个名为ValidateTwoFactor的新方法:
public async Task<bool> ValidateTwoFactor(string userName,
string tokenProvider, string token, HttpContext
httpContext)
{
var user = await GetUserByEmail(userName);
if (await _userManager.VerifyTwoFactorTokenAsync
(user,tokenProvider, token))
{
...
}
return false;
}
- 在
ValidateTwoFactor方法中,添加以下通过身份声明执行验证的实际代码:
if (await _userManager.VerifyTwoFactorTokenAsync(user,
tokenProvider, token))
{
var identity = new ClaimsIdentity
(CookieAuthenticationDefaults.Authentication
Scheme);
identity.AddClaim(new Claim(ClaimTypes.Name,
user.UserName));
identity.AddClaim(new Claim(ClaimTypes.GivenName,
user.FirstName));
identity.AddClaim(new Claim(ClaimTypes.Surname,
user.LastName));
identity.AddClaim(new Claim("displayName", $"
{user.FirstName} {user.LastName}"));
if (!string.IsNullOrEmpty(user.PhoneNumber))
identity.AddClaim(new Claim
(ClaimTypes.HomePhone, user.PhoneNumber));
identity.AddClaim(new Claim("Score",
user.Score.ToString()));
await httpContext.SignInAsync
(CookieAuthenticationDefaults.
AuthenticationScheme,
new ClaimsPrincipal(identity), new
AuthenticationProperties
{ IsPersistent = false });
return true;
}
- 更新
AccountController,增加两种新的双因素认证验证方法:
public async Task<IActionResult> ValidateTwoFactor(string email, string code)
{
return await Task.Run(() =>
{ return View(new ValidateTwoFactorModel { Code = code, UserName =
email }); });
}
[HttpPost]
public async Task<IActionResult> ValidateTwoFactor(ValidateTwoFactorModel validateTwoFactorModel)
{
if (ModelState.IsValid)
{
await _userService.ValidateTwoFactor(validateTwoFactorModel
.UserName, "Email",
validateTwoFactorModel.Code, HttpContext);
return RedirectToAction("Index", "Home");
}
return View();
}
- 启动应用,以现有用户身份登录,然后转到帐户详细信息页面。启用双因素身份验证(在此步骤之前,您可能需要重新创建数据库并注册新用户):

- 以用户身份注销,转到登录页面,然后再次登录。这一次,您将被要求输入双因素身份验证代码:

- 您将收到一封带有双因素身份验证代码的电子邮件:

- 点击电子邮件中的链接,所有内容都会自动填写。登录并验证一切正常工作:

您可以看到实现双因素身份验证是多么容易,正如我们在上一节中所做的那样。经过不同形式的身份验证后,您可能会忘记密码,因此我们需要能够安全地重置密码,以便在重新验证凭据后允许我们返回应用。我们将在下一节介绍这一点。
添加忘记密码和密码重置机制
现在您已经了解了如何向应用添加身份验证,您必须考虑如何帮助用户重置忘记的密码。用户总是会忘记他们的密码,所以你需要有一些机制。
处理此类请求的标准方法是向用户发送电子邮件重置链接。然后,用户可以更新其密码,而无需通过电子邮件以明文形式发送密码的风险。直接向用户电子邮件发送用户密码是不安全的,应该不惜一切代价避免。
现在,您将看到如何向 Tic Tac Toe 应用添加重置密码功能:
- 更新登录表单,并在此处注册链接后直接添加名为
Reset Password Here的新链接:
<div class="col-md-12 control">
<div style="border-top: 1px solid#888; padding-top:15px;
font-size:85%">
Don't have an account?
<a asp-action="Index"
asp-controller="UserRegistration">Sign Up Here</a>
</div>
<div style="font-size: 85%;">
Forgot your password?
<a asp-action="ForgotPassword">Reset Password Here</a>
</div>
</div>
- 将名为
ResetPasswordEmailModel的新模型添加到Models文件夹:
public class ResetPasswordEmailModel
{
public string DisplayName { get; set; }
public string Email { get; set; }
public string ActionUrl { get; set; }
}
- 更新
AccountController,然后添加一个名为ForgotPassword的新方法:
[HttpGet]
public async Task<IActionResult> ForgotPassword()
{
return await Task.Run(() =>
{
return View();
});
}
- 将名为
ResetPasswordModel的新模型添加到Models文件夹:
public class ResetPasswordModel
{
public string Token { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public string ConfirmPassword { get; set; }
}
- 将名为
ForgotPassword的新视图添加到Views/Account文件夹:
@model TicTacToe.Models.ResetPasswordModel
@{
ViewData["Title"] = "GameInvitationConfirmation";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div class="form-gap"></div>
<div class="container">
<div class="row">
<div class="col-md-4 col-md-offset-4">
<div class="panel panel-default">
<div class="panel-body">
<div class="text-center">
<h3><i class="fa fa-lock fa-4x"></i></h3>
<h2 class="text-center">Forgot Password?</h2>
<p>You can reset your password here.</p>
<div class="panel-body">
<form id="register-form" role="form"
autocomplete="off" class="form"
method="post" asp-controller="Account"
asp-action="SendResetPassword">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon"><i
class="glyphicon glyphicon-envelope
color-blue"></i></span>
<input id="email" name="UserName"
placeholder="email address"
class="form-control" type="email">
</div>
</div>
<div class="form-group">
<input name="recover-submit"
class="btn btn-lg btn-primary btn-block"
value="Reset Password" type="submit">
</div>
<input type="hidden" class="hide"
name="token" id="token" value="">
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
- 更新
UserService类和用户服务界面,然后添加一个名为GetResetPasswordCode的新方法:
public async Task<string> GetResetPasswordCode(UserModel
user)
{
return await _userManager.
GeneratePasswordResetTokenAsync(user);
}
- 在名为
ResetPasswordEmail的View/EmailTemplates文件夹中添加新视图:
@model TicTacToe.Models.ResetPasswordEmailModel
@{
ViewData["Title"] = "View";
Layout = "_LayoutEmail";
}
<h1>Welcome @Model.DisplayName</h1>
You have requested a password reset, please click <a
href="@Model.ActionUrl">here</a> to continue.
- 更新
AccountController,然后添加一个名为SendResetPassword的新方法:
[HttpPost]
public async Task<IActionResult> SendResetPassword(string UserName)
{
var user = await _userService.GetUserByEmail(UserName);
var urlAction = new UrlActionContext
{
Action = "ResetPassword", Controller = "Account",
Values = new { email = UserName, code = await
_userService.GetResetPasswordCode(user) },
Protocol = Request.Scheme, Host = Request.Host.ToString()
};
var resetPasswordEmailModel = new ResetPasswordEmailModel
{
DisplayName = $"{user.FirstName} {user.LastName}", Email =
UserName, ActionUrl = Url.Action(urlAction)
};
var emailRenderService = HttpContext.RequestServices.
GetService<IEmailTemplateRenderService>();
var emailService = HttpContext.RequestServices.
GetService<IEmailService>();
var message = await emailRenderService.RenderTemplate(
"EmailTemplates/ResetPasswordEmail",
resetPasswordEmailModel,Request.Host.ToString());
try
{ emailService.SendEmail(UserName,"Tic-Tac-Toe Reset Password",
message).Wait(); }
catch { }
return View("ConfirmResetPasswordRequest",
resetPasswordEmailModel);
}
- 将名为
ConfirmResetPasswordRequest的新视图添加到Views/Account文件夹:
@model TicTacToe.Models.ResetPasswordEmailModel
@{
ViewData["Title"] = "ConfirmResetPasswordRequest";
Layout = "~/Views/Shared/_Layout.cshtml";
}
@section Desktop{<h2>@Localizer["DesktopTitle"]</h2>}
@section Mobile {<h2>@Localizer["MobileTitle"]</h2>}
<h1>@Localizer["You have requested to reset your password,
an email has been sent to {0}, please click on the provided
link to continue.", Model.Email]</h1>
- 更新
AccountController,然后添加一个名为ResetPassword的新方法:
public async Task<IActionResult> ResetPassword(string email, string code)
{
var user = await _userService.GetUserByEmail(email);
ViewBag.Code = code;
return View(new ResetPasswordModel { Token = code, UserName = email });
}
- 在名为
SendResetPassword的Views/Account文件夹中添加新视图:
@model TicTacToe.Models.ResetPasswordEmailModel
@{
ViewData["Title"] = "SendResetPassword";
Layout = "~/Views/Shared/_Layout.cshtml";
}
@section Desktop{<h2>@Localizer["DesktopTitle"]</h2>}
@section Mobile {<h2>@Localizer["MobileTitle"]</h2>}
<h1>@Localizer["You have requested a password reset, an
email has been sent to {0}, please click on the link to
continue.", Model.Email]</h1>
- 将名为
ResetPassword的新视图添加到Views/Account文件夹:
@model TicTacToe.Models.ResetPasswordModel
@{
ViewData["Title"] = "ResetPassword";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div class="container">
<div id="loginbox" style="margin-top:50px;"
class="mainbox
col-md-6 col-md-offset-3 col-sm-8 col-sm-offset-2">
<div class="panel panel-info">
<div class="panel-heading">
<div class="panel-title">Reset your Password</div>
</div>
<div style="padding-top:30px" class="panel-body">
<div class="text-center">
<form asp-controller="Account"
asp-action="ResetPassword" method="post">
<input type="hidden" asp-for="Token" />
<div asp-validation-summary="All"></div>
<div style="margin-bottom: 25px" class="input-
group">
<span class="input-group-addon"><i
class="glyphicon glyphicon-envelope
color-blue"></i></span>
<input id="email" asp-for="UserName"
placeholder="email address"
class="form-control" type="email">
</div>
<div style="margin-bottom: 25px" class="input-
group">
<span class="input-group-addon"><i
class="glyphicon glyphicon-lock
color-blue"></i></span>
<input id="password" asp-for="Password"
placeholder="Password"
class="form-control" type="password">
</div>
<div style="margin-bottom: 25px" class="input-
group">
<span class="input-group-addon"><i
class="glyphicon glyphicon-lock
color-blue"></i></span>
<input id="confirmpassword"
asp-for="ConfirmPassword"
placeholder="Confirm your Password"
class="form-control" type="password">
</div>
<div style="margin-bottom: 25px" class="input-
group">
<input name="submit"
class="btn btn-lg btn-primary btn-block"
value="Reset Password" type="submit">
</div>
</form>
</div>
</div>
</div>
</div>
</div>
- 更新
UserService类和用户服务界面,然后添加一个名为ResetPassword的新方法:
public async Task<IdentityResult> ResetPassword(string userName, string password, string token)
{
_logger.LogTrace($"Reset user password {userName}");
try
{
var user = await _userManager.FindByNameAsync(userName);
var result = await _userManager.ResetPasswordAsync(user, token,
password);
return result;
}
catch (Exception ex)
{
_logger.LogError($"Cannot reset user password {userName} -
{ex}");
throw ex;
}
}
- 更新
AccountController,然后添加一个名为ResetPassword的新方法:
[HttpPost]
public async Task<IActionResult> ResetPassword(ResetPasswordModel reset)
{
if (ModelState.IsValid)
{
var result = await _userService.ResetPassword(reset.UserName,
reset.Password, reset.Token);
if (result.Succeeded)
return RedirectToAction("Login");
else
ModelState.AddModelError("", "Cannot reset your password");
}
return View();
}
- 启动应用并转到登录页面。到达后,单击重置密码此处链接:

- 在忘记的密码上输入现有用户电子邮件?页这将向用户发送一封电子邮件:

- 打开密码重置电子邮件并单击提供的链接:

- 在密码重置页面上,为用户输入新密码,然后单击重置密码。您应自动重定向到登录页面,因此请使用新密码登录:

您将非常高兴地了解到,我们现在已经完成了所有的身份验证过程,凭借所掌握的技能,您现在能够为您可能拥有的任何应用提供合理的身份验证。现在一个用户能够被认证,换句话说,我们知道我们的用户是谁,我们不会就此止步。
进入应用并不一定意味着你可以做应用提供的任何事情。我们现在需要知道用户是否有权执行此操作或该操作。这就是我们将在下一节中看到的。
实施授权
在本章的第一部分中,您了解了如何处理用户身份验证以及如何使用用户登录。在下一部分中,您将看到如何管理用户访问,这将允许您微调谁有权访问什么。
最简单的授权方法是使用[Authorize]元装饰器,它完全禁用匿名访问。在这种情况下,用户需要登录才能访问受限资源。
现在,让我们来看看如何在 Tic-Tac-Toe 应用中实现它:
- 在
HomeController中添加一个名为SecuredPage的新方法,并通过添加[Authorize]装饰符来删除对该方法的匿名访问:
[Authorize]
public async Task<IActionResult> SecuredPage()
{
return await Task.Run(() =>
{
ViewBag.SecureWord = "Secured Page";
return View("SecuredPage");
});
}
- 将名为
SecuredPage的新视图添加到Views/Home文件夹:
@{
ViewData["Title"] = "Secured Page";
}
@section Desktop {<h2>@Localizer["DesktopTitle"]</h2>}
@section Mobile {<h2>@Localizer["MobileTitle"]</h2>}
<div class="row">
<div class="col-lg-12">
<h2>Tic-Tac-Toe @ViewBag.SecureWord</h2>
</div>
</div>
- 尝试在未登录时手动输入其 URL
http://<host>/Home/SecuredPage来访问受保护的页面。您将自动重定向到登录页面:

- 输入有效的用户凭据并登录。您应该自动重定向到安全页面,现在可以查看该页面:

另一种比较流行的方法是使用基于角色的安全性,它提供了一些更高级的功能。这是保护 ASP.NET Core 3 web 应用安全的推荐方法之一。
以下示例说明了如何使用它:
- 在
Models文件夹中添加一个名为UserRoleModel的新类,并将其从IdentityUserRole<long>继承。这将由内置 ASP.NET Core 3 身份验证功能使用:
public class UserRoleModel : IdentityUserRole<Guid>
{
[Key]
public long Id { get; set; }
}
- 更新
GameDbContext内的OnModelCreating方法:
protected override void OnModelCreating(ModelBuilder
modelBuilder)
{
...
modelBuilder.Entity<IdentityUserRole<Guid>>()
.ToTable("UserRoleModel")
.HasKey(x => new { x.UserId, x.RoleId });
}
- 打开 NuGet Package Manager 控制台,执行
Add-Migration IdentityDb2命令。然后,执行Update-Database命令。 - 更新
UserService,修改构造函数,创建两个角色Player和Administrator,如果它们还不存在:
public UserService(RoleManager<RoleModel> roleManager,
ApplicationUserManager userManager, ILogger<UserService>
logger, SignInManager<UserModel> signInManager)
{
...
if (!roleManager.RoleExistsAsync("Player").Result)
roleManager.CreateAsync(new RoleModel {
Name = "Player" }).Wait();
if (!roleManager.RoleExistsAsync("Administrator").Result)
roleManager.CreateAsync(new RoleModel {
Name = "Administrator" }).Wait();
}
- 更新
UserService中的RegisterUser方法,然后在用户注册时将用户添加到Player角色或Administrator角色:
...
try
{
userModel.UserName = userModel.Email;
var result = await _userManager.CreateAsync
(userModel,userModel.Password);
if (result == IdentityResult.Success)
{
if(userModel.FirstName == "Jason")
await _userManager.AddToRoleAsync(userModel,"Administrator");
else
await _userManager.AddToRoleAsync(userModel, "Player");
}
return result == IdentityResult.Success;
}
...
Note that in the example, the code to identify whether a user has the administrator role is intentionally very basic. You should implement something more sophisticated in your applications.
- 启动应用并注册新用户,然后在 SQL Server Object Explorer 中打开
RoleModel表并分析其内容:

- 在 SQL Server 对象资源管理器中打开
UserRoleModel表并分析其内容:

- 更新
UserService中的SignInUser方法,将角色与声明进行映射:
...
identity.AddClaim(new Claim("Score",
user.Score.ToString()));
var roles = await _userManager.GetRolesAsync(user);
identity.AddClaims(roles?.Select(r => new
Claim(ClaimTypes.Role, r)));
await httpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(identity),
new AuthenticationProperties { IsPersistent = false });
...
- 更新
HomeController中的SecuredPage方法,使用管理员角色确保访问安全,然后将最初存在的Authorize装饰器替换为以下内容:
[Authorize(Roles = "Administrator")]
- 启动应用。如果您尝试在未登录的情况下访问
http://<host>/Home/SecuredPage,您将被重定向到登录页面。以具有播放机角色的用户身份登录,您将被重定向到拒绝访问页面(该页面不存在,因此出现 404 错误),因为该用户没有管理员角色:

- 注销,然后以具有管理员角色的用户身份登录。您现在应该看到安全页面,因为用户具有必要的角色:

在以下示例中,您将看到如何以注册用户身份自动登录,以及如何激活基于声明和基于策略的身份验证:
- 更新
SignInUser方法,然后在UserService中添加一个名为SignIn的新方法:
public async Task<SignInResult> SignInUser(LoginModel loginModel, HttpContext httpContext)
{
...
await SignIn(httpContext, user);
return isValid;
}
catch (Exception ex)
{
...
}
finally
{
...
}
}
执行SignIn方法如下:
private async Task SignIn(HttpContext httpContext, UserModel user)
{
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.
AuthenticationScheme);
identity.AddClaim(new Claim(ClaimTypes.Name, user.UserName));
identity.AddClaim(new Claim(ClaimTypes.GivenName,
user.FirstName));
identity.AddClaim(new Claim(ClaimTypes.Surname, user.LastName));
identity.AddClaim(new Claim("displayName", $"{user.FirstName}
{user.LastName}"));
if (!string.IsNullOrEmpty(user.PhoneNumber))
identity.AddClaim(new Claim(ClaimTypes.HomePhone,
user.PhoneNumber));
identity.AddClaim(new Claim("Score", user.Score.ToString()));
var roles = await _userManager.GetRolesAsync(user);
identity.AddClaims(roles?.Select(r => new Claim(ClaimTypes.
Role, r)));
if (user.FirstName == "Jason")
identity.AddClaim(new Claim("AccessLevel", "Administrator"));
await httpContext.SignInAsync(CookieAuthenticationDefaults.
AuthenticationScheme,
new ClaimsPrincipal(identity),new AuthenticationProperties {
IsPersistent = false });
}
Note that, in the example, the code to identify whether a user has administrator privileges is intentionally very basic. You should implement something more sophisticated in your applications.
- 更新
UserService中的RegisterUser方法,新增参数,用户注册后自动登录,然后重新提取用户服务界面:
public async Task<bool> RegisterUser(UserModel userModel,
bool isOnline = false)
{
...
if (result == IdentityResult.Success)
{
...
if (isOnline)
{
HttpContext httpContext =
new HttpContextAccessor().HttpContext;
await Signin(httpContext, userModel);
}
}
...
}
- 更新
UserRegistrationController中的Index方法,自动登录新注册用户:
...
await _userService.RegisterUser(userModel, true);
...
- 更新
GameInvitationController中的ConfirmGameInvitation方法,自动登录邀请用户:
...
await _userService.RegisterUser(new UserModel
{
Email = gameInvitation.EmailTo,
EmailConfirmationDate = DateTime.Now,
EmailConfirmed = true,
FirstName = "",
LastName = "",
Password = "Azerty123!",
UserName = gameInvitation.EmailTo
}, true);
...
- 在 MVC 中间件配置之后,向
Startup类添加一个名为AdministratorAccessLevelPolicy的新策略:
services.AddAuthorization(options =>
{
options.AddPolicy("AdministratorAccessLevelPolicy",
policy => policy.RequireClaim("AccessLevel",
"Administrator"));
});
- 更新
HomeController中的SecuredPage方法,使用Policy代替Role来确保访问安全,然后更换Authorize装饰器:
[Authorize(Policy = "AdministratorAccessLevelPolicy")]
Note that it can be required to limit access to only one specific middleware since several kinds of authentication middleware can be used with ASP.NET Core 3 (cookie, bearer, and more) at the same time.
对于这种情况,您以前看到的Authorize装饰器允许您定义哪些中间件可以对用户进行身份验证。
下面是一个允许 cookie 和承载令牌的示例:
[Authorize(AuthenticationSchemes = "Cookie,Bearer",
Policy = "AdministratorAccessLevelPolicy")]
- 启动应用,以
Administrator访问级别注册新用户,登录,然后访问http://<host>/Home/SecuredPage。一切都应该像以前一样工作。
Note that you might need to clear your cookies and log in again to create a new authentication token with the required claims.
- 尝试以没有所需访问级别的用户身份访问安全页面;如前所述,您应该被重定向到
http://<host>/Account/AccessDenied?ReturnUrl=%2FHome%2FSecuredPage:

- 注销,然后以具有
Administrator角色的用户身份登录。您现在应该看到安全页面,因为用户具有必要的角色。
总结
在本章中,您学习了如何保护 ASP.NET Core 3 应用的安全,包括管理应用用户的身份验证和授权。
您已经在示例应用中添加了基本形式的身份验证,以及通过 Facebook 进行的更高级的外部提供商身份验证。这将为您提供一些关于如何在自己的应用中处理这些重要主题的好主意。
此外,您还了解了如何添加标准重置密码机制,因为用户总是忘记密码,您需要尽可能安全地响应此类请求。
我们甚至讨论了双因素身份验证,它可以为关键应用提供更高的安全级别。
在本章末尾,您还了解了如何以多种方式(基本、角色、策略)处理授权,以便您可以决定哪种方法最适合您的特定用例。
一般来说,您已经掌握了对应用的用户进行身份验证以及授权他们在应用中执行指定功能的重要技能。
在下一章中,我们将讨论在开发 ASP.NET Core 3 web 应用时可能存在的其他不同漏洞。
十一、保护 ASP.NET 应用——漏洞
在最后一章中,我们主要从身份验证和授权的角度讨论了安全性。我们了解了如何确保我们知道谁在访问我们的应用,以及他们在应用中可以做什么。
不幸的是,未经验证的登录和未经授权的访问并不是我们需要防范的唯一方面。在您作为应用开发人员的任务中,您将负责处理具有不同安全重要性的不同应用。对于那些能够激励人们积极寻找利用应用的方法的应用,作为开发人员,您需要确保能够抵御潜在的黑客。
本章让您了解使用 ASP.NET Core 3 构建的 web 应用可能受到攻击的最常见方式。
对于您构建的每个应用,建议您从一开始就考虑其安全性,而不仅仅是在部署时考虑。
对于一些严肃的应用,如企业应用,甚至有一个威胁建模会话,在该会话中,您尝试并分析您将要构建的应用所面临的任何威胁,然后在整个开发阶段将这些威胁考虑在内。
在本章中,您将学习恶意用户通常用来利用 web 应用进行攻击的不同方法,并且,除了了解这些方法外,您还将学习确保应用免受任何潜在黑客攻击的基本方法。
本章将介绍以下主题:
- 跨站点脚本(XSS)
- 偷饼干
- 窃听、邮件篡改和邮件重播
- 打开重定向/XSR
- SQL 注入
- 跨站点请求伪造(XSRF/CSRF)
- JSON 劫持
- 超额投递
- 点击劫持
- 正确的错误报告和堆栈跟踪
跨站点脚本(XSS)
您经常会发现,跨站点脚本以其最简单的形式被称为XSS,可以将其描述为 HTML 注入攻击的一种形式。
如果没有适当的措施允许用户的浏览器拥有可以执行的脚本,网站将很容易受到 XSS 攻击。在这种情况下,大多数情况下,攻击者假定网站上的用户身份,并使用此类脚本劫持真实用户的会话。
一旦会话掌握在攻击者手中,您的应用将在会话期间任由他们摆布。他们可以做任何事情,包括让你的网页看起来像他们想要的任何方式,他们甚至可以通过你的网页对其他网站发起攻击。这可能发生在真实用户仍然能够做其他事情的时候,但是 XSS 攻击可以让黑客完全控制浏览器。
如果一个网站允许用户上传链接,那么它也容易受到 XSS 攻击,在这种攻击中,用户可以获取通过表单上传的数据,还可以提取网站的安全信息。
XSS 攻击也可能以黑客试图劫持 cookie 的形式出现。这些 cookie 可以具有登录标识和/或会话标识。一旦 cookie 被劫持,黑客就有可能获得有关用户的大部分信息。通过相同的 cookie 劫持,黑客可能会在用户执行正常功能提交恶意内容(如脚本)时攻击用户,而用户不会意识到此类活动。
防止 XSS
考虑一个常见的、真实的场景,其中用户被重定向到一个被黑客精心设计的页面,以与真实的应用完全相同。这种情况可以通过多种方式发生,包括通过电子邮件内容将您重定向到受损的 URL。允许 XSS 攻击盛行的一个关键因素是输入数据未得到充分验证。
为了确保您的输入被彻底验证,您必须确保任何类型的输入都有一个最大长度。必须确保输入字段的输入类型是有限的。
始终确保过滤掉标记中任何可能的 Unicode 字符,例如小于和大于符号:<和>。
Razor 中带有@属性的自动 HTML 编码是防止 XSS 攻击的有效方法,但您仍然需要采取额外的步骤来保护您的站点。
还有一个 JavaScript 编码器,您可以将其注入到视图中,如下所示:@inject``JavaScriptEncoder jEncoder;然后直接调用编码器以供使用,如@jEncoder.encode(...);中所示。
收到请求后,ASP.NET Core 3 作为一个框架始终保持警惕,以评估请求。它检查请求中的脚本和/或任何标记,如果遇到根据预定义参数感到可疑的内容,则抛出异常。
Microsoft SDL(简称安全开发生命周期)在整个应用开发生命周期中指导您解决安全问题。在这个链接中有更详细的解释:https://www.microsoft.com/en-us/securityengineering/sdl 并有几条建议供您参考。
Here's an example tip from the SDL: make sure to avoid using the eval() function in JavaScript, or indeed any similar functions that are meant to evaluate and subsequently execute a string input as a script, for example, eval('2019+ 5').
偷饼干
用户体验是任何 web 应用的一个非常重要的方面。Cookies可以在创建一个能够提供良好用户体验的网站方面发挥作用。有许多网站在用户登录后实际使用 cookies 来识别他们的用户。在这样的网站上,如果你拿出 cookies,你将不得不在导航到不同页面时一次又一次地登录。
如果一个黑客可以偷你的饼干,他们可以很容易地伪装成你。在这方面,您可能只想从浏览器中禁用 cookie 的使用,但同时,有许多应用迫使您启用 cookie。
Cookie 可用于存储浏览历史记录或网站首选项,这些信息并不都是敏感的,但它们也可以包含网站可用于在请求之间识别您的数据。
如果用于身份验证的 cookie 可能被窃取,那么也可以假定用户的身份,因此被劫持用户的所有功能都被授予访问权限。然而,要做到这一点,该网站还必须易受 XSS 攻击,这在前面已经描述过。黑客只有在能够向目标网站注入脚本的情况下才能窃取 cookie。
防止偷饼干
您可以使用HttpOnly属性标记 cookie。这将确保只有服务器才能访问具有此标记的 cookie。这意味着 cookie 不会被来自客户端的任何脚本访问。
HttpOnly tagged cookies make it harder for a bulk of XSS attacks to succeed.
HttpOnly属性可以在web.config中设置,如下代码片段所示:
<httpCookies domain=”String” httpOnlyCookies=”true” requireSSL=”true”>
还可以为每个 cookie 单独设置属性,如下所示:
Response.Cookies[“CookieExample”].Value= "Value to be remembered";
Response.Cookies[“CookieExample].HttpOnly=true;
"CookieExample"字符串意味着包含您选择的名称,您可以将该名称分配给作为开发人员的 cookie。Value和HttpOnly都是命名 cookie 的属性或属性,您可以为其赋值,如前一示例所示。
窃听、邮件篡改和邮件重播
正如标题中所暗示的,窃听、消息篡改和消息重播的漏洞通常被解释为组。这是因为它们的行为方式非常相似,因此以相同的方式识别。它们也可以通过类似的方式预防。
黑客可能利用网络数据捕获工具记录客户对网站的请求和响应。这是窃听的一个例子。
如果你没有采取反窃听的措施,黑客可能会捕获 HTTP 请求,对其进行修改,然后再次提交到网站。这就是现在所谓的消息重播。这对于黑客来说是聪明的,因为网站将能够处理请求,就像在普通请求中一样,而不会引起任何怀疑。这是因为,对于需要身份验证的网站,它通常具有所需的安全令牌。
当我们谈到消息篡改时,我们的意思是 HTTP 请求可能被恶意修改,包括执行事务和修改甚至删除数据。
防止窃听和消息重播
在使用 HTTP 的 web 应用中,防止消息重播的一种普遍接受的方法是要求通过安全套接字层(SSL进行通信。
通过在非匿名模式下使用 SSL,您可以保护应用不被指示将消息重播回应用服务器。如果这样做,还可以防止 HTTP 请求和响应内容暴露给任何作为窃听者进行侦听的人。关于 SSL 是否也能防止消息篡改,您的猜测和我的一样好。
It is recommended that web applications use SSL in non-anonymous mode.
当通过 SSL 连接到服务器时,SSL 基本上就是这样工作的。客户端通过验证服务器 URI 是否与 SSL 证书中的主机名相同来检查其连接到的服务器的标识是否正确。
有时,客户端可能没有可用于验证服务器的证书。有时,服务器也会使用不必具有服务器标识的 SSL 协议。在这两种情况下,SSL 都以匿名模式使用。
您可以选择在某些 web 服务器上配置匿名 SSL,但不能在其他 web 服务器上配置匿名 SSL。
It must be noted that when one party poses as another during a client-server connection, then anonymous SSL cannot protect your application from spoofing threats or message replay. However, anonymous SSL can protect you from eavesdropping and tampering.
打开重定向/XSR
开放重定向,顾名思义,本质上是将用户重定向到随机网站。这些也通常被称为跨站点重定向(XSR),它们通过 web 应用的 URL 发生。
一旦黑客成功重定向,他们就可以利用重定向进行一系列攻击,包括垃圾邮件和网络钓鱼。黑客还可以利用您的 web 应用向他人提供恶意软件。
XSR 威胁与通过查询字符串和/或 HTTP 请求形式的数据使用 URL 重定向的 web 应用更为相似。
打开重定向示例
下面是一个简单的、真实的开放重定向示例。您可以作为真正的真实用户登录网站,但如果黑客泄露了返回 URL,在您登录后更改字符串的部分内容将导致您被带出应用。作为用户,您可能不会注意到这一点,因为您被重定向到的站点可能会故意创建为与原始站点完全相同。
URL 中的折衷方法很难在较长的 URL 中发现,在这种情况下,仅仅更改一个字母就可以欺骗您,使您认为自己在同一个站点上。有意图的黑客几乎拥有真实站点的精确副本,当你在他们的受损站点上时,他们可能会出于虚构的原因要求你再次登录,然后你的用户名和密码就消失了!
防止打开重定向
为了安全起见,建议避免应用中的任何重定向。当你只需要重定向时,有一个助手方法UrlHelper.IsLocalUrl(),你可以使用它来确保你只被重定向到站点内。
With the UrlHelper.IsLocalUrl() method, you can make certain that a redirection goes to the same web server as the originating call, and never taken outside of your web application.
SQL 注入
关于SQL 注入,几乎任何应用都会使用对数据库存储的查询。显然,对于任何黑客来说,这都提供了一个机会,可以利用查询来完成他们不打算做的事情。他们可以通过修改查询来达到预期目的。
如果您连接字符串以生成 SQL 语句和/或以其他方式使用动态 SQL,则这会带来一个特别危险的环境,可通过 SQL 注入加以利用。
防止 SQL 注入
开发应用所使用的技术并不重要:它们都容易受到 SQL 注入的影响。因此,您需要采取措施确保您的应用免受此类攻击。建议:在构造动态 SQL 语句时,始终使用类型安全的参数编码。
在几乎所有的数据 API 中,都允许您精确地指定要传递的参数类型。这甚至包括 ADO.NET 作为一种技术,这种技术已经存在了一段时间。这些参数可以是整数、布尔值或其他基本类型。大多数数据 API 都提供编码或转义,以防止黑客攻击。
Before you deploy your application into production, do a security audit on both your code and the application in general. Make sure that your database is locked down, with only the minimally required permissions for your application.
保护 SQL 连接字符串
保护连接字符串始终是至关重要的。建议您在配置或应用设置中仅将其作为纯文本。将其作为纯文本存储在代码中的任何其他地方都会带来麻烦。通过微软中间语言(MSIL****反汇编程序,如果您将连接字符串放入代码中,实际上任何人都很容易看到它。黑客可以使用Ildasm.exe命令查看代码各自的 MSIL,通过 MSIL 可以暴露字符串。
**另一个需要考虑的事实是连接字符串的不同形式确实起作用。某些形式的连接字符串可以有用户名和密码;其他人只是使用可信连接或集成安全性。如果可以这样做,建议使用不明确指定用户名和密码的选项。
Desist from using a username and password for Windows authentication; rather, go for Trusted_Connection = true or Integrated Security = SSPI.
在连接字符串中使用 Persist Security Info 默认值
持久化安全信息的默认值为False。将其设置为True允许在打开连接后从连接中获取安全敏感信息,包括用户 ID 和密码。当设置为False时,安全信息将被处理(在它被用于打开连接之后),以确保任何不受信任的源都无法访问它。
使用对象关系映射器(ORMs)
第 9 章中介绍的对象关系映射器(ORMs的用户,如第 9 章中介绍的实体框架核心 3访问数据的用户,通常与对象打交道,大多数 ORMs 提供强大的,面向对象的查询功能,因此 SQL 注入不是常见的威胁。
Note that you could also use stored procedures along with Entity Framework Core. Usage of stored procedures further reduces the risk of SQL injection either when used alongside Entity Framework Core or on their own, mainly because of their parameterized features.
即使使用字符串查询,ORM 通常也会使使用参数比使用 ADO.NET 参数容易得多,因为大多数 ORM 都没有使用字符串连接的驱动程序。
但是,如果您碰巧使用 NHibernate,HQL(简称Hibernate 查询语言)与 SQL 非常相似,其行为方式与执行原始 SQL 语句类似。
If you are an NHibernate ORM user, desist from using HQL in your Data Access Layer (DAL) as it makes your application susceptible to SQL injection.
跨站点请求伪造(XSRF/CSRF)
在书籍和博客中都多次提到 SQL 注入和 XSS,但很少有人提到鲜为人知的跨站点请求伪造威胁,这同样具有毁灭性。简称为XSRF或CSRF。
简言之,当您以合法用户身份登录应用时,您的身份可能会被利用,用于向受损的 web 应用发送请求,该应用将使用您的身份执行请求。
Hackers can easily take advantage of XSRF/CSRF because of the concept of how the web itself is supposed to work in a stateless manner.
XSRF/CSRF 以混乱代理攻击的形式进行。这意味着一项行动可能会被其他实体毫无防备地愚弄,但滥用其合法权威会带来毁灭性的后果。
XSRF/CSRF 示例
让我们看一个简单控制器的示例,它可能容易受到 XSRF/CSRF 攻击。
乍一看,一切都是安全的,但在下文中,我们将看到一个拥有这样代码的控制器如何让 XSRF/CSRF 黑客垂涎三尺:
public class ContactController : Controller
{
public ViewResult ContactDetails()
{ return View(); }
public ViewResult Update()
{
Contact contact = DbContext.GetContact();
contact.ContactId = Request.Form["ContactId"];
contact.Name = Request.Form["Name"];
SaveContact(contact);
return View();
}
}
考虑一个黑客设置一个故意针对这种控制器的页面的场景。然后,黑客可以说服用户访问他们的页面,然后该页面将尝试发布到该控制器。当用户已经通过基于表单的身份验证或 Windows 身份验证进行身份验证时,此控制器将无法检测到预期的 XSRF/CSRF 攻击。请参阅以下代码:
<body onload="document.getElementById('contactForm').submit()">
<form id="contactForm" action="http://.../Contact/Update"
method="post">
<input name="ContactId" value="123456" />
<input name="Name" value="My Hack Example" />
</form>
</body>
如下一节所述,这类攻击以不同的方式减轻。
防止 XSRF/CSRF
以下是阻止 XSRF/CSRF 攻击的最常见方法。
域引用程序
建议检查并查看传入的 HTTP 请求头引用域是否确实是您的。当您这样做时,您可以防止来自您的域之外的潜在危害源的任何请求。
然而,这种预防方法并非万无一失。如果用户安装了 AdobeFlash,黑客实际上可能会利用并伪造标题。一些用户也可能出于隐私考虑,决定不发送推荐人标题。
用户生成的令牌
建议使用隐藏的 HTML 字段为特定用户保留令牌(通常从源服务器生成),然后验证提交的令牌是否有效。您可以使用用户的会话或 HTTP cookie 来保留生成的令牌,以供以后检索。
在 ASP.NET Core MVC(Model View Controller 的简称)中,您可以通过为特定用户创建在视图和控制器之间传递的令牌来验证请求。如果令牌不相同,则这可能是 XSRF/CSRF 攻击,您可以做出不允许请求继续的规定。如上所述,所有这些都可以通过使用 ASP.NET Core MVC 中提供的 HTML 帮助程序来实现,该帮助程序是@Html.AntiForgeryToken(),用于需要提交的表单中,该帮助程序位于视图部分。对于每个请求,该助手将添加一个名为RequestVerificationToken的隐藏字段,其中包含来自视图的令牌,该令牌需要控制器进行验证。
You can use the AntiForgeryToken functionality that is available with ASP.NET Core MVC to prevent XSRF/CSRF attacks.
这种方法要求相应的控制器同步工作,以确保它理解它正在接收的表单数据包含防伪令牌。这是通过使用ValidateAntiForgeryToken属性装饰控制器中的特定操作来完成的。如前所述,此属性确认 HTTP 请求同时包含 cookie 值和隐藏表单字段,并验证这些值是否相同。
It is advisable to decorate every form with an anti-forgery token. This includes login forms.
局限性
正如您所看到的,如前所述,反 XSRF 帮助程序非常有用,但它们有几个限制。如果用户不接受其浏览器上的 Cookie,则控制器操作将拒绝其请求,并饰以ValidateAntiForgeryToken。您还需要确保您的应用不会受到 XSS 威胁;否则,可以读取防伪令牌。您还必须注意,防伪令牌不适用于 HTTPGET请求,而仅适用于 HTTPPOST请求。
JSON 劫持
大多数现代 web 应用都使用 JSON 来传递数据。JSON 劫持顾名思义,就是黑客从另一个站点访问 JSON 响应。
黑客尝试阅读不适合他们的 JSON 响应的动机是,网站通常会包含用户信息,这些信息可以在 JSON 响应中识别个人。这是一个潜在恶意用户的金矿。
防止 JSON 劫持
防止任何人劫持您的 JSON 非常简单,主要是确保您从未将 API 设计为将 JSON 数组作为 HTTP 响应返回。
您还可以使用HttpPost属性来修饰各自控制器中的特定操作,以便它只响应使用 HTTPPOST操作的 HTTP 请求。
Make sure that JSON services always return responses as non-array JSON objects.
超额投递
ASP.NET Core MVC 有一个名为模型绑定的功能,这使您作为开发人员能够轻松地将用户输入自动映射到特定模型。
您可以看到黑客利用此功能向您的模型中插入用户未在相应表单中实际填写的内容的动机。
漏洞示例
让我们使用一个假设的博客帖子页面,用户可以在其中发表评论:
public class BlogComment
{
public int CommentID { get; set; } // Primary key
public int BlogPostID { get; set; } // Foreign key
public BlogPost BlogPost { get; set; } // Foreign entity
public string UserName { get; set; }
public string Comment { get; set; }
public bool IsApproved { get; set; }
}
然后,我们可以有一个需要对博客读者可见的表单:
Name: @Html.TextBox(“UserName”)
Comment: @Html.TextBox(“Comment”)
现在,在这种情况下,我们的期望是让博客用户只向UserName和Comment字段提供输入。有目的的黑客可以使用高级浏览器工具查找IsApproved布尔字段,并将其作为待发布数据的一部分或作为IsApproved=true添加到查询字符串中。
我们正在探索的特性,称为模型绑定,通常不知道哪些字段是合法填写的,并且会适当地强制将IsApproved设置为等于true。
对于这个例子来说,这很简单,但是你能想象如果模型是,例如,'student',黑客能够简单地设置student.Grade = 99吗?是的,这给了我们大多数人很大的希望,但你需要注意的是,这可能会产生严重的后果。想象一下一个银行账户,它有一个AvailableBalance字段,可以很容易地通过这种方式进行操作。在下一节中,我们将重点探讨进行反检查的重要性。
防止过度投递
我们可以使用BindProperty属性来修饰模型或特定的控制器动作。
在使用BindProperty属性时,您可以使用黑名单或白名单方法。事实证明,白名单方法更安全、更简单,因为您只需将需要绑定的属性作为目标。
作为另一种缓解方式,我们可以创建一个视图模型,其中只包含用户需要填写的属性,这样可以防止任何直接针对完整模型的绑定。
使用BlogCommentViewModel将能够防止过度张贴:
public class BlogCommentViewModel {
public string UserName { get; set; }
public string Comment { get; set; }
}
点击劫持
与点击劫持相关的同义词有几个,包括UI 补救攻击和UI 补救。在这些实例中,UI 意味着用户界面。
当黑客破坏应用的链接和按钮时,就会出现此漏洞,用户认为他们正在点击链接/按钮来执行功能A,而实际上他们正在执行功能B。这个被破坏的功能是由黑客决定的。
当黑客设法将脚本嵌入易受攻击的链接和/或按钮时,这种攻击主要发生在浏览器中。用户出于无辜的意图点击这些链接,但这样做,他们很容易泄露敏感信息,或者黑客完全控制了他们的计算机。
单击劫持示例
你可以有一个场景,其中有一个按钮旨在执行功能A,但黑客会放置一个 z 索引较低的复制按钮,当用户点击他们能够看到的正常按钮时,该按钮会被触发执行功能B。
防止点击劫持
我们可以使用一个名为x-frame-options的 HTTP 响应头来对抗 web 应用上的点击劫持。我们可以在以下两种实现中的任何一种实现中实现这一点。
- 当您的应用启动时,您可以将以下代码放入实现
IHttpModule的模块中。
当 web 应用启动时,将x-frame-options头设置为"DENY":
public void Init(HttpApplication application)
{
application.BeginRequest += (new
EventHandler(this.Application_BeginRequest));
}
private void Application_BeginRequest(object sender, EventArgs e)
{
HttpContext context = ((HttpApplication)source).Context;
context.Current.Response.AddHeader("x-frame-options", "DENY");
}
我们在这里做的主要事情是将x-frame-optionsHTTP 头设置为DENY,但如果这是您的托管选项,建议您在您的互联网信息服务(IIS)中实际执行此操作。
- 您可以进入 IIS 管理器,然后在选择网站的情况下执行以下操作:
- 从功能列表中选择或双击以编辑 HTTP 响应头。
- 然后单击添加。。。从右侧的“操作”部分。请参见以下屏幕截图:

前面提到的大多数潜在攻击都源于积极的黑客意图,设计各种方法和途径来发现漏洞,从而进入易受攻击的 web 应用。但是,作为一名使用 ASP.NET Core 3 的 web 应用开发人员,以下漏洞源于您应始终避免的一点粗心。
正确的错误报告和堆栈跟踪
当 ASP.NET Core 3 web 应用中出现未经处理的异常或错误时,可能会出现死亡屏幕,有时简称为SOD。此处显示了一个示例:

这有时被称为死亡的黄色屏幕,因为当你使用谷歌 Chrome 或 Firefox 作为浏览器时,与这些异常消息相关的黄色。
错误报告漏洞示例
简单地说,对于 web 应用开发人员来说,允许实际应用用户看到这些类型的错误有点粗心。死亡屏幕上包含的信息只属于应用开发人员,而不是应用的访问者。对于一个有决心的黑客来说,这些信息将给他们提供一个开始的地方,因为它提供了应用的内部信息,这可能会给你的应用如何实际工作的线索和洞察。
错误处理需要遵循正确的思维过程。这甚至适用于应用开发人员需要使用安全强制转换和适当类型转换(如TryParse)的看似微不足道的情况,因为这在防止可能导致死亡屏幕的特殊错误方面有很大的帮助。
防止死亡屏幕
有一个简单的解决方案可以防止应用生成死亡屏幕,那就是确保配置并指定自定义错误页面。幸运的是,ASP.NET Core 3 通过Startup类中的中间件为您提供了现成的管道。
It's not a nice experience on the part of the user to see a screen of death. It is better to think of your user and give them a friendly error page when something goes wrong.
您可以在Configure方法的Startup类中指定自定义错误页:
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
这决定了将根据环境向用户显示何种类型的错误页面,无论是开发环境还是其他类型的环境,包括生产环境。
总结
在本章中,我们介绍了使用 ASP.NET Core 3 开发软件应用时需要注意的常见漏洞。如果我们要有效地构建一个安全的解决方案,那么了解恶意攻击将从哪个角度出发是非常重要的。
我们研究了 XSS 攻击,恶意用户利用真实用户的身份向 HTML 中注入脚本。我们发现黑客获取用户身份的方法之一是通过 cookie 窃取,我们可以通过在 cookie 上加上HttpOnly属性来防止这种行为。
我们研究了使用网络小工具的窃听、消息篡改和消息重播,还研究了开放重定向/XSR 攻击,它将用户重定向到外部恶意网站。我们研究了 SQL 注入、XSRF/CSRF、JSON/JSON 劫持、过度发布和点击劫持。我们还看到了正确的错误报告是多么重要。
在了解了可能的攻击并了解了如何确保我们为这些类型的攻击做好准备之后,我们现在可以将我们的应用投入生产,并向公众用户公开。现在是进入下一章的好时机,在下一章中,我们将部署和托管我们的安全 ASP.NET Core 3 应用。保持专注。**
十二、托管 ASP.NET Core 3 应用
就这样:本书快结束了,这意味着我们几乎完成了整个应用开发生命周期,因此,客户很快就能使用您的应用了!值得骄傲的是,在阅读并理解了本书倒数第二章之后,您将拥有强大的技能,能够创建和部署自己的具有强大技术基础的令人激动的应用!
让我们回顾一下:从本书开始到现在,您已经了解了如何设置开发环境,如何使用 ASP.NET Core 3 的各种功能开发现代 web 应用,如何通过 Entity Framework Core 将它们连接到数据库,最后,在前两章中,如何保护他们免受恶意网络罪犯的攻击。
现在,我们需要讨论开发周期的最后一步,这包括在应用生产就绪后托管和部署应用。
在我们讨论将应用部署到云中的具体细节之前,我们将首先了解应用的一般托管情况,这两个平台是最常见的:Amazon Web Services(AWS)和 Microsoft Azure;最后,我们将研究如何使用 Docker 容器来承载我们的应用。
本章的目标是解释托管应用时的不同选项,如何选择正确的选项,以及如何使用最新的技术和云提供商部署 ASP.NET Core 3 web 应用。
在本章中,我们将介绍以下主题:
- 托管应用
- 在 AWS 中部署应用
- 在 Microsoft Azure 中部署应用
- 将应用部署到 Docker 容器中
托管应用
您可以构建世界上最好、最有用的应用,但如果您的客户无法轻松地从任何设备访问这些应用,您可能无法获得预期的成功。如下图所示,应用越来越需要全渠道,这意味着客户需要能够在一台设备上启动,然后在另一台设备上继续:

您的应用需要可部署到多个目标,在某些情况下,还需要部署到多个操作系统,以实现高度的灵活性和设备可用性。这就是托管发挥作用的地方。
主机负责应用启动和生存期管理,包括提供和配置服务器以及请求处理。根据您托管 ASP.NET Core 3 应用的方式,您可以为应用支持不同的设备。所选技术对设备和操作系统的可能选择有重大影响。
ASP.NET Core 3 完全支持多种平台和操作系统上的所有当前托管机制。这完全取决于您特定的应用上下文。
ASP.NET Core 应用的一些托管示例如下:
- 通过互联网信息服务(IIS在 Windows 上托管)
- Windows 服务中的主机
- Linux 上的主机,使用 NGINX
- 主机在 Linux 上,使用 Apache
在开发期间,或者如果您不需要与其他人共享您的应用,那么使用自托管机制或 IIS Express 可能会很有趣,它为断开连接、概念验证(PoC)或测试项目提供了一个快速简便的解决方案。
但是,如果您开始与他人共享应用,则需要更复杂的托管解决方案和相应的服务器技术。
例如,要通过 internet 公开 ASP.NET Core 应用,您需要一个可在本地网络之外访问的 web 服务器。实现这一目标有几种可能的解决方案,以下是其中两种:
- 一种是使用 internet 主机提供商托管 web 服务器。但是,您需要大小,并且需要自己管理服务器,这可能既昂贵又耗时。
- 另一种选择是使用公共云提供商,它提供了更大的灵活性和可扩展性,同时允许降低成本,因为您只需支付所需费用。最著名的是 AWS 和 Microsoft Azure,它们在世界各地都有数据中心。
此外,当使用公共云平台作为(PaaS提供的服务时,您甚至不必再管理操作系统或平台。云平台为您提供一切。相反,您可以访问云服务,云服务通过高服务级别协议(SLA提供 web 服务器或数据库服务器功能。两个例子是 AWS Elastic Beanstalk 和 Microsoft Azure 应用服务。
在看到您可以使用的各种托管选项之后,您将能够决定部署目标。对于公开可用的 web 应用,您将希望部署到公共云提供商。下一节将向您展示如何部署到最常见和最著名的公共云提供商,以及如何使用最新的技术来实现这一点。
在 AWS 中部署应用
AWS 是 Amazon.com,Inc.的子公司,提供一个公共云计算平台,用于在全球可用的 AWS 数据中心内构建、测试、部署和管理应用和服务。它支持许多不同的编程语言、工具、框架和系统。
在本节中,我们将探讨 AWS,并将了解如何创建帐户并将 ASP.NET Core 3 应用部署到 AWS Elastic Beanstalk。
首先,你必须在 AWS 上注册一个帐户;这只需要五分钟,但你需要一张信用卡。
让我们按照以下步骤完成帐户注册步骤:
- 打开浏览器,进入https://aws.amazon.com ,点击创建免费账户按钮,如下图所示:

- 填写“创建新 AWS 帐户”表单,继续填写联系信息,然后单击“继续”,如以下屏幕截图所示:

- 填写付款信息,然后单击“继续”。填写身份信息验证表并单击“继续”,然后选择支持计划并单击“继续”,如以下屏幕截图所示:

- 注册完成后,系统会自动将您重定向到欢迎页面,您应在该页面中单击登录控制台按钮,如以下屏幕截图所示:

创建新的 AWS 用户帐户后,现在可以在 AWS 中部署第一个 ASP.NET Core 应用。
使用 AWS 时,在部署 ASP.NET Core web 应用方面,您基本上有以下两种选择:
- 弹性豆茎
- AWS EC2 集装箱服务
下一节将介绍如何在 AWS Elastic Beanstalk 中部署应用。所以,请继续关注,系好安全带,享受旅程!
在 AWS Elastic Beanstalk 中部署应用
AWS Elastic Beanstalk 是针对 AWS 中基于 web 的应用的 PaaS 产品,包括自动缩放功能。在这方面,它可以与 Microsoft Azure 应用服务相媲美,您将在本章后面的章节中看到。
AWS Elastic Beanstalk 不再需要管理基础设施,相反,您只需要关注应用的构建和托管。对于完整的 DevOps 方法,如果您想使用 AWS,建议您使用此 PaaS 服务。
For more information on AWS Elastic Beanstalk, check out https://aws.amazon.com/elasticbeanstalk/.
以下说明逐步说明如何在 AWS Elastic Beanstalk 中部署 Tic-Tac-Toe 应用。
让我们从创建 AWS Beanstalk 应用开始,如下所示:
- 登录 AWS,进入 AWS 管理控制台,在 AWS 服务文本框中输入
Beanstalk,点击显示的链接;您将被重定向到 Beanstalk 欢迎页面,如下所示:

- 在 Beanstalk 欢迎页面上,单击“开始”按钮,如以下屏幕截图所示:

这将带您进入以下屏幕截图中显示的页面。选择.NET(Windows/IIS)作为平台,然后单击“创建应用”按钮,如下所示:

Note that you can change the IIS version and network settings (Network Load Balancer or single instance) by clicking on the Configure more options link, shown in the preceding screenshot.
- 等待创建 Beanstalk 应用(取决于您的 internet 连接和 AWS,这可能需要一段时间),如下所示:

在部署 Tic-Tac-Toe 应用并最终运行它之前,需要在接下来的步骤中准备好技术环境。
正如您在前面几章中所看到的,应用需要一个数据库来持久化用户和应用数据。为此,我们将在 AWS 中提供名为亚马逊关系数据库服务(亚马逊 RDS)的 SQL Server PaaS 服务,如下例所示:
- 返回 AWS 管理控制台,单击最近访问的服务部分中的 Elastic Beanstalk,如以下屏幕截图所示:

- 在 Beanstalk 所有应用页面上,选择所需的环境,然后单击默认环境,您将看到以下结果屏幕:

- 在特定 Beanstalk 应用页面上,单击左侧菜单中的配置,如以下屏幕截图所示:

- 向下滚动,然后单击修改数据库链接,如以下屏幕截图所示:

- 选择作为 DB Engine SQL Server Express(sqlserver ex)并输入主用户名和密码;将其余字段保留为默认值,单击页面底部的应用按钮,等待数据库创建完成,如以下屏幕截图所示:

Note that, depending on your application's needs, the SQL Server Express edition may not be enough since it is limited in size, meaning that the Enterprise or Web editions may be necessary, which will result in higher cloud-provider costs. For the Tic-Tac-Toe sample application, it is, however, largely sufficient.
- 转到 AWS 管理控制台,在 AWS 服务文本框中输入
RDS,然后单击显示的链接。您将被重定向到 Amazon RDS 页面;单击资源菜单中的 DB 实例,如以下屏幕截图所示:

- 单击实例,将显示实例仪表板。向下滚动以检索端点地址,该地址将用于在部署前更新应用连接字符串,如以下屏幕截图所示:

- 在右侧,在 Amazon RDS 实例页面的连接和安全选项卡中,单击 VPC 安全组链接,如前面的屏幕截图所示。
- 在“安全组”页面上,单击页面底部菜单中的“入站”选项卡,然后单击“编辑”,以便能够更新刚刚创建的数据库安全组的入站规则,如以下屏幕截图所示:

- 单击添加规则按钮,选择 MS SQL 作为类型,自定义作为源,并输入默认安全组,然后单击保存按钮,如以下屏幕截图所示:

Note that you should configure stricter security group inbound rules in a real production environment, and set real IP restrictions. The source Anywhere should not be used for production environments.
- 您可以选择使用SQL Server Management Studio(SSMS)、Visual Studio 2019 中的 SQL Server 对象资源管理器或 Azure Data Studio(如下注释所述)来处理云中的数据库。让我们在 Visual Studio 2019 中使用 SQL Server 对象资源管理器。打开它,右键单击 SQL Server,然后添加 SQL Server,并使用以前的端点地址、用户名和密码登录,如以下屏幕截图所示:

- 创建一个名为
TicTacToe的新数据库,如下所示:

- 更新
appsettings.json文件中的DatabaseConnectionString,并用以下相应值替换参数。您可能还记得,在步骤 7中,我们提到了将用于在部署之前更新应用连接字符串的检索到的端点。这是我们需要更新的地方:
"Server=<YourEndPoint>;Database=TicTacToe;
MultipleActiveResultSets=true;
User id=<YourUser>;pwd=<YourPassword>"
您已经成功地配置了技术环境,这意味着您现在可以发布数据库架构以及部署 web 应用。
Note that Azure Data Studio is another great cross-platform option for working with SQL Server in the cloud. This is useful when you are not in need of Visual Studio for running Entity Framework (EF) Migrations. A comparison of its features compared to the commonly used SSMS, to help you decide when it is best to use Azure Data Studio or SSMS can be found here: https://docs.microsoft.com/en-us/sql/azure-data-studio/what-is?view=sql-server-ver15.
您是否急切地等待在云中运行应用?只要保持专注,再继续一点,您很快就会看到您的应用在 AWS 中运行。
发布数据库架构时有三种选择,如下所示:
- 生成 SQL 脚本以通过 EF 迁移从 Visual Studio 2019 中创建数据库。
- 更改
Data\GameDbContextFactory.cs中的默认连接字符串,并在 Package Manager 控制台中执行Update-Database指令。 - 运行应用以创建数据库。
最合适的解决方案取决于应用及其数据库的类型和大小。根据经验,最好生成一个脚本,然后为较大的应用创建数据库,而对于较小的应用,可以在应用首次运行时自动创建数据库。
让应用在 AWS 上运行
让我们看看在 AWS 中运行 Tic Tac Toe 应用之前需要做什么,如下所示:
- 在 Visual Studio 2019 中打开 Package Manager 控制台,执行
Script-Migration指令,如下图:

- 将生成的脚本复制到 Amazon RDS 数据库的查询窗口中,然后执行脚本以创建数据库和各种数据库对象。
- 从下载并安装适用于 Visual Studio 2017 和 2019 的 AWS 工具包 https://marketplace.visualstudio.com/items?itemName=AmazonWebServices.AWSToolkitforVisualStudio201 7,如下图截图所示(如果您使用的是 Visual Studio 代码,您还可以从获得一个针对 Visual Studio 代码的 AWS 工具包。)https://aws.amazon.com/visualstudiocode/ :

- 进入 AWS 管理控制台,在 AWS 服务文本框中输入
IAM,点击显示的链接;您将被重定向到 Amazon 身份和访问管理(IAM)页面,如以下屏幕截图所示:

- 在 Amazon 身份和访问管理(IAM)页面上,单击创建单个 IAM 用户,然后单击管理用户并单击添加用户按钮,如以下屏幕截图所示:

- 在“添加用户”页面上,为新用户提供一个有意义的用户名并授予他们编程访问权限,然后单击页面底部的“下一步:权限”按钮,如以下屏幕截图所示:

- 您现在必须为新用户设置权限;为此,请单击“直接附加现有策略”按钮,如以下屏幕截图所示:

- 从现有策略中选择 AdministratorAccess,然后单击页面底部的 Next:Tags 按钮,如以下屏幕截图所示:

- 我们现在可以忽略标记,因为我们不打算有很多用户,只需转到下一步:检查并验证用户名和 AWS 访问类型以及所选策略是否正确,然后单击“创建用户”按钮,如以下屏幕截图所示:

- 等待创建新用户;当显示成功页面时,您可以下载
.csv文件,我们将使用该文件配置 Visual Studio 2019 和 AWS,如以下屏幕截图所示:

- 打开 Visual Studio 2019 并通过查看| AWS Explorer 显示 AWS Explorer,如以下屏幕截图所示:

- 单击“新建帐户配置文件”按钮(唯一活动的按钮),如以下屏幕截图所示:

- 将显示一个向导;在 AWS 上创建新用户的过程中,保留配置文件名为默认值,并使用您以前下载的
.csv文件中的值填写访问密钥 ID 和机密访问密钥字段,如以下屏幕截图所示:

- 由于 AWS 基于 IIS 作为.NET Core 应用的主机,因此您现在必须向
TicTacToe应用添加一个web.config文件,该文件指定 IIS web 服务器属性。用于处理外部请求并给出响应的处理程序被分配了aspNetCore属性,并设置为允许所有 HTTP 动词,如GET、POST、PUT、DELETE等。aspNetCore实例的处理路径被分配了我们的dll路径,我们还通过将forwardWindowsAuthToken属性设置为true来确保支持 Windows 身份验证,如下代码块所示:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*"
modules="AspNetCoreModule"
resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="dotnet"
arguments=".\TicTacToe.dll"
stdoutLogEnabled="true"
stdoutLogFile=".\logs\stdout"
forwardWindowsAuthToken="true" />
</system.webServer>
</configuration>
- 此外,我们必须启用 IIS 集成。为此,我们打开
Program.cs文件,并通过添加webBuilder.UseIISIntegration()更改 WebHost builder 配置以启用 IIS 集成,如下所示:
public static IHostBuilder CreateHostBuilder(string[]
args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.CaptureStartupErrors(true);
webBuilder.PreferHostingUrls(true);
webBuilder.UseUrls("http://localhost:5000");
webBuilder.ConfigureLogging((hostingcontext,
logging) =>
{
logging.AddLoggingConfiguration(
hostingcontext.Configuration);
});
webBuilder.UseIISIntegration();
});
- 右键单击 TictaToe 项目,然后单击发布到 AWS Elastic Beanstalk。。。在关联菜单中,如以下屏幕截图所示:

- 将显示一个向导;单击 Createanewapplicationenvironment 并单击 Next 按钮,如以下屏幕截图所示:

- 对于环境,您有三个选项:开发、测试或生产,但请选择之前创建的默认环境,然后单击下一步按钮,如以下屏幕截图所示:

- 验证框架版本是否设置为 netcoreapp3.0,表示 ASP.NET Core 3 应用,并保留所有默认值,然后单击下一步按钮,如以下屏幕截图所示:

- 选择 Generate AWSDeploy configuration,这将允许您使用 AWS 重新部署应用的副本,然后单击 Deploy 按钮,如以下屏幕截图所示:

- 部署将开始;通过转到 Output | Amazon Web Services,您可以看到部署过程的进展,如以下屏幕截图所示:

- 部署应用时,可以使用 AWS 资源管理器获取应用的 URL,如下所示:

- 打开浏览器并转到 AWS 中的应用 URL,启动应用,然后尝试注册新用户。
Note that if the application is not working as expected, you will get a 404 Not Found HTTP response. Everything is working locally and the deployment in AWS was successful, but something is wrong. You will see in the next chapter, which is about logging and monitoring, how to analyze, diagnose, understand, and fix this problem.
祝贺您在公共云中成功部署了第一个应用。它现在对外开放,用户可以连接到它并开始使用它。
以下是与 AWS 相关的示例。但是,我们仍然有一些引人注目的内容,因为我们将在接下来的部分中探索如何部署到其他目标,例如 Microsoft Azure 和 Docker 容器;所以,保持敏锐,继续阅读以下章节。
在 Microsoft Azure 中部署应用
Microsoft Azure 是 Microsoft 提供的一个公共云计算平台,用于在全球可用的 Microsoft 数据中心内构建、测试、部署和管理应用和服务。Microsoft Azure 不仅旨在满足来自 Microsoft 的编程语言和框架方面的工具,而且还包括专有或开源的第三方语言、框架和工具。
在 Microsoft Azure 中部署 web 应用时,您基本上有四种选择,如下所示:
- Azure 应用服务
- Azure 服务结构
- Azure 容器服务
- Azure 虚拟机
但是,在开始在 Microsoft Azure 中部署应用之前,您需要注册订阅;那么,让我们现在就这样做,如下所示:
- 您需要 Microsoft 帐户才能注册 Microsoft Azure 订阅。您可以使用您在Azure DevOps订阅中使用的同一个,但如果您还没有,请转到创建它 http://www.live.com 点击创建一个!链接,如以下屏幕截图所示:

- 转到https://portal.azure.com 并使用您的 Microsoft 帐户登录;你会被问到是否想去旅游。选择 Maybe later(不过,您真的应该稍后再进行巡演!),您将被重定向到 Microsoft Azure 管理门户,如以下屏幕截图所示:

- 单击左侧菜单底部的“更多服务”,然后单击“订阅”按钮,如以下屏幕截图所示:

- 单击添加按钮,如以下屏幕截图所示:

- 单击“免费试用”按钮并填写不同的表单,直到您创建了 Microsoft Azure 订阅,如下所示:

Note that there is no credit card required for Microsoft Azure (unlike AWS), and this is great, especially if you are a student.
令人兴奋的您现在已经准备好提供技术环境,然后将您的 ASP.NET Core 3 web 应用部署到世界各地的 Microsoft Azure 数据中心!
在 Microsoft Azure 应用服务中部署应用
Azure 应用服务是针对 Microsoft Azure 中基于 web 的应用的 PaaS 产品,包括自动扩展功能。在这方面,它可以与 AWS Elastic Beanstalk 相媲美,您之前已经在 AWS 部分中看到过。
Azure 应用服务不再需要管理基础架构;相反,您只需要关心构建和托管应用。对于完整的 DevOps 方法,如果您想使用 Microsoft Azure,建议使用此 PaaS 服务。
For more information on Microsoft Azure App Service, check out https://docs.microsoft.com/en-us/azure/app-service/app-service-web-overview.
正在运行 Azure 应用服务实例
以下示例说明了我们如何准备逐步将 Tic-Tac-Toe 应用部署到 Azure 应用服务:
- 转到 Microsoft Azure 管理门户,您会发现有许多服务可供您使用,包括最常用的服务,它们位于欢迎页面的显著位置,如以下屏幕截图所示:

- 此时我们需要的是应用服务,所以将鼠标指针悬停在应用服务图标上,它将显示以下弹出窗口。单击创建,如下所示:

- 您将获得一个新的 Web 应用表单。填写项目细节。在这个屏幕截图中,我已经有了一个 VisualStudioEnterprise 订阅,但是您可以使用您创建的试用订阅,或者您可能拥有的任何其他订阅。我已经创建了一个资源组,但是正如您所看到的,您可以创建一个新的资源组;如果您将鼠标悬停在信息图标上,它将告诉您资源组是共享相同生命周期、权限和策略的资源的简单集合。为应用填写唯一的名称,然后选择以代码形式发布应用。选择.NET Core 3.0(当前版本)作为运行时堆栈,将自动为您选择操作系统。对于该地区,我选择了南非北部,因为它是离我最近的地区,但你可以在现实生活中的应用中选择任何你觉得更接近目标受众的地区。
如果您没有决定要使用和付费的应用服务计划,请创建该计划,并确保再次检查您是否选择了使用共享基础架构并允许最多 1 GB 内存的免费层,如下所示:

- 填写上表并单击 Next:Monitoring 之后,您将看到以下屏幕,您可以在其中选择是否要启用 Application Insights。选择“是”,然后单击“下一步:标记”,如下所示:

- 单击下一步:标记后,您将进入标记部分。此应用不需要它们,因此单击最后一个“下一步”按钮查看并创建,您将看到以下摘要:

- 点击下载自动化模板链接,将下载一个
template.zip文件夹,里面有两个文件:parameters.json和template.json,我们可能需要这两个文件以备将来使用。下载后,通过导航链接返回 web 应用,它仍然会有摘要。单击 Create(创建),您将看到部署正在进行屏幕,然后是部署完成屏幕,如下所示:

- 单击上一屏幕中的 Go to resource(转到资源),然后将为您部署的应用显示一个仪表板,如下所示:

- 此时,如果您点击您在步骤 3:
https://[your-unique-project-name].azurewebsites.net/中创建的具有唯一名称的链接,您将看到您已经拥有一个网站,如以下屏幕截图所示:

祝贺如果你已经做到了这一点,你在 Azure 上运行了一个健康的应用服务,但是你会注意到,我们仍然没有在 Azure 上运行我们的 Tic-Tac-Toe 演示应用。接下来,在下一节中,让我们看看如何做到这一点。
在 Azure 上发布您的代码
现在您已经知道您的应用服务实例正在运行,继续并单击前面屏幕截图中显示的 Deployment Center 按钮,您将看到下面屏幕截图中显示的屏幕。也可以转到部署中心,方法是转到主页,浏览最近的资源,选择项目,然后在左侧窗格中的“部署”下选择“部署中心”,如下所示:

您会很高兴看到您有几个选项可以用来部署应用。你会记得在 Azure DevOps 中的第 3 章持续集成管道中,我们在Azure Repos中有我们的应用,你可能在 Azure Repos 或本地 Git 项目的整章中都在更新你的代码。无论哪种方式,我们都可以选择从 Azure Repos 或本地 Git 进行部署。
GitHub now has a cool feature called GitHub Actions that makes deployment much easier. It has a lot of workflows provided by its vibrant community, such as Deploy to Kubernetes, AWS, and Azure App Service, and you can create your own actions using the starter workflows here: https://github.com/actions/starter-workflows. You can find out more about GitHub Actions at https://github.com/features/actions.
我们还可以选择通过 GitHub 存储库或 Bitbucket 进行部署,但我们还需要进行更多的配置,这不在本书的范围之内。
出于演示目的,我们将使用 Azure 回购,如下一节所示。
与 Azure Repos 的持续集成
如果您一直在更新 Azure Repos 上的代码,只需单击 Azure Repos 按钮即可继续此过程。如果您尚未进行更新,并且一直在使用本地开发环境,请确保您查看了 Azure DevOps 中第 3 章、持续集成管道中的 Azure Repos 代码,并使用最新版本进行更新,然后提交。遵循以下步骤:
- 在部署中心,单击 Azure Repos 并继续,如以下屏幕截图所示:

- 您将看到构建提供商的两个选项,应用服务构建服务或 Azure 管道(预览)。它们之间的主要区别在于 Azure 管道做了一些额外的工作,例如运行负载测试,并首先部署到暂存,然后部署到生产环境。此时,请确保您同时拥有暂存和生产插槽,您可以通过单击左侧的 Deployment slots 菜单来添加它们。选择 Azure Pipelines(预览)并单击 Continue,如下所示:

- 然后,添加在 Azure DevOps 中第 3 章持续集成管道中创建的 Azure 回购项目的配置,如以下屏幕截图所示。单击“继续”,如下所示:

- 然后会显示一个摘要页面,您可以使用该页面检查所有参数是否正常。如果没有,您可以返回并更正;但假设一切正常,单击 Finish,如果一切正常,您将看到以下成功设置连续交付和触发生成消息:

- 执行刷新,您将看到一条已成功部署的消息。该消息还将包含发布版本,以及指向构建详细信息和源版本的链接,如下所示:

- 单击“同步”以确保您的 web 应用具有最新的代码,您将从存储库中获得消息“已成功触发连续交付”,其中包含最新的源代码,如以下屏幕截图所示:

这是个好消息。您的代码现在位于 Azure 门户上,但是如果您检查项目的 URL-https://[your-unique-project-name].azurewebsites.net/,您将面临一个错误。不要绝望;这仍然是个好兆头。我们在网站上有我们的代码,但我们没有连接到任何数据库。这就是我们将在下一节中看到的。
连接数据库
在以前的 Microsoft Azure 版本中,曾经有一个选项可以将 Web App+SQL 作为单个 Azure 服务,但最近它们被拆分。我们需要创建一个单独的数据库实例,以下是我们需要完成的步骤,以使数据库启动并运行,并准备好供演示应用使用:
- 转到您的个人主页portal.azure.com并将鼠标悬停在 SQL 数据库上,然后单击“创建”按钮:

- 在生成的表格上填写所需信息,如下所示:

- 为服务器名称、服务器管理员登录名和密码输入一些值,然后单击确定按钮,如下所示:

- 单击上一屏幕中的下一步:联网,在下一屏幕中配置是否允许公众访问此数据库。就我们的应用而言,我们只需要对其进行内部访问,如下所示:

- 单击 Review+create,然后单击 Finally create,如果部署成功,您将看到类似于以下屏幕的最终屏幕:

- 您需要允许访问 SQL 数据库以执行
TicTacToe应用的数据库生成脚本。在左侧下拉菜单或主页中,单击 SQL 数据库并选择TicTacToe数据库,如下图所示:

- 单击前面屏幕中的 Set server firewall(设置服务器防火墙),可以添加允许从 IP 访问 SQL 数据库的新规则。单击添加客户端 IP,验证您的 IP,然后单击保存以添加新规则,结果显示以下屏幕:

- 打开 Visual Studio 2019,转到 SQL Server 对象资源管理器,并使用
TicTacToeAzure 数据库连接字符串中的连接信息添加新的 SQL Server。如果您单击 Azure 链接,您会发现此内容已自动填写,如下所示:

- 向 Azure SQL Server 实例添加一个新数据库,就像您在 AWS 示例中所做的那样;它将用于执行
TicTacToe数据库生成脚本,如下所示:

- 如果您没有遵循 AWS 示例,请在 Visual Studio 2019 中打开 Package Manager 控制台并执行脚本迁移指令;否则,您可以重复使用相同的脚本,如前一个示例中所示。
- 将生成的脚本复制到 Azure
TicTacToe数据库的查询窗口中,然后执行脚本创建数据库和各种数据库对象,如下所示:

- 如果您没有阅读 AWS 部署示例,请记住添加与前面 AWS 示例相同的
web.config文件,并在Program.cs中的CreateHostBuilder方法中添加webBuilder.UseIISIntegration(),因为 App Service 基于 IIS 作为.NET Core 应用的主机。
现在,您已经在部署方面做了足够的工作。所有代码文件都在中,数据库已设置并连接。在下一章也是最后一章中,我们将了解下一步要做什么,以确保用户能够看到正在运行的应用。但同时,如果您发现使用 CI/CD 工具和 Azure DevOps 进行部署有点牵连,那么您有一个更简单的选择,这将在下一节中讨论:通过 Web 部署**l进行部署。保持专注。
通过 Web 部署工具进行部署
前面的示例使用了通过 Azure DevOps CI/CD 功能进行部署。或者,您可以使用Web Deploy直接从 Visual Studio 2019 将您的项目发布到 Azure,因此让我们严格准备应用,并通过 Visual Studio 2019 将其部署到您之前创建的 Microsoft Azure 应用服务实例中,如下所示:
- 由于应用服务基于 IIS 作为.NET Core 应用的主机,因此您现在必须向
TicTacToe项目添加一个web.config文件。但是,如果您以前遵循 AWS 示例,您应该已经这样做了,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<handlers>
<add name="aspNetCore" path="*"
verb="*" modules="AspNetCoreModule"
resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="dotnet"
arguments=".\TicTacToe.dll"
stdoutLogEnabled="true"
stdoutLogFile=".\logs\stdout"
forwardWindowsAuthToken="true" />
</system.webServer>
</configuration>
- 此外,您必须启用 IIS 集成;为此,请打开
Program.cs文件并更改 WebHost builder 配置以启用 IIS 集成。但是,如果您以前遵循 AWS 示例,您应该已经这样做了,如下所示:
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.CaptureStartupErrors(true);
webBuilder.PreferHostingUrls(true);
webBuilder.UseUrls("http://localhost:5000");
webBuilder.ConfigureLogging((hostingcontext, logging) =>
{
logging.AddLoggingConfiguration(hostingcontext.
Configuration); });
webBuilder.UseIISIntegration();
});
- 转到 Microsoft Azure 管理门户并单击左侧菜单中的应用服务。选择您之前创建的
TicTacToe应用,单击获取发布配置文件,然后下载 Azure 应用服务发布配置文件,如下所示:

- 右键点击
TicTacToe项目,点击上下文菜单中的发布,然后点击导入配置文件按钮,如下图:

- 选择下载的 Azure App Service 发布配置文件,发布过程应自动启动,如下所示:

- 您可以在“Web 发布活动”视图中看到发布过程,如下所示:

- 打开浏览器并转到 Microsoft Azure 中的应用 URL,启动应用,然后尝试注册新用户。
Note that if the application is not working as expected, you will get a 404 Not Found HTTP response. Everything is working locally and the deployment in Microsoft Azure was successful, but something is wrong. You will see in the next chapter (which is about logging and monitoring) how to analyze, diagnose, understand, and fix this problem.
以下是 Microsoft Azure 的示例。下一节将解释如何将应用部署到 Docker 容器中。
将应用部署到 Docker 容器中
Docker 通过使用容器简化了应用的构建、部署和运行。容器允许将库以及任何其他依赖项打包到单个应用包(容器映像)中,然后可以将该应用包作为单个一致性资源提供。此技术可确保打包的应用在容器可以使用的任何位置都能正确运行,而不考虑任何特定于环境的设置或配置。
以下是 Docker 工作原理的高级模式:

使用 Docker 容器时,基本上有三种选择,如下所示:
- 根据操作系统的不同,在本地或云中使用虚拟机(VM)与Docker for Windows或Docker Enterprise(Windows Server 2019 和 2016)一起使用
- 使用 Docker Hub(https://hub.docker.com )
- 使用 Microsoft Azure 容器服务或 AWS EC2 容器服务
For more information on Docker, visit the following links:
将应用部署到 Docker 容器中
Docker for Windows 提供了在 Windows 环境中开始使用 Docker 容器所需的一切,而 Docker Enterprise(Windows Server 2019 和 2016)专为需要基于 Docker 技术为生产环境提供必要支持的公司而设计。
让我们看看如何在 Windows 中使用 Docker,以及在这种情况下如何部署应用,如下所示:
- 如果尚未安装 Docker for Windows,请转至https://hub.docker.com 。如果您有 Docker ID,请使用 Docker ID 登录;否则,单击“注册”并填写您选择的 Docker ID,以及密码和电子邮件地址,如下所示:

- 然后,您将在您的帐户上填写个性化详细信息,验证后,在欢迎页面上,单击“Docker 桌面入门”,您将看到以下弹出窗口。单击下载 Docker Desktop for Windows 并在下载后安装,如下所示:

To install Docker Enterprise Edition for Windows 2016, go to https://hub.docker.com/editions/enterprise/docker-ee-server-windows and follow the installation instructions. After the installation, you should skip the following step, and continue directly with Step 4.
- 在安装过程中,请确保选择 Windows 容器,如下所示:

- 您也可以在安装后通过右键单击 Docker 托盘图标并进一步单击切换到 Windows 容器来切换到 Windows 容器。。。在关联菜单中,如下所示:

- 如果您的 Windows 安装中尚未启用容器功能,Docker 将询问您是否希望这样做。单击确定按钮,如下所示:

- 打开一个新的具有管理员权限的提升命令提示符,下载 Docker 官方 Microsoft SQL Server 映像,并执行
docker pull microsoft/mssql-server-windows-express指令,如下所示:

- 下载 Docker 官方 Microsoft ASP.NET Core 镜像,执行
docker pull microsoft/aspnetcore指令,如下:

- 为了能够将 Visual Studio 2019 中的应用直接编译并发布到 Docker 中,您还需要下载特定的构建映像并执行
docker pull microsoft/aspnetcore-build指令,如下所示:

- 如果检查到目前为止已安装的 Docker 映像,则应具有以下内容:

- 打开 Visual Studio 2019,然后打开
TicTacToe项目;在菜单中,单击 Project | Docker Support 并选择 Windows 操作系统,如下所示:

- 一个名为
docker-compose的新项目将自动生成并添加到解决方案中;它应该包含一个.dockerignore文件(部署过程中要忽略的文件)和一个docker-compose.yml文件(部署说明),如下所示:

- 更新 Docker Compose 项目中的
docker-compose.yml文件,如下所示:
version: '3'
services:
sql:
image: "microsoft/mssql-server-windows-express"
environment:
sa_password: "123TicTacToe!"
ACCEPT_EULA: "Y"
tictactoe:
image: tictactoe
build:
context: .
dockerfile: TicTacToe\Dockerfile
ports:
- "8081:5000"
depends_on:
- sql
- 更新
TicTacToe应用中appsettings.json文件中的DefaultConnection,如下所示:
"DefaultConnection":
"Server=sql;Database=Master;MultipleActiveResultSets=true;
User id=sa;pwd=123TicTacToe!"
- 更新
TicTacToe项目中的Program.cs文件;删除 IIS 集成,因为 Docker ASP.NET Core 映像基于 Kestrel 而不是 IIS,如下所示:
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.CaptureStartupErrors(true);
});
- 按F5键启动应用(应将
docker-compose项目设置为启动)。应用现在应该已经自动部署到 Docker 容器中;按如下所示,验证一切仍按预期工作:

- 打开命令提示符,执行
docker ps指令,查看所有正在运行的 Docker 进程。应该有多个正在运行的容器实例,如下所示:

在本例中,我们有一个用于开发环境的 TictaToe 应用映像实例,以及 microsoft/mssql server windows express 版本。在下一节中,我们将看到如何将此类图像发布到 Docker Hub。
将图像发布到 Docker Hub
您可以将应用映像上载到名为 Docker Hub 的基于云的 Docker 中央存储库,然后在 Microsoft Azure、AWS 或任何其他 Docker 支持的环境中使用它们。
Note that there are also other Docker registries you could use, such as Azure Container Registry and others. Since Docker provides its own registry via Docker Hub, it is, however, advised to use that.
有关 Docker Hub 的更多信息,请查看https://docs.docker.com/docker-hub 。
以下示例演示如何将示例TicTacToe应用发布并上载到 Docker Hub:
- 右键点击
TicTacToe项目,在关联菜单中选择发布;由于您已经在前面的示例中创建了发布配置文件,因此必须添加一个新的发布配置文件。单击创建新配置文件,如下所示:

- 单击容器注册表按钮,选择 Docker Hub,然后单击发布按钮,如以下屏幕截图所示:

- 输入您的 Docker Hub 用户名和密码,然后单击保存,如以下屏幕截图所示:

- 您的集装箱图像将发布到 Docker Hub;完成后,转到 Docker Hub 并验证图像是否已上载,如下所示:

做得很好,在应用开发方面,我们几乎从各个角度研究了如何开发出世界级的应用。你应该庆幸你的应用终于进入了容器中心。
总结
在本章中,我们讨论了托管和部署 ASP.NET Core 3 web 应用时的各种选项。您已经了解了托管是什么,以及如何为给定用例选择适当的解决方案。这将允许您为自己的应用做出更好的决策。
您已经了解了如何注册 AWS 帐户、如何提供技术环境以及如何部署 ASP.NET Core 3 web 应用。此外,您还了解了如何注册 Microsoft Azure 帐户,如何提供技术环境,以及如何使用这个强大的公共云计算平台部署 ASP.NET Core 3 web 应用。
然后,我们讨论了 Docker 以及当您使用这一现代、越来越多地被采用且具有影响力的技术时,您所拥有的各种部署选择。您已经为未来做好了充分的准备,因为 Docker 可能会完全改变我们部署和管理应用的思维方式。
您已经获得了任何一个认真的开发人员都应该具备的非常重要的技能,包括托管 web 应用、部署到 Docker 容器以及部署到 AWS 或 Microsoft Azure;最后但并非最不重要的一点是,您已经获得了持续部署所需的技能。
在下一章中,我们将解释如何有效地管理和监督已部署的 web 应用,这对于 DevOps 方法非常重要。
十三、管理 ASP.NET Core 3 应用
在完成了开发生命周期之后,我们本可以就此停止。然而,增加了最后一章,以强调彻底的DevOps方法的重要性。
现在,我们只讨论了 DevOps 中的开发(开发)端,但是您还应该接受操作(操作)端,它包括在运行时管理和监督您的应用。
这个非常重要的主题经常被低估,甚至更糟糕的是,有时被完全抛在一边。开发人员倾向于认为这不是他们工作的一部分。他们经常说这样的话:但它在我的机器上工作,这是你的问题,不是我的。这也通常被称为混乱之墙。敏捷方法和 DevOps 旨在避免这种想法,本章将为您提供一些建议和示例,说明如何在 ASP.NET Core 3 应用中更好地解决这些问题。
应用的成功将取决于您如何帮助 IT 运营部门了解运行时发生的情况。这意味着为他们提供了快速有效地管理和监督应用的方法。
只有这样,您才能以较低的平均修复(平均修复时间)提供高质量的应用,从而在您特定的市场中成为未来的市场领导者。
此外,在使用 ASP.NET Core 3 时,您很容易解决这些问题,因为大多数情况下,您可以利用集成的或提供的功能,而无需进行任何较大的代码更改。
我们将首先了解如何为 Azure 和Amazon Web 服务(AWS)添加日志记录,然后了解如何在本地和 Docker 中监控应用,然后再了解如何在 Azure 和 AWS 中进行监控。
在本章中,我们将介绍以下主题:
- 登录 ASP.NET Core 3 应用
- 监视 ASP.NET Core 3 应用
登录 ASP.NET Core 3 应用
在第 12 章托管 ASP.NET Core 3 应用中,我们解释了如何将您的 ASP.NET Core 3 应用部署到 Microsoft Azure、AWS 和 Docker。让我们进一步了解如何将日志记录和监视添加到这些环境中,这对于诊断意外行为和错误非常重要。
首先是一些理论背景,然后是一些实例。您准备好学习如何帮助 it 运营了吗?来吧这是最后一章。走吧!
应用内的日志记录包括创建数据,以帮助了解运行时发生的情况。可以记录多种类型的消息,例如信息、警告和错误。
然后,应将这些数据持久化到日志文件、数据库、SaaS 解决方案或其他目的地。为了提高应用性能,建议允许 it 操作在应用运行时更改收集的日志数据的详细程度。例如,在生产环境中,只应记录警告和错误,而在开发期间更有效地记录所有内容并更好地了解幕后发生的事情是非常有意义的。
建议使用标准框架,如Windows 事件跟踪(ETW)来构造和格式化日志数据,以便 It 运营部门可以使用其首选的监控工具快速、轻松地读取和诊断错误原因。著名的日志框架,如Serilog或Log4net也支持标准输出格式,因此如果您愿意,也可以使用它们。
因此,让我们看一些具体的例子,介绍如何在不同的环境(如内部部署、公共云和 Docker)中处理 ASP.NET Core 3 应用的日志记录。
在内部部署环境中,日志数据大部分时间存储在日志文件中。在这种情况下,应用需要具有写入权限才能写入日志文件,建议将所有日志文件存储在应用路径下名为logs的中心文件夹中。
在 Microsoft Azure 中,您基本上有三种不同的解决方案来处理应用中的日志记录,如下所示:
- 标准文件记录:这是最简单的方法,没有任何代码修改,但也是功能最差的方法。您需要下载文件以检索应用的日志数据。
- Azure 应用服务诊断:如果您的应用服务只有一个实例,那么这是推荐的解决方案,因为没有提供日志集中功能。
- Azure Application Insights:这是最集成、最强大的解决方案,可跨所有应用层工作。
AWS 提供用于日志记录和监视的 CloudWatch。提供的日志机制与 Microsoft Azure 的日志机制非常相似。当您了解了这些在 Microsoft Azure 中的工作方式后,您将能够轻松、快速地将您的知识应用到 AWS,正如您将在提供的示例中看到的那样。
For more information, you can visit the AWS CloudWatch website at https://aws.amazon.com/en/cloudwatch.
Docker 不提供任何针对 Microsoft Azure 或 AWS 的集成监控或日志记录服务。这意味着要在 Docker 中向 ASP.NET Core 3 应用添加、记录和监视功能,必须使用日志文件。此外,您必须提供自己的集中式日志恢复和分析机制,以获得一致的日志记录和监视数据。
但是,由于应用可以多次实例化,因此这可能不是最好的方法。相反,您还可以直接登录到集中式控制台,这应该是 Docker 环境中最有效、最合适的解决方案。
登录 Microsoft Azure
好啊现在您已经看到了几种用于在不同环境中登录的解决方案,我们将重点介绍 Microsoft Azure。如果您担任 IT 运营部门的角色,需要诊断应用在 Microsoft Azure 中无法按预期工作的原因,会发生什么情况?你的选择是什么?最好的解决方案是什么?这正是您将在本节中学习的内容。
如果您还记得的话,我们已经在本书的第 4 章、通过自定义应用登录 ASP.NET Core 3 的基本概念:第 1 部分中讨论了应用级别。在那里,我们将记录应用事件添加到应用文件夹的logs子文件夹中的日志文件中。此文件夹需要同步并监控磁盘空间使用情况,因为当它变得太大时,它本身很可能成为故障的原因。
此外,由于应用日志和环境日志(互联网信息服务(IIS))、Windows、SQL Server 等都是分开处理的,因此日志的来源是多个的。你必须综合所有信息,才能全面了解幕后发生的事情。这是非常复杂和耗时的。
如您所见,在这种情况下,阅读和分析应用日志需要大量的手工工作。如果您需要同时监视和管理大量应用,那么这将成为一个更大的问题。手动操作并不是一种真正的选择。我们需要找到更好的解决办法。
此外,Microsoft Azure 中还有更好、更集成的解决方案!例如,如果您在 Azure 应用服务中部署应用,则可以使用 Azure 应用服务诊断。可以直接从门户启用此功能。此外,应用日志和环境日志自动集中在一个地方,这有助于以更快、更直接的方式发现问题。
启用 Microsoft Azure 应用服务
启用 Microsoft Azure 应用服务诊断非常简单,现在让我们看看如何做到这一点:
- 在 Visual Studio 2019 中打开 Tic Tac Toe web 项目,并将名为
AzureAppServiceDiagnosticExtension的新扩展添加到Extensions文件夹中,如下所示:
public class AzureAppServiceDiagnosticExtension
{
public static void AddAzureWebAppDiagnostics
(IConfiguration configuration, ILoggingBuilder
loggingBuilder)
{
loggingBuilder.AddAzureWebAppDiagnostics();
}
}
- 更新
ConfigureLoggingExtension类中的AddLoggingConfiguration方法,并为之前新增的 AzureApplicationServiceDiagnosticExtension添加一个案例,如下所示:
foreach (var provider in loggingOptions.Providers)
{
switch (provider.Name.ToLower())
{
case "console": { loggingBuilder.AddConsole(); break; }
case "file": { ... }
case "azureappservices":
{
AzureAppServiceDiagnosticExtension
.AddAzureWebAppDiagnostics(configuration,loggingBuilder);
break;
}
default: { break; }
}
}
- 更新
appsettings.json配置文件,并为 Azure App Service 添加新的提供商,如下所示:
"Logging": {
"Providers": [
{
"Name": "Console",
"LogLevel": "1"
},
{
"Name": "File",
"LogLevel": "2"
},
{
"Name": "azureappservices"
}
],
"MinimumLevel": 1
}
- 更新
Program.cs文件,更改 WebHost builder 配置以启用 IIS 集成,并添加日志记录配置,如下所示:
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.CaptureStartupErrors(true);
webBuilder.PreferHostingUrls(true);
webBuilder.ConfigureLogging((hostingcontext, logging) =>
{
logging.AddLoggingConfiguration(hostingcontext.
Configuration); });
webBuilder.UseIISIntegration();
});
- 将 Tic Tac Toe web 应用发布到 Azure 应用服务。如果你不知道怎么做,你可以在第 12 章、托管 ASP.NET Core 3 应用中查找。
- 转到 Microsoft Azure 门户网站,单击菜单中的应用服务,选择已部署的 Tic Tac Toe 应用服务,然后向下滚动,直到看到监控部分,如以下屏幕截图所示:

- 在监控部分,单击应用服务日志,然后设置应用日志(文件系统)打开按钮。选择“级别为详细”,启用详细的错误消息和失败的请求跟踪,然后单击“保存”按钮,如下所示:

Tic Tac Toe 应用现在将开始在 Azure 应用服务文件系统中记录数据。然而,这只是第一步。您需要检索日志才能对其进行分析。
根据您的具体需要,有多种访问日志的方法。其中一些在此处指定,如下所示:
- 使用 FTP 或 FTPS 浏览
logs文件夹 - 配置 Azure Blob 存储,然后下载 Blob 内容,这还具有将多个服务的日志集中在一个位置的好处
- 使用专用应用自动检索日志
幸运的是,社区已经在 GitHub 上开发了一个开源解决方案,称为 Azure 网站日志浏览器扩展,您可以使用它。此解决方案包括向 Azure 门户添加扩展。
现在,您将看到如何通过以下步骤将 Azure 网站日志浏览器扩展添加到 Microsoft Azure 门户以分析日志:
- 转到 Microsoft Azure 门户网站,单击菜单中的应用服务,选择您在上一示例中部署的 Tic Tac Toe 应用服务,向下滚动直到看到“开发工具”部分,单击扩展,然后单击添加按钮,如下图所示:

- 选择并安装 Amit Apple 发布的 Azure 网站日志浏览器扩展,如下所示:

- 安装完成后,该扩展将添加到 Tic Tac Toe 应用服务的活动扩展中,如下所示:

- 单击 Azure 网站日志浏览器扩展,您将看到扩展名、作者、版本号以及其他附加信息的概述。单击浏览按钮,如以下屏幕截图所示:

- 新的浏览器窗口将自动打开,您可以在其中看到不同的日志文件源。单击文件系统-应用日志,如以下屏幕截图所示:

- 选择包含需要分析的诊断数据的日志文件,如下所示:

- 读取并滚动浏览彩色编码的日志文件内容。您将自动看到生成的日志条目,以及您在前面章节中添加的日志条目,如下所示:

为了拥有有意义的日志并能够查看它们,您只需要知道这些。日志对于每个应用都非常重要,如果设计得当,它们可以为您节省大量时间和精力,进而节省金钱,例如,如果由于日志记录不足而花费很长时间才发现异常,您可能会因此而蒙受损失。在下一节中,让我们看看如何在 AWS 中实现同样的功能。
登录 AWS
如果您使用的是 AWS,那么将日志添加到 ASP.NET Core 3 应用将非常简单。您只需将应用日志写入控制台,部署在 AWS Elastic Beanstalk 中的应用将自动将其日志存储在 AWS CloudWatch 中。然后,您将能够使用 CloudWatch 仪表板分析正在发生的事情。这与您在前面的示例中看到的 Application Insights 及其仪表板相当。
现在,您将了解如何访问部署到 AWS Elastic Beanstalk 的应用的日志,如下所示:
- 将 Tic Tac Toe Web 应用发布到 AWS Elastic Beanstalk。如果您不知道怎么做,可以在第 12 章、托管 ASP.NET Core 3 应用中查找。
- 启动应用,转到 AWS 管理控制台,在 AWS 服务查找服务文本框中输入
Beanstalk,然后单击显示的链接。您将被重定向到 Elastic Beanstalk 欢迎页面,如下所示:

- 在 Elastic Beanstalk 欢迎页面上,选择上一步部署的
TicTacToe应用,如下所示:

- 单击左侧菜单中的日志,然后单击请求日志|最后 100 行。您现在可以下载需要分析的日志文件,如以下屏幕截图所示:

- 下载日志文件并检查其内容,如下所示:

您已经了解了如何在各种环境、本地和云中处理日志记录。下一节将向您介绍监视,以及它如何帮助您实时分析问题。
监视 ASP.NET Core 3 应用
在上一节中,您了解了如何为 ASP.NET Core 3 web 应用生成和分析应用日志,这将帮助您更好地了解意外行为和应用错误。这将帮助 IT 运营部门在事件发生后跟踪不同的步骤,直到找到问题的根本原因。
但是,这无助于他们不断地监视和监督应用,因为在这种情况下,使用日志机制将导致性能不佳,并对应用产生负面的整体影响。日志记录不是连续监视的正确解决方案!
监控的目标是实时分析和监控大量应用指标,并自动检测应用异常。这些指标需要具有非常低的消息占用空间,才能有效地工作。
下面列出了 ASP.NET Core 3 最常见的监控框架:
EventSource使用 ETW,速度非常快,类型强。这是在.NET4 中引入的,仅适用于 WindowsDiagnosticSource与EventSource非常相似,可以跨平台工作,比如EventSource与 Windows 的 ETW,以及 Linux 的 LTTng
For more information on ETW, go to the following website:
https://docs.microsoft.com/en-us/windows/win32/etw/about-event-tracing.
有关 LTTng 的更多信息,请访问以下网站:
http://lttng.org 。
在这些框架之上,大多数公共云提供商都提供自己的监控解决方案。例如,对于 Microsoft Azure,建议使用 Azure Application Insights,而您应该使用针对 AWS 的 CloudWatch。这两个监控解决方案完全是 SaaS,并且与各自的公共云提供商门户集成得更多。
现场和码头内的监控
内部部署和 Docker 环境本身没有标准的监控解决方案,但有一些社区认可的监控框架,如EventSource或DiagnosticSource,您可以使用它们来实现自己的解决方案。
由于这些框架遵守 ETW 等市场标准,IT 运营部门将能够使用其标准监控工具连接您的 ASP.NET Core 3 web 应用,他们会非常喜欢的!
例如 Windows 上的 PerfMon,它可以接收 ETW 事件并生成用于监视目的的图表。
在使用DiagnosticSource时,首先创建一个侦听器。此侦听器接收应用事件并提供事件名称和参数。创建监听器最简单的方法是创建一个普通的旧 CLR 对象(POCO)类,该类包含需要使用[DiagnosticName]修饰符修饰的方法,并设计为接受适当类型的参数。
以下示例说明如何使用DiagnosticSource在内部部署和 Docker 环境中向 ASP.NET Core 3 应用添加监控:
- 在 Visual Studio 2019 中打开 Tic Tac Toe web 项目,并添加一个名为
Monitoring的新文件夹;在此文件夹中,添加一个名为ApplicationDiagnosticListener的新类,如下所示:
public class ApplicationDiagnosticListener
{
[DiagnosticName("TicTacToe.MiddlewareStarting")]
public virtual void OnMiddlewareStarting(HttpContext
httpContext)
{
Console.WriteLine
($"TicTacToe Middleware Starting, path:
{httpContext.Request.Path}");
}
[DiagnosticName("TicTacToe.NewUserRegistration")]
public virtual void NewUserRegistration(string name)
{
Console.WriteLine($"New User Registration {name}");
}
}
- 更新
Startup类中的Configure方法,添加DiagnosticListener,订阅ApplicationDiagnosticListener,如下图:
public void Configure(IApplicationBuilder app,
IHostingEnvironment env, DiagnosticListener
diagnosticListener)
{
var listener = new ApplicationDiagnosticListener();
diagnosticListener.SubscribeWithAdapter(listener);
...
}
- 更新
CommunicationMiddleware,添加一个名为_diagnosticSource的新私有成员,并更新构造函数,如下所示:
private readonly RequestDelegate _next;
private DiagnosticSource _diagnosticSource;
public CommunicationMiddleware(RequestDelegate next,
DiagnosticSource diagnosticSource)
{
_next = next;
_diagnosticSource = diagnosticSource;
}
- 更新
CommunicationMiddleware中的Invoke方法,如果诊断源启用,则写入事件,如下所示:
public async Task Invoke(HttpContext context)
{
if (context.WebSockets.IsWebSocketRequest)
{
if (_diagnosticSource.IsEnabled
("TicTacToe.MiddlewareStarting"))
{
_diagnosticSource.Write("TicTacToe.
MiddlewareStarting",
new
{
httpContext = context
});
}
...
- 在 Visual Studio 2019 中更改调试设置,并将项目和 emulator 设置为 TictaToe,如下所示:

- 按F5以调试模式启动应用。控制台将自动打开。注册新用户并检查控制台输出;您将看到 TictaToe 中间件启动消息,如下所示:

如前所述,向控制台发送日志记录和监视数据是本地环境的一种可能解决方案,也是 Docker 环境的推荐解决方案。
Microsoft Azure 中的监控
Microsoft Azure 提供了一个名为 Azure Application Insights 的集成解决方案,它允许 IT 运营部门实时监控应用、资源和服务。它适用于整个 Azure 订阅,包括用于快速访问分析数据的仪表盘和图表。
下图说明了 Azure Application Insights 的一些功能:

让我们在一个易于理解的示例中使用应用洞察力;为此,您将首先在 Microsoft Azure 中创建一个新的 Azure Application Insights 资源及其相应的 API 密钥,如下所示:
- 转到 Microsoft Azure 门户网站,单击菜单中的应用服务,选择您在上一示例中部署和配置的 Tic Tac Toe 应用服务,向下滚动直到看到监控部分,单击应用洞察,填写所有字段,然后单击确定按钮。将为您创建一个新的 Application Insights 资源,如下所示:

- 单击菜单中的监视器。将显示一个新选项卡。转到解决方案部分,选择应用洞察,然后选择创建的应用洞察资源,如下所示:

- 将显示应用洞察资源选项卡;向下滚动直到看到配置部分,然后单击 API 访问,如以下屏幕截图所示:

- 单击 Create API key 以生成密钥,该密钥将用于 Tic Tac Toe 示例应用,如以下屏幕截图所示:

- 配置 API 密钥访问权限(读取遥测、写入注释、验证 SDK 控制通道),并为其指定一个有意义的名称,如以下屏幕截图所示:

您现在已经完成了 Microsoft Azure 中 Application Insights 资源的创建和配置。Visual Studio 2019 包含一些高级内置功能,允许您直接从集成开发环境(IDE中连接 ASP.NET Core 3 应用。
在接下来的步骤中,您将为 Azure application Insights 配置 ASP.NET Core 3 web 应用,如下所示:
- 打开 Tic Tac Toe web 项目,单击顶部菜单中的 project,然后选择 Add Application Insights Telemetry…,如以下屏幕截图所示:

- 将显示 Application Insights 配置页面。单击开始自由按钮,如以下屏幕截图所示:

- 输入您的帐户和订阅详细信息,选择资源,然后单击注册按钮,如以下屏幕截图所示:

-
将 Tic Tac Toe web 应用重新发布到 Microsoft Azure 应用服务,以便应用 application Insights 配置。
-
转到 Microsoft Azure 门户网站,单击菜单中的 Monitor,向下滚动到解决方案部分,单击 Application Insights,然后选择新创建的 Application Insights 资源。
-
将显示 Application Insights 仪表板。它用于获得全球概览,以及深入了解不同的监控区域,如以下屏幕截图所示:

- 点击搜索查看申请流程;在这里,您可以看到在用户注册过程中发生了一个错误,如下所示:

在将 Tic-Tac-Toe 应用部署到 Microsoft Azure 或 AWS 之后,您可能已经在托管 ASP.NET Core 3 应用的第 12 章以及本章前面的日志部分中看到了这些错误。一切都在本地和 Docker 中工作,但当您将其部署到公共云时,它就不再工作了。很奇怪!我们不能再等了;它真的需要修复!
我们现在将更详细地分析问题,并尝试了解解决问题需要做什么,如下所示:
-
在 Azure Application Insights 中,您可以清楚地看到用户注册存在一个问题:更具体地说,是 404 Not Found HTTP 响应。
-
查看日志文件时,如前一节所述,您可以看到无法找到
EmailTemplates文件夹中的UserRegistrationEmail视图,这会导致其他错误,如以下屏幕截图所示:

- 转到 Microsoft Azure 门户网站,单击菜单中的应用服务,选择您在上一示例中部署和配置的 Tic Tac Toe 应用服务,向下滚动直到看到开发工具部分,单击应用服务编辑器(预览),然后单击Go链接,如以下屏幕截图所示:

- 将自动打开带有应用服务编辑器页面的新窗口;点击搜索按钮,搜索
EmailTemplates文件夹。无法找到,因为在发布过程中,所有视图都被预编译到一个名为TicTacToe.PrecompiledViews.dll的动态链接库(DLL)中,如下所示:

- 通过在发布过程中停用预编译,对此问题应用临时修复程序。打开 Tic Tac Toe web 项目的
.csproj文件,然后将以下配置元素添加到PropertyGroup部分:
<PropertyGroup>
...
<PreserveCompilationContext>true
</PreserveCompilationContext>
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
</PropertyGroup>
Note that this is only a temporary fix, for example purposes. You should reactivate precompilation, and target the precompiled views in your code for a more industrialized and production-ready solution.
- 将 Tic Tac Toe web 应用重新发布到 Microsoft Azure 应用服务。现在一切都正常了,包括用户注册。
Note that you have to register a completely new user with a strong password such as Azerty1234!, for example, otherwise you might get additional errors if you don't. The application is missing some more advanced error handling due to a lack of space within the book. Keep in mind that it was only given to better understand all the ASP.NET Core 3 concepts. You can, however, use the sample application as a base and then refine it as you like, and add the missing error handling.
您已经了解了如何配置 ASP.NET Core 3 web 应用,并能够使用 Azure Application Insights 对其进行监视。您甚至在应用的用户注册过程中发现了一个问题。您已经分析了日志记录和监控数据,并且能够解决问题。
这对于.NET Core 代码非常有效,但是目前,您无法看到应用的 JavaScript 部分中是否出现任何错误。由于现代应用包含大量 JavaScript 代码,如果您也能够监控这些部分,那就太好了;正当好吧,你可以做到;你只需要稍微修改一下代码。
让我们看看如何调整代码并能够监控 JavaScript 应用流,如下所示:
- 启动 Visual Studio 2019 并打开 Tic Tac Toe web 项目,更新
Views文件夹中的_ViewImports.cshtml文件,并将 Application Insights JavaScript 片段添加到文件底部,如下所示:
@inject Microsoft.ApplicationInsights.AspNetCore
.JavaScriptSnippet JavaScriptSnippet
- 更新布局页面和移动布局页面,然后在两个页面的
head部分添加以下行:
@Html.Raw(JavaScriptSnippet.FullScript)
- 更新
Startup类,并注册 Application Insights 服务,如下所示:
services.AddApplicationInsightsTelemetry(_configuration);
- 将 Tic Tac Toe web 应用重新发布到 Microsoft Azure 应用服务,以便应用新的 application Insights 配置。
- 启动应用并打开 Microsoft Azure 门户网站中的 application Insights 仪表板,单击搜索,然后单击过滤器并选择仅请求,取消选择所有其他事件类型,如以下屏幕截图所示:

伟大的您可以不断监视整个应用,无论是在 JavaScript 端还是在.NET Core 端,这在出现错误行为时都非常有用。
在最后一步中,您将学习如何添加和监视自定义度量,这将允许您跟踪应用中的业务度量,如下所示:
- 打开 Tic Tac Toe web 项目,将名为
AzureApplicationInsightsMonitoringService的新服务添加到Services文件夹中,如下所示:
public class AzureApplicationInsightMonitoringService
{
readonly TelemetryClient _telemetryClient = new
TelemetryClient();
public void TrackEvent(string eventName, TimeSpan
elapsed,
IDictionary<string, string> properties = null)
{
var telemetry = new EventTelemetry(eventName);
telemetry.Metrics.Add("Elapsed",
elapsed.TotalMilliseconds);
if (properties != null)
foreach (var property in properties)
{
telemetry.Properties.Add(property.Key,
property.Value);
}
_telemetryClient.TrackEvent(telemetry);
}
}
- 从 Azure
ApplicationInsightsMonitoringService类中提取接口并调用它IMonitoringService。 - 在
Options文件夹中添加一个名为MonitoringOptions的新选项,如下所示:
public class MonitoringOptions
{
public string MonitoringType { get; set; }
public string MonitoringSetting { get; set; }
}
- 更新
Startup类中的Configure方法,如果在appsettings.json配置文件中配置了 AzureApplicationInsightsMonitoringService类,则注册 AzureApplicationInsightsMonitoringService类,如下所示:
...
services.AddApplicationInsightsTelemetry(_configuration);
var section = _configuration.GetSection("Monitoring");
var monitoringOptions = new MonitoringOptions();
section.Bind(monitoringOptions);
services.AddSingleton(monitoringOptions);
if (monitoringOptions.MonitoringType ==
"azureapplicationinsights")
{
services.AddSingleton<IMonitoringService,
AzureApplicationInsightsMonitoringService>();
}
- 更新
UserService并添加一个名为_telemetryClient的新私有成员,然后更新构造函数以初始化私有成员,如下所示:
...
private readonly IMonitoringService _telemetryClient;
public UserService(RoleManager<RoleModel> roleManager,
ApplicationUserManager userManager, ILogger<UserService>
logger, SignInManager<UserModel>
signInManager,IMonitoringService telemetryClient)
{
...
_telemetryClient = telemetryClient;
...
}
- 更新
UserService中的RegisterUser方法,使用TrackEvent方法,然后添加一个名为RegisterUser的自定义度量,如下所示:
...
finally
{
stopwatch.Stop();
_telemetryClient.TrackEvent("RegisterUser",
stopwatch.Elapsed);
_logger.LogTrace($"Start register user {userModel.Email}
finished at {DateTime.Now} - elapsed
{stopwatch.Elapsed.TotalSeconds} second(s)");
}
...
- 更新
appsettings.json配置文件,添加新的Monitoring部分,然后将其配置为 Azure Application Insights,如下所示:
"Monitoring": {
"MonitoringType": "azureapplicationinsights",
"MonitoringSettings": ""
}
-
将 Tic Tac Toe web 应用重新发布到 Microsoft Azure 应用服务,以便应用新的 application Insights 配置。
-
启动应用并打开 Microsoft Azure 门户网站上的 application Insights 仪表板,单击搜索,然后输入
RegisterUser作为搜索词;您现在只会看到定制的RegisterUser业务指标,如下所示:

这就是我们在 Azure 上监控甚至相当复杂的应用所需要的一切,如果您更喜欢使用 AWS 托管应用,下一节将向您展示我们如何在 AWS 平台上实现类似的功能。
自动气象站的监测
与 Microsoft Azure 一样,AWS 提供了一个集成的解决方案,允许 IT 运营部门实时监控应用、资源和服务。在 AWS 中,此解决方案称为 CloudWatch。它提供了与 Application Insights 几乎相同的功能,这意味着它适用于整个 AWS 订阅,并包括仪表板和图表,用于快速访问分析数据。
以下示例说明了如何使用 AWS CloudWatch 监控通用指标和自定义指标,以便您了解如何根据自己的需要部署它:
- 打开 Tic Tac Toe web 项目,下载并安装名为
AWSSDK.Core的.NET Core 运行时 NuGet 软件包 Amazon web Services SDK,以及名为AWSSDK.CloudWatch的 Amazon web Services CloudWatch NuGet 软件包。 - 将名为
AmazonWebServicesMonitoringService的新服务添加到Services文件夹中,使其继承IMonitoringService接口,并使用 AWS 特定代码实现TrackEvent方法,如下代码块所示:
public class AmazonWebServicesMonitoringService :
IMonitoringService
{
readonly AmazonCloudWatchClient _telemetryClient = new
AmazonCloudWatchClient();
public void TrackEvent(string eventName, TimeSpan
elapsed,
IDictionary<string, string> properties = null)
{
...
}
}
- 以下是
TrackEvent方法中的实际代码:
var dimension = new Dimension { Name = eventName, Value = eventName };
var metric1 = new MetricDatum
{
Dimensions = new List<Dimension> { dimension },
MetricName = eventName, StatisticValues = new StatisticSet(),
Timestamp = DateTime.Today, Unit = StandardUnit.Count
};
if (properties?.ContainsKey("value") == true)
metric1.Value = long.Parse(properties["value"]);
else metric1.Value = 1;
var request = new PutMetricDataRequest
{ MetricData = new List<MetricDatum>() { metric1 }, Namespace = eventName };
_telemetryClient.PutMetricDataAsync(request).Wait();
- 更新
Startup类中的Configure方法,如果appsettings.json配置文件中已经配置,则注册 Amazon Web Services 云监控服务,如下所示:
...
if (monitoringOptions.MonitoringType ==
"azureapplicationinsights")
{
services.AddSingleton<IMonitoringService,
AzureApplicationInsightsMonitoringService>();
}
else if (monitoringOptions.MonitoringType ==
"amazonwebservicescloudwatch")
{
services.AddSingleton<IMonitoringService,
AmazonWebServicesMonitoringService>();
}
- 更新
appsettings.json配置文件中的Monitoring部分,并将其配置为 AWS CloudWatch,如下所示:
"Monitoring": {
"MonitoringType": "amazonwebservicescloudwatch",
"MonitoringSettings": ""
}
- 将 Tic Tac Toe web 应用发布到 AWS Elastic Beanstalk,以便应用新的 AWS CloudWatch 配置。如果您不知道怎么做,可以在第 12 章、托管 ASP.NET Core 3 应用中查找。
- 启动应用。转到 AWS 管理控制台,在 AWS 服务文本框中输入
CloudWatch,然后单击显示的链接。您将被重定向到 AWS CloudWatch 欢迎页面,如下所示:

- 在 CloudWatch 欢迎页面上,单击
TicTacToe应用,如下所示:

- 单击报警以获取有关报警的更多详细信息,如下所示:

- 返回 CloudWatch 欢迎页面,在文本框中输入
RegisterUser作为搜索词,然后单击浏览指标,如下所示:

- 您将看到一个带有自定义
RegisterUser业务度量的图,如图所示:

这应该足以让您了解如何监控您的平台,但建议您四处看看所有额外的功能。我很肯定,无论您负责什么应用,您都会从检测和预防异常中获得乐趣。
总结
在本章中,我们讨论了如何管理和监督 ASP.NET Core web 应用,以帮助 IT 运营部门更好地了解运行时、错误发生之前和之后发生的情况。
我们讨论了日志的概念,以及它如何帮助减少理解和修复 bug 的时间。我们演示了不同的日志记录解决方案:内部部署、Microsoft Azure、AWS 和 Docker。
在一个详细的示例中,您体验了如何使用 Azure 应用服务和 Azure 应用服务诊断以及用于日志文件分析的 Azure 网站日志浏览器扩展在 Microsoft Azure 环境中配置日志记录。
然后,您看到了如何在 AWS 中通过使用 AWS CloudWatch 访问和下载应用日志来执行相同的操作。
然后,我们介绍了监视的概念,并解释了如何将监视添加到内部部署和 Docker 环境中。
您已配置 Azure Application Insights 以实时监视 ASP.NET Core web 应用。你甚至能够理解并解决404 Not Found问题背后的奥秘。
在最后一步中,我们向您展示了如何使用 AWS CloudWatch 在 AWS 环境中进行监控。
在下一章中,我们将……嗯,没有下一章。你已经看到了这本书所提供的一切。我们希望你们喜欢它,希望你们在理解和吸收我们给出的众多例子中发现了一些价值。
现在由您来创造您自己的体验,并进一步提高您的 ASP.NET Core 技能。
正如 Nicolas Clerc(微软法国云架构师)在本书开头的前言中所说,现在你可以作为一名老兵开始你的旅程。
祝你好运,谢谢你花时间阅读不同的章节,也谢谢你和我们在一起这么久!
第一部分:介绍和环境设置
本节将优雅地向您介绍 ASP.NET Core 3 及其功能,以及使用 web 框架的意义。在本节结束时,您将了解 ASP.NET Core 3 的功能,并已建立了一个开发环境,我们将在其中开发本书中使用的演示应用。
本节包括以下章节:
第二部分:ASP.NET Core 3 的实践演示
在本节中,您将学习如何开发一个真实的 ASP.NET Core 3 应用,从基础到功能齐全的应用。在本节末尾,我们将把前面介绍的所有基本概念拼凑到一个可用的应用中。
本节包括以下章节:
- 第 4 章、通过自定义应用实现 ASP.NET Core 3 的基本概念:第 1 部分
- 第 5 章、通过自定义应用实现 ASP.NET Core 3 的基本概念:第 2 部分
- 第 6 章介绍 Razor 组件及信号机
- 第 7 章创建 ASP.NET Core MVC 应用
- 第 8 章创建 Web API 应用
第三部分:ASP.NET Core 3 所支持的生态系统
在本节中,我们将引导您通过使用 EntityFrameworkCore3 持久化数据并在需要时检索数据。然后,我们将处理托管已部署应用的问题,并引导您完成监视它们的过程。为了强调确保应用安全的重要性,将在专门讨论安全性的一章中详细讨论安全性。
本节包括以下章节:


浙公网安备 33010602011771号