ASP.NET Core2 学习手册(一)

原文:Learning ASP.NET Core 2.0

协议:CC BY-NC-SA 4.0

零、前言

每天,软件开发人员、应用架构师和 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 框架:

<assets/b55ecb1a-436c-422e-8505-cd335be0d2b0.png>

微软在 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 项目,创建三个名为ControllersServicesViews的新文件夹,并在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并注册,将文件直接通过电子邮件发送给您。

你可以按以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。
  2. 将鼠标指针悬停在顶部的 SUPPORT 选项卡上。
  3. 点击代码下载和勘误表。
  4. 在搜索框中输入书名。
  5. 选择您想要下载代码文件的书。
  6. 从下拉菜单中选择你购买这本书的地方。
  7. 点击代码下载。

下载文件后,请确保使用最新版本的解压或解压缩文件夹:

  • WinRAR / 7-Zip for Windows
  • 泥柱/学校不洁净
  • 7-Zip / PeaZip for Linux

该书的代码包也托管在 GitHub 上的以下存储库:

我们还可以在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 框架版本和组件是如何一起工作的:

<assets/adbca7c0-7096-42d0-93f4-41f6d5ef809c.png>

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 社区版:

  1. 启动 Visual Studio 2017 Community Edition 安装程序,您将看到各种可安装工作负载的列表。 默认情况下,你会看到 Windows,网页和云,手机和游戏,以及其他工具:

<assets/26619233-58e5-4167-b09e-dbf72d8ca922.png>

  1. 选择您想要的组件,它们将在接下来的步骤中安装。 如果这就是你所需要的,那么就没有其他事情可做了。 如前所述,这是快速安装。

  2. 如果您需要自定义安装的组件,以添加或删除单个组件,那么您必须单击单个组件。 显然,你将做什么是所谓的自定义安装:

<assets/9060c47d-cbb0-4b8d-8db7-0b85d7e01cc9.png>

  1. 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:

    <assets/883c1ecb-0070-4fa7-8235-664b47ac374f.png>

对于更高级的场景,比如自动化和脚本化 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,以及一些常见开发人员任务的快捷方式,比如创建或打开项目:

<assets/75d2f7bc-7b26-4e26-9dc4-bbc9d7de21ba.png>

开始页是完全可定制的,所以如果你不想看到新闻部分,例如,包含来自微软官方频道的开发者新闻,那么你只需要关闭折叠列表或完全删除它。 您可以对起始页进行更多的定制,如果您有兴趣的话,您可以在 MSDN 的https://msdn.microsoft.com/en-us/library/ff425532.aspx中查找详细信息。

Visual Studio 最重要的特性之一是智能感知。 它通过提供列表成员、参数信息、快速信息和完整的 Word 等特性来帮助开发人员提高工作效率。 它在 Visual Studio 2017 中得到了改进,加入了一些非常有趣的新特性,因为现在可以根据类型(类、名称空间或关键字)和 CamelCase 搜索进行过滤。

现在也可以从结果列表中选择最匹配的结果,而不是只选择最前面的一个:

<assets/c2135675-2014-472e-bb37-b731520872b6.png>

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

<assets/31eb0300-9f08-4441-8b2f-0a9bcb29efc7.png>

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

<assets/ec64286a-ada5-47c5-aef0-c55bcb866b50.png>

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

<assets/6aff62db-0274-4234-a12b-e77df080ca2a.png>

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

<assets/61c3aa05-8cb5-43df-be3c-7bd6c580edbe.png>

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

<assets/ce1be239-6759-4276-8273-1ba55576d54b.png>

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 应用:

  1. 如果尚未安装。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.

  1. 启动 Visual Studio 2017。
  2. 创建一个新项目通过点击文件|新|项目:

<assets/0b443a46-9d3b-45d6-b6ce-e2d659eb3dc1.png>

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

<assets/ca0b4821-5101-48dd-9a86-a90f7b6fd2cb.png>

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

<assets/cfb13077-f0d5-4238-a18e-696e6b05526c.png>

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.

  1. 示例应用项目生成之后,将显示一个项目开始页面。 在这里,您可以配置其他选项,如连接服务(Application Insights 等)和发布目标(Microsoft Azure App services、IIS、FTP、Folder 等)。 让一切保持不变:

<assets/52d772b0-4a39-43ba-9544-c69dc62a4dfd.png>

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

<assets/ce5900da-40e1-4d99-8e1e-63a7140911cd.png>

创建您的第一个 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 命令行创建你的第一个示例应用:

  1. 如果尚未安装。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.

  1. 为示例应用创建一个文件夹mkdir aspnetcoresample
  2. 移动到创建的文件夹cd aspnetcoresample
  3. 基于空 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.

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

<assets/aae1ffad-d9a4-4652-9409-0dc8a7093d79.png>

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

<assets/bf75496c-ac27-4e3b-957c-965736fab55f.png>

您已经了解了如何使用 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 运行时。

它有一个流线型的,干净的,非常高效的用户界面。 左边有一个文件和文件夹管理器,右边有一个源代码编辑器,显示你已经打开和当前正在处理的文件的内容:

<assets/98e76ef3-ae45-468e-998f-ecfd643f1d10.png>

用户界面包括以下几个方面:

  • 活动栏:提供几个不同的视图和额外的上下文特定的指示器,如在启用 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等):

<assets/4c45e27b-9899-4768-8407-9f231a033069.png>

让我们看看如何在 Linux Ubuntu 上安装 Visual Studio Code:

  1. 首先,从https://go.microsoft.com/fwlink/?LinkID=760868下载 Linux Ubuntu 安装.deb包(64 位):

<assets/fc3b7469-f26c-45c4-b238-c48317366de6.png>

  1. 在 Ubuntu 中打开一个新的终端窗口。
  2. 通过sudo dpkg -i <file>.deb安装下载的软件包。
  3. 输入sudo apt-get install -f
  4. 输入命令xdg-mime default code.desktop text/plain将 Visual Studio Code 设置为默认文本文件编辑器。

安装将开始并自动安装 APT 存储库和启用自动包更新的签名密钥,以及 Visual Studio Code:

<assets/c0667dd0-e37e-4e88-a15a-dcb294ea2259.png>

您也可以手动安装存储库和签名密钥,更新包缓存,然后最终启动 Visual Studio Code 包安装,如下所示:

  1. 在 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
  1. 输入命令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 终端窗口。 然后,你将安装所有必要的扩展,以便能够在最后运行和调试它:

  1. 启动 Visual Studio 代码; 在资源管理器视图中还没有打开任何文件夹:

<assets/00743f11-43d5-43dc-b1a1-ae33200a6c77.png>

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

<assets/7e7b4825-78c9-455d-85f6-1bc504dc44c7.png>

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

<assets/dca15ecb-ecb1-45b9-89d8-c4b9e0486d86.png>

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

<assets/5fc0e1d9-788c-449a-9f10-986dde87ed51.png>

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

<assets/d9f79123-3879-4db1-9ba7-129d767f91bb.png>

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

<assets/b819fc10-6456-407c-a25f-2b785643c3a8.png>

创建您的第一个 ASP。 Linux 中的 NET Core 2.0 应用

要创建并运行您的第一个示例应用,只使用 Linux 中的 Terminal 窗口,您必须执行以下步骤:

  1. 如果. 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
  1. 为示例应用创建一个文件夹
    mkdir ~/Documents/aspnetcoremvcsample
  2. 移动到创建的文件夹cd ~/Documents/aspnetcoremvcsample
  3. 基于 asp.net 开发一个新的 web 应用.NET Core 2.0 MVC web 应用模板,dotnet new mvc:

<assets/8ac9170c-49c9-4c0b-9096-c7146ad88834.png>

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

<assets/e2ef3f53-bdc4-405a-b439-2e5951fcf63e.png>

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

<assets/3e5ada9b-ed77-407d-8da5-1dedbe51b5d9.png>

总结

在本章中,您已经学习了如何设置您的开发环境,以便能够使用 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 订阅和您的第一个项目。 你将在后面使用它来尝试和理解这本书中的例子:

  1. 转到https://www.visualstudio.com/team-services,点击 Get Started 免费按钮:

<assets/e4da6310-2cb8-42db-b382-5cde6a2bee23.png>

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

<assets/3b492af9-88e9-414e-a276-7b29909c70e2.png>

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

<assets/266aecb0-ee82-4161-bd9a-f3e0bb875017.png>

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

<assets/c51d2a68-f130-4ae5-b6dc-addf9cbc34ec.png>

  1. 新项目生成了,现在可以创建第一个工作项和 Git 存储库了,如本书后面所示。

通过工作项组织工作

工作项用于计划、分配、跟踪,更一般地说,在软件开发项目期间组织您的工作。 它们有助于更好地理解需要做什么,并对项目的状态提供见解。

一些常见的工作项用法是:

  • 为应用特性创建、划分优先级并跟踪用户描述
  • 创建和跟踪实现用户描述所需的开发任务
  • 创建、划分优先级并跟踪应用错误
  • 确定应用质量和发布日期
  • 在一个看板板中显示用户故事、任务和 bug 的进展

如前所述,您可以在 VSTS 项目创建期间选择工作项流程。 这个选择定义了可用的标准工作项类型(WITs)。

默认情况下,有超过 14 个 WITs,您可以为高级场景创建自己的自定义 WITs。 大多数时候,您不需要创建自己的自定义 WITs。

可能的工作项过程选择有:

  • Scrum,如果你的团队使用 Scrum 方法,并且你想在看板板上跟踪你的产品待办事项列表(PBI)
  • 敏捷,如果你的团队在实践一种敏捷方法,但不想遵守特定的 Scrum 约束和术语
  • 如果您的团队遵循更正式的开发任务,那么您可以跟踪请求、更改、风险和评审

下面是一个取决于工作项流程的 WITs 列表:

| 领域 | Scrum | 敏捷 | cmmi |
| 产品规划 | PBI 错误 | 用户故事错误 | 要求改变错误 |
| 投资组合 | 史诗功能 | 史诗功能 | 史诗功能 |
| 任务和冲刺计划 | 任务 | 任务 | 任务 |
| 缺陷积压管理 | 错误 | 错误 | 错误 |
| 问题与风险管理 | 障碍 | 问题 | 问题风险审查 |

在我们的例子中,我们选择了 Scrum 流程。 产品所有者创建主题、特性和产品待办事项列表项(相当于用户描述)。 在 sprint 计划开发期间,任务被定义并与产品待办事项列表项相关联。 通过云中的看板板,整个团队都可以看到所有内容:

<assets/97171d7e-98c7-40f3-8ac9-ca89e230618a.png>

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

<assets/38c6dd49-bc1d-40bc-bae2-d9415f536548.png>

VSTS 允许您分层地组织您的工作。 您可以向上钻取、向下钻取、重新排序和修改父项,以及在层次视图中使用过滤器。

For even more information, go to https://www.visualstudio.com/en-us/docs/work/backlogs/create-your-backlog.

让我们更详细地看看不同的元素。 史诗可以被描述为具有大量工作的大型用户故事。 它必须被分解成特性和更小的产品待办事项列表项,以便能够完全理解它的需求,然后在多个 sprint 中高效地实现它:

<assets/085f1570-82be-4667-ac41-9d6c2b77212a.png>

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

<assets/dd12390c-50bb-4240-9497-cac61c0c4199.png>

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

<assets/07ac9e5c-a80d-44a1-bee3-e02890512724.png>

任务描述了在 sprint 期间实现预期的产品待办事项项功能所需的开发工作。 它们链接到产品待办事项列表项,以实现可跟踪性,并能够自动计算项目进展。

bug 包含已经提出的问题,需要在 sprint 中解决。 它们链接到相应的产品待办事项列表:

<assets/6d4a92bc-83e6-4925-8481-a71e89b92252.png>

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

<assets/09ff4eea-66d9-4b0e-8693-6a8268c1a6c1.png>

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

<assets/a6d389c7-51da-4645-8068-30e69f10dc3e.png>

此外,每个工作项都有一个随时间变化的状态。 该状态允许您跟踪工作成就并过滤工作项,以便更好地理解和检测问题。

下图显示了依赖于工作项过程的各种默认工作项状态:

| | Scrum | 敏捷 | cmmi |
| 工作项状态 | 新批准承诺完成删除 | 新活跃的解决关闭删除 | 提出了活跃的解决关闭 |

您可以查询工作项,创建图表,并将它们发布到 VSTS 项目主页。 如果您需要检索特定的工作项或需要获得项目的整体视图,这是一个非常有用的特性:

<assets/77f5089a-0c96-4363-bedd-a404e902405d.png>

使用 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 中使用,然后创建你的第一个提交:

  1. 在您的 VSTS 项目中,单击顶部菜单中的代码,然后单击在 Visual Studio 中的克隆按钮:

<assets/856a52ae-5abd-440c-825f-8f732ad152b0.png>

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

<assets/8e5bb1e0-638d-4e4d-a636-f6b8655a2448.png>

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

<assets/bf9b95dd-32cd-4422-83c4-1d931f779ef6.png>

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

<assets/f5971aa6-79c0-4a1f-8b29-1d53b3e4ba67.png>

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

<assets/ea5212b9-1a52-4ba4-985e-6f3818236834.png>

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

<assets/a369dbc3-3090-4b8c-b9b4-685d72e11c3a.png>

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

<assets/9cc15c2a-b31a-47ee-8cb2-119e0335e7d6.png>

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

<assets/27413034-f77d-4d8a-89ac-d95733b5b3c0.png>

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

<assets/2b2801b4-ced0-442f-a47d-eae587155493.png>

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

<assets/527394db-50d5-42e7-8413-1b38b1f6e4ec.png>

就是这样! 您已经创建并初始化了 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 创建一个特性分支:

  1. 打开 Visual Studio 2017,进入 Team Explorer 选项卡,点击 Branches 按钮:

<assets/d7a070a0-16d0-420f-a11e-cbc96ed4f105.png>

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

<assets/15e8a59b-8766-444c-9ce2-a1c820adfe2d.png>

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

<assets/651438fe-ea77-4b20-bf72-88c632cae29d.png>

合并更改和解决冲突

有时,团队成员在同一时间处理相同的文件,从而导致冲突。 让我们看看在这种情况下如何合并更改和解决冲突:

  1. 创建一个名为HelloWorld.txt的文本文件,并将其添加到本地存储库中。 将文件推到服务器,并在服务器上和本地存储库中更新该文件。
  2. 如果你试图推送已经在本地和远程存储库中修改过的HelloWorld.txt文件,你会得到一条错误消息,并且推送失败:

<assets/84936bdc-9be3-4dc6-8242-bf0af34ef703.png>

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

<assets/88394857-54d9-411d-9278-563538de84c9.png>

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

<assets/1ce138d3-5882-4882-8a86-8e2208a38c73.png>

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

<assets/03c2f6b3-7a41-4f47-92eb-784b526d084b.png>

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

<assets/29482ebd-1f79-458c-804c-885482682477.png>

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

<assets/1b06fc51-5128-4e40-9e87-52466bf04f7c.png>

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

<assets/a4c049a1-5daa-46c8-8407-e93fffae218f.png>

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

<assets/3a292800-b792-4d92-aa3c-7b189b84b148.png>

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

<assets/e14d54a0-1bc7-4bcc-a5d0-350b65fbc157.png>

创建 VSTS 构建管道

在计划和组织好你的工作并创建了 Git 仓库之后,你现在应该配置一个 VSTS 构建管道,这将允许你对你的应用进行持续集成:

  1. 打开 Visual Studio 2017,进入 Team Explorer 选项卡,然后单击 build 按钮:

<assets/8ff5ba94-7337-4e1d-8b3d-a0747465c03b.png>

  1. 接下来,点击 New Build Definition 链接:

<assets/d123e3de-f790-4d25-9751-eb88f586ccd9.png>

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

<assets/2358629e-1535-4611-9b81-913112a3a768.png>

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

<assets/51b4a9f9-b3b2-4556-8383-8dfc45b80e42.png>

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

<assets/88688c19-9915-46c2-9768-8be1068c7078.png>

  1. 要启用持续集成,请单击“构建定义”菜单中的“触发器”,然后单击“启用此触发器”按钮:

<assets/b95e4d89-4d56-44a9-a778-3dc342f54143.png>

  1. 在确认已经选择了 Git 存储库和主分支后,正确地单击 Save 或 Save & queue 按钮。 配置已经完成,每次代码提交到存储库时都会自动触发构建:

<assets/8f5ad5c5-4141-4945-ba6e-aae5a0cbbf47.png>

创建 VSTS 发布管道

您的应用不断地被集成,并且您已经看到了一些巨大的好处,例如可以更快地检测和修复错误和问题。 我们不要止步于此; 进一步改进开发过程比你想象的要容易得多!

现在我们来看看如何通过创建一个 VSTS 发布管道来采用应用的持续部署:

  1. 打开 VSTS 网站,点击上方菜单中的 Build & Release,点击 Releases,然后点击 New definition 按钮,选择 Empty definition 模板:

<assets/f14809de-0599-4052-9e99-27325480a283.png>

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

<assets/747fbc0a-d39c-45f5-b803-02d0d53fd8d4.png>

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

<assets/0bc5974d-3eaf-42b0-bdea-64a7bf1cbefe.png>

所示的示例版本定义目前并没有做太多工作。 稍后我们将在相应的 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 及其相关技术。

在本章中,我们将涵盖以下主题:

  • StartupProgram
  • 创建页面和服务
  • 使用 Bower 和布局页面
  • 使用依赖注入
  • 使用内置中间件
  • 创建自己的中间件
  • 使用静态文件
  • 使用路由、URL 重定向和 URL 重写
  • 错误处理和模型验证

创建井字游戏

让我们做些有趣的事吧! 让我们来构建一字棋游戏,也被称为 noughts and cross 或 Xs and Os。 玩家将选择谁接受 x,谁接受 o。 然后,他们将轮流在 3×3 网格中标记空间,每回合一个标记。 在水平、垂直或对角线上成功放置三个标记的玩家获胜。

玩家必须输入他们的电子邮件和名字注册创建一个帐户才能开始游戏。 每一场比赛后,他们都会收到一个比赛分数,这个分数会被加到他们的总分中。

排行榜提供有关玩家排名和最高分数的信息。

为了创建游戏,玩家必须向其他玩家发送邀请,然后一个特定的等待页面会显示给他,直到其他玩家做出回应。 其他玩家在收到邀请邮件后,可以确认请求并加入游戏。 当两名玩家在线时,游戏开始。

如上一章所述,我们可以使用 VSTS 及其工作项来组织和安排Tic-Tac-Toe应用的实现。 为此,我们必须创建史诗、特性和产品待办事项列表项,然后进行 sprint 计划,确定优先级,并决定首先执行什么。

正如你在下面的截图中看到的,我们已经决定在第一个 sprint 中处理 5 个产品待办事项列表,并将它们添加到 sprint 待办事项列表中:

<assets/4225fa74-6f78-4c3c-902f-9a6a8ff734ed.png>

你还记得在实现任何新特性之前,接下来需要做什么吗? 你不记得了? 也许特征分支听起来很耳熟?

在上一章中,我们展示了创建开发的最佳实践,它们是独立的,易于维护和发布。 它们包括在 Git 存储库中为每个要添加到应用的新特性创建一个特性分支。

因此,每个开发人员都可以在自己的特定特性分支中处理自己的特定特性,直到他决定可以发布它为止。

最后,所有准备发布的特性都合并到开发(或发布或主)分支中。 然后完成集成测试,如果一切正常,就会交付一个新的应用版本。

我们选择首先处理的特性是用户注册,所以我们要做的第一件事就是创建一个名为 FEA-UserRegistration 的特性分支。 如果你不知道怎么做,你可以转到第 3 章在 VSTS 中创建一个持续集成管道,并得到一个完整的步骤步骤和详细的解释:

<assets/01acca98-f539-4804-9dfc-af11e6209ff3.png>

构思并实现你的第一个井字游戏功能

在实现用户注册特性之前,我们必须理解它并决定一切应该如何工作。 我们必须定义用户描述和工作流。 为此,我们需要更详细地分析前面提到的Tic-Tac-Toe游戏描述。

如前所述,用户只有拥有用户帐号才能创建并加入游戏。 要创建这个帐户,他必须输入他的名字,他的姓氏,他的电子邮件地址,和一个新的密码。 然后系统验证输入的电子邮件地址是否已经注册。 一个指定的电子邮件地址只能注册一次。 如果电子邮件地址是新的,则生成用户帐户,如果电子邮件地址是已知的,则必须显示一个错误。

让我们来看看用户注册过程和实现它所需要的不同组件:

  1. 主页上有一个用户注册的链接,新用户必须点击注册来创建自己的玩家账户。 单击用户注册链接将用户重定向到专用的注册页面。
  2. 注册页面包含一个注册表单,用户必须在其中输入个人信息,然后进行确认。
  3. JavaScript 客户机验证表单,提交数据并将其发送给通信中间件,然后等待结果。
  4. 通信中间件接收请求并将其路由到注册服务。
  5. 注册服务接收请求,验证数据完整性,检查电子邮件是否已经用于注册,然后注册用户或返回错误消息。
  6. 通信中间件接收结果并将其路由到等待的 JavaScript 客户机。
  7. JavaScript 客户端重定向用户,以便如果结果是成功,他可以开始玩游戏,如果结果是失败,它将显示一条错误消息。

下面的序列图显示了用户注册过程。 它是更容易和更快的理解与更直观的表现:

<assets/38148018-a086-48f7-9427-9ae7ec28b56d.png>

首先,我们需要创建一个新的空 ASP.NET Core 2.0 Web Application,它将在本章和本书的其余部分用于添加各种组件和包。 然后,我们将逐步添加新的概念和功能,这将让您真正了解发生了什么,以及一切如何工作:

  1. 启动 Visual Studio 2017,点击文件|新建|项目。
  2. 在。net Core 部分选择 ASP.NET Core Web Application,输入应用名称、存储库的位置和解决方案名称,然后单击 OK:

<assets/e8b4c242-00bb-40f1-b808-8ab995dceeb2.png>

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.

  1. 选择空模板:

<assets/ee4512db-d10a-4d50-a887-d0b42a849951.png>

  1. 一个新的空 ASP.NET Core 2.0 Web Application 项目将生成,只包含Program.csStartup.cs文件:

<assets/dfec03ca-775d-434f-87e5-727eb76d6af0.png>

太好了,我们已经创建了我们的项目,现在准备实现我们的第一个功能! 但在此之前,让我们花些时间看看 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)中已经创建了多个文件夹和文件:

<assets/881d36aa-b640-4afe-9761-dc5f1778f831.png>

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

<assets/a1021a15-30c2-467c-8217-df7e46aec289.png>

使用 Microsoft.AspNetCore.All 元包

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

<assets/182999cc-5eb0-42e4-98d9-6efcbea44c51.png>

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); 

默认情况下,此设置是不启用的,这意味着在发生错误时,主机将退出。 这可能不是我们想要的行为,我们建议相应地修改这个参数。

另外两个有用的参数是PreferHostingUrlsUseUrls。 您可以指示主机是否应该侦听由您提供的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文件,用于定义所有视图的公共布局。

让我们创建项目结构:

  1. 打开 Visual Studio 2017,打开井字游戏 您创建的 NET Core 2.0 项目,创建三个新文件夹ControllersServicesViews,并在Views文件夹中创建一个子文件夹Shared:

<assets/24c20229-df17-4a0f-9960-55f4fd591ade.png>

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

<assets/6955f5ee-07bd-4972-acc5-b801e2165f9b.png>

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 应用:

  1. 如前所述,更新Program.csStartup.cs文件。
  2. 添加一个新控制器,在Controllers文件夹的解决方案资源管理器中右键单击,然后选择 Add |控制器:

<assets/1c8cc3cc-2754-4424-b85c-db64de749543.png>

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

<assets/5540e85e-01e7-42c1-a4ed-cb51f52c8228.png>

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

<assets/b2ff3c95-25de-4bfb-9c63-73f56c076cf6.png>

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

<assets/ef933527-6c1d-47d1-a917-9e1617da2347.png>

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

<assets/53888773-709b-4ddf-b25b-f472906e09ba.png>

通过使用 Bower 和布局页面,使您的网页具有更现代的外观

在上一节中,您了解了如何创建一个基本的 web 页面。 知道如何在技术上做到这一点是一回事,但创建成功的 web 应用不仅在于技术实现,还在于如何使您的应用具有视觉吸引力和用户友好性。 虽然这本书不是关于网页设计和用户体验的,但我们想给你一些快速和简单的方法来构建更好的 web 应用。

为此,我们建议使用 Bower(https://bower.io),即 Web 中自称的包管理器,与 ASP 结合使用.NET Core 布局页面。

在过去的几年中,Bower 在 web 开发社区中取得了一些显著的成功。 它有助于安装带有静态内容(如 HTML、CSS、JavaScript、字体和图像)的客户端包,包括它们的依赖项。

Visual Studio 2017 中对 Bower 有一些很棒的集成和支持; 您只需要正确地配置它,以有效地使用它。 让我们看看怎么做:

  1. 右键单击“一字棋”项目,选择“添加|新项目”,在搜索框中输入Bower,选择“凉亭配置文件”,单击“添加”:

<assets/80f14add-0258-4636-856a-165a5b08a2ce.png>

  1. 添加 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" 
          } 
        }
  1. 添加 Bower 配置文件应该已经添加了一个.bowerrc文件。 更新这个文件并定义资产应该放置的目录:
        { 
          "directory": "wwwroot/lib" 
        } 
  1. 右键单击bower.json文件,点击恢复包:

<assets/8dc910f3-50f7-488d-abd5-ee5560b2d735.png>

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

<assets/61faa6e0-6d75-44e4-925f-8f02b4a3d8c1.png>

  1. 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>&copy; 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> 

当启动应用时,你会看到新的主页设计:

<assets/1f59f053-bf68-41b7-9d1f-e850ca229e3c.png>

创建用户注册一字棋界面

现在将第二个组件,即用户注册页面与其表单集成在一起,它将允许新用户注册来玩Tic-Tac-Toe游戏。

  1. 向项目中添加一个名为Models的新文件夹。
  2. 通过右键单击项目中的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; } 
        } 
  1. 添加一个新控制器并将其命名为UserRegistrationController(如果您不知道如何做到这一点,请参考创建井字游戏主页节)。
  2. 右键单击名为Index的方法并选择 Add View。 这一次,选择创建模板,像前面提到的模型UserModel,并启用布局页面的使用:

<assets/d0453c47-9732-4b71-983b-f509858b1fae.png>

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.

  1. 从视图中移除自动生成的IdIsEmailConfirmedEmailConfirmationDateScore元素; 对于用户注册表单,我们不需要它们。
  2. 视图现在已经准备好了; 点击F5,点击主页注册链接显示:

<assets/4e90953d-abd7-4c9f-8bad-cae1446b3df7.png>

使用依赖注入来鼓励应用中的松散耦合

开发应用时最大的问题之一是组件间的依赖关系。 这些依赖关系使单独维护和发展组件变得困难,因为修改可能会严重影响其他依赖组件。 但请放心,有一些机制允许分解这些依赖项,其中之一是依赖项注入(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 机制:

  1. Services文件夹中添加一个名为UserService.cs的新类。
  2. 为用户注册添加一个新方法,使用上一节中创建的模型作为参数:
        public class UserService 
        { 
          public Task<bool>RegisterUser(UserModel userModel) 
          { 
            return Task.FromResult(true); 
          } 
        }
  1. 右键单击类,选择 Quick Actions 和 Refactorings,然后单击 Extract Interface:

<assets/9f19bcc2-59b4-4c96-86c0-fe33824e020f.png>

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

<assets/eb7556eb-8938-4b15-bc4f-aa526b292b36.png>

  1. Visual Studio 2017 将生成一个名为IUserService.cs的新文件,其中包含提取的接口定义,如下所示:
        public interface IUserService 
        { 
          Task<bool>RegisterUser(UserModeluserModel); 
        } 
  1. 更新之前创建的UserRegistrationController并应用构造函数注入机制:
        public class UserRegistrationController : Controller 
        { 
          private IUserService _userService; 
          public UserRegistrationController(IUserService userService) 
          { 
            _userService = userService; 
          } 

          public IActionResult Index() 
          { 
            return View(); 
          } 
        } 
  1. 添加一些简单的代码来处理UserRegistrationController中的用户注册(我们将在本章后面添加验证):
        [HttpPost] 
        public async Task<IActionResult> Index(UserModel userModel) 
        { 
          await _userService.RegisterUser(userModel); 
          return Content($"User {userModel.FirstName} 
           {userModel.LastName} has been registered sucessfully"); 
        } 
  1. Startup类,在ConfigureServices方法中声明UserService,使其可用于应用:
        public void ConfigureServices(IServiceCollection services) 
        {             
          services.AddMvc(); 
          services.AddSingleton<IUserService, UserService>(); 
        } 
  1. F5测试您的应用,填写注册页面,然后点击 OK。 你应该得到以下输出:

<assets/401d9132-b53e-4a17-8686-5d06154bace5.png>

非常好,您已经创建了多个组件的Tic-Tac-Toe应用,进展非常好! 请保持敏锐,因为下一节非常重要,因为它详细解释了中间件。

使用中间件)

如前所述,Startup类负责在 ASP 中添加和配置中间件.NET Core 2.0 应用。 但什么是中间件? 何时以及如何使用它,以及如何创建自己的中间件? 这些都是我们现在要讨论的问题。

本质上,多个中间件组成了 ASP 的功能.NET Core 2.0 应用。 您现在可能已经注意到,即使是最基本的功能,比如提供静态内容,也由它们来执行。

中间件是 ASP 的一部分.NET Core 2.0 请求管道用于处理请求和响应。 当它们链接在一起时,它们可以将传入的请求从一个传递给另一个,并在管道中调用下一个中间件之前和之后执行操作:

<assets/84afda0d-37f1-44f4-b9ee-4f66ef5f00b9.png>

使用中间件可以使您的应用更加灵活和完善,因为您可以在Startup类的Configure方法中轻松地添加和删除中间件。

此外,在Configure方法中调用中间件的顺序就是调用它们的顺序。 为了保证更好的性能、功能和安全性,建议按照以下顺序调用中间件:

  1. 异常处理中间件)。
  2. 静态文件中间件)。
  3. 验证仿真中间件。
  4. MVC middlewares.

如果您不按此顺序调用它们,您可能会得到一些意想不到的行为,甚至错误,因为中间件操作可能在请求管道中应用得太晚或太早。

例如,如果您不首先调用异常处理中间件,那么您可能无法捕获在调用它之前发生的所有异常。 另一个例子是在静态文件中间件之后调用响应压缩中间件。 在这种情况下,您的静态文件将不会被压缩,这可能不是期望的行为。 所以,要注意中间件调用的顺序; 它可以带来巨大的不同。

下面是一些你可以在你的应用中使用的内置中间件(列表不是详尽的; 还有更多):

| 身份验证 | OAuth 2 和 OpenID 认证,基于最新版本的 IdentityModel |
| CORS | 基于 HTTP 报头的跨源资源共享保护 |
| 响应缓存 | HTTP 响应缓存 |
| 响应压缩 | HTTP 响应 gzip 压缩 |
| 路由 | HTTP 请求路由框架 |
| 会话 | 基本的本地和分布式会话对象管理 |
| 静态文件 | HTML, CSS, JavaScript,和图像支持包括目录浏览 |
| URL 重写 | URL SEO 优化和重写 |

内置的中间件将足以满足最基本的需求和标准用例,但您肯定需要创建自己的中间件。 有两种方法:在 Startup 类中内联创建它们,或者在自包含类中创建它们。

首先让我们看看如何定义内联中间件; 以下是可采用的方法:

  • Run
  • Map
  • MapWhen
  • Use

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应用创建一个基本的通信中间件:

  1. 在项目中创建一个名为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); 
          } 
        }
  1. 在项目中创建一个名为Extensions的新文件夹,然后添加一个名为CommunicationMiddlewareExtension.cs的新类,代码如下:
        public static class CommunicationMiddlewareExtension 
        { 
          public static IApplicationBuilder
           UseCommunicationMiddleware(this IApplicationBuilder app) 
          { 
            return app.UseMiddleware<CommunicationMiddleware>(); 
          } 
        } 
  1. 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?}"); 
          }); 
        } 
  1. 在通信中间件实现中设置一些断点,并按F5启动应用。 你会看到,如果一切正常工作,断点将被命中:

<assets/b2e8ad24-d0e6-4614-96af-0e85112862d0.png>

这只是一个如何创建自己的中间件的基本示例; 在这个部分和其他部分之间没有可见的功能更改。 在下一章中,您将进一步实现完成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.

此外,出于安全原因,目录浏览在默认情况下是禁用的。 但是,如果你需要让用户看到文件夹和文件,你可以很容易地激活它:

  1. 在调用AddMvc()方法之后,在Startup类的ConfigureService方法中添加DirectoryBrowsingMiddleware:
        services.AddDirectoryBrowser(); 
  1. Startup类的Configure方法中,调用UseDirectoryBrowser方法(在调用UseCommunicationMiddleware方法之后)来激活目录浏览:
        app.UseDirectoryBrowser(); 

<assets/7714593a-9b8b-4581-ae08-1d181905b020.png>

  1. 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}");
    }

如果你用一些查询字符串参数调用它,你会得到以下结果:

<assets/0ae9b372-a206-46f4-a2b3-56d093e1f84c.png>

另一个重要的中间件是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开始被服务:

<assets/e59fe8ac-a422-4f06-8f39-22d9047cef28.png>

向应用添加错误处理

在开发应用时,问题不是错误和 bug 是否会发生,而是它们什么时候会发生。 构建应用是一项非常复杂的任务,几乎不可能考虑运行时可能发生的所有情况。 即使您认为您已经考虑了所有的事情,但是环境的行为并不如预期的那样,例如,服务不可用或者处理请求花费的时间比预期的要多。

对于这个问题,您有两个解决方案,它们需要同时应用于单元测试和错误处理。 从应用的角度来看,单元测试将确保开发期间的正确行为,而错误处理帮助您在运行时为环境问题做好准备。 我们将看看如何为你的 ASP 添加有效的错误处理.NET Core 2.0 应用。

默认情况下,如果根本没有错误处理,并且如果异常发生,您的应用将停止,用户将不能再使用它,在最坏的情况下,将会出现服务中断。

在开发期间要做的第一件事是激活默认的开发异常页面; 它显示发生的异常的详细信息。 你已经在本章的开头看到了如何做到这一点:

    if (env.IsDevelopment()) 
    { 
      app.UseDeveloperExceptionPage(); 
    } 

在默认的开发异常页面上,您可以深入研究原始异常细节,以分析堆栈跟踪。 您有多个选项卡,允许查看查询字符串参数、客户端 cookie 和请求头。

这些是一些有力的指标,有助于更好地理解发生了什么以及为什么会发生。 它们可以帮助你在开发期间更快速地发现问题并解决问题。

下面是一个发生异常的例子:

<assets/dfb67e01-c92c-4948-92cf-e4bbb57a1cdb.png>

但是,不建议在生产环境中使用默认的开发异常页,因为它包含了太多关于系统的信息,可能会被用来危害系统。

对于生产环境,建议配置一个带有静态内容的专用错误页面。 在下面的示例中,您可以看到在开发期间使用了默认的开发异常页面,如果应用被配置为在非开发环境中运行,则会显示一个特定的错误页面:

    if (env.IsDevelopment()) 
    { 
      app.UseDeveloperExceptionPage(); 
      app.UseBrowserLink(); 
    } 
    else 
    { 
      app.UseExceptionHandler("/Home/Error"); 
    } 

缺省情况下,400599之间的 HTTP 错误码不显示信息。 例如,这包括404(未找到)和500(内部服务器错误)。 用户只会看到一个空白页面,这不是很友好。

您应该激活Startup类中的特定UseStatusCodePages中间件。 它将帮助您定制在本例中需要显示的内容。 有意义的信息将帮助用户更好地理解应用中发生的事情,从而提高客户满意度。

最基本的配置可能只是显示一条文本消息:

    app.UseStatusCodePages("text/plain", "HTTP Error - Status Code: {0}"); 

<assets/1753cd19-5e9f-4352-9e0f-4086557b006d.png>

但是,你可以更进一步。 例如,您可以针对特定的 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实现,我们可以看到它有多个缺陷。 如果字段没有正确填写或者根本没有填写,该怎么办? 如果模型定义没有得到尊重怎么办? 目前,我们不要求任何东西,也不验证任何东西。

让我们来解决这个问题,看看如何构建一个更健壮的应用:

  1. 更新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; } 
        } 
  1. 更新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); 
        }  
  1. 如果你没有填写必要的字段,或者你给出了一个错误的电子邮件地址,然后点击确定,你现在会得到一个相应的错误消息:

<assets/93110761-9ba6-43e3-bb65-b21a42973e0a.png>

总结

在本章中,你已经学习了 ASP 的一些基本概念.NET 2.0。 有很多东西要理解,有很多东西要看,我们希望你在自己尝试这些东西时获得了一些乐趣。 你肯定取得了巨大的进步!

一开始,您创建了Tic-Tac-Toe项目; 然后,您开始实现它的不同组件。 我们学习了ProgramStartup类,了解了如何使用 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),但是我们刚刚开始,所以它们还没有完成。

<assets/548a3081-c3ae-4130-8cd6-40564f7820d8.png>

当与Tic-Tac-Toe应用的初始工作流相比较时,我们可以看到仍有许多东西缺失,比如整个客户端部分,真正与通信中间件一起工作,以及我们仍需要实现的多个其他特性。

让我们从客户端部分开始,看看如何应用更高级的技术。 然后,您将学习如何尽可能地优化一切。

如果您还记得,上次我们是在用户向注册表单提交数据之后停止的,注册表单被发送到UserService。 然后,我们只显示了一条纯文本消息,如下所示:

<assets/f4a79ea8-389b-4338-937d-2e1803adf8ed.png>

但是,这里的处理还没有结束。 我们需要添加整个电子邮件确认过程使用客户端开发和 JavaScript,这是我们接下来要做的:

  1. 打开 Visual Studio 2017 并打开井字游戏项目。 在UserRegistrationController中添加一个名为EmailConfirmation的新方法:
        [HttpGet] 
        public IActionResult EmailConfirmation (string email) 
        { 
          ViewBag.Email = email; 
          return View(); 
        } 
  1. 右键单击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. 
  1. 转到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); 
          } 
        } 
  1. F5启动应用,注册一个新用户,并验证新的 EmailConfirmation 页面是否正确显示:

<assets/87327110-1320-4ae2-8973-b34f4e8635fc.png>

非常好,您已经实现了完成用户注册过程所需的第一组修改。 在接下来的部分中,我们需要检查用户是否确认了他的电子邮件地址。 让我们看看接下来怎么做:

  1. IUser接口中添加两个新方法GetUserByEmailUpdateUser。 这些将用于处理电子邮件确认更新:
        public interface IUserService 
        { 
          Task<bool> RegisterUser(UserModel userModel); 
          Task<UserModel> GetUserByEmail(string email); 
          Task UpdateUser(UserModel user); 
        } 
  1. 实现新方法,使用静态的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; 
          } 
        } 
  1. 添加一个名为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; } 
        } 
  1. 添加一个名为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); 
          } 
        } 
  1. 通过右键单击Index方法生成相应的视图,同时选择 Create 模板并使用前面的GameInvitationModel作为 Model 类:

<assets/299cc6a7-752d-4255-aca1-e7876d9bc4c6.png>

  1. 修改自动生成的视图,删除所有不必要的输入控件,只留下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> 
  1. 现在,更新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(); 
      }
  1. F5启动应用,注册新用户,验证 Email 确认页面是否和之前一样。 在 Microsoft Edge 中,按F5重新加载页面,如果一切正常,你现在应该被重定向到游戏邀请页面:

<assets/4ef2d8a6-e834-40f4-ac88-cac1ad407b3e.png>

太好了,又有进步了! 在游戏邀请之前,一切都很顺利,但不幸的是,仍然需要用户干预。 用户需要按F5手动刷新 Email 确认页面,直到 Email 确认完成; 只有这样,他才会被重定向到游戏邀请页面。

整个刷新过程必须在下一步中自动化并优化。 你的选择是:

  • 在页面的标题部分放置一个 HTML 元刷新标记
  • 使用简单的 JavaScript,它以编程方式进行刷新
  • 使用 jQuery 实现XMLHttpRequest(XHR

HTML3 引入了 meta refresh 标签,可以在一定时间后自动刷新页面。 然而,这种方法是不可取的,因为它创建了一个高服务器负载,并且在 Microsoft Edge 的安全设置可能完全禁用它,一些广告拦截器将停止它的工作。 所以,如果你使用它,你不能确定它是否能正常工作。

使用简单的 JavaScript 可以很好地以编程的方式自动化页面刷新,但它主要有相同的缺陷,因此不推荐使用。

XHR 是我们真正需要的,因为它提供了我们的井字策略应用所需要的东西。 它允许:

  • 更新网页而不重新加载他们
  • 即使在页面加载之后,也从服务器请求和接收数据
  • 发送数据到后台的服务器

<assets/47774150-cddd-4ef3-a954-054d97154f31.png>

现在,您将使用 XHR 来自动化和优化用户注册电子邮件确认处理的客户端实现。 这样做的步骤如下:

  1. wwwroot文件夹中创建名为app的新文件夹(此文件夹将包含以下步骤中的所有客户端代码),并在此文件夹中创建名为js的子文件夹。
  2. wwwroot/app/js文件夹中添加一个新的 JavaScript 文件scripts1.js,其内容如下:
        var interval; 
        function EmailConfirmation(email) { 
          interval = setInterval(() => { 
            CheckEmailConfirmationStatus(email); 
          }, 5000); 
        } 
  1. 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"); 
              } 
          }); 
        } 
  1. Views\Shared\_Layout.cshtml文件中打开布局页面,并在关闭body标签之前添加一个新的Development环境元素(最好将其放在那里):
        <environment include="Development"> 
          <script src="~/app/js/scripts1.js"></script> 
          <script src="~/app/js/scripts2.js"></script> 
        </environment> 
  1. 更新通信中间件中的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(); 

          } 
        } 
  1. 通过在页面底部添加对 JavaScriptEmailConfirmation函数的调用来更新EmailConfirmation视图:
        @section Scripts 
        { 
          <script> 
             $(document).ready(function () { 
               EmailConfirmation('@ViewBag.Email'); 
             }); 
          </script> 
        } 
  1. 更新UserRegistrationController中的EmailConfirmation方法。 由于通信中间件现在要模拟有效的电子邮件确认,删除以下行:
        user.IsEmailConfirmed = true; 
        user.EmailConfirmationDate = DateTime.Now; 
        await _userService.UpdateUser(user); 
  1. F5启动应用并注册一个新用户。 你会看到一个 JavaScript 警告框返回WaitingForEmailConfirmation,一段时间后,另一个提示 OK:

<assets/9043d3c7-3e4a-4351-8508-2b57e9ef2464.png>

  1. 现在,您必须更新scripts2.js文件中的CheckEmailConfirmationStatus方法,以便在收到确认的电子邮件时重定向。 为此,删除alert("OK");指令,并在其位置添加以下指令:
        window.location.href = "/GameInvitation?email=" + email;
  1. F5启动应用并注册一个新用户。 一切都应该自动化,你应该自动重定向到游戏邀请页面的最后:

<assets/0d2319f8-c3ad-4c0b-ab65-14eef942b8de.png>

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 布局页面使用它们:

<assets/6a02c739-e033-48f7-be36-3d8dbcfeb9a5.png>

在当今现代 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 文件:

  1. 在顶部菜单中选择“工具|扩展和更新”,单击“在线”,在搜索框中输入Bundler & Minifier,选择 Bundler&Minifier,最后单击“下载”:

<assets/68df6517-ec6c-4b5f-9e87-82e29e2840d4.png>

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

<assets/2d34abe4-e86e-4907-9c10-6128137054ba.png>

  1. 重新启动 Visual Studio。 您现在要通过捆绑和缩小来优化打开连接的数量和带宽使用。 为此,向项目中添加一个名为bundleconfig.json的新 JSON 文件。
  2. 更新bundleconfig.json文件,将两个 JavaScript 文件捆绑为一个名为site.js的文件,并缩小site.csssite.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 
          }   
        ] 
  1. 右键单击项目,选择 Bundler & Minifier |更新包:

<assets/c6e0f0a5-e91e-4bef-8257-80db83df9be9.png>

  1. 在解决方案资源管理器中,您可以看到已经生成了两个名为site.min.csssite.min.js的新文件。
  2. 在任务运行器资源管理器中,你可以看到你为项目配置的捆绑和缩小过程:

<assets/b8066953-34e4-4b74-b9ed-76a93b555133.png>

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

<assets/9e3349ba-1f82-440b-9c5c-7dafe432a1d0.png>

  1. 通过右键单击 Update 所有文件并选择 Bindings | after build 来安排每次构建之后执行的过程。 将生成一个名为bundleconfig.json.bindings的新文件,如果您删除wwwroot/js文件夹并重新构建项目,那么这些文件将自动生成。

  2. 要查看新生成的文件的运行情况,转到项目设置中的 Debug 选项卡,将ASPNETCORE_ENVIRONMENT变量设置为Staging,然后保存:

<assets/5a216721-db45-4b8f-89fd-628f915806cb.png>

  1. F5启动应用,在 Microsoft Edge 中按F12打开开发人员工具,重新注册过程。 您将看到只有捆绑的和缩小的site.min.csssite.min.js文件被加载,并且加载时间更快:

<assets/f4bf4eb0-0a23-4036-9c3a-8f5fce5c7933.png>

好了,现在我们知道了如何实现客户端,以及如何在现代 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应用的客户端实现:

  1. 转到Configure方法中的一字棋Startup类,在通信中间件和 MVC 中间件之前添加 WebSockets 中间件(记住中间件调用顺序对确保正确的行为很重要):
        app.UseWebSockets(); 
        app.UseCommunicationMiddleware(); 
        ... 
  1. 更新通信中间件,为 WebSockets 通信添加两个新方法SendStringAsyncReceiveStringAsync:
       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(); 
           } 
         } 
       } 
  1. 更新通讯中间件,添加一个名为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); 
          } 
        } 
  1. 更新通信中间件中的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); 
          } 
        }  
  1. 修改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); 
          } 
        } 
  1. 修改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) { 
         }; 
       };
  1. 当您启动应用并继续进行用户注册时,您将获得是否支持 WebSockets 的信息。 如果是,你会被重定向到游戏邀请页面,像以前一样,但好处是更快的处理时间:

<assets/a783615c-37c6-4363-8177-59de822b9356.png>

这就是我们在 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应用中激活内存会话提供程序来存储用户界面文化和语言:

  1. 打开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>
  1. 打开HomeController并添加一个名为SetCulture的新方法。 这将包含在一个会话变量中存储用户区域性设置的代码:
        public IActionResult SetCulture(string culture) 
        { 
          Request.HttpContext.Session.SetString("culture", culture); 
          return RedirectToAction("Index"); 
        } 
  1. 更新从区域性会话变量中检索区域性的HomeController方法的Index:
        public IActionResult Index() 
        { 
          var culture =
            Request.HttpContext.Session.GetString("culture"); 
          ViewBag.Language = culture; 
          return View(); 
        } 
  1. 进入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; 
        } 
  1. 添加 ASP 的内置会话中间件.NET Core 2.0 中的ConfigureServices方法Startup类:
        services.AddSession(o => 
        { 
          o.IdleTimeout = TimeSpan.FromMinutes(30); 
        });
  1. Startup类的Configure方法中激活会话中间件,将其添加到静态文件中间件之后:
        app.UseStaticFiles(); 
        app.UseSession(); 
  1. 更新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); 
        }
  1. F5 启动应用。 你应该看到新的用户界面语言下拉菜单,有英语和法语之间的选择:

<assets/47e0c2c9-b642-480c-ad29-91c5be2b9072.png>

很好,您已经了解了如何激活和使用会话状态。 然而,大多数时候你会有多个 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:

  1. 转到Services文件夹并添加一个名为CultureProviderResolverService的新服务。 这将通过查看Culture查询字符串、Culturecookie 和Culture会话变量(在本章前一节中创建)来检索区域性设置。
  2. 通过从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); 
          } 
        } 
  1. Startup类的ConfigureServices方法的顶部添加本地化服务:
        public void ConfigureServices(IServiceCollection services) 
        { 
          services.AddLocalization(options => options.ResourcesPath = 
            "Localization"); 
          ... 
        } 
  1. 将本地化中间件添加到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)

  1. 在解决方案资源管理器中,添加一个名为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.

<assets/0cd48d89-447f-4114-9ff1-9f0953ddd635.png>

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.

  1. 打开GameInvitationController.resx资源文件,添加一个新的GameInvitationConfirmationMessage英文:

<assets/918567fc-d6b4-4d0c-8d0a-8361835c4a3b.png>

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

<assets/7a1f9799-fdff-46f0-9990-88e8a32a6756.png>

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

<assets/01748327-1d73-456f-9861-dcb99b7fd552.png>

  1. 使用用户界面语言下拉菜单将应用语言更改为法语,然后注册一个新用户,直到你收到以下文本消息,现在应该是法语:

<assets/9494c15e-83ed-48a6-adbc-c4c281485296.png>

就这样,您已经了解了如何在应用中本地化任何类型的字符串,这对于某些特定的应用用例可能很有用。 但是,在处理视图时,这不是推荐的方法。

ASP.NET Core 2.0 框架为本地化视图提供了一些强大的特性。 在下一个例子中,你将使用视图本地化方法:

  1. 更新Startup类中的ConfigureServices方法,并将视图本地化服务添加到 MVC 服务声明中:
        services.AddMvc().AddViewLocalization(
          LanguageViewLocationExpanderFormat.Suffix,
          options => options.ResourcesPath = "Localization"); 
  1. 修改Views/ViewImports.cshtml文件,添加 View Localizer 功能,使其适用于所有视图:
        @using Microsoft.AspNetCore.Mvc.Localization 
        @inject IViewLocalizer Localizer 
  1. 打开主页视图并添加一个新标题,它将进一步本地化,如下所示:
        <h2>@Localizer["Title"]</h2> 
  1. 在解决方案资源管理器中,转到Localization文件夹并创建一个名为Views的子文件夹,然后在该文件夹中添加两个名为Home.Index.resxHome.Index.fr.resx的新资源文件:

<assets/d01eb6ce-79c0-445d-b050-f81cd13a370c.png>

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

<assets/8ef495ea-0d7f-4f07-97dd-e9681b4dad77.png>

  1. 打开Home.Index.fr.resx文件,为法语标题添加一个条目:

<assets/fa131026-d083-4974-911f-d7f0ed9c7513.png>

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

<assets/6882dee9-3c65-4eb1-b535-8b066e5cc8a5.png>

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

<assets/7a066dfe-2e1e-4b99-a467-48640900e6c9.png>

您已经了解了如何轻松地本地化视图,但是如何本地化视图中使用数据注释的表单呢? 让我们更详细地看一下; 你会惊讶于 ASP.NET Core 2.0 框架必须提供在这种情况下!

我们将在下面的例子中完全本地化用户注册表单:

  1. 在解决方案资源管理器中,转到Localization/Views文件夹,添加两个新的资源文件UserRegistration.Index.resxUserRegistration.Index.fr.resx

  2. 打开UserRegistration.Index.resx文件,添加一个TitleSubTitle元素的英文翻译:

<assets/974b47fd-667f-446c-a86c-326d37460bb3.png>

  1. 打开UserRegistration.Index.fr.resx文件,添加一个TitleSubTitle元素,并使用法语翻译:

<assets/e7f402b5-43e8-46ac-9b3a-c2cc80574ced.png>

  1. 更新用户注册索引视图以使用视图定位器:
        @model TicTacToe.Models.UserModel 
        @{ 
           ViewData["Title"] = Localizer["Title"]; 
        } 
        <h2>@ViewData["Title"]</h2> 
        <h4>@Localizer["SubTitle"]</h4> 
        <hr /> 
        <div class="row"> 
        ... 
  1. 启动应用,通过“用户界面语言”下拉菜单设置语言为法语,然后进入“用户注册”界面。 标题应该用法语显示。 点击 Create 而不输入任何内容,看看会发生什么:

<assets/be63522d-85fe-4c7a-b5ff-07e534b9a649.png>

这里缺了点什么。 您已经为页面标题和用户注册页面的副标题添加了本地化,但是我们仍然缺少表单的一些本地化。 但我们遗漏了什么?

您肯定已经看到错误消息还没有本地化和翻译。 我们正在使用 Data Annotation 框架进行错误处理和表单验证,那么如何本地化 Data Annotation 验证错误消息呢? 这就是你现在将要看到的:

  1. Startup类的ConfigureServices方法中将数据注释本地化服务添加到 MVC 服务声明中:
        services.AddMvc().AddViewLocalization(
          LanguageViewLocationExpanderFormat.Suffix, options =>
          options.ResourcesPath = "Localization") 
          .AddDataAnnotationsLocalization();
  1. 转到Localization文件夹并创建名为Models的子文件夹,然后添加两个名为UserModel.resxUserModel.fr.resx的新资源文件。
  2. 用英文更新UserModel.resx文件:

<assets/b94e405f-2c91-42cc-933d-062c13c11c5d.png>

  1. 用法语翻译更新UserModel.fr.resx文件:

<assets/c4858a3d-010d-4c67-b1ec-779c21aade6d.png>

  1. 更新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; } 
        }
  1. 重新构建解决方案并启动应用。 你会看到整个用户注册页面,包括错误消息,现在完全翻译时,更改用户界面语言为法语:

<assets/e8d120bf-6c6c-4ffc-b84f-4752d09027e7.png>

您已经了解了如何使用数据注释本地化字符串、视图甚至错误消息。 为此,您使用了 ASP 的内置特性.NET Core 2.0,因为它们包含了开发多语言本地化 web 应用的一切。 下一节将介绍如何配置应用和服务。

配置您的应用和服务

在前面的部分中,通过向用户注册过程中添加缺失的组件,甚至本地化Tic-Tac-Toe应用的部分,您进行了进一步的改进。 但是,您总是通过在代码中以编程方式设置用户确认来模拟电子邮件确认。 在本节中,我们将修改此部分,以便真正向新注册用户发送电子邮件,并使所有内容完全可配置。

首先,你要添加一个新的电子邮件服务,它将被用来发送电子邮件给刚刚在网站上注册的用户:

  1. Services文件夹中,添加一个名为EmailService的新服务,并实现一个默认的SendEmail方法(我们稍后将更新它):
        public class EmailService 
        { 
          public Task SendEmail(string emailTo, string subject,
           string message) 
          { 
            return Task.CompletedTask; 
          } 
        } 
  1. 提取IEmailService接口:

<assets/ce5cef1d-c4f0-4c9e-91e9-8163190f7c09.png>

  1. 将新的 Email 服务添加到Startup类的ConfigureServices方法中(我们想要一个单一的应用实例,所以将其添加为 Singleton):
        services.AddSingleton<IEmailService, EmailService>(); 
  1. 更新UserRegistrationController以访问在上一步中创建的EmailService:
        readonly IUserService _userService; 
        readonly IEmailService _emailService; 
        public UserRegistrationController(IUserService userService,
         IEmailService emailService) 
        { 
          _userService = userService; 
          _emailService = emailService; 
        } 
  1. 更新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 配置文件:

  1. 向项目添加一个新的appsettings.json配置文件,并添加以下自定义部分。 这将用于配置电子邮件服务:
        "Email": { 
          "MailType": "SMTP", 
          "MailServer": "localhost", 
          "MailPort": 25, 
          "UseSSL": false, 
          "UserId": "", 
          "Password": "", 
          "RemoteServerAPI": "", 
          "RemoteServerKey": "" 
        } 
  1. 在解决方案资源管理器中,在项目的根目录下创建一个名为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; 
          } 
        }  
  1. 更新EmailService实现,添加EmailServiceOptions,并向类添加一个参数化构造函数:
        private EmailServiceOptions _emailServiceOptions; 
        public EmailService(IOptions<EmailServiceOptions> 
         emailServiceOptions) 
        { 
          _emailServiceOptions = emailServiceOptions.Value; 
        } 
  1. 添加一个新的构造函数到Startup类,以允许您配置您的电子邮件服务:
        public IConfiguration _configuration { get; } 
        public Startup(IConfiguration configuration) 
        { 
          _configuration = configuration; 
        } 
  1. 更新Startup类的ConfigureServices方法:
        services.Configure<EmailServiceOptions>
         (_configuration.GetSection("Email")); 
        services.AddSingleton<IEmailService, EmailService>(); 
  1. 更新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; 
        } 
  1. 将一个断点放入EmailService构造函数中,并按F5以 Debug 模式启动应用,验证是否已从配置文件中正确检索到电子邮件服务选项值。 如果你有一个 SMTP 服务器,你也可以验证邮件是否真的被发送:

<assets/c26e1a64-39b0-4999-9639-5d1efed96708.png>

您已经看到了如何通过使用 ASP 的内置配置 API 来配置您的应用和服务.NET Core 2.0,它允许您编写更少的代码,提高效率,同时最终提供一个更优雅、更可维护的解决方案。

使用日志

当你在开发你的应用时,你会使用一个著名的集成开发环境,比如 Visual Studio 2017 或 Visual Studio Code,如本书开篇章节所述。 你每天都这样做,你做的大多数事情都是反射性的,一段时间后你会自动地去做。

通过使用 Visual Studio 2017 的高级调试特性,您可以很自然地调试应用并理解运行时发生的事情。 查找变量值、查看以何种顺序调用哪些方法、了解注入了哪些实例以及捕获异常,是构建健壮并响应业务需求的应用的关键。

然后,在将应用部署到生产环境时,您突然错过了所有这些特性。 您很少会发现安装了 Visual Studio 的生产环境,但是,错误和意外行为会发生,您需要能够尽快地理解和修复它们。

这就是测井和遥测技术发挥作用的地方。 检测应用和日志记录时进入和离开的方法,以及重要的变量值或任何你认为重要的信息在运行时,你将能够去应用日志,看看发生在生产环境的问题。

在上一节中,我们添加了用于发送电子邮件的 Email Service,并使用外部配置文件对其进行了配置。 如果配置的 SMTP 服务器没有响应怎么办? 如果我们忘记将服务器设置从开发更新到生产呢? 现在,我们只会在浏览器中显示一个异常消息:

<assets/cbf71ee1-18c6-4801-99a1-225d98cde96a.png>

在本节中,我们将向您展示如何使用日志记录和异常处理来为这类问题提供更好、更工业化的解决方案。

ASP.NET Core 2.0 提供了内置的对以下目标日志的支持:

  • Azure AppServices 乳制品
  • 控制台
  • Windows 事件源
  • 跟踪
  • 调试器输出
  • 应用的见解

但是默认情况下不支持文件、数据库和日志服务。 如果要将日志发送到这些目标,则需要使用第三方日志记录器解决方案,如 log4net、Serilog、NLog、Apache、ELMAH 或 logger。

你也可以通过实现ILoggerProvider接口轻松创建你自己的提供商,这就是你将在这里看到的:

  1. 添加一个新的类库(.NET Core)项目到解决方案,并将其命名为TicTacToe.Logging(删除自动生成的Class1.cs文件):

<assets/2c52ca2b-5ac0-4cec-83c8-b3e9fc19cc0c.png>

  1. 通过 NuGet 包管理器添加 NuGet 包Microsoft.Extensions.LoggingMicrosoft.Extensions.Logging.Configuration:

<assets/f602c7c9-356d-4550-b78e-724b08bc4c9f.png>

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

<assets/dc501cd6-2f68-4b03-8acf-179e31e4a2f9.png>

  1. 添加一个名为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; } 
        } 
  1. 添加一个名为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(); 
            } 
          } 

        }
  1. 添加一个名为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)); 
          } 
        } 
  1. 添加一个名为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() 
          { 
          } 
        }
  1. 为了简化从 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; 
          } 
        } 
  1. 在 TicTacToe Web 项目中,在Options文件夹中添加两个新选项LoggingProviderOptionLoggingOptions:
        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; } 
        }
  1. 在 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; 
          } 
        }
  1. 转到 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;

  1. appsettings.json文件中添加一个名为Logging的新部分:
        "Logging": { 
          "Providers": [ 
            { 
              "Name": "Console", 
              "LogLevel": "1" 
            }, 
            { 
              "Name": "File", 
              "LogLevel": "2" 
            } 
          ], 
          "MinimumLevel": 1 
        }
  1. 启动应用,并验证是否在应用文件夹中名为logs的文件夹中创建了一个新的日志文件:

<assets/bcb94304-129f-4782-8045-4ac86dd32b0a.png>

这是第一步,简单而快速地完成。 现在,您有了一个日志文件,可以将日志写入其中。 您将看到,使用集成的日志功能从 ASP 的任何地方创建日志都是同样容易的.NET Core 2.0 应用(ControllersServices等)。

让我们快速添加一些日志到井字游戏应用:

  1. 更新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; 
       } 
  1. 更新UserRegistrationController中的EmailConfirmation方法,并在方法开始处添加日志:
        _logger.LogInformation($"##Start## Email confirmation
         process for {email}");
  1. 更新 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; 
          } 
        }
  1. 打开生成的日志文件,分析其内容:

<assets/95bb6579-a3ea-4acd-af6e-8193c478dae3.png>

实现高级依赖注入概念

在前一章中,您看到了依赖注入(DI)的工作方式以及如何使用构造函数注入方法。 但是,如果您需要在运行时注入许多实例,那么这个方法可能会相当麻烦,并且会使代码的理解和维护变得复杂。

因此,您可以使用一种更先进的 DI 技术,称为方法注射。 这允许直接从代码中访问实例。

在下面的示例中,您将添加一个处理游戏邀请的新服务,并更新Tic-Tac-Toe应用,在使用方法注入的同时,可以发送电子邮件联系其他用户加入游戏:

  1. 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)); 
          } 
        }
  1. 提取IGameInvitationService接口:

<assets/c18c0690-fc0b-410d-ad4e-ad4df5edc8fe.png>

  1. Startup类的ConfigureServices方法中添加新的游戏邀请服务(我们想要一个单一的应用实例,所以将其添加为 Singleton):
        services.AddSingleton<IGameInvitationService,
         GameInvitationService>(); 
  1. 更新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.

  1. GameInvitationController中添加一个名为GameInvitationConfirmation的新方法:
        [HttpGet] 
        public IActionResult GameInvitationConfirmation(Guid id,
         [FromServices]IGameInvitationService gameInvitationService) 
        { 
          var gameInvitation = gameInvitationService.Get(id).Result; 
          return View(gameInvitation); 
        } 
  1. 为前面添加的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> 
        } 
  1. 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); 
          } 
        }
  1. 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; 
            } 
          }); 
        } 
  1. 更新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) { 
          }; 
        };  
  1. 在通信中间件中添加一个名为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;

  1. 添加一个名为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); 
          } 
        } 
  1. 更新通信中间件中的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)时才需要使用它:

  1. 将 SendGrid NuGet 包添加到项目中。 这将用于未来 Azure 生产部署的Tic-Tac-Toe应用:

<assets/a9b4312a-ecfb-4b20-91ac-678ce11c6a02.png>

  1. 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; 
          } 
        } 
  1. 添加一个新的扩展方法,以便能够更容易地针对特定环境声明特定的电子邮件服务。 为此,转到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; 
          } 
        } 
  1. 更新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.

  1. 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 类,例如StartupDevelopmentStartupStagingStartupProduction。 与之前的方法一样,它们将被自动使用; 你这边什么都不用做了。 只需更新Program类,实现特定于环境的 Startup 类,它就可以工作了。 ASP.NET Core 2.0 提供了这些有用的特性,让我们的生活变得轻松多了。

总结

在本章中,你已经学习了 ASP 的一些更高级的概念.NET Core 2.0,并实现了Tic-Tac-Toe应用中缺少的一些组件。

首先,您使用 JavaScript 创建了Tic-Tac-Toeweb 应用的客户端部分。 我们已经探索了如何通过使用捆绑和缩小来优化你的 web 应用,以及如何使用 WebSockets 来实现实时通信场景。

此外,您还看到了如何从集成的用户和会话处理中获益,这在一个易于理解的示例中显示。

然后,我们介绍了多语言用户界面、应用和服务配置的全球化和本地化,以及日志记录,以便更好地理解应用在运行时发生的事情。

最后,我们通过一个实际示例演示了如何一次性构建应用,然后根据部署目标使用多个ConfigureServicesConfigure方法以及多个Startup类的概念,使其适应不同的环境。

在下一章中,我们将讨论 ASP.NET Core MVC、MVC 中的 Razor(区域、布局、局部视图等等)、Razor 页面和视图引擎。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值