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

微软在 20 世纪 90 年代末开始致力于我们现在所知道的。net 框架,并在 2001 年底发布了。net Framework 1.0 的第一个测试版。
最初,这个框架被命名为下一代 Windows 服务 NGWS(内部代码为 Lightning/Project 42)。 一开始,开发人员只能使用 VB.NET 作为编程语言。 超过 10 个框架版本之后,已经实现了很多。 今天,您可以在大量的语言、框架和技术之间进行选择。
一开始,InterDev 是开发 ASP Pages 的主要开发环境,您必须使用命令行 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 Pages Framework,提供了皮肤、模板、母版页和样式表等特性。
. net Framework 3.0 (WinFX)于 2006 年 11 月发布。 它包含了一组新的托管代码 api。 这个版本添加了一些新技术来构建新类型的应用,例如 Windows Presentation Foundation (WPF)、Windows Communication Foundation (WCF)、Windows Workflow Foundation (WWF)和 Windows CardSpace(后来集成到 Windows Identity Foundation 中)。
. net Framework 3.5 在一年后的 2007 年扩展了 WinFX 特性。 这个版本包含了 Linq、ADO 等关键特性。 净,ADO.NET 实体框架和 ADO。 净数据服务。 此外,它还附带了两个新的程序集,这两个程序集后来成为了 MVC 框架的基础:System.Web.Abstraction 和 System.Web.Routing。
.NET Framework 4.0 于 2009 年 5 月发布; 它为公共语言运行时(CLR)提供了一些主要的升级,并添加了 Parallel 扩展来改进对并行计算、动态分派、命名参数和可选参数的支持,以及代码契约和 BigIntegerComplex 数字格式。
在。net Framework 4.0 发布之后,微软发布了一系列改进,以 Windows Server AppFabric 框架的形式构建微服务。 本质上,它提供了一个 InMemory 分布式缓存和一个应用服务器场。
.NET Framework 4.5 于 2012 年 8 月发布; 它增加了一个所谓的 Metro 风格的应用(后来演变成通用 Windows 平台应用)、核心特性和微软扩展框架(MEF)。
关于 ASP.NET,这个版本更兼容 HTML5, jQuery,并提供捆绑和缩小,以提高网页性能。 它也是第一个支持 WebSockets 和异步 HTTP 请求和响应的。
.NET Framework 4.6.1 于 2015 年 11 月发布; 它需要 Windows 7 SP1 或更高版本,这是一个重要的版本。 其中的一些新特性和 api 包括对 AlwaysOn、Always Encrypted 的 SQL 连通性的支持,以及在使用 Azure SQL 数据库时改进的连接弹性。 它还为使用更新后的系统的分布式事务添加了 Azure SQL 数据库支持。 事务 api,并在 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 creator 更新中。 一些新特性包括椭圆曲线加密的增强加密和改进的传输层安全(TLS)支持,尤其是在版本 1.2 中。 它还引入了对象缓存存储,使开发人员能够通过实现 ICacheStoreProvider 接口轻松地提供自定义提供者。
在应用和内存监视器之间还有一个更好的集成,并且著名的内存限制了反应,这使得开发人员能够在 CLR 截断内存中缓存的对象并覆盖默认行为时观察到它。
然后,微软从一开始就考虑到开源多平台,开发了一个全新的。net 框架。 它被介绍为 ASP.NET 5,后来重命名为 ASP.NET Core 框架。
2016 年 6 月,理查德•兰德(Richard Lander)发布了第一个版本 1.0; ASP.NET MVC 和 Web API 框架合并到一个单一的框架包中,您可以通过 NuGet 轻松地添加到您的项目中。
第二版,.NET Core Framework 1.1,于 2017 年 11 月发布; 它运行在更多的 Linux 发行版上,性能得到了提高,它与 Kestrel 一起发布,在 Azure 上的部署得到了简化,生产力得到了提高。 Entity Framework Core 开始支持 SQL Server 2016。
请注意。net Core Framework 1.0 和 1.1 将被微软支持到 2019 年 6 月。
. NET Core 框架的最新版本是 2.0。 首个预览版于 2017 年 5 月发布。 第二版预览版于 2017 年 6 月出版,也是本书的最终版,已于 2017 年 8 月出版。
微软已经极大地改进了。NET Core 框架。 这些改进和扩展是。net Core 2.0 愿景的结果; 它使您能够在更多的地方使用更多的代码。
.NET Core 2.0 中包含了以下改进:
- 相对于。net Core 1.x,大量 API 增加(>100%)
- 支持。net 标准 2.0
- 支持引用。net 框架库和 NuGet 包
- 对 Visual Basic 的支持
此外,.NET Standard 2.0 带来了以下新特性:
- 更大的 API 表面——它扩展到涵盖。net 框架和 Xamarin 之间的交集。 这也使得。net Core 2.0 更大,因为它实现了。net Standard 2.0。 . net 标准增加的 api 总数约为 20000 个。
- 它可以引用现有的。net 框架库。 最好的办法是不需要重新编译,所以这包括现有的 NuGet 包。
- . net Core 支持更多的 Linux 发行版。 三星电子正在努力为移动操作系统 Tizen 提供支持。
- 最重要的是,. net Core 是。net 世界中最快的应用运行时。
另外,请注意大多数常规库都可以在 GitHub 上找到。 任何想要扩展或改变任何标准行为的人都可以分叉并重新构建它们。
这本书的内容
这本书被组织成解释 ASP 的多个章节.NET Core 2.0 特性以一种简单易懂的格式和实际示例呈现。 ASP 的大部分关键特性.NET Core 2.0 使用简洁、高效的例子和循序渐进的指令来说明立即产生的工作结果。
你不需要按任何顺序阅读章节就能发现这本书有用。 除了第一章,每一章都是独立的,第一章详细介绍了 ASP 的基本原理.NET core—如果您从未涉足过桌面应用开发之外的领域,那么您可能想先阅读一下它。
以下主题将贯穿全书:
第一章,什么是 ASP ? NET Core 2.0 ? ,描述了 ASP 的特点和功能.NET Core 2.0,但也有技术限制,这应该允许您了解在哪些情况下它可能很适合您自己的需求和期望什么。
第二章,环境设置详细说明了如何设置您的开发环境以及如何创建您的第一个 ASP.NET Core 2.0 应用。 您将学习如何使用 Visual Studio 2017 或 Visual Studio Code,如何安装运行时,以及如何使用 Nuget 检索所有必要的 ASP.NET Core 2.0 依赖。
第三章,在 VSTS 中创建一个连续集成管道,展示了如何建立一个完整的 Visual Studio Team Services (VSTS)连续集成管道。 您将学习如何使用云中的 VSTS 完全自动化构建、测试和部署应用。
第四章、ASP 的基本概念; NET Core 2.0 -第 1 部分,阐述了 ASP 的基本结构和概念.NET Core 2.0 应用。 它展示了所有内容在内部是如何工作的,以及可以使用哪些类和方法来覆盖基本行为。 同时也为其他章节提供了理论背景。
第五章、ASP 的基本概念; NET Core 2.0 -第 2 部分,在第 4 章、中介绍了 ASP 的基本概念.NET Core 2.0 -第 1 部分,本章将深入探讨 asp.net 的核心.NET Core 2.0 概念。 您将了解由 ASP 提供的组件和功能.NET Core 来构建响应式 web 应用。
第六章,创建 MVC 应用,提供了所有的概念和必要的一切来创建您的第一个 ASP.NET Core 2.0 MVC 应用。 您将学习 MVC 应用的细节,以及如何有效地实现它们。 此外,您将看到单元测试和集成测试如何帮助您构建 bug 更少的更好的应用,从而降低维护成本。
第七章,创建 Web API 应用,涵盖了 Web API 框架,并提供了创建您的第一个 ASP.NET Core 2.0 Web API。 您将看到不同的 Web API 样式,如 RPC、REST 和 HATEOAS,并了解何时使用它们以及如何以有效的方式实现它们。
第八章,访问数据使用实体框架核心 2,展示了如何访问数据库使用实体框架核心 2,在使用的所有高级功能(代码首先,流利的 API、数据迁移、InMemory 数据库,等等)提供。
第 9 章,保钉 NET Core 2.0 应用,说明了如何使用内置的 ASP.NET Core 2.0 的用户身份验证特性,以及如何通过添加外部提供者来扩展这些特性。 如果你需要保护你的应用,那么这一章就是你想去的地方。
第十章,ASP 的托管和部署 NET Core 2.0 应用,是关于当托管和部署您的 ASP. NET Core 2.0 应用时,您所拥有的各种选项.NET Core 2.0 web 应用。 您将了解如何为给定的用例选择适当的解决方案,这将允许您为自己的应用做出更好的决策。
第十一章、管理和监督 ASP.NET Core 2.0 Applications,最后将是关于如何管理和监督部署后的产品应用的一章。 它将极大地帮助您诊断您的 ASP 问题.NET Core 2.0 web 应用在运行时,减少了理解和修复 bug 的时间。
你写这本书需要什么
你将需要 Visual Studio 2017 社区版或 Visual Studio Code,这两者都是免费的测试和学习目的,能够遵循本书中的代码示例。 您还可以使用您选择的任何其他文本编辑器,然后使用 dotnet 命令行工具,但是建议使用前面提到的开发环境之一,以提高生产率。
在本书的后面,我们将使用数据库,所以你也需要一个版本的 SQL Server(任何版本中的任何版本都可以)。 我们建议使用 SQL Server 2016 Express Edition,测试也是免费的。
在接下来的章节中可能会介绍其他的工具或框架。 我们将解释当它们被使用时如何检索它们。
如果你需要为 Linux 开发,那么 Visual Studio Code 和 SQL Server 2016 是你的主要选择,因为它们是唯一运行在 Linux 上的。
此外,你还需要 Azure 订阅和 Amazon Web 服务订阅来阅读书中展示的一些例子。 有多个章节专门向您展示如何利用云。
这本书是给谁的
这本书是为那些想要用 ASP 构建现代 web 应用的开发人员编写的。 2.0 NET Core。 没有 ASP 的先验知识.NET 或。NET Core 是必需的。 但是,需要具备基本的编程知识。 此外,以前的 Visual Studio 经验会有所帮助,但不是必需的,因为详细的说明将指导您通过本书的示例。 本书还可以帮助从事基础设施工程和操作的人员在 ASP 运行时监视和诊断问题.NET Core 2.0 web 应用。
约定
在这本书中,你会发现许多不同的文本样式来区分不同种类的信息。 下面是这些风格的一些例子以及对它们含义的解释。
文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 url、用户输入和 Twitter 句柄如下所示:“启动 Visual Studio 2017,打开Tic-Tac-ToeASP。 您创建的 NET Core 2.0 项目,创建三个名为Controllers、Services和Views的新文件夹,并在Views文件夹中创建名为Shared的子文件夹。
一段代码设置如下:
[HttpGet]
public IActionResult EmailConfirmation (string email)
{
ViewBag.Email = email;
return View();
}
任何命令行输入或输出都写如下。 为了提高可读性,输入命令可能被分成几行,但需要在提示符中连续输入一行:
sudo apt-get install code
新词语、重要词语以粗体显示。 您在屏幕上看到的单词,例如菜单或对话框中,会出现如下文本:“打开 Visual Studio 2017,转到 Team Explorer 选项卡,并单击 Branches 按钮”。
Warnings or important notes appear like this. Tips and tricks appear like this.
读者的反馈
我们欢迎读者的反馈。 让我们知道你对这本书的看法——你喜欢或不喜欢这本书。 读者反馈对我们来说很重要,因为它能帮助我们开发出你能真正从中获益最多的游戏。
要向我们发送一般性的反馈,只需发送电子邮件feedback@packtpub.com,并在邮件的主题中提到这本书的标题。
如果有一个主题,你有专业知识,你有兴趣写或贡献一本书,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在,你已经自豪地拥有了一本书,我们有一些东西可以帮助你从购买中获得最大的好处。
下载示例代码
您可以从您的帐户http://www.packtpub.com下载本书的示例代码文件。 如果您在其他地方购买了这本书,您可以访问http://www.packtpub.com/support并注册,将文件直接通过电子邮件发送给您。
你可以按以下步骤下载代码文件:
- 使用您的电子邮件地址和密码登录或注册我们的网站。
- 将鼠标指针悬停在顶部的 SUPPORT 选项卡上。
- 点击代码下载和勘误表。
- 在搜索框中输入书名。
- 选择您想要下载代码文件的书。
- 从下拉菜单中选择你购买这本书的地方。
- 点击代码下载。
下载文件后,请确保使用最新版本的解压或解压缩文件夹:
- WinRAR / 7-Zip for Windows
- 泥柱/学校不洁净
- 7-Zip / PeaZip for Linux
该书的代码包也托管在 GitHub 上的以下存储库:
- https://github.com/JasonDeOliveira/Learning-ASP.NET-Core-2.0/commits/master
- https://github.com/PacktPublishing/Learning-ASP.NET-Core-2.0
我们还可以在https://github.com/PacktPublishing/中找到丰富的图书和视频目录中的其他代码包。 检查出来!
勘误表
尽管我们已经竭尽全力确保内容的准确性,但错误还是会发生。 如果你在我们的书中发现错误,也许是文本或代码上的错误,如果你能向我们报告,我们将不胜感激。 通过这样做,您可以使其他读者免受挫折,并帮助我们改进这本书的后续版本。 如果您发现任何勘误表,请访问http://www.packtpub.com/submit-errata,选择您的图书,点击勘误表提交表格链接,并输入您的勘误表详细信息。 一旦您的勘误表被核实,您的提交将被接受,勘误表将被上载到我们的网站或添加到该标题的勘误表部分下的任何现有勘误表列表中。
要查看之前提交的勘误表,请访问https://www.packtpub.com/books/content/support并在搜索字段中输入书名。 所需信息将出现在勘误表部分。
盗版
在互联网上盗版受版权保护的材料是一个贯穿所有媒体的持续问题。 在 Packt,我们非常重视版权和授权的保护。 如果您在网上发现我们的作品以任何形式的非法拷贝,请立即提供我们的地址或网站名称,以便我们进行补救。
请通过copyright@packtpub.com与我们联系,并提供疑似盗版资料的链接。
我们感谢您的帮助,保护我们的作者和我们的能力,为您带来有价值的内容。
问题
如果您对本书的任何方面有任何疑问,您可以通过questions@packtpub.com与我们联系,我们将尽力解决问题。
一、什么是 ASP.NET Core 2.0 ?
第一个预览版的 ASP.NET 在大约 15 年前作为。NET 框架的一部分出现。 从那时起,数以百万计的软件开发人员已经使用它来构建和运行所有类型的伟大的 web 应用。 多年来,微软增加并发展了许多特性,直到对 ASP 进行了彻底的重新设计。 asp.net 框架命名为 NET Core2016 年 6 月。 在 ASP.NET Core 1.0 和 1.1,2.0 版本是 asp.net 的第三个也是最新的一部分。 净的核心。 让我们看看它提供了什么,以及什么时候在项目中使用它是有意义的。
ASP.NET Core 2.0 是一种新的开源跨平台框架,用于构建基于云的现代应用,如 web 应用、物联网(物联网)应用甚至移动后端。
ASP.NET Core 2.0 应用既可以运行在。NET Core Framework 上,也可以运行在完整的。NET Framework 上。 ASP.NET Core 框架的架构是为了为应用提供一个优化的开发框架,这些应用必须部署在云或本地。 它由开销最小的模块化组件组成,因此在构思和实现您的软件解决方案时,您可以保持高度的灵活性。 您可以开发和运行您的 ASP。 Windows、Linux 和 macOS 上的 NET Core 2.0 应用。
在下面的图表中,你可以看到不同的。net 框架版本和组件是如何一起工作的:

ASP。 与之前的框架相比,NET Core 2.0 包含了一些架构上的变化,从而产生了一个更精简、更模块化的框架。 它不再基于System.Web.dll,相反,它使用一组粒状的、经过良好分解的 NuGet 包。 这允许优化应用,只包括真正需要的 NuGet 包。
较小的应用表面积的好处包括:
- 更好的安全性
- 减少组件之间的依赖关系
- 提升的性能
- 降低了“按使用付费”云消费者世界的优化财务成本
作为一名开发人员,当基于经典的。net 框架构建应用时,您必须在 6 个应用模型(WPF、Windows Forms、Web Forms、Web MVC、Web API 和 Xamarin)之间进行选择,这可能会令人困惑,而且效率不高。
随着 ASP 的发布.NET Core 1.0 和 1.1,它被优化并缩减为三个不同的应用模型,缺点是你不能在它们之间共享代码。
ASP.NET Core 2.0 中,应用模型的数量进一步减少到两个,代码现在是可共享的,这意味着您现在可以重用 90%以上的代码。 对于作为开发人员的您来说,这使您的工作效率更高,并且允许在应用模型之间快速、轻松地切换。
在本章中,我们将涵盖以下主题:
- ASP.NET 2.0 特性
- 跨平台的支持
- Microservice 架构
- 使用 Docker 和容器
- 性能和可伸缩性
- 并排的部署
- 技术的限制
- 什么时候选择 ASP。 2.0 NET Core
ASP.NET Core 2.0 特性
新的Microsoft.AspNet.Core.All包包含所有的 ASP.NET Core 2.0 特性在单个库中。 它包括身份验证、MVC、Razor、监控、Kestrel 支持等等。 在书的后面会有更详细的解释。
Note that if you want to selectively add packages one by one, you can still reference them manually instead of using the single packages that contain it all but then you will miss several advantages as you will see here.
运行库是 ASP 附带的一个重要的新组件。 2.0 NET Core。 它包含使用本机语言编译的编译包,这是提高性能的关键。 所有使用Microsoft.AspNet.Core.All包的应用都可以从中受益,因为它们不再需要使用所有依赖的包进行部署。 一切都已经准备好了,所以它们的部署规模将会减少,它们的执行时间将会优化。
ASP.NET Core 2.0 允许您创建符合模型-视图-控制器(MVC**)模式的良好分解和可测试的 web 应用。 我们在本书后面用了整整一章来讨论这个话题。
此外,还可以使用自定义和内置的格式化程序(如 JSON 或 XML)以及 RESTful 服务来构建完全支持内容协商的 HTTP 服务。
ASP.NET Core 2.0 完全支持 Razor,它包含了一种用于创建视图的高效语言,而 Tag Helpers 使服务器端代码能够参与到 Razor 文件中创建和呈现 HTML 元素的过程中。
模型绑定自动将来自 HTTP 请求的数据映射到操作方法参数,模型验证自动执行客户端和服务器端验证。
在客户端开发方面,ASP.NET Core 2.0 旨在与各种客户端框架无缝集成,包括 AngularJS、KnockoutJS 和 Bootstrap。
此外,它还提供了以下基本改进:
- ASP.NET MVC 和 Web API 被组合成一个单一的框架
- 现代客户端框架和开发工作流
- 为云托管准备的基于环境的配置系统
- 内置依赖注入功能
- 新的轻量级和模块化 HTTP 请求管道
- 在 IIS、self-host、Docker、Cloud 甚至自己的进程中托管相同的应用
- 并排托管一个应用或组件的多个版本
- 完全以 NuGet 包的形式发布
- 简化现代 web 开发的新工具
- 简化了
csproj文件,使其更容易与 Visual Studio 以外的开发环境一起工作(例如,在 Linux 和 macOS 上) - 已经扩展了
Program.cs类,使 Kestrel 的集成、ContentRootPath的设置、加载配置文件、初始化日志中间件和其他步骤完全自动化,只需要调用一个方法 - 通过将日志记录和配置移到 WebHost 构建器初始化中,已经简化了
Startup.cs
跨平台的支持
如前所述,ASP.NET Core 2.0 框架从一开始就考虑到了跨平台支持。 它支持多种操作系统和技术,如 Windows、Linux、macOS、Docker、Azure 等。
ASP.NET Core 2.0 目前支持以下 Linux 发行版:
- Ubuntu 14 日 16
- Linux Mint 17, 18
- Debian 8
- Fedora
- CentOS 7.1 和 Oracle 7.1
- SUSE Enterprise Server 64 位
- OpenSuse 64 bits
关于 macOS,它目前只支持(稍后可能会添加其他版本):
- macOS 10 11
- macOS 10。
对于应用开发,您可以使用 Visual Studio 或 Visual Studio Code 在 Windows 上进行开发,然后部署您的 ASP.NET Core 2.0 应用到目标系统。
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 Code、VIM/VI、Sublime 或 Emacs 等。 在 macOS 上,你可以使用 Visual Studio for Mac、Visual Studio Code 或任何其他 Mac 特定的文本编辑器。
不过,Visual Studio 2017 或 Visual Studio Code 开发环境将是首选,因为它们提供了实现高生产率、调试和理解代码以及轻松导航所需的一切。 这就是为什么我们将在本书的其余部分中使用这些 ide。
在构建应用之后,您可以使用多个 web 服务器来运行它。 下面是一些例子:
- Apache
- IIS
- 红隼 self-host
- Nginx
Microservice 架构
微服务也称为微服务体系结构,是一种体系结构布局,它将应用结构为松散耦合的服务集合,这些服务实现业务功能。 它可以用于构建电子商务系统、业务应用和物联网。
ASP。 当您想采用这种系统架构时,NET Core 2.0 是最好的选择。 ASP.NET Core 2.0 框架是轻量级的,它的 API 表面可以最小化到特定微服务的范围内。 微服务体系结构还允许您跨服务边界混合使用技术,从而能够逐步过渡到 ASP。 净的核心。
注意,微服务是用 ASP 构建的.NET Core 2.0 可以与其他技术(如完整的经典。NET Framework、Java、Ruby,甚至其他更多的遗留技术)协同工作。 当您需要逐步将单块应用转换为更(微)的面向服务的应用时,这是一个很大的优势。
您没有被绑定到特定的底层基础设施,相反,您有广泛的选择,因为 ASP.NET Core 2.0 几乎支持你现在能想到的所有技术。 此外,您可以在需要时修改基础设施,这样就不会对基于基础设施开发的应用产生技术锁定。
要高效地编排和管理用 c#编写的、大规模的、本地的和云端的微服务,你的主要选择应该是 Microsoft Service Fabric。 它正是为此而构思的,微软已经在各种 Azure 服务(SQL 数据库等)中使用了很多年。
微服务 Docker 容器方法可能也适合你的需求,我们将在下一段解释它的用例。 综上所述,ASP.NET Core 2.0 是在任何技术环境中实现和托管微服务的理想选择。
使用 Docker 和容器
码头工人和集装箱现在到处都是。 每个人都在谈论它们,而且它们似乎非常适合很多用例。 它们提供了一种高效、轻量级和自包含的方法,用于在重用底层操作系统文件和资源的同时,将应用及其依赖项打包。
它们非常适合微服务体系结构,但也可以用于任何其他应用原型。 它们与 ASP 配合得非常好.NET Core 2.0 应用,因为它们都考虑到了模块化、性能、可伸缩性、轻量级性质和效率。
Note that Docker container images including ASP.NET Core 2.0 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 2.0 框架,提供完整的跨平台支持(Windows、Linux 和 macOS)。 此外,您可以在本地和云中托管容器。 例如,您可以通过IAAS部署或通过 Azure 容器服务(后者还允许混合和匹配不同的操作系统和技术)来使用 Azure。
性能和可伸缩性
如果你需要最好的性能和支持高可伸缩性的场景,那么你绝对需要使用 ASP.NET Core 2.0 和底层的。NET Core Framework。 ASP.NET Core 2.0 是为高性能和高可伸缩性场景而从头构建的。 它在这些领域确实很出色,可以被认为是最好的选择。
它比经典的 ASP 快十倍.NET,你甚至可以认为它是目前。NET 世界中最快的 web 应用运行时!
此外,它还为微服务体系结构提供了最佳解决方案,在微服务体系结构中,性能和可伸缩性非常重要。 没有其他技术能在消耗如此低的系统资源的情况下如此高效,这也降低了基础设施和云托管成本。
并排的部署
如果您希望能够安装依赖于不同版本的。net Framework 的应用,那么您应该考虑使用 ASP.NET Core 2.0 框架,因为它提供了 100%的并行部署功能。
并排部署不同的。net Core 和 ASP.NET Core 版本允许在同一台服务器上拥有多个服务和应用。 它们都可以使用各自框架的专用版本,从而在进行应用升级和常见 IT 操作时消除风险并节省资金。
技术的限制
请仔细查看本节中显示的技术。 如果您在当前的应用中使用了一种技术或框架(在这里列出了并且还不受支持),那么您可能会发现很难甚至不可能迁移到 ASP。 2.0 NET Core。
并不是所有的。net 框架技术都可以在 ASP 中使用.NET Core 2.0 和一些可能永远不会被移植,因为它们不符合新的。NET Core 特定的范例和模式。
下面的列表显示了在 ASP 中不能直接找到的最常见的技术.NET Core 和。NET Core,知道一些可以通过多目标特性使用:
- NET Web Forms 应用:传统的 Web Forms 技术只能使用完整的经典。NET Framework,不能使用 asp.net Framework.NET Core 和。NET Core 用于这些类型的应用。
- asp.net Web Pages 应用:它们不包含在 asp.net 中.NET Core 2.0 是这样的,但也可以使用 Razor 网页引擎来提供相同的功能。
- NET SignalR 应用:目前,ASP. NET SignalR 应用 NET SignalR 不能用于 ASP。 净的核心。 但是,您可以在相应的服务器端和客户端库 GitHub 存储库中找到第一个预览版本,因此它们应该包含在下一个版本中。
- WCF Services: NET Core 2.0 包含一个用于访问 WCF 服务的 WCF 客户端,但不支持创建 WCF 服务。 不过,该特性可能会在未来的版本中添加。
- Workflow Services:不支持 Windows Workflow Foundation、Workflow Services 和 WCF Data Services,也没有将它们添加到 ASP 的计划.NET Core 在未来。
- WPF 和 Windows 窗体应用:Windows Presentation Foundation 和 Windows 窗体不能用 ASP 构建.NET Core,它会违背跨平台范式。 但是,您可以将 WPF 应用替换为 XAML2 通用标准提供的 UWP 应用。
并不是所有的。net 语言目前都被 ASP。 2.0 NET Core。 例如,f#没有任何工具支持。 Visual Studio 2017 最新版本增加了对 Visual Basic 的支持。 将会有越来越多的语言被支持。
除了官方的 ASP.NET Core 路线图,还有其他框架和技术,计划在接下来的几个月里移植到。NET Core 上。 要获得更多关于哪些将被移植,哪些不会被移植的信息,请访问。NET Core 库的 GitHub 库(https://github.com/dotnet/corefx)。
但对于那些计划中的项目,并不能保证它们真的会被移植过来。 但是,您将会发现一个很好的指示,说明您可以在 ASP 的下一个版本中期待什么。 净的核心。 注意,在某些情况下,您可以使用 ASP 的多目标特性.NET Core 2.0 能够调用目前不被 ASP 直接支持的框架。 2.0 NET Core。
如果您关心项目中需要的特定框架或组件,请考虑参与 GitHub 上的讨论。 也许其他人也会有同样的需求,微软决定相应地优先考虑他们的。net Core 迁移。
一些微软服务,甚至一些第三方平台,不支持 ASP。 净的核心。 例如,一些 Azure 服务,如服务结构有状态的可靠服务和服务结构可靠角色需要完整的经典。net 框架。
同样,有时 ASP.NET Core sdk 还没有提供或尚未可用。 同时,您总是可以使用等价的 REST api 来代替客户端 sdk,然后在以后替换它们。 请放心,所有 Azure 服务都将支持 ASP。 在各自的产品路线图中可以看到。
什么时候选择 ASP。 2.0 NET Core
在了解了 ASP 提供的各种特性和功能之后.NET Core 2.0,你可以问问自己,它是否会在未来取代完全经典的。NET 框架。 ASP.NET Core 2.0 和底层的。NET Core Framework 提供了一些主要的增强和性能改进,但仍有一些特定的场景,这些新的应用模式并不适用,而完整的。NET Framework 将是最好的,有时甚至是唯一的选择。
将整个现有的应用迁移到 ASP。 从一开始就实现 NET Core 可能是困难的,甚至是不可能的。 您应该考虑如何逐步地转换您的应用,以降低失败或过于复杂的风险,并给自己时间来真正理解新的模式和范例。
你可以只使用 ASP 来启动实例.NET Core 2.0 用于所有新的开发,然后看看以后如何迁移您的遗留代码,有时甚至让它保持不变,因为迁移它不会有真正的好处。 如果您真的对迁移主题感兴趣,请考虑附录,因为我们有一个完整的章节专门讨论这个重要的主题。
ASP.NET Core 和。NET Core Framework 每天都得到越来越多的框架和客户端库支持。 微软、工具和框架供应商以及不同的开发人员社区都在努力提供大量的功能,以支持功能丰富和高性能的 web 应用。 每个人都想致力于这项有前途的技术,它可以以一种可持续的方式塑造未来。
在使用。net Standard 2.0 时,同时使用。net Core 和。net Framework 库的可能性进一步扩展了这种可能性,并为开发人员提供了一个临时的解决方案,直到每个重要的特性和主要的框架都可以在。net Core 中使用。
回顾一下本章所讨论的内容,你应该使用 ASP。 当您的服务器应用:
- 你有跨平台的需求
- 你的目标是微服务
- 您希望使用 Docker 容器
- 您需要高性能和高度可伸缩的应用
- 您需要将具有不同。net 版本的多个应用并排放置
- 本文介绍的技术限制不适用于您的应用要求
总结
在本章中,你已经学习了 ASP.NET Core 2.0 框架及其特性。 您已经看到,它包含了在使用 Docker 等微服务架构和容器技术时在跨平台环境中高效工作所必需的一切。
此外,您已经了解到它为您的 web 应用提供了非常好的性能和卓越的可伸缩性,甚至支持并排部署。
最后,我们讨论了技术上的限制以及什么时候使用 ASP 是可取的.NET Core 2.0 框架。
在下一章中,我们将讨论如何设置您的开发环境,包括 Visual Studio 2017 或 Visual Studio Code 作为集成开发环境。**
二、设置环境
你已经决定学习 ASP.NET Core 2.0 是目前市场上最先进、最高效的跨平台 web 应用框架。 非常好的选择! 您肯定急于马上开始编程,但在我们开始之前,我们必须设置所需的技术先决条件和工具。
在本章中,我们将介绍 Visual Studio 2017 社区版和 Visual Studio Code,然后安装它们作为开发环境。 然后,我们将构建一个简单的基于 ASP 的示例应用.NET Core 2.0 框架。
在本章中,我们将涵盖以下主题:
- Visual Studio 2017 作为开发环境
- 如何安装 Visual Studio 2017 社区版
- 创建您的第一个 ASP.NET Core 2.0 应用在 Visual Studio 和通过命令行
- Visual Studio Code 作为开发环境
- 如何在 Linux 上安装 Visual Studio Code
- 创建您的第一个 ASP.NET Core 2.0 应用在 Visual Studio Code
- 创建您的第一个 ASP。 Linux 中的 NET Core 2.0 应用
Visual Studio 2017 作为开发环境
作为一名开发人员,您需要一个用于日常开发任务的环境,而 Microsoft Visual Studio 2017 正是如此。 它提供了一个非常高效和高效的集成开发环境(IDE),用于创建新的软件项目并开发、调试和测试它们。 它将帮助您以非常快速和直观的方式构建高质量的应用。 它的许多特性都是围绕共同的开发任务以及如何在一个工具中简化和优化它们而构建的。
您可以创建 web 应用,web 服务,桌面应用,移动应用,和许多其他类型的应用没有涵盖在这本书中。
此外,您可以使用广泛的编程语言,如 c#、Visual Basic、f#、JavaScript,甚至 Java。
Visual Studio 2017 有不同的版本,每个版本都有自己独特的功能和授权。 例如,Visual Studio 2017 社区版是免费的,但不能用于在生产环境中运行的应用。 这个版本的主要目的是私人使用和学习目的。
Visual Studio 2017 专业版和企业版包含了在生产环境中构建和运行应用所需的一切,包括必要的许可证。
Visual Studio 2017 专业版包含了企业版中提供的所有功能的子集。 通常从这个版本开始,然后在必要时升级到企业版就足够了。
Visual Studio 2017 Enterprise Edition 包含了许多额外的特性,以进一步提高开发人员的工作效率,比如实时依赖验证、测试、架构图、架构验证、代码克隆等等。 如果您需要这些功能,那么您就需要使用这个版本。
Note that multiple versions of Visual Studio (2013, 2015, 2017, 2017 Preview, and more) can be installed side by side on a developer machine, which has earlier versions of the Visual Studio IDE installed.
传统上,Visual Studio 只针对 Windows 发布,但自 2016 年以来已经出现了一个 macOS 版本,称为针对 macOS 的 Visual Studio。 您可以使用它在这个操作系统上开发。net 应用。
Visual Studio 2017 社区版正是我们尝试和理解这本书中的例子所需要的,所以这就是为什么我们将在接下来的章节中使用这个版本。
如何安装 Visual Studio 2017 社区版
Visual Studio 2017 社区版的安装与任何其他 Windows 应用一样。
Note however that you need administrator rights during the installation. These rights will not be required when developing with Visual Studio later.
对于 Visual Studio 2017 社区版安装,您可以选择以下三种不同的 Visual Studio 2017 安装模式:
- 快速安装以一种简单快捷的方式安装所有被微软视为默认组件的组件。 如果您需要此列表中没有的特定 Visual Studio 特性,则需要使用自定义安装。
- 自定义安装为您提供了所有可以安装的 Visual Studio 2017 特性的完全选择。 例如,您可以安装互补的功能,如 Visual c++、f#、SQL Server 数据工具、移动平台和其他几个 sdk,以及特定的语言包。
- 在使用脱机安装时,可以在没有任何网络连接的情况下安装 Visual Studio 2017。 当您无法连接到互联网,但仍然希望准备一台开发人员机器时,这是非常方便的。 在这种情况下,您必须准备一个外部支持,例如移动硬盘或 USB key,并事先将 Visual Studio 2017 安装程序文件放在其中。
准备这种外部支持的一种方法是从 Visual Studio 网站https://www.visualstudio.com/downloads/下载必要的 Visual Studio 安装程序(社区版、专业版或企业版),并将其内容解压到一个文件夹中。 然后,通过在命令行窗口中执行命令<executable name> --layout来检索各种安装包。 一段时间后,所有内容都被下载,您就有了一个可以用于离线安装的外部支持。
Note that you can use the same procedure to download all of the installation files to a central network storage and then create a shared folder for being able to install Visual Studio 2017 from within your own network to optimize installation times and lower network bandwidth needs.
现在我们来看看如何使用之前提到的 Microsoft Visual Studio 网站上下载的安装程序手动安装 Visual Studio 2017 社区版:
- 启动 Visual Studio 2017 Community Edition 安装程序,您将看到各种可安装工作负载的列表。 默认情况下,你会看到 Windows,网页和云,手机和游戏,以及其他工具:

-
选择您想要的组件,它们将在接下来的步骤中安装。 如果这就是你所需要的,那么就没有其他事情可做了。 如前所述,这是快速安装。
-
如果您需要自定义安装的组件,以添加或删除单个组件,那么您必须单击单个组件。 显然,你将做什么是所谓的自定义安装:

-
When you have finished selecting your desired workloads and components, the installation will start. The installation time is dependent on the number of workloads and components you have selected, as well as your internet connection speed, if you are not using the Offline Installation method described previously:
![]()
对于更高级的场景,比如自动化和脚本化 Visual Studio 2017 安装,您可以通过命令提示符启动安装程序。 有各种各样的命令行参数,它们有助于定义需要在哪里安装什么。
下面是一些命令行参数的列表,并对它们的作用进行了简要描述。 请访问https://docs.microsoft.com/en-us/visualstudio/install/use-command-line-parameters-to-install-visual-studio以获得更多信息,以及所有现有命令行参数的完整列表:
| 参数 | 描述 |
| /AddRemoveFeatures | 这将添加所选的特性 |
| /AdminFile | 它指定要以静默方式安装的文件 |
| /CreateAdminFile | 这指定在安装后生成静默响应文件 |
| /CustomInstallPath | 这将指定目标路径 |
| /ForceRestart | 这会迫使你的电脑重新启动 |
| /Full | 这将安装所有的特性 |
| /noweb | 这将禁用互联网搜索功能和下载 |
| /ProductKey | 这指定要使用的键 |
Visual Studio 2017 的第一步
在安装 Visual Studio 2017 之后,您现在可以探索它为提高开发人员的生产力所提供的一切。 下面是提供的一些特性的列表。
打开 Visual Studio 2017,你首先看到的是 Visual Studio 起始页。 默认情况下,它会显示一个 Get Started 部分,其中包括帮助主题列表、你最近工作的项目历史、开发人员和社区新闻 feed,以及一些常见开发人员任务的快捷方式,比如创建或打开项目:

开始页是完全可定制的,所以如果你不想看到新闻部分,例如,包含来自微软官方频道的开发者新闻,那么你只需要关闭折叠列表或完全删除它。 您可以对起始页进行更多的定制,如果您有兴趣的话,您可以在 MSDN 的https://msdn.microsoft.com/en-us/library/ff425532.aspx中查找详细信息。
Visual Studio 最重要的特性之一是智能感知。 它通过提供列表成员、参数信息、快速信息和完整的 Word 等特性来帮助开发人员提高工作效率。 它在 Visual Studio 2017 中得到了改进,加入了一些非常有趣的新特性,因为现在可以根据类型(类、名称空间或关键字)和 CamelCase 搜索进行过滤。
现在也可以从结果列表中选择最匹配的结果,而不是只选择最前面的一个:

Visual Studio 2017 的代码重构和实时代码分析功能加速了开发,并确保代码的可读性和可维护性。 例如,您可以自动添加缺失的命名空间或删除不必要的命名空间:

下面是一个代码重构建议的例子:

顾名思义,Find All References 特性允许开发人员轻松快速地查找方法或对象的所有引用。 着色、分组和 Peek Preview 功能在视觉上帮助你更好地在代码中导航,并真正帮助理解它:

Peek Definition 和 Go to Definition 功能用于在弹出窗口中检查方法、接口或类的定义,而不更改当前窗口,或者直接打开包含有请求定义的源代码的文件。 Go To Implementation 功能做同样的事情,但导航到实现:

另外一个重要的特性,也是我们最喜欢的特性之一,是实时单元测试。 它需要 Visual Studio 2017 Enterprise Edition,并允许您在每次修改或编译代码后自动在后台运行单元测试。 可以在测试设置中配置和激活它。 例如,您可以设置测试进程的数量、每次测试的最大持续时间和最大内存完成:

Visual Studio 2017 中有许多更有趣和令人兴奋的特性,我们邀请您访问 Visual Studio 官方网页https://docs.microsoft.com/en-us/visualstudio/welcome-to-visual-studio了解更多细节。 对于开发人员来说,尽可能了解自己的开发 IDE 并熟悉它的许多特性是非常关键的,这些特性可以帮助他更好更快地完成工作。 因此,在开始开发应用之前,一定要花些时间研究一下这个问题。
创建您的第一个 ASP。 Visual Studio 2017 中的 NET Core 2.0 应用
您已经耐心地阅读了前面的章节,了解了通过阅读这本书将学到什么,并为您的开发机器做好了准备。 现在,您已经准备好创建第一个示例应用。
让我们看看创建第一个 ASP 的不同选项.NET Core 2.0 应用的更多细节。
在 Visual Studio 2017 中创建新项目时,首先看到的是模板资源管理器,它显示一个树形视图,用于在已安装模板、特定于语言的模板和在线模板之间进行选择。
在树视图中选择模板源之后,将显示不同的模板。 ASP.NET Core,你可以看到控制台应用,类库,单元测试项目,xUnit 测试项目和 ASP.NET Core Web 应用.NET Core),例如。
由于有些模板集成了多种应用类型,因此有时必须做出额外的选择来指定要创建的应用的确切类型。 这就是 ASP 的情况.NET Core web 应用,因为你必须在空的、web API、web 应用或 web 应用(Razor Pages)项目模板之间进行选择。 此外,您可以启用 Docker 支持,并更改认证模式,在不认证、个人用户帐户、工作或学校帐户或 Windows 认证之间切换。
下面是创建第一个 ASP 的逐步说明.NET Core 2.0 示例 web 应用:
- 如果尚未安装。net Core 2.0 SDK,请从https://www.microsoft.com/net/core/preview下载并安装。net Core Preview 2。
Note that this step might no longer be needed at the time of reading this book, since it should have been released officially by then.
- 启动 Visual Studio 2017。
- 创建一个新项目通过点击文件|新|项目:

- 选择作为项目模板 Visual c# |。net Core | ASP.NET Core Web 应用。 核心网):

- 现在您可以选择特定的 web 应用类型。 选择 Web 应用(剃刀页面),并保持 Docker 支持(禁用)和认证(不认证)选项不变:

Note that at the time of the elaboration of this book, only Visual Studio 2017 Preview 15.3 had support for ASP.NET Core 2.0. It should be included in the standard version, though, and at the time of publication.
- 示例应用项目生成之后,将显示一个项目开始页面。 在这里,您可以配置其他选项,如连接服务(Application Insights 等)和发布目标(Microsoft Azure App services、IIS、FTP、Folder 等)。 让一切保持不变:

- 您现在可以通过按F5或单击 Debug |开始调试来开始调试您的应用:

创建您的第一个 ASP.NET Core 2.0 应用通过命令行
在上一节中,您看到了如何创建您的第一个 ASP.NET Core 2.0 样例应用与 Visual Studio 2017,这应该是大多数常见开发人员的首选方法。
然而,如果你更喜欢使用命令行或 Visual Studio Code(我们将在本书稍后介绍),那么使用 Visual Studio 2017 并不是一个真正的选项。 幸运的是。net Core 和 ASP.NET Core 2.0 提供了对命令行的全面支持。 这甚至可能是您在其他操作系统(如 Linux 或 macOS)上的唯一选择。 相同的命令行指令可以在所有不同的操作系统上工作,因此,一旦您习惯了它们,就可以在任何环境上工作。
现在让我们看看如何使用 Windows 命令行创建你的第一个示例应用:
- 如果尚未安装。net Core 2.0 SDK,请从https://www.microsoft.com/net/core/preview下载并安装。net Core Preview 2。
Note that this step might no longer be needed at the time of reading this book, since it should have been released officially by then.
- 为示例应用创建一个文件夹
mkdir aspnetcoresample。 - 移动到创建的文件夹
cd aspnetcoresample。 - 基于空 ASP 创建一个新的 web 应用.NET Core 2.0 web 应用模板,
dotnet new web。
Previous versions of .NET Core required an additional -t parameter for choosing the 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.
Note that you can verify your .NET version by entering dotnet (with no parameters) if you are not sure about your environment, since it will display the current .NET Core version.
- 通过执行
dotnet run运行示例应用:

- 打开浏览器,进入
http://localhost:5000。 如果一切正常,您应该会看到一个 Hello World! 页面:

您已经了解了如何使用 Visual Studio 2017 或命令行创建第一个示例应用。 现在,您将了解如何使用 Visual Studio Code,以及它如何帮助您构建 ASP。 Linux 或 macOS 上的 NET Core 2.0 应用。
Visual Studio Code 作为开发环境
Visual Studio Code 是一个轻量级的、强大的跨平台开发环境,适用于 Windows、Linux 和 macOS。
你可以通过语言和运行时扩展使用各种编程语言,如 JavaScript、TypeScript 和 Node.js,以及 c++、c#、Python、PHP、Go 和。net Core 和 Unity 运行时。
它有一个流线型的,干净的,非常高效的用户界面。 左边有一个文件和文件夹管理器,右边有一个源代码编辑器,显示你已经打开和当前正在处理的文件的内容:

用户界面包括以下几个方面:
- 活动栏:提供几个不同的视图和额外的上下文特定的指示器,如在启用 Git 时输出代码的更改。
- 侧边栏:包含用于处理项目的文件和文件夹资源管理器。
*** 编辑器组:这是处理代码并在其中导航的主要区域。 最多可以同时并排打开三个源代码编辑器窗口。* Panels:用于显示带有输出或调试信息、错误和警告的面板,或集成终端。* 状态栏:关于已编辑的项目和文件的附加信息。**
**请访问https://code.visualstudio.com/docs获取更多关于 Visual Studio Code 及其功能和功能的信息。 这将是我们的主要选择来说明如何建立 ASP。 Linux 上的 NET Core 2.0 应用。
如何在 Linux 上安装 Visual Studio Code
现在我们将解释在 Linux 上安装 Visual Studio Code 是多么容易和快速。 最流行的 Linux 发行版之一 Ubuntu 16.04 就是一个例子。
如果您没有可用的 Linux Ubuntu 的物理或虚拟安装,您可以很容易地将其安装到 Azure 中,以便尝试 Visual Studio Code 并理解各种 ASP.NET Core 2.0 示例,然后通过微软远程桌面应用连接到它。
在这种情况下,从 Azure Marketplace 中选择 Linux Ubuntu 16.04 LTS 映像,并在 Azure 中创建一个新的 Linux Ubuntu VM。 保留所有默认选项,然后将其配置为允许远程桌面连接(安装兼容桌面、安装 xrdp、开放端口3389等):

让我们看看如何在 Linux Ubuntu 上安装 Visual Studio Code:
- 首先,从https://go.microsoft.com/fwlink/?LinkID=760868下载 Linux Ubuntu 安装
.deb包(64 位):

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

您也可以手动安装存储库和签名密钥,更新包缓存,然后最终启动 Visual Studio Code 包安装,如下所示:
- 在 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 Code 设置为默认文本文件编辑器。
For more information and details on how to install Visual Studio Code on other Linux distributions such as RHEL, Fedora, CentOS, openSUSE, SLE, or others, please go to https://code.visualstudio.com/docs/setup/linux.
创建您的第一个 ASP.NET Core 2.0 应用在 Visual Studio Code
现在您将看到如何初始化您的第一个 ASP.NET Core 2.0 应用使用内建的 Visual Studio Code 终端窗口。 然后,你将安装所有必要的扩展,以便能够在最后运行和调试它:
- 启动 Visual Studio 代码; 在资源管理器视图中还没有打开任何文件夹:

- 单击“打开文件夹”,然后单击“创建文件夹”。 将文件夹命名为
aspnetcoremvcsample,然后单击确定:

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

- 当打开任何 c#文件时,都会要求您安装额外的项目依赖项和 Visual Studio Code 扩展。 这样做可以在接下来的步骤中构建、运行和调试你的应用:

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

- 在代码中的某处设置一个断点,并通过按F5或单击调试视图中的绿色闪光来开始调试。 尝试命中断点; 一切都应该正常工作:

创建您的第一个 ASP。 Linux 中的 NET Core 2.0 应用
要创建并运行您的第一个示例应用,只使用 Linux 中的 Terminal 窗口,您必须执行以下步骤:
- 如果. net Core 2.0 SDK 还没有安装,那么从您的 Linux 发行版的https://www.microsoft.com/net/core/preview下载并安装。net Core Preview 2。 下面是如何在 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
- 为示例应用创建一个文件夹
mkdir ~/Documents/aspnetcoremvcsample。 - 移动到创建的文件夹
cd ~/Documents/aspnetcoremvcsample。 - 基于 asp.net 开发一个新的 web 应用.NET Core 2.0 MVC web 应用模板,
dotnet new mvc:

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

- 打开浏览器,进入
http://localhost:5000:

总结
在本章中,您已经学习了如何设置您的开发环境,以便能够使用 ASP。 2.0 NET Core。 您已经了解了如何安装 Visual Studio 2017 或 Visual Studio Code。
然后您创建了您的第一个 ASP.NET Core 2.0 web 应用在这两个开发环境中,你甚至已经在 Linux 中构建了一个项目来展示跨平台功能。
在下一章中,我们将讨论如何使用 Visual Studio Team Services(包括工作项和 Git 分支)建立一个持续集成管道,以及构建和发布管道。**
三、在 VSTS 中创建持续集成管道
构建伟大的应用不是一项简单的任务。 相反,这是一项困难而复杂的工作,许多参与者需要有效地一起工作,以创建符合高端用户期望的应用。
今天,一切都进展得非常快,上市时间对成功非常重要。 本章将介绍帮助您优化开发过程的方法、过程和工具,从而以较短的发布周期构建高质量的软件。
传统上,构建软件是通过从头到尾规划整个软件项目,编写详细的规格说明,开发和测试(通常是匆忙的),同时希望一切都按照预期工作(v 模型)来完成的。
这种方法有时有效,有时无效。 当它不起作用时,开发人员在只进行手动测试的同时实现特性,其目标是稍后添加单元测试。 然后,在项目结束时,他们必须加快速度以确保按时交付,但往往没有时间。
这将导致具有重大技术、功能和质量缺陷的项目,大量的 bug 和巨大的维护工作将导致较长的发布周期。 在最坏的情况下,最终用户将不喜欢交付的特性,因此最终产品可能被认为是一个彻底的失败。
有一种更好的做事方式,人们已经讨论了一段时间了,你肯定已经听说过敏捷方法论了!
敏捷方法,结合持续集成(CI)和持续部署(****CD),为建设更好的软件解决方案提供一个快速上市时间,降低维护成本,更好的整体质量,和更高的客户满意度。
虽然这本书并不是关于敏捷方法论本身,但我们建议您熟悉这个主题,并且我们将解释所有伴随和围绕它的工具和过程。
在本章中,我们将涵盖以下主题:
- 持续集成、持续部署以及构建和发布管道
- 使用Visual Studio Team Services(VSTS)进行持续集成和持续部署
- 创建免费的 VSTS 订阅和您的第一个 VSTS 项目
- 通过工作项组织工作
- 使用 Git 作为版本控制系统
- 创建 VSTS 构建管道
- 创建 VSTS 发布管道
持续集成、持续部署以及构建和发布管道
当使用持续集成时,开发团队编写代码,在代码审查之后,将其集成到版本控制系统中,并在那里自动构建和测试。 这种情况通常一天发生多次。 因此,开发团队可以快速检测问题和错误,并尽早修复它们,从而实现通常称为Fail Fast的功能。
持续部署是持续集成的自然扩展,因为它确保了构建和测试后的每个应用修改都是可发布的。 它由自动升级开发、测试、登台和生产系统组成。
管道定义了一个完整的开发和发布工作流。 它包含了概念、开发、质量保证和测试所需的所有步骤,直到最终产品的交付。 它包括持续集成和持续部署流程,用于以工业化的方式构建高质量的应用。
Note that you can separate your development process into two different pipelines, a build and a release pipeline, or have only one single pipeline that does it all, depending on your specific needs.
有各种各样的技术和工具可以帮助您实现基于持续集成和持续部署的高效、高效、全自动和工业化的软件开发过程。 在下面的示例中,我们将使用 Visual Studio Team Services。
使用 VSTS 进行持续集成和持续部署
如果你需要相互协作和共享代码,规划和管理您的用户故事和开发任务,跟踪进度的特性和缺陷,所有在敏捷环境中,然后 VSTS 是一个解决方案你可以找到在云中,甚至可能是最好的。
它支持许多不同的编程语言(c#、Java、JavaScript 等等)和各种开发工具(Visual Studio、Eclipse 等等),并且可以扩展到任何团队规模。
此外,在一个私人团队项目中,最多 5 个用户可以免费使用它,这对尝试本书中展示的示例非常有帮助。
VSTS 提供以下主要功能:
- 工作项目和看板:计划和分配工作和任务
- 源代码管理:在版本控制系统中共享代码
- 测试:创建并执行包含测试用例的测试计划
- Package store:将自己的 NuGet Package 放入 store
- 构建管道:构建用于创建应用包的代码
- 发布管道:将应用包部署到不同的发布目标
For further information on VSTS and all of its features, please go to https://www.visualstudio.com/team-services/features.
创建免费的 VSTS 订阅和您的第一个 VSTS 项目
我们现在将解释如何创建您自己的免费 VSTS 订阅和您的第一个项目。 你将在后面使用它来尝试和理解这本书中的例子:
- 转到https://www.visualstudio.com/team-services,点击 Get Started 免费按钮:

- 用你的工作、学校或个人微软账户登录:

- 如果你是第一次连接,输入额外的信息,如你的姓名,你的国家,和你的电子邮件地址,然后点击继续:

- 现在您的帐户已经创建,让我们创建一个新项目。 在我们的例子中,选择 Git 作为版本控制,点击 Change Details,然后选择 Work item process-Scrum:

- 新项目生成了,现在可以创建第一个工作项和 Git 存储库了,如本书后面所示。
通过工作项组织工作
工作项用于计划、分配、跟踪,更一般地说,在软件开发项目期间组织您的工作。 它们有助于更好地理解需要做什么,并对项目的状态提供见解。
一些常见的工作项用法是:
- 为应用特性创建、划分优先级并跟踪用户描述
- 创建和跟踪实现用户描述所需的开发任务
- 创建、划分优先级并跟踪应用错误
- 确定应用质量和发布日期
- 在一个看板板中显示用户故事、任务和 bug 的进展
如前所述,您可以在 VSTS 项目创建期间选择工作项流程。 这个选择定义了可用的标准工作项类型(WITs)。
默认情况下,有超过 14 个 WITs,您可以为高级场景创建自己的自定义 WITs。 大多数时候,您不需要创建自己的自定义 WITs。
可能的工作项过程选择有:
- Scrum,如果你的团队使用 Scrum 方法,并且你想在看板板上跟踪你的产品待办事项列表(PBI)
- 敏捷,如果你的团队在实践一种敏捷方法,但不想遵守特定的 Scrum 约束和术语
- 如果您的团队遵循更正式的开发任务,那么您可以跟踪请求、更改、风险和评审
下面是一个取决于工作项流程的 WITs 列表:
| 领域 | Scrum | 敏捷 | cmmi |
| 产品规划 | PBI 错误 | 用户故事错误 | 要求改变错误 |
| 投资组合 | 史诗功能 | 史诗功能 | 史诗功能 |
| 任务和冲刺计划 | 任务 | 任务 | 任务 |
| 缺陷积压管理 | 错误 | 错误 | 错误 |
| 问题与风险管理 | 障碍 | 问题 | 问题风险审查 |
在我们的例子中,我们选择了 Scrum 流程。 产品所有者创建主题、特性和产品待办事项列表项(相当于用户描述)。 在 sprint 计划开发期间,任务被定义并与产品待办事项列表项相关联。 通过云中的看板板,整个团队都可以看到所有内容:

测试人员通过使用 VSTS web 门户或 Microsoft test Manager 来创建和执行测试用例。 他们创建并分配 bug 和代码缺陷,并且可以跟踪阻塞问题:

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

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

产品待办事项列表项是一个具有商业价值的工作单元,它足够小,可以在一次 sprint 中完成。 如果你不能在一个 sprint 内完成它,那么它就必须被认为是一个特性,必须进一步分解:

任务描述了在 sprint 期间实现预期的产品待办事项项功能所需的开发工作。 它们链接到产品待办事项列表项,以实现可跟踪性,并能够自动计算项目进展。
bug 包含已经提出的问题,需要在 sprint 中解决。 它们链接到相应的产品待办事项列表:

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

每个团队成员的工作能力可以被定义为每个 sprint,一个工作细节报告可以让你实时跟踪他们的工作成就:

此外,每个工作项都有一个随时间变化的状态。 该状态允许您跟踪工作成就并过滤工作项,以便更好地理解和检测问题。
下图显示了依赖于工作项过程的各种默认工作项状态:
| | Scrum | 敏捷 | cmmi |
| 工作项状态 | 新批准承诺完成删除 | 新活跃的解决关闭删除 | 提出了活跃的解决关闭 |
您可以查询工作项,创建图表,并将它们发布到 VSTS 项目主页。 如果您需要检索特定的工作项或需要获得项目的整体视图,这是一个非常有用的特性:

使用 Git 作为版本控制系统
Git 在过去几年中取得了相当大的成功。 它已经成为开发人员社区首选的分布式版本控制系统。
VSTS 和 Git 之间有一个很好的集成,并且你可以使用一些强大的、高效的特性(https://www.visualstudio.com/en-us/docs/work/backlogs/connect-work-items-to-git-dev-ops):
- Git 分支可以在待办事项列表或看板板中创建
- 可以直接从 VSTS 网站轻松地为多个工作项创建 Git 特性分支
- 拉请求和提交会自动链接到相应的工作项
- Build Summary 页面将链接到提交的工作项显示为相关联的工作项
让我们看看如何创建一个新的 Git 仓库,本地克隆,在 Visual Studio 2017 中使用,然后创建你的第一个提交:
- 在您的 VSTS 项目中,单击顶部菜单中的代码,然后单击在 Visual Studio 中的克隆按钮:

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

- Visual Studio 2017 自动启动,您可以使用您的工作、学校或个人微软帐户进行认证:

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

- 进入团队资源管理器-主页,点击设置:

- 在 Team Explorer - Settings 中,点击 Repository Settings:

- 在忽略&属性文件部分,为每个文件点击添加:

- 返回 Team Explorer - Home,这次单击 Changes,为第一次提交输入注释,然后单击 commit staging 按钮:

- 你的第一个提交已经在本地创建; 点击 Sync 链接将其推送到服务器:

- 进入 VSTS 网站,点击上方菜单中的 Code; 你可以看到你创建的文件已经上传:

就是这样! 您已经创建并初始化了 Git 存储库。 就这么简单! 从这里开始,你有多条路可以走。 例如,将所有内容都放在同一个分支中并不是一个好主意,特别是当您必须维护应用的多个版本时。
You can get some guidance for different branching strategies from https://www.visualstudio.com/en-us/articles/git-branching-guidance.
使用特性分支
特性分支背后的哲学是,每次开始处理一个新的 VSTS 特性(甚至是 VSTS 产品待办事项列表项)时,您必须做的第一件事就是创建一个新的,所谓的特性分支。
然后,您在这个分支中完全独立地工作,直到您准备好将经过测试和验证的修改推到主分支(或者在更复杂的环境中,推到开发分支)。 在推出之前,它不会干扰你的其他功能,也不会导致 bug 或降低整体质量。
如果一个项目的最后期限临近,而你没有及时完成所有计划的功能,你就不需要再紧张了! 为什么? 因为您只能集成准备发布的特性。 您将拥有一个功能较少的产品,但您可以确信这些功能将会像预期的那样工作,没有任何风险。
让我们看看如何使用 Visual Studio 2017 和 Git 创建一个特性分支:
- 打开 Visual Studio 2017,进入 Team Explorer 选项卡,点击 Branches 按钮:

- 在 Team Explorer - Branches 中,点击 New Branch 链接:

- 输入一个新的特性分支名称(使用
FEA-前缀),然后单击 Create branch 按钮:

合并更改和解决冲突
有时,团队成员在同一时间处理相同的文件,从而导致冲突。 让我们看看在这种情况下如何合并更改和解决冲突:
- 创建一个名为
HelloWorld.txt的文本文件,并将其添加到本地存储库中。 将文件推到服务器,并在服务器上和本地存储库中更新该文件。 - 如果你试图推送已经在本地和远程存储库中修改过的
HelloWorld.txt文件,你会得到一条错误消息,并且推送失败:

- 当查看输出窗口时,你会得到额外的信息:

- 单击 Pull 链接,您将获得远程更改,这将导致本地副本和远程副本之间的冲突。 单击“解决冲突”或“冲突”链接:

- 您将看到一个冲突文件列表。 点击你想解决的冲突,然后点击合并按钮:

- 您将看到冲突的修改。 选择你想保留的(左边的,右边的,或者两者都保留),然后点击 Accept Merge 按钮:

- 回到 Team Explorer,点击 Commit Merge 按钮:

- 输入一条注释,然后单击 Commit staging 按钮,在本地完成并提交合并:

- 在本地创建提交后,单击 Sync 链接,然后单击 Push 链接:

- 现在你应该看到修改已经被上传到远程存储库:

创建 VSTS 构建管道
在计划和组织好你的工作并创建了 Git 仓库之后,你现在应该配置一个 VSTS 构建管道,这将允许你对你的应用进行持续集成:
- 打开 Visual Studio 2017,进入 Team Explorer 选项卡,然后单击 build 按钮:

- 接下来,点击 New Build Definition 链接:

- 打开 VSTS 网站,你会看到一个构建定义模板的选择,选择 ASP.NET Core 模板:

- 在新的构建定义中,输入名称并选择默认代理队列。 我们推荐使用 Hosted VS2017:

- 要选择源存储库,请单击 Get sources。 在我们的例子中,我们使用默认值(这个项目,Branch: master, Clean: false):

- 要启用持续集成,请单击“构建定义”菜单中的“触发器”,然后单击“启用此触发器”按钮:
- 在确认已经选择了 Git 存储库和主分支后,正确地单击 Save 或 Save & queue 按钮。 配置已经完成,每次代码提交到存储库时都会自动触发构建:

创建 VSTS 发布管道
您的应用不断地被集成,并且您已经看到了一些巨大的好处,例如可以更快地检测和修复错误和问题。 我们不要止步于此; 进一步改进开发过程比你想象的要容易得多!
现在我们来看看如何通过创建一个 VSTS 发布管道来采用应用的持续部署:
- 打开 VSTS 网站,点击上方菜单中的 Build & Release,点击 Releases,然后点击 New definition 按钮,选择 Empty definition 模板:

- 你现在可以选择 Project 和 Source (Build 定义)并启用连续部署,然后点击 Create 按钮:

- 发布定义被创建,你可以在列表中看到它:

所示的示例版本定义目前并没有做太多工作。 稍后我们将在相应的 Azure 章节中看到部署到 Azure 的更高级的版本。
总结
在本章中,您已经学习了持续集成、持续部署、构建和发布管道、它们的好处以及如何使用 VSTS 实现它们。
您已经创建了一个新的 VSTS 订阅并初始化了一个新项目。 然后我们探讨了一些基本概念,例如用于源代码控制的工作项和 Git。 最后,我们通过一个实际示例说明了如何配置 VSTS 构建管道和 VSTS 发布管道。
在接下来的两章中,我们将解释 ASP 的基本概念.NET Core 2.0 包括 Startup 类,使用中间件、路由、错误处理等。
四、ASP.NET Core 2.0 的基本概念——第 1 部分
在最后三章中,你已经看到了 ASP.NET Core 2.0 是关于从全局的角度,以及设置你的开发环境,包括 Visual Studio 2017(或 Visual Studio Code)和一个与 Git 存储库持续集成和持续交付的 VSTS 管道。
这很有趣,但也很理论化。 现在,是时候做一些实际的事情了,是时候付诸行动了,是时候建立一些自己的东西了!
在本章中,我们将构建一个应用来展示 ASP 的基本概念.NET Core 2.0 框架。 在接下来的章节中,我们将不断地改进这个应用,同时使用和说明 ASP 的各种特性.NET Core 2.0 及其相关技术。
在本章中,我们将涵盖以下主题:
Startup和Program类- 创建页面和服务
- 使用 Bower 和布局页面
- 使用依赖注入
- 使用内置中间件
- 创建自己的中间件
- 使用静态文件
- 使用路由、URL 重定向和 URL 重写
- 错误处理和模型验证
创建井字游戏
让我们做些有趣的事吧! 让我们来构建一字棋游戏,也被称为 noughts and cross 或 Xs and Os。 玩家将选择谁接受 x,谁接受 o。 然后,他们将轮流在 3×3 网格中标记空间,每回合一个标记。 在水平、垂直或对角线上成功放置三个标记的玩家获胜。
玩家必须输入他们的电子邮件和名字注册创建一个帐户才能开始游戏。 每一场比赛后,他们都会收到一个比赛分数,这个分数会被加到他们的总分中。
排行榜提供有关玩家排名和最高分数的信息。
为了创建游戏,玩家必须向其他玩家发送邀请,然后一个特定的等待页面会显示给他,直到其他玩家做出回应。 其他玩家在收到邀请邮件后,可以确认请求并加入游戏。 当两名玩家在线时,游戏开始。
如上一章所述,我们可以使用 VSTS 及其工作项来组织和安排Tic-Tac-Toe应用的实现。 为此,我们必须创建史诗、特性和产品待办事项列表项,然后进行 sprint 计划,确定优先级,并决定首先执行什么。
正如你在下面的截图中看到的,我们已经决定在第一个 sprint 中处理 5 个产品待办事项列表,并将它们添加到 sprint 待办事项列表中:

你还记得在实现任何新特性之前,接下来需要做什么吗? 你不记得了? 也许特征分支听起来很耳熟?
在上一章中,我们展示了创建开发的最佳实践,它们是独立的,易于维护和发布。 它们包括在 Git 存储库中为每个要添加到应用的新特性创建一个特性分支。
因此,每个开发人员都可以在自己的特定特性分支中处理自己的特定特性,直到他决定可以发布它为止。
最后,所有准备发布的特性都合并到开发(或发布或主)分支中。 然后完成集成测试,如果一切正常,就会交付一个新的应用版本。
我们选择首先处理的特性是用户注册,所以我们要做的第一件事就是创建一个名为 FEA-UserRegistration 的特性分支。 如果你不知道怎么做,你可以转到第 3 章,在 VSTS 中创建一个持续集成管道,并得到一个完整的步骤步骤和详细的解释:

构思并实现你的第一个井字游戏功能
在实现用户注册特性之前,我们必须理解它并决定一切应该如何工作。 我们必须定义用户描述和工作流。 为此,我们需要更详细地分析前面提到的Tic-Tac-Toe游戏描述。
如前所述,用户只有拥有用户帐号才能创建并加入游戏。 要创建这个帐户,他必须输入他的名字,他的姓氏,他的电子邮件地址,和一个新的密码。 然后系统验证输入的电子邮件地址是否已经注册。 一个指定的电子邮件地址只能注册一次。 如果电子邮件地址是新的,则生成用户帐户,如果电子邮件地址是已知的,则必须显示一个错误。
让我们来看看用户注册过程和实现它所需要的不同组件:
- 主页上有一个用户注册的链接,新用户必须点击注册来创建自己的玩家账户。 单击用户注册链接将用户重定向到专用的注册页面。
- 注册页面包含一个注册表单,用户必须在其中输入个人信息,然后进行确认。
- JavaScript 客户机验证表单,提交数据并将其发送给通信中间件,然后等待结果。
- 通信中间件接收请求并将其路由到注册服务。
- 注册服务接收请求,验证数据完整性,检查电子邮件是否已经用于注册,然后注册用户或返回错误消息。
- 通信中间件接收结果并将其路由到等待的 JavaScript 客户机。
- JavaScript 客户端重定向用户,以便如果结果是成功,他可以开始玩游戏,如果结果是失败,它将显示一条错误消息。
下面的序列图显示了用户注册过程。 它是更容易和更快的理解与更直观的表现:

首先,我们需要创建一个新的空 ASP.NET Core 2.0 Web Application,它将在本章和本书的其余部分用于添加各种组件和包。 然后,我们将逐步添加新的概念和功能,这将让您真正了解发生了什么,以及一切如何工作:
- 启动 Visual Studio 2017,点击文件|新建|项目。
- 在。net Core 部分选择 ASP.NET Core Web Application,输入应用名称、存储库的位置和解决方案名称,然后单击 OK:

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 2.0 Web Application 项目将生成,只包含
Program.cs和Startup.cs文件:

太好了,我们已经创建了我们的项目,现在准备实现我们的第一个功能! 但在此之前,让我们花些时间看看 Visual Studio 2017 在幕后为我们做了什么。
针对项目的.csproj 文件中的不同。net 框架版本
对于 Visual Studio 2017 生成的每个项目,它都会创建一个相应的.csproj文件,该文件包含多个项目范围的设置,例如引用程序集、. net 框架目标版本、包含的文件和文件夹,以及多个其他。
例如,当打开 ASP.NET Core 2.0 项目,你可以看到以下结构:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All"
Version="2.0.0-preview2-final" />
</ItemGroup>
</Project>
您可以看到TargetFramework设置,它允许您定义应该包含哪些. net 框架版本,以及用于构建和执行源代码。
在我们的例子中,它被设置为netcoreapp2.0,这是使用。net Core 2.0 框架的特定值:
<TargetFramework>netcoreapp2.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.
For instance, if you want to cross-target .NET Core 2.0 and .NET 4.7, you have to use the following settings:
<TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks>
在调试模式下按F5键执行应用时,可以看到应用的Debug文件夹(\bin\Debug)中已经创建了多个文件夹和文件:

如果您更改了.csproj文件并添加了其他目标框架,您将看到将生成其他文件夹。 然后,每个特定。net 框架版本的 dll 被放入相应的文件夹中。 下面的示例使用了。net Core 和。net 4.7 的TargetFrameworks设置:

使用 Microsoft.AspNetCore.All 元包
在依赖项| NuGet 部分的解决方案资源管理器中,您可以看到一些非常有趣的东西,特别是针对 ASP.NET Core 2.0 项目:Microsoft.AspNetCore.All元包:

Microsoft.AspNetCore.All项目依赖项是在您创建 ASP. php 时自动添加的.NET Core 2.0 Web 应用。 对于这种类型的项目,默认情况下会这样做。
然而,Microsoft.AspNetCore.All不是一个标准的 NuGet 包,因为它不包含任何代码或 dll。 相反,它充当一个元包,引用它所依赖的其他包。 更具体地说,它包含了 ASP 的所有包.NET Core 和实体框架 Core,以及它们的内部和外部依赖,并利用。NET Core 运行时存储。
在这个示例中,您可以看到检索到各种各样的包,例如 Application Insights、Authentication、Authorization、Azure App Services 等。
在旧版本的。net Core(版本 1.0 和 1.1)中,你必须自己添加这些 NuGet 包。 现在微软已经创建了 ASP 的概念.NET Core 元包,你可以在一个地方找到所有东西。
此外,包裁减排除了未使用的二进制文件,因此在部署应用时不会发布它们。
使用 Program 类
Program类是 ASP 的主要入口.NET Core 2.0 应用。 事实上,ASP。 在这方面,NET Core 2.0 应用非常类似于标准的。NET Framework 控制台应用。 两者都有一个在运行应用时执行的Main方法。 即使是接受字符串数组作为参数的Main方法的基本签名也是一样的,如下面的代码中所示。 毫无疑问,这是由于 ASP.NET Core 应用实际上是一个托管 web 应用的控制台应用:
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
namespace TicTacToe
{
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
}
通常,您不需要以任何方式接触Program类。 默认情况下,运行应用所需的所有东西都已经在那里并预先配置好了。
但是,您可能希望激活一些更高级的功能。
例如,您可以在服务器启动期间启用错误捕获并显示一个错误页面。 在这种情况下,你只需要使用以下指令:
WebHost.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) - Localhost(例如,
https://localhost:443) - Unix 套接字(例如,
http://unix:/run/dan-live.sock)
下面是一个如何设置这些参数的例子:
WebHost.PreferHostingUrls(true);
WebHost.UseUrls("http://localhost:5000");
最后,通过设置以下参数,您可以将应用与 Application Insights 集成,这是一个可扩展的应用性能管理服务,允许在运行时监视应用并检测性能异常,以及诊断问题和了解用户的操作:
WebHost.UseApplicationInsights();
下面是一个Program类的例子,它包含了前面展示的所有概念:
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.CaptureStartupErrors(true)
.UseStartup<Startup>()
.PreferHostingUrls(true)
.UseUrls("http://localhost:5000")
.UseApplicationInsights()
.Build();
}
与 Startup 类一起工作
另一个自动生成的元素,它存在于所有类型的 ASP.NET Core 2.0 项目,是Startup类。 正如前面看到的,Program类主要处理宿主环境周围的一切。 类是关于服务和中间件的预加载和配置的。 这两个类是所有 ASP 的基础.NET Core 2.0 应用。
让我们来看看Startup类的基本结构,以便更好地理解它提供了什么以及如何最好地利用它的功能:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace TicTacToe
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
}
public void Configure(IApplicationBuilder app,
IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
});
}
}
}
有两种方法需要你的注意,因为你会经常使用它们:
ConfigureServices方法,由运行时调用,用于向容器添加服务- 用于配置 HTTP 管道的
Configure方法
我们在本章开始时说过,我们需要更多的实际工作,所以让我们回到我们的井字游戏,看看如何使用Startup类在一个真实的例子!
我们将使用 MVC 来实现应用,但由于您已经使用了空白的 ASP.NET Core 2.0 Web 应用模板,Visual Studio 2017 在项目生成过程中没有添加任何内容。 你必须自己添加一切; 这是一个很好的机会,可以更好地了解一切事物是如何运作的!
首先要做的是将 MVC 添加到服务配置中。 你可以使用ConfigureServices方法,然后添加 MVC 中间件:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
你可能会说这太简单了,那么有什么问题呢? 没有圈套! 一切都在 ASP.NET Core 2.0 是围绕着简单性、清晰性和开发人员生产力而开发的。
你可以在配置 MVC 中间件和设置路由路径时再次看到这一点(我们将在后面更详细地解释路由):
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
再次,非常清晰和简短的说明,使我们作为开发人员的生活更容易和更高效。 现在是成为一名开发人员的好时机!
在下一步中,您需要在您的 ASP 中启用静态内容的使用.NET Core 2.0 应用能够使用 HTML, CSS, JavaScript 和图像。
你知道怎么做吗? 是的,您是对的,您需要添加另一个中间件。 你可以像之前一样调用相应的 app 方法:
app.UseStaticFiles();
作为一名开发人员,您需要能够快速分析和理解 HTML、CSS 和 JavaScript 行为和问题。 为此,ASP.NET Core 2.0 包含一个非常方便的特性浏览器链接。 启用后,它会在 Visual Studio 2017 之间建立一个专用的通信通道,以提高开发人员的生产力。
启用浏览器链接真的很容易:
app.UseBrowserLink();
下面是一个示例,你可以在井字游戏中使用Startup.cs类,在配置了前面看到的各种服务设置后:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
public void Configure(IApplicationBuilder app,
IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
编制项目基本结构
你肯定想看到一些东西运行,并创建井字游戏。 现在我们已经从功能的角度定义了一切应该如何工作,我们需要从创建应用的基本项目结构开始。
ASP.NET Core 2.0 web 应用,最好使用以下项目结构:
- 一个包含应用的所有控制器的文件夹。
- 一个文件夹
Services,包含应用的所有服务(例如,外部通信服务)。 - 包含应用所有视图的
Views文件夹。 这个文件夹应该包含一个Shared子文件夹以及每个控制器一个文件夹。 - 一个
_ViewImports.cshtml文件,用于定义在所有视图中可用的一些名称空间。 - 一个
_ViewStart.cshtml文件,用于定义在每个视图呈现开始时执行的一些代码(例如,为所有视图设置布局页面)。 - 一个
_Layout.cshtml文件,用于定义所有视图的公共布局。
让我们创建项目结构:
- 打开 Visual Studio 2017,打开井字游戏 您创建的 NET Core 2.0 项目,创建三个新文件夹
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游戏 web 应用:
- 如前所述,更新
Program.cs和Startup.cs文件。 - 添加一个新控制器,在
Controllers文件夹的解决方案资源管理器中右键单击,然后选择 Add |控制器:

- 在弹出的 Add Scaffold 窗口中,选择 MVC Controller - Empty 并将新控制器命名为
HomeController:

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

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

- 祝贺您,您的视图将自动生成,您可以通过按F5来测试您的应用。 我们将在本章稍后完成:

通过使用 Bower 和布局页面,使您的网页具有更现代的外观
在上一节中,您了解了如何创建一个基本的 web 页面。 知道如何在技术上做到这一点是一回事,但创建成功的 web 应用不仅在于技术实现,还在于如何使您的应用具有视觉吸引力和用户友好性。 虽然这本书不是关于网页设计和用户体验的,但我们想给你一些快速和简单的方法来构建更好的 web 应用。
为此,我们建议使用 Bower(https://bower.io),即 Web 中自称的包管理器,与 ASP 结合使用.NET Core 布局页面。
在过去的几年中,Bower 在 web 开发社区中取得了一些显著的成功。 它有助于安装带有静态内容(如 HTML、CSS、JavaScript、字体和图像)的客户端包,包括它们的依赖项。
Visual Studio 2017 中对 Bower 有一些很棒的集成和支持; 您只需要正确地配置它,以有效地使用它。 让我们看看怎么做:
- 右键单击“一字棋”项目,选择“添加|新项目”,在搜索框中输入
Bower,选择“凉亭配置文件”,单击“添加”:

- 添加 Bower 配置文件应该已经添加了一个
bower.json文件。 用以下内容更新此文件:
{
"name": "asp.net",
"private": true,
"dependencies": {
"bootstrap": "3.3.6",
"jquery": "2.2.0",
"jquery-validation": "1.14.0",
"jquery-validation-unobtrusive": "3.2.6"
}
}
- 添加 Bower 配置文件应该已经添加了一个
.bowerrc文件。 更新这个文件并定义资产应该放置的目录:
{
"directory": "wwwroot/lib"
}
- 右键单击
bower.json文件,点击恢复包:

- 客户端包(
bootstrap、jquery和更多)被下载到您已经定义的文件夹(wwwroot/lib)中。 静态内容现在可以在你的应用中使用:

- 在
wwwroot文件夹中,创建一个名为css的文件夹。 在这个文件夹中添加一个新的样式表site.css:
body {
padding-top: 50px;
padding-bottom: 20px;
}
/* Set padding to keep content from hitting the edges */
.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;
}
/* styles for validation helpers */
.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;
}
一个成功的 web 应用应该有一个共同的布局和一致的用户体验时,从一个页面导航到另一个页面。 这是用户采用和用户满意度的关键。 ASP.NET Core 布局页面是正确的解决方案。
它们可以用于在 web 应用中定义视图模板。 您的所有视图可以使用相同的模板,也可以根据您的特定需求使用不同的模板。
我们将使用更新后的布局页面,如下所示,用于我们的示例应用:
<!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>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle"
data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-area="" asp-controller="Home" asp-action="Index"
class="navbar-brand">Tic-Tac-Toe</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-area="" asp-controller="Home"
asp-action="Index">Home</a></li>
<li><a asp-area="" asp-controller="Home"
asp-action="About">About</a></li>
<li><a asp-area="" asp-controller="Home"
asp-action="Contact">Contact</a></li>
</ul>
</div>
</div>
</nav>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>© 2017 - 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>
<environment exclude="Development">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/
jquery-2.2.0.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-K+ctZQ+LL8q6tP7I94W+qzQsfRV2a+
AfHIi9k8z8l9ggpc8X+Ytst4yBo/hH+8Fk">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/
3.3.7/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery&&window.jQuery
.fn&&window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA
7l2mCWNIpG9mGCD8wGNIcPD7Txa">
</script>
<script src="~/js/site.min.js" asp-append-version="true">
</script>
</environment>
@RenderSection("Scripts", required: false)
</body>
</html>
在下一节中创建用户注册页面之前,让我们更新之前创建的主页,以显示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
marks in a horizontal, vertical, or diagonal row
wins the game.</p>
</div>
<p>
<h3>Register by clicking <a asp-controller="UserRegistration"
asp-view="Index">here</a></h3>
</p>
</div>
</div>
当启动应用时,你会看到新的主页设计:

创建用户注册一字棋界面
现在将第二个组件,即用户注册页面与其表单集成在一起,它将允许新用户注册来玩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(如果您不知道如何做到这一点,请参考创建井字游戏主页节)。 - 右键单击名为
Index的方法并选择 Add View。 这一次,选择创建模板,像前面提到的模型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.
The _ViewStart.cshtml file is used to share settings between views, while the _ViewImports file is used to share using namespaces and inject dependency injection instances. Visual Studio 2017 includes two templates for these files.
- 从视图中移除自动生成的
Id、IsEmailConfirmed、EmailConfirmationDate和Score元素; 对于用户注册表单,我们不需要它们。 - 视图现在已经准备好了; 点击F5,点击主页注册链接显示:

使用依赖注入来鼓励应用中的松散耦合
开发应用时最大的问题之一是组件间的依赖关系。 这些依赖关系使单独维护和发展组件变得困难,因为修改可能会严重影响其他依赖组件。 但请放心,有一些机制允许分解这些依赖项,其中之一是依赖项注入(DI)。
依赖注入允许组件一起工作,同时提供松耦合。 一个组件只需要知道另一个组件实现的契约就可以使用它。 使用 DI 容器,组件不会直接实例化,也不会使用静态引用来查找另一个组件的实例。 相反,DI 容器负责在运行时检索正确的实例。
当一个组件在设计时考虑到依赖注入,它在默认情况下是非常演进的,并且不依赖于任何其他组件或行为。 例如,身份验证服务可以使用提供者来进行使用 DI 的身份验证,如果添加了新的提供者,现有的提供者将不会受到影响。
ASP.NET Core 2.0 包含了一个非常简单的内置 DI 容器,它支持构造函数注入。 要使服务对容器可用,您必须将其添加到Startup类的ConfigureService方法中。 在不知道的情况下,你已经在 MVC 中做过了:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
实际上,您必须对您自己的自定义服务做同样的事情,您必须在这个方法中声明它们。 当你知道你在做什么时,这真的很容易做到!
然而,有多种方式注入你的服务,你需要选择哪一种最适合你的需求:
- 瞬时注入:每次调用方法时创建一个实例(例如,无状态服务):
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.
Here is an example of how to instantiate your services by yourself:
services.AddSingleton(new ExampleService());
现在您已经了解了如何使用 DI,让我们应用您的知识并为我们的示例应用创建下一个组件。
使用实例创建井字用户服务
我们已经创建了一个主页以及一个用户注册页面。 用户可以单击注册链接并填写注册表单,但表单数据尚未以任何方式处理。 我们将添加一个用户服务,该服务将负责处理与用户相关的任务,例如用户注册请求。 此外,您将应用一些 ASP。 之前看到的 NET Core 2.0 DI 机制:
- 在
Services文件夹中添加一个名为UserService.cs的新类。 - 为用户注册添加一个新方法,使用上一节中创建的模型作为参数:
public class UserService
{
public Task<bool>RegisterUser(UserModel userModel)
{
return Task.FromResult(true);
}
}
- 右键单击类,选择 Quick Actions 和 Refactorings,然后单击 Extract Interface:

- 在弹出窗口中保留所有的默认值,然后单击 OK:

- Visual Studio 2017 将生成一个名为
IUserService.cs的新文件,其中包含提取的接口定义,如下所示:
public interface IUserService
{
Task<bool>RegisterUser(UserModeluserModel);
}
- 更新之前创建的
UserRegistrationController并应用构造函数注入机制:
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 sucessfully");
}
- 到
Startup类,在ConfigureServices方法中声明UserService,使其可用于应用:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddSingleton<IUserService, UserService>();
}
- 按F5测试您的应用,填写注册页面,然后点击 OK。 你应该得到以下输出:

非常好,您已经创建了多个组件的Tic-Tac-Toe应用,进展非常好! 请保持敏锐,因为下一节非常重要,因为它详细解释了中间件。
使用中间件)
如前所述,Startup类负责在 ASP 中添加和配置中间件.NET Core 2.0 应用。 但什么是中间件? 何时以及如何使用它,以及如何创建自己的中间件? 这些都是我们现在要讨论的问题。
本质上,多个中间件组成了 ASP 的功能.NET Core 2.0 应用。 您现在可能已经注意到,即使是最基本的功能,比如提供静态内容,也由它们来执行。
中间件是 ASP 的一部分.NET Core 2.0 请求管道用于处理请求和响应。 当它们链接在一起时,它们可以将传入的请求从一个传递给另一个,并在管道中调用下一个中间件之前和之后执行操作:

使用中间件可以使您的应用更加灵活和完善,因为您可以在Startup类的Configure方法中轻松地添加和删除中间件。
此外,在Configure方法中调用中间件的顺序就是调用它们的顺序。 为了保证更好的性能、功能和安全性,建议按照以下顺序调用中间件:
- 异常处理中间件)。
- 静态文件中间件)。
- 验证仿真中间件。
- MVC middlewares.
如果您不按此顺序调用它们,您可能会得到一些意想不到的行为,甚至错误,因为中间件操作可能在请求管道中应用得太晚或太早。
例如,如果您不首先调用异常处理中间件,那么您可能无法捕获在调用它之前发生的所有异常。 另一个例子是在静态文件中间件之后调用响应压缩中间件。 在这种情况下,您的静态文件将不会被压缩,这可能不是期望的行为。 所以,要注意中间件调用的顺序; 它可以带来巨大的不同。
下面是一些你可以在你的应用中使用的内置中间件(列表不是详尽的; 还有更多):
| 身份验证 | OAuth 2 和 OpenID 认证,基于最新版本的 IdentityModel |
| CORS | 基于 HTTP 报头的跨源资源共享保护 |
| 响应缓存 | HTTP 响应缓存 |
| 响应压缩 | HTTP 响应 gzip 压缩 |
| 路由 | HTTP 请求路由框架 |
| 会话 | 基本的本地和分布式会话对象管理 |
| 静态文件 | HTML, CSS, JavaScript,和图像支持包括目录浏览 |
| URL 重写 | URL SEO 优化和重写 |
内置的中间件将足以满足最基本的需求和标准用例,但您肯定需要创建自己的中间件。 有两种方法:在 Startup 类中内联创建它们,或者在自包含类中创建它们。
首先让我们看看如何定义内联中间件; 以下是可采用的方法:
RunMapMapWhenUse
Run方法用于添加中间件并立即返回响应,从而使请求管道短路。 它不调用以下任何中间件,并结束请求管道。 因此,建议将它放在中间件调用的末尾(参见前面讨论的中间件排序)。
如果请求路径以特定路径开始,那么Map方法允许执行某个分支并添加相应的中间件,这意味着您可以有效地对请求管道进行分支。
MapWhen方法提供了与分支请求管道和添加特定中间件基本相同的概念,但是可以控制分支条件,因为它是基于Func<HttpContext, bool>谓词的结果。
Use方法添加中间件,并允许调用在线的下一个中间件或使请求管道短路。 但是,如果您希望在执行特定操作之后传递请求,则必须使用带有当前上下文作为参数的next.Invoke手动调用下一个中间件。
下面是一些如何使用这些扩展方法的例子:
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.");
});
}
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.");
});
}
如前所述,您可以内联创建中间件,但不建议用于更高级的场景。 在这种情况下,我们建议您将中间件放在自包含的类中,这样做的过程非常简单。 中间件只是一个具有特定结构的类,它是通过扩展方法公开的。
让我们为Tic-Tac-Toe应用创建一个基本的通信中间件:
- 在项目中创建一个名为
Middlewares的新文件夹,然后添加一个名为CommunicationMiddleware.cs的新类,使用以下代码:
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的新类,代码如下:
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.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
- 在通信中间件实现中设置一些断点,并按F5启动应用。 你会看到,如果一切正常工作,断点将被命中:

这只是一个如何创建自己的中间件的基本示例; 在这个部分和其他部分之间没有可见的功能更改。 在下一章中,您将进一步实现完成Tic-Tac-Toe应用的各种功能,在本章中看到的通信中间件将很快完成一些实际工作。
使用静态文件
在使用 web 应用时,大多数时候,你必须使用 HTML、CSS、JavaScript 和图像,这些都被 ASP 视为静态文件。 2.0 NET Core。
对这些文件的访问在默认情况下是不可用的,但是在本章的开始部分,您看到了需要做些什么才能允许静态文件在应用中使用。 事实上,你必须在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.
此外,出于安全原因,目录浏览在默认情况下是禁用的。 但是,如果你需要让用户看到文件夹和文件,你可以很容易地激活它:
- 在调用
AddMvc()方法之后,在Startup类的ConfigureService方法中添加DirectoryBrowsingMiddleware:
services.AddDirectoryBrowser();
- 在
Startup类的Configure方法中,调用UseDirectoryBrowser方法(在调用UseCommunicationMiddleware方法之后)来激活目录浏览:
app.UseDirectoryBrowser();

- 从
Startup类中移除对UseDirectoryBrowser方法的调用; 对于示例应用,我们不需要它
使用路由、URL 重定向和 URL 重写
在构建应用时,路由用于将传入请求映射到路由处理程序(URL 匹配),并为响应生成 URL (URL 生成)。
ASP 的路由能力.NET Core 2.0 结合并统一了以前存在的 MVC 和 Web API 的路由功能。 它们被从头开始重新构建,以创建一个公共路由框架,将所有不同的功能集中在一个地方,对所有类型的 ASP 都可用.NET Core 2.0 项目。
让我们看看路由在内部是如何工作的,以便更好地理解它在应用中是如何有用的,以及如何将它应用到我们的Tic-Tac-Toe示例中。
对于每个接收到的请求,将根据请求 URL 检索一个匹配的路由。 路由按照它们在路由集合中出现的顺序进行处理。
更具体地说,传入的请求被分派到相应的处理程序。 大多数情况下,这是基于 URL 中的数据完成的,但您也可以在更高级的场景中使用请求中的任何数据。
如果你正在使用 MVC 中间件,你可以在Startup类中定义和创建你的路由,如本章开头所示。 这是最简单的方式开始与 URL 匹配和 URL 生成:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
还有一个专用的路由中间件,您可以使用它在应用中处理路由,您在前面关于中间件的部分中已经看到了它。 你只需要在Startup类中添加它:
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting();
}
下面是如何使用它调用Startup类中的UserRegistration服务的示例:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddSingleton<IUserService, UserService>();
services.AddRouting();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
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 sucessfully created.");
});
var newUserRoutes = routeBuilder.Build();
app.UseRouter(newUserRoutes);
app.UseCommunicationMiddleware();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
app.UseStatusCodePages("text/plain",
"HTTP Error - Status Code: {0}");
}
如果你用一些查询字符串参数调用它,你会得到以下结果:

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

向应用添加错误处理
在开发应用时,问题不是错误和 bug 是否会发生,而是它们什么时候会发生。 构建应用是一项非常复杂的任务,几乎不可能考虑运行时可能发生的所有情况。 即使您认为您已经考虑了所有的事情,但是环境的行为并不如预期的那样,例如,服务不可用或者处理请求花费的时间比预期的要多。
对于这个问题,您有两个解决方案,它们需要同时应用于单元测试和错误处理。 从应用的角度来看,单元测试将确保开发期间的正确行为,而错误处理帮助您在运行时为环境问题做好准备。 我们将看看如何为你的 ASP 添加有效的错误处理.NET Core 2.0 应用。
默认情况下,如果根本没有错误处理,并且如果异常发生,您的应用将停止,用户将不能再使用它,在最坏的情况下,将会出现服务中断。
在开发期间要做的第一件事是激活默认的开发异常页面; 它显示发生的异常的详细信息。 你已经在本章的开头看到了如何做到这一点:
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
在默认的开发异常页面上,您可以深入研究原始异常细节,以分析堆栈跟踪。 您有多个选项卡,允许查看查询字符串参数、客户端 cookie 和请求头。
这些是一些有力的指标,有助于更好地理解发生了什么以及为什么会发生。 它们可以帮助你在开发期间更快速地发现问题并解决问题。
下面是一个发生异常的例子:

但是,不建议在生产环境中使用默认的开发异常页,因为它包含了太多关于系统的信息,可能会被用来危害系统。
对于生产环境,建议配置一个带有静态内容的专用错误页面。 在下面的示例中,您可以看到在开发期间使用了默认的开发异常页面,如果应用被配置为在非开发环境中运行,则会显示一个特定的错误页面:
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
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,根据需要使用 decorator 设置一些属性,并要求特定的数据类型:
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 sucessfully");
}
return View(userModel);
}
- 如果你没有填写必要的字段,或者你给出了一个错误的电子邮件地址,然后点击确定,你现在会得到一个相应的错误消息:

总结
在本章中,你已经学习了 ASP 的一些基本概念.NET 2.0。 有很多东西要理解,有很多东西要看,我们希望你在自己尝试这些东西时获得了一些乐趣。 你肯定取得了巨大的进步!
一开始,您创建了Tic-Tac-Toe项目; 然后,您开始实现它的不同组件。 我们学习了Program和Startup类,了解了如何使用 Bower 和布局页面,学习了如何应用依赖注入,并使用了静态文件。
此外,我们还介绍了用于更高级场景的中间件和路由。 最后,我们通过一个实际示例说明了如何为您的应用添加有效的错误处理。
在下一章中,我们将继续并介绍更多的概念,如 WebSockets、全球化、本地化和配置,以及一次性构建和在多个环境中运行。
五、ASP.NET Core 2.0 的基本概念——第 2 部分
前一章让你了解了在使用 ASP 时可以使用的各种功能和特性.NET Core 2.0 用于构建高效和更易于维护的 web 应用。 我们已经解释了一些基本概念,并且您已经看到了如何将它们应用于一个名为Tic-Tac-Toe的实际应用的多个示例。
到目前为止,你已经很好地进步了,因为你已经吸收了 ASP.NET Core 2.0 应用具有内部结构,如何正确配置它们,以及如何使用自定义行为扩展它们,这是未来构建您自己的应用的关键。
但我们不要止步于此! 在本章的最后,你将发现如何最好地实现缺失的组件,进一步发展现有的组件,并添加客户端代码,以允许你拥有一个完整运行的端到端三连字应用。
在本章中,我们将涵盖以下主题:
- 使用 JavaScript、捆绑和缩小优化客户端开发
- 使用 WebSockets 进行实时通信
- 利用会话和用户缓存管理
- 为多语言用户界面应用全球化和本地化
- 配置您的应用和服务
- 使用测井和遥测技术进行监测和监督
- 实现高级依赖注入概念
- 一次构建并在多个环境中运行
使用 JavaScript 进行客户端开发
在前一章中,您使用 MVC 模式创建了一个主页和一个用户注册页面。 您实现了一个控制器(UserRegistrationController)和一个相应的视图,用于处理用户注册请求。 然后您添加了一个服务(UserService)和中间件(CommunicationMiddleware),但是我们刚刚开始,所以它们还没有完成。

当与Tic-Tac-Toe应用的初始工作流相比较时,我们可以看到仍有许多东西缺失,比如整个客户端部分,真正与通信中间件一起工作,以及我们仍需要实现的多个其他特性。
让我们从客户端部分开始,看看如何应用更高级的技术。 然后,您将学习如何尽可能地优化一切。
如果您还记得,上次我们是在用户向注册表单提交数据之后停止的,注册表单被发送到UserService。 然后,我们只显示了一条纯文本消息,如下所示:

但是,这里的处理还没有结束。 我们需要添加整个电子邮件确认过程使用客户端开发和 JavaScript,这是我们接下来要做的:
- 打开 Visual Studio 2017 并打开井字游戏项目。 在
UserRegistrationController中添加一个名为EmailConfirmation的新方法:
[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启动应用,注册一个新用户,并验证新的 EmailConfirmation 页面是否正确显示:

非常好,您已经实现了完成用户注册过程所需的第一组修改。 在接下来的部分中,我们需要检查用户是否确认了他的电子邮件地址。 让我们看看接下来怎么做:
- 在
IUser接口中添加两个新方法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的新模型。 这将用于用户注册成功后的游戏邀请:
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属性:
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方法生成相应的视图,同时选择 Create 模板并使用前面的GameInvitationModel作为 Model 类:

- 修改自动生成的视图,删除所有不必要的输入控件,只留下
EmailTo输入控件:
@model TicTacToe.Models.GameInvitationModel
@{
ViewData["Title"] = "Index";
}
<h4>GameInvitationModel</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Index">
<input type="hidden" asp-for="Id" />
<input type="hidden" asp-for="InvitedBy" />
<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-default" />
</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启动应用,注册新用户,验证 Email 确认页面是否和之前一样。 在 Microsoft Edge 中,按F5重新加载页面,如果一切正常,你现在应该被重定向到游戏邀请页面:

太好了,又有进步了! 在游戏邀请之前,一切都很顺利,但不幸的是,仍然需要用户干预。 用户需要按F5手动刷新 Email 确认页面,直到 Email 确认完成; 只有这样,他才会被重定向到游戏邀请页面。
整个刷新过程必须在下一步中自动化并优化。 你的选择是:
- 在页面的标题部分放置一个 HTML 元刷新标记
- 使用简单的 JavaScript,它以编程方式进行刷新
- 使用 jQuery 实现XMLHttpRequest(XHR
HTML3 引入了 meta refresh 标签,可以在一定时间后自动刷新页面。 然而,这种方法是不可取的,因为它创建了一个高服务器负载,并且在 Microsoft Edge 的安全设置可能完全禁用它,一些广告拦截器将停止它的工作。 所以,如果你使用它,你不能确定它是否能正常工作。
使用简单的 JavaScript 可以很好地以编程的方式自动化页面刷新,但它主要有相同的缺陷,因此不推荐使用。
XHR 是我们真正需要的,因为它提供了我们的井字策略应用所需要的东西。 它允许:
- 更新网页而不重新加载他们
- 即使在页面加载之后,也从服务器请求和接收数据
- 发送数据到后台的服务器

现在,您将使用 XHR 来自动化和优化用户注册电子邮件确认处理的客户端实现。 这样做的步骤如下:
- 在
wwwroot文件夹中创建名为app的新文件夹(此文件夹将包含以下步骤中的所有客户端代码),并在此文件夹中创建名为js的子文件夹。 - 在
wwwroot/app/js文件夹中添加一个新的 JavaScript 文件scripts1.js,其内容如下:
var interval;
function EmailConfirmation(email) {
interval = setInterval(() => {
CheckEmailConfirmationStatus(email);
}, 5000);
}
- 在
wwwroot/app/js文件夹中添加一个新的 JavaScript 文件scripts2.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标签之前添加一个新的Development环境元素(最好将其放在那里):
<environment include="Development">
<script src="~/app/js/scripts1.js"></script>
<script src="~/app/js/scripts2.js"></script>
</environment>
- 更新通信中间件中的
Invoke方法,并添加一个名为ProcessEmailConfirmation的新方法,它将模拟电子邮件确认:
public async Task Invoke(HttpContext context)
{
if (context.Request.Path.Equals(
"/CheckEmailConfirmationStatus"))
{
await ProcessEmailConfirmation(context);
}
else
{
await _next?.Invoke(context);
}
}
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函数的调用来更新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 应用,并使用捆绑和缩小
正如你在第四章,中看到的,ASP 的基本概念.NET Core 2.0 -第 1 部分,我们选择了经过社区验证的 Bower 作为客户端包管理器。 我们保持了bower.json文件不变,这意味着我们恢复了四个默认包,并在 ASP 中添加了一些引用.NET Core 2.0 布局页面使用它们:

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

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

- 重新启动 Visual Studio。 您现在要通过捆绑和缩小来优化打开连接的数量和带宽使用。 为此,向项目中添加一个名为
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的新文件。 - 在任务运行器资源管理器中,你可以看到你为项目配置的捆绑和缩小过程:

- 右键单击 Update all files 并选择 Run。 现在你可以更详细地看到和理解这个过程在做什么:

-
通过右键单击 Update 所有文件并选择 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游戏,并进一步优化它并添加缺失的组件。
使用 WebSockets 进行实时通信
在上一节的末尾,一切都像预期的那样完全自动化地工作。 然而,仍有一些改进的空间。
实际上,客户端定期向服务器端发送请求,以查看电子邮件确认状态是否已更改。 这可能会导致大量请求查看状态是否发生了变化。
此外,一旦电子邮件被确认,服务器端不能立即通知客户端,因为它必须等待客户端请求的响应。
在本节中,您将了解 WebSockets(https://docs.microsoft.com/en-us/aspnet/core/fundamentals/websockets)的概念,以及它们将如何允许您进一步优化客户端实现。
WebSockets 支持基于 TCP 的持久双向通信通道,这对于需要运行实时通信场景(聊天、股票行情、游戏等)的应用尤其有趣。 碰巧,我们的应用是一个游戏,它是主要的应用类型之一,很大程度上受益于直接使用套接字连接。
Note that you could also consider SignalR as an alternative. At the time of writing this book, the SignalR Core version was not yet available. However, it could be available after publication, so you should look it up and use it instead if it is available. It will provide a better solution for real-time communication scenarios and encapsulate some of the functionalities missing from WebSockets you might have implemented for yourself manually.
You can look it up at https://github.com/aspnet/SignalR.
让我们通过使用 WebSockets 来优化Tic-Tac-Toe应用的客户端实现:
- 转到
Configure方法中的一字棋Startup类,在通信中间件和 MVC 中间件之前添加 WebSockets 中间件(记住中间件调用顺序对确保正确的行为很重要):
app.UseWebSockets();
app.UseCommunicationMiddleware();
...
- 更新通信中间件,为 WebSockets 通信添加两个新方法
SendStringAsync和ReceiveStringAsync:
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);
}
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的新方法,通过 WebSockets 进行邮件确认处理:
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方法,并从上一步中添加对 WebSockets 特定方法的调用,同时仍然保留不支持 WebSockets 的浏览器的标准实现:
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"))
{
await ProcessEmailConfirmation(context);
}
else
{
await _next?.Invoke(context);
}
}
- 修改
scripts1.js文件,添加一些特定于 websocket 的代码来打开和使用 socket:
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文件,添加一些特定于 websockets 的代码,用于打开和使用 sockets,并在邮件确认后重定向到游戏邀请页面:
function CheckEmailConfirmationStatus(email) {
$.get("/CheckEmailConfirmationStatus?email=" + email,
function (data) {
if (data === "OK") {
if (interval !== null)
clearInterval(interval);
window.location.href = "/GameInvitation?email=" + email;
}
});
}
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) {
};
};
- 当您启动应用并继续进行用户注册时,您将获得是否支持 WebSockets 的信息。 如果是,你会被重定向到游戏邀请页面,像以前一样,但好处是更快的处理时间:

这就是我们在 ASP 下进行客户端开发和优化的过程.NET Core 2.0。 现在,您将看到如何进一步扩展和完成井字策略应用。 这些核心概念将帮助您在日常工作中构建多语言的、可用于生产的 web 应用。
利用会话和用户缓存管理
作为一名 web 开发人员,您可能知道 HTTP 是一种无状态协议,这意味着默认情况下不存在会话这样的概念。 每个请求都是独立处理的,不同请求之间不保留任何值。
尽管如此,处理数据有不同的方法。 您可以使用查询字符串、提交表单数据,也可以使用 cookie 在客户机上存储数据。 然而,所有这些机制或多或少都是手动的,需要自己管理。
如果你是一个有经验的 ASP.NET 开发人员,您将熟悉会话状态和会话变量的概念。 这些变量存储在 web 服务器上,您可以在不同的用户请求有一个中心位置来存储和接收数据时访问它们。 会话状态非常适合存储特定于会话的用户数据,而不需要永久持久性。
Note that it is best 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, other users might continue with the same session, which could provide security risks.
ASP.NET Core 2.0 通过使用专用的会话中间件来提供会话状态和会话变量。 基本上,有两种不同类型的会话提供程序:
- 内存中的会话提供程序(本地到单个服务器)
- 分布式会话提供程序(在多个服务器之间共享)
让我们看看如何在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的新方法。 这将包含在一个会话变量中存储用户区域性设置的代码:
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;
}
.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 2.0 中的
ConfigureServices方法Startup类:
services.AddSession(o =>
{
o.IdleTimeout = TimeSpan.FromMinutes(30);
});
- 在
Startup类的Configure方法中激活会话中间件,将其添加到静态文件中间件之后:
app.UseStaticFiles();
app.UseSession();
- 更新
GameInvitationController中的Index方法,设置 email 会话变量:
[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";
});
- 分布式复述,缓存:
services.AddDistributedRedisCache(o =>
{
o.Configuration = _configuration["CacheRedis:Connection"];
o.InstanceName = _configuration["CacheRedis:InstanceName"];
});
我们在本节中添加了一个新的用户界面语言下拉菜单,但是您还没有看到如何在您的应用中处理多种语言。 不能再浪费时间了; 让我们看看如何做到这一点,并在下一节中使用下拉菜单和会话变量来动态地更改用户界面语言。
为多语言用户界面应用全球化和本地化
有时您的应用取得了成功,有时甚至取得了相当大的成功,因此您希望在国际上向更广泛的受众提供它们,并在更大的规模上部署它们。 但糟糕的是,您无法轻松实现这一点,因为您从一开始就没有想到要对应用进行本地化,而现在您必须修改已经运行的应用,并冒着回归和不稳定的风险。
不要掉进这个陷阱! 从一开始就要考虑您的目标用户和未来的部署策略!
在项目开始的时候就应该考虑本地化应用,特别是在使用 ASP 的时候,这是非常容易和直接的.NET Core 2.0 框架。 它提供了用于此目的的现有服务和中间件。
为显示、输入和输出构建支持不同语言和文化的应用称为全球化,而使全球化应用适应特定的文化称为本地化。
本地化 ASP 有三种不同的方法.NET Core 2.0 web 应用:
- 字符串定位器
- 视图定位器
- 本地化数据注释
在本节中,您将了解全球化和本地化的概念,以及它们如何让您进一步优化您的网站以实现国际化。
For additional information on globalization and localization, please visit https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization.
那么,你如何开始呢? 首先,让我们看看如何让Tic-Tac-Toe应用可本地化,通过使用 String Localizer:
- 转到
Services文件夹并添加一个名为CultureProviderResolverService的新服务。 这将通过查看Culture查询字符串、Culturecookie 和Culture会话变量(在本章前一节中创建)来检索区域性设置。 - 通过从
RequestCultureProvider继承CultureProviderResolverService来实现CultureProviderResolverService,并覆盖其特定的方法:
public class CultureProviderResolverService :
RequestCultureProvider
{
private static readonly char[] _cookieSeparator = new[] {'|' };
private static readonly string _culturePrefix = "c=";
private static readonly string _uiCulturePrefix = "uic=";
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;
}
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;
}
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);
}
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;
}
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方法中,并定义所支持的区域性:
Note that the order of adding middlewares is important, as you have already seen. You have to add the Localization Middleware just before the MVC Middleware.
...
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(...);
Note that you can use different methods to change the culture of your applications during runtime:
**Query strings: **Provide the culture in the URI
Cookies: Store the culture in a cookie
Browser: Browser page language settings
Custom: Implement your own provider (shown in this example)
- 在解决方案资源管理器中,添加一个名为
Localization的新文件夹(它将用于存储资源文件),创建一个名为Controllers的子文件夹,然后在该文件夹中添加一个名为GameInvitationController.resx的新资源文件。
Note that you can put your resource files either into subfolders (for example, Controllers, Views, and more) or directly name your files accordingly (for example, Controllers.GameInvitationController.resx, Views.Home.Index.resx, and more). However, we advise you to use the folder approach for clarity, readability, and better organization of your files.

If you have errors when using your resource files with .NET Core, right-click on each file and select Properties. Then, check in each file that the Build Action is set to Content instead of Embedded Resource. There are bugs that should have been fixed by the final release, but if they are not, you can use this handy work-around 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 2.0 框架为本地化视图提供了一些强大的特性。 在下一个例子中,你将使用视图本地化方法:
- 更新
Startup类中的ConfigureServices方法,并将视图本地化服务添加到 MVC 服务声明中:
services.AddMvc().AddViewLocalization(
LanguageViewLocationExpanderFormat.Suffix,
options => options.ResourcesPath = "Localization");
- 修改
Views/ViewImports.cshtml文件,添加 View Localizer 功能,使其适用于所有视图:
@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文件,为法语标题添加一个条目:

- 启动应用,将用户界面语言设置为 English:

- 通过“用户界面e Lalanguage”下拉菜单,将应用语言修改为法语。 标题现在应该显示在法语:

您已经了解了如何轻松地本地化视图,但是如何本地化视图中使用数据注释的表单呢? 让我们更详细地看一下; 你会惊讶于 ASP.NET Core 2.0 框架必须提供在这种情况下!
我们将在下面的例子中完全本地化用户注册表单:
-
在解决方案资源管理器中,转到
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">
...
- 启动应用,通过“用户界面语言”下拉菜单设置语言为法语,然后进入“用户注册”界面。 标题应该用法语显示。 点击 Create 而不输入任何内容,看看会发生什么:

这里缺了点什么。 您已经为页面标题和用户注册页面的副标题添加了本地化,但是我们仍然缺少表单的一些本地化。 但我们遗漏了什么?
您肯定已经看到错误消息还没有本地化和翻译。 我们正在使用 Data Annotation 框架进行错误处理和表单验证,那么如何本地化 Data Annotation 验证错误消息呢? 这就是你现在将要看到的:
- 在
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文件:

- 更新
UserModel实现,使其能够使用上面的资源文件:
public class UserModel
{
public Guid Id { get; set; }
[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; }
public bool IsEmailConfirmed { get; set; }
public System.DateTime? EmailConfirmationDate { get; set; }
public int Score { get; set; }
}
- 重新构建解决方案并启动应用。 你会看到整个用户注册页面,包括错误消息,现在完全翻译时,更改用户界面语言为法语:

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

- 将新的 Email 服务添加到
Startup类的ConfigureServices方法中(我们想要一个单一的应用实例,所以将其添加为 Singleton):
services.AddSingleton<IEmailService, EmailService>();
- 更新
UserRegistrationController以访问在上一步中创建的EmailService:
readonly IUserService _userService;
readonly IEmailService _emailService;
public UserRegistrationController(IUserService userService,
IEmailService emailService)
{
_userService = userService;
_emailService = emailService;
}
- 更新
UserRegistrationController中的EmailConfirmation方法来调用EmailService中的SendEmail方法:
[HttpGet]
public async Task<IActionResult> EmailConfirmation(string email)
{
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 web
site, please click here to confirm your email " +
$"{Url.Action(urlAction)}";
try
{
_emailService.SendEmail(email,
"Tic-Tac-Toe Email Confirmation", message).Wait();
}
catch (Exception e)
{
}
if (user?.IsEmailConfirmed == true)
return RedirectToAction("Index", "GameInvitation",
new { email = email });
ViewBag.Email = email;
return View();
}
很好,你现在有了电子邮件服务,但你的工作还没有完成。 您需要能够配置服务,以设置与环境相关的参数(SMTP 服务器名称、端口、SSL 等),然后发送电子邮件。 将来几乎所有您创建的服务都将具有某种类型的配置,这些配置应该可以在您的代码外部进行配置。
ASP.NET Core 2.0 有一个内置的配置 API。 它提供了在应用运行时从多个源读取配置数据的各种功能。 Name-value对用于配置数据持久化,可以划分为多级结构。 此外,配置数据可以自动反序列化为普通的旧 c#对象(POCO),其中只包含私有成员和属性。
支持以下配置源:
- 配置文件(JSON、XML,甚至是经典的 INI 文件)
- 环境变量
- 命令行参数
- 内存中的。net 对象
- 加密用户商店
- Azure 关键库
- 定制的供应商
For more information on the Configuration API, please visit https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration?tabs=basicconfiguration.
让我们看看如何通过使用 ASP 来快速配置电子邮件服务.NET Core 2.0 配置 API 和 JSON 配置文件:
- 向项目添加一个新的
appsettings.json配置文件,并添加以下自定义部分。 这将用于配置电子邮件服务:
"Email": {
"MailType": "SMTP",
"MailServer": "localhost",
"MailPort": 25,
"UseSSL": false,
"UserId": "",
"Password": "",
"RemoteServerAPI": "",
"RemoteServerKey": ""
}
- 在解决方案资源管理器中,在项目的根目录下创建一个名为
Options的新文件夹。 在这个文件夹中添加一个名为EmailServiceOptions的新 POCO,并为前面看到的选项实现私有成员和公共属性:
public class EmailServiceOptions
{
public string MailType { get; set; }
public string MailServer { get; set; }
public string MailPort { get; set; }
public string UseSSL { get; set; }
public string UserId { get; set; }
public string Password { get; set; }
public string RemoteServerAPI { get; set; }
public 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以 Debug 模式启动应用,验证是否已从配置文件中正确检索到电子邮件服务选项值。 如果你有一个 SMTP 服务器,你也可以验证邮件是否真的被发送:

您已经看到了如何通过使用 ASP 的内置配置 API 来配置您的应用和服务.NET Core 2.0,它允许您编写更少的代码,提高效率,同时最终提供一个更优雅、更可维护的解决方案。
使用日志
当你在开发你的应用时,你会使用一个著名的集成开发环境,比如 Visual Studio 2017 或 Visual Studio Code,如本书开篇章节所述。 你每天都这样做,你做的大多数事情都是反射性的,一段时间后你会自动地去做。
通过使用 Visual Studio 2017 的高级调试特性,您可以很自然地调试应用并理解运行时发生的事情。 查找变量值、查看以何种顺序调用哪些方法、了解注入了哪些实例以及捕获异常,是构建健壮并响应业务需求的应用的关键。
然后,在将应用部署到生产环境时,您突然错过了所有这些特性。 您很少会发现安装了 Visual Studio 的生产环境,但是,错误和意外行为会发生,您需要能够尽快地理解和修复它们。
这就是测井和遥测技术发挥作用的地方。 检测应用和日志记录时进入和离开的方法,以及重要的变量值或任何你认为重要的信息在运行时,你将能够去应用日志,看看发生在生产环境的问题。
在上一节中,我们添加了用于发送电子邮件的 Email Service,并使用外部配置文件对其进行了配置。 如果配置的 SMTP 服务器没有响应怎么办? 如果我们忘记将服务器设置从开发更新到生产呢? 现在,我们只会在浏览器中显示一个异常消息:

在本节中,我们将向您展示如何使用日志记录和异常处理来为这类问题提供更好、更工业化的解决方案。
ASP.NET Core 2.0 提供了内置的对以下目标日志的支持:
- Azure AppServices 乳制品
- 控制台
- Windows 事件源
- 跟踪
- 调试器输出
- 应用的见解
但是默认情况下不支持文件、数据库和日志服务。 如果要将日志发送到这些目标,则需要使用第三方日志记录器解决方案,如 log4net、Serilog、NLog、Apache、ELMAH 或 logger。
你也可以通过实现ILoggerProvider接口轻松创建你自己的提供商,这就是你将在这里看到的:
- 添加一个新的类库(.NET Core)项目到解决方案,并将其命名为
TicTacToe.Logging(删除自动生成的Class1.cs文件):

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

- 在 TicTacToe Web 应用项目中添加一个项目引用,以便能够使用
TicTacToe.Logging类库中的资产:

- 添加一个名为
LogEntry的新类。 这将包含日志数据:
internal 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的新类。 这将用于文件操作:
internal class FileLoggerHelper
{
private string fileName;
public FileLoggerHelper(string fileName)
{
this.fileName = fileName;
}
static ReaderWriterLock locker = new ReaderWriterLock();
internal 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接口:
public sealed class FileLogger : ILogger
{
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);
}
public IDisposable BeginScope<TState>(TState state)
{
return null;
}
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);
}
public bool IsEnabled(LogLevel logLevel)
{
return (_filter == null || _filter(_categoryName, logLevel));
}
}
- 添加一个名为
FileLoggerProvider的新类,并实现ILoggerProvider接口。 这将在稍后注入:
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;
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);
}
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;
}
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;
}
}
- 在 TicTacToe Web 项目中,在
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; }
}
- 在 TicTacToe Web 项目中,添加一个名为
ConfigureLoggingExtension的新扩展到Extensions文件夹:
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using TicTacToe.Logging;
...
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;
}
}
- 转到 TicTacToe Web 应用项目的
Program类,更新BuildWebHost方法,并调用之前的扩展:
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.CaptureStartupErrors(true)
.UseStartup<Startup>()
.PreferHostingUrls(true)
.UseUrls("http://localhost:5000")
.UseApplicationInsights()
.ConfigureLogging((hostingcontext, logging) =>
{
logging.AddLoggingConfiguration(
hostingcontext.Configuration);
})
.Build();
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 2.0 应用(Controllers,Services等)。
让我们快速添加一些日志到井字游戏应用:
- 更新
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}");
- 更新 Email Service 实现,在其构造函数中添加一个记录器,并添加一个新的
SendMail方法:
public class EmailService : IEmailService
{
private EmailServiceOptions _emailServiceOptions;
readonly ILogger<EmailService> _logger;
public EmailService(IOptions<EmailServiceOptions>
emailServiceOptions, ILogger<EmailService> logger)
{
_emailServiceOptions = emailServiceOptions.Value;
_logger = logger;
}
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("example@example.com",
emailTo, subject, message));
}
}
catch (Exception ex)
{
_logger.LogError($"Cannot send email {ex}");
}
return Task.CompletedTask;
}
}
- 打开生成的日志文件,分析其内容:

实现高级依赖注入概念
在前一章中,您看到了依赖注入(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方法中添加新的游戏邀请服务(我们想要一个单一的应用实例,所以将其添加为 Singleton):
services.AddSingleton<IGameInvitationService,
GameInvitationService>();
- 更新
GameInvitationController中的Index方法,使用RequestServicesprovider 通过方法注入一个游戏邀请服务实例:
[HttpPost]
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;, otherwise the .GetService<IGameInvitationService>(); method cannot be used and you will get build errors.
- 在
GameInvitationController中添加一个名为GameInvitationConfirmation的新方法:
[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);
}
}
- 在
scripts2.js文件中添加一个名为CheckGameInvitationConfirmationStatus的方法。 您可以使用与现有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方法,添加具体的游戏邀请案例:
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";
}
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;
}
};
socket.onopen = function () {
var json = JSON.stringify({
"Operation": operation,
"Parameters": parameter
});
socket.send(json);
};
socket.onclose = function (event) {
};
};
- 在通信中间件中添加一个名为
ProcessGameInvitationConfirmation的新方法。 这将在不支持 WebSockets 的浏览器下处理游戏邀请请求:
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的新方法,并向通信中间件添加附加参数。 这将处理游戏邀请请求,同时使用支持它的浏览器的 WebSockets:
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方法。 从现在开始,这必须适用于电子邮件确认和游戏邀请确认,无论是否使用 WebSockets:
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 2.0 web 应用。 这是注入服务的首选方法,您应该在适当的时候使用它。
此外,你已经很好地实现了井字游戏。 几乎所有关于用户注册、电子邮件确认、游戏邀请和游戏邀请确认的内容现在都已经实现了。
一次构建并在多个环境中运行
在构建应用之后,您必须考虑将它们部署到不同的环境中。 正如您在前面关于配置的部分中已经看到的,您可以使用配置文件来更改服务甚至应用的配置。
在多个环境的情况下,您必须为每个环境复制appsettings.json文件并相应地将其命名为appsettings.{EnvironmentName}.json。
ASP.NET Core 2.0 将自动按层次顺序检索配置设置,首先从公共appsettings.json文件中检索,然后从相应的appsettings.{EnvironmentName}.json文件中检索,同时在必要时添加或替换值。
然而,根据不同的部署环境和配置开发使用不同组件的条件代码一开始似乎很复杂。 在传统应用中,您必须创建大量代码来自己处理所有不同的情况,然后对其进行维护。
在 ASP.NET Core 2.0,您可以使用大量的内部功能来实现这个目标。 然后,您可以简单地使用环境变量(开发、登台、生产等)来指示特定的运行时环境,从而为该环境配置应用。
正如您将在本节中看到的,您可以使用特定的方法名甚至类名来使用 ASP 提供的现有注入和覆盖机制.NET Core 2.0 开箱即用,用于配置您的应用。
在下面的例子中,我们正在向应用(SendGrid)添加一个特定于环境的组件,只有当应用部署到特定的生产环境(Azure)时才需要使用它:
- 将 SendGrid NuGet 包添加到项目中。 这将用于未来 Azure 生产部署的Tic-Tac-Toe应用:

- 在
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;
}
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 of the common code into a ConfigureCommon method and call it from the other methods, as shown below for the specific ConfigureServices methods.
- 按F5启动应用,并验证一切仍然正常运行。 您应该看到添加的方法将被自动使用,并且应用功能齐全。
这很简单也很直接! 环境没有特定的条件代码,没有复杂的发展和维护,只是非常清晰和易于理解的方法,这些方法包含它们为之开发的环境名称。 对于一次性构建并在多个环境中运行的问题,这是一个非常干净的解决方案。
但是,这还不是全部! 如果我们告诉你,你不需要有一个单独的启动类? 如果每个环境都有一个专用的 Startup 类,其中只有适用于其上下文的代码,那会怎么样呢? 那太好了,对吧? 这正是 ASP.NET Core 2.0 提供。
为了能够在每个环境中使用专用的 Startup 类,你只需要更新 Program 类,它是 ASP 的主要入口点.NET Core 2.0 应用。 您只需更改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 2.0 提供了这些有用的特性,让我们的生活变得轻松多了。
总结
在本章中,你已经学习了 ASP 的一些更高级的概念.NET Core 2.0,并实现了Tic-Tac-Toe应用中缺少的一些组件。
首先,您使用 JavaScript 创建了Tic-Tac-Toeweb 应用的客户端部分。 我们已经探索了如何通过使用捆绑和缩小来优化你的 web 应用,以及如何使用 WebSockets 来实现实时通信场景。
此外,您还看到了如何从集成的用户和会话处理中获益,这在一个易于理解的示例中显示。
然后,我们介绍了多语言用户界面、应用和服务配置的全球化和本地化,以及日志记录,以便更好地理解应用在运行时发生的事情。
最后,我们通过一个实际示例演示了如何一次性构建应用,然后根据部署目标使用多个ConfigureServices和Configure方法以及多个Startup类的概念,使其适应不同的环境。
在下一章中,我们将讨论 ASP.NET Core MVC、MVC 中的 Razor(区域、布局、局部视图等等)、Razor 页面和视图引擎。
六、创建 MVC 应用
当今大多数现代 web 应用都是基于模型视图控制器模式,也通常称为MVC。 您应该已经注意到,在前面的章节中,我们也使用它来构建井字游戏示例应用的基础。
因此,您已经在多个地方使用了它,甚至不知道在后台发生了什么,以及为什么应用这个特定模式很重要。
ASP 的初始前版本.NET MVC 发布于 2007 年。 它是由 Scott Guthrie 构想和设计的,他也共同创建了 ASP.NET,以及领导开发团队的 Phil Haack。 第一个打包的官方版本是 ASP.NET MVC 1,发布于 2009 年。
从那时起,ASP.NET MVC 框架多年来已经证明了自己,直到有效地成为市场标准。 微软已经成功地将其发展成为一个工业化的、高效的框架,具有很高的开发人员生产力。
有许多 web 应用的例子充分利用了 MVC 所提供的多种特性。 两个很好的例子是 Stack Overflow 和 CodePlex。 它们为开发人员提供信息,并拥有非常高的用户基础,需要同时扩展到数千甚至数百万用户。
在本章中,我们将涵盖以下主题:
- 理解模型-视图-控制器模式
- 为多个设备创建专用的布局
- 使用“查看页”、“分部视图”、“查看组件”和“标记帮助程序”
- 将 web 应用划分为多个区域
- 应用视图引擎、单元测试和集成测试等高级概念
理解模型-视图-控制器模式
MVC 模式将应用分为三个主要层——模型、视图和控制器。 此模式的好处之一是关注点分离,也称为单一责任原则(SRP),它使独立开发、调试和测试应用特性成为可能。
当使用 MVC 模式时,用户请求被路由到控制器,控制器将使用模型检索数据和执行操作。 控制器选择相应的视图以显示给用户,同时为其提供来自模型的必要数据。
如果一个层(例如,视图)发生变化,其影响较小,因为它现在与应用的其他层(例如,控制器和模型)松散耦合。
测试应用的不同层也容易得多。 最后,通过使用这个模式,你将拥有更好的可维护性和更健壮的代码:

模型
模型包含逻辑数据结构以及应用的数据,独立于它们的可视化表示。 在 ASP.NET Core 2.0,它还支持本地化和验证,正如你在前面的章节中看到的。
模型可以与视图和控制器在同一个项目中创建,也可以在一个专门的项目中创建,以便更好地组织。 脚手架使用模型来自动生成视图。 此外,模型可以用于将表单自动绑定到实体对象。
在数据持久性方面,可以使用各种数据存储目标。 在数据库的情况下,你应该使用实体框架,它将在本书的以下章节中介绍。 在使用 Web api 时,模型是序列化的。
的观点
视图为应用提供可视化表示和用户界面元素。 当使用 ASP.NET Core 2.0,视图是使用 html 和 Razor 标记编写的。 它们通常有一个.cshtml文件扩展名。
一个视图可以包含一个完整的网页,一个网页部分(称为部分视图),或者一个布局。 在 ASP.NET Core 2.0 中,一个视图可以通过它们自己的行为被划分成逻辑的子分区,这些子分区被称为视图组件。
此外,Tag Helpers 允许您将 HTML 代码集中和封装在一个标签中,并在所有应用中使用它。 ASP.NET Core 2.0 已经包含了许多现有的 Tag Helpers 来提高开发人员的工作效率。
控制器
控制器管理模型和视图之间的交互。 它为您的应用提供逻辑行为和业务逻辑。 它选择为特定的用户请求呈现哪个视图。
一般来说,由于控制器提供应用的主要入口点,这意味着它们控制应用如何响应用户请求。
单元测试
单元测试的主要目标是验证控制器中的业务逻辑。 通常,单元测试放在它们自己的外部单元测试项目中,同时有多个测试框架可用(XUnit、NUnit 或 MSTest)。
如前所述,由于在使用 MVC 模式时一切都是完全解耦的,因此您可以使用单元测试在任何点独立于应用的其他部分来测试您的控制器。
集成测试
应用功能的端到端验证是通过集成测试完成的。 它们从应用用户的角度检查一切是否正常工作。 因此,控制器和它们对应的视图一起测试。
与单元测试一样,集成测试通常放在它们自己的测试项目中,您可以使用各种测试框架(XUnit、NUnit 或 MSTest)。 然而,对于这种类型的测试,您还需要使用 web 服务器自动化工具包。
为多个设备创建专用的布局
现代 web 应用使用网页布局来提供一致和一致的风格。 最好将 HTML 和 CSS 结合使用来定义这种布局。 在 ASP.NET Core 2.0 中,常见的网页布局定义集中在一个布局页面中。 这个页面包括所有常见的用户界面元素,比如页眉、菜单、边栏和页脚。
此外,常见的 CSS 和 JavaScript 文件在布局页面中被引用,因此它们可以在整个应用中使用。 这允许您减少视图中的代码,从而帮助您应用DRY(Don't Repeat Yourself)原则。
我们从井字游戏示例应用的早期版本就开始使用布局页面。 当我们在前一章添加它时,它第一次被介绍。 我们用它来给我们的应用一个现代的外观,正如你在这里看到的:

让我们更详细地看看布局页面,了解它是什么,以及如何利用它的特性为具有不同形式因素的多种设备(pc、电话、平板电脑等)创建专用布局。
第四章、ASP 的基本概念; NET Core 2.0 -第 1 部分,我们在Views\Shared文件夹中添加了一个名为_Layout.cshtml的布局页面。 当打开这个页面并分析它的内容时,你可以看到它包含了适用于你的应用中所有页面的通用元素(header, menu, footer, CSS, JavaScripts 等):

布局页面中的公共头部部分包含 CSS 链接,也包含 SEO 标签,如标题、描述和关键字。 正如你之前已经看到的,ASP.NET Core 2.0 提供了一个简洁的特性,它允许您通过环境标记(开发、登台、生产等)自动包含特定于环境的内容。
Bootstrap 已经成为渲染menu和navbar组件的准标准,这也是我们在Tic-Tac-Toe应用中也使用它的原因。
最好的做法是把常见的 JavaScript 文件放在布局页面的底部; 它们也可以根据 ASP.NET Core 环境标签。
您可以使用Views\_ViewStart.cshtml文件在中心位置定义所有页面的布局页面。 或者,如果你想手动设置一个特定的布局页面,你可以把它设置在页面的顶部:
@{
Layout = "_Layout";
}
为了更好地构建布局页面,可以定义部分来组织某些页面元素(包括公共脚本部分)的位置。 一个例子是您可以在布局页面中看到的脚本部分,它是我们在Tic-Tac-Toe应用的第一个示例中添加的。 默认情况下,它已经通过添加一个专用的元标签放在页面的底部:
RenderSection: @RenderSection("Scripts", required: false)
您还可以在视图中定义用于添加文件或客户端脚本的部分。 我们已经在 Email Confirmation View 的上下文中做过了,在这里你添加了一个调用客户端 JavaScriptEmailConfirmation方法的部分:
@section Scripts{
<script>
$(document).ready(function () {
EmailConfirmation('@ViewBag.Email');
});
</script>
}
理论的讨论已经够多了,让我们自己动手做点什么吧! 让我们看看如何优化Tic-Tac-Toe应用的移动设备:
- 我们想要改变移动设备的显示,所以打开 Visual Studio 2017,进入解决方案资源管理器,创建一个名为
Filters的新文件夹,然后添加一个名为DetectMobileFilter的新文件:
public class DetectMobileFilter : IActionFilter
{
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(
ActionExecutedContext context)
{
string userAgent = (context.HttpContext.Request.Headers
as FrameRequestHeaders)?.HeaderUserAgent;
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;
}
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";
}
}
public void OnActionExecuting(ActionExecutingContext context)
{
}
}
- 复制现有的
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">
...
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 in your other views, you will get errors in the next steps. However, this is only a temporary solution; we will see later in the chapter how to address this problem more effectively by using conditional statements.
- 更新资源文件。 这是一个英文主页索引资源文件的例子; 你还应该加上法语翻译:

- 通过以下步骤替换
@RenderBody()元素来修改Views/Shared/_Layout.cshtml文件;Desktopsection 应该被显示,Mobilesection 应该被忽略:
@RenderSection("Desktop", required: false)
@{IgnoreSection("Mobile");}
@RenderBody()
- 通过以下步骤替换
@RenderBody()元素来修改Views/Shared/_LayoutMobile.cshtml文件;Mobilesection 应该被显示,Desktopsection 应该被忽略:
@RenderSection("Mobile", required: false)
@{IgnoreSection("Desktop");}
@RenderBody()
- 转到
Views/_ViewStart.cshtml文件,更改所有网页的布局分配,以便能够使用前面代码中的布局定义:
@{Layout = Convert.ToString(ViewData["Layout"]);}
- 在最后一步中,更新
Startup类,并将DetectMobileFilter作为参数添加到 MVC 服务注册中:
services.AddMvc(o =>
o.Filters.Add(typeof(DetectMobileFilter)))...
- 在 Microsoft Edge 中正常启动Tic-Tac-Toe应用:

- 点击F12打开开发人员工具,进入仿真选项卡,选择移动设备,然后重新加载井字策略应用; 它将显示为你已经在设备上打开它:

在本节中,您已经了解了如何为特定设备提供特定的布局。 现在,您将看到如何应用其他高级 ASP.NET Core 2.0 MVC 特性有助于提高生产力和更好的应用。
使用“查看页”、“分部视图”、“查看组件”和“标记帮助程序”
ASP.NET Core 2.0 和 Razor 与 Visual Studio 2017 结合在一起时,提供了一些创建 MVC 视图的功能。 在本节中,您将看到这些功能如何帮助您提高工作效率。
例如,你可以使用 Visual Studio 2017 集成的脚手架特性来创建视图,你已经在前面的章节中多次做过了。 它允许您自动生成以下类型的视图:
- 视图页面
- 局部视图
你想了解它们是什么,以及如何使用 Visual Studio 2017 有效地使用它们吗? 保持警惕,因为我们现在要详细解释一切。
使用视图页面
View Pages 用于根据操作呈现结果,并对 HTTP 请求进行响应。 在 MVC 方法中,它们定义和封装应用的可见部分—表示层。 而且,它们使用.cshtml文件扩展名,默认存储在应用的Views文件夹中。
Visual Studio 2017 的脚手架特性提供了不同的视图页面模板,正如你在这里看到的:
- Create:生成插入数据的表单
- Edit:生成更新数据的表单
- Delete:生成一个显示记录的表单,并带有一个确认删除的按钮
- Details:生成一个显示记录的表单,有两个按钮,一个用于编辑表单,一个用于删除显示的记录页面
- List:生成一个 HTML 表来显示对象列表
- Empty:不使用任何模型生成空白页面
如果你不能使用 Visual Studio 2017 来生成你的页面视图,你可以通过自己将它们添加到Views文件夹来手动实现它们。 在这种情况下,您必须遵守 MVC 约定。 因此,在匹配动作名称的同时,将它们添加到相应的子文件夹中,以允许 ASP.NET 来查找您手动创建的视图。
让我们为Tic-Tac-Toe游戏创建排行榜,并观察其运行情况:
- 打开解决方案资源管理器,转到
Views文件夹并创建一个新的子文件夹Leaderboard,右键单击该文件夹并在向导中选择 Add | new Item | MVC View Page,然后单击 Add 按钮:

- 打开创建的文件并清除其内容,通过在页面顶部添加以下说明,将排行榜视图与用户模型关联起来:
@model IEnumerable<TicTacToe.Models.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文件夹并选择 Add | Class,将其命名为LeaderboardController.cs,然后单击 Add 按钮:

- 更新排行榜控制器的执行:
public class LeaderboardController : Controller
{
public IActionResult Index()
{
return View();
}
}
Note that Razor matches views with actions as follows:
<actionname>.cshtml or <actionname>.<culture>.cshtml in the Views/<controllername> folder
- 更新
Views/Shared文件夹中的_Layout.cshtml和_LayoutMobile.cshtml文件,并添加一个 ASP.NET 标签帮助器,用于在navbar菜单中调用Home元素之后的新排行榜视图:
<li><a 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 表,同时使用前面提到的控制结构:
@model IEnumerable<TicTacToe.Models.UserModel>
@{ViewData["Title"] = "Index";}
<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>}
<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>
</div>
</div>
- 在
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());
}
- 更新排行榜控制器以调用新方法:
public class LeaderboardController : Controller
{
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#完全相同。
如前所述,您需要在所有视图中定义Desktop和Mobile部分:
@section Desktop { }
@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 2.0 和 Razor。
For additional information on Razor please visit: https://docs.microsoft.com/en-us/aspnet/core/mvc/views/razor
使用局部视图
您已经看到了如何使用 Razor 创建视图页面,但有时您必须在所有或部分视图页面中重复元素。 在这种情况下,如果您可以在视图中创建可重用组件,这不是很有帮助吗? 不出所料,ASP.NET Core 2.0 通过提供所谓的分部视图在默认情况下实现了这个特性。
分部视图在调用视图页面中呈现。 与标准 View Pages 一样,它们也有.cshtml文件扩展名。 您可以定义它们一次,然后在所有视图页面中使用它们。 这是一种通过减少代码重复来优化代码的好方法,这样可以提高质量,减少维护!
你将看到如何从现在的优化布局和移动布局页面使用单一菜单受益:
- 转到
Views/Shared文件夹,添加一个新的 MVC 视图页面_Menu.cshtml,它将被用作菜单部分视图:

- 从一个布局页面复制
nav栏,并粘贴到菜单部分视图:
<nav class="navbar navbar-inverse navbar-fixed-top">
...
</nav>
- 在两个布局页面中用
@Html.Partial("_Menu")替换nav栏。 - 启动应用并验证一切是否仍像以前一样工作。 你不应该看到任何区别,但这是一件好事; 你已经将菜单封装并集中在分部视图中了。
使用视图组件
您已经了解了如何通过使用分部视图(可以从应用中的任何视图页面调用)来创建可重用组件,并将此概念应用于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
{
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的新视图:
@using Microsoft.AspNetCore.Http
@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>
}
We advise using the following syntax for putting all Partial Views for your View Components in their corresponding folders:
Views\Shared\Components\<ViewComponentName>\<ViewName>
- 更新
_ViewImports.cshtml文件以使用视图组件:
@addTagHelper *, TicTacToe
- 在
Views文件夹中创建一个新文件夹GameSession,然后添加一个新视图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>
}
@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中添加一个公共构造函数来获取 User Service 的实例:
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方法:
public class GameSessionController : Controller
{
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);
}
}
Note that for calling RequestServices.GetService<T>(); you must also add using Microsoft.Extensions.DependencyInjection; as you have already done in other examples.
- 启动应用,注册新用户,邀请其他用户玩游戏,等待新的 game Session 页面显示:

使用标签的帮手
标签助手是 ASP 的一个新特性.NET Core 2.0,它允许在创建和呈现 HTML 元素时使用服务器端代码。 它们可以与现有的、众所周知的用于呈现 HTML 内容的 HTML 帮助程序相比较。
ASP.NET Core 2.0 已经提供了许多内置的标签帮助程序,比如可以在应用中使用的ImageTagHelper和LabelTagHelper。
在创建自己的 Tag Helpers 时,可以基于元素名称、属性名称或父标记来定位 HTML 元素。 然后你可以在你的视图中使用标准的 HTML 标签,而在 web 服务器上应用 c#编写的表示逻辑。
此外,您甚至可以创建自定义标记,正如您将在本节中看到的创建 Gravatar 标记。 你将在井字游戏应用中使用这个:
- 打开解决方案资源管理器并创建一个名为
TagHelpers的新文件夹,然后添加一个名为GravatarTagHelper.cs的新类。 - 实现
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; }
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:image/jpeg;base64,{base64String}");
}
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;
}
}
private byte[] GetPhoto(string email)
{
var httpClient = new HttpClient();
return httpClient.GetByteArrayAsync(
new Uri($"http://www.gravatar.com/avatar/
{HashEmailForGravatar(email)}")).Result;
}
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; }
}
- 在
Views/Shared文件夹中添加一个新的分部视图_Account.cshtml:
@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 to 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应用,并在同一电子邮件注册。 你现在应该在顶部菜单中看到一个带有照片和显示名称的新下拉菜单:

Note that you have to be online for this to work. If you want to test your code offline, you should put a photo in the wwwroot\images folder called no-photo.jpg; otherwise, you will get an error since no offline photo can be found.
易于理解和使用,但什么时候使用视图组件和什么时候使用标签助手? 下面这些简单的规则可以帮助你决定什么时候使用这些概念:
- 每当你需要视图模板、渲染一组元素并将服务器代码与之关联时,就会使用视图组件。
- 标签帮助程序用于将行为附加到单个 HTML 元素,而不是一组元素。
将 web 应用划分为多个区域
有时,在处理较大的 web 应用时,将它们逻辑地分离为多个较小的功能单元可能会很有趣。 然后每个单元都可以拥有自己的控制器、视图和模型,这使得随着时间的推移更容易理解、管理、发展和维护它们。
ASP.NET Core 2.0 提供了一些基于文件夹结构的简单机制,用于将 web 应用划分为多个功能单元,也称为Areas。
例如,将应用中的标准 Area 与更高级的管理 Area 分开。 然后,标准 Area 甚至可以对某些页面启用匿名访问,同时要求对其他页面进行身份验证和授权,而管理 Area 将始终要求对所有页面进行身份验证和授权。
以下公约和限制适用于区域:
- Area 是文件夹
Areas下的子目录 - 一个区域至少包含两个子文件夹:
Controllers和Views - Area 可以包含特定的布局页面以及专用的
_ViewImport.cshtml和_ViewStart.cshtml文件 - 您必须注册一个特定的路由,它允许在路由定义中使用 Areas,这样才能在您的应用中使用 Areas
- 区域 url 推荐使用以下格式:
http://<Host>/<AreaName>/<ControllerName>/<ActionName> - 标签助手
asp-area可以用于将一个区域附加到一个 URL 上
让我们看看如何为帐户管理创建一个特定的管理区域:
- 打开解决方案资源管理器并创建一个名为
Areas的新文件夹,右键单击该文件夹并选择 Add | Area,输入Account作为 Area 名称,然后单击 Add 按钮:

- 脚手架将为帐户区域创建一个专用的文件夹结构:

- 在
Startup类的Configure方法中的UseMVC声明中添加Areas的新路由:
app.UseMvc(routes =>
{
routes.MapRoute(name: "areaRoute",
template: "{area:exists}/{controller=Home}/{action=Index}");
routes.MapRoute(name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
- 右键单击帐户区域内的
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>
- 更新帐户分部视图,并添加一个链接来显示前面的视图(就在现有的 Log off 链接之后):
<a class="btn btn-default btn-block" asp-action="Index"
asp-controller="Account">View Details</a>
- 启动应用,注册一个新用户,并通过点击 Account 下拉菜单中的 Account Details 链接调用新区域:

我们将在此停止行政区域的实施,并在第 9 章、中回到行政区域的实施.NET Core 2.0 Applications,在这里您将看到如何安全访问它。
应用先进的概念
现在我们已经看到了 ASP 的所有基本特性.NET Core 2.0 MVC,让我们来看看一些更高级的特性,这些特性可以在你作为开发人员的日常工作中帮助你。
您还将学习如何使用 Visual Studio 2017 来测试您的应用,从而为用户提供更好的质量。
使用视图引擎
在 ASP.NET Core 2.0 使用服务器端代码来渲染 HTML,它使用一个视图引擎。 默认情况下,当使用相关的.cshtml文件构建标准视图时,你可以使用带有 Razor 语法的 Razor 视图引擎。
按照惯例,该引擎能够处理位于Views文件夹中的视图。 由于它是内置的并且是默认引擎,它会自动绑定到 HTTP 请求管道,而无需您为它做任何工作。
如果您需要使用 Razor 来渲染位于Views文件夹之外的文件,并且这些文件不是直接来自 HTTP 请求管道(例如电子邮件模板),那么您就不能使用默认的 Razor 视图引擎。 相反,您需要定义自己的视图引擎,并让它负责在本例中生成 HTML 代码。
在下面的例子中,我们将解释如何使用 Razor 基于邮件模板渲染一封邮件,而邮件模板不是来自 HTTP 请求管道:
- 打开解决方案资源管理器,创建一个名为
ViewEngines的新文件夹,添加一个名为EmailViewEngine.cs的新类,并提取其接口IEmailViewEngine:
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;
}
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);
}
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();
}
}
private ActionContext GetActionContext()
{
var httpContext = new DefaultHttpContext
{
RequestServices = _serviceProvider
};
return new ActionContext(httpContext, new RouteData(),
new ActionDescriptor());
}
}
- 创建名为
Helpers的新文件夹,并添加一个名为EmailViewRenderHelper.cs的新类:
public class EmailViewRenderHelper
{
IHostingEnvironment _hostingEnvironment;
IConfiguration _configurationRoot;
IHttpContextAccessor _httpContextAccessor;
public async Task<string> RenderTemplate<T>(string template,
IHostingEnvironment 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的新服务,并提取其接口IEmailTemplateRenderService:
public class EmailTemplateRenderService
{
private IHostingEnvironment _hostingEnvironment;
private IConfiguration _configuration;
private IHttpContextAccessor _httpContextAccessor;
public EmailTemplateRenderService(IHostingEnvironment
hostingEnvironment, IConfiguration configuration,
IHttpContextAccessor httpContextAccessor)
{
_hostingEnvironment = hostingEnvironment;
_configuration = configuration;
_httpContextAccessor = httpContextAccessor;
}
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;
}
}
- 在
Startup类中注册EmailViewEngine和EmailTemplateRenderService:
services.AddTransient<IEmailTemplateRenderService,
EmailTemplateRenderService>();
services.AddTransient<IEmailViewEngine, EmailViewEngine>();
Note that it is required to register the EmailViewEngine and the EmailTemplateRenderService as transient because of the HTTP Context Accessor injection.
- 在
Views/Shared文件夹中添加一个新的布局页面_LayoutEmail.cshtml:
<!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>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>© 2017 - 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>
<environment exclude="Development">
<script src="https://ajax.aspnetcdn.com/
ajax/jquery/jquery-2.2.0.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-K+ctZQ+LL8q6tP7I94W+qzQsfRV2a+
AfHIi9k8z8l9ggpc8X+Ytst4yBo/hH+8Fk">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/
3.3.7/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn
&& window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7
l2mCWNIpG9mGCD8wGNIcPD7Txa">
</script>
<script src="~/js/site.min.js"
asp-append-version="true"></script>
</environment>
@RenderSection("Scripts", required: false)
</body>
</html>
- 添加一个名为
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方法,以便在发送任何邮件之前使用新的 Email 视图引擎:
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());
Note that for calling RequestServices.GetService<T>();, you must also add using Microsoft.Extensions.DependencyInjection; as you have already done in other examples.
- 启动应用并注册一个新用户,打开
UserRegistrationEmail,分析其内容(查看wwwroot/Emails文件夹):

Note that a View Engine can be used for rendering email content, as seen in the preceding example, but it can also be used for rendering views outside of the Views folder, for rendering views from within a database, or for using the themes folder as in ASP.NET 4.
在本书的各个章节中,您已经看到了许多概念和代码示例,但是我们还没有讨论如何确保您的应用具有优秀的质量和可维护性。 下一节将介绍这个主题,因为它专门讨论应用测试。
通过创建单元测试和集成测试提供更好的质量
构建高质量的应用并使应用用户满意是一件困难的事情。 甚至,在应用的维护阶段,交付具有技术和功能缺陷的产品可能会导致巨大的问题。
最坏的情况是,由于维护对时间和资源的要求非常高,您将无法尽可能快地发展您的应用以降低您的上市时间,并且您将无法提供令人兴奋的新特性。 但请放心,你的竞争对手不会等待! 他们将超越你,你将失去市场份额和市场领导地位。
但是你怎样才能成功呢? 如何减少检测 bug 和功能问题的时间? 您必须测试您的代码和应用! 你必须尽可能多地做,越快越好。 众所周知,在开发阶段修复 bug 更便宜、更快,而在生产阶段修复 bug 则需要更多的时间和金钱。
当你想要成为你特定市场的未来市场领导者时,拥有一个较低的平均修复时间(MTTR)的漏洞会有很大的不同。
让我们继续开发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<Models.TurnModel> turns;
if (gameSession.Turns != null && gameSession.Turns.Any())
turns = new List<Models.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;
}
- 在
GameSessionController中添加一个新的方法SetPosition:
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}") for playing the Tic-Tac-Toe game.
Please click <a href="@Model.ConfirmationUrl">here</a> for
joining 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);
}
- 在
GameInvitationController中添加一个新的方法ConfirmGameInvitation:
[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 2017 包含了用于单元测试的强大功能。 测试资源管理器帮助您运行单元测试以及查看和分析测试结果。 为此,您可以使用内置的 Microsoft 测试框架或其他框架,如 NUnit 或 xUnit。
此外,您可以在每次构建之后自动执行单元测试,因此如果某些内容没有按照预期工作,开发人员可以快速做出反应。
重构代码无需担心回归,因为单元测试可以确保一切仍像以前一样工作。 没有更多的借口没有最好的代码质量!
您甚至可以进一步应用测试驱动开发(TDD),即在编写实现之前编写单元测试。 此外,在这种情况下,单元测试成为某种设计文档和功能规范。
This book is about ASP.NET Core 2.0, so we will not 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 for building better applications.
现在我们将看到使用 xUnit 是多么容易,它是 ASP 首选的单元测试框架.NET Core:
- 添加一个新的 xUnit 测试项目(.NET Core)类型称为
TicTacToe.UnitTests到 TicTacToe 解决方案:

- 使用 NuGet 包管理器将 xUnit 和 microt.net . test . sdk NuGet 包更新到最新版本:

- 添加对
TicTacToe和TicTacToe.Logging项目的引用:

- 删除自动生成的类,添加一个名为
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 服务和缓存)的真实环境中测试应用中多个技术组件之间的集成。
这种类型的测试的目标是确保在将各种技术组件组合在一起创建应用行为时,一切都能很好地一起工作,并提供预期的功能。
此外,集成测试应该始终具有清理步骤,以便它们可以重复运行而没有错误,并且不会在数据库或文件系统中留下任何工件。 在以下示例中,您将了解如何将集成测试应用于井字策略应用:
- 添加一个新的 xUnit 测试项目(。 类型为
TicTacToe.IntegrationTests的 TicTacToe 解决方案,更新 NuGet 包并添加对TicTacToe和TicTacToe.Logging项目的引用,如前面为单元测试项目所示。 - 添加
Microsoft.AspNetCore.TestHostNuGet 包,可以使用 xUnit 创建全自动集成测试:

- 删除自动生成的类,添加名为
IntegrationTests.cs的新类,并实现名为ShouldGetHomePageAsync的新方法:
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
using System.Linq;
using Microsoft.CodeAnalysis;
...
public class IntegrationTests
{
private readonly TestServer _testServer;
private readonly HttpClient _httpClient;
public IntegrationTests()
{
string applicationBasePath =
Path.GetFullPath(Path.Combine(
Directory.GetCurrentDirectory(),
@"..\..\..\..\TicTacToe"));
Directory.SetCurrentDirectory(applicationBasePath);
Environment.SetEnvironmentVariable(
"ASPNETCORE_ENVIRONMENT", "Development");
var builder = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(applicationBasePath)
.UseStartup<Startup>()
.ConfigureServices(services =>
{
services.Configure((RazorViewEngineOptions options) =>
{
var previous = options.CompilationCallback;
options.CompilationCallback = (context) =>
{
previous?.Invoke(context);
var assembly =
typeof(Startup).GetTypeInfo().Assembly;
var assemblies =
assembly.GetReferencedAssemblies().Select(x =>
MetadataReference.CreateFromFile(
Assembly.Load(x).Location)).ToList();
assemblies.Add(MetadataReference.CreateFromFile(
Assembly.Load(new AssemblyName(
"mscorlib")).Location));
assemblies.Add(MetadataReference.CreateFromFile(
Assembly.Load(new AssemblyName(
"System.Private.Corelib")).Location));
assemblies.Add(MetadataReference.CreateFromFile(
Assembly.Load(new AssemblyName("netstandard,
Version = 2.0.0.0, Culture = neutral,
PublicKeyToken = cc7b13ffcd2ddd51")).Location));
assemblies.Add(MetadataReference.CreateFromFile(
Assembly.Load(new AssemblyName(
"System.Linq")).Location));
assemblies.Add(MetadataReference.CreateFromFile(
Assembly.Load(new AssemblyName(
"System.Threading.Tasks")).Location));
assemblies.Add(MetadataReference.CreateFromFile(
Assembly.Load(new AssemblyName(
"System.Runtime")).Location));
assemblies.Add(MetadataReference.CreateFromFile(
Assembly.Load(new AssemblyName(
"System.Dynamic.Runtime")).Location));
assemblies.Add(MetadataReference.CreateFromFile(
Assembly.Load(new AssemblyName(
"Microsoft.AspNetCore.Razor.Runtime")).Location));
assemblies.Add(MetadataReference.CreateFromFile(
Assembly.Load(new AssemblyName(
"Microsoft.AspNetCore.Mvc")).Location));
assemblies.Add(MetadataReference.CreateFromFile(
Assembly.Load(new AssemblyName(
"Microsoft.AspNetCore.Razor")).Location));
assemblies.Add(MetadataReference.CreateFromFile(
Assembly.Load(new AssemblyName(
"Microsoft.AspNetCore.Mvc.Razor")).Location));
assemblies.Add(MetadataReference.CreateFromFile(
Assembly.Load(new AssemblyName(
"Microsoft.AspNetCore.Html.Abstractions")).Location));
assemblies.Add(MetadataReference.CreateFromFile(
Assembly.Load(new AssemblyName(
"System.Text.Encodings.Web")).Location));
context.Compilation =
context.Compilation.AddReferences(assemblies);
};
});
});
_testServer = new TestServer(builder)
{
BaseAddress = new Uri("http://localhost:5000")
};
_httpClient = _testServer.CreateClient();
}
[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);
}
}
- 在“测试资源管理器”中运行测试,并确保它们成功执行:

现在,您已经在前面的示例中了解了如何测试您的应用,您可以继续添加额外的单元和集成测试,以完全理解这些概念,并构建一个测试覆盖率,使您能够提供高质量的应用。
总结
在本章中,您了解了 MVC 模式、它的不同组件和层,以及它对于构建优秀的 ASP 是多么重要.NET Core 2.0 web 应用。
您了解了如何使用布局页面及其周围的特性来创建特定于设备的布局,从而使用户界面适应它们将要运行的设备。
此外,您已经使用 View Pages 构建了 web 应用的可见部分,即表示层。
然后我们介绍了分部视图、视图组件和标签助手,以更好地在应用的不同视图中封装和重用您的表示逻辑。
最后,我们介绍了视图引擎等高级概念,以及使用较低的MTTR来创建高质量应用的单元测试和集成测试。
在下一章中,我们将讨论 ASP.NET Core 2.0 Web API 框架以及如何构建、测试和部署 Web API 应用。
七、创建 Web API 应用
你还不知道,但这一章就是你一直在等待的一章! 它的特殊有很多原因。
首先,我们将完成游戏部分,您将能够开始玩井字游戏。 是的,最后,整个应用将运行,您将能够与其他用户竞争。 非常激动人心!
其次,您将看到如何将您的应用与其他系统和服务集成。 这是非常重要的,因为应用不再是孤立的筒仓。 相反,他们相互沟通,不断交换数据,为客户提供更多的价值。 你是怎么做到的? 您提供了可互操作的 Web api,这些 api 允许将组件(有时基于完全不同的技术)连接在一起!
第三,使用 Web api 不仅允许您与其他系统集成; 它还将帮助您构建更灵活和可重用的应用组件,然后您可以组合这些组件来创建新的应用,以响应更高级的用例。
你将在本章中创建的 api 不仅对你正在开发的 MVC Web 前端有用,而且对你将来可能构建的新的移动前端也有用。 这将让你接触到更多的客户。 你将能够为你的客户提供全方位的体验,他们开始使用一种设备,结束在另一种。
在本章中,我们将涵盖以下主题:
- 应用 Web API 概念和最佳实践
- 构建 rpc 风格的 Web api
- 构建 rest 风格的 Web api
- 构建 hateoas 风格的 Web api
应用 Web API 概念和最佳实践
ASP.NET Core 2.0 结合了 ASP 的最佳特性.NET MVC 和 Web api 一起成为一个单一的框架。 这是完全有意义的,因为它们提供了许多类似的功能。
在合并之前,当开发人员需要通过 MVC 和 Web api 公开不同格式的数据时,他们必须重写代码。 他们必须同时使用多个框架和概念。 幸运的是,整个过程在 ASP 中已经完全简化了.NET Core 2.0,正如你将在本章中看到的。
下图说明了 ASP 如何处理客户端 HTTP 请求。 关于 Web api 和 MVC 的 NET Core 2.0:

Web api 通常使用 JSON 或 XML 作为响应格式。 JSON 将是首选格式,因为它已经成为市场上的准标准格式,而且由于其简单性和效率,每个人都在使用它。
此外,过滤器和中间件可以与 Web api 一起使用,因为 ASP.NET Core 2.0 以与标准 MVC 控制器相同的方式管理 Web api。 这在某些用例中非常方便,开发人员可以更广泛地应用他们的技能。
一般来说,使用 ASP 创建 Web api 有三种不同的风格.NET 2.0 核心:
- rpc 样式
- rest 样式的
- hateoas 风格
Note that it is also possible to use SOAP for creating 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风格基于远程过程调用范例,该范例已经存在很长一段时间了(从 1980 年代早期开始)。 它基于在 URL 中包含一个操作名称,因此它非常类似于标准 MVC 操作。
ASP 最大的优势之一.NET Core 2.0 是你不需要把 MVC 部分和 Web API 部分分开。 相反,您可以在控制器实现中使用这两种方法。
控制器现在能够呈现视图结果以及 JSON/XML API 响应,这使得从一个到另一个的迁移变得容易。 此外,您可以为您的 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 });
}
- 更新
Views/Shared/Components/GameSession/default.cshtml文件中GameSessionViewComponent中的 table 元素:
@using Microsoft.AspNetCore.Http
@model TicTacToe.Models.GameSessionModel
@{
var email = Context.Session.GetString("email");
}
<div id="gameBoard">
<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" 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"
style="width:100%;height:100%"></i>
}
else
{
<i class="glyphicon glyphicon-remove-circle"
style="width:100%;height:100%"></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>
</div>
<div class="alert" id="divAlertWaitTurn">
<i class="glyphicon glyphicon-alert">Please wait until the
other user has finished his turn.</i>
</div>
- 在
wwwroot\app\js文件夹中添加一个新的 JavaScript 文件GameSession.js; 它将用于调用 Web API。 添加临时警告框用于测试:
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
},
- 在
TurnModel模型中添加一个名为Email的新属性:
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))
{
var bodyString = reader.ReadToEnd();
if (string.IsNullOrEmpty(bodyString))
return BadRequest("Body is empty");
var turn =
JsonConvert.DeserializeObject<TurnModel>(bodyString);
turn.User =
await HttpContext.RequestServices.GetService
<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");
}
}
return BadRequest("Id is empty");
}
Note that it is best practice to prefix Web APIs with a meaningful name and a version number (for example, /restapi/v1) as well as to support JSON and XML.
- 更新
Views文件夹中的游戏会话索引视图,并调用带有相应参数的 JavaScriptSetGameSession函数:
@using Microsoft.AspNetCore.Http
@model TicTacToe.Models.GameSessionModel
@{
var email = Context.Session.GetString("email");
}
@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>
}
@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>)
}
<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>
}
- 更新通信中间件中 WebSockets 的
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);
}
}
- 更新通信中间件中 WebSockets 的
ProcessGameInvitationConfirmation方法:
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 Task.Delay(500);
gameInvitationModel = await gameInvitationService.Get(id);
await SendStringAsync(webSocket, "WaitForConfirmation", ct);
}
if (gameInvitationModel.IsConfirmed)
{
await SendStringAsync(webSocket,
JsonConvert.SerializeObject(new
{
Result = "OK",
Email = gameInvitationModel.InvitedBy,
gameInvitationModel.EmailTo,
gameInvitationModel.Id
}), 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 标签助手中的
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:image/jpeg;base64,{Convert.ToBase64String(photo)}");
}
}
- 更新
GameInvitationService中的Add方法:
public Task<GameInvitationModel> Add(
GameInvitationModel gameInvitationModel)
{
_gameInvitations.Add(gameInvitationModel);
return Task.FromResult(gameInvitationModel);
}
-
更新桌面布局页面和移动布局页面; 通过删除两个页面底部包含
script1.js和script2.js的开发environment标记来清除。 -
更新
scripts1.jsJavaScript 文件,删除所有显示 WebSockets 是否启用的警告框。 -
启动应用,注册新用户,邀请其他用户开始游戏会话,点击单元格,你现在会看到一个 JavaScript 警告框:

很好; 您已经看到了如何将现有的GameSessionController操作转换为 rpc 样式的 Web API。 因为所有不同的 ASP.NET web 框架已经集中到一个单一的 asp.net 框架中.NET Core 2.0,这可以轻松、快速地完成,而无需重写代码或对现有代码进行太多更改。
在接下来的步骤中,我们将看到如何在 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($"can not found session {sessionId}");
}
}
else
{
return BadRequest("session id is null");
}
}
- 更新
GameSessionService中的AddTurn方法,计算IconNumber和TurnNumber:
public async Task<GameSessionModel> AddTurn(Guid id,
string email, int x, int y)
{
List<Models.TurnModel> turns;
var gameSession = _sessions.FirstOrDefault(
session => session.Id == id);
if (gameSession.Turns != null && gameSession.Turns.Any())
turns = new List<Models.TurnModel>(gameSession.Turns);
else
turns = new List<TurnModel>();
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;
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;
}
- 更新游戏会话索引视图,使用图像,并添加启用和禁用 gameboard 的可能性:
@using Microsoft.AspNetCore.Http
@model TicTacToe.Models.GameSessionModel
@{
var email = Context.Session.GetString("email");
}
@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>
}
@section Mobile{
<h1>Game Session @Model.Id</h1>
<h2>Started at @(DateTime.Now.ToShortTimeString())</h2>
User 1: @Model.User1 <i class="glyphicon
glyphicon-unchecked"></i><br />
User 2: @Model.User2 (<i class="glyphicon
glyphicon-remove-circle"></i>)
}
<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");
EnableCheckTurnIsFinished();
@if(email != Model.ActiveUser?.Email)
{
<text>DisableBoard(@Model.TurnNumber);</text>
}
else
{
<text>EnableBoard(@Model.TurnNumber);</text>
}
</script>
}
- 在
wwwroot\app\js文件夹中添加一个新的 JavaScript 文件CheckTurnIsFinished.js; 相应更新bundleconfig.json文件:
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);
}
});
}
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;
}
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>');
}
- 更新
GameSession.jsJavaScript 文件中的SetGameSession方法; 默认设置TurnNumber为零:
function SetGameSession(gdSessionId, strEmail) {
window.GameSessionId = gdSessionId;
window.EmailPlayer = strEmail;
window.TurnNumber = 0;
}
- 更新
GameSession.jsJavaScript 文件中的SendPosition方法,删除之前添加的临时测试警告框; 我们不再需要它了,游戏将在本节结束时功能完整:
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"
});
}
- 在
GameSessionController中添加两个新方法,第一个叫CheckGameSessionIsFinished,第二个叫CheckIfUserHasWon:
[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.");
}
}
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;
}
- 在
wwwroot\app\js文件夹中添加一个新的 JavaScript 文件CheckGameSessionIsFinished.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 控制器操作。 在下一节中,您将看到一种完全不同的方法,它基于资源和资源管理。
恭喜你; 现在,您已经完成了实现,并创建了一个漂亮的、现代的、基于浏览器的游戏,在这个游戏中,两个用户可以相互竞争。
做好准备,因为您将看到更高级的技术,并了解如何使用两种最著名的 API 通信风格 REST 和 HATEOAS 来提供用于互操作性的 Web API。
要玩这款游戏,你可以使用两个单独的私人浏览器窗口,或者使用两个不同的浏览器,如 Chrome、Edge 或 Firefox。 为了测试您的 Web api,建议安装并使用 Postman,但是您也可以使用任何其他与 HTTP rest 兼容的客户机,比如 Fiddler,甚至通过 Firefox 的高级特性。
构建 rest 风格的 Web api
rest 风格是由 Roy Fiedling 在 2000 年代发明的,它是在基于多种技术的系统之间提供互操作性的最佳方法之一,无论它是在您的网络中还是在 internet 上。
此外,REST 方法本身并不是一种技术,而是一些有效使用 HTTP 协议的最佳实践。
REST 使用 HTTP 协议的不同元素来提供服务,而不是添加像 SOAP 或 XML-RPC 这样的新层:
- URI 标识一个资源
- HTTP 动词标识一个动作
- 响应不是资源,而是资源的表示
- 客户端身份验证作为请求头中的参数传递
与 rpc 风格不同,其主要目的不再是提供操作,而是管理和操作资源。
To get even more information on the concepts and ideas behind REST, you should read the dissertation of Roy Fiedling, which you can find at http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm.
如下图所示,在TicTacToe应用中主要有三种类型的资源:
- 用户
- 游戏邀请
- 游戏会话

现在我们将演示如何使用 REST 风格来构建一个 Game Invitation REST API:
- 在
GameInvitationService中增加两个新方法All和Delete,并相应更新游戏邀请服务接口:
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文件夹,选择 Add | Controller,然后选择带有读写操作模板的 API 控制器:

- 删除自动生成的代码,并用下面的 REST 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);
}
}
Note that for learning purposes, we have just given a very basic example of what you could implement. Normally, you should provide the same functionalities as in your controller implementations (sending emails, confirming emails, verifying data, etc.) and some advanced error handling.
- 启动应用,安装并启动 Postman,以便对您现在提供的新 REST API 进行一些手动测试,并向
http://<yourhost>/restapi/v1/GameInvitation发送 HTTP GET 请求。 不会有游戏邀请,因为你还没有创建任何:

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

Note that we have added the automatic creation of a user if it does not exist for testing purposes in one of the previous chapters. In a real worked scenario, you will have to implement the user registration Web APIs and call them before the Game Invitation Web APIs. Otherwise, you will get a bad request, since we have added some code to assure data coherence and integrity.
- 你可以通过发送一个 HTTP GET Request 到
http://<yourhost>/restapi/v1/GameInvitation或者更具体地说,通过发送一个 HTTP GET Request 到http://<yourhost>/restapi/v1/GameInvitation/7223160d-6243-498b-9d35-81b8c947b5ca来检索游戏邀请:

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

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

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

- 验证游戏邀请删除,并发送一个 HTTP GET 请求到
http://<yourhost>/restapi/v1/GameInvitation:

rest 样式是目前市场上最常见的 Web api 样式。 它很容易理解,并且非常适合于互操作性用例。
在下一节中,您将看到一种更高级的样式,称为 HATEOAS,它特别适合于不断发展的 Web api。
构建 hateoas 风格的 Web api
HATEOAS(超媒体作为应用状态引擎)风格是提供高效 Web api 的另一种方法。 然而,它与之前提到的另外两种风格完全不同。 使用这种方法,客户端可以通过遍历 HTTP 响应中提供的各种超媒体链接来动态导航到所需的资源。
这种样式的优点是服务器不再驱动应用状态; 相反,它是由服务器返回的超媒体链接监督。
此外,与其他样式相比,使用这种样式时 API 更改的处理要好得多,因为客户机不再硬编码动作(rpc 样式)或资源(rest 样式)的 uri。 相反,它们可以使用服务器随每个响应返回的超媒体链接,这是一个有趣的概念,它允许更灵活和可扩展的 Web api。
下面的图表展示了如何将 hateoa 风格应用于TicTacToe应用:

这个图的一个 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\"}"
}
}
]
}
}
让我们来看看如何从技术上为TicTacToe应用的游戏邀请执行 HATEOAS:
- 进入 NuGet 包管理器并添加
Halcyon.Mvc包,这将允许你更快更容易地实现 HATEOAS Web api:

- 更新
Startup类,使用 HAL Json 格式化器代替标准 Json 格式化器:
services.AddMvc(o =>
{
o.Filters.Add(typeof(DetectMobileFilter));
o.OutputFormatters.RemoveType<JsonOutputFormatter>();
o.OutputFormatters.Add(new JsonHalOutputFormatter(new
string[] { "application/hal+json",
"application/vnd.example.hal+json",
"application/vnd.example.hal.v1+json" }));
}).AddViewLocalization(
LanguageViewLocationExpanderFormat.Suffix,
options => options.ResourcesPath =
"Localization").AddDataAnnotationsLocalization();
- 更新
GameInvitationAPiController中的Get方法,使用Halcyon.Mvc的特定特性,返回HAL结果:
[HttpGet]
public async Task<IActionResult> Get()
{
var invitations = await _gameInvitationService.All();
var responseConfig = new HALModelConfig
{
LinkBase = $"{Request.Scheme}://{Request.Host.ToString()}",
ForceHAL = Request.ContentType ==
"application/hal+json" ? true : false
};
var response = new HALResponse(responseConfig);
response.AddLinks(new Link("self", "/GameInvitation"),
new Link("confirm", "/GameInvitation/{id}/Confirm"));
List<HALResponse> invitationsResponses = new List<HALResponse>();
foreach (var invitation in invitations)
{
var rInv = new HALResponse(invitation, responseConfig);
rInv.AddLinks(new Link("self", "/GameInvitation/" +
invitation.Id));
rInv.AddLinks(new Link("confirm",
$"/GameInvitation/{invitation.Id}/confirm"));
var invitedPlayer =
_userService.GetUserByEmail(invitation.EmailTo);
rInv.AddEmbeddedResource("invitedPlayer", invitedPlayer,
new Link[]
{
new Link("self", $"/User/{invitedPlayer.Id}")
});
var invitedBy =
_userService.GetUserByEmail(invitation.InvitedBy);
rInv.AddEmbeddedResource("invitedBy", invitedBy, new Link[]
{
new Link("self", $"/User/{invitedBy.Id}")
});
invitationsResponses.Add(rInv);
}
response.AddEmbeddedCollection("invitations",
invitationsResponses);
return this.HAL(response);
}
- 启动应用和 Postman,向
http://<yourhost>/restapi/v1/GameInvitation发送 HTTP POST 请求,创建一个新的 Game Invitation,点击 Body,选择 raw 和 JSON,使用"id":"7223160d-6243-498b-9d35-81b8c947b5ca","EmailTo":"example@example.com"和"InvitedBy":"test@test.com"作为参数:

- 通过使用
Content-Type: application/hal+json向http://<yourhost>/restapi/v1/GameInvitation发送一个 HTTP GET 请求来获取游戏邀请; 你会看到 HTTP 响应现在包含了 HATEOAS 链接:

HATEOAS 提供了一些强大的特性,这些特性允许独立地演进组件。 客户端可以与运行在服务器上的业务工作流完全解耦,服务器通过使用链接和其他超媒体构件(如表单)来管理交互。
总结
在本章中,您学习了如何为您的应用构建 Web api,以实现集成目的和松散耦合的应用架构。
我们已经探讨了 Web api 的不同风格,比如 RPC、REST 和 HATEOAS。 每种风格都有特定的优势和用例。 您必须仔细选择,这取决于您的特定应用需求,因为没有任何一种样式比其他样式更好。
您已经看到了如何将现有控制器操作转换为 rpc 样式的 Web api,以及如何从头构建 rest 样式和 hateoas 样式的 Web api 的示例。
我们已经使用 Postman 手动测试我们的 Web api,并且您已经获得了足够的知识来将所有这些新概念应用到您自己的环境中。
在下一章中,我们将讨论如何在 ASP 中使用 Entity Framework Core 2 来访问数据.NET Core 2.0 应用。
八、使用实体框架核心 2 访问数据
我们已经大大提高了Tic-Tac-Toeweb 应用的实现,但是当您重新启动应用时,所有的用户注册和应用数据将被重置。 这是因为我们还没有持久化任何数据。
为了持久化数据并能够在应用启动时重新加载它,您必须将其放入某种持久化存储,如文件(XML、JSON、CSV)或数据库。
数据库将是最好的选择,因为与简单的文件存储相比,它提供了更好的性能和更大的安全性,这就是为什么我们将在下面的示例中使用它。
因为 ASP。 在 NET 3.0 中,你可以使用一个叫做实体框架的 ORM 框架来以一种更高效、更简单的方式访问数据库中的数据。 ASP.NET Core 2.0 与该框架的专用版本 Entity framework Core 2 一起工作。
在本章中,我们将涵盖以下主题:
- 开始使用实体框架 Core 2
- 使用实体框架 Core 2 数据注释
- 使用实体框架 Core 2 迁移
- 创建、读取、更新和删除数据
- 处理请求特性
开始使用实体框架 Core 2
Meta 包Microsoft.AspNetCore.All包含实体框架 Core 2,包括所有使用 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 currently available Entity Framework Core 2 NuGet packages here: https://www.nuget.org/packages?page=2&q=Tags%3A%22entity-framework-core%22.
建立连接
要打开到数据库的会话并查询和更新实体的实例,可以使用DbContext,它基于工作单元和存储库模式的组合。
让我们看看如何准备Tic-Tac-Toe应用使用实体框架 Core 2 通过DbContext和一个连接字符串连接到 SQL 数据库:
- 转到解决方案资源管理器,添加一个名为
Data的新文件夹,添加一个名为GameDbContext.cs的新类,并为每个模型(UserModel、TurnModel等)实现一个DbSet属性:
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类中注册 Game Db Context,并在构造函数中传递连接字符串和数据库提供程序作为参数。 您只需要一个实例,所以使用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);
- 更新
UserService使其能够与游戏数据库上下文一起工作; 为 Game Db Context 添加一个新的公共构造函数和一个私有成员:
private DbContextOptions<GameDbContext> _dbContextOptions;
public UserService(DbContextOptions<GameDbContext>
dbContextOptions)
{
_dbContextOptions = dbContextOptions;
}
- 更新
UserService中的RegisterUser方法以使用 Game Db Context:
public async Task<bool> RegisterUser(UserModel userModel)
{
using(var db = new GameDbContext(_dbContextOptions))
{
db.UserModels.Add(userModel);
await db.SaveChangesAsync();
return true;
}
}
- 在
Extensions文件夹中添加一个名为ModelBuilderExtensions的新扩展。 这将用于定义表名约定:
public static class ModelBuilderExtensions
{
public static void RemovePluralizingTableNameConvention(
this ModelBuilder modelBuilder)
{
foreach (IMutableEntityType entity in
modelBuilder.Model.GetEntityTypes())
{
entity.Relational().TableName = entity.DisplayName();
}
}
}
- 更新 Game Db Context 中的
OnModelCreating方法,以进一步配置通过约定从DbSet属性中暴露的实体类型中发现的模型; 调用之前的扩展来应用表名约定:
protected override void OnModelCreating(ModelBuilder
modelBuilder)
{
modelBuilder.RemovePluralizingTableNameConvention();
}
Note that you could also use another method called OnConfiguring in the DB Context, to configure the DB Context without using DbContextOptions.
- 在
Data文件夹中添加一个名为GameDbContextFactory的新类。 这将被用于实例化 Game Db Context 的特定选项:
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);
}
}
如果您已经使用过数据库,那么应该熟悉连接字符串的概念。 它们包含连接到数据库所需的配置(地址、用户名、密码等)和设置(加密、协议等)。
在 ASP。 您可以使用一个appSettings.<env>.json文件来配置连接字符串。 当使用这个文件中的ConnectionStrings节时,连接字符串会自动加载:
"ConnectionStrings": {
"DefaultConnection":
"Server=(localdb)\\MSSQLLocalDB;Database=TicTacToe;
Trusted_Connection=True;MultipleActiveResultSets=true"
},
正如您在前面的示例中看到的,您可以使用GetConnectionString方法在 ASP 运行时检索连接字符串.NET Core 2.0 应用:
var databaseConnectionString =
_configuration.GetConnectionString("DefaultConnection");
这就是使用 Game Db Context 和存储在Tic-Tac-Toe应用的appsettings.json配置文件中的相应默认连接字符串所需要知道的一切。
通过数据注释定义主键和外键
在下一步中,您需要修改现有的模型,以便能够在 SQL 数据库中持久化它们。 为了允许 Entity Framework Core 2.0 创建、读取、更新和删除记录,您需要为每个模型指定一个主键。 这可以通过使用数据注释来实现,它允许您使用[Key]装饰器来装饰属性。
下面是一个如何为UserModel使用数据注解的例子:
public class UserModel
{
[Key]
public long Id { get; set; }
...
}
你应该将此应用于井字棋应用的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=] for defining the position of the property, if you need to order a composite key.
在使用 SQL Server(或任何其他 SQL 92 DBMS)时,首先要考虑的是表之间的关系。 在实体框架 Core 2 中,你可以使用[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; }
[ForeignKey(nameof(InvitedByUserId))]
public UserModel InvitedByUser { get; set; }
public Guid InvitedByUserId { get; set; }
public bool IsConfirmed { get; set; }
public DateTime ConfirmationDate { get; set; }
}
- 更新
GameSessionModel; 添加外键到UserId1:
public class GameSessionModel
{
[Key]
public Guid Id { get; set; }
public Guid UserId1 { get; set; }
public Guid UserId2 { get; set; }
[ForeignKey(nameof(UserId1))]
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; }
public int TurnNumber { get; set; }
}
- 更新
TurnModel; 添加外键到UserId:
public class TurnModel
{
[Key]
public Guid Id { get; set; }
public Guid UserId { get; set; }
[ForeignKey(nameof(UserId))]
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; }
}
默认情况下,Entity Framework Core 2 将模型中的所有属性都映射到模式表示中。 但是一些更复杂的属性类型是不兼容的,这就是为什么您应该将它们从自动映射中排除。 但是我们怎么做呢? 嗯,通过使用[NotMapped]装饰器。 这有多简单和直接?
例如,对于Tic-Tac-Toe应用,将活跃用户持续一个回合是没有意义的,所以你应该使用GameSessionModel中的[NotMapped]装饰器将他们从自动映射中排除:
public class GameSessionModel
{
[Key]
public Guid Id { get; set; }
public Guid UserId1 { get; set; }
public Guid UserId2 { get; set; }
[ForeignKey(nameof(UserId1))]
public UserModel User1 { get; set; }
public UserModel User2 { get; set; }
public IEnumerable<TurnModel> Turns { 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; }
}
For more information on Entity Framework Data Annotations, please visit the following link:
https://msdn.microsoft.com/en-us/library/jj591583(v=vs.113).aspx
好的,现在您已经通过使用 Entity Framework Core 2 Data Annotations 装饰了所有模型,但是您很快就会看到,在指向同一个UserModel实体的GameSessionModel中,有两个属性User1和User2。 这将导致循环关系,因此在使用关系数据库进行级联更新或级联删除等操作时将成为一个问题。
为了避免示例中的循环关系,你需要用[ForeignKey]装饰器装饰User1,并更新 Game Db 上下文中的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);
使用实体框架 Core 2 迁移
正如您所看到的,在开发应用时,在重构和最终完成项目时,模型可能会频繁更改。 这可能导致数据库模式不同步,因此需要通过创建升级脚本手动更新数据库模式。
幸运的是,Entity Framework Core 2 包含一个名为 Migrations 的特性,通过自动保持模型和相应的数据库模式同步来帮助您完成这项乏味的任务。
在你更新了模型、服务和控制器以符合上述约束,并相应地修改了 Game Db 上下文之后,你现在就可以使用 Entity Framework Core 2 Migrations 了:
- 添加一个名为
InitialDbSchema的 Db 模式的第一个版本,通过单击 Tools | NuGet Package Manager | Package Manager Console 的顶部菜单,打开 NuGet Package Manager,并执行Add-Migration InitialDbSchema命令:

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

如果你的数据库可以从你的开发环境中访问,你可以直接在 Visual Studio 2017 中更新它:
- 进入包管理器控制台,执行
Update-Database命令。 这将在第一次使用时创建数据库,或者在你改变模型时自动更新它:

- 进入 SQL Server 对象资源管理器,分析实体框架 2 迁移在 SQL Server 中自动生成的数据库模式:

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

如果你的数据库不能从你的开发环境中访问(例如,在登台或生产环境中),你必须生成一个 SQL 脚本文件:
- 进入包管理器控制台,执行
Script-Migration命令自动生成一个 SQL 脚本文件,该脚本文件可用于创建井字策略应用数据库:

- 使用您喜欢的数据库工具(SQL Server Management Studio 等)在特定环境中执行生成的 SQL 脚本文件,以创建Tic-Tac-Toe应用数据库。
您还可以使用实体框架核心 2 迁移直接从代码以确保数据库中经常与你同步模型通过调用Migrate``GameDbContext实例的方法在Configure``Startup类的方法,如下所示:
- 更新
Startup类中的Configure方法; 在方法底部添加以下说明:
using (var scope =
app.ApplicationServices.GetService<IServiceScopeFactory>()
.CreateScope())
{
scope.ServiceProvider.GetRequiredService<GameDbContext>()
.Database.Migrate();
}
- 按F5启动井字连击应用:
Note that if a table or a property does not exist in the database and if the connection string provides enough access rights, Entity Framework Core 2 will automatically create it.
在更新了模型和相应的应用数据库之后,所有模型数据现在都持久化了,即使在应用重新启动之后,应用状态也将可用。 这意味着你不能注册已经存在的邮件,你必须手动添加新的邮件,所以现在截断数据库并删除它们。
创建、读取、更新和删除数据
在前面的小节中,我们已经完成了定义模型的所有工作,并以一致和一致的方式建立和运行数据库。 在本节中,您将最终了解如何使用数据并执行创建、读取、更新和删除操作。
让我们看看如何使用GameDbContext来处理数据:
- 更新
UserService; 删除ConcurrencyBag和静态构造函数,并更新GetUserByEmail方法:
public async Task<UserModel> GetUserByEmail(string email)
{
using (var db = new GameDbContext(_dbContextOptions))
{
return await db.UserModels.FirstOrDefaultAsync(
x => x.Email == email);
}
}
- 更新
UserService中的UpdateUser方法,看看如何使用 Db Context 来更新数据:
public async Task UpdateUser(UserModel userModel)
{
using (var gameDbContext =
new GameDbContext(_dbContextOptions))
{
gameDbContext.Update(userModel);
await gameDbContext.SaveChangesAsync();
}
}
- 更新
UserService中的GetTopUsers方法,学习如何使用 Db Context 构建带有排序和过滤数据的高级查询:
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);
}
}
现在,您已经了解了如何配置您的应用,以使用 Entity Framework Core 2 及其所有有用和有趣的特性。 它提供了一种很好的方法来抽象复杂性,并从您作为开发人员的日常生活中消除耗时的任务。 您不再需要学习任何其他语言(例如 SQL); 您也不需要更改创建、读取、更新和删除数据库中的记录的环境。 所有事情都可以在代码中完成,也可以在 Visual Studio 中完成,以确保开发人员的高生产率和效率。
总结
在本章中,你已经学习了如何使用 Entity Framework Core 2 和 ASP.NET Core 2.0 与 SQL Server 数据库的工作。
我们已经了解了如何使用 Db Context 和连接字符串连接到 SQL Server 数据库。 然后,我们使用实体框架 Core 2 数据注释更新了Tic-Tac-Toe应用中的模型,使用主键和外键定义,并在 Db 上下文中覆盖了OnModelCreating方法。
你已经使用了 Entity Framework Core 2 Migrations 来保持你代码中的模型与它们对应的数据库表示一致。
此外,您还学习了如何以一种简单、高效的方式插入、更新和查询数据。
在下一章中,我们将讨论如何安全访问您的 ASP。 使用集成的 asp.net Core 2.0 应用.NET Core 2.0 授权特性。
九、保护 ASP.NET Core 2.0 应用
在当今这个数字犯罪和互联网欺诈不断增加的世界里,所有现代 web 应用都需要实现强大的安全机制来防止攻击和用户身份盗用。
到目前为止,我们一直专注于理解如何构建高效的 ASP。 净核心 2.0 web 应用,而不考虑用户身份验证、用户授权、或数据保护,但由于井字应用变得越来越复杂,我们必须解决安全问题,最后将它部署到公众。
构建一个 web 应用而不考虑安全性将是一个巨大的失败,甚至可能导致最伟大和最著名的网站崩溃。 在安全漏洞和个人数据盗窃的情况下,负面的声誉和用户信心影响可能是巨大的,没有人愿意与这些应用和更麻烦的公司一起工作。
这是一个需要认真对待的话题。 您应该与安全公司合作进行代码验证和入侵测试,以确保您符合最佳实践和高安全性标准(例如,OWASP10)。
幸运的是,ASP.NET Core 2.0 包含了帮助您处理这个复杂但重要的主题所需的一切。 大多数内置功能甚至不需要高级编程或安全技能。 您将看到,通过使用 ASP 来理解和实现安全应用是非常容易的.NET Core 2.0 身份框架。
在本章中,我们将涵盖以下主题:
- 添加基本的用户表单验证
- 添加外部提供者身份验证
- 增加忘记密码和密码重置机制
- 使用双因素身份验证
- 实现授权
实现身份验证
身份验证允许应用识别特定的用户。 它既不用于管理用户访问权限(这是授权角色),也不用于保护数据(这是数据保护角色)。
有几种验证应用用户的方法,例如:
- 基本用户表单身份验证,使用带有登录框和密码框的登录表单
- 单点登录(SSO)身份验证,其中用户仅对其公司上下文中的所有应用进行一次身份验证
- 社交网络外部提供商身份验证(如 Facebook 和 LinkedIn)
- 证书或公钥基础设施(PKI)认证
ASP.NET Core 2.0 支持所有这些方法,但在本章中,我们将集中讨论使用用户登录和密码进行表单身份验证,以及通过 Facebook 进行外部提供商身份验证。
在下面的示例中,您将看到如何使用这些方法对应用用户进行身份验证,以及一些更高级的特性,如电子邮件确认和密码重置机制。
最后(但并非最不重要),您将看到如何使用内置的 ASP 实现双因素身份验证。 最关键的应用的 NET Core 2.0 身份验证特性。
让我们准备为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. php 一起使用.NET Core 2.0 身份验证功能,并删除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 keyNormalizer,
IdentityErrorDescriber errors, IServiceProvider services,
ILogger<UserManager<UserModel>> logger) :
base(store, optionsAccessor, passwordHasher,
userValidators, passwordValidators, keyNormalizer,
errors, services, logger)
{
_store = store;
_dbContextOptions = dbContextOptions;
}
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 isValide = await base.VerifyUserTokenAsync(user,
Options.Tokens.EmailConfirmationTokenProvider,
ConfirmEmailTokenPurpose, token);
if (isValide)
{
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以与 ApplicationUser Manager 一起工作,添加两个新方法GetEmailConfirmationCode和ConfirmEmail,并更新用户服务接口:
public class UserService
{
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);
}
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<string> GetEmailConfirmationCode(
UserModel user)
{
return
await _userManager.GenerateEmailConfirmationTokenAsync(user);
}
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 comment the test out and update 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. php 使用.NET Core 2.0 身份验证功能:
public class RoleModel : IdentityRole<Guid>
{
public RoleModel()
{
}
public RoleModel(string roleName) : base(roleName)
{
}
}
- 更新游戏数据库上下文,并添加一个新的角色模型 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 2.0 的身份:
private async Task ProcessEmailConfirmation(HttpContext
context, WebSocket currentSocket, CancellationToken ct,
string email)
{
var userService =
context.RequestServices.GetRequiredService<IUserService>();
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);
}
}
private async Task ProcessEmailConfirmation(HttpContext context)
{
var userService =
context.RequestServices.GetRequiredService<IUserService>();
var email = context.Request.Query["email"];
UserModel 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");
}
}
- 更新
GameInvitationService,并将公共构造函数设置为 static。 - 从
Startup类中删除以下DbContextOptions注册; 它将在下一步被另一个替换:
var dbContextOptionsbuilder =
new DbContextOptionsBuilder<GameDbContext>()
.UseSqlServer(connectionString);
services.AddSingleton(dbContextOptionsbuilder.Options);
- 更新
Startup类,并添加一个新的DbContextOptions注册:
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 = "Azerty123!",
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和AddTurn方法,重新提取游戏会话服务接口:
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;
}
public async Task<GameSessionModel> AddTurn(Guid id,
UserModel user, int x, int y)
{
List<Models.TurnModel> turns;
var gameSession = _sessions.FirstOrDefault(session =>
session.Id == id);
if (gameSession.Turns != null && gameSession.Turns.Any())
turns = new List<Models.TurnModel>(gameSession.Turns);
else
turns = new List<TurnModel>();
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;
else
gameSession.ActiveUser = gameSession.User1;
gameSession.TurnFinished = true;
_sessions = new ConcurrentBag<GameSessionModel>
(_sessions.Where(u => u.Id != id))
{
gameSession
};
return gameSession;
}
- 更新
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 =
Request.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:
gameSession = await _gameSessionService.AddTurn(gameSession.Id,
turn.User, turn.X, turn.Y);
- 更新 Game Db Context 中的
OnModelCreating方法,并添加一个WinnerId外键:
...
modelBuilder.Entity(typeof(GameSessionModel))
.HasOne(typeof(UserModel), "Winner")
.WithMany()
.HasForeignKey("WinnerId").OnDelete(DeleteBehavior.Restrict);
...
- 更新
GameInvitationController中的GameInvitationConfirmation方法; 它必须是异步的工作与 ASP.NET Core 2.0 标识:
[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 2.0 标识:
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 2.0 的身份:
public async Task<IActionResult> Index()
{
return await Task.Run(() =>
{
return View();
});
}
-
打开包管理器控制台并执行
Add-Migration IdentityDb命令。 -
通过在包管理器控制台中执行
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 2.0 Identity in this section, which require complex passwords.
添加基本的用户表单验证
太棒了! 您已经注册了身份验证中间件并准备了数据库。 在下一步中,您将为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; }
}
- 在
Views文件夹中添加一个名为Account的新文件夹,并在此新文件夹中添加一个名为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;
...
}
...
- 在
UserService中添加两个新方法SignInUser和SignOutUser,并更新用户服务接口:
public async Task<SignInResult> SignInUser(
LoginModel loginModel, HttpContext httpContext)
{
var start = DateTime.Now;
_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($"can not sigin user
{loginModel.UserName} - {ex}");
throw ex;
}
finally
{
stopwatch.Stop();
_logger.LogTrace($"sigin user {loginModel.UserName}
finished in {stopwatch.Elapsed}");
}
}
public async Task SignOutUser(HttpContext httpContext)
{
await _signInManager.SignOutAsync();
await httpContext.SignOutAsync(new AuthenticationProperties {
IsPersistent = false });
return;
}
- 在
Controllers文件夹中添加名为AccountController的新控制器,并实现三种处理用户身份验证的新方法:
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");
}
}
- 更新
GameSessionController中的CheckGameSessionIsFinished方法:
[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.Id == session.User1.Id).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.Id == session.User2.Id).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.");
}
}
- 更新
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文件,为已经通过身份验证的用户显示 display Name 元素,或为通过身份验证的用户显示 Login 元素; 为此,替换最后一个<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文件,以改善视觉表现:
@using Microsoft.AspNetCore.Http
@model TicTacToe.Models.GameSessionModel
@{
var email = Context.Session.GetString("email");
}
<div id="gameBoard">
<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>
</div>
<div class="alert" id="divAlertWaitTurn">
<i class="glyphicon glyphicon-alert">Please wait until the
other user has finished his turn.</i>
</div>
- 启动应用,单击顶部菜单中的 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 2.0 Identity, and that you may even create your own providers and integrate them in the same way.
我们现在要通过 Facebook 实现外部提供者认证:
- 更新登录表单,并在标准登录按钮之后直接添加一个名为
Login with 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和ExternalLoginSignInAsync:
public async Task<AuthenticationProperties>
GetExternalAuthenticationProperties(string provider,
string redirectUrl)
{
return await Task.FromResult(
_signInManager.ConfigureExternalAuthenticationProperties(
provider, redirectUrl));
}
public async Task<ExternalLoginInfo> GetExternalLoginInfoAsync()
{
return await _signInManager.GetExternalLoginInfoAsync();
}
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和ExternalLoginCallBack:
[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);
}
[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 in the Facebook developer portal before being able to authenticate logins with a Facebook account.
Please go to http://developer.facebook.com for more information.
- 启动应用,点击登录与 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; }
}
- 通过添加相应的
DbSet在 Game Db 上下文中注册TwoFactorCodeModel:
public DbSet<TwoFactorCodeModel> TwoFactorCodeModels { get; set; }
- 打开 NuGet Package Manager Console 并执行
Add-Migration AddTwoFactorCode命令,然后通过执行Update-Database命令更新数据库。 - 更新应用用户管理器,然后添加三个新方法
SetTwoFactorEnabledAsync、GenerateTwoFactorTokenAsync和VerifyTwoFactorTokenAsync:
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文件夹,更新索引视图:
@model TicTacToe.Models.UserModel
@using Microsoft.AspNetCore.Identity
@inject UserManager<TicTacToe.Models.UserModel> UserManager
@{
var isTwoFactor =
UserManager.GetTwoFactorEnabledAsync(Model).Result;
ViewData["Title"] = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<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>
<p>
<i class="glyphicon glyphicon-star">
</i> @Model.Score
</p>
<p>
<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>
}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
- 在
Areas/Account/Views文件夹中添加一个名为_ViewImports.cshtml的新文件:
@using TicTacToe
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
@addTagHelper *, TicTacToe
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
- 更新
UserService和用户服务接口,并添加两个新方法EnableTwoFactor和GetTwoFactorCode:
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;
}
}
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)
{
var start = DateTime.Now;
_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;
}
if (await _userManager.GetTwoFactorEnabledAsync(user))
{
return SignInResult.TwoFactorRequired;
}
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($"Ca not sigin user
{loginModel.UserName} - {ex}");
throw ex;
}
finally
{
stopwatch.Stop();
_logger.LogTrace($"Sigin user {loginModel.UserName}
finished in {stopwatch.Elapsed}");
}
}
- 转到
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 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 for calling RequestServices.GetService<T>();, you must also add using Microsoft.Extensions.DependencyInjection; as you have done before 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))
{
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()));
await httpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(identity),
new AuthenticationProperties { IsPersistent = false });
return true;
}
return false;
}
- 更新
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();
}
- 启动应用,以现有用户身份登录,然后转到 Account Details 页面。 启用双因素身份验证(在此步骤之前,您可能需要重新创建数据库并注册一个新用户):

- 以用户身份登录,进入登录页面,然后再次登录。 这次您将被要求输入一个双因素认证码:

- 您将收到一封带有双重认证码的电子邮件:

- 点击电子邮件中的链接,一切都会自动为你填写。 登录并验证一切是否正常工作:

增加忘记密码和密码重置机制
现在您已经了解了如何向应用添加身份验证,您必须考虑如何帮助用户重置他们忘记的密码。 用户会忘记他们的密码,这是可能发生的,所以您需要一些适当的机制。
处理这类请求的标准方法是向用户发送一个电子邮件重置链接。 然后用户可以更新他们的密码,而不用冒着通过电子邮件发送明文密码的风险。 将用户密码直接发送到用户电子邮件是不安全的,应该不惜一切代价避免。
您现在将看到如何添加重置密码功能到井字策略应用:
- 更新登录表单,并在 Sign Up Here 链接之后直接添加一个名为
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);
}
- 在
View/EmailTemplates文件夹中添加一个名为ResetPasswordEmail的新视图:
@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 });
}
- 在
Views/Account文件夹中添加一个名为SendResetPassword的新视图:
@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)
{
var start = DateTime.Now;
_logger.LogTrace($"Reset user password {userName}");
var stopwatch = new Stopwatch();
stopwatch.Start();
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;
}
finally
{
stopwatch.Stop();
_logger.LogTrace($"Reset user password {userName}
finished in {stopwatch.Elapsed}");
}
}
- 更新
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 2.0 web 应用。
下面的例子解释了如何使用它:
- 在
Models文件夹中添加一个名为UserRoleModel的新类,并使其继承IdentityUserRole<long>; 它将被内置的 ASP.NET Core 2.0 身份验证特性:
public class UserRoleModel : IdentityUserRole<Guid>
{
[Key]
public long Id { get; set; }
}
- 更新游戏数据库上下文中的
OnModelCreating方法:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
...
modelBuilder.Entity<IdentityUserRole<Guid>>()
.ToTable("UserRoleModel")
.HasKey(x => new { x.UserId, x.RoleId });
}
- 打开 NuGet 包管理控制台,执行
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 Object Explorer 中打开
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)
{
var start = DateTime.Now;
_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;
}
if (await _userManager.GetTwoFactorEnabledAsync(user))
{
return SignInResult.TwoFactorRequired;
}
await SignIn(httpContext, user);
return isValid;
}
catch (Exception ex)
{
_logger.LogError($"Ca not sigin user
{loginModel.UserName} - {ex}");
throw ex;
}
finally
{
stopwatch.Stop();
_logger.LogTrace($"Sigin user {loginModel.UserName}
finished in {stopwatch.Elapsed}");
}
}
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);
...
- 在
Startup类中添加一个名为AdministratorAccessLevelPolicy的新策略,就在 MVC 中间件配置之后:
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 2.0 (Cookie, Bearer, and more) at the same time.
For this case, the Authorize decorator you have seen before allows you to define which middleware can authenticate a user.
Here is an example to allow Cookie and Bearer:
[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 2.0 应用,包括管理应用用户的身份验证和授权。
您已经通过 Facebook 向示例应用添加了基本表单身份验证和更高级的外部提供者身份验证。 这将为您提供一些关于如何在自己的应用中处理这些重要主题的好想法。
此外,您还学习了如何添加标准的重置密码机制,因为用户总是会忘记他们的密码,您需要尽可能安全地响应这种类型的请求。
我们甚至还讨论了双因素身份验证,它可以为关键应用提供更高的安全级别。
最后,您了解了如何以多种方式(基本、角色、策略)处理授权,因此您可以决定哪种方法最适合您的特定用例。
在下一章中,我们将讨论在托管和部署您的 ASP 时您将拥有的不同选择.NET Core 2.0 web 应用。
十、托管和部署 ASP.NET Core 2.0 应用
就这样,我们几乎到了本书的末尾,这意味着我们几乎完成了整个应用开发生命周期,因此,客户很快就可以使用您的应用了! 不要悲伤; 相反,应该感到自豪,因为在阅读和理解本书的倒数第二章之后,您将获得强大的技能,以创建和部署具有强大技术基础的令人惊叹的应用!
让我们回顾一下,从本书开始到现在:您已经了解了如何设置开发环境,如何使用 ASP 的各种特性.NET Core 2.0 来开发现代 web 应用,如何通过实体框架核心将它们连接到数据库,最后,在最后一章中,如何保护它们免受恶意网络罪犯的攻击。
现在,我们需要讨论循环中的最后一个步骤,该步骤包括在应用准备就绪后托管和部署它们。
本章的目标是解释你拥有的不同选项,如何选择正确的选项,以及如何部署你的 ASP.NET Core 2.0 web 应用使用最新的技术和云提供商。
在本章中,我们将涵盖以下主题:
- 托管应用
- 在 Amazon Web 服务中部署应用
- 在 Microsoft Azure 中部署应用
- 部署应用到 Docker 容器中
托管应用
您可以构建世界上最好、最有用的应用,但如果您的客户不能轻松地从任何设备访问它们,那么您可能无法获得预期的成功。 正如你在下图中看到的,应用需要越来越多的全渠道,这意味着客户需要能够在一个设备上启动,然后在另一个设备上继续:

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

- 填写“创建一个新的 AWS 帐户”表单,然后单击“继续”:

- 填写联系信息表单,点击创建帐户并继续:

- 填写付款信息表单并点击继续:

- 填写身份信息验证表单,点击 Continue,然后选择一个支持计划,点击 Continue:

- 注册完成后,你会被自动重定向到欢迎页面,在这里你应该点击启动管理控制台按钮:

在创建了新的 Amazon Web Services 用户帐户之后,现在可以准备部署第一个 ASP。 Amazon Web Services 中的 NET Core 2.0 应用。
当使用 Amazon Web Services 时,你基本上有两种选择来部署你的 Asp.NET Core 2.0 web 应用:
- Amazon Web Services Elastic Beanstalk
- Amazon Web Services EC2 容器服务
下一节将介绍如何在 Amazon Web Services Elastic Beanstalk 中部署应用。 所以,敬请期待,系好安全带,享受你的旅程吧!
在 AWS Elastic Beanstalk 中部署应用
- AWS Elastic Beanstalk 是一个为 Amazon Web Services 中的基于 Web 的应用提供的 PaaS 产品,其中包括自动伸缩。 在这方面,它可以与 Microsoft Azure App Services 相媲美,你将在本章后面的章节中看到。
- 它消除了管理基础设施的需要; 相反,您只需要关心应用的构建和托管。 对于完整的 DevOps 方法,如果你想使用 Amazon Web Services,建议使用这个 PaaS 服务。
For more information on AWS Elastic Beanstalk, check out https://aws.amazon.com/fr/elasticbeanstalk/.
下面的示例逐步说明如何在 Amazon Web Services Beanstalk 中部署Tic-Tac-Toe应用。
让我们从创建 AWS Beanstalk 应用开始:
- 登录 AWS,进入 AWS Management Console,在 AWS 服务文本框中输入
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 Change platform version link.
- 等待直到创建了 Beanstalk 应用; 取决于您的互联网连接和 AWS,这可能需要一段时间:

在能够部署Tic-Tac-Toe应用并在最后运行它之前,需要在接下来的步骤中准备技术环境。
在前面的章节中,您可能已经看到,应用需要一个数据库来持久化用户和应用数据。 为此,我们将在 Amazon Web Services 中提供一个称为 RDS 服务的 SQL Server PaaS 服务,如下例所示:
- 返回到 AWS 管理控制台,并在最近访问的服务部分单击 Elastic Beanstalk:

- 在 Beanstalk All Applications 页面上,选择所需的环境,然后单击 Default-Environment:

- 在特定的 Beanstalk 应用页面上,单击左侧菜单中的 Configuration:

- 向下滚动并点击创建一个新的 RDS 数据库链接:

- 选择 DB Engine SQL Server Express (sqlserver-ex),并输入主用户名和密码; 保留其余字段的默认值,单击页面底部的 Apply 按钮,等待数据库创建完成:

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 Management Console,在 AWS 服务文本框中输入
RDS,点击显示的链接; 你将被重定向到 Amazon RDS 页面,点击左侧菜单中的 Instances:

- 点击你的实例; 将显示实例仪表板。 向下滚动获取端点地址,这将用于在部署前更新应用连接字符串:

- 在 Amazon RDS 实例页面上进一步向下滚动,并单击安全组:

- 在安全组页面上,单击页面底部菜单中的入站,然后单击编辑,以便能够更新您刚刚创建的数据库的安全组的入站规则:

- 点击 Add Rule 按钮,选择 All TCP 作为类型,Anywhere 作为源,并输入有意义的描述,然后点击 Save 按钮:

Note that you should configure the security group inbound rules stricter in a real production environment and set real IP restrictions. The source Anywhere should not be used for production environments.
- 在 Visual Studio 2017 中打开 SQL Server 对象管理器 使用之前的端点地址、用户名和密码登录,然后创建一个名为
TicTacToe的新数据库:

- 更新
appsettings.json文件中的DatabaseConnectionString,将参数替换为相应的值:
"Server=<YourEndPoint>;Database=TicTacToe;
MultipleActiveResultSets=true;
User id=<YourUser>;pwd=<YourPassword>"
您已经成功地配置了技术环境,这意味着您现在能够发布数据库模式以及部署 web 应用。
您是否迫切地等待在云中运行应用? 只要保持专注并继续做下去,您很快就会看到您的应用在 Amazon Web Services 中运行。
发布数据库模式时,你有三种选择:
- 通过实体框架迁移在 Visual Studio 2017 中生成一个 SQL 脚本来创建数据库。
- 更改
Data\GameDbContextFactory.cs中的默认连接字符串,并在包管理器控制台中执行Update-Database指令。 - 运行应用来创建数据库。
最合适的解决方案取决于应用及其数据库的类型和大小。 根据经验,最好是为较大的应用生成脚本然后创建数据库,而对于较小的应用,在应用第一次运行时自动创建数据库也是可以接受的。
让我们看看在你看到Tic-Tac-Toe应用在 Amazon Web Services 中运行之前需要做些什么:
- 打开 Visual Studio 2017 中的包管理器控制台,执行
Script-Migration指令,如下所示:

- 获取生成的脚本并将其复制到 Amazon RDS 数据库的查询窗口中,然后执行脚本来创建数据库和各种数据库对象。
- 从https://marketplace.visualstudio.com/items?itemName=AmazonWebServices.AWSToolkitforVisualStudio2017下载并安装 AWSToolkitforVisualStudio2017:

- 进入 AWS Management Console,在 AWS 服务文本框中输入
IAM,点击显示的链接; 你将被重定向到亚马逊身份和访问管理页面:

- 在 Amazon 身份和访问管理页面,点击用户,然后点击添加用户按钮:

- 在 Add User 页面上,给新用户一个有意义的用户名,并授予他编程访问权限,然后单击页面底部的 Next:Permissions 按钮:

- 现在必须为新用户设置权限; 为此,点击附加现有的策略直接按钮:

- 从现有的策略中选择 AdministratorAccess,然后单击页面底部的 Next:Review 按钮:

- 验证用户名和 AWS 访问类型以及所选策略是否正确,然后单击 Create User 按钮:

- 等待新用户被创建; 当成功页面显示时,你就可以下载
.csv文件,我们将用它来配置 Visual Studio 2017 的 AWS:

- 打开 Visual Studio 2017 并通过查看| AWS Explorer 显示 AWS Explorer:

- 点击新帐户配置文件按钮(唯一的活动按钮):

- 将显示一个向导; 保留配置文件名称为默认值,并使用您之前在 AWS 上创建新用户过程中下载的
.csv文件中的值填充 Access Key ID 和 Secret Access Key:

- 因为 AWS 是基于 IIS 作为。NET Core 应用的主机,你现在需要在
TicTacToe项目中添加一个web.config文件:
<?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 构建器配置以启用 IIS 集成,如下所示:
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup("TicTacToe")
.CaptureStartupErrors(true)
.UseApplicationInsights()
.UseIISIntegration()
.Build();
- 右键单击
TicTacToe项目,点击 Publish to AWS Elastic Beanstalk… 在上下文菜单中:

- 将显示一个向导; 单击“重新部署到现有环境”,并选择之前创建的默认环境,然后单击“下一步”按钮:

- 验证框架版本设置为
netcoreapp2.0; 保留所有默认值,然后单击 Next 按钮:

- 选择 Generate AWSDeploy 配置,这将允许你用 AWS 重新部署一个应用的副本,然后单击 Deploy 按钮:

- 部署将开始; 你可以通过 Output | Amazon Web Services 看到部署过程的进展:

- 当应用部署后,你可以使用 AWS Explorer 获取应用的 URL,如下所示:

- 打开浏览器,进入 Amazon Web Services 中的应用 URL,启动应用并尝试注册一个新用户。
Note that the application is not working as expected, you will get a 404 Not Found HTTP response. Everything is working locally and the deployment in Amazon Web Services 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.
祝贺您,您已经成功地在公共云中部署了您的第一个应用。 现在外部世界可以使用它,用户可以连接到它并开始使用它。
以上就是 Amazon Web Services 的示例。 然而,我们仍然有一些引人注目的内容,因为我们将在接下来的章节中探索如何部署到其他目标,如 Microsoft Azure 和 Docker 容器; 所以,保持头脑清醒,继续阅读下面的章节。
在 Microsoft Azure 中部署应用
Microsoft Azure 是微软提供的一个公共云计算平台,用于在全球各地的微软数据中心内构建、测试、部署和管理应用和服务。 它支持许多不同的编程语言、工具和框架,包括特定于 microsoft 的、第三方的和开源的软件和系统。
在 Microsoft Azure 中部署 web 应用时,你基本上有四种选择:
- Azure 应用服务
- Azure 服务结构
- Azure 容器服务
- Azure 虚拟机
然而,在你开始在 Microsoft Azure 中部署你的应用之前,你需要注册一个订阅; 所以,让我们现在就做:
- 您需要一个 Microsoft 帐户才能注册 Microsoft Azure 订阅。 可以使用相同的你的用于 Visual Studio 团队服务(VSTS)订阅,但如果你没有一个,创建了 http://www.live.com,点击创建一个! 链接:

- 到https://portal.azure.com,用你的微软账号登录; 你会被问到是否想去参观。 选择 Maybe later(不过你应该稍后再参观!),你将被重定向到微软 Azure 管理门户:

- 点击左侧菜单底部的更多服务,然后点击订阅按钮:

- 点击 Add 按钮:

- 点击免费试用按钮,填写不同的表格,直到你创建了你的微软 Azure 订阅:

令人兴奋的! 现在您已经准备好提供技术环境,然后部署您的 ASP.NET Core 2.0 web 应用到全世界的微软 Azure 数据中心!
在 Microsoft Azure 应用服务中部署应用
Azure App Services 是为 Microsoft Azure 中基于 web 的应用提供的一种 PaaS 服务,其中包括自动伸缩功能。 在这方面,它类似于 AWS Beanstalk,您可能已经在之前的 AWS 小节中看到过它。
它消除了管理基础设施的需要; 相反,您只需要关心应用的构建和托管。 对于完整的 DevOps 方法,如果你想使用 Microsoft Azure,最好使用这个 PaaS 服务。
For more information on Microsoft Azure App Services, check out https://docs.microsoft.com/en-us/azure/app-service/app-service-web-overview.
下面的例子演示了如何将Tic-Tac-Toe应用逐步部署到 Azure App Services:
- 进入微软 Azure 管理门户,单击左侧菜单中的 App Services,然后单击 Add 按钮:

- 点击 Web Apps 部分的 Web App + SQL 按钮:

- 阅读服务细节并点击 Create 按钮:

- 如果你以前没有注册过你的微软 Azure 订阅,你现在可以通过点击注册来获得一个新的订阅链接; 你会被重定向到你在这一节开始看到的相同的形式:

- 为应用和资源组选择一个名称,然后单击 SQL 数据库按钮来配置数据库选项:

- 单击 Create Database 并选择一个数据库名称。 保留其他选项的默认值,并单击目标服务器:

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

- 点击新数据库服务器的 Select 按钮,新数据库,最后点击 App Service 的 Create 按钮; 等待,直到它已供应:

- 您需要允许访问 SQL 数据库,以执行
TicTacToe应用的数据库生成脚本; 在左侧菜单中,点击 SQL 数据库,选择TicTacToe数据库:

- 点击设置服务器防火墙,可以添加一个新的规则,允许从您的 IP 访问 SQL 数据库:

- 点击 Add client IP,验证你的 IP,然后点击 Save 添加新规则:

- 点击连接字符串来检索你之前创建的
TicTacToeAzure 数据库的连接字符串:

- 打开 Visual Studio 2017,进入 SQL Server 对象资源管理器,使用
TicTacToeAzure 数据库连接字符串中的连接信息添加一个新的 SQL Server。 - 在 Azure SQL Server 中添加一个新的数据库,就像你在 Amazon Web Services 示例中所做的那样; 它将用于执行
TicTacToe数据库生成脚本:

- 如果你之前没有在 Amazon Web Services 的例子中做过,在 Visual Studio 2017 中打开包管理器控制台并执行
Script-Migration指令; 否则,您可以重用相同的脚本。 - 获取生成的脚本并将其复制到 Azure
TicTacToe数据库的查询窗口中,然后执行脚本来创建数据库和各种数据库对象。
现在,技术基础已经在 Microsoft Azure 中准备好并初始化了,接下来的步骤就是部署示例应用。
所以,让我们来准备应用,并通过 Visual Studio 2017 将其部署到你之前创建的 Microsoft App Service 中:
- 因为 App Services 是基于 IIS 作为。net Core 应用的主机,所以你现在必须在
TicTacToe项目中添加一个web.config文件; 然而,如果你遵循了之前的亚马逊网络服务的例子,你应该已经这样做了:
<?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 构建器配置以启用 IIS 集成。 然而,如果你遵循了之前的亚马逊网络服务的例子,你应该已经这样做了:
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup("TicTacToe")
.CaptureStartupErrors(true)
.UseApplicationInsights()
.UseIISIntegration()
.Build();
- 转到微软 Azure 管理门户,单击左侧菜单中的 App Services,选择之前创建的
TicTacToe应用,单击获取发布配置文件,并下载 Azure App Service 发布配置文件:

- 右键单击
TicTacToe项目,单击上下文菜单中的 Publish,然后单击 Import Profile 按钮,如下所示:

- 选择下载的 Azure App Service Publish 配置文件,发布过程应该会自动启动:

- 你可以在 Web 发布活动视图中看到发布流程:

- 打开浏览器,进入 Microsoft Azure 中的应用 URL,启动应用并尝试注册一个新用户。
Note that 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 容器时,你基本上有三种选择:
- 根据操作系统的不同,在本地或云中使用 Docker (Windows)或 Docker Enterprise Edition (Windows 2016)
- 使用 Docker Hub(https://hub.docker.com)和 Docker Store(https://store.docker.com)
- 可以使用 Microsoft Azure 容器服务或 Amazon Web 服务 EC2 容器服务
For more information on Docker, visit the following links:
https://www.docker.com
https://docs.microsoft.com/en-us/dotnet/core/docker/docker-basics-dotnet-core
使用 Windows 和企业版 Docker 将应用部署到 Docker 容器中
Docker for Windows 提供了在 Windows 环境中开始使用 Docker 容器所需的一切,而 Docker Enterprise Edition for Windows 2016 是为需要提供基于 Docker 技术的生产环境的公司提供必要的支持。
让我们看看如何在 Windows 中使用 Docker,以及在这种情况下如何部署你的应用:
- 如果你还没有安装 Docker for Windows,去https://docs.docker.com/docker-for-windows/install/,点击获取 Docker for Windows (Stable)按钮,并安装它:

To install the Docker Enterprise Edition for Windows 2016, go to https://docs.docker.com/engine/installation/windows/docker-ee/ and follow the installation instructions. After the installation, you should skip the following steps and continue directly with the fourth step.
- 右键单击 Docker 托盘图标,点击切换到 Windows 容器… 在上下文菜单中:

- 如果在你的 Windows 安装中还没有启用 Container 特性,Docker 会要求为你启用; 点击 Ok 按钮:

- 打开一个新的升高的命令提示符,下载官方 Docker Microsoft SQL Server 镜像,并执行
docker pull microsoft/mssql-server-windows-express指令,如下所示:

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

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

- 打开 Visual Studio 2017,然后打开
TicTacToe项目; 在菜单中,点击项目| Docker 支持,选择 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 image 基于 Kestrel 而不是 IIS:
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup("TicTacToe")
.CaptureStartupErrors(true)
.Build();
}
- 按F5启动应用(应该将
docker-compose项目设置为启动),应用现在应该自动部署到 Docker 容器中; 验证一切仍按预期工作:

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

将图像发布到 Docker Hub
你可以把你的应用镜像上传到 Docker 的中心云存储库 Docker Hub,然后在 Microsoft Azure、Amazon Web Services 或任何其他支持 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 advised to use that though.
For more information on Docker Hub, check out https://docs.docker.com/docker-hub.
下面的示例展示了如何将示例TicTacToe应用发布和上传到 Docker Hub:
- 右键单击
TicTacToe项目,在上下文菜单中选择 Publish; 由于在前面的示例中已经创建了一个发布配置文件,因此必须添加一个新配置文件。 点击创建新配置文件:

- 点击 Container Registry 按钮,选择 Docker Hub,然后点击 Publish 按钮:

- 输入你的 Docker Hub 用户名和密码,然后点击保存:

- 您的容器图像将被发布到 Docker Hub; 完成后,进入 Docker Hub,确认图片已经上传:

总结
在这一章中,我们讨论了托管和部署 ASP 的各种选择.NET Core 2.0 web 应用。
您了解了什么是托管以及如何为给定的用例选择适当的解决方案。 这将允许您为自己的应用做出更好的决策。
您已经了解了如何注册 Amazon Web Services 帐户,如何提供技术环境,以及如何部署 ASP.NET Core 2.0 web 应用。
此外,您还了解了如何注册 Microsoft Azure 帐户、如何提供技术环境以及如何部署 ASP.NET Core 2.0 web 应用使用这个强大的公共云计算平台。
然后,我们讨论了 Docker 以及在使用这种现代的、越来越被采用的、有影响力的技术时所拥有的各种部署选择。 你已经为未来做好了充分的准备,因为 Docker 可能完全改变我们部署和管理应用的思维方式。
在下一章中,我们将解释如何有效地管理和监督已部署的 web 应用,这对 DevOps 方法非常重要。
十一、管理和监控 ASP.NET Core 2.0 应用
在完成开发生命周期之后,我们可以就此停止。 然而,添加这最后一章是为了强调彻底的 DevOps 方法的重要性。
现在,我们只谈到了发展(Dev),但你也应该接受操作(行动)在【显示】DevOps,由管理和监督您的应用在运行时。
这个非常重要的问题常常被低估,有时甚至被完全抛在一边。 开发人员往往认为这不是他们工作的一部分。 他们经常这样说,但它在我的机器上工作和这是你的问题而不是我的问题。 这也通常被称为混淆壁。 敏捷方法和 DevOps 旨在避免这种思维,本章将提供一些建议和例子,告诉你如何在 ASP 中更好地解决这些问题.NET Core 2.0 应用。
应用的成功将取决于您如何帮助IT Operations理解运行时发生的事情。 这意味着为他们提供快速有效地管理和监督应用的方法。
只有这样,您才能以较低的平均修复时间(MTTR)为 bug 提供高质量的应用,这对您在特定市场中成为未来的市场领导者具有完全不同的意义。
此外,在使用 ASP 时,您可以很容易地解决这些问题.NET Core 2.0,因为在大多数情况下,您可以利用集成的或提供的特性,而无需进行任何较大的代码更改。
在本章中,我们将涵盖以下主题:
- 日志在 ASP.NET Core 2.0 应用
- 监控 ASP.NET Core 2.0 应用
日志在 ASP.NET Core 2.0 应用
在第十章,中 ASP 的托管和部署.NET Core 2.0 应用,我们解释了如何部署您的 ASP.NET Core 2.0 应用到 Microsoft Azure、Amazon Web Services 和 Docker。 让我们进一步了解如何在这些环境中添加日志记录和监视,这对于诊断意外行为和错误非常重要。
首先是一些理论背景,然后是一些实际例子。 你准备好学习如何帮助 it 运营了吗? 拜托,这是最后一章了。 我们走吧!
应用中的日志记录包括创建数据,以帮助理解运行时发生的事情。 可以记录几种类型的消息,例如信息、警告和错误。
然后,这些数据应该持久化到日志文件、数据库、SaaS 解决方案或其他目的地。 为了提高应用性能,建议允许 it Operations 在应用运行时更改收集的日志数据的详细级别。 例如,在生产环境中,只应该记录警告和错误,而在开发期间记录所有内容是非常有意义的,这样可以提高效率,更好地理解幕后发生的事情。
建议使用 ETW 这样的标准框架来构造和格式化日志数据,以便 It 运维人员可以使用他们喜欢的监控工具快速、轻松地读取和诊断错误原因。 著名的日志框架,如 Serilog 或 Log4Net 也支持标准输出格式,所以如果您愿意,也可以使用它们。
因此,让我们看一些具体的例子,看看如何为您的 ASP 处理日志记录.NET Core 2.0 应用在不同的环境中,如本地、公共云和 Docker。
在本地环境中,日志数据大部分时间存储在日志文件中。 在这种情况下,应用需要具有写访问日志文件的权限,建议将所有日志文件存储在应用路径下名为logs的中心文件夹中。
在 Microsoft Azure 中,你基本上有三种不同的解决方案来处理应用中的日志记录:
- 标准文件日志:这是最简单的方法,不需要任何代码修改,但也是功能最弱的; 您需要下载文件来检索应用的日志记录数据。
- Azure 应用服务诊断:如果您的应用服务没有多个实例,那么这是推荐的解决方案,因为没有提供日志集中化功能。
- Azure Application Insights:这是最集成和最强大的解决方案,可以跨所有应用层工作。
Amazon Web Services 提供 CloudWatch 用于日志和监控。 所提供的日志记录机制与 Microsoft Azure 非常相似。 当你理解了如何为 Microsoft Azure 做这件事之后,你将能够将你的知识轻松快速地应用到 Amazon Web Services 中,正如你将在给出的例子中看到的那样。
For more information, you can visit the Amazon Web Services CloudWatch website at https://aws.amazon.com/en/cloudwatch.
Docker 不像微软 Azure 或亚马逊 Web 服务那样提供任何集成的监控或日志服务。 这意味着,要在 ASP 中添加、记录和监视功能。 在 Docker 的 NET Core 2.0 应用中,你必须使用日志文件。 此外,您必须提供自己的集中式日志恢复和分析机制,以获得一致的日志记录和监视数据。
但是,由于应用可以实例化多次,这可能不是最好的方法。 相反,您也可以直接登录到一个集中的控制台,这应该是 Docker 环境中最有效和最合适的解决方案。
登录 Microsoft Azure
好了,现在您已经看到了几种用于在不同环境中登录的解决方案,接下来我们将关注 Microsoft Azure。 如果您担任 IT 操作的角色,谁需要诊断为什么一个应用在 Microsoft Azure 中不能正常工作? 你的选择是什么,最好的解决方案是什么? 这正是你将在本节学习的内容。
如果你还记得,我们已经在第 4 章、、中讨论了在应用级别上的日志记录.NET Core 2.0 -第 1 部分,本书。 在那里,我们将记录应用事件添加到应用文件夹中名为logs的子文件夹中的日志文件中。 需要对这个文件夹进行同步并监视其磁盘空间的使用情况,因为当它变得太大时,它本身也可能成为一个故障原因。
此外,还有多个日志源,因为应用日志和环境日志(IIS、Windows、SQL Server 等)是分开处理的。 你必须把所有的信息结合起来,才能对幕后发生的事情有一个全面的了解。 这是非常复杂和耗时的。
如您所见,在这种情况下,需要大量的手动工作来读取和分析应用日志。 如果您需要同时监视和监督大量的应用,这就会成为一个更大的问题。 手动执行所有操作并不是一个可行的选择。 我们需要找到一个更好的解决办法。
此外,在 Microsoft Azure 中有更好、更集成的解决方案! 例如,如果您在 Azure 应用服务中部署应用,则可以使用 Azure 诊断应用服务。 可以从门户直接启用该特性。 此外,应用日志和环境日志自动集中在一个地方,这有助于以更快、更直接的方式发现问题。
启用 Microsoft Azure Application Service 诊断非常简单,所以让我们看看现在如何做:
- 在 Visual Studio 2017 中打开井字游戏 Web 项目,并在
Extensions文件夹中添加一个新的扩展名为AzureAppServiceDiagnosticExtension:
public class AzureAppServiceDiagnosticExtension
{
public static void AddAzureWebAppDiagnostics(IConfiguration
configuration, ILoggingBuilder loggingBuilder)
{
loggingBuilder.AddAzureWebAppDiagnostics();
}
}
- 更新配置日志扩展中的
AddLoggingConfiguration方法,并为新添加的 Azure 应用服务诊断扩展添加一个案例:
public static class ConfigureLoggingExtension
{
public static ILoggingBuilder AddLoggingConfiguration(this
ILoggingBuilder loggingBuilder, IConfiguration configuration)
{
var loggingOptions = new Options.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;
}
case "azureappservices":
{
AzureAppServiceDiagnosticExtension
.AddAzureWebAppDiagnostics(configuration,
loggingBuilder);
break;
}
default:
{
break;
}
}
}
return loggingBuilder;
}
}
- 更新
appsettings.json配置文件,为 Azure App Services 添加一个新的提供商:
"Logging": {
"Providers": [
{
"Name": "Console",
"LogLevel": "1"
},
{
"Name": "File",
"LogLevel": "2"
},
{
"Name": "azureappservices"
}
],
"MinimumLevel": 1
}
- 更新
Program.cs文件,更改 WebHost 构建器配置以启用 IIS 集成,并添加如下日志配置:
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.CaptureStartupErrors(true)
.UseStartup("TicTacToe")
.PreferHostingUrls(true)
.UseApplicationInsights()
.UseIISIntegration()
.ConfigureLogging((hostingcontext, logging) =>
{
logging.AddLoggingConfiguration(
hostingcontext.Configuration);
})
.Build();
-
将一字棋 Web 应用发布到 Azure 应用服务中; 如果你不知道如何做,你可以在第 10 章,托管和部署 ASP.NET Core 2.0 应用。
-
打开微软 Azure 门户网站,点击菜单中的应用服务,选择你已经部署的一字棋应用服务,向下滚动,直到你看到“监控”部分:

- 在监视部分,单击应用日志记录(文件系统)on 按钮,选择详细级别,启用详细错误消息和失败请求跟踪,然后单击保存按钮:

Tic-Tac-Toe应用现在开始将数据记录到 Azure App Service 文件系统中。 然而,这只是第一步。 您将需要检索日志以便能够分析它们。
根据您的特定需求,有多种访问日志的方式。 这里列出了其中一些:
- 使用 FTP 或 FTPS 浏览
logs文件夹 - 配置 Azure Blob 存储,然后下载 Blob 内容,这还可以将多个服务的日志集中到一个地方
- 使用专用的应用自动检索日志
幸运的是,社区已经在 GitHub 上开发了一个开源解决方案,叫做 Azure Web Site Logs Browser Extension,你可以使用它。 这个解决方案包括向 Azure Portal 添加一个扩展。
现在,您将看到如何将 Azure Web Site Logs 浏览器扩展添加到 Microsoft Azure Portal 来分析日志:
- 转到 Microsoft Azure Portal 网站,单击菜单中的应用服务,选择你在前面示例中部署的一字连字应用服务,向下滚动直到你看到开发工具部分,单击扩展,然后在添加按钮:

- 选择和安装 Azure Web Site Logs 浏览器扩展由 Amit 苹果发布:

- 安装完成后,扩展将被添加到您的井字游戏应用服务的活动扩展:

- 单击 Azure Web Site Logs Browser Extension,您将看到扩展名称、作者和版本号以及其他附加信息的概述。 点击浏览按钮:

- 一个新的浏览器窗口将自动打开,在那里您可以看到不同的日志文件源; 点击文件系统-应用日志:

- 选择一个包含您需要分析的诊断数据的日志文件:

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

登录亚马逊网络服务
如果您正在使用 Amazon Web Services,那么将日志记录添加到您的 ASP.NET Core 2.0 应用对你来说将是非常简单的。 您只需要将应用日志写入控制台,部署在 Amazon Web Services Elastic Beanstalk 中的应用将自动将其日志存储在 Amazon Web Services CloudWatch 中。 然后,您将能够使用 CloudWatch 仪表板来分析正在发生的事情。 这与您在前面示例中看到的 Application Insights 及其仪表板类似。
现在,您将学习如何访问已部署到 Amazon Web Services Elastic Beanstalk 中的应用的日志:
- 在 Amazon Web Services Elastic Beanstalk 上发布井字游戏 Web 应用 如果你不知道如何做,你可以在第 10 章,托管和部署 ASP.NET Core 2.0 应用。
- 启动应用,进入 AWS Management Console,在 AWS Services 文本框中输入
Beanstalk,点击显示的链接; 你会被重定向到弹性豆茎欢迎页面:

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

- 单击左边菜单中的日志,并单击请求日志|最近 100 行; 你现在可以下载你需要分析的日志文件:

- 下载一个日志文件并检查其内容:

您已经了解了如何在各种环境中处理日志记录,包括内部环境和云环境; 下一节将介绍监视,以及它如何帮助您实时分析问题。
监控 ASP.NET Core 2.0 应用
在上一节中,您了解了如何为您的 ASP 生成和分析应用日志.NET Core 2.0 web 应用,这将帮助您更好地理解意外行为和应用错误。 这将帮助 IT 操作在事件发生后跟踪不同的步骤,直到找到问题的根本原因。
但是,这并不能帮助他们持续地监视和监督应用,因为在这种情况下使用日志记录机制将导致糟糕的性能和对整个应用的负面影响。 日志记录并不是持续监视的正确解决方案!
监视的目标是实时分析和监督大量应用指标,并自动检测应用异常。 为了有效地工作,度量标准需要具有非常低的消息足迹。
最常见的 ASP 监控框架.NET Core 2.0 的列表如下:
- 带有 ETW 的 EventSource 非常快,而且是强类型的,它是在。net 4 中引入的,只在 Windows 上工作
- 诊断源,这是非常相似的 EventSource,工作于跨平台,像 EventSource 与 ETW 为 Windows 和像 LTTNG 为 Linux
For more information on ETW, go to the following website:
https://msdn.microsoft.com/en-us/library/windows/desktop/aa363668(v=vs.85).aspx
For more information on LTTNG, go to the following website:
http://lttng.org
在这些框架之上,大多数公共云提供商提供自己的监控解决方案。 对于 Microsoft Azure,建议使用 Azure Application Insights,而对于 Amazon Web Services,建议使用 CloudWatch。 这两个监视解决方案都是完全 SaaS 的,并且与各自的公共云提供商门户集成得更好。
监控现场和 Docker
目前还没有针对本地和 Docker 环境的标准监控解决方案,但有一些社区批准的监控框架,如 EventSource 或 diagnostics source,你可以使用它们来实现你自己的解决方案。
由于这些框架遵循诸如 ETW 之类的市场标准,IT 运营部门将能够连接您的 ASP.NET Core 2.0 web 应用使用他们的标准监控工具,他们会非常喜欢的!
Windows 上的 Perfmon 就是一个例子,它可以接收 ETW 事件并生成用于监视目的的图表。
使用 diagnostics source 时,首先创建一个侦听器。 该侦听器接收应用事件并提供事件名称和参数。 创建侦听器的最简单方法是创建 POCO 类,该类包含需要用[DiagnosticName]装饰器装饰的方法,并且设计为接受适当类型的参数。
下面的示例说明如何使用 diagnostics source 将监视添加到您的 ASP.NET Core 2.0 在本地和 Docker 环境中的应用:
- 在 Visual Studio 2017 中打开一字棋 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);
...
}
- 更新通信中间件,添加一个新的私有成员
_diagnosticSource,并更新构造函数如下:
private readonly RequestDelegate _next;
private DiagnosticSource _diagnosticSource;
public CommunicationMiddleware(RequestDelegate next,
DiagnosticSource diagnosticSource)
{
_next = next;
_diagnosticSource = diagnosticSource;
}
- 更新通信中间件中的
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 2017 中的调试设置,并将项目和模拟器设置为
TicTacToe:

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

如前所述,将日志和监控数据发送到控制台是本地环境的一种可能的解决方案,也是 Docker 环境的推荐解决方案。
Microsoft Azure 中的监控
Microsoft Azure 提供了一个称为 Azure 应用洞察的集成解决方案,它允许 IT 运维实时监控应用、资源和服务。 它适用于整个 Azure 订阅,包括仪表板和图表,用于快速访问分析数据。
下面的图表展示了 Azure Application Insights 的一些特性:

让我们在一个易于理解的示例中使用 Application Insights; 为此,你需要在微软 Azure 中创建一个新的 Azure 应用洞察资源,并提供相应的 API 密钥:
- 转到 Microsoft Azure Portal 网站,在菜单中单击 App Services,选择在前面示例中部署和配置的 Tic-Tac-Toe App Service,向下滚动,直到看到 Monitoring 部分,单击 Application Insights,填写所有字段,然后单击 Ok 按钮。 一个新的应用洞察资源将为您创建:

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

- 应用洞察资源选项卡将显示; 向下滚动,直到你看到配置部分,然后单击 API Access:

- 点击 Create API key 可以生成一个 key,它将用于Tic-Tac-Toe示例应用:

- 配置 API 密钥访问权限(读取遥测、写注释、认证 SDK 控制通道),并给它一个有意义的名称:

现在,您已经完成了 Microsoft Azure 中 Application Insights 资源的创建和配置。 Visual Studio 2017 包含了一些高级的内置特性,这些特性允许您连接 ASP.NET Core 2.0 应用直接从 IDE 中。
在接下来的步骤中,您将配置 ASP。 面向 Azure 应用的 NET Core 2.0 web 应用
- 打开井字游戏 Web 项目,在顶部菜单中单击项目,并选择添加应用洞见遥测…

- 将显示“Application Insights Configuration”页面; 点击开始自由按钮:

- 输入您的帐户和订阅详情,选择一个资源,并点击注册按钮:

- 重新发布 Tic-Tac-Toe Web 应用到 Microsoft Azure AppService,以便应用应用洞察配置。
- 转到 Microsoft Azure Portal 网站,单击菜单中的 Monitor,向下滚动到 Solutions 部分并单击 Application Insights,然后选择新创建的 Application Insights Resource。
- 应用观察仪表板将显示; 它提供了一个全球概况,以及深入到不同的监测领域:

- 点击搜索查看应用流程; 在这里,你可以看到错误已经发生在用户注册过程中:

你可能已经在第 10 章,中看到了这些错误.NET Core 2.0 应用,在将Tic-Tac-Toe应用部署到 Microsoft Azure 或 Amazon Web Services 以及本章前面的日志记录部分之后。 一切都在本地和 Docker 中工作,但当你将其部署到公有云时,它就不再工作了。 非常奇怪! 我们不能再等下去了; 真的需要修理了!
现在我们将更详细地分析这个问题,并尝试了解需要做些什么来解决它:
- 在 Azure Application Insights 中,您可以清楚地看到用户注册存在一个问题,更具体地说,是一个
404 Not FoundHTTP 响应。 - 在查看日志文件时,正如前面部分所解释的,您可以看到无法找到
EmailTemplates文件夹中的UserRegistrationEmailView,这将导致额外的错误:

- 去微软 Azure 门户网站,点击应用服务菜单,选择井字应用服务部署和配置在前面的例子中,向下滚动,直到您看到开发工具部分,点击应用服务编辑器(预览),然后单击链接:
**
- 会自动打开一个带有应用服务编辑器页面的新窗口; 点击 Search 按钮搜索
EmailTemplates文件夹,无法找到,因为在发布过程中,所有视图都被预编译成一个名为TicTacToe.PrecompiledViews.dll的 DLL:

- 通过在发布过程中停用预编译来应用此问题的临时修复,打开井字策略 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 pre-compilation and target the precompiled views in your code for a more industrialized and production-ready solution.
- 重新发布一字棋 Web 应用到微软 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 lack of space within the book. Keep in mind that it was only given to better understand all the ASP.NET Core 2.0 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 2.0 web 应用,并且能够通过使用 Azure Application Insights 来监控它们。 您甚至在用户注册应用期间发现了一个问题。 您已经分析了日志和监视数据,并且能够解决问题。
这在。net Core 代码中工作得非常好,但是目前,您无法看到应用的 JavaScript 部分是否发生任何错误。 由于现代应用包含大量 JavaScript 代码,所以如果能够监视这些部分,那就太好了,对吧? 你可以这样做,你只需要稍微调整一下代码。
让我们看看如何调整代码,并能够监控 JavaScript 应用流:
- 打开 Visual Studio 2017 并打开井字游戏 Web 项目,更新
Views文件夹中的_ViewImports.cshtml文件,并在文件底部添加 Application Insights JavaScript 片段,如下所示:
@inject Microsoft.ApplicationInsights.AspNetCore
.JavaScriptSnippet JavaScriptSnippet
- 更新布局页面和移动布局页面,并在两个页面的标题部分添加如下一行:
@Html.Raw(JavaScriptSnippet.FullScript)
- 更新
Startup类,注册 Application Insights 服务如下:
services.AddApplicationInsightsTelemetry(_configuration);
- 将 Tic-Tac-Toe Web 应用重新发布到 Microsoft Azure AppService,以便应用新的 Application Insights 配置。
- 启动应用并在Microsoft Azure Portal 网站中打开应用洞察力仪表板,点击搜索,然后点击过滤器并选择仅请求,取消选择所有其他事件类型:

很好,你可以不断地监控你的整个应用,无论是在 JavaScript 端还是在。net Core 端,这将在错误行为的情况下非常有用。
在最后一步,您将学习如何添加和监控自定义指标,这将允许您跟踪业务指标在您的应用:
- 打开井字游戏 Web 项目,在
Services文件夹中添加一个名为AzureApplicationInsightsMonitoringService的新服务:
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 Application Insights 监控服务中提取接口并将其命名为
IMonitoringService。 - 在
Options文件夹中添加一个新的选项MonitoringOptions:
public class MonitoringOptions
{
public string MonitoringType { get; set; }
public string MonitoringSetting { get; set; }
}
- 更新
Startup类中的Configure方法,并注册 Azure Application Insights Monitoring Service(如果它已经在appsettings.json配置文件中配置):
...
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 AppService,以便应用新的 Application Insights 配置。
- 启动应用,在微软 Azure 门户网站上打开应用洞察力仪表板,点击搜索并输入
RegisterUser作为搜索词; 您现在只会看到自定义的RegisterUser业务度量:

Amazon Web 服务中的监视
就像 Microsoft Azure 一样,Amazon Web Services 提供了一个集成的解决方案,它允许 IT 运维实时监控应用、资源和服务。 在亚马逊网络服务中,它被称为 CloudWatch。 它提供了与 Applications Insights 几乎相同的功能,也就是说,它适用于整个 AWS 订阅,包括仪表板和图表,用于快速访问分析数据。
下面的例子说明了如何使用 Amazon Web Services CloudWatch 来监控通用指标和自定义指标,以便您可以学习如何为自己的需求做它:
- 打开 Tic-Tac-Toe Web 项目,下载并安装 Amazon Web Services SDK for . net - Core Runtime NuGet 包
AWSSDK.Core,以及 Amazon Web Services cloudwatch NuGet 包AWSSDK.CloudWatch。 - 在
Services文件夹中添加一个名为AmazonWebServicesMonitoringService的新服务,使其继承IMonitoringService接口,并使用 AWS 特定的代码实现Track方法,如下所示:
public class AmazonWebServicesMonitoringService :
IMonitoringService
{
readonly AmazonCloudWatchClient _telemetryClient =
new AmazonCloudWatchClient();
public void TrackEvent(string eventName, TimeSpan elapsed,
IDictionary<string, string> properties = null)
{
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 Cloud Watch Monitoring Service:
...
if (monitoringOptions.MonitoringType ==
"azureapplicationinsights")
{
services.AddSingleton<IMonitoringService,
AzureApplicationInsightsMonitoringService>();
}
else if (monitoringOptions.MonitoringType ==
"amazonwebservicescloudwatch")
{
services.AddSingleton<IMonitoringService,
AmazonWebServicesMonitoringService>();
}
- 更新
appsettings.json配置文件中的Monitoring部分,并将其配置为 Amazon Web Services CloudWatch:
"Monitoring": {
"MonitoringType": "amazonwebservicescloudwatch",
"MonitoringSettings": ""
}
- 将 Tic-Tac-Toe Web 应用发布到 Amazon Web Services Beanstalk,以便应用新的 Amazon Web Services CloudWatch 配置; 如果你不知道如何做,你可以在第 10 章,托管和部署 ASP.NET Core 2.0 应用。
- 启动应用。 进入 AWS Management Console,在 AWS 服务文本框中输入
CloudWatch,点击显示的链接; 您将被重定向到 AWS CloudWatch 欢迎页面:

- 在 CloudWatch 欢迎页面,点击
TicTacToe应用:

- 点击一个告警可以获得更多的详细信息:

- 返回到 CloudWatch 欢迎页面,并在文本框中输入
RegisterUser作为搜索项,然后单击 Browse Metrics:

- 您将看到一个图表,如图所示,其中包含自定义的
RegisterUser业务度量:

总结
在这一章中,我们讨论了如何管理和监督您的 ASP.NET Core 2.0 web 应用,帮助 IT 操作更好地理解在运行时发生错误之前和之后发生了什么。
我们讨论了日志记录的概念,以及它如何帮助减少理解和修复错误的时间。 我们在 Microsoft Azure、Amazon Web Services 和 Docker 中展示了不同的本地日志解决方案。
在一个详细的示例中,您体验了如何使用 Azure AppServices 和 Azure 应用服务诊断以及 Azure Web 站点日志浏览器扩展在 Microsoft Azure 环境中配置日志文件分析。
然后,您看到了如何使用 Amazon Web Services CloudWatch 访问和下载应用日志,从而在 Amazon Web Services 中实现相同的功能。
然后我们介绍了监控的概念,并解释了如何在现场和 Docker 环境中添加监控。
您配置了 Azure 应用洞察来监视您的 ASP.NET Core 2.0 实时 web 应用。 你甚至能够理解和解决问题背后的秘密404 Not Found。
在最后一步中,我们展示了如何使用 Amazon Web Services CloudWatch 在 Amazon Web Services 环境中进行监视。
在下一章,我们会…没有下一章了。 你已经看到了这本书所提供的一切。 我们希望你喜欢它,并在理解和吸收我们所给出的众多例子中发现一些价值。
现在轮到你来创造自己的体验,并进一步提高你的 ASP.NET Core 2.0 技能。
现在你可以作为一个老手开始你的旅程,正如 Nicolas Clerc(云架构师,微软法国)在本书开头的前言中所述。
祝你好运,感谢你花时间阅读不同的章节,感谢你和我们在一起这么久!**




浙公网安备 33010602011771号