ASP-NET-Core2-基础知识-全-
ASP.NET Core2 基础知识(全)
零、前言
这本书以对 web 应用的介绍为基础,并帮助您理解久经考验的真正的 MVC 体系结构。您将了解有关视图的所有信息,从什么是 Razor 视图引擎到标记帮助程序。您将深入了解什么是模型,如何绑定它们,以及如何使用正确的模型迁移数据库。当您熟悉 ASP.NET 世界时,您将了解验证和路由。您还将学习高级概念,例如设计 RESTful 应用、为其创建实体以及创建 EF 上下文和迁移
这本书平衡了理论和练习,并包含多个开放式活动,这些活动使用真实的商业场景,让您在高度相关的环境中练习和应用新学到的技能。我们在 38 个主题中包含了 60 多个实践活动和练习,以加强您的学习。当您读完本书时,您将能够像专业人员一样以最佳方式使用 ASP.NET 开发、单元测试和部署应用。
这本书是给谁的
如果您希望使用 ASP.NET Core 构建 web 应用,或者希望成为使用 Microsoft 技术构建 web 应用的专家,这本书非常适合您。假设之前已经接触并理解了 C#、JavaScript、HTML 和 CSS 语法。本书是在.NETCore2.0 预览时编写的。
这本书涵盖的内容
第一章为搭建舞台,首先讲解 web 应用 HTTP、客户端和服务器端的基本概念。讨论了 ASP.NETMVC 的三种编程模式。最后,它提供了简单易用的分步说明来设置 ASP.NET Core Web 应用项目和项目结构。
第 2 章控制器解释了控制器在 ASP.NET MVC 应用中的作用。它还详细介绍了创建控制器的过程和操作方法。它还描述了如何修改控制器,使其使用视图。最后,它描述了如何添加模型并将该模型数据传递给视图。
第 3 章视图本质上更具实践性,因为它教授如何在 Razor 视图引擎中编程并使用不同的编程结构。它还深入解释了如何创建和调用局部视图、创建视图构件以及创建自定义标记辅助对象。
第 4 章模型解释了如何使用 ASP.NET MVC 中的模型来表示业务领域数据。它首先解释如何创建一个简单模型和一个特定于 ViewModel 的模型。然后,它提供了如何在 ASP.NET MVC 应用中使用实体框架的分步指导
第 5 章验证描述了在存储数据进行进一步处理之前验证输入数据的重要性。首先简要说明不同类型的验证。接下来通过一个示例说明如何执行客户端和服务器端验证。最后,介绍了如何使用 jQuery 库执行不引人注目的 JavaScript 验证。
第 6 章路由讨论了在 ASP.NET 中定制路由的几个选项。首先,介绍如何使用 MapRoute 配置路由。然后,通过示例,介绍如何使用不同类型的路由。
第 7 章Rest Buy详细介绍了一个名为 Rest Buy 的简单购物车应用的开发。与大多数项目的开始方式一样,本文讨论了 Rest Buy 的设计。然后,继续讨论应用的实体。最后,它处理 EF 上下文和迁移。
第 8 章新增功能、测试和部署围绕将注册功能添加到我们的应用中,并将其测试和部署到云端而构建。因此,它负责编写单元测试并将其升级到 bootstrap4。它还详细说明了如何将我们的应用部署到 Azure。
充分利用这本书
您需要一个配备以下硬件和软件的计算机系统:
-
为了获得最佳体验,我们建议采用以下硬件配置:
- 处理器:3.2 GHz 或更快的处理器(双核多线程)
- 内存:4 GB 的 RAM(如果在虚拟机上运行,则为 1.5 GB)
- 存储:安装需要 20-50 GB 的可用硬盘空间(根据安装的功能,要求的可用空间可达 130 GB)
-
您还必须提前安装以下软件:
- 操作系统:Windows Server 2008 R2 SP1(及以上)或 Windows 7 SP1(及以上)
- Visual Studio 社区 2017 IDE(https://www.visualstudio.com/downloads/ )
- 包和框架,如 NuGet、Bootstrap 和 project.json
- 小提琴手(https://www.telerik.com/download/fiddler )
下载示例代码文件
您可以从您的账户www.packtpub.com下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,将文件通过电子邮件直接发送给您。
您可以通过以下步骤下载代码文件:
- 登录或注册www.packtpub.com。
- 选择“支持”选项卡。
- 点击代码下载和勘误表。
- 在搜索框中输入图书名称,然后按照屏幕上的说明进行操作。
下载文件后,请确保使用以下最新版本解压或解压缩文件夹:
- WinRAR/7-Zip for Windows
- 适用于 Mac 的 Zipeg/iZip/UnRarX
- 适用于 Linux 的 7-Zip/PeaZip
该书的代码包也托管在 GitHub 上的https://github.com/TrainingByPackt/Beginning-ASP_DOT_NET 。如果代码有更新,它将在现有 GitHub 存储库中更新。
我们的丰富书籍和视频目录中还有其他代码包,请访问https://github.com/PacktPublishing/ 。看看他们!
使用的惯例
本书中使用了许多文本约定。
CodeInText:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。下面是一个示例:“将下载的WebStorm-10*.dmg磁盘映像文件作为系统中的另一个磁盘装载。”
代码块设置如下:
public class ValuesController : Controller
{
// GET api/<controller>
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
}
当我们希望提请您注意代码块的特定部分时,相关行或项目以粗体显示:
[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)
粗体:表示一个新术语、一个重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词出现在文本中,如下所示。下面是一个示例:“打开 Visual Studio 2017。导航到文件|新项目| Web。”
Warnings or important notes appear like this.
联系
我们欢迎读者的反馈。
一般反馈:发送电子邮件feedback@packtpub.com并在邮件主题中提及书名。如果您对本书的任何方面有疑问,请发送电子邮件至questions@packtpub.com。
勘误表:尽管我们已尽一切努力确保内容的准确性,但还是会出现错误。如果您在本书中发现错误,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/submit-errata,选择您的书籍,点击 errata 提交表单链接,然后输入详细信息。
盗版:如果您在互联网上发现我们作品的任何形式的非法复制品,请您提供我们的位置地址或网站名称,我们将不胜感激。请通过copyright@packtpub.com与我们联系,并提供该材料的链接。
如果您有兴趣成为一名作家:如果您对某个主题有专业知识,并且您有兴趣撰写或贡献一本书,请访问authors.packtpub.com。
评论
请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?然后,潜在读者可以看到并使用您的无偏见意见做出购买决定,我们 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们书籍的反馈。非常感谢。
有关 Packt 的更多信息,请访问packtpub.com。
一、搭建舞台
ASP.NET Core Microsoft 重新设计的 ASP.NET 是服务器端 web 应用开发框架,可帮助您有效地构建 web 应用。它运行在.NET Core 平台之上,使您的应用能够在各种平台上运行,包括 Linux 和 macOS。这打开了大量的机会,在这个时代成为一名.NET 开发人员是令人兴奋的。
在本章结束时,您将能够:
- 解释有关 web 应用 HTTP、客户端和服务器端的基本概念
- 解释 ASP.NET MVC 的三种编程模型
- 掌握 ASP.NET MVC 的理念
- 创建第一个 ASP.NET Core Web 应用项目和项目结构
Web 应用简介
在讨论 ASP.NET Core 及其特性之前,让我们先了解 web 应用开发的基础知识。
Remember this principle: If you want to be an expert at something, you need to be very good at the fundamentals.
Web 应用的工作原理
所有 web 应用,不管它们是否使用 ASP.NET MVC(MVC代表模型视图控制器)构建,实际上都是受 Ruby on Rails 或任何其他新的闪亮技术的成功启发,使用 HTTP 协议。一些应用使用 HTTPS(HTTP 的一种安全版本),其中数据在通过电线之前进行加密。但是 HTTPS 仍然使用 HTTP。
对称加密是确保传输数据完整性的常规方法。它只使用一个称为对称密钥的密钥进行加密和解密。发送方和接收方都拥有此密钥。发送方使用它进行加密,而接收方使用它进行解密。凯撒密码是对称加密的一个很好的例子。
非对称加密使用两个加密密钥。这些密钥称为公钥和私钥。要发送的信息由公钥加密。私钥用于解密接收到的信息。这两个过程背后都有相同的算法。RSA 算法是用于非对称加密的一种流行算法。
加密通过使用加密密钥确保传输的数据的完整性。这些密钥仅由传输数据的发送方和接收方知道。这意味着数据不会被任何其他人篡改。这可以防止中间人攻击。
什么是 HTTP 协议?
协议只不过是一组控制通信的规则。HTTP 协议是遵循请求-响应模式的无状态协议。
HTTP代表超文本传输协议,是为分布式超媒体系统设计的应用协议。超文本传输协议中的超文本是指使用超链接在文档之间进行遍历的结构化文本。HTTP 标准由互联网工程任务组(IETF)和万维网联盟(W3C)开发。HTTP 的当前版本为 HTTP/2,并于 2015 年标准化。大多数 web 浏览器都支持它,如 Microsoft Edge、Google Chrome 和 Mozilla Firefox。
HTTP/2 在 HTTP/1.x 上的优势
在高层,HTTP/2:
- 是二进制的,而不是文本的
- 完全多路复用,而不是有序和阻塞
- 使用一个连接实现并行性
- 使用头压缩来减少开销
- 允许服务器主动将响应推送到客户端缓存中
请求-响应模式
在讨论请求-响应模式之前,让我们先讨论两个术语:客户机和服务器。服务器是从客户机接收请求并为其提供服务的计算资源。服务器通常是一台高性能的机器,具有巨大的内存来处理许多请求。客户端是发送请求并接收响应的计算资源。客户机通常可以是发送请求的任何应用。
回到请求-响应模式,当您从服务器请求资源时,服务器将使用请求的资源响应您。资源可以是任何内容—网页、文本文件、图像或其他数据格式。

你提出请求。服务器使用资源进行响应。这称为请求-响应模式。
HTTP 的无状态性质
当您再次请求相同的资源时,服务器会再次使用请求的资源响应您,而不知道之前已请求并提供了相同的资源。HTTP 协议本质上不知道先前接收和服务的任何请求的状态。有几种机制可以维护该状态,但 HTTP 协议本身并不维护该状态。稍后我们将解释维持该状态的机制。
HTTP 的优势
以下是使用 HTTP 协议的几个优点:
- HTTP 是运行在 TCP/IP 之上的基于文本的协议
- HTTP 是防火墙友好的
- HTTP 更容易调试,因为它是基于文本的
- 所有浏览器都知道 HTTP。因此,它在任何设备或任何平台上都是非常便携的
- 它将应用级协议标准化为适当的请求-响应周期
With TCP/IP, everybody has to invent their own application protocol.
HTTP is traditionally not full duplex, but with HTML5 we can use Web Sockets to upgrade HTTP connections to a full duplex connection.
处理无状态和请求-响应模式
借助一个简单的实际示例,让我们来处理无状态和请求-响应模式。以下是步骤:
- 键入此 URL:https://en.wikipedia.org/wiki/ASP.NET_Core 。这是一个关于 ASP.NET Core 的维基百科网页。
We'll talk about ASP.NET later in this chapter.
-
浏览器从前面的 URL 向 Wikipedia 服务器发出请求。
-
维基百科的 web 服务器为您提供 ASP.NET Core 网页。
-
您的浏览器接收并显示该网页。
-
现在,再次输入相同的 URL(来请求相同的页面 https://en.wikipedia.org/wiki/ASP.NET_Core 并按进入。
-
浏览器再次向 Wikipedia 服务器发出请求。
-
Wikipedia 为您提供相同的 ASP.NET Core 网页,而不知道以前请求过相同的资源。
-
以下是维基百科页面的屏幕截图,显示请求和响应:

As mentioned earlier, there are several mechanisms to maintain the state. Let us assume, for the time being, that no such mechanism is implemented here.
客户端和服务器端
有必要了解 web 应用的客户端和服务器端,以及在任何一方都可以做什么。对于 web 应用,客户端是浏览器,服务器可以是 web 服务器/应用服务器。
客户端是浏览器中发生的任何事情。它是 JavaScript 代码运行和 HTML 元素驻留的地方。
服务器端是计算机另一端的服务器上发生的任何事情。您从浏览器发出的请求必须通过网络(可能通过网络)执行某些服务器端代码并返回相应的响应。您的浏览器不知道服务器端技术或编写服务器端代码的语言。服务器端也是您的 C#代码所在的地方。
让我们讨论一些事实,让事情变得更清楚:
- 事实 1:所有浏览器只能理解 HTML、CSS(层叠样式表)和 JavaScript,无论浏览器供应商:
- 您可能正在使用 Microsoft Edge、Firefox、Chrome 或任何其他浏览器。然而,事实是,您的浏览器只能理解 HTML、CSS 和 JavaScript。它不能理解 C#、Java 或 Ruby。这就是您可以通过同一浏览器访问使用任何技术构建的 web 应用的原因:

- 事实 2:任何 web 开发框架的目的都是将服务器端代码转换为 HTML、CSS 和 JavaScript:
- 这与前一点有关。由于浏览器只能理解 HTML、CSS 和 JavaScript,所有 web 开发技术都应该将服务器端代码转换为 HTML、CSS 和 JavaScript,以便浏览器能够理解。这是任何 web 开发框架的主要目的。无论您是使用 ASP.NETMVC、ASP.NETWeb 表单、RubyonRails 还是 J2EE 构建 web 应用,都是如此。关于如何生成 HTML、CSS 和 JavaScript,每个 web 开发框架可能都有一个独特的概念/实现,并且可能以不同的方式处理安全性能等特性。但是,每个框架都必须生成 HTML,因为这是浏览器所理解的。
编程风格–RPC 与 REST
基本上,编程 HTTP 时有两种常见的样式:远程过程调用和 REST。让我们看看这里的每一个:
- 远程过程调用:在 RPC 风格中,我们通常将 HTTP 作为传输介质,而不关注 HTTP 本身。我们只是简单地依靠 HTTP。我们的服务提供了一些可直接调用的操作集。换句话说,从我们的客户机,我们调用方法就像调用普通方法和传递参数一样。通常,RPC 是通过SOAP(简单对象访问协议)应用的,这是另一种运行在 HTTP 之上的 XML 协议。RPC 在 2008 年之前很流行,现在 RESTful 方法更流行,因为 RPC 风格在客户端和服务器之间引入了更多的耦合。
- REST:REST 表示代表状态转移。在 REST 中,我们使用 URL 来表示我们的资源,例如
https://api.example.com/books/。此 URL 基本上是图书集合的标识符。例如,以下可能是 ID 为 1 的书籍的标识符:https://api.example.com/books/1。
然后,我们使用 HTTP 谓词与这些资源交互。HTTP 谓词和 HTTP 方法是同义词。HTTP 中可用的方法有GET、HEAD、POST、PUT、DELETE、TRACE、OPTIONS、CONNECT和PATCH。因此,当我们使用GET发出 HTTP 请求时,我们基本上要求 web 服务器返回该资源表示。而且,即使对于每个请求,这种表示也可能发生变化。
服务器可以为一个请求返回 XML,为另一个请求返回 JSON,这取决于客户端接受的内容,这是由Accept头指定的。
我们为什么需要休息?这一切都是关于标准化的。假设我们使用GET动词访问资源;我们天生就知道,我们并没有改变服务器中的任何东西。类似地,当我们通过PUT发送请求时,我们本质上知道请求是幂等的,这意味着重复的请求不会对同一资源进行任何更改。一旦我们建立了这个标准,我们的应用的行为就像浏览器一样。就像浏览器在浏览页面时不需要 API 文档一样,我们的应用也不需要文档,只需要遵守标准。
使用 HTTP 方法
HTTP 定义方法(有时称为动词,以指示要在已识别资源上执行的所需操作。它是 HTTP 规范的一部分。尽管 HTTP 协议的所有请求都遵循请求-响应模式,但请求的发送方式可能会有所不同。HTTP 方法定义如何将请求发送到服务器。
HTTP 中可用的方法有GET、HEAD、POST、PUT、DELETE、TRACE、OPTIONS、CONNECT和PATCH。在大多数 web 应用中,GET和POST方法被广泛使用。在本节中,我们将讨论这些方法。稍后,我们将在需要了解的基础上讨论其他 HTTP 方法。
GET 方法
GET是 HTTP 协议的一种方法,用于从服务器获取资源。使用GET方法的请求只应检索数据,不应有任何副作用。这意味着,如果您一次又一次地发出相同的GET请求,您应该会得到相同的数据,并且服务器的状态不应该因为该GET请求而发生任何变化。
在GET方法中,参数作为请求 URL 的一部分发送,因此最终用户可以看到。这种方法的优点是,用户可以将 URL 添加到书签中,并在需要时再次访问页面。一个例子是https://yourwebsite.com/?tech=mvc6 &db=sql。
我们在前面的GET请求中传递了几个参数。tech为第一参数,取值为mvc6,db为第二参数,取值为sql。假设您的网站使用前面的参数值,并在数据库中进行搜索,以在向用户展示这些博客文章之前检索有关mvc6和sql的博客文章:

GET 方法的缺点是,由于数据在 URL 中以明文形式作为参数传递,因此无法用于发送敏感信息。此外,大多数浏览器对 URL 中的字符数有限制,因此,在使用 GET 请求时,我们无法发送大量数据。
POST 方法
POST请求通常用于更新或创建服务器上的资源,以及发送一些数据以供服务器处理时。特别是在 REST 的上下文中,更为精确地考虑 OLE T1 作为一个过程而不是 Ty2 T2。
数据在请求主体中传递。这有以下影响:
- 您可以向服务器发送相对敏感的信息,因为数据嵌入在请求主体中,最终用户在 URL 中看不到。但是,请注意,除非您使用 HTTPS,否则您的数据永远不会真正安全。即使您在请求主体内发送数据,而不使用 HTTPS,中间人很容易窃听您的数据。
- 由于数据不是通过请求 URL 发送的,因此不会占用 URL 中的空间,因此不存在 URL 长度限制问题:

我们已经介绍了基本知识,现在可以继续讨论 ASP.NET。
重要方法清单
在讨论 HTTP 方法之前,让我们回顾一下 HTTP 谓词的三个方面:
-
幂等性:幂等性是 HTTP 调用中的一个重要概念。在幂等请求中,可以更改服务器端状态(但是,只能更改一次)。也就是说,如果您向服务器发出多个幂等请求,净效果将与您执行了一个请求一样。
-
安全:安全请求根本不会导致任何副作用。它们仅用于检索数据。关于副作用,我们指的是内存、数据库或任何其他外部系统中的任何持久性更改。注册用户是一个副作用。转账是一种副作用。但查看用户信息并不是副作用。
-
可缓存性:服务器或客户端或代理可以缓存请求的响应。
下表列出了重要的 HTTP 方法及其特性:
| 方法 | 说明 | 幂等元 | 安全 | 可缓存 |
| GET | 读取资源。 | 对 | 对 | 对 |
| POST | 创建资源或触发
进程。 | 不 | 不 | 不 |
| PUT | 将某物放入资源
ID。如果某物退出,则覆盖。
不要与
更新混淆。 | 对 | 不 | 不 |
| PATCH | 更新资源的一部分。 | 不 | 不 | 不 |
| DELETE | 删除资源。 | 对 | 不 | 不 |
在上表中,我们可以看到GET方法是唯一安全的方法。这就是为什么,例如,像谷歌这样的搜索引擎只使用GET方法来扫描我们这边。遵守此标准可确保在搜索引擎扫描过程中没有任何更改。
其他方法
其他一些值得注意的方法如下:
CONNECT:出于安全原因,用于 HTTP 隧道。这在典型的 web 应用和服务中并不常见。TRACE:用于调试目的。这在典型的 web 应用和服务中并不常见。OPTIONS:通过使用OPTIONS动词,我们可以查询该资源的 web 服务器支持哪些方法。
下面是调用OPTIONS方法后的部分响应:
HTTP/1.1 200 OK
Allow: OPTIONS, GET, HEAD, POST
活动:使用请求-响应模式
场景
您的公司希望您监控其网站的网络流量。在这里,我们使用https://www.google.com/ 作为参考。
瞄准
检查的请求-响应模式 https://www.google.com/ 。
完成步骤
- 打开您喜爱的浏览器。
- 点击F12打开开发者工具。
- 然后,点击网络选项卡。
- 下一步,转到https://www.google.com/ 。
- 研究请求和响应的头体。
您应该会看到类似于以下屏幕截图所示的内容:

ASP.NET 简介
ASP.NET 是一个服务器端 web 应用开发框架,由 Microsoft 开发,允许开发人员构建 web 应用、网站和 web 服务。
它目前在此 URL 中是完全开源的,并且仍然由 Microsoft 维护:https://github.com/aspnet
基本上,ASP.NET 有三种主要的编程模型:ASP.NET Web 窗体、ASP.NET MVC 和 ASP.NET 网页。它们以以下方式构成 ASP.NET 框架的一部分:
- ASP.NET for.NET Framework:它有以下子部分:
- Web 表单:以快速应用开发而闻名。这试图模仿桌面行为。
- MVC:应用模型-视图-控制器模式。
- Web API:这是一个 MVC 风格的 Web 服务。
- 单页应用:这里,服务器给出初始 HTML 请求,但进一步呈现完全在浏览器中进行。
- ASP.NET Core:全新的 ASP.NET 平台,跨平台运行。各小节包括:
- Web API:主要用于开发 Web 服务。
- Web 应用:用于 MVC 应用。它也可以用于开发 web 服务。WebAPI 和 MVC 已经成为一种几乎统一的东西。
- Web 应用(Razor Pages):Razor Pages 是 ASP.NET Core MVC 的一项功能,它使编写以页面为中心的场景更容易、更高效。
A recent trend for developers is the use of Single-Page Application frameworks on top of web services like Web APIs. However, MVC and Single-Page Application frameworks also play nicely together. In the future, we expect Microsoft to put more effort on .NET Core instead of .NET Framework. .NET Framework is already mature. Perhaps it will be put into maintenance mode but nothing is certain yet.
尽管前面所有编程模型的最终结果都是有效地生成动态网页,但它们遵循的方法却各不相同。让我们讨论一下 ASP.NETMVC。
ASP.NETMVC
ASP.NET MVC 是 MVC 模式在 ASP.NET 中的实现。ASP.NET MVC 解决了试图在 Web 环境中模仿 Windows 开发的 ASP.NET Web 窗体的缺点,如对 HTML 生成的控制有限、与业务代码和 UI 代码耦合、难以掌握以及页面生命周期复杂。由于大多数现代应用都是由客户端 JavaScript 库/框架控制的,比如jQuery、KnockoutJS、AngularJS和ReactJS,因此完全控制生成的 HTML 至关重要。至于 Knockout、Angular 和 React,这些单页库实际上通过自己的模板引擎直接在浏览器中生成 HTML。换句话说,渲染是在浏览器而不是服务器中完成的。这释放了服务器资源,并允许 web 应用像断开连接的应用一样运行,就像在移动应用中一样。
让我们谈一谈模型-视图-控制器模式以及它对 web 应用开发的好处。
模型-视图-控制器模式
这是一种软件体系结构模式,有助于定义每个组件的责任,以及它们如何配合以实现总体目标。此模式主要用于构建用户界面,适用于许多领域,包括开发桌面应用和 web 应用。但我将从 web 开发的上下文中解释 MVC 模式。
MVC 模式主要有三个组件:
- 型号:此组件表示您的域数据。请注意,这不是您的数据库。此模型组件可以与数据库通信,但该模型仅表示域数据。例如,如果您正在构建一个电子商务 web 应用,那么模型组件可能包含诸如
Product、Supplier和Inventory之类的类。 - 视图:该组件负责向用户展示什么。通常,该组件将包含 HTML 和 CSS 文件。这还可能包括布局信息,这些信息控制 web 应用对最终用户的外观。
- 控制器:顾名思义,控制器负责与不同组件交互。它接收请求(通过路由模块),与模型对话,并向用户发送适当的视图。
下图介绍了 MVC 模式:

这种职责分离为 web 应用开发带来了极大的灵活性,允许单独和独立地管理每个区域。
ASP.NET Core 的代码如下所示:
public class ValuesController : Controller
{
// GET api/<controller>
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
}
基本上,每个控制器都由一个派生自Controller类的类表示,尽管我们也可以编写控制器而不必派生自Controller。控制器的每个公共方法都表示操作。
在这种情况下,如果我们定义一个GET方法(通过www.yoursite.com/controller访问而不写入GET,它将返回一个字符串数组作为响应。这些字符串的返回方式取决于内容协商。
基于文件的项目
无论何时在文件系统(ASP.NET Core 项目文件夹内)中添加文件或文件夹,更改都会自动反映在应用中。
我们将静态文件捆绑到一个文件中,因为对于每个静态文件,浏览器将向服务器发出单独的请求以检索它。如果您有 100 个 CSS 和 JavaScript 文件,这意味着将有 100 个单独的请求来检索这些文件。显然,减少请求数量肯定会提高应用的性能。因此,捆绑可以有效地减少请求数量。
HTTP/2 uses only 1 persistent connection for all files and requests. Thus, bundling is less useful in HTTP/2. However, it's still recommended since HTTP/1.x is here to stay for a long time.
The development and deployment of an ASP.NET Core application on a Linux machine will be explained in a later chapter.
以下是基于文件的项目中的重要文件夹和文件:
| 文件夹/文件 | 说明 |
| Controllers | 此文件夹包含所有控制器文件。控制器负责处理请求、通信模型和生成视图。 |
| Models | 表示域数据的所有类都将出现在此文件夹中。 |
| Views | 这些文件包含前端组件,并提供给应用的最终用户。此文件夹包含所有 RazorView 文件。 |
| wwwroot | 该文件夹充当根文件夹,是放置所有静态文件(如 CSS 和 JavaScript 文件)的理想容器。放置在wwwroot文件夹中的所有文件都可以直接从路径访问,而无需通过控制器。 |
| 其他文件 | appsettings.json文件是配置文件,您可以在其中配置应用级设置。以前,.xml文件用于配置;但是,.json格式不太详细,Bower和npm(节点包管理器是客户端技术,由 ASP.NET Core 应用支持。Bundle.config文件允许我们配置如何将 CSS 和 JS 文件捆绑到一个文件中。 |
以下是 ASP.NET Core 的项目结构:

Despite the fact that current ASP.NET Core templates are using Bower, Bower itself is obsolete now. Instead, npm or yarn is recommended. Just like NuGet, the JavaScript world needed package managers as there are hundreds of thousands of libraries and they have complex dependencies on each other. These package managers allow you to automate the installation and upgrades of these libraries by writing single commands from the command line.
创建第一个项目
按照以下步骤创建第一个项目:
- 打开 Visual Studio 2017。导航到文件|新项目|网站。您将看到以下屏幕:

- 选择 ASP.NET Core Web 应用。(可选)为项目指定名称,或接受默认名称。然后,单击 OK。
- 请确保选择.NET Core 2.0。如果没有显示,请从下载.NET Core SDKhttps://www.microsoft.com/net/download/ 核心并重新启动 Visual Studio。然后选择 Web 应用并单击 OK。
- 在项目上单击鼠标右键,然后单击“构建”。这将恢复依赖项。
创建第一个应用
现在是创建第一个 ASP.NET Core 应用的时候了。
启动 Visual Studio 并执行以下步骤:
- 通过在 Visual Studio 中选择文件|新建项目来创建项目。第一个选项用于创建 ASP.NET web 应用的早期版本。第二个选项是使用.NET Core 框架创建 ASP.NET Core 应用。NET Core 仅支持核心功能。使用.NET Core 库的优点是它可以部署在任何平台上。选择 ASP.NET Core Web 应用:

Routing and controllers work together to render the correct view.
We'll use the name Lesson2 here to avoid reinventing the wheel in Chapter 2, Controllers.
- 从 ASP.NET Core 模板列表中选择空模板。第二个选项用于创建 Web API 应用(用于构建基于 HTTP 的服务),第三个选项用于创建包含一些基本功能的 Web 应用,这些功能可以在开箱即用的情况下运行,而无需编写任何内容:

- 单击窗口中的“确定”后,如前一屏幕截图所示(选择清空模板选项后),将创建一个解决方案,如以下屏幕截图所示:

- 当您运行应用时(按F5)不做任何更改,您将得到一个简单的 Hello World!屏幕上的文本,如以下屏幕截图所示:

我们还没有在这个新创建的应用中进行任何编码。那么,你有没有想过它是如何显示文本 Hello World!?
答案在Startup.cs文件中,该文件包含一个名为 Startup 的类。
When an exception occurs, we want to display the callstack for better diagnosis, for instance. However, doing so in a production environment would be a security risk. Hence, we have development-specific code.
ASP.NET Core 运行时通过 main 方法调用ConfigureServices和Configure方法。例如,如果要配置任何服务,可以在此处添加它。您的应用的任何自定义配置都可以添加到此Configure方法中:
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!");
});
}
Configure 方法中只有几个语句。让我们在第二次发言中暂时撇开async、await和context,我们将在稍后讨论。本质上,第二条语句告诉运行时返回 Hello World!对于所有传入请求,不考虑传入 URL。
当您在浏览器中键入 URLhttp://localhost:50140/Hello时,它仍将返回相同的 Hello World!
这就是我们得到 Hello World 的原因!当我们运行应用时。
由于我们在创建 ASP.NET Core 应用时选择了空模板,因此不会安装任何组件。当您像我们一样选择空模板时,默认情况下甚至不会安装 MVC。
总结
在本章中,您学习了 web 开发的基础知识,包括构成服务器端和客户端的内容。HTTP 是 web 开发中的关键协议。我们甚至讨论了 ASP.NET Core 的特性。我们将 REST 和 RPC 视为两种 web 编程风格。
我们还讨论了 ASP.NET Core 应用的新项目结构以及与以前版本相比的变化。
在下一章中,我们将讨论控制器及其角色和功能。我们还将构建一个控制器和相关的操作方法,看看它们是如何工作的。
二、控制器
在上一章中,我们讨论了所有 web 应用都接收来自服务器的请求,并生成一个返回给最终用户的响应。本章介绍了 ASP.NET MVC 应用中控制器的角色,并详细介绍了创建控制器的过程和操作方法
在本章结束时,您将能够:
- 解释 ASP.NET MVC 应用中控制器的角色
- 使用路由引擎
- 在应用中安装 ASP.NET Core NuGet 软件包
- 创建第一个控制器和操作方法
- 添加视图并进行更改,以允许控制器使用该视图
- 添加模型并将该模型数据传递到视图
ASP.NET MVC 应用中控制器的角色
在 ASP.NET MVC 中,控制器负责接收请求并根据输入数据生成输出。您可以将控制器想象为组织应用流的业务流的入口点。
If you are intending to write a complex application, it is best to avoid business logic in your controllers. Instead, your controllers should call your business logic. In this way, you can keep the core part of your business technology-agnostic.
在高层,控制器在模型和视图之间进行协调,并将输出发送回用户。这也是通常通过操作过滤器进行身份验证的地方。动作过滤器基本上是拦截器,将在本章的过滤器部分详细讨论。下图说明了 ASP.NET MVC 中请求的高级流程(包括步骤),并向我们展示了控制器如何融入全局:

以下是当用户访问 ASP.NET Core 应用时将在高层发生的事件序列:
- 用户在浏览器中键入 URL。
- 路由引擎根据 URL 的模式选择适当的控制器。
- 控制器与模型对话,通过其动作方法获取任何相关数据。动作方法是控制器类中的方法。
- 然后,控制器将数据传递给视图,以可视格式(通常为 HTML 元素)呈现数据。
- 视图最终被交付给用户,用户将在浏览器中查看该视图。
在讨论控制器之前,让我们先讨论路由概念的基本原理,因为路由引擎在运行时只选择适当的controller和action方法。
分层 Web 应用的理想数据流
让我们看看分层 web 应用的理想数据流是什么样子的。请看下图:

让我们分析一下图表。解释如下:
- 浏览器是用户输入 URL 的媒介
- 相应的控制器开始工作
- 控制器与业务层和数据层通信
- 业务层将用户请求的数据返回控制器
业务层和数据层构成了 MVC 的模型部分。现在,这些层是什么?业务层的工作是使用业务逻辑或功能。数据层涉及外部系统,如数据库。它提供对这些系统的访问。
路由简介
路由引擎负责获取传入请求,并根据 URL 模式将该请求路由到适当的控制器。我们可以配置路由引擎,以便它可以根据相关信息选择适当的控制器。换句话说,路由是一种编程映射,它根据某个 URL 模式声明要调用哪个控制器的哪个方法。
按照惯例,ASP.NET MVC 遵循以下模式:Controller/Action/Id。
如果用户键入 URLhttp://yourwebsite.com/Hello/Greeting/1,路由引擎选择Hello controller中的Hello controller类和Greeting action方法,并传递Id值,因为1\. XXXController是一种命名约定,并且假设您的控制器总是以控制器后缀结尾。您可以为某些参数指定默认值,并使某些参数成为可选参数。
以下是示例配置:
The template: "{controller=Hello}/{action=Greeting}/{id?}");
在前面的配置中,我们为路由引擎提供三条指令:
- 使用路由模式
controller/action/id。 - 如果 URL 中未提供
controller或action的值,则分别使用controller和action的默认值Hello和Greeting。 - 将
id参数设置为可选,以便 URL 不需要此信息。如果 URL 包含此 Id 信息,它将使用它。否则,id 信息将不会传递给action方法。
让我们讨论一下路由引擎如何为不同的 URL 选择controller类、action方法和id值。我们将从URL1开始,这里:
URL1: http://localhost/
Controller: Hello
Action method: Greeting
Id: no value is passed for the id parameter
The Hello controller is passed as the default value as per the routing configuration, as no value is passed as the controller in the URL.
当传递前面的 URL 时,路由处理程序将拾取以下action方法:
public class HelloController : Controller
{
public ActionResult Greeting(int id)
{
return View();
}
}
我们来看URL2,这里:
URL2: http://localhost/Hello/Greeting2
Controller: Hello
Action method: Greeting2
Id: no value is passed for the id parameter
The Hello controller will be chosen as the URL contains Hello as the first parameter, and the Greeting2 action method will be chosen as the URL contains Greeting2 as the second parameter. Please note that the default value mentioned in the configuration would be picked only when no value is present in the URL. As the id parameter is optional and the URL does not contain the value for id, no value is passed to the id parameter.
当传递前面的 URL 时,路由处理程序将拾取以下操作方法Greeting2:
public class HelloController : Controller
{
public ActionResult Greeting(int id)
{
return View();
}
public ActionResult Greeting2(int id)
{
return View();
}
}
我们来看URL3,这里:
URL3: http://localhost/Hello2/Greeting2
Controller: Hello2
Action method: Greeting2
Id: no value is passed for the id parameter
As Hello2 is passed as the first parameter, the Hello2 controller will be selected, and Greeting2 is the action method selected since Greeting2 is passed as the second parameter. As the id parameter is optional and no value is passed for the parameter id, no value will be passed for the id.
当传递前面的 URL 时,路由处理程序将选择以下操作方法:
public class Hello2Controller : Controller
{
public ActionResult Greeting2(int id)
{
return View();
}
}
我们来看URL4,这里:
URL4: http://localhost/Hello3/Greeting2/1
Controller: Hello3
Action method: Greeting2
Id: 1
Hello3 is the controller selected as it is mentioned as the first parameter. Greeting4 is the action method, and 1 is the value passed as the id.
当传递前面的 URL 时,路由处理程序将选择以下操作方法:
public class Hello3Controller : Controller
{
public ActionResult Greeting2(int id)
{
return View();
}
}
另一种常见的模式是使用更多 RESTful 编程实践。相反,我们将 URL 视为资源 ID,并将操作作为 HTTP 方法发送,如GET或POST。
因此,从经典的 MVC 的角度来看,如果你需要编辑一本书,你可以向http://yourwebsite.com/Books/Edit/1发送一篇带有POST请求的帖子,正文包含新书的详细信息。
但是,从 RESTful 的角度来看,您可以将http://yourwebsite.com/Books/1与PUT或PATCH请求一起使用,请求主体包含书籍细节。
RESTful 编程的要点是要有某种人人都同意的标准化,至少在一定程度上是如此。对于 RESTful,您不必记录 API。每个人都知道,PUT请求会替换 HTTP 标准的资源。但是,对于操作,有人可以将其称为“编辑”,而另一个人可以将其称为“更新”。当然,对于不打算用作 API 的 web 应用,这就没有什么价值了。但是,您可能希望 web 应用附带一个可重用的 API。这就是 RESTful 方法的亮点所在。在这种情况下,您的 web 页面只会成为 API 的一个特例,并且复制更少。
活动:查找为 URL 调用的正确方法
场景
你们公司发生了网络攻击。管理员想知道黑客通过向您发送恶意 URL 调用了哪个方法。
瞄准
查找针对给定 URL(http://localhost/Hello3/Welcome/1调用的正确方法。
完成步骤
打开编辑器并键入以下代码:
Go to https://goo.gl/2Jy3W4 to access the code.
public class Hello3Controller : Controller
{
public ActionResult Welcome(int id)
{
return View();
}
...
...
{
return View();
}
}
一旦请求到达控制器,控制器将通过与模型对话创建响应,并可能将数据传递给视图,然后将视图呈现给最终用户。
We will discuss routing in detail in a later chapter.
在应用中安装 ASP.NET Core NuGet 包
我们将立即开始在您的应用中安装 ASP.NET Core NuGet 包。
按照以下步骤安装 ASP.NET MVC 的 NuGet 软件包:
- 在依赖项上单击鼠标右键,然后选择“管理 NuGet 软件包”选项:

- 我们将看到安装了一个名为 Microsoft.ASPNetCore.All 的包(如下面的屏幕截图所示)。这个包实际上是一个元包,它安装了我们需要的大多数依赖项。

- 如果我们从依赖项扩展此包,我们将看到:

因此,无论是否使用空项目,我们所需的一切都已安装。
我们的应用中安装了 ASP.NET Core。现在,我们需要告诉我们的应用使用 ASP.NETMVC。
这需要对Startup.cs文件进行一些更改:
- 配置应用以添加 MVC 服务。这可以通过在 Startup 类的
ConfigureServices方法中添加以下行来实现:
Go to https://goo.gl/RPXUaw to access the code.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
- 配置路由,以便根据输入的 URL 为传入请求选择正确的控制器。以下代码段需要在
Startup.cs文件的Configure方法中更新:
Go to https://goo.gl/Xa1YcD to access the code.
public void Configure(IApplicationBuilder app,IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}
/{action=Index}/{id?}");
});
}
在前面的语句中,我们正在为应用配置路由。
在本章以及本课程的大部分章节中,我们将手动编写代码或选择一个空模板,而不是依赖脚手架模板。对于那些不熟悉术语 scaffolding 的人来说,scaffolding 是一种功能,它为您生成所选项目(例如,控制器)的所有必要样板代码,而不是您需要编写所有内容。
Though scaffolding templates are useful and save time in generating the boilerplate code, they hide many of the details that beginners have to understand. Once you write code manually, you'll know all the intricacies of how each of the components is contributing to the big picture. Once you are strong in the fundamentals, you can use scaffolding templates to save you time in writing the boilerplate code. Scaffolding is also useful for creating quick administrative pages to edit our database.
我们的第一个控制器
在创建控制器之前,我们需要删除以下app.Run语句,因为这将返回 Hello World!对于所有传入的请求。由于我们希望传入的请求由控制器处理,因此需要从Startup类的 Configure 方法中删除以下代码:
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
};
我们已经在应用中安装了 ASP.NET Core。因此,我们准备创建我们的第一个 ASP.NET Core 控制器。创建一个名为Controllers的文件夹,并从上下文菜单中添加一个新控制器,如下图所示:

上下文基本上表示请求-响应对以及处理请求所需的其他元数据
For people who used OWIN to develop your own web custom framework without using MVC, it is analogous to IOwinContext. And apparently, app.Run would be a good entry point to handle HTTP requests manually or for writing a custom framework. After all, HTTP is about retrieving requests and returning responses.
导航到添加|新项目后,将显示以下选项列表。我们将向项目中添加一个 MVC 控制器类:


将创建包含以下内容的类:
public class HomeController : Controller
{
// GET: /<controller>/
public IActionResult Index()
{
return View();
}
}
所有控制器,包括 MVC 和 Web API 控制器,都继承自Controller基类。在 ASP.NET MVC 的早期版本中,MVC 控制器将从Controller类继承,而 Web API 控制器将从APIController类继承。
在前面的HomeController类中,我们有一个由Index执行的单一操作方法,该方法返回相应的视图。当您按原样运行应用时,将出现 500 个内部服务器错误。原因是没有为HomeController的Index操作创建任何视图,ASP.NET Core 试图搜索该视图。由于视图不可用,它返回一个 500 内部服务器错误,并显示消息“InvalidOperationException:未找到视图“Index”。搜索了以下位置:”。无论何时状态码以5XX开头,我们都认为是服务器故障。每当状态代码以4XX开头时,它就与客户端相关。
与其创建并返回该视图,不如对该操作方法进行简单更改。让我们回传一串,你好世界!我正在学习 MVC!,并更改IActionResult的退货类型:
public string Index()
{
return "Hello World! I am learning MVC!";
}
运行应用。你会看到你好世界!我正在学习 MVC!在浏览器中,如以下屏幕截图所示。请确保删除Configure方法中的app.Run语句,如前所述:

瞧!我们已经更改了 ASP.NET Core 应用,以呈现自定义内容,而不是无聊的 Hello World!我们所做的似乎只是一个小小的改进,但我们在 ASP.NET Core 应用中使用了控制器和操作方法,这为 web 应用开发带来了很多结构和灵活性。
以下屏幕截图显示了运行应用时在后台发生的情况:

以下是运行应用时发生的一系列步骤:
- 应用在 URL
http://localhost:50140上运行,其中50140是 IIS Express 选择的在我的本地系统上运行应用的端口号。这个数字可能会有所不同。 - 由于我们没有传递任何参数,因此将选择
Controller和动作方法的默认值。在我们的案例中,HomeController将被选为Controller,而HomeController中的Index将被选为动作方式。由于ID是可选值且未传递,因此忽略此ID参数。 - 路由引擎选择
Controller和action方法后,控制传递给所选控制器的action方法。在我们的案例中,它将是HomeController的Index动作方式。 - 在
Index动作方法中,我们返回一个字符串,Hello World!我正在学习 ASP.NETMVC!该文本从控制器返回,然后返回给用户。
IActionResult
如果您注意到,控制器的action方法中的默认返回类型是IActionResult,然后我们将返回类型更改为字符串,以便返回文本 Hello World!。
IActionResult是我们可以用来返回不同类型ActionResult的接口,从简单的字符串到复杂的 JSON 数据,所以我们不需要改变action方法的返回类型来返回字符串。
在前面的示例中,将返回类型更改为字符串以简化操作。现在,让我们做一个简单的更改,通过保持返回类型(IActionResult)不变来返回字符串:
// GET: /<controller>/
public IActionResult Index()
{
return Content("Hello World! I am learning MVC!");
}
在返回字符串时,我们在前面的action方法中使用Controller类(继承HomeController的基础控制器)中名为Content的虚拟方法。此Content()方法的目的是将字符串转换为IActionResult类型。
IActionResult能够返回不同的数据类型:
ContentResult:可以返回文本结果。EmptyResult:返回空结果。FileResult:返回二进制输出以写入响应。HttpStatusCodeResult:提供返回的方式。JavaScriptResult:返回可以从客户端执行的脚本。JSonResult:返回一个序列化的 JSON 对象。RedirectResult:重定向到另一个动作方法。RedirectToRouteResult:表示使用指定的路由值字典执行重定向的结果。
These are actually methods in the ControllerBase class.
活动:实现您自己的 IActionResult
场景
您希望了解如何访问字符串的底层响应流。
瞄准
编写一个将给定字符串大写的活动结果。
完成步骤
- 首先,将以下类添加到项目中:
Go to https://goo.gl/GDi6JS to access the code.
public class UpperStringActionResult : ActionResult
{
readonly string str;
public UpperStringActionResult(string str)
{
this.str = str;
}
public override void ExecuteResult(ActionContext context)
{
var upperStringBytes =
Encoding.UTF8.GetBytes(str.ToUpper());
context.HttpContext.Response.Body.Write(
upperStringBytes, 0, upperStringBytes.Length);
}
}
What is encoding? Encoding is basically a process in which a sequence of characters is put into a specialized format. The characters could be numerical, alphabet, symbols, and so on. The purpose is to serve efficient transmission and storage. What is UTF-8? UTF-8 is the encoding for the web for efficiency reasons.
- 然后,修改控制器操作,如下所示:
Go to https://goo.gl/DTWzN4 to access the code.
public IActionResult IndexUpper()
{
return new UpperStringActionResult("Hello World! I am learning MVC!");
}
- 然后,运行应用。您将获得以下输出:

正如你所看到的,所有的字母都是大写的。
添加视图
到目前为止,我们正在从控制器返回一个简单的字符串。虽然这解释了Controller和action方法如何工作的概念,但它没有太多实际用途。
让我们创建一个名为Index2的新action方法:
Go to https://goo.gl/UhaHyz to access the code.
public IActionResult Index2()
{
return View(); // View for this 'Index2' action method
}
现在,我们已经创建了返回视图的action方法,但是我们仍然没有添加视图。按照惯例,ASP.NETMVC 将尝试在Views\{ControllerName}\{ActionMethod.cshtml}文件夹中搜索我们的视图。对于前面的示例,它将尝试搜索Views\Home\Index2.cshtml。请注意,controller文件夹的名称是Home,而不是HomeController。前缀仅根据约定需要。由于此文件夹结构和文件不可用,当您尝试通过 URLhttp://localhost:50140/Home/Index2访问此操作方法时,将出现 500 内部服务器错误。
那么,让我们创建一个文件夹结构。右键单击解决方案,从关联菜单中导航到添加|新文件夹,创建名为Views的文件夹,然后在Views文件夹中创建名为Home的子文件夹:

右键点击Home文件夹,从关联菜单中导航至添加新项目。将出现一个对话框,如下面的屏幕截图所示。将文件名命名为Index2.cshtml,因为我们的action方法名为Index2。cshtml是使用 C#时使用的 Razor 视图引擎(这将在下一章的视图引擎和Razor 视图引擎部分中详细讨论)扩展。

单击上一屏幕中的添加按钮时,将创建一个名为Index2.cshtml的文件,其中包含以下内容:

@*是 Razor 视图引擎中的注释语法。您可以在@{}块中编写任何 C#代码。
让我们在生成的代码之后添加一个简单的 HTML 块:
<html>
<body>
Hello! This is <b>my first View</b>
</body>
</html>
现在,当您运行应用时,您将获得以下输出:

下图说明了请求流以及我们如何通过视图生成响应:

添加模型
模型代表您的业务领域类。现在,我们将学习如何在控制器中使用模型。创建一个Models文件夹并添加一个简单的Employee类。这只是一个普通的老 C#类:
Go to https://goo.gl/uBtpw3 to access the code.
public class Employee
{
public int EmployeeId { get; set; }
public string Name { get; set; }
public string Designation { get; set; }
}
在我们的HomeController中创建一个新的action方法Employee,并使用一些值创建Employee模型的对象,并将模型传递给视图。我们的想法是使用视图中的员工价值模型将其呈现给用户:
Go to https://goo.gl/r4Jc9x to access the code.
public IActionResult Employee()
{
//Sample Model - Usually this comes from database
Employee emp1 = new Employee
{
EmployeeId = 1,
Name = "Jon Skeet",
Designation = " Software Architect"
};
return View(emp1);
}
现在,我们需要为这个action方法添加相应的视图。在View\Home文件夹中添加一个名为Employee.cshtml的新 Razor 视图文件。
添加以下代码段。@符号后面的任何内容都被视为剃刀代码。在下面的代码中,我们试图访问传递给视图的Model对象的属性。在我们的例子中,Model表示我们在action方法中构造的employee对象。您可以使用Model关键字从视图访问对象:
Go to https://goo.gl/u4gCzN to access the code.
<html>
<body>
Employee Name : @Model.Name <br />
Employee Designation: @Model.Designation <br />
</body>
</html>
当您运行应用并键入 URLhttp://localhost:50140/Home/ Employee时,您将看到以下输出:

Optional: Take Up a Challenge Alter your View code so that it displays EmployeeID.
Try to display a non-existing property such as @Model.Age. What happens when you do it?
Note that we get an error message if we try to access a non-existing property.
将数据从控制器传递到视图
我们刚刚讨论了如何使用Model对象将数据从控制器传递到视图。调用视图时,我们将模型数据作为参数传递。但有时您需要从
控制器向视图传递一些临时数据。此临时数据可能不值得使用model类。在这种情况下,我们可以使用ViewBag或ViewData。
ViewData是字典,ViewBag是相同值的动态表示。
让我们使用ViewBag和ViewData添加 company name 和 company location 属性,如下代码片段所示:
Go to https://goo.gl/oYH7am to access the code.
public IActionResult Employee()
{
//Sample Model - Usually this comes from database
Employee emp1 = new Employee
{
EmployeeId = 1,
Name = "Jon Skeet",
Designation = " Software Architect"
};
ViewBag.Company = "Google Inc";
ViewData["CompanyLocation"] = "United States";
return View(emp1);
}
在Employee.cshtml视图文件中进行相应的更改,以便我们可以显示Company名称和CompanyLocation值:
Go to https://goo.gl/KmqUhx to access the code.
<html>
<body>
Employee Name : @Model.Name <br />
Employee Designation: @Model.Designation <br />
Company : @ViewBag.Company <br />
Company Location: @ViewData["CompanyLocation"] <br />
</body>
</html>
在进行上述更改后运行应用:

ViewBag和ViewData代表同一个集合,尽管集合中的条目是通过不同的方法访问的。ViewBag值是动态值,在运行时执行,ViewData是通过字典访问的。
为了测试这一点,让我们对view文件做一个简单的更改:
<html>
<body>
Employee Name : @Model.Name <br />
Employee Designation: @Model.Designation <br />
Company : @ViewData["Company"] <br />
Company Location : @ViewBag.CompanyLocation <br />
</body>
</html>
尽管Company值是使用Controller中的ViewBag存储的,但我们正在使用ViewData访问它。CompanyLocation值的情况也是如此。我们已经使用ViewData在控制器中存储了该值,但我们正在使用ViewBag访问该值。
在进行上述更改后运行应用时,您将看到与以前相同的结果。
过滤器
ASP.NET MVC 中的过滤器使您能够在执行管道中的特定阶段之前或之后运行代码。它们可以全局配置、按控制器配置或按操作配置。可以将过滤器视为拦截器。
有不同种类的过滤器,每个过滤器在管道中的不同阶段执行。例如,在执行action方法时执行动作过滤器。
让我们用一个简单的例子来看看动作过滤器(一种过滤器)是如何工作的。
我们已经创建了一个简单的控制器,DateController,在这里我们只是显示时间。在这个action方法中,我们使用一个名为ResponseCache的预定义操作过滤器,它在指定的持续时间(以秒为单位)内缓存响应。在下面的代码片段中,我们提到了持续时间为600秒。因此,响应将被缓存 10 分钟:
Go to https://goo.gl/pEBqt6 to access the code.
public class DateController : Controller
{
[ResponseCache(Duration = 600)]
public IActionResult Index()
{
return Content(DateTime.Now.ToShortTimeString());
}
}
当我们第一次运行它时,它会按预期显示时间。但是,当您刷新浏览器(这会间接再次触发请求)时,时间不会更新,因为响应已被应用缓存。
在下面的屏幕截图中,即使时间是 7:43,应用仍显示为 7:40:

现在,我们来看看 ASP.NET Core 中可用的一些预定义类型的过滤器。
以下是一些不同类型的过滤器:
- 授权过滤器:这些过滤器用于授权,主要用于确定当前用户是否被授权进行请求。
- 资源过滤器:这些过滤器在授权后处理请求,是在请求离开过滤管道之前最后处理请求的过滤器。它们用于实现缓存或通过传递过滤器管道。
- 动作过滤器:这些 wrap 调用指向单个
action方法调用,可以操作动作中传递的参数以及从中返回的动作结果。 - 异常过滤器:异常过滤器用于管理 ASP.NET MVC 中未处理的异常。
- 结果过滤器:结果过滤器包装单个动作结果,仅在
action方法成功执行时运行。
With the help of caching we can immediately return results that are calculated previously, thus totally avoiding executing the request-response pipeline. The disadvantage is that we would be showing stale data.
活动:编写自定义筛选器
场景
您需要编写一个过滤器,该过滤器只允许在周日应用的操作。你会怎么做?
瞄准
编写自定义过滤器。
完成步骤
- 打开编辑器并编写以下代码:
Go to https://goo.gl/9QKgbS to access the code.
public class SundayFilter : Attribute, IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
if (DateTime.Now.DayOfWeek != DayOfWeek.Sunday)
context.Result = new ContentResult()
{
Content = "Sorry only on sundays!"
};
}
public void OnActionExecuted(ActionExecutedContext context)
{
// do something after the action executes
}
}
Setting results in the filter causes short circuiting, so our action does not run.
- 现在,我们可以将此属性应用于我们的操作:
Go to https://goo.gl/x1ij7Z to access the code.
[SundayFilter]
public IActionResult Employee()
{
…
…
}
您已成功创建自定义筛选器。
总结
在本章中,我们从头构建了第一个 ASP.NET Core 应用。我们已经了解了控制器如何适应整个 ASP.NET MVC 应用,以及如何使用action方法构建我们的第一个控制器。我们还学习了如何在控制器中使用模型和视图。我们还讨论了使用ViewBag和ViewData将数据从控制器传递到视图的不同方式。我们还学习了 ASP.NET MVC 中的过滤器以及如何使用 ASP.NET Core 中预定义的过滤器。
三、视图
视图是交付给用户的应用的实际输出。它们是用户访问应用时在屏幕上实际看到的内容。所有组件,包括菜单、输入元素、对话框以及用户看到的所有其他内容都来自您的视图。如果您在访问应用时没有提供良好的用户体验,用户将不会关心您的应用有多好。因此,在构建 ASP.NET MVC 应用时,视图起着至关重要的作用。将视图与控制器分离可以使 HTML 设计过程与逻辑分离。这对于控制器的单元测试也是有益的。
在本章结束时,您将能够:
- 解释视图引擎和 Razor 视图引擎的用途
- 在 Razor 视图引擎中编程,并使用不同的编程结构
- 使用 ASP.NET Core 中的布局及其功能
- 生成 HTML 代码
- 创建并调用局部视图
- 创建视图组件
- 创建自定义标记帮助程序
视图引擎和 Razor 视图引擎
正如第 1 章所述,设置舞台,浏览器只能理解 HTML、CSS 和 JavaScript。视图引擎的目的是从视图生成 HTML 代码,并将其发送到浏览器,以便浏览器能够理解内容。主要有两种不同类型的视图引擎:Razor 视图引擎和 Web 表单视图引擎。尽管这两个视图引擎是 ASP.NET MVC 现成的,但您可以使用任何自定义视图引擎。
Razor 视图引擎
Razor 视图引擎是 ASP.NET Core 中的默认和推荐视图引擎。展望未来,它可能是安装 ASP.NET MVC 时唯一现成的视图引擎。
您可以在 Razor 视图中混合使用 C#代码和 HTML 代码,Razor 视图引擎足够智能,能够区分这两种代码并生成预期的输出。在某些情况下,您可能需要向 Razor 视图提供附加信息以生成适当的结果。Razor 代码块以@符号开始,但不需要关闭@。
在 Razor 视图引擎中编程
在 Razor 视图引擎中编程就像在 C#中编程一样。不同之处在于,在 Razor 视图引擎中,您的 C#代码将与 HTML 混合以生成所需的 HTML 输出。
Razor 视图中的变量
您可以在 Razor 块中声明一个变量,并使用@符号使用该变量。
In all the examples in this chapter, we will only present the code samples of the view.
使用 Razor 视图
下面是一个示例,供我们探索 Razor 视图。遵循以下步骤:
- 创建新的空 ASP.NET Core 项目。
- 创建一个
Controllers文件夹和一个名为HomeController的控制器。 - 创建一个名为
Views,的文件夹,一个名为Home的子文件夹,以及一个名为Index.cshtml的视图文件,方法是:右键单击上下文菜单,导航到添加新项目,然后从列表中选择 MVC 视图页面。
According to the pattern of configuration over convention, the controller name must match the appropriate view folder. Hence, we name the controller HomeController and the view folder Home.
- 确保您的
Startup.cs文件如下所示:
Go to https://goo.gl/qzz2aT to access the code.
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit
https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
…
…
}
HomeController.cs文件将具有以下代码:
Go to https://goo.gl/vWxjRq to access the code.
When copying code from the link provided, remember to append it with the closing curly brace for the HomeController class, as shown in the preceding code snippet.
public class HomeController : Controller
{
// GET: /<controller>/
public IActionResult Index()
{
return View();
}
}
接下来是更新的 MVC 视图页面,我们将在其中声明一个变量并使用它。前五行和后两行是简单的 HTML 元素。
我们将集中于粗体的线条。然后,我们将使用@ { … }创建一个 Razor 块,并在其中声明一个变量。剃须刀座以闭合的卷曲支架结束。片段Value:被认为是简单的 HTML 文本。由于我们希望使用 Razor 变量值,我们将使用@i来指示 Razor 视图引擎i不是正常的 HTML 文本;这是一种剃须刀结构,需要进行相应的处理。完整的 HTML 代码如下所示:
Go to https://goo.gl/Jch17b to access the code.
<html>
<head>
<title> Views demo</title>
</head>
<body>
@{
int i = 5;
}
Value: @i
</body>
</html>
运行应用时,您将看到以下输出:

When you access the Razor variable, you will need to use the @ symbol. Without this, the Razor view engine sees the i variable as text and not as an expression.
下面的屏幕截图是您访问变量时不带@符号的结果:

Razor 视图中的编程构造
您可以在 Razor 视图中使用 C#中提供的大多数编程结构。让我们详细看看其中的一些。
for 循环
为for循环编写代码非常简单。让我们为 for 循环构造编写一段代码for。
下面是 for 循环构造的代码,其中我们循环文件五次并打印变量名:
@{
for (int i = 0; i < 5; i++)
{
<li>@(i + 1)</li>
}
}
以下是需要注意的几点:
- 由于
for循环是 Razor 代码,我们应该以@符号开始循环,以指示后面的代码是 Razor 代码,而不是普通的 HTML。 - 每当我们使用 HTML 元素或标记时,Razor 视图引擎就会返回 HTML 模式。如果要在 HTML 标记中使用 Razor 表达式,则需要再次包含
@符号,以告知 Razor 视图引擎,后面的内容是 Razor 代码,而不是 HTML 元素。这就是我们在前面的表达式中再次使用@符号的原因,即使在父根级代码中也是如此。
Razor is a template engine. We use Razor expressions on the dynamically generated HTML parts.
视图的完整代码如下所示:
<html>
<head>
<title> Views demo</title>
</head>
<body>
<ul>
@{
for (int i = 0; i < 5; i++)
{
<li>@(i + 1)</li>
}
}
</ul>
</body>
</html>
while 循环
让我们为while循环编写一段代码。我们将实现与上一个示例相同的循环。请注意,加粗的表达式增加了变量i。我们不会使用@符号,因为它不在 HTML 元素中。
以下是while循环构造的代码,我们在该文件中循环五次并打印变量名:
@{
int i = 0;
while (i < 5)
{
<li>@(i + 1)</li>
i++;
}
}
foreach 循环
Razor 视图中的foreach循环与 C#中的foreach循环相同。
以下是foreach循环构造的代码,我们在其中初始化一个整数列表,遍历该列表,并将其作为列表项打印:
<ul>
@{
List<int> integers = new List<int>
{
1,2,3,4,5
};
foreach (int i in integers)
{
<li>@i</li>
}
}
</ul>
if 条件
让我们看一个if条件的例子;我们将检查变量的值是否小于 10。如果小于 10,我们将打印 i 小于 10,否则,我们将说 i 大于 10。您可能想知道为什么我们必须包含文本标记以及它的用途。
由于我们在 Razor 视图代码块中,文本 i 小于 10 将被视为 Razor 表达式,但它不是。
此text标记用于指示 Razor 视图引擎,text标记后面的内容将被视为文本,而不是 Razor 表达式。
以下是检查变量值是否小于 10 的if条件代码:
@{
int i = 5;
if (i < 10)
{
<text>i is less than 10</text>
}
else
{
<text>i is greater than 10</text>
}
}
活动:打印从 1 到 100 的素数
场景
你正在做一个计算不同数学公式的项目。你从一个基本的打印素数的开始。
瞄准
编写一个视图,将质数从 1 到 100 打印到浏览器。
完成步骤
将名为IsPrime的函数与@function声明一起使用,如下所示:
Go to https://goo.gl/pCLuFD to access the code.
@functions
{
public bool IsPrime(int n)
{
if (n <= 1) return false;
if (n <= 3) return true;
if (n % 2 == 0 || n % 3 == 0) return false;
for (var i = 5; i * i <= n; i = i + 6)
if (n % i == 0 || n % (i + 2) == 0)
return false;
return true;
}
}
<ul>
…
…
</ul>
布局
在前面讨论的所有示例中,我们在一个文件中完成了完整的视图编码。这导致缺乏灵活性和可重用性降低。
考虑下面的网页结构,其中顶部包含公司徽标或横幅,侧部包含指向网站各个部分的链接。每个页面的内容部分都会更改:

如果我们在一个视图中编写完整的内容,我们可能需要在每个页面上复制顶部和侧面部分。如果我们想更改侧面部分中的任何内容,我们必须更改所有文件。这清楚地表明,单一视图文件不是最佳解决方案。
在这个场景中,布局起到了解救作用,它定义了站点结构,可以在所有网页上重用。布局不需要有顶部或侧面;它可以包含一个简单的 HTML 结构,其中可以包含公共内容,主体内容将从单个视图呈现。
我们可以使用 HTML 框架代替布局。然而,当我们使用 HTML 框架时,每个框架都会变得孤立,看起来像是完全独立的页面。
构建我们的第一个布局
要使用布局,请执行以下步骤:
- 获取布局文件的名称:此信息应在
_ViewStart.cshtml中提供。按照惯例,所有共享文件的名称都将以下划线开头,该文件直接位于Views文件夹下。 - 创建布局文件:按照惯例,文件名为
_Layout.cshtml,将位于Shared文件夹中。所有共享内容(如局部视图)也将在此处提供。部分观点将在本章后面讨论。 - 创建内容视图文件:该视图文件与我们之前创建的视图文件几乎相同,只有一个区别;此文件中只有特定于页面的内容可用,这意味着您在此处没有任何
html,``head或title标记。文件夹结构如下所示:

The project.json file is deprecated and will be missing in new projects. After the creation of _ViewStart.cshtml, _Layout.cshtml, and page-specific view files, the folder structure will look like the preceding screenshot.
创建 _ViewStart.cshtml
_ViewStart文件可用于定义要在每个视图渲染开始时执行的公共视图代码。由于此代码在每个视图的开头执行,因此我们不再需要在任何单个视图文件中显式设置布局。
在“视图”文件夹上单击鼠标右键,然后从关联菜单中选择“添加新项目”。然后,从添加新项目对话框中选择 MVC 视图起始页,如以下屏幕截图所示:

单击“添加”按钮时,它将创建一个包含以下内容的文件:
@{
Layout = "_Layout";
}
正在创建 _Layout.cshtml
在Views文件夹中创建一个名为Shared的文件夹。然后,右键点击Shared文件夹,从关联菜单中选择添加新项目,如下图所示:

当您点击添加按钮时,将创建具有以下内容的_Layout.cshtml:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
</head>
<body>
<div>
@RenderBody()
</div>
</body>
</html>
前面的布局文件是带有几个 Razor 表达式的简单 HTML 内容。@ViewBag.title用于显示从控制器传递的标题信息,@RenderBody是一个 Razor 表达式,它调用特定于页面的视图并合并那里的内容。
添加特定于页面的视图
让我们添加一个特定于页面的视图。在添加视图之前,我们需要在HomeController文件中添加一个操作方法,从中我们将调用页面特定视图。按照以下步骤添加页面特定视图:
- 让我们添加一个名为
Index2的action方法,如下所示:
Go to https://goo.gl/xMK1zK to access the code.
public IActionResult Index2()
{
ViewBag.Title = "This is Index2";
return View();
}
ViewBag用于将信息从控制器传递到视图。在这里,我们将Title信息从动作方法传递到视图。请记住,ViewBag和ViewData是关键值集合,您可以从控制器中填充,并从视图中使用。ViewBag是动态对象,而ViewData是字符串键值对。
- 现在,右键单击 Views 文件夹,导航到 Add | New Item,选择 MVC View Page,并将文件另存为
Index2.cshtml。在生成的视图中,我们添加了简单的Hello文本。此文本将呈现在布局页面的主体中。查看文件的完整代码如下所示:
Go to https://goo.gl/vePWui to access the code.
@*
For more information on enabling MVC for empty projects,
visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}
Hello. This text will be rendered in body of the layout
page
- 一切都安排好了。运行应用并在浏览器中键入 URL
http://localhost:50132/Home/Index2。
The port number after the localhost may vary when you run the application from your PC.
您将获得以下输出:

正如预期的那样,您将看到前面屏幕截图中显示的文本。然而,我们的重点不是文本,而是生成的 HTML 内容的结构。
- 按Ctrl+U(在 Windows 的 Chrome 浏览器上)查看源代码。您将看到以下 HTML 内容:

顶部内容(即html、head、body和div开始标记)和底部内容(即html、body和div结束标记)来自布局文件,文本来自特定于页面的视图。
活动:创建另一个布局并将视图更改为该布局
场景
您已经为您的网站创建了布局。现在,您需要创建另一个布局,并由公司将视图更改为该布局。
瞄准
创建另一个布局,然后在点击action方法时动态地将视图更改为该布局。
完成步骤
- 首先,我们为我们的
action方法编写以下代码:
Go to https://goo.gl/Jo2NQu to access the code.
public IActionResult Index()
{
var view = View();
view.ViewData["Layout"] =
"~/Views/Shared/_Another.cshtml";
return view;
}
- 然后,我们修改我们的
_ViewStart.cshtml文件,如下所示:
Go to https://goo.gl/WQqWvn to access the code.
@{
Layout = (string)ViewData["Layout"] ?? "_Layout";
}
我们已经动态地改变了我们的布局!
生成 HTML
正如第 1 章中所述,设置舞台时,浏览器只能理解 HTML、CSS 和 JavaScript,而不考虑构建 web 应用所使用的技术。在 ASP.NET MVC 中构建应用时也是如此。
大多数应用获取用户输入,处理输入,然后将所需信息存储在数据库中,以便以后检索。在 web 应用的上下文中,HTML 表单元素用于获取用户输入。
HTML 帮助程序和标记帮助程序是在 ASP.NET Core 中生成 HTML 元素的两种方法。
HTML 帮助程序是服务器端方法,有助于生成浏览器可以理解的 HTML 元素。在 ASP.NET MVC 5 之前,HTML 助手一直是生成 HTML 元素的主要方法。
ASP.NET Core 中引入的标记帮助程序也会生成 HTML 元素。标记帮助器(我们将在本章后面的一节中讨论)看起来就像 HTML 元素,您可以在其中添加属性以将它们标识为标记帮助器。与 HTML 帮助程序相比,使用标记帮助程序的优点是用户界面设计者/工程师不需要担心 Razor 代码;它们只是使用 HTML 元素和附加属性编写代码。
在讨论 HTML 助手和标签助手之前,让我们后退一步,讨论为什么我们首先需要它们。
使用简单表单生成 HTML
考虑一个简单的表单,如下面的截图所示,在这里我们希望得到用户的姓名和年龄。如果用户输入的年龄等于或大于 18 岁,我们将显示您有资格投票!如果没有,我们将显示您现在没有资格投票:

以下是显示上述简单表单的 HTML 代码:
Go to https://goo.gl/f59Ep8 to access the code.
<form>
<table>
<tr>
<td>
<label for="txtName">Name</label>
</td>
…
…
<td colspan="2">
<input type="submit" />
</td>
</tr>
</table>
</form>
这种直接编码 HTML 元素的方法既耗时又容易出错。例如,在前面的表单中,标签和输入 HTML 元素在第一组中引用相同的元素(txtName,在第二组中引用相同的元素txtAge。如果我们手工编写 HTML 元素的代码,则在构建 HTML 元素时可能会出现打字错误。
HTML 助手
HTML 助手是为您生成 HTML 的服务器端方法。
使用 HTML 帮助程序生成表单
我们可以使用 HTML 助手生成相同的表单,如下所示(HTML.BeginForm、@Html.Label和@Html.TextBox分别生成 HTML 表单、标签和文本框元素):
@using (Html.BeginForm())
{
<table>
<tr>
<td>@Html.Label("Name")</td>
<td>@Html.TextBox("txtName")</td>
</tr>
<tr>
<td>@Html.Label("Age")</td>
<td>@Html.TextBox("txtAge")</td>
</tr>
<tr>
<td colspan="2"><input type="submit" value="Submit">
</td>
</tr>
</table>
}
The form tag will automatically close when you use the block.
以下屏幕截图简要介绍了 HTML 助手的任务:

您可能想知道,当我们可以手动编写 HTML 代码时,为什么需要使用 HTML 助手。当我们将模型从控制器传递到视图时,事情会变得更加复杂。使用 HTML 帮助程序,我们可以直接从模型文件中构建表单元素,以便它们从您使用的模型中选择名称。
例如,让我们创建一个名为Models的文件夹和一个名为Person的类。此类将充当模型,如以下屏幕截图所示:

Person类只是一个POCO(普通旧 C#Object类)类,将作为一个模型。此类的完整代码如下所示:
Go to https://goo.gl/UBmSdM to access the code.
public class Person
{
public int PersonId { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
让我们创建一个名为ValidateAge的新action方法。在这个方法中,我们将创建一个空的Person类,并将模型传递给视图。我们还将在ViewBag中创建一个名为Title的动态属性,以便在视图中显示该值:
Go to https://goo.gl/xCkyeY to access the code.
[HttpGet]
public IActionResult ValidateAge()
{
ViewBag.Title = "Validate Age for voting";
Person person1 = new Person();
return View(person1);
}
在视图中,使用以下 HTML 帮助程序创建表单:
Go to https://goo.gl/hnkvzS to access the code.
@model Lesson3.Models.Person
@using (Html.BeginForm("ValidateAge", "Home", FormMethod.Post))
{
<table>
<tr>
<td>@Html.LabelFor(Model => Model.Name) </td>
<td>@Html.TextBoxFor(Model => Model.Name) </td>
</tr>
<tr>
<td>@Html.LabelFor(Model => Model.Age)</td>
<td>@Html.TextBoxFor(Model => Model.Age)</td>
</tr>
<tr>
<td colspan="2"><input type="submit" value="Submit">
</td>
</tr>
</table>
}
在第一行中,我们告诉视图我们正在传递类型为Person类的模型。这使我们能够使用强类型模型,即当我们键入Model和一个点时,IntelliSense为我们提供了Person类的所有属性。
Strongly typed model is beneficial, besides IntelliSense because if in the future you rename the properties of your model, you will detect these errors at compile time.
在第二行中,我们使用重载的BeginFormHTML 帮助程序,它接受三个参数:操作方法名称、控制器名称和Form方法。
简单地说,当用户提交表单时,信息应该传递给控制器的上述操作。
在LabelFor和TextBoxForHTML 帮助程序中,我们只是传递模型属性(名称和年龄);这将自动查询和获取模型属性,并构建相应的 HTML 元素。这是使用 HTML 帮助程序的主要优点之一。如果不使用 HTML 帮助程序,此过程可能会变得复杂。
现在,让我们以同样的方式编写相应的POST动作方法。在下面的POST动作方式中,根据表单中输入的年龄,我们将动态属性设置为Message:
Go to https://goo.gl/e4cB6j to access the code.
[HttpPost]
public IActionResult ValidateAge(Person person1)
{
if (person1.Age >= 18)
{
ViewBag.Message = "You are eligible to Vote!";
}
else
{
ViewBag.Message = "Sorry.You are not old enough to vote!";
}
return View();
}
我们使用 POST 而不是 GET,因为我们的目标不是检索数据,而是在服务器端发布和处理数据。
需要注意的是,GET和POST动作方式都引用了相同的视图—ValidateAge.cshtml。将以下内容添加到表单元素正上方的视图中:
Go to https://goo.gl/6Cx4fg to access the code.
@if (ViewBag.Message != null)
{
<b>@ViewBag.Message</b>
}
用户提交表单后,POST action 方法在ViewBag中设置动态Message属性。但是,当视图作为GET请求的一部分呈现时,此属性的值将为 null。如果该值不为 null,请在页面顶部插入消息。
运行应用时,将获得以下输出:

活动:使用复选框
场景
网页的字段已准备就绪。现在,要求您使用年龄选项的复选框,而不是输入年龄,如图所示:

瞄准
使用年龄选项的复选框,而不是输入年龄。
完成步骤
- 使用以下代码更改视图:
Go to https://goo.gl/WtzFSg to access the code.
@model Activity3C1.Models.Person
@if (ViewBag.Message != null)
{
<b>@ViewBag.Message</b>
}
@{
}
@using (Html.BeginForm("ValidateAge", "Home",
FormMethod.Post))
{
<table>
<tr>
<td>@Html.LabelFor(Model => Model.Name) </td>
<td>@Html.TextBoxFor(Model => Model.Name) </td>
</tr>
…
…
</table>
}
- 使用以下代码更改您的操作:
Go to https://goo.gl/Fqkrg3 to access the code.
[HttpPost]
public IActionResult ValidateAge(Person person1)
{
if(Convert.ToBoolean(
Request.Form["OlderThan18"][0]))
{
ViewData["OlderThan18"] = true;
ViewBag.Message = "You are eligible to Vote!";
}
else
{
ViewBag.Message = "Sorry.You are not old enough to vote!";
}
return View();
}
我们使用Request.Form["OlderThan18"][0]的原因是,默认情况下,复选框助手创建一个隐藏表单,这意味着如果复选框未选中,我们将得到一个假值。由于默认情况下 HTML 不会为未选中的复选框发送值,因此我们也会填充ViewBag,以便保留值。
局部视图
局部视图是可以在整个应用中重用的视图。可以将局部视图视为可插入、可重用的块,您可以从任何位置调用这些块,并显示局部视图的内容。布局和局部视图之间的区别在于布局围绕着我们的页面,而局部视图包含在我们的页面中。
考虑下面的网页结构,它是我们以前使用过的布局页面,但是有两个变化。最新的新闻块已添加到侧面部分,登录块已添加到顶部部分。这些块不限于顶部部分或侧面部分,可以在应用中的任何位置使用,包括内容部分,如下图所示:

这些局部视图不限于静态内容,可以包含表单元素。在前面的屏幕截图中,最新新闻部分视图包含文本内容,登录部分视图包含获取电子邮件 ID 和密码的表单元素。
框架不限制局部视图的位置。但是,按照惯例,如果您的局部视图仅由控制器使用,则可以在控制器特定的Views文件夹中创建该局部视图。例如,如果您的局部视图仅在HomeController文件中使用,您可以在Home下的Views文件夹中创建该局部视图。
让我们来看看如何创建和使用部分视图。
如前所述,局部视图与普通视图一样。因此,我们将以与创建普通视图相同的方式创建局部视图。
右键单击共享文件夹并导航到添加|新项目。按照惯例,与所有共享内容一样,局部视图的名称也将以“u”(下划线)开头,如以下屏幕截图所示:

We are creating this partial view based on the assumption that it can be used from anywhere in the application.
要在生成的局部视图中添加简单的静态内容文本和简单表,请使用以下代码段:
<b>This content and below table is coming from partial view</b>
<table border="1">
<tr>
<th>Employee No</th>
<th>Employee Name</th>
</tr>
<tr>
<td>10012</td>
<td>Jon Skeet</td>
</tr>
<tr>
<td>10013</td>
<td>Scott Guthrie</td>
</tr>
</table>
调用局部视图
可以使用@Html.PartialHTML 帮助程序调用局部视图。
在本例中,我们将调用Index2.cshtml文件中的部分视图。传递的参数将是部分文件的名称。它将按该名称搜索局部视图,并将完整内容作为Index2.cshtml文件的一部分呈现。
Index2.html文件的内容如下:
Go to https://goo.gl/1nRe4M to access the code.
Hello. This text will be rendered in body of the layout page<br />
<br />
<br />
@Html.Partial("_PartialHelloWorld")
现在,运行应用并访问 URLhttp://localhost:50132/Home/Index2。您将看到以下输出:

另外,通过右键单击并选择Peek Definition查看@Html.Partial的其他重载。这将允许您通过ViewBag的模型或副本。我们之所以称之为副本,是因为在局部视图中对包所做的更改不会传播回父视图。
活动:使用静态数据
场景
您正致力于在项目中实现 MVC 模式。您希望从动作生成静态数据,并将其作为模型传递给局部视图。
瞄准
从动作生成静态数据,并将其作为模型传递给局部视图。
完成步骤
- 我们首先创建一个
Employee模型对象:
Go to https://goo.gl/j9N4BV to access the code.
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
}
- 然后,我们从操作返回数据:
Go to https://goo.gl/hAuXfB to access the code.
public IActionResult Index2()
{
var employees = new List<Employee>
{
new Employee { Id = 10012 , Name = "John Skeet"},
new Employee { Id = 10013 , Name = "Scott Guthrie"},
};
return View(employees);
}
- 然后我们将数据从
Index2.cshtml传递到局部视图,如下所示:
Go to https://goo.gl/ZYirD8 to access the code.
@model List<Activity3D1.Models.Employee>
Hello. This text will be rendered in body of the layout
page<br /> <br /> <br />
@Html.Partial("_PartialHelloWorld", @Model)
- 最后,从局部视图呈现 HTML:
Go to https://goo.gl/cj2aeR to access the code.
@model List<Activity3D1.Models.Employee>
<b>This content and below table is coming from partial
view</b>
<table border="1">
<tr>
<th>Employee No</th>
<th>Employee Name</th>
</tr>
@{
foreach (var employee in Model)
{
<tr>
<td>@employee.Id</td>
<td>@employee.Name</td>
</tr>
}
}
</table>
视图组件
视图组件是 ASP.NET Core 中引入的一项新功能。它们类似于局部视图;然而,它们在本质上更强大。
使用局部视图时,对控制器有依赖关系。但是,当您使用ViewComponent属性时,您不必依赖于控制器。因此,我们能够建立关注点分离,并具有更好的可测试性。尽管仍然支持现有的部分视图 HTML 帮助程序,但在使用.NET Core 时,如果希望显示可重用的信息,最好使用视图组件。
创建视图组件
可以使用以下任一方法创建视图构件:
- 通过从
ViewComponent属性派生一个类来创建一个类 - 增强具有[
ViewComponent属性的类,或从具有[ViewComponent属性的类派生该类 - 通过创建以后缀
ViewComponent属性结尾的类来使用约定
无论选择哪个选项,视图组件都应该是公共、非嵌套和非抽象类。
与控制器一样,您也可以在ViewComponent属性中使用依赖项注入(通过构造函数)。由于ViewComponent属性与控制器生命周期分离,您可能无法使用ViewComponents中的操作过滤器。
有一个名为Invoke(或InvokeAsync,异步等价于Invoke)的方法,它将返回IComponentViewResultinterface。此方法类似于控制器返回视图的action方法。
创建 ViewComponent 属性
按照以下步骤创建一个ViewComponent属性:
- 在项目中创建一个名为
ViewComponents的新文件夹和一个名为SimpleViewComponent,的新类,如下图所示:

我们创建的SimpleViewComponent文件如下所示:
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Lesson3.ViewComponents
{
public class SimpleViewComponent : ViewComponent
{
public IViewComponentResult Invoke()
{
...
...
}
}
}
Go to https://goo.gl/4KAXW8 to access the code.
- 我们只有两种方法:一种是填充数据,另一种是
Invoke方法,我们将在其中渲染视图。创建了ViewComponent属性后,需要在Views下的_ViewImports.cshtml文件中包含ViewComponent名称空间,以便ViewComponents属性可用于所有视图。以下突出显示的代码段将添加到视图中:
Go to https://goo.gl/pK9tau to access the code.
@using Lesson3
@using Lesson3.ViewComponents
@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"
- 我们已经创建了
ViewComponents并将其提供给所有视图。HomeController文件中的一个简单动作方法只是返回视图:
Go to https://goo.gl/EYmx7M to access the code.
public ActionResult Sample()
{
return View();
}
- 在关联视图中,我们可以调用组件,如以下代码段所示:
Go to https://goo.gl/cbQnR3 to access the code.
<p>
This is a sample web page <br />
<div>
@await Component.InvokeAsync("Simple")
</div>
</p>
调用组件时,它将在以下两个文件夹中搜索:
- 视图组件的默认视图名称为
Default,这使得您的视图文件名为Default.cshtml。因此,我们需要在Views\Shared\Simple\Default.cshtml文件夹中创建Default.cshtml文件,如下图所示:

Go to https://goo.gl/ELUVkG to access the code.
在ViewComponent文件的视图(即Default.cshtml文件)中,我们只是迭代模型中的项,并将它们显示为无序列表项,如下代码所示:
@model IEnumerable<string>
<h3> Sample list</h3>
<ul>
@foreach (var item in Model)
{
<li>@item</li>
}
</ul>
当您运行应用并访问 URL(http://localhost:50132/Home/Sample时,您应该会看到以下输出:

第一行,这是一个示例网页,来自父视图文件(sample.cshtml,而后续列表来自ViewComponent属性。
ViewComponent属性通常在视图中引用。但是,如果您想直接从控制器调用ViewComponent,您可以这样做。
我们调用Sampleaction 方法直接调用简单的ViewComponent,而不是通过另一个视图调用,如下所示:
Go to https://goo.gl/X6e2Xm to access the code.
public ActionResult Sample()
{
return View("Simple");
}

因此,与旧的 HTML 局部视图相比,这些视图组件具有更大的灵活性和特性,例如依赖项注入。这确保了视图组件是可单独测试的。
活动:将字符串作为附加数据传递
瞄准
在调用时通过使用视图组件的一个重载将附加数据传递给视图组件。因此,让我们将Four字符串作为附加数据传递,以便输出如以下屏幕截图所示:

完成步骤
- 我们将我们的观点修改如下:
Go to https://goo.gl/rxkDYt to access the code.
<p>
This is a sample web page <br />
<div>
@await Component.InvokeAsync("Simple", new { additionalData = "Four" } )
</div>
</p>
- 然后,修改我们的
ViewComponent如下:
Go to https://goo.gl/XhCkmZ to access the code.
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Lesson3.ViewComponents
{
public class SimpleViewComponent : ViewComponent
{
…
…
}
}
标记助手
标记助手是 ASP.NET Core 中的一项新功能;它们帮助生成 HTML 元素。在 HTML 助手中,我们将编写 C#/Razor 代码来生成 HTML。这种方法的缺点是许多前端工程师不知道 C#/Razor 代码;它们使用简单的 HTML、CSS 和 JavaScript。标记帮助程序看起来就像 HTML 代码,但具有服务器端呈现的所有功能。您甚至可以根据需要构建自定义标记帮助器。Razor 优于标记助手的优点是,虽然标记助手对前端开发人员更友好,但有时我们可能需要 Razor 的强大功能,因为它是一个强大的编程模型
让我们来看看如何使用标签助手。标签助手包已包含在Microsoft.AspNet.Core.All NuGet包中。
请记住,我们已经在上一节的ViewImports文件中添加了标记帮助程序支持。
如果我们将_ViewImports.cshtml文件包含在Home文件夹下,则标记帮助程序将仅适用于主文件夹下的视图。因此,我们应该使用视图文件夹的根目录。
让我们在HomeController文件中添加一个名为Index3的简单操作方法,在关联视图中,我们将使用标记帮助器,如下代码所示:
Go to https://goo.gl/zwYvmh to access the code.
public IActionResult Index3()
{
ViewBag.Title = "This is Index3";
Person person = new Person();
return View(person);
}
为Index3动作方式添加相应的视图Index3.cshtml文件,代码如下:
Go to https://goo.gl/s545Nw to access the code.
@model Lesson3.Models.Person
<form asp-controller="Home" asp-action="Index3">
<table>
<tr>
<td><label asp-for ="Name">Name</label></td>
<td><input asp-for="Name" /></td>
</tr>
<tr>
<td><label asp-for ="Age">Age</label></td>
<td><input asp-for ="Age" /></td>
</tr>
<tr>
<td colspan="2"><input type="submit" value="Submit"/></td>
</tr>
</table>
</form>
以下是在前面代码中使用标记帮助程序时需要注意的几点:
- 所有表单元素看起来就像标准的 HTML 元素,只是对属性做了一些更改。这使前端开发人员能够独立工作,而无需学习 HTML/Razor 代码,从而更容易实现关注点分离。
- 前面视图的第一行指示从控制器传递到视图的模型数据的类型。
form元素有两个属性,asp-controller和asp-action,分别表示控制器名称和动作方法名称。label和input标记帮助器就像 HTML 元素一样,只是增加了一个asp-for属性。这些属性的值表示模型特性。输入这些属性的值时,可以利用 IntelliSense。
自定义标记帮助程序
ASP.NET Core 提供了许多内置标记帮助程序,帮助您为许多场景创建必要的 HTML 元素。但是,它们并不涵盖所有场景。有时,您可能希望对生成的 HTML 元素进行一些更改,或者您可能希望创建具有新属性的 HTML 元素,或者创建一个新的 HTML 元素。您不限于在 ASP.NET 中使用现有的标记帮助程序。NET Core 应用。如果现有的标记帮助程序不适合您的需要,您可以创建自己的标记帮助程序。让我们创建一个简单的标记帮助器来创建电子邮件链接:
<a href="mailto:mugil@dotnetodyssey.com">
有两种方法可以创建标记帮助器:实现ITagHelper接口或继承TagHelper类。TagHelper类有一个Process方法,您可以重写该方法来编写自定义标记帮助程序。TagHelper类还有TagHelperOutputparameter,您可以使用它编写和生成所需的输出 HTML。因此,最好通过继承TagHelper类来创建标记帮助器。
我们的目标是编写一个自定义的电子邮件标记帮助器,以便当有人使用该标记帮助器时,它被转换为以下代码行:
<a href="mailto:mugil@greatestretailstore.com">Drop us a mail</a>
创建自定义标记辅助对象
以下是在 ASP.NET Core 应用中创建自定义标记帮助器所需执行的步骤:
- 创建一个名为
TagHelpers的文件夹,并添加一个名为EmailTagHelper.cs的新项目。根据约定,所有标记帮助器类都应以TagHelper,结尾,即使我们可以覆盖此约定:

- 创建文件后,需要重写
Process方法以生成所需的 HTML 输出:
Go to https://goo.gl/xNJoqB to access the code.
using Microsoft.AspNetCore.Razor.TagHelpers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Lesson3.TagHelpers
{
public class EmailTagHelper : TagHelper
{
public override void Process(TagHelperContext context,
…
…
}
}
上述代码中使用的参数解释如下:
- 我们已经创建了电子邮件标签助手。现在,我们必须使它对我们的视图可用,以便我们可以在视图中使用标记辅助对象。编辑
Views\_ViewImports.cshtml以包含TagHelpers的名称空间,并添加关联的标记帮助程序。在以下_ViewImports.cshtml文件中,我们添加了以粗体突出显示的内容:
@using Lesson3
@using Lesson3.ViewComponents
@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"
@addTagHelper "*, Lesson3"
下一行中的*符号告诉视图引擎在 Lesson3 命名空间中包含所有的TagHelpers:
@addTagHelper "*, Lesson3"
您只能包含具体的TagHelpers.例如,以下行将只包含EmailTagHelper,以便我们的视图可以使用它:
@addTagHelper " Lesson3.TagHelpers.EmailTagHelper, Lesson3"
- 让我们在家庭控制器中创建一个简单的操作方法。在关联操作方法的视图中,我们将使用电子邮件标记帮助器:
public IActionResult AboutUs()
{
return View();
}
以下是前面AboutUs动作方式的视图:
<h3>About Us</h3>
We are one of the biggest electronics retail store serving
millions of
people across the nation. blah.blah. blah <br />
If you want to hear great offers from us
<email mailTo="mugil@greatestretailstore.com"></email>
- 当您运行应用并访问
http://localhost:50132/Home/AboutUsURL 时,您将看到以下输出:

在这里,我们创建了一个锚定标记,该锚定标记具有mailto属性,电子邮件值作为href属性值。
我们打开开发者工具窗口(按F12进行此操作并选择 DOM Explorer 选项卡)以查看生成的 HTML。
You can find more samples at https://github.com/dpaquette/TagHelperSamples.
活动:替换电子邮件标记帮助程序
场景
您正在为您的公司维护一个应用。该公司希望从代码中删除所有的电子邮件标签助手。你打算怎么做?
瞄准
更换EmailTagHelper以便我们可以按照如下方式使用它:
<email> mugil@greatestretailstore.com </email>
完成步骤
使用以下代码:
Go to https://goo.gl/VCdwpB to access the code.
We can still use HTML attributes on Tag Helpers, as in:
<email style="color:red" mailTo="mugil@greatestretailstore.com"></email>
This will render the link in red.
public class EmailTagHelper : TagHelper
{
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
...
...
}
总结
在本章中,您了解了什么是视图引擎以及如何使用 Razor 视图引擎构建视图。我们还讨论了可以在 Razor 中使用的不同编程结构,以生成所需的 HTML 输出。然后,您了解了布局以及如何在 ASP.NET MVC 应用的所有页面上提供一致的站点结构。在本章的后面,我们通过一个示例讨论了如何使用局部视图来提高可重用性。最后,您学习了如何使用标记帮助程序生成干净的 HTML。
四、模型
数据是每个应用的核心。用户将数据输入应用,编辑输入的数据,然后搜索数据。我们甚至可以说,我们构建的应用只是我们对应用数据执行的操作的接口。因此,任何框架都绝对有必要提供一种机制,使数据操作更容易和更易于管理。ASP.NET MVC 中的模型用于表示业务域数据。
在本章结束时,您将能够:
- 解释模型及其用途
- 创建一个简单的模型,并在 ASP.NET MVC 应用的控制器和视图中使用它
- 创建特定于 ViewModel 的模型
- 在模型和 ViewModels 上下文中使用 ASP.NET MVC 应用中的数据流
- 解释实体框架的目的及其特点和优点
- 使用实体框架添加、更新和删除数据
- 在 ASP.NET MVC 应用中使用实体框架
模型简介
模型是简单的POCO(普通的旧 C#Objects类)类,代表您的业务领域数据。它们基本上模拟真实世界的实体。我们可以将它们视为真实世界概念和实体的代码反射。对于电子商务业务,模型类应为Product、Order和Inventory。如果你正在为一所大学申请,示范班应该是Student、Teacher和Subject。模型表示应用中的业务域数据,它们不知道应用中正在使用的底层数据库。事实上,您甚至不需要数据库来处理模型。
创建 ASP.NET Core 应用
以下是创建 ASP.NET Core 应用的步骤:
- 确保使用空模板创建 ASP.NET Core 应用。
- 创建一个
Controllers文件夹,用一个Index动作方法创建一个HomeController。 - 为 ViewModel 创建以下文件夹/文件:
Views:此文件夹位于您的项目中。Views\_ViewStart.cshtml:标识Layout文件的名称。Views\Shared:此文件夹保存应用的所有共享视图组件。Shared\_Layout.cshtml:此文件标识 web 应用结构的外观。Views\Home:此文件夹包含您的HomeController的所有视图。Views\Home\Index.cshtml:这是HomeController索引动作方式对应的视图。
- 并确保您的
Startup.cs如下所示:
Go to https://goo.gl/edbYJx to access the code.
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit
https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices( IServiceCollection services)
…
…
}
现在,我们已经创建了一个带有控制器和视图的 ASP.NET Core 应用。
让我们在应用中创建一个Models文件夹;此文件夹将包含所有模型文件。在实际应用中,此文件夹和相应的模型文件将位于不同的项目中。为了简单起见,我们将Models文件夹及其文件保存在同一个项目中。
让我们在Models文件夹中创建一个简单的Product模型类:
public class Product
{
public int ProductId { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
这个Product模型类与任何其他 C#类没有什么不同,它包含一些关于产品的属性。
更新HomeController中的Index动作方法,使用Product模型,如下代码片段所示。我们正在构建模型数据,并将模型数据传递给视图,以便向用户显示。但是,不建议在控制器的操作方法中构建模型数据,因为它违反了关注点分离。仅为了简单起见,我们在 action 方法中构建模型数据:
Go to https://goo.gl/Pobwwi to access the code.
public IActionResult Index()
{
/* Build the products model. It is NOT RECOMMENDED to build models in Controller action methods like this.
* In real world application, these models and the respective Data Access Layer(DAL) would be in separate projects.
* We are creating it here to make things simpler to explain */
List<Product> Products = new List<Product>
{
…
…
};
return View(Products);
}
更新相应的Index视图方法,在每个产品中使用模型数据循环,并将其显示为无序列表项。第一行中的@model表示模型元数据,即传递给视图的数据类型。foreach循环中的模型表示实际数据本身,即我们案例中的产品列表:
When copying from GitHub remember to remove the comments before the return statement and the curly brace.
@model List<Lesson4.Models.Product>
<ul>
@foreach (var Product in Model)
{
<li>@Product.Name</li>
}
</ul>
运行应用时,将获得以下输出:

我们已经成功地创建了一个模型,并在控制器和视图中使用了它。
让我们创建一个相对复杂的模型类,Order(Models文件夹中的Order.cs,其中包含产品及其总量的列表:
Go to https://goo.gl/p97rGp to access the code.
public class Order
{
public int OrderId { get; set; }
public List<Product> Products { get; set; }
public decimal Total { get; set; }
}
对于Total字段,我们选择十进制而不是双精度,因为为了提高效率,双精度在内存中存储为二进制。然而,这使得一些十进制数字无法表示,因此计算机会进行一些舍入来近似。另一方面,十进制的存储方式和我们人类的计算方式完全一样,所以十进制对于货币价值来说很有用。双倍有利于科学计算。
以下是您的文件夹结构的外观:

现在我们必须更新Index动作方法以使用Order模型。一旦我们建立了产品列表,我们就将产品列表分配给Order属性,并计算订单的总成本。这些计算通常作为业务层的一部分进行。同样,为了简单起见,我们在这里构建数据模型和计算;在现实世界的应用中永远不应该出现这种情况。
粗体突出显示的代码是我们在操作方法中所做的更改:
// GET: /<controller>/
public IActionResult Index()
{
…
…
};
Order order = new Order();
order.Products = Products;
order.Total = Products.Sum(product =>
product.Price);
return View(order);
}
视图将更新以适应模型更改。模型元数据(@model已更改,表示Order的信息已传递给视图,而不是产品列表。
然后,我们以表格形式显示产品列表。请注意,所有模型数据(本例中为Order对象及其属性)都可以通过模型访问。例如,Products类可以通过Model.Products访问,Total的值可以通过Model.Total获取:
Go to https://goo.gl/efhye6 to access the code.
@model Lesson4.Models.Order
<table border="1">
<tr>
<th>Product Name</th>
<th>Price</th>
</tr>
@foreach (var Product in Model.Products)
...
...
</table>
运行应用时,您将看到以下输出:

特定于视图组件的模型
在某些情况下,您可能只想更新大型模型中的几个属性,或者希望基于几个模型创建新模型。在这种情况下,最好创建特定于视图的新模型。
例如,假设我们正在构建一个屏幕,您可以在其中更新产品的价格。这个简单的屏幕可能只包含三个属性:产品 ID、产品名称和产品价格。但是产品的模型可能包含 30 多个属性来保存产品的所有细节,例如制造商、颜色和尺寸。我们不需要发送包含所有属性的完整模型,而是可以创建一个特定于此视图的新模型,该模型只包含几个属性 ID、名称和价格。
视图模型
ViewModels 是更新模型时,视图将自动更新的实体,反之亦然。在许多在线文章中,甚至在一些书籍中,作者提到了视图模型,而实际上它们指的是特定于视图的模型。
在 ViewModels 中,绑定是双向的:当您更新模型或视图时,另一个将自动更新。
Let us consider a simple example. You have a form with various fields on the left-hand side and print preview on the right-hand side. In this case, whatever you type in real time in the form will be reflected immediately on the right-hand side. In such cases, you can use pure ViewModels when you type; your ViewModel will be updated and that ViewModel will be consumed in the right-hand side print preview. These pure ViewModels are being used in advanced JavaScript frameworks such as KnockoutJS and AngularJS.
在特定于视图的模型中,我们仅以一种方式进行绑定,即从模型到视图。这里,我们将发送一个特定于视图的模型,而不是通用模型(表示业务域类)。
然而,在本课程中,为了简洁起见,我们将特定于视图的模型称为 ViewModels。除非另有规定,否则应将所有视图模型作为特定于视图的模型来读取。所以,我犯了其他作者犯的同样的错误(我不打算这么做)。
Using ViewModels is entirely optional.
使用 ViewModels 有几个缺点。首先,必须为 ViewModel 创建一个新类。其次,您需要编写从 ViewModel 到 view 的转换代码,反之亦然。有一些框架可以自动化这个过程,比如 AutoMapper。
ViewModels 在任何有意义的地方都是很好的实践,因为它减少了耦合。
关于模型的数据流
以下框图显示了 ASP.NET MVC 应用中的数据流:

数据流的两个重要方面如下所示:
- 数据源:表示您的应用数据。应用数据可以驻留在任何地方,从成熟的 RDBMS(如 SQL Server)到简单的 Excel 电子表格,或者介于两者之间的任何东西。
- 模型:如前所述,这些模型代表应用的业务域数据,与所使用的数据源无关。同一模型可用于不同的数据源。
我们可以使用我们视图中的模型来获取数据或呈现数据。在某些视图中,可能不需要模型的所有属性。因此,我们不是将整个模型发送到视图,而是创建特定于视图的模型,并在视图中使用它们。这使事情变得更简单。
以下是使用模型在 ASP.NET Core 中存储或检索记录时发生的高级事件序列:
-
用户在应用中以表单(使用视图创建)的形式输入数据。表单中的字段不需要表示完整的模型,因为我们只需要模型中的一些属性。
-
输入的数据被传递到发生模型绑定的控制器。模型绑定是将在视图中输入的数据映射到模型或 ViewModel 的过程。
-
如果在 ViewModel 中接收到数据,那么我们将把 ViewModel 转换为模型。
-
最后,模型数据存储在数据源中。
Until now, we have been handling only in-memory data in our application. In almost all real-world applications, some form of the database will be used for data storage, access, and retrieval. In the next section, we will discuss Entity Framework (ORM framework), which makes data access simpler from a .NET application.
活动:修改代码以显示总折扣
场景
您希望修改代码,使其在总额大于 1000 的情况下将总额折扣 10%,并在总额中显示此折扣。
瞄准
修改代码以显示总折扣。
完成步骤
- 更改
Order类,如下所示:
Go to https://goo.gl/Q58VD2 to access the code.
public class Order
{
public int OrderId { get; set; }
public List<Product> Products { get; set; }
public decimal Total { get; set; }
public decimal Discount => Total > 1000M ? Total * 0.1M : Total;
}
- 更改视图,如下所示:
Go to https://goo.gl/ScepWa to access the code.
@model Lesson4.Models.Order
<table border="1">
<tr>
<th>Product Name</th>
<th>Price</th>
</tr>
@foreach (var Product in Model.Products)
{
<tr>
<td>@Product.Name</td>
<td>@Product.Price</td>
</tr>
}
...
...
</table>
模型绑定
模型绑定是将来自视图的模型数据映射到控制器中动作方法的 ViewModel 参数的过程。
模型绑定消除了手动读取表单数据并将其分配给现有对象的需要。这将是非常乏味和容易出错的。还可以增强和自定义模型绑定。它是自动解析传入请求的强大机制。
让我们考虑一个简单的表单,有两个表单字段:提交表单时,这些值将映射到控制器动作方法的 ViewModel 对象。模型绑定负责这个映射。模型绑定器在表单字段、查询字符串和请求参数中查找匹配项。
在前面的示例中,ModelBinder 将无任何问题地拾取具有这些属性的任何类。
由于以下Person类包含Name和EmailID属性,因此模型绑定器不会抱怨使用此模型映射表单中输入的值:
public class Person
{
public string Name { get; set; }
public string EmailID { get; set; }
}
下面的代码片段展示了如何在 action 方法中使用Person类:
public ActionResult Add(Person p)
{
return View();
}
实体框架
如果我们使用关系数据库,那么数据和域类之间存在阻抗不匹配,因为数据是关系的,而域是由对象组成的。使用 ORM 的目的是消除(或隐藏)这种不匹配,这样我们就可以完全忽略持久性问题,转而关注代码,而不是试图生成讨厌的 SQL 语句。话虽如此,仍有许多有效的案例可以回溯到 SQL 语句,例如性能调优案例或复杂报告。
实体框架(EF)是对象关系映射(ORM)框架,使开发人员能够直接处理特定领域的对象进行数据访问,而不是处理数据库查询。这大大降低了应用数据访问层的代码复杂性。
在讨论实体框架及其功能之前,让我们暂停片刻,思考一下在使用 ADO.NET 时尝试将某些信息保存到数据库时所遵循的步骤:
- 构造业务域对象。
- 创建到数据库的连接。
- 打开连接。
- 与命令类型一起创建命令对象。
- 将业务域对象的属性添加到命令对象的参数中。
- 执行将数据保存到数据库中的命令。
对于常见的操作,例如将一段数据保存到数据库中,我们必须遵循以下六个步骤。
如果您使用的是 ORM 框架,如实体框架,则只需要三个步骤:
- 构造业务域对象。
- 为您的业务域对象创建
DbContext类。DbContext类的实例表示与数据库的会话。 - 使用
DBContext类的实例将其保存到数据库中。
你可能想知道这怎么可能。
事实上,在后台,Entity Framework 创建到数据库的连接并执行查询以将业务域对象保存到数据库。为了简单起见,Entity Framework 为您编写所有数据访问代码,以便您能够集中精力实现应用的业务功能,而不是编写数据库层代码。
实体框架独立于 ASP.NET MVC
如前所述,Entity Framework 是用于访问数据的 ORM 框架,独立于 ASP.NET MVC。实体框架可以应用于 Type T0. Windows 通信基金会 Ty1 T1(MultT2WCF PosiT3)服务、Web API 服务,甚至在控制台应用中。您可以在任何类型的应用中使用实体框架,并利用它使用对象访问数据。实体框架的概念和功能保持不变,而与您使用的应用类型无关。
现在,我们将在控制台应用中使用实体框架。这使我们能够专注于手头的任务并演示实体框架的功能,而不是处理 ASP.NET Core 应用的样板代码。在本章后面的部分中,我们将集成实体框架和 ASP.NET Core 应用。
SQL server 实体框架的最新版本是 EntityFrameworkCore。与以前的版本(Entity Framework 6)相比,它带来了显著的变化。但是,在构建 ASP.NET Core 应用时,EntityFrameworkCore 是推荐的版本,因此我们将在本书中使用该版本。
We need a database to explain many of the features of Entity Framework. Before continuing, please use the following link to install SQL Server 2016 Express or (LocalDB)j or newer on your PC: https://www.microsoft.com/en-us/sql-server/sql-server-editions-express
使用实体框架创建控制台应用
接下来我们将研究如何使用实体框架创建控制台应用。请按照以下步骤创建一个简单的控制台应用:
-
导航到文件|新建项目并选择控制台应用(.NET Core)。
-
将项目命名为
ConsoleEF并单击确定:

安装实体框架核心 NuGet 包
有两种方法可以在应用中安装任何 NuGet 软件包:
- 使用 NuGet 包管理器
- 使用包管理器控制台
我们将研究使用 NuGet 包管理器的第一个选项。
使用 NuGet 包管理器
喜欢图形界面的人可以使用此选项:
- 右键单击 console 项目并从上下文菜单中选择 Manage NuGet Packages:

- 在 NuGet 包中搜索
Microsoft.EntityFrameworkCore.SqlServer。选择 Microsoft.EntityFrameworkCore.SqlServer 后,单击安装:

- 单击 Install 后,NuGet 软件包管理器将要求您查看更改。单击“确定”:

- 在许可证接受窗口中单击我接受:

- 单击 I Accept 后,它将安装实体框架及其所有依赖项。在输出窗口中,安装完成后,您将收到一条完成消息:
Time Elapsed: 00:00:03.0619983
========== Finished ==========
Restoring NuGet packages...
Time Elapsed: 00:00:00.3355224
========== Finished ==========
安装实体框架命令
为了执行迁移活动,我们需要安装 EntityFramework 工具包。迁移包括创建数据库及其关联表。架构中的任何更改也将由迁移来处理:

如前所述,在使用实体框架时,为了与数据库交互,我们需要遵循三个步骤:
- 创建模型类。
- 为您的业务域对象创建
DbContext类。DbContext类的实例表示与数据库的会话。 - 构造业务域对象,并使用
DBContext类的实例将其保存到数据库中。
让我们详细讨论前面的每个步骤,并尝试将对象保存到数据库中。
创建模型类
Model类是简单的 POCO 对象,可以与实体框架一起使用。
让我们为我们的业务域对象创建一个 POCO 类,在我们的例子中是Employee类。在我们的控制台应用中命名新文件名Employee.cs。这个Employee类包含员工的一些属性,并且没有特殊的属性或字段使其与实体框架一起工作。
让我们看看下面的代码片段:
public class Employee
{
public int EmployeeId { get; set; }
public string Name { get; set; }
public decimal Salary { get; set; }
public string Designation { get; set; }
}
按照惯例,如果属性名为Id或ClassName+Id,则实体框架在创建数据库表时会将其视为主键。
具有字符串数据类型的属性将创建为nvarchar(max)类型的字段。但是,我们可以通过使用注释来覆盖此行为,这将在后面讨论。
创建 DbContext 类
DbContext类的实例表示到数据库的会话,该DbContext类为您的应用执行大部分繁重的数据访问。创建一个名为 EmployeeDbContext 的新类,其内容如下:
Go to https://goo.gl/hbju3w to access the code.
using Microsoft.EntityFrameworkCore;
namespace ConsoleEF
{
public class EmployeeDbContext : DbContext
{
public DbSet<Employee> Employees { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(@"Data Source=
(localdb)\MSSQLLocalDB;Initial Catalog=
EFConsole;Integrated Security=True;Connect
Timeout=30;
Encrypt=False;TrustServerCertificate=True;
ApplicationIntent=ReadWrite;
MultiSubnetFailover=False");
}
}
在前面的代码片段中有几点需要注意:
- 将
Microsoft.EntityFrameworkCore名称空间包括在此名称空间中可用的DbContext类。我们的连接字符串目前是硬编码的,但我们可以很容易地使其可配置。 - 为了使用
DbContextAPI,必须创建一个继承自DbContext类的类,以便我们可以访问DbContextAPI 的方法。我们已经创建了EmployeeDbContext类,它是从 DbContext 类继承的。 DbSet是允许对给定实体类型执行实体框架操作的类。我们需要为应用中使用的每个实体类型创建DbSet对象。在本例中,我们只使用一个DbSet对象,因为我们正在使用Employee类。
创建迁移
迁移是记录数据库所有更改的过程。编写迁移可以确保数据库模式也随着应用的发展而发展。它还解决了版本控制问题。如果我们有 10 个客户,并且每个客户都运行不同版本的应用,那么我们就不必跟踪哪个客户安装了哪个模式。由于迁移的自动化过程和迁移是代码本身的一部分,我们所要做的就是运行迁移,并保证数据库模式将处于正确的状态。
按照以下步骤创建迁移:
Add-Migration是增加迁移的实体框架命令,如下图:

- 添加迁移后,您可以通过执行
Remove-Migration实体框架命令撤销更改。Migrations目录如下:

- 通过发出实体框架命令
Update-Database更新数据库,该命令根据迁移中可用的信息更新数据库表。由于我们之前已经安装了EntityFramework.Commands包,这些命令将可用于应用:

- 更新数据库后,通过从“视图”菜单中检查 SQL Server 对象资源管理器,可以查看数据库中的更改:

- 执行数据库操作以将业务域对象保存在数据库中。您可以手动创建数据库,如果数据库不可用,它将为您创建一个数据库。
Main方法更新为以下代码:
using System;
namespace ConsoleEF
{
class Program
{
static void Main(string[] args)
{
AddEmployee();
}
static void AddEmployee()
{
using (var db = new EmployeeDbContext())
{
Employee employee = new Employee
{
Designation = "Software Engineer",
Name = "Scott",
Salary = 5600
};
db.Employees.Add(employee);
int recordsInserted = db.SaveChanges();
Console.WriteLine("Number of records inserted:" + recordsInserted);
Console.ReadLine();
}
}
}
}
首先,我们正在构建业务域对象。然后,我们将构造的 Employee 对象添加到DbContext类的 Employee 的DbSet中。最后,我们将调用SaveChanges方法DbContextAPI,它将保存对数据库的所有挂起的更改。
您可能想知道,当我们甚至没有为它提供连接字符串时,它如何将其保存到数据库中。
让我们讨论一下运行程序时幕后发生了什么:
- 当您更改任何
DbSet集合时,Entity Framework 会检查数据库是否存在。如果不存在,则使用<Namespace of DbContextName>模式创建一个新的。在我们的例子中,将创建一个名为EF6.EmployeeDbContext的数据库。 - 然后,为
DbSet中声明的实体创建数据库表。按照惯例,实体框架使用实体的复数形式作为表名。正如我们为Employee实体声明的DbSet,实体框架创建了Employee的复数形式,并创建了名为Employees的表。
执行以下代码时,将创建数据库和表:
db.Employees.Add(employee);
当执行SaveChanges方法时,Employee对象中的数据将保存到数据库中,并返回受影响的记录数。在前一种情况下,它返回1。
再次运行应用时,将跳过前面提到的前两个步骤,因为数据库和表已经创建。
查询数据库时,可以看到新插入的记录:

以下是查询数据的屏幕截图:

SaveChanges 方法的工作原理
当我们进行更改时,实体框架跟踪每个对象的状态,并在调用SaveChanges方法时执行相应的查询。这使得开发人员无法跟踪在代码的不同位置发生的对象更改。开发人员很难自己跟踪这些更改。
SaveChanges方法为您带来了透明的持久性,也就是说,您的应用将在业务对象上设置数据,并且在底层的某个地方,它将被保存,但会自动保存。SaveChanges方法只是将跟踪的更改刷新到数据库中,保持内存和数据库同步。
例如,当我们将一个Employee对象添加到员工集合(DbSet中时,该对象在Addedstate中被跟踪为Entity。调用SaveChanges时,实体框架为其创建insert查询并执行。更新和删除对象的情况也是如此。实体框架将各个对象的Entity状态设置为Modified和Deleted。调用SaveChanges时,创建并执行Update和Delete查询:

上图解释了SaveChanges方法如何在不同类型变更的高级别上工作。我们有两个 POCO 对象(对象 1 和对象 2),它们已添加到 employeesDbSet对象中。假设对象 3 和对象 4 已修改,对象 5 和对象 6 处于删除状态。当您调用SaveChanges方法时,它会创建三组查询。第一组查询用于添加对象,从而针对数据库执行insert查询。在第二组查询中,为状态被修改的对象创建并执行Update查询。最后,对所有删除的状态对象执行Delete查询。
更新记录
让我们尝试使用实体框架更新插入的员工记录的工资:
Go to https://goo.gl/4k5J6a to access the code.
static void UpdateSalary()
{
using (var db = new EmployeeDbContext())
{
Employee employee = db.Employees.Where(emp
=> emp.EmployeeId == 1).FirstOrDefault();
if (employee != null)
{
employee.Salary = 6500;
int recordsUpdated = db.SaveChanges();
Console.WriteLine("Records updated:" +recordsUpdated);
Console.ReadLine();
}
}
}
在前面的方法中,我们找到带有EmployeeId = 1的员工。然后,我们将员工的工资更新为6500并将employee对象保存到数据库中。请注意,在前面的方法中,我们与数据库交互两次,一次找到正确的员工记录(读取操作),另一次更新记录(更新操作):
static void Main(string[] args)
{
UpdateSalary();
}
还要确保您添加了using System.Linq;到文件的顶部。
更新Main方法以调用UpdateSalary方法。查询数据库时,您应该会看到包含更新信息的记录:

确保单击“刷新”按钮。
删除记录
删除记录有点棘手,因为它需要直接设置状态。在下面的方法中,我们首先获取对象并将对象的状态设置为 Deleted。使用db.Delete方法而不是设置状态将意味着该记录立即从数据库
中删除。然而,情况并非如此。实际删除将一直挂起,直到我们调用SaveChanges。一旦我们调用了SaveChanges方法,它将为对象生成删除查询并执行,然后最终删除数据库中的记录:
Go to https://goo.gl/QgqKCM access the code.
static void DeleteEmployee()
{
using (var db = new EmployeeDbContext())
{
Employee employeeToBeDeleted =
db.Employees.Where(emp => emp.EmployeeId ==1).FirstOrDefault();
if (employeeToBeDeleted != null)
...
...
}
}
在 Main 方法中进行更改,并确保其外观如下所示:
static void Main(string[] args)
{
DeleteEmployee();
}
活动:手动控制事务
瞄准
手动控制事务并自己提交。
完成步骤
代码如下:
Go to https://goo.gl/Wk9RUH to access the code.
static void DeleteEmployee()
{
using (var db = new EmployeeDbContext())
using(var transaction = db.Database.BeginTransaction())
...
...
}
在 ASP.NET MVC 应用中使用实体框架
在控制台应用中使用实体框架与 ASP.NET MVC 应用之间没有太大区别。现在,我们将用一个屏幕构建一个简单的应用,如下面的屏幕截图所示。
在这个屏幕中,我们将有一个表单,用户将在其中输入有关员工的信息;用户提交表单后,信息将保存到数据库中,如以下屏幕截图所示:

我们可以为员工创建一个简单的模型。我们需要为此视图构建一个 ViewModel,因为我们需要从用户那里获取员工信息,并且我们还需要在同一屏幕上显示员工列表。
以下是为上述目标创建应用的分步说明:
-
通过选择空的 ASP.NET Core Web 应用,在 Visual Studio 中创建 ASP.NET Core 项目。
-
我们需要的所有实体框架软件包都是现成的,因此不需要安装任何东西。
-
使用 Visual Studio 添加一个
appsettings.json文件,如下图:

- 并改变
appsettings.json使其类似于:
{
"ConnectionStrings":
{
"DefaultConnection": "Server=(localdb)
\\MSSQLLocalDB;Database=Validation;
Trusted_Connection=True;
MultipleActiveResultSets=true"
}
}
- 在
Startup类(Startup.cs类)中配置 MVC:- 在构造函数中,我们通过读取
appsettings.json文件来构建配置。 - 将 MVC 服务和实体框架服务添加到
ConfigureServices方法中的服务中。 - 以
Configure方式配置 MVC 路由:
- 在构造函数中,我们通过读取
Go to https://goo.gl/VezQkv to access the code.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.EntityFrameworkCore;
using MVCEF.Models;
namespace MVCEF
{
public class Startup
{
...
...
}
}
- 创建
Models和DbContext类。 - 创建
Models文件夹并添加Employee模型类和EmployeeDbContext类。 - 创建
Employee模型类(Models文件夹中的Employee.cs:
public class Employee
{
public int EmployeeId { get; set; }
public string Name { get; set; }
public decimal Salary { get; set; }
public string Designation { get; set; }
}
- 创建
EmployeeDbContext(Models文件夹中的EmployeeDbContext.cs:
Go to https://goo.gl/G9Sm11 to access the code.
using Microsoft.EntityFrameworkCore;
using MVCEF.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace MVCEF.Models
{
public class EmployeeDbContext : DbContext
{
public EmployeeDbContext(DbContextOptions<EmployeeDbContext> options) :
base(options)
{
}
public DbSet<Employee> Employees { get; set; }
}
}
- 创建视图模型。
由于我们将在同一屏幕中显示员工列表和添加员工的表单,我们将构建一个特定于此视图的模型。此模型将包含有关员工列表和要添加的员工的信息。
- 创建
ViewModels文件夹并添加EmployeeAddViewModel。
Go to https://goo.gl/Z7rSRa to access the code.
using MVCEF.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace MVCEF.ViewModels
{
public class EmployeeAddViewModel
{
public List<Employee> EmployeesList { get; set; }
public Employee NewEmployee { get; set; }
}
}
这个ViewModel有两个属性:EmployeesList和NewEmployee。EmployeesList将包含员工名单。此列表将从数据库中获取。NewEmployee将保存用户输入的员工信息。
- 创建
Controllers来处理传入请求:
* 创建一个Controllers文件夹并添加EmployeeController类,其中包含两个动作方法,一个用于GET,另一个用于POST。当您访问 URL(http://localhost/Employee/Index或运行应用时,将调用GET操作方法对应的索引操作方法。当您提交以下表单时,将调用POST Index操作方法:
Go to https://goo.gl/Yivh8J to access the code.
public IActionResult Index()
{
EmployeeAddViewModel
employeeAddViewModel = new EmployeeAddViewModel();
var db = this.employeeDbContext;
employeeAddViewModel.EmployeesList = db.Employees.ToList();
employeeAddViewModel.NewEmployee = new Employee();
return View(employeeAddViewModel);
}
Go to https://goo.gl/gsoJnE to access the code.
[HttpPost]
public IActionResult Index(EmployeeAddViewModel employeeAddViewModel)
{
var db = this.employeeDbContext;
db.Employees.Add(employeeAddViewModel.NewEmployee);
db.SaveChanges();
//Redirect to get Index GET method
return RedirectToAction("Index");
}
readonly EmployeeDbContext
employeeDbContext;
public EmployeeController(EmployeeDbContext employeeDbContext)
{
this.employeeDbContext = employeeDbContext;
}
Constructor parameter EmployeeDbContext comes from services.AddEntityFrameworkSqlServer(). Once this line is executed, we are basically instructing the runtime to inject this service wherever it is necessary within our controllers. This way, we don't have to keep track of it. When the request ends, the context returns to its own pool, waiting to be used.
Go to https://goo.gl/eQHBT6 to access the code.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using MVCEF.ViewModels;
using MVCEF.Models;
namespace MVCEF.Controllers
{
...
...
}
- 添加
Views文件夹。 - 创建具有以下内容的
Views\_ViewStart.cshtml:
@{
Layout = "_Layout";
}
- 创建具有以下内容的
Views\Shared\_Layout.cshtml:
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
</head>
<body>
<div>
@RenderBody()
</div>
</body>
</html>
- 创建具有以下内容的
Views\Employee\Index.cshtml:
Go to https://goo.gl/Nf8kep to access the code.
@model MVCEF.ViewModels.EmployeeAddViewModel
@*
//For more information on enabling MVC for empty projects, visit http:
//go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}
<div>
@using (Html.BeginForm("Index", "Employee", FormMethod.Post))
...
...
</div>
在前面的Index视图中,我们创建了一个表单,从最上面的div元素中的用户那里获取员工信息。在下一个div元素中,我们以表格格式显示员工列表。
创建所有文件夹和文件后,项目结构应如下所示:

修改索引(EmployeeAddViewModel EmployeeAddViewModel)方法
我们将考虑修改Index(EmployeeAddViewModel employeeAddViewModel)方法,以便首先检查是否存在同名员工。如果它存在,它将更新该记录。
代码如下:
[HttpPost]
public IActionResult Index(EmployeeAddViewModel employeeAddViewModel)
{
var db = this.employeeDbContext;
var newRecord = employeeAddViewModel.NewEmployee;
var existingEmployee =
db.Employees.FirstOrDefault(k => k.Name == newRecord.Name);
if (existingEmployee != null)
{
existingEmployee.Designation = newRecord.Designation;
existingEmployee.Salary = newRecord.Salary;
}
else
{
db.Employees.Add(existingEmployee);
}
db.SaveChanges();
//Redirect to get Index GET method return RedirectToAction("Index");
}
The version might be different in your case.
数据库迁移
我们已经创建了业务实体Employee类。现在,我们可以继续迁移了。迁移过程分为两步:第一步,我们创建迁移文件。我们已经了解了如何从 PackageManager 控制台创建迁移。还有一种方法可以从命令行工具创建迁移。为此,首先我们需要编辑MVCEF.csproj项目文件,并在文件末尾添加工具。因此,项目文件的结尾应如下所示:
...
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools"
Version="2.0.0-preview2-final" />
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet"
Version="2.0.0-preview2-final" />
</ItemGroup>
</Project>
接下来,我们将执行命令行工具。这可以通过从项目上下文的命令提示符执行以下命令来完成:
dotnet ef migrations add InitialMigration
InitialMigration is just a name. You can give it any name you like.
最后,此命令将在项目中创建迁移文件,如以下屏幕截图所示:

您还将看到创建了一个模型快照。这允许 EF 从现有模型中获得新的差异,以便随着模型的发展,它可以为您生成迁移。
快照文件必须与创建它的迁移保持同步,因此您不能仅通过删除名为_.cs的文件来删除迁移。如果删除该文件,其余迁移将与数据库快照文件不同步。要删除上次添加的迁移,请使用dotnet ef migrations remove命令。
然后执行以下命令以创建数据库:

此命令将读取在上一步中创建的迁移文件,并创建数据库以及相关表:

以下屏幕截图显示了创建的数据库:

运行应用。您将看到以下屏幕,用户可以在其中以表单的形式输入员工信息。当我们在视图中使用强类型模型时,它接受所有属性的默认值。Name和Designation是string类型的属性,这些字段的默认值是空字符串,Salary是十进制类型,decimal的default值是0,因此Salary字段加载时会在表单中显示0。
由于没有记录,我们在List of employees表中显示0记录:

当您在表单中输入信息并提交时,信息将保存在数据库中,Employees表中的所有数据库记录将显示如下:

向 Employee 类添加 Age 属性并进行新迁移
Employee类将类似于此:
Go to https://goo.gl/2i6iRj to access the code.
public class Employee
{
public int EmployeeId { get; set; }
public string Name { get; set; }
public decimal Salary { get; set; }
public string Designation { get; set; }
public int Age { get; set; }
}
然后从包管理器控制台运行Add-Migration EmployeeAge命令。
总结
在本章中,我们了解了什么是模型以及它如何适合 ASP.NET MVC 应用。然后,我们创建了一个简单的模型,在控制器中构建模型数据,将模型传递给视图,并使用视图显示数据。我们已经了解了特定于视图的模型,并讨论了与模型相关的数据流。我们了解了 Entity Framework,一个来自 Microsoft 的 ORM 框架,以及它如何简化.NET 应用的数据库访问。我们创建了一个简单的控制台应用,在其中插入、更新和删除记录。最后,我们构建了一个 ASP.NET Core 应用,它使用模型、视图模型和实体框架。
五、验证
我们永远不能依赖用户输入的数据。有时,他们可能对应用一无所知,因此可能在不知不觉中输入了错误的数据。有时,一些恶意用户可能希望通过在应用中输入不适当的数据来破坏应用。在任何一种情况下,我们都需要在存储数据以供进一步处理之前验证输入数据。
在本章结束时,您将能够:
- 解释不同类型的验证
- 使用示例执行服务器端验证
- 使用示例执行客户端验证
- 使用 jQuery unobtrusive 库执行 unobtrusive JavaScript 验证
验证简介
在理想情况下,用户将以适当的格式在应用中输入有效数据。但是,正如你可能意识到的,现实世界并不那么理想。用户将在应用中输入不正确的数据。作为开发人员,验证应用中的用户输入是您的责任。如果输入的输入无效,您需要通知用户,解释错误,以便用户更正输入数据并再次提交表单。
验证可以在客户端、服务器端或两端进行。如果验证是在将数据发送到服务器之前完成的,则称为客户端验证。例如,如果用户没有在必填字段中输入任何数据,我们可以在客户端本身验证表单(通过查找未输入的数据)。
不需要将表单数据发送到服务器。JavaScript 是客户端验证最常用的语言:

如果验证在服务器端完成(将表单数据发送到服务器),则称为服务器端验证。例如,您可能希望根据数据库中的数据验证用户输入的数据。在这种情况下,最好进行服务器端验证,因为我们不能将数据库中的所有数据都放在客户端:

即使我们不使用数据库,服务器端验证也是必不可少的,因为您无法信任用户。恶意用户可以在浏览器中更改您的数据、HTML 或 JavaScript,并提交实际上不正确的数据。服务器端验证是您唯一真正的防御。
Never trust client-side validation. For critical data, always do the validation on the server.
客户端和服务器端验证
在现实世界中,这不是服务器端或客户端验证的情况。服务器端验证有利于我们自身的安全。客户端验证对用户来说很方便。它还提高了我们的性能。由于验证会立即在用户的浏览器中运行,因此不会对我们的服务器产生任何影响。我们可以同时进行两种类型的验证。事实上,建议在两端验证数据,以避免不必要的处理:

上图显示在客户端和服务器端都在执行验证。如果数据没有输入到 required 字段中,我们可以在客户端本身发现这个问题。无需将数据发送到服务器以最终发现没有输入数据。输入所有必需的数据后,数据将发送回服务器,以根据某些业务逻辑验证输入的数据。如果验证失败,表单数据将再次发送到浏览器并显示错误消息,以便用户可以再次发送数据。
我们已经介绍了有关验证需求和应用中通常使用的验证类型的足够理论。让我们通过向上一章中构建的应用添加验证来解决问题。
下面的屏幕截图是我们在上一章中构建的表单。这种形式没有什么特别之处,只有三个字段。
当用户在表单中输入数据时,数据将存储在数据库中,所有员工信息将被取回并以表格格式显示:

在我们构建的现有应用中,即使用户没有在任何字段中输入任何信息并提交,我们也不会向用户显示任何消息。相反,我们静默地存储字段的默认值(字符串类型为空值,十进制类型为0.00),如以下屏幕截图所示:

但事实并非如此。我们应该通知用户输入的数据无效,并要求用户更正输入的数据。
服务器端验证
让我们继续上一章中构建的应用。要执行服务器端验证,我们需要执行以下操作:
- 向
ViewModels模型类添加数据注释属性。根据此元数据验证输入数据,并自动更新模型状态。 - 更新
view方法以显示每个字段的验证消息。具有asp-validation-for属性的span标记帮助器将用于显示验证错误消息。 - 更新控制器操作方法以验证模型状态。如果模型状态有效,我们将数据插入数据库。否则,将更新 ViewModel,并使用验证错误消息再次呈现
view方法,以便用户可以使用有效的输入数据进行更新并再次提交表单。
使用“数据注释”属性更新 ViewModels
数据注释属性定义了Model/ViewModel属性的验证规则。如果输入数据与模型中的属性定义不匹配,验证将失败,从而使关联的模型状态无效。向 ViewModels 添加注释的原因是我们将 ViewModels 公开给外部世界,而不是模型。此外,我们不想用 ASP.NET 特定属性污染我们的业务类。
有几个数据注释属性可用于验证数据。以下是最常用的数据批注属性:
- 必选:该属性表示该属性为必选属性。
- 范围:该属性定义最小约束和最大约束。
- MinLength:定义属性必须具有的最小长度,以便验证成功。
- MaxLength:顾名思义,该属性定义属性的最大长度。如果属性值的长度超过最大长度,验证将失败。
- RegularExpression:如果使用此属性,可以使用正则表达式进行数据验证。
由于数据注释属性在System.ComponentModel.DataAnnotations名称空间中可用,我们需要包含此名称空间。以下为第 4 章、车型更新后的 ViewModel 代码:
Go to https://goo.gl/EgT2vC to access the code.
using MVCEF.Models;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace MVCEF.ViewModels
{
public class EmployeeAddViewModel
{
public List<Employee> EmployeesList { get; set; }
[Required(ErrorMessage = "Employee Name is required")]
public string Name { get; set; }
[Required(ErrorMessage = "Employee Designation is required")]
[MinLength(5, ErrorMessage = "Minimum length of designation should be 5 characters")]
public string Designation { get; set; }
[Required]
[Range(1000, 9999.99)]
public decimal Salary { get; set; }
}
}
我们为所有三个属性添加了数据注释属性:Name、Designation和Salary。
ErrorMessage属性显示验证失败时将显示的消息。如果验证失败且未提及ErrorMessage,则显示默认错误消息。
使用属性的缺点是它只能处理编译时可用的文本。如果我们需要更多的动态验证,我们可以从ValidationAttribute派生并添加我们自己的逻辑。此外,还可以使用资源文件对项目进行本地化。
更新 ViewModel 以显示验证错误消息
对于每个字段,我们都添加了一个 span 标记,当验证失败时,错误消息以红色显示。验证成功后,将不会显示任何错误消息。asp-validation-for的属性值表示必须显示验证错误消息的字段名。例如,我们使用了带有asp-validation-for属性和Name值的 span 标记,它告诉 ASP.NET MVC 显示Name字段的验证错误消息。
Views/Employee/Index.cshtml如下所示:
Go to https://goo.gl/k9JMRf to access the code.
@model MVCEF.ViewModels.EmployeeAddViewModel
@*
//For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860 *@
@{
}
...
<td>
<span asp-validation-for="Name" style="color:red"></span>
</td>
...
Make sure you have Views\_ViewImports.cshtml to enable tag helpers, as follows: @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
更新控制器操作方法以验证模型状态
模型状态将根据在 ViewModel 和输入数据上指定的数据注释属性自动更新。我们正在验证以下Index方法中的模型状态是否有效,这是一种POST操作方法。如果模型状态有效(当验证成功时),我们将输入的数据保存到数据库中。如果验证失败,则自动将ModelState设置为invalid。然后,我们用输入的数据填充ViewModel并再次呈现View方法,以便用户可以更正输入数据并重新提交数据:
Go to https://goo.gl/iQHZYS to access the code.
[HttpPost]
public IActionResult Index(EmployeeAddViewModel employeeAddViewModel)
{
if (ModelState.IsValid)
{
...
...
}
employeeAddViewModel.EmployeesList =
employeeDbContext.Employees.ToList();
return View(employeeAddViewModel);
}
在进行上述更改后运行应用并提交表单而不输入值时,字段旁边将显示错误消息,如以下屏幕截图所示。请注意,即使在验证错误的情况下,我们也会在下表中显示员工的数据,这是通过使用前面代码段中的代码块实现的:

在前面的验证及其错误消息中,有几点需要注意:
- 如果验证失败,将按预期显示错误消息。
- 如果同一字段有多个验证,它将一次显示一条错误消息。例如,
Designation字段有两个验证:Required和MinLength属性。如果没有为字段输入数据,则只显示所需的字段错误消息。只有在解决所需字段错误时(通过在字段中输入一些字符),才会显示第二条验证错误消息。 - 如果没有可用的错误消息,并且验证失败,则显示默认错误消息。我们没有为
Salary字段提供错误消息。因此,当该字段的验证失败时,ASP.NET MVC 将根据字段名称和验证失败的类型显示默认错误消息。
It's not possible to circumvent the validation by editing the HTML code from the browser. The validation happens on the server side. It is safe and there is no way to work around it.
下图描述了服务器端验证中的高级事件序列:

以下是上图的描述:
- 用户输入的数据无效。
- 基于 ViewModel 中的“数据注释”属性,模型状态将自动更新。这在模型绑定过程中发生,视图方法中的数据映射到模型或视图模型中的数据。
- 在控制器的动作方法中,我们正在验证模型状态。
- 如果模型状态有效,我们将把输入的数据保存到数据库中。
- 如果模型状态无效,我们将再次呈现 ViewModel 并显示验证错误消息,以便用户可以更正输入数据并使用有效的输入数据再次提交表单。
活动:为指定添加新的验证规则
场景
您的公司希望您为指定添加新的验证规则,使其至少包含两个单词。(提示:使用正则表达式。)
瞄准
为指定特性添加新的验证规则。
完成步骤
将EmployeeAddViewModel.cs中的designation属性修改如下:
Go to https://goo.gl/bK5Ece to access the code.
[Required(ErrorMessage = "Employee Designation is required")]
[MinLength(5, ErrorMessage = "Minimum length of designation should be 5 characters")]
[RegularExpression(@"^[a-z]+(?:\s[a-z]+)+$", ErrorMessage = "Designation should be at least two
words")]
public string Designation { get; set; }
客户端验证
有些情况下,我们不需要去服务器验证输入数据。在前面的服务器端验证示例中,我们不需要去服务器验证用户是否已经输入了Name字段的数据。我们可以在客户端本身进行验证。这可以防止往返服务器并减少服务器负载。
我们将使用 JavaScript 来验证来自客户端的数据。JavaScript 是一种高级解释语言,主要用于客户端编程。
At present, JavaScript is also being used at the server side as part of Node.js.
执行客户端验证
按照以下步骤执行客户端验证:
- 我们将在 ViewModel(即
Index.cshtml文件)中做一些更改,以在客户端验证表单:- 表单更改:将
id属性添加到所有 span 标记中,以便我们可以访问此 HTML 元素以显示 HTML 错误消息。提交表单时,调用 JavaScript 函数验证输入数据。 - 添加 scripthtml 元素并创建一个 JavaScript 函数来验证输入数据。
- 表单更改:将
- 在下面的代码中,我们在提交表单时调用
validateFormJavaScript 函数。如果validateForm函数返回true,数据将被发送到服务器。否则,将不发送数据。我们为所有 span 标记添加了id属性,以便我们可以识别span标记并在那里显示验证错误消息:
Go to https://goo.gl/vjjtRp to access the code.
<form asp-controller="Employee" asp-action="Index" onsubmit="return validateForm()">
<table>
<tr>
<td><label asp-for="Name"></label></td>
<td><input asp-for="Name" /></td>
<td>
<span id="validationName" asp-validation-for="Name" style="color:red"></span>
</td>
</tr>
...
...
</tr>
</table>
</form>
The purpose of onsubmit="return validateForm()" function is that returns false due to a validation error, then it will prevent the form from getting submitted to the server. Do not forget the return keyword, otherwise it won't work as expected.
- 我们添加了 JavaScript 函数来验证这三个字段。我们得到三个字段的值,并将它们存储在单独的变量中。然后我们验证每个变量的值是 null 还是空。如果该值为空,我们将获取相应字段的 span 元素,并使用验证错误消息设置文本上下文:
Go to https://goo.gl/3uPtH1 to access the code.
<script type="text/javascript">
function validateForm()
{
var isValidForm = true;
var nameValue = document.getElementById("Name").value;
var designationValue = document.getElementById("Designation").value;
var salaryValue = document.getElementById("Salary").value;
...
...
}
</script>
- 当您运行应用并在不输入数据的情况下提交表单时,您将收到从客户端本身生成的错误消息,而无需转到服务器:

In real-world applications, we would not be hand coding the validation code on the JavaScript. Instead, most applications use
unobtrusive validation, where we do not write JavaScript code for validating each of the fields. Simply adding the respective JavaScript libraries will do.
您可能想知道,在没有编写代码的情况下,字段是如何得到验证的。神奇之处在于基于数据注释属性添加到输入 HTML 元素的data属性。这个 jQuery unobtrusive 库获取一个字段列表,其中添加了data-属性,并对其进行了验证。
运行应用并按Ctrl+U查看源代码。源代码如下所示:
Go to https://goo.gl/gTYZKb to access the code.
<div>
<form action="/" method="post">
<table>
<tr>
<td><label for="Name">Name</label></td>
<td><input type="text" data-val="true" data-val-required="Employee Name is required"
id="Name" name="Name" value="" /></td>
...
...
</form>
</div>
不同的属性将添加到不同类型的数据注释属性中。数据属性是根据我们在 ViewModel 顶部定义的属性生成的。对于要验证的字段,data-val属性将设置为true。对于在 ViewModel 中标记为 required 的属性,data-val-required属性将具有关联属性的错误消息的值。
活动:向 JavaScript 函数添加新的验证规则
瞄准
将新的验证规则添加到 JavaScript 函数中进行指定,使其至少包含两个单词。(提示:使用正则表达式。)
完成步骤
我们需要修改 JavaScript。检查以下各项:
Go to https://goo.gl/u8y4Ur to access the code.
//validate the designation field
if (designationValue == null || designationValue == "" )
{
document.getElementById("validationDesignation").textContent =
"Employee Designation is required - from
client side";
isValidForm = false;
}
else if (!(/^[a-z]+(?:\s[a-z]+)+$/.test(designationValue )))
{
document.getElementById(
"validationDesignation").textContent =
"Employee Designation must be at least two
words - from client side";
isValidForm = false;
}
实施
布局文件(_Layout.cshtml定义 web 应用的布局结构。由于 JavaScript 库将在所有页面中使用,因此这是添加常见功能(如不引人注目的验证)的正确位置。
只需将 JavaScript 库(在下面的代码段中以粗体突出显示)添加到布局文件(_Layout.cshtml中),以便它们可用于所有View文件:
Go to https://goo.gl/MKJ39B to access the code.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
</head>
<body>
<div>
@RenderBody()
</div>
<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.2.3.js"></script>
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/ 1.14.0/jquery.validate.min.js"> </script>
<script src="https://ajax.aspnetcdn.com/ajax/mvc/5.2.3/jquery.validate.unobtrusive.min.js"></script>
</body>
</html>
ViewModel没有任何更改,只是删除了我们前面编写的用于验证字段的 JavaScript 函数。视图的完整代码如下所示:
Go to https://goo.gl/pxazJH to access the code.
@model MVCEF.ViewModels.EmployeeAddViewModel
@*
//For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}
...
...
</table>
</div>

上图描述了不引人注目的客户验证过程:
Model/ViewModels中增加了数据注释。- 视图获取
Model/ViewModels并生成 HTML。 - 从 ViewModel 生成的 HTML 包含
data-*属性:- 对于设置了
Required属性的字段,将创建data-val-required属性,并将错误消息作为其值。 - 对于具有
MinLength数据注释属性的字段,data-val-minlength属性设置为错误消息作为其值。 - 对于范围、数据注释,
data-val-range属性设置为错误消息作为其值。data-val-range-max表示范围内的最大值,data-val-range-min属性表示范围内的最小值。
- 对于设置了
- jQuery unobtrusive validation 库读取这些具有
data-*属性的元素,并执行客户端验证。这意味着开发人员不必使用 JavaScript 编写分离验证代码,因为所有问题都由配置本身解决。
The data-val attributes are only generated from our pre-defined data attributes. If we had a complex custom logic and if we need
them to validate from the client side, then we need to go back to write JavaScript.
活动:通过扩展 ValidationAttribute 为指定添加新的验证规则
瞄准
为名称添加新的验证规则,使其至少包含两个单词。但这次不要在服务器端使用正则表达式。而是扩展ValidationAttribute。
完成步骤
- 创建 validator 属性,如下所示:
Go to https://goo.gl/m63jKP to access the code.
public class TwoWordsValidationAttribute : ValidationAttribute, IClientModelValidator
{
public void AddValidation(ClientModelValidationContext context)
...
...
}
- 请注意,通过使用客户机模型验证器,我们还可以与 jQuery 不引人注目的验证框架进行交互。然后将以下代码放入页面底部的布局文件中:
Go to https://goo.gl/Px3d12 to access the code.
...
<script type="text/javascript">
$(function () {
jQuery.validator.addMethod('twowords', function (value, element, params)
{
var value = $(params[0]).val();
return /^[a-z]+(?:\s[a-z]+)+$/.test(value);
});
...
...
</script>
</body>
</html>
总结
在本章中,我们了解了验证的必要性以及可用的各种验证。我们甚至讨论了客户端和服务器端验证是如何工作的,以及每种类型验证的优缺点。后来,我们进行了代码更改,以在服务器端验证输入数据。然后,我们使用 JavaScript 在客户端本身验证输入数据。最后,我们使用 jQuery unobtrusive 库进行客户端验证,而无需编写 JavaScript 代码在客户端验证输入数据。
在下一章中,我们将讨论路由原则以及如何定制路由。在前面的一章中,我们看到了 ASP.NET 5 应用中路由的基础知识。现在我们将深入探讨这个话题。
六、路由
路由是 ASP.NET MVC 应用中最重要的概念之一,因为它负责处理传入的请求并将它们映射到相应的控制器操作。
我们在第 2 章、控制器中简要讨论了路由。在本章中,我们将讨论路由以及在 ASP.NETCore 中定制路由的几个选项。
在本章结束时,您将能够:
- 使用 MapRoute 方法配置路由
- 使用基于约定和属性的示例处理不同类型的路由
- 在基于属性的路由中使用 HTTP 谓词
基于约定的路由
路由引擎负责将传入请求映射到控制器的适当操作方法。
我们应该有路由名称,因为它为路由提供了一个逻辑名称,这样命名的路由就可以用于 URL 生成。这大大简化了 URL 创建,因为路由的顺序可能会使 URL 生成变得复杂。路由名称在应用范围内必须是唯一的。
路由名称对 URL 匹配或请求处理没有影响;它们仅用于生成 URL。虽然 URL 生成是一个不同的研究主题,但我们可以简单地说,我们使用它在视图中生成从一个页面到另一个页面的链接。
在Startup类的Configure方法中,我们映射了以下路径:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Employee}/{action=Index}/{id?}");
});
Alternatively, you can use the following code:
app.UseMvcWithDefaultRoute();
This is equal to the following:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
MapRoute方法有两个参数:
name:这表示路由的名称,因为我们可以为同一应用配置多个路由。template:表示路线的实际配置。此配置值包含三个部分。由于我们提供默认参数,如果没有传递值,它将采用默认参数值。{controller=Employee}:第一个值作为控制器的名称。当控制器值在 URL 中不可用时,我们使用 Employee 控制器作为默认控制器。{action=Index}:Index动作方式将作为默认动作方式。URL 中的第二个参数将作为操作方法名称。{id?}:在 id 参数后指定“?”,表示id为可选参数。如果该值作为第三个参数传递,则取id值。否则,将不予考虑。
让我们看几个例子并观察路由引擎是如何工作的。对于前面的示例,我们假设以下路由:
"{controller=Employee}/{action=Index}/{id?}"
例 1
以下是 URL 的显示方式:

在此 URL 中,我们没有传递controller、action或id参数的值。由于我们没有传递任何内容,因此它将采用控制器和操作的默认值。因此,路由引擎将 URL 转换为以下 URL:

例 2
以下是 URL 的显示方式:

在这个 URL 中,我们已经为控制器(第一个参数)传递了值,它是Employee,而我们没有为action方法(第二个参数)或id(第三个参数)传递任何内容。因此,URL 将转换为以下 URL,取action方法的默认值:

例 3
以下是 URL 的显示方式:

路由引擎将第一个参数Manager作为控制器名称,第二个参数List作为动作方法名称。
例 4
以下是 URL 的显示方式:

我们已在此 URL 中传递了所有三个参数。因此,第一个参数值Manager将被视为控制器方法名称。第二个参数值将被视为action方法名称。第三个参数值将被视为id。
在定义地图路线时,我们使用带有几个参数的MapRoute方法。第一个参数 name 表示路由的名称,第二个参数template表示要与默认值匹配的 URL 模式。下面是一些示例代码供您观察:
routes.MapRoute(name: "default",
template:
"{controller=Employee}/{action=Index}/{id?}");
这个MapRoute方法还有其他重载变体。下面是另一个常见的重载MapRoute方法,其中传入的 URL 模式和默认值是针对不同的参数传递的。路由名称为FirstRoute,此路由将应用于以Home开头的所有 URL。控制器和动作的默认值分别为Home和Index2,如下所示:
routes.MapRoute(name: "FirstRoute",
template: "Home",
defaults: new { controller = "Home", action = "Index2" });
您可以为 ASP.NET MVC 应用定义任意数量的路由映射。路由图没有限制或限制。让我们向应用添加另一个路由映射。我们在应用中添加了另一个名为FirstRoute的路线图:
Go to https://goo.gl/36qj7c to access the code.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMvc(routes =>
{
routes.MapRoute(name: "FirstRoute",
template: "Home", defaults: new
{
controller = "Home",
action = "Index2"
});
routes.MapRoute(name: "default",
template:
"{controller=Employee}/{action=Index}/{id?}");
});
}
我们添加了另一个名为HomeController的控制器方法,其中包含两个返回不同字符串的简单操作方法:
Go to https://goo.gl/avdJM9 to access the code.
public class HomeController : Controller
{
// GET: /<controller>/
public IActionResult Index()
{
return Content("Index action method");
}
public IActionResult Index2()
{
return Content("Index2 action method");
}
}
当您尝试通过 URLhttp://localhost:49831/Hello访问应用时,路由映射FirstRoute和默认值都与 URL 模式匹配。
路由引擎根据以下因素映射传入 URL:
- 匹配模式
- 在路由引擎中定义的顺序
第一个因素是显而易见的。对于路由引擎拾取的路由映射,传入 URL 的模式应该与路由映射中定义的模板匹配。
第二个因素是微妙但重要的。如果有多个路由映射与传入 URL 匹配,则路由引擎将选择配置中定义的第一个 URL。
例如,如果传入 URL 与FirstRoute和default映射都匹配,路由引擎将按照配置中首先定义的方式选择FirstRoute映射:

如果路由引擎无法将传入 URL 映射到任何映射路由,我们会得到一个HTTP 404 error,这意味着找不到任何资源。您可以通过查看开发者工具中的“网络”选项卡查看状态(200表示正常,404表示未找到资源),如下图所示:

基于属性的路由
到目前为止,我们一直使用基于约定的路由。在基于约定的路由中,我们在适用于所有可用控制器的集中位置定义路由模板(仅为参数化字符串)。基于约定的路由的问题在于,如果我们想为不同的控制器定义不同的 URL 模式,我们需要定义一个所有控制器都通用的自定义 URL 模式。此外,基于约定的路由设置了我们的通用路由模板。它更加灵活;可以在运行时以编程方式更改它。这使事情变得困难。
还有另一个选项用于配置基于属性的引擎路由。在基于属性的路由中,不是在集中的位置配置所有路由,而是在控制器级别进行配置。我们可以自定义和覆盖我们的约定。基于属性的路由更为静态,并在运行时确定。
正在处理一个基于属性的路由示例
按照以下步骤探索基于属性的路由:
- 首先,让我们删除前面在
startup.cs类文件的Configure方法中创建的基于约定的路由:
public void Configure(IApplicationBuilder app,
IHostingEnvironment env)
{
app.UseMvc();
//app.UseMvc(routes =>
//{
// routes.MapRoute(name: "FirstRoute",
// template: "Home", defaults: new
// {
// controller = "Home",
// action = "Index2"
// });
// routes.MapRoute(name: "default",
// template:
"{controller=Employee}/{action=Index}/{id?}");
//});
}
- 然后,我们可以在控制器本身配置路由。在下面的代码中,我们为前面创建的
home控制器添加了路由配置:
public class HomeController : Controller
{
// GET: /<controller>/
[Route("Home")]
public IActionResult Index()
{
return Content("Index action method");
}
[Route("Home/Index3")]
public IActionResult Index2()
{
return Content("Index2 action method");
}
}
我们在控制器的action方法中使用了 Route 属性。路由attribute中传递的值将作为 URL 模式。例如,当我们访问http://localhost:49831/Home/ URL时,会调用HomeController的索引方法。当我们访问http://localhost:49831/Home/Index3 URL时,会调用HomeController的Index2方法。
The URL pattern and action method name do not need to match.
在前面的示例中,我们正在调用Index2操作方法,但是 URL 模式使用Index3、http://localhost:49831/Home/Index3。
当同时使用基于属性的路由和基于约定的路由时,将优先使用基于属性的路由。
控制器级别的路由属性
您会注意到,对于action方法Index和Index2的 URL 模式,我们在两种 URL 模式Home和Home/Index3中重复控制器名称Home。我们不必在action方法级别重复controller方法名称(或 URL 中的任何公共部分),而可以在控制器级别定义它。
在下面的代码中,URL 的公共部分(主页)在控制器级别定义,唯一部分在action方法级别定义。当 URL 模式被映射到控制器的action方法时,两个路由部分(在controller级别和action方法级别)被合并和匹配。因此,前面定义的路线与后面定义的路线没有区别。
如果希望在基于属性的布线中使用任何参数,可以在花括号内传递它们。在下面的示例中,我们对SayHello操作方法执行了此操作。
例如,http://localhost:49831/Home/Index3URL 模式仍然会映射到Homecontroller的Index2方法:
[Route("Home")]
public class HomeController : Controller
{
// GET: /<controller>/
[Route("")]
public IActionResult Index()
{
return Content("Index action method");
}
[Route("Index3")]
public IActionResult Index2()
{
return Content("Index2 action method");
}
[Route("SayHello/{id}")]
public IActionResult SayHello(int id)"
{
return Content("Say Hello action method" + id);
}
}
路由模板中的令牌替换
如果您希望路由基于类和操作名称,并且仍然希望使用属性,那么有一种在常规路由中使用属性的中间方法:令牌替换。
[action、area和[controller将替换为定义路由的操作中的操作名称、区域名称和控制器名称的值。让我们看一个例子。以下是原始代码:
[Route("[controller]/[action]")]
public class ProductsController : Controller
{
[HttpGet] // Matches '/Products/List'
public IActionResult List()
{
// ...
}
[HttpGet("{id}")] // Matches '/Products/Edit/{id}'
public IActionResult Edit(int id)
{
// ...
}
}
以下代码显示了更改:
public class ProductsController : Controller
{
[HttpGet("[controller]/[action]")] // Matches '/Products/List'
public IActionResult List()
{
// ...
}
[HttpGet("[controller]/[action]/{id}")] // Matches '/Products/Edit/{id}'
public IActionResult Edit(int id)
{
// ...
}
}
属性路由也可以与继承相结合。与代币替换相结合,这一功能尤其强大:
[Route("api/[controller]")]
public abstract class MyBaseController : Controller { ... }
public class ProductsController : MyBaseController
{
[HttpGet] // Matches '/api/Products'
public IActionResult List() { ... }
[HttpPost("{id}")] // Matches '/api/Products/{id}'
public IActionResult Edit(int id) { ... }
}
活动:组合以开头的路线模板/
场景
应用于以/开头的操作的路由模板不会与应用于控制器的路由模板组合。此示例匹配一组类似于默认路由的 URL 路径。根据这些信息,修改SayHello以便我们可以通过编写http://localhost:<yourport>/Employee/SayHello/1来访问它。
瞄准
解决了将以应用于控制器的路由模板开头/与应用于控制器的路由模板相结合的问题。
完成步骤
使用以下代码:
Go to https://goo.gl/q97bvZ to access the code.
[Route("/Employee/SayHello/{id}")]
public IActionResult SayHello(int id)
{
return Content("Say Hello action method" + id);
}
以下代码显示了多个路由的使用:
[Route("Store")]
[Route("[controller]")]
public class ProductsController : Controller
{
[HttpPost("Buy")] // Matches 'Products/Buy' and 'Store/Buy'
[HttpPost("Checkout")] // Matches 'Products/Checkout' and 'Store/Checkout'
public IActionResult Buy()
}
在控制器中的 HTTP 操作谓词中传递路由值
我们不需要将路由值作为路由属性传递,而是可以在 HTTP 动作动词中传递路由值,例如HTTPGet和HTTPPost。
在下面的代码中,我们使用了HTTPGet属性来传递路由值。对于Index方法,我们没有传递任何值,因此在controller方法级别定义的路由值不会附加任何路由值。对于Index2方法,我们传递Index3值。Index3将附加到controller级别定义的路由值。请注意,只有带有 GET 方法的 URL 才会映射到action方法。如果您使用POST方法访问相同的 URL 模式,这些路由将不会匹配,因此这些action方法将不会被调用:
[Route("Home")]
public class HomeController : Controller
{
// GET: /<controller>/
[HttpGet()]
public IActionResult Index()
{
return Content("Index action method");
}
[HttpGet("Index3")]
public IActionResult Index2()
{
return Content("Index2 action method");
}
}
活动:用不同的动词定义两个同名的动作
场景
你的任务是用不同的动词定义两个名称相同的动作,如POST和GET。您将如何做到这一点,使您对控制器更加安心?
瞄准
定义两个名称相同但动词不同的动作,如POST和GET。
完成步骤
使用以下代码:
Go to https://goo.gl/nxs7tK to access the code.
[Route("Home")]
public class HomeController : Controller
{
// GET: /<controller>/
[HttpGet()]
public IActionResult Index()
{
return Content("Index action method");
}
[HttpGet("Index3")]
public IActionResult Index2()
{
return Content("Index2 action method");
}
[HttpPost("Index3")]
public IActionResult Index2_Post()
{
return Content("Index2 post method");
}
}
请参见 Fiddler 输出,如下所示:

路线约束
路由约束使您能够约束传递给控制器操作的值的类型。这完全是关于路由的操作选择。所以,我们可以说一个动作应该触发整数输入,而另一个动作应该触发非整数输入。例如,如果要限制传递给int类型的值,可以这样做。以下是其中一个例子:
[HttpGet("details2/{id:int}")]
public IActionResult Details2(int id = 123)
{
return View();
}
ASP.NET Core 甚至支持默认参数值,以便您可以传递默认参数:
[HttpGet("details2/{id:int}")]
public IActionResult Details2(int id = 123)
{
return View();
}
有许多可用的约束,例如int和bool。
活动:创建实现 IActionConstraintFactory 的属性
场景
通过使用IActionConstraint和IActionConstraintFactory接口,我们可以添加自定义约束。创建一个实现IActionConstraintFactory并返回IActionConstraint的属性。使用它并修改下面的代码,以便对Accept: text/html header调用Index2_HTML,而对Accept: application/json调用Index2_Json。您可以使用 Fiddler 发送请求。
[Route("Home")]
public class HomeController : Controller
{
// GET: /<controller>/
[HttpGet()]
public IActionResult Index()
{
return Content("Index action method");
}
[HttpGet("Index2")]
public IActionResult Index2_HTML()
{
return Content("HTML response returns");
}
[HttpGet("Index2")]
public IActionResult Index2_JSON()
{
return Content("Json response returns");
}
}
瞄准
创建实现IActionConstraintFactory并返回IActionConstraint的属性
完成步骤
- 我们首先创建
AcceptHeaderActionConstraint如下:
Go to https://goo.gl/nUvECW to access the code.
public class AcceptHeaderActionConstraint : IActionConstraint
{
readonly string headerValue;
public AcceptHeaderActionConstraint(string headerValue)
{
this.headerValue = headerValue;
}
public int Order => 0;
public bool Accept(ActionConstraintContext context)
{
var headerVal = context.RouteContext.
HttpContext.Request.Headers["Accept"];
return headerVal.Contains(this.headerValue);
}
}
- 然后,我们创建我们的行动,如下所示:
Go to https://goo.gl/HdMzjq to access the code.
public class AcceptHeaderAttribute : Attribute,
IActionConstraintFactory
{
readonly string value;
public AcceptHeaderAttribute(string value)
=> this.value = value;
public bool IsReusable => true;
public IActionConstraint CreateInstance(IServiceProvider services)
=> new AcceptHeaderActionConstraint(this.value);
}
- 并装饰我们的行为,如下所示:
Go to https://goo.gl/dK9ZJ1 to access the code.
[Route("Home")]
public class HomeController : Controller
{
// GET: /<controller>/
[HttpGet()]
public IActionResult Index()
{
return Content("Index action method");
}
[AcceptHeader("text/html")]
[HttpGet("Index2")]
public IActionResult Index2_HTML()
{
return Content("HTML response returns");
}
[AcceptHeader("application/json")]
[HttpGet("Index2")]
public IActionResult Index2_JSON()
{
return Content("Json response returns");
}
}
示例请求的输出如下所示:

注意,为了简单起见,我们只返回任意字符串。在实际应用中,我们将返回真实对象。
总结
在本章中,我们学习了路由及其工作原理。我们了解了各种可用的路由。我们用不同的例子讨论了基于约定的路由和基于属性的路由。我们还讨论了路由约束和可以传递的默认参数值。
在下一章中,我们将开始构建 Rest Buy 应用。
七、RestBuy
路由是 ASP.NET MVC 应用中最重要的概念之一,因为它负责处理传入的请求并将它们映射到相应的控制器操作。
在本章中,我们将尝试应用所学知识,并开始构建一个简化的实际应用。我们将开始开发一个名为Rest Buy的简单购物车应用。我们称之为 Rest-Buy,因为在开发过程中,我们还将尝试使用 Rest-ful 策略。
在 Rest Buy 项目中,我们将使用 ASP.NET Core MVC 以及实体框架和 SQL Server,因为这是本书的主题,并尝试采用 RESTful 策略。通过开发,您将更好地理解我们所说的 RESTful 是什么意思。
请注意,现在,单页应用(SPA)非常流行。对于在 Angular 和 React(用于单页应用)等框架上工作的开发人员来说,有很多工作机会。这些应用可以将整个页面变成一个 HTML 文档。这有一些显著的优势,因为客户端开发人员几乎可以完全独立于服务器端工作,他们开发 UI 就像开发任何移动应用一样。此外,它们还可以将自己与服务器中使用的技术堆栈隔离开来。除此之外,Visual Studio 2017 现在还提供了可供使用的单页应用模板。然而,在本课程中,我们将不构建单页应用,因为我们认为这种方法也有一些潜在的缺点,或者至少有时会有点不舒服。
在 SPA 方法中,我们通常首先加载 HTML 页面而不加载任何数据。稍后,数据将通过第二个 AJAX 请求加载。这可能需要,至少是第一次,您的应用具有某种初始加载屏幕。这种加载屏幕在移动应用中是可以容忍的。然而,当涉及到 web 应用时,您的访问者甚至不希望等待几秒钟。
其次,尽管现在的搜索引擎可以呈现 JavaScript,但使用 SPA 方法进行搜索引擎优化仍然很棘手。
第三,路由机制可能存在一些差异,因为使用 SPA,您开始从 JavaScript 以及服务器端管理 URL 路由。这种二元性有时很难处理。
最后,从 RESTful 的角度来看,我们想,一旦我们点击了一个 URL,比如/Products,当我们使用text/htmlaccept 头时,我们的目的是将产品表示为 HTML,而不仅仅是伪 SPA HTML,然后在那里加载产品。我们知道万维网是一项经过验证的技术,它背后的理念,如 Rest 和超媒体,是我们可以信赖的。
尽管如此,这并不意味着我们可以忽略客户端状态。我们仍然需要使用 AJAX,这不仅是为了改善用户体验,也是因为浏览器负责维护其内存中的应用状态。(我们也可以使用 cookies 和本地存储,但这会有点困难。)
总之,我们决定采用混合方法。我们将在需要时简要地将这些 SPA 框架用于页面片段,但通常也会坚持使用服务器端呈现和路由。在某些情况下,可以先从服务器端渲染某些部分,然后从客户端渲染。这是一种固执己见的方法,我们建议你在职业生涯中坚持任何适合你的方法。
在本章结束时,您将能够:
- 设计休息购买
- 为应用创建实体
- 创建 EF 上下文和迁移
设计 Rest 购买
我们将以迭代的方式开始开发一个简单的购物车应用。功能、屏幕和用户故事也将在此过程中进行解释。但是,为了简单起见,我们将为您提供一个基本的框架或框架,您需要填充剩余的事务。本应用的目的是了解实际应用是如何完成的。我们将假定数据(如产品)是通过其他方式输入数据库的。在本例中,我们将手工编辑或使用数据库迁移。
在本书中,我们将在第 8 章中介绍注册特性,添加特性、测试和部署。如前所述,我们正在跟踪应用的迭代构建。因此,当开发下一个功能并且您希望参考它们时,您可以在此处找到它们:https://github.com/OnurGumus/Packt_ASPNET_Core_RestBuy
特色和故事
让我们定义我们的用户故事和功能:
| 特征 | 故事 |
| | 功能
1 | 功能
2 | 功能
3 | 功能
4 |
| 列出产品 | 作为已注册或未注册的客户,当您访问主页时,应显示产品列表。 | 作为产品列表页面中的已注册或未注册客户,当您单击产品时,应显示产品详细信息页面。 | | |
| 搜索
并过滤产品 | 作为已注册或未注册的客户,您应该能够按类别筛选产品。一个产品可以与多个类别关联。因此,当您选择一个或多个类别并单击过滤器时,应该应用过滤器,直到页面更改。 | 作为已注册或未注册的客户,您应该能够按价格范围筛选产品。在这里,将有一个最低和最高的价格,当您选择一个范围,这应该与类别过滤器一起应用。 | 作为已注册或未注册的客户,当您在搜索文本框中写入一些文本时,它应该过滤产品的描述和名称。 | |
| 展示
产品
详情 | 作为已注册或未注册的客户,您应该能够为每个产品添加书签,并使用唯一的 URL 显示这些产品。 | 作为已注册或未注册的客户,当您访问产品页面时,该页面应显示产品的名称、照片、说明和价格。 | | |
| 购物车 | 作为已注册或未注册的客户,您应该能够向购物车添加或删除产品。 | 作为已注册或未注册的客户,您应该能够利用“结帐”面板查看当前添加的项目。 | 作为已注册或未注册的客户,您应该能够通过单击“结帐”面板转到“结帐”页面。 | 作为注册
客户,您应该能够签出
并确认
您的购物车
转换为订单。 |
| 管理
订单 | 作为注册客户,您应该能够查看您的订单详细信息。 | | | |
| 登录 | 作为注册客户,您应该能够登录到应用。 | | | |
| 注销 | 作为注册客户,您应该能够注销应用。 | | | |
From the agile point of view, features and stories are not hierarchical. Features are more like tags to the user stories. Still, for simplicity and clarification, it is shown as hierarchical.
布局和页面
在定义了我们的特性之后,设计应用的一个好方法是 UX。用户体验指的是用户体验。用户体验并不意味着我们应该首先编程或设计用户界面。UX 帮助我们从用户的角度确定应用的可能流程,这些流程必须得到功能的支持和补充。
主页
主页的外观可以类似于以下屏幕截图所示:

产品详情
“产品详细信息”页面的外观可能与以下屏幕截图中显示的类似:

结账
“签出”页面的外观可能类似于以下屏幕截图中显示的内容:

结帐成功
“Checkout Successful”(签出成功)页面的外观可能类似于以下屏幕截图所示:

以前的订单
上一个订单页面的外观可能类似于以下屏幕截图中显示的内容:

正如您在一些屏幕截图中看到的,我们省略了过滤器。这是一个武断的决定;我们认为,对于我们没有显示产品的页面,保留过滤器是没有意义的,但我们仍然将搜索框保持在顶部。
定义我们的领域和模型
下一步是定义我们的模型。在这里,我们将尝试应用领域驱动设计。在领域驱动设计中,我们根据业务领域和术语对类进行建模。显然,如果您已经阅读了用户故事,我们已经有了很多关于域、类及其属性的线索。
目前,我们可以将我们的域建模为 5 个类:产品、订单、用户、订单项和库存量。在这种情况下,产品和用户是单独的聚合。聚合是一个领域驱动的开发概念。聚合用于指定域类的独立部分。通常,每个聚合由一个或多个类组成,每个类表示一组独立的不变量。我们有以下不变量:
- 订单至少有一个订单项。
- 订单具有创建日期和用户 ID。
- 订单项表示数量、价格和产品 ID。
- 对于订单项,数量和价格不能为零。
- 未确认订单表示当前购物车。
- 客户最多可以有 1 个未确认订单。(签出完成时降至零)
- 对于订单项,产品 ID 必须有效。对于产品,库存量和价格必须大于或等于零。
- 对于 StockAmount,数量必须等于订单项中的数量。

在上图中,我们定义了三个聚合为订单、产品和用户。上面的虚线表示通过 ID 字段的间接引用,订单和订单项之间的实线表示直接引用。我们选择这种方法的原因是,应用的不同模块(如管理模块)可能希望独立处理聚合。更改用户的邮政地址不应影响用户的现有订单和订单项目。他们彼此非常独立。我们还可以将这些实体移动到不同的微服务。毕竟,我们的业务不变量和约束只适用于每个聚合。同样,没有绝对正确的方法可以做到这一点,这取决于项目以及您对未来的投资。对于购物车本身,如果订单未确认,我们将使用订单实体。
创建 RestBuy 项目
在掌握了最初的设计思想后,我们可以开始在实践中实施我们的项目。遵循以下步骤:
- 创建一个新的解决方案和 Web 应用,如下面的屏幕截图所示。确保我们将项目命名为
RestBuy.Web,解决方案名称为RestBuy:

- 选择 MVC 选项并单击 OK:

您将看到:

- 将新项目添加为名为
RestBuy的.NET 标准类库:

- 将另一个项目添加为名为
RestBuy.InfraStructure的.NET 标准类库:

- 右键单击
RestBuy.Web项目解决方案资源管理器中的依赖项部分,并将依赖项添加到其他项目:

- 与
RestBuy.InfraStructure类似,添加对RestBuy的引用。
The RestBuy project itself references no other project.
现在,您的项目结构将如下所示:

In case our business logic is not aware of the database layer, we will put our database interfaces to the business layer and do their implementations in the infrastructure and use a dependency injection (not inversion) framework to inject those interfaces to retrieve data from the database. This is the dependency inversion principle.
活动:为网站准备功能和故事
场景
你的客户碰巧是一位时装设计师。他想让你为他建一个网站。你需要先为他的网站准备功能和故事。
瞄准
为网站准备功能和故事。
完成步骤
- 列出时装设计师所做的所有活动。
- 列出他开展业务所需的网页。
- 写下这些特征以及伴随它们的故事。
活动:为网站准备线框图
场景
您的客户,即时装设计师,希望查看网页的线框图。您可以为主页准备线框图。
瞄准
为网页准备线框图。
完成步骤
- 转到https://www.draw.io/ 。
- 在“将图表保存到:”对话框中,选择“设备”。
- 在“设备”对话框中,单击“创建新图表”。
- 在下一个窗口中,选择 Basic(1),为文件命名,然后单击 Create。
- 您将看到工作区屏幕,如以下屏幕截图所示:

- 单击屏幕左下角的“更多形状”按钮(在前面的屏幕截图中用蓝色框标记)。
- 在“形状”对话框中,选择“软件”类别下的“实体模型”。您可以取消选中其余选项。
- 单击应用。你准备好出发了。
活动:为网站设计域模型
场景
你想为你正在为你的客户(时装设计师)构建的网站设计一个域模型。
瞄准
为网站设计域模型。
完成步骤
- 列出为域建模所需的类。
- 定义聚合。
- 使用 draw.io 设计图表,并提及聚合和类。
- 使用虚线显示类之间的间接引用。
- 使用实线显示类之间的直接引用。
创建实体
现在我们已经澄清了依赖关系,我们可以开始创建实体了。您可以删除库中已有的Class1.cs文件。
按照以下步骤为 Rest Buy 创建实体:
- 在
RestBuy项目中创建Entities文件夹,如下图所示:

- 在
Entities文件夹中,我们需要创建四个类:BaseEntity:这是我们所有实体的基类Order:此类将包含产品订购背后的逻辑。OrderItem:此类用于包含订单明细。Product:此类用于包含产品的详细信息。StockAmount:此类表示一个类型的商店中剩余的产品数量。
- 在
BaseEntity内有此代码:
Go to https://goo.gl/E8DaGb to access the code.
using System;
using System.Collections.Generic;
using System.Text;
namespace RestBuy.Entities
public abstract class BaseEntity
{
protected int id;
public int Id => this.id;
}
}
- 在
Order内有此代码:
Go to https://goo.gl/7JBmVH to access the code.
using System;
using System.Collections.Generic;
using System.Text;
namespace RestBuy.Entities
{
public class Order : BaseEntity
{
int userId;
DateTime createDate;
List<OrderItem> orderItems = new List<OrderItem>();
private Order() { }
...
...
}
}
- 在
OrderItem内有此代码:
Go to https://goo.gl/J11TQu to access the code.
using System;
using System.Collections.Generic;
using System.Text;
namespace RestBuy.Entities
{
public class OrderItem : BaseEntity
{
int productId;
int quantity;
decimal price;
private OrderItem() { }
public OrderItem(int productId, int quantity, decimal price)
...
...
}
}
- 在
Product内有此代码:
Go to https://goo.gl/MPukF3 to access the code.
using System;
using System.Collections.Generic;
using System.Text;
namespace RestBuy.Entities
{
public class Product : BaseEntity
{
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public string PictureUri { get; set; }
public string Category { get; set; }
}
}
- 在
StockAmount内有此代码:
Go to https://goo.gl/RmgHzn to access the code.
using System;
using System.Collections.Generic;
using System.Text;
namespace RestBuy.Entities
{
public class StockAmount : BaseEntity
...
...
}
创建 EF 上下文和迁移
首先,我们需要在Infrastructure项目中安装实体框架及其工具。
按照以下步骤创建 EF 上下文:
- 只需右键单击 dependencies 并选择 Manage NuGet Packages。
- 然后在搜索框中写入
Microsoft.EntityFramework.Core.SqlServer并安装。
您的屏幕应如下所示:

- 同样,安装
Microsot.EntityFrameworkCore.Tools包,如下所示:

因此,您的Infrastructure项目文件夹如下所示:

- 接下来,我们在
Infrastructure项目中创建EF文件夹,并使用名为RestBuyContext的类实现我们的DbContext。确保您在Infrastructure.中引用了RestBuy项目。请使用以下代码:
Go to https://goo.gl/wYLwRA to access the code.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using RestBuy.Entities;
using System;
using System.Collections.Generic;
using System.Text;
...
void ConfigureStockAmount(EntityTypeBuilder<StockAmount> builder)
{
...
builder.Property(ci => ci.Quantity).IsRequired().IsConcurrencyToken();
}
...
In the preceding mapping, the Quantity property of StockAmount is marked as ConcurrencyToken as we don't want two orders to reduce the stock amount simultaneously. Suppose that we have two parallel requests racing to buy for the last item. Only one of them can win. Using ConcurrencyToken causes Entity Framework to generate a query that confirms that the value has not been changed. Doing so will cause DbUpdateConcurrencyException; in this case, we have to retry.
这里我们使用 HiLo 算法生成密钥。如果不使用 HiLo,通常 EF 和 SQL server 使用自动递增的 ID。虽然自动递增的 ID 更简单,但只要将实体添加到上下文中,此添加就会强制将实体插入到数据库中。这是因为只有在实际插入发生在自动递增 ID 的情况下,我们才能检索 ID。HiLo 算法通过使用数据库序列预先保留 ID,使我们摆脱了这种限制。我们还更改了序列的默认值,使其从 1000 开始,每增加 100。通过使用这种方法,我们可以将设计时数据插入到前 1000 个可用插槽中。
创建迁移
一旦定义了数据库上下文,下一步就是生成迁移。然而,由于我们现在有一个基础设施项目,我们更希望我们的基础设施项目成为迁移的宿主。
为此,请执行以下步骤:
- 在
EF文件夹中创建RestBuyContextFactory:
Go to https://goo.gl/BnBPLD to access the code.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Infrastructure;
using RestBuy.Infrastructure.EF;
using System;
using System.Collections.Generic;
using System.Text;
namespace RestBuy.Infrastructure.EF
{
class RestBuyContextFactory : IDesignTimeDbContextFactory<RestBuyContext>
{
public RestBuyContext CreateDbContext(string[] args)
...
...
}
}
- 看看文件夹结构。我们的
EF文件夹如下所示:

注意,我们将连接字符串嵌入到这个工厂类中。原因是,正如接口的名称所暗示的,任何实现IDesignTimeDbContextFactory的东西都只用于设计时。也就是说,我们的迁移生成工具将利用这一点。另一方面,由于我们使用了可信连接,因此将其作为字符串嵌入是相对安全的。如果它包含任何密码,无论设计时与否,将这些字符串嵌入二进制文件是一个非常糟糕的想法。
- 显然,下一步是添加迁移。现在,让我们打开 package manager,您可以在快速启动搜索中键入 package:

以下内容将显示在您的屏幕上:

- 然后,确保在 package manager 控制台中选择我们的基础设施项目类型
Add-MigrationInitialMigration。运行此命令将在我们的Infrastructure项目中生成迁移:

- 接下来,我们使用
Update-Database命令创建数据库:

然后,我们可以看到我们的表是从 SQL Server 对象资源管理器创建的:

活动:添加表示产品供应商的供应商实体
场景
您希望添加一个表示产品供应商的供应商实体。
瞄准
为产品添加供应商实体。
完成步骤
- 我们可以添加
Supplier类如下:
Go to https://goo.gl/pTCc56 to access the code.
namespace RestBuy.Entities
{
class Supplier : BaseEntity
{
public string Name { get; set; }
}
}
或者,我们可以向Product或SupplierId添加Supplier属性,具体取决于我们是否希望它们处于相同的聚合中。这是一个设计选择。
- 对于迁移,我们使用以下代码:
void ConfigureSupplier(EntityTypeBuilder<Supplier>builder)
{
builder.ToTable("Suppliers");
builder.HasKey(ci => ci.Id);
builder.Property(ci => ci.Name)
.IsRequired()
.HasMaxLength(50);
builder.HasIndex(c => c.Name).IsUnique();
}
总结
在本章中,我们设计了我们的应用。我们继续为我们的应用创建实体。最后,我们研究了创建 EF 上下文和迁移。
八、添加功能、测试和部署
在本章中,我们将介绍如何将注册功能添加到应用中。接下来将创建一个单元测试。这里的目标是帮助您添加一个特性并对其进行测试。最后,我们将把我们的项目升级到 bootstrap4。
我们将继续实施 Rest Buy 服务。这一部分比较关键,因为我们将主要实现逻辑来实现我们的应用。
在本章结束时,您将能够:
- 添加注册功能
- 创建单元测试
- 将我们的项目升级到 bootstrap4
- 将 Rest Buy 部署到 Azure
添加注册功能
因为我们将开始向应用添加逻辑,所以现在是向项目添加应用层的好时机。正如我们在上一章中所讨论的,我们将应用设计为分层的,并遵循领域驱动的设计标准。
在领域驱动设计中,一种常见的分层方法是洋葱架构。在洋葱架构中,每一层都可以利用内层,但外层必须适应内层。我们尝试从内到外设计我们的应用。这就是我们从实体开始设计应用的原因:

在上图中,我们的核心是一个域模型,在此基础上还有应用服务。紫色的矩形是接口,黑色的箭头表示编译时依赖关系,洋红色的圆圈表示基础设施的外部依赖关系。
应用服务用于处理命令和请求。
There is an on-going discussion on whether entities in the domain model should be accessible outside. Some people prefer using DTOs to pass the data evenly between layers. We will not go with that approach here. Instead, we will ensure the entities are immutable from the outside layers.
登录和注销机制
首先,让我们从我们的登录和注销机制开始。我们需要一个User实体。到目前为止,我们还没有创建一个。让我们创建它。
按照以下步骤开始使用我们的登录和注销机制:
- 使用以下代码:
Go to https://goo.gl/cD8tDQ to access the code.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace RestBuy.Entities
{
public class User : BaseEntity
{
...
...
}
}
有趣的是,我们定义了一个散列密码算法。我们还使用一种秘盐。这样,即使我们的密码数据库被破坏,我们的用户密码也不容易恢复(当然,还要结合强大的密码策略)。通过使用 salt,在本例中为secretBytes和用户名,我们实现了两件事:
- 通过添加以下代码更新我们的
RestBuyContext:
Go to https://goo.gl/wWvhiL to access the code.
void ConfigureUser(EntityTypeBuilder <User> builder)
{
builder.ToTable(userTable);
builder.HasKey(ci => ci.Id);
builder.Property(ci => ci.UserName)
.IsRequired()
.HasMaxLength(50);
builder.Property(ci => ci.Password)
.IsRequired();
}
我们班现在是这样的:
Go to https://goo.gl/CjSv8g to access the code.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using RestBuy.Entities;
using System;
using System.Collections.Generic;
using System.Text;
namespace RestBuy.Infrastructure.EF
{
public class RestBuyContext : DbContext
{
...
...
}
}
- 最后,我们在 package manager 控制台中使用
Add-Migration User添加迁移:
Do not forget to change the default project to Infrastructure; otherwise, you will get an error.
- 然后,我们最终更新了数据库:
PM> Update-Database
Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
User profile is available. Using 'C:\Users\Onur.Gumus\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
Applying migration '20170909141639_User'.
Done.
PM>
创建应用层
对于应用层,我们需要创建另一个项目。
按照以下步骤开始创建应用层:
- 创建另一个项目,如下所示:

This project will contain the application service as well as the necessary interfaces required for the external world.
- 我们在
RestBuy.Application中增加RestBuy的引用,如下所示:

- 接下来,我们将开始定义接口。首先,让我们在
RestBuy.Application项目的Services/Queries(如果不存在这些文件夹,则创建它们)文件夹中定义一个IQuery<T>接口,如下所示:
Go to https://goo.gl/K9QpWN to access the code.
using RestBuy.Entities;
using System;
using System.Linq.Expressions;
namespace RestBuy.Application.Services.Queries
{
public interface IQuery<T> where T : BaseEntity
{
Expression<Func<T, bool>> Criteria { get; }
int Take { get; }
...
...
int Skip { get; }
}
}
- 在
BaseQuery中增加一个默认实现,如图所示:
Go to https://goo.gl/HNHd9k to access the code.
using RestBuy.Entities;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text;
namespace RestBuy.Application.Services.Queries
{
public abstract class BaseQuery<T> : IQuery<T> where T: BaseEntity
...
...
}
We will use this interface to query our repositories. The Take property represents how many items we want to take from the database and Skip is used for setting how many items to skip. These properties are used for the paging of the records. We will also define a lambda expression that denotes the filtering.
- 接下来,为
RestBuy.Application项目的Repos文件夹中的所有实体定义基础回购(如果不存在,请再次创建):
Go to https://goo.gl/nzouBU to access the code.
using RestBuy.Entities;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using RestBuy.Application.Services.Queries;
namespace RestBuy.Application.Repos
{
public interface IEntityRepo<T> where T : BaseEntity
{
Task<T> GetById(int id, CancellationToken cancellationToken = default);
Task<List<T>> ListAsync(IQuery<T> query = null, CancellationToken cancellationToken = default);
}
}
When using default keyword, the IDE will warn you if you want to use the latest version of C#. Comply to it.
我们之所以使用Task,是因为大多数数据库操作都是异步的和可取消的(实际上,大多数 IO 操作都是异步的)。
- 然后,我们将
IUserRepo接口定义如下:
Go to https://goo.gl/Uykbex to access the code.
using RestBuy.Entities;
using System.Threading;
using System.Threading.Tasks;
namespace RestBuy.Application.Repos
{
public interface IUserRepo : IEntityRepo<User>
{
Task AddAsync(User user, CancellationToken ct = default());
}
}
我们在这里只定义了一个Add方法,因为目前我们只计划添加用户。根据我们的需要,我们可以修改这个界面。
- 最后,我们定义了一个 IUoW 接口,它代表工作单元,如下所示:
Go to https://goo.gl/ePL5Fc to access the code.
using System.Threading;
using System.Threading.Tasks;
namespace RestBuy.Application.Repos
{
public interface IUoW
{
Task SaveChangesAsync(CancellationToken cancellationToken = default);
}
}
UoW represents a database transaction.

接下来,让我们在基础设施项目中进行实现。
在基础设施项目中执行实现
按照以下步骤在Infrastructure项目中执行:
- 我们首先在
Infrastructure中添加对Application项目的引用:

- 然后我们实现我们的接口,从
Infrastructure项目的EF文件夹中的 IUoW 开始:
Go to https://goo.gl/SFBMLt to access the code.
using RestBuy.Application.Repos;
using System.Threading;
using System.Threading.Tasks;
namespace RestBuy.Infrastructure.EF
{
public class RestBuyUoW : IUoW
{
...
...
}
}
- 接下来,定义一个
BaseRepo,它将是我们所有存储库实现的基类:
Go to https://goo.gl/s7LVBU to access the code.
using Microsoft.EntityFrameworkCore;
using RestBuy.Application.Repos;
using RestBuy.Application.Services.Queries;
using RestBuy.Entities;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace RestBuy.Infrastructure.EF
{
...
if (query.Skip > 0)
{
filterQuery = filterQuery.Skip(query.Skip);
}
if (query.Take > 0)
{
filterQuery = filterQuery.Take(query.Take);
}
if (query.OrderBy != null || query.OrderByDescending != null)
...
}
前面的代码基本上检查查询是否与Take、Skip或OrderBy子句相关,并相应地修改查询(取决于定义的查询的这些部分)。
- 现在,定义
UserRepo:
Go to https://goo.gl/9wPekN to access the code.
using RestBuy.Application.Repos;
using RestBuy.Entities;
using System.Threading;
using System.Threading.Tasks;
namespace RestBuy.Infrastructure.EF
{
public class UserRepo : BaseRepo<User> , IUserRepo
{
public UserRepo(RestBuyContext restBuyContext) : base(restBuyContext)
{ }
public Task AddAsync(User user, CancellationToken ct = default) =>
this.restBuyContext.Users.AddAsync(user, ct);
}
}
我们的 EF 文件夹如下所示:

最后,我们需要定义我们的服务接口。我们将从用户注册开始。首先,让我们为注册定义 ViewModel。我们将直接在应用层中定义 ViewModel,因为这将是我们服务接口的输入。通常在洋葱分层架构中,我们尽量不跨层边界传递数据。这就是为什么我们不直接使用实体。这条规则的一个显著例外是持久性。由于持久化层只负责持久化实体,因此直接将实体传递到持久化层并没有什么害处。尽管如此,这仍然是一个激烈争论的话题。
为注册定义 ViewModel
按照以下步骤定义要注册的 ViewModel:
- 让我们在
RestBuy.Application项目中创建一个ViewModels文件夹,并添加一个名为NewUserViewModel的类:

- 在
NewUserViewModel中使用此代码:
Go to https://goo.gl/ATiDPz to access the code.
using RestBuy.Entities;
using System.ComponentModel.DataAnnotations;
namespace RestBuy.Application.ViewModels
{
public class NewUserViewModel
{
[Required, MaxLength(50)]
public string Username { get; set; }
[Required, DataType(DataType.Password)]
public string Password { get; set; }
[DataType(DataType.Password), Compare
(nameof(Password))]
public string ConfirmPassword { get; set; }
internal User CreateUser() =>
new User(this.Username, this.Password);
}
}
Make sure you add System.ComponentModel.DataAnnotations.dll from the Assemblies Framework section of the Add References popup.
注意CreateUser模型。此方法从ViewModel构建User对象。或者,我们可以使用工厂模式。请注意,我们将该方法标记为内部。这是为了我们不希望从其他层调用这些工厂方法。
- 创建托管我们服务的
Services文件夹并定义IRegistrationService:

- 在
IRegistrationService内有此代码:
Go to https://goo.gl/wNEq3F to access the code.
using RestBuy.Application.ViewModels;
using System.Threading;
using System.Threading.Tasks;
namespace RestBuy.Application.Services
{
public interface IRegistrationService
{
Task RegisterUser(NewUserViewModel newUserViewModel, CancellationToken token = default);
}
}
- 接下来,将我们的应用项目的引用添加到 web 项目:

最后,我们可以开始定义控制器。首先,让我们定义Accounts控制器。您可以删除已有的HomeController。
定义我们的控制器
按照以下步骤定义控制器:
- 通过右键单击
Controllers文件夹,添加新项目,然后选择 MVC 控制器类,在Controllers文件夹中创建新控制器:

我们的Web项目如下:

- 对于 RESTful 实践,当
GET请求到达Accounts控制器时,我们将返回一份登记表。使用此代码:
using Microsoft.AspNetCore.Mvc;
namespace RestBuy.Web.Controllers
{
[Route("[controller]")]
public class AccountsController : Controller
{
[HttpGet]
public IActionResult RegistrationForm()
{
return View();
}
}
}
- 定义我们的观点:


- 将以下代码添加到其中:
Go to https://goo.gl/SvE6Qv to access the code.
@model RestBuy.Application.ViewModels.NewUserViewModel
@{
ViewBag.Title = "Register";
}
<h1>Register</h1>
<form method="post" >
<div asp-validation-summary="ModelOnly"></div>
...
...
<div>
<input type="submit" value="Register" />
</div>
</form>
We have not specified any path for posting in our form, because we will post to the same URL as with this page.
现在,如果我们运行我们的应用并尝试从http://localhost:49163/Accounts访问我们的 AccxOuts 页面(您的端口号可能不同),我们将得到以下表单:

显然,表单看起来没有对齐,但这是我们稍后将要解决的问题。
注册成功后,我们还需要一个登录页。
创建注册后登录页
按照以下步骤创建注册后登录页:
- 我们可以使用相同的表单页面,但在本例中,我们将在
Accounts文件夹中创建SuccesfullyRegistered.cshtml,如下所示:

- 使用以下代码:
@{
ViewBag.Title = "Registration Successful";
}
<h1>You have registered successfully!</h1>
<ul>
<li><a asp-action="" asp-controller="">Home</a></li>
<li><a asp-action="" asp-controller="">Login</a></li>
</ul>
- 在前面的代码中,我们将在后面填写必要的 action 和 controller 字段。尽管此页面过于简单,但就本演示而言,它暂时是可以接受的。最后一步是修改我们的控制器,如下所示:
using Microsoft.AspNetCore.Mvc;
using RestBuy.Application.Services;
using RestBuy.Application.ViewModels;
using System.Threading;
using System.Threading.Tasks;
namespace RestBuy.Web.Controllers
{
[Route("[controller]")]
...
...
}
我们已经将我们的IRegistrationService添加到构造函数中。我们的服务将在我们针对一个具体的服务(我们还没有编写)注册后由 ASP.NET 运行时注入。
其次,我们定义了一个Register方法来获取 ViewModel 和取消令牌。取消令牌在这里是完全可选的。ASP.NET 运行时具有智能行为:无论何时从客户端中止 HTTP 请求,它都将触发取消令牌。通常,建议为任何 I/O 呼叫传递取消令牌。在本例中,我们将查询数据库,但如果您发现这种方法过于冗长,可以跳过它。接下来,我们将检查 ViewModel 是否有效。
Remember that we have decorated our view model with few attributes. At the model binding phase, ASP.NET Runtime automatically validates our model and sets the model state accordingly. If our model is valid, then we proceed with the registration by calling the registration service and returning the view.
如果视图无效,我们将显示完全相同的表单页面及其验证错误,这些错误将自动显示。
此时,我们建议您针对控制器编写一个单元测试来验证行为,但我们将跳过该测试,直接实现注册服务。我们的注册服务必须检查数据库中是否存在这样的用户。因此,我们首先为它创建一个查询,它将从IQuery<User>开始实现。
为注册服务创建查询
我们创建查询对象而不是使用 linq,因为这些类型的查询与业务逻辑相关联,将查询包装到对象中允许我们重用它们以及对它们进行单元测试。此外,使用类命名它们可以为我们提供更多关于查询目的的线索,而不是即时查询。
按照以下步骤为注册服务创建查询:
在Application项目的Queries文件夹中,我们创建了一个UserExistsQuery类,如下所示:
Go to https://goo.gl/3r92QU to access the code.
using RestBuy.Entities;
using System;
using System.Linq.Expressions;
namespace RestBuy.Application.Services.Queries
{
class UserExistsQuery : BaseQuery<User>
{
public UserExistsQuery(string userName) =>
this.UserName = userName;
public string UserName { get; }
public override Expression<Func<User, bool>>
Criteria =>
u => u.UserName == this.UserName;
public override int Take => 1;
}
}
基本上,此查询对象搜索具有给定用户名的用户,并通过设置Take = 1尝试获取第一条记录。
Remember, one of the goals of software engineering is to have readable and understandable code. Short code doesn't always mean good code. So whenever necessary, feel free to create new classes and name them properly.
正在验证注册
按照以下步骤验证注册:
- 我们在
Services/Core文件夹中执行服务实现。我们的服务实施如下:
Go to https://goo.gl/Xxv3kj to access the code.
using RestBuy.Application.ViewModels;
using RestBuy.Application.Repos;
using RestBuy.Application.Services.Queries;
using System.ComponentModel.DataAnnotations;
using System.Threading;
using System.Threading.Tasks;
using RestBuy.Application.Services;
namespace Restbuy.Application.Services.Core
{
public class RestBuyRegistrationService : IRegistrationService
{
readonly IUserRepo userRepo;
readonly IUoW uow;
...
...
}
}
在构造器中,我们的工作单元被注入。然后我们履行我们的接口合同。通过使用验证上下文,我们再次验证了视图模型。虽然我们将在控制器中验证视图模型,但应用不(也不应该)知道。
As a security practice, inner layers should not trust upper layers when it comes to data validation.
- 因为我们正在从应用层抛出一个验证异常,所以我们的控制器应该处理并显示与此相关的验证消息。我们将我们的注册方法修改如下:
Go to https://goo.gl/KWuyPd to access the code.
[HttpPost]
public async Task<IActionResult> Register(NewUserViewModel newUserViewModel, CancellationToken cancellationToken)
{
if (ModelState.IsValid)
{
...
...
}
return View(nameof(RegistrationForm));
}
Don't forget to add using System.ComponentModel.DataAnnotations; at the beginning of the file.
如果发生验证错误,我们将在注册页面中显示它。
- 然后,我们调用 user exists 查询并获得同名用户。如果用户名已经存在,我们的
userList将有一个大于 0 的计数(实际上是 1,因为我们知道我们的查询使用 Take=1)。在这种情况下,我们需要拒绝注册。否则,我们通过从 UoW 获取UserRepo来注册用户,将其添加到用户 repo 中,并通过调用SaveChangesAsync提交更改。
There may be trouble if two requests come with the same username registration request. Although this is a tiny possibility, a malicious user can try to break our data.
我们当前的代码不检查这些并行请求,也不必检查。对于这类请求,我们应该在数据库中设置一个唯一的约束。数据库约束是最后一道防线。因为发生这种情况的几率很小(恶意用户案例除外),所以您的唯一约束将失败,并且您将显示一个错误页面,告诉用户发生了问题,他们应该重试该操作。因此,我们下一步将向Username属性添加我们的唯一索引。
- 通过修改
RestBuyContext的ConfigureUser方法,将唯一索引添加到UserName属性中,如下所示:
Go to https://goo.gl/Ncc42w to access the code.
void ConfigureUser(EntityTypeBuilder<User> builder)
{
builder.ToTable(userTable);
builder.HasKey(ci => ci.Id);
builder.Property(ci => ci.UserName)
.IsRequired()
.HasMaxLength(50);
builder.HasIndex(c => c.UserName).IsUnique();
builder.Property(ci => ci.Password)
.IsRequired();
}
注意,我们添加了HasIndex方法。
- 现在,我们需要通过在 package manager 控制台中执行以下命令来升级迁移和数据库:
Add-Migration AddUsernameIndex
Update-Database
Make sure you select RestBuy.Infrastructure before running the commands from the default project combobox.
我们的应用项目现在如下所示:

- 最后,我们需要创建服务注册。由于我们已经使用了接口及其实现,我们会将这些接口与其实现与内置的依赖注入机制相关联。在 web 项目中,我们将更新
Startup.cs中的ConfigureServices方法,如下所示:
Go to https://goo.gl/NJ2zvC to access the code.
public void ConfigureServices(IServiceCollection services)
{
services.AddEntityFrameworkSqlServer()
.AddDbContext<RestBuyContext>(options =>
options.UseSqlServer(Configuration.
GetConnectionString("DefaultConnection")));
services.AddScoped<IRegistrationService,
RestBuyRegistrationService>();
services.AddScoped<IUoW, RestBuyUoW>();
services.AddScoped<IUserRepo, UserRepo>();
services.AddMvc();
}
因此,我们进行了三次注册:IRegistrationService到DefaultRegistrationService、IUoW到RestBuyUoW、IUserRepo到UserRepo。我们使用范围方法,这样每个请求只创建一个这些服务的实例。在每次请求结束时,它们将被处理掉。
- 接下来,让我们设置连接字符串。我们在
Web项目中编辑appsettings.json:
Go to https://goo.gl/zTgTcc to access the code.
{
"ConnectionStrings":
{
"DefaultConnection": "Server=(localdb)\\
mssqllocaldb;Database=RestBuy;Trusted_
Connection=True;MultipleActiveResultSets=true"
},
"Logging":
{
"IncludeScopes": false,
"LogLevel":
{
"Default": "Warning"
}
}
}
现在我们可以运行我们的应用了。我们应该访问/Accounts页面查看登记表:

注册成功后,我们将重定向到注册成功页面:

如果我们检查我们的数据库,我们可以看到我们的用户已经被插入,如下所示:

创建单元测试
在过去的十年中,单元测试变得很流行。但是很多时候,单元测试的真正目标并没有被很好地理解,可能是由于命名的原因。诚然,单元测试在发现软件中的错误方面非常有用。
编写单元测试非常有用,即使您有一个只包含少量代码的函数。原因是,尽管单元测试是为了寻找 bug,但有一类 bug 是它们最有助于发现的。这就是回归。回归通常是通过在添加新代码时破坏现有功能来实现的。
关于回归,单元测试就像一个熔丝盒。它将确保,如果我们在没有意识到的情况下破坏现有功能,旧的测试将开始失败。如果他们不这样做,程序员会感到舒服,因为他或她知道现有功能不太可能被破坏。尽管单元测试不能保证这种安全性,但如果您不断地添加或更改代码,它将是一项宝贵的资产。许多人将单元测试视为用代码编写的应用规范。不可否认,测试驱动开发最近已经演变为行为驱动开发,开发人员开始将应用的正式规范作为单元测试来编写。
编写单元测试
按照以下步骤为我们的应用编写单元测试:
- 让我们创建一个单元测试。我们将其命名为
RestBuy.Test,如下图所示:

- 将
UnitTest1.cs重命名为EFTest并将appsettings.json从您的 web 项目复制到测试项目中。但将数据库名称更改为RestBuy_Test。
Make sure you add references to all other projects from Test. You also need a reference to Microsoft.EntityFramework.Core, and then install Microsoft.Extensions.Configuration from NuGet.
您的项目应如下所示:

- 现在,确保您的
appsettings.json文件类似于以下内容:
{
"ConnectionStrings":
{
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;
Database=RestBuy_Test;Trusted_Connection=True;
MultipleActiveResultSets=true"
},
"Logging":
{
"IncludeScopes": false,
"LogLevel":
{
"Default": "Warning"
}
}
}
- 右键单击“属性”中的
appsettings.json文件,通过为“复制到输出目录”选项选择“如果较新,则复制到输出文件夹”,确保该文件已部署到输出文件夹: - 确保您的
EFTest文件也类似于此:
Go to https://goo.gl/NK99He to access the code.
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using RestBuy.Entities;
using RestBuy.Infrastructure.EF;
using System.IO;
using System.Threading.Tasks;
namespace RestBuy.Test
{
...
...
}
这个测试基本上创建数据库,在一个上下文中将产品保存到数据库中,然后在另一个上下文中查询并提交它。
运行单元测试
按照以下步骤运行单元测试:
- 现在,要运行测试,请首先构建解决方案,然后从测试菜单打开测试资源管理器,如下所示:

- 然后右键单击测试并运行它:

我们的基本模型创建到此结束。
活动:编写用于删除的单元测试
场景
编写一个测试产品删除的单元测试。
完成步骤
- 我们可以按如下方式重构测试:
Go to https://goo.gl/EUCb5B to access the code.
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using RestBuy.Entities;
using RestBuy.Infrastructure.EF;
using System.IO;
using System.Threading.Tasks;
namespace RestBuy.Test
{
[TestClass]
public class EFTest
{
...
...
}
}
- 现在要运行测试,请首先构建解决方案,然后从测试菜单打开测试资源管理器,如下所示:

- 然后右键单击测试并运行它:

将我们的项目升级到 bootstrap4
在较新版本的 VisualStudio 中,Bower 已被删除。我们将考虑手动将项目升级为引导。如果您使用的是 ASP.NET Core 2.2 或更高版本,则可能不必执行以下步骤,因为这些较新版本附带了 Bootstrap 4。
按照以下步骤启动我们的项目:
- 打开
_Layout.cshtml。更新脚本引用,如下所示:
Go to https://goo.gl/GtuEk7 to access the code.
<!DOCTYPE html>
<html>
<head>
...
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/css/bootstrap.min.css" integrity="sha384-Zug+QiDoJOrZ5t4lssLdxGhVrurbmBWopoEl+
M6BdEfwnCJZtKxi1KgxUyJq13dy" crossorigin="anonymous">
...
</body>
</html>
请注意,我们应该在脚本部分中的引导之前将 popper 添加为 UMD 模块。此外,我们还添加了ValidationScriptsPartial页面。该文件由 Visual Studio 模板生成,并包含必要的客户端验证代码。
- 在
ValidationScriptsPartial.cshtml文件中,我们做了以下更改,以便在出现故障时添加相关的引导样式:
Go to https://goo.gl/xud8jd to access the code.
<environment include="Development">
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
</environment>
...
...
</script>
- 接下来,我们使用 Bootstrap 修改注册表:
Go to https://goo.gl/ad7JYA to access the code.
@model Restbuy.Application.ViewModels.NewUserViewModel
@{
ViewBag.Title = "Register";
}
<h1>Register</h1>
<form method="post">
<div asp-validation-summary="ModelOnly"></div>
<div class="form-group">
...
...
</form>
以下内容将显示在您的屏幕上:

我们的表单现在在/AccountsURL 中看起来好多了。
- 现在让我们点击注册按钮。

这看起来不错,但我们的密码字段看起来是绿色的,好像它是有效的。它是绿色的原因是我们忘记将Required属性添加到ViewModel和ConfirmPassword字段中。
- 更新
NewUserViewModel:
Go to https://goo.gl/7YN6ke to access the code.
using RestBuy.Entities;
using System.ComponentModel.DataAnnotations;
namespace RestBuy.Application.ViewModels
{
public class NewUserViewModel
{
[Required, MaxLength(50)]
...
...
}
}
- 之后,再次运行我们的页面:

这次我们成功地看到了三个红色方框。注意,即使我们禁用 JavaScript 验证,我们的表单仍然从服务器端受到保护。只是在这种情况下,我们不会看到这些红线,因为它们是由 JavaScript 生成的。还请注意,如果存在相同的用户,则客户端验证无法轻松涵盖发生的错误。相反,我们将使用
ValidationTagHelper。
- 将
TagHelpers文件夹添加到 Web 项目中,并创建ValidationClassTagHelper类,如下所示:
Go to https://goo.gl/3CBcBe to access the code.
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using System.Linq;
namespace RestBuy.Web.TagHelpers
{
...
...
}
前面的类只是根据值的有效性设置适当的引导类。
- 在
Views文件夹中更改您的ViewImports.chtml文件,如下所示:
@using RestBuy.Web
@using RestBuy.Web.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, RestBuy.Web
- 变更
RegistrationForm.cshtml,具体如下:
Go to https://goo.gl/TGhPzF to access the code.
@model RestBuy.Application.ViewModels.NewUserViewModel
@{
ViewBag.Title = "Register";
}
<h1>Register</h1>
...
...
</form>
注意,我们在输入字段中添加了bootrap-validation属性。
- 一切都好;现在,我们必须讨论警告。ASP.NET 运行时将清空无效帖子上的密码字段。但如果我们让它们保持原样,它们也会呈现绿色,尽管它们是空的。因此,如果密码字段验证步骤有效,我们希望将其标记为跳过。这是为了让他们永远不会看起来绿色和空虚。为此,我们将控制器方法更改如下:
Go to https://goo.gl/PpWxDY to access the code.
[HttpPost]
public async Task<IActionResult> Register(
NewUserViewModel newUserViewModel,
CancellationToken cancellationToken)
{
if (ModelState.IsValid)
{
try
...
...
}
return View(nameof(RegistrationForm));
}
Make sure you add using Microsoft.AspNetCore.Mvc.ModelBinding; at the top of the file.
- 现在,如果我们尝试向现有用户注册,我们会得到以下结果:

- 最后,我们使用 Bootstrap 在
SuccessfullyRegistered页面中成功注册,使用以下代码:
Go to https://goo.gl/C9F1nz to access the code.
@{
ViewBag.Title = "Registration Successful";
}
<div class="alert alert-success" role="alert">
<h4 class="alertheading">You have registered successfully!!</h4>
</div>
<ul class="nav">
<li class="nav-item">
<a class="nav-link" asp-action="" asp-controller="">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" asp-action="" asp-controller="">Login</a>
</li>
</ul>
现在,我们的SuccessfullyRegistered页面显示如下:

We should write unit tests to prevent regression. As of now, Bower has been deprecated and using NPM is recommended.
We can style our application and even style the validation. We can style our application and even style the validation using
Bootstrap.
活动:添加 EULA 协议
场景
您希望添加最终用户许可协议(EULA),以防止用户未经检查就注册。
瞄准
向应用添加最终用户许可协议
完成步骤
- 修改
NewUserViewModel,如下:
Go to https://goo.gl/nKcvUq to access the code.
using RestBuy.Entities;
using System.ComponentModel.DataAnnotations;
namespace RestBuy.Application.ViewModels
{
...
public bool TermsAndConditions { get; set; }
internal User CreateUser() =>
new User(this.Username, this.Password);
}
}
注意,我们添加了一个TermsAndConditions验证器和一个只能为 true 的范围验证器。
- 然后修改注册页面,如下所示:
Go to https://goo.gl/uUxNau to access the code.
@model RestBuy.Application.ViewModels.NewUserViewModel
@{
ViewBag.Title = "Register";
}
<h1>Register</h1>
...
...
<input class="btn btn-primary" type="submit" value="Register" />
</div>
</form>
- 在
_ValidationScriptsPartial文件的底部,添加以下内容:
Go to https://goo.gl/P6GBey to access the code.
<script>
// extend jquery range validator to work for required checkboxes
var defaultRangeValidator = $.validator.methods.range;
$.validator.methods.range = function (value, element, param)
{
if (element.type === 'checkbox')
{
// if it's a checkbox return true if it is checked
return element.checked;
}
else
{
// otherwise run the default validation function
return defaultRangeValidator.call(this, value, element, param);
}
}
</script>
现在我们的表格如下所示:
将 RestBuy 部署到 Azure
Microsoft Azure 是 Microsoft 提供的用于构建、部署和管理应用和服务的云计算平台和基础设施。它支持不同的编程语言和服务数组。
您可以将应用部署在您网络中具有互联网信息服务(IIS的任何服务器上。但这将限制您的应用只能从网络中访问,假设您的服务器只能从
网络中访问(与大多数网络设置一样)。在本节中,我们将在 Microsoft Azure 中部署 ASP.NET Core 应用,以便您的全球用户可以访问您的应用。
注册 Microsoft Azure
为了将您的应用部署到 Azure,您需要在 Azure 上拥有一个帐户。您可以免费创建一个 Azure 帐户,您将有足够的积分在前 30 天内免费部署您的应用(https://azure.microsoft.com/ )。
要注册,请执行以下步骤:
- 转到https://azure.microsoft.com/ 。您将在屏幕上看到此页面:
- 单击开始免费按钮或免费帐户链接(在页面右上角):
- 您将被重定向到下一页。输入您的 Microsoft 帐户凭据,然后单击“登录”按钮。如果您没有 Microsoft 帐户,可以单击页面底部的“立即注册”链接创建一个帐户:
These pages may appear differently than what's shown in the screenshots because of regular Microsoft updates, but the actions you should take are the same.
- 登录后,系统将询问您的国家/地区、名字、第二个名字和工作电话的详细信息,如下所示:
- 一旦您输入了所有必要的详细信息,就会要求您输入国家代码和电话号码,以便 Azure 可以发短信或打电话给您,验证您是真人而不是机器人。如果您选择文本我的选项,您将获得一个代码到您的手机;您需要在最后一个字段中输入它,然后单击验证代码。
- 一旦您通过电话验证,您需要在以下表格中输入您的信用卡信息。您将收到大约 1 美元的账单,并将在五到六个工作日内退还至您的帐户。收集此信息是为了识别用户的身份,除非用户明确选择付费服务,否则不会向用户计费。
- 输入信用卡信息并单击“下一步”后,您必须同意订阅协议,作为注册过程的最后一步。
- 注册过程完成后,将显示以下屏幕。您还将收到一封确认电子邮件(发送至您在第一步中提供的电子邮件 ID),其中包含订阅详细信息:
Azure 部署的先决条件
要从 Visual Studio 2017 社区版将 ASP.NET Core 应用发布到 Azure,请执行以下步骤:
- 启动 Visual Studio 安装程序,如下所示:
- 然后您的选择应该如下所示:
- 然后单击右下角的“修改”按钮,开始下载。
现在,让我们将 Rest Buy 发布到 Azure。我们应该注意的一点是,我们要运行数据库迁移。通常默认情况下,如果我们在 web 项目中定义了迁移,那么向导会自动询问我们是否也要生成迁移。但是,我们已经在Infrastructure项目中定义了迁移。在这种情况下,我们必须添加一些代码,这样每当我们的应用访问RestBuyContext时,如果有迁移,它就会生成迁移。
将 Rest Buy 部署到 Azure
按照以下步骤将 Rest Buy 部署到 Azure:
- 更改我们的
RestBuyContext,如下所示:
public class RestBuyContext : DbContext
{
const string hiloName = "order_hilo";
const string productTable = "Product";
const string orderTable = "Order";
const string orderItemTable = "OrderItem";
const string userTable = "User";
static bool initialized;
...
...
}
/// rest of the code remains the same
基本上在代码中,我们添加了一个静态初始化属性,该属性仅在第一次调用构造函数时才被调用。我们不想每次创建上下文时都检查是否应用了迁移;相反,我们希望每个应用启动一次。Database.Migrate()方法确保应用了必要的迁移。一旦我们定义了这个,我们就可以像往常一样继续发布了。
- 右键单击解决方案资源管理器中的
Web项目,然后选择发布。您将看到以下屏幕: - 从
Services开始,点击加号并填写必要的表格项目,为我们的应用创建一个新的数据库,如下图所示: - 填写上述表单并单击“创建”后,单击“发布”窗格中的“设置”。选择 DefaultConnection 复选框(请注意,此复选框可能需要几秒钟的时间显示),如图所示:
- 单击保存,然后单击发布。这样做将发布并运行我们的应用。这是注册屏幕:
- 单击 Register 将产生以下结果:
总结
我们已经研究了注册功能的实现。我们为它创建了一个单元测试。我们将项目升级到 Bootstrap4。最后,我们将应用部署到 Microsoft Azure。做得好!我们已经成功地完成了这门课程。


浙公网安备 33010602011771号