ASP-NET-Core3-现代-Web-开发-全-
ASP.NET Core3 现代 Web 开发(全)
零、前言
在本书中,我们将讨论 Microsoft 添加到 ASP.NET Core 3 中的新功能和最新功能。
我们将深入研究这些应用,并了解如何将它们应用于已引入的各种新工具。我们将关注 Blazor、gRPC、dotnet工具、错误处理方法和 Razor 页面。这次我们有这么多新的话题要看,所以这将是一个地狱般的欢乐之旅。坐下来享受吧!
这本书是给谁的
如果您是一名具备 ASP.NET MVC 框架基本知识的开发人员,并且希望构建功能强大的应用,那么本书适合您。希望探索 ASP.NET Core 3.0 最新变化以构建专业级应用的开发人员也会发现本书很有用。熟悉 C#、ASP.NET Core、HTML 和 CSS 是本书最大的收获。
这本书涵盖的内容
第 1 章、ASP.NET Core 入门,介绍了.NET 和 ASP.NET Core 的基础知识,包括 MVC 模式,这是 ASP.NET Core 的典型使用模式。
第 2 章配置向您展示了可供.NET/ASP.NET Core 开发人员使用的配置选项。
第 3 章路由解释了 HTTP 请求是如何通过路由映射到控制器动作的,它们是如何选择的,以及参数是如何匹配的。
第 4 章控制器和动作解释了控制器和动作是如何工作的,什么是 API 和 OData 控制器,控制器的生命周期是什么,以及如何找到控制器。
第 5 章视图解释了如何使用视图,这些视图构成了 ASP.NET Core 的用户界面。
第 6 章使用表单和模型向我们展示了如何处理表单中用户提交的数据。
第 7 章实现 Razor Pages描述了 Razor Pages 是 ASP.NET Core 的替代开发模型。
第 8 章API 控制器向我们展示了如何使用 API(非可视)控制器。
第 9 章可重用组件讲述了 ASP.NET Core 中的可重用性。
第 10 章了解过滤器,讲述了 ASP.NET Core 开发人员可以使用的各种过滤器。
第 11 章安全向我们展示了如何实现身份验证和授权。这里,我们还将介绍如何实施 HTTPS 安全性以及如何防止篡改。
第 12 章记录、跟踪和诊断解释了我们如何了解 ASP.NET Core 应用的运行情况。
第 13 章了解测试工作原理解释了如何将单元和功能/集成测试添加到我们的解决方案中。
第 14 章客户端开发介绍了如何将 ASP.NET Core 与常用客户端框架集成。
第 15 章提高性能和可扩展性介绍了如何提高我们的 web 应用的性能。
第 16 章实时通信将帮助我们学习如何将实时通信技术应用到代码中。
第 17 章介绍 Blazor,是该版本的新添加,将解释 Blazer 的互操作性、依赖注入、HTTP 调用等。
第 18 章、gRPC 和其他主题是一个主题和框架细节的集合,ASP.NET Core 开发人员应该知道这些主题和框架细节,但这不适合本书的其他章节。
第 19 章应用部署将帮助我们了解如何将 ASP.NET Core 应用部署到不同的目标,如本地和云。
第 20 章附录 A:dotnet 工具简要介绍了与 ASP.NET Core 相关的基础知识和其他有用主题,包括其工具和功能的说明。
充分利用这本书
熟悉 C#、ASP.NET Core、HTML 和 CSS 是一件好事,可以充分利用本书。不言而喻,阅读这本书的第一版会很有帮助。
| 本书涵盖的软件/硬件 | 操作系统要求 |
| 码头工人 | Windows 或 Linux |
| Visual Studio 2019 社区版 | Windows、Linux |
下载示例代码文件
您可以从您的账户www.packt.com下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,将文件通过电子邮件直接发送给您。
您可以通过以下步骤下载代码文件:
- 登录或注册www.packt.com。
- 选择“支持”选项卡。
- 点击代码下载。
- 在搜索框中输入图书名称,然后按照屏幕上的说明进行操作。
下载文件后,请确保使用以下最新版本解压或解压缩文件夹:
- WinRAR/7-Zip for Windows
- 适用于 Mac 的 Zipeg/iZip/UnRarX
- 适用于 Linux 的 7-Zip/PeaZip
该书的代码包也托管在 GitHub 上的https://github.com/PacktPublishing/Modern-Web-Development-with-ASP.NET-Core-3-Second-Edition 。如果代码有更新,它将在现有 GitHub 存储库中更新。
我们的丰富书籍和视频目录中还有其他代码包,请访问https://github.com/PacktPublishing/ 。看看他们!
下载彩色图像
我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在这里下载:https://static.packt-cdn.com/downloads/9781789619768_ColorImages.pdf 。
使用的惯例
本书中使用了许多文本约定。
CodeInText:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。这里有一个例子:“请记住,一般来说,索赔并不意味着什么,但有一些例外:Name和Role可用于安全检查,我们稍后将看到……”
代码块设置如下:
var principal = new WindowsPrincipal(identity);
var isAdmin = principal.IsInRole(WindowsBuiltInRole.Administrator);
当我们希望提请您注意代码块的特定部分时,相关行或项目以粗体显示:
"iisSettings": {
"windowsAuthentication": true, "anonymousAuthentication": false, "iisExpress": {
"applicationUrl": "http://localhost:5000/",
"sslPort": 0
}
}
任何命令行输入或输出的编写方式如下:
Add-Migration "Initial"
Update-Database
粗体:表示一个新术语、一个重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词出现在文本中,如下所示。下面是一个示例:“右键单击 web 项目并选择新的脚手架项目…”
Warnings or important notes appear like this. Tips and tricks appear like this.
联系
我们欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中注明书名,并发送电子邮件至customercare@packtpub.com。
勘误表:尽管我们已尽一切努力确保内容的准确性,但还是会出现错误。如果您在本书中发现错误,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata,选择您的书籍,单击 errata 提交表单链接,然后输入详细信息。
盗版:如果您在互联网上发现我们作品的任何形式的非法复制品,请您提供我们的位置地址或网站名称,我们将不胜感激。请通过copyright@packt.com与我们联系,并提供该材料的链接。
如果您有兴趣成为一名作家:如果您对某个主题有专业知识,并且您有兴趣撰写或贡献一本书,请访问authors.packtpub.com。
评论
请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?然后,潜在读者可以看到并使用您的无偏见意见做出购买决定,我们 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们书籍的反馈。非常感谢。
有关 Packt 的更多信息,请访问Packt.com。
一、ASP.NET Core 起步
欢迎阅读我关于 ASP.NET Core 3 的新书!
.NET 和 ASP.NET Core 在技术领域相对较新,因为它们仅在 2017 年 8 月正式发布。考虑到.NET 的名称,您可能会认为这些可能只是非常流行的.NET 框架的新版本,但事实并非如此:我们谈论的是一些真正新的东西!
这不仅仅是多平台支持(howdy,Linux!),而且还有更多。这是所有内容中的新模块化:我们现在可以通过透明的方式改变事情——我们眼前的源代码戏弄我们为之做出贡献,使之变得更好——这与以前版本的.NET Core 确实有很大不同!
在第一章中,我们将讨论 ASP.NET 和.NET 在核心版本中的变化,以及新的底层概念,如 OWIN、运行时环境和依赖注入(DI)。
在本章中,我们将介绍以下主题:
-
ASP.NET Core 的历史
-
NET Core 简介
-
控制反转与 DI
-
奥温
-
MVC 模式
-
群众或部队的集合
-
环境
-
ASP.NET Core 应用的引导过程如何工作
-
通用主机
-
自 ASP.NET Core 2 以来的新功能
-
NuGet 和
dotnet工具
技术要求
本章不需要任何特定的软件组件,因为它更多地涉及概念。
开始
Microsoft ASP.NET 是 15 年前,即 2002 年发布的,是当时崭新的.NET 框架的一部分。它继承了其前身的名称ASP(简称Active Server Pages),除了作为一种为互联网开发动态服务器端内容的技术(仅在 Windows 平台上运行)之外,它几乎没有与之共享任何其他内容。
可以说,ASP.NET 获得了巨大的普及,并与其他流行的 web 框架展开了激烈的竞争,如Java 企业版(JEE)和 PHP。事实上,它仍然如此,像BuiltWith这样的网站让它的份额达到了 21%(ASP.NET 和 ASP.NET MVC 加起来),远远领先于 Java(https://trends.builtwith.com/framework 。ASP.NET 不仅仅用于编写动态网页。它还可以用于 XML(SOAP)web 服务,在 2000 年初非常流行。它得益于.NET Framework 及其大型类库和可重用组件,这使企业开发看起来几乎很容易!
它的第一个版本 ASP.NET 1 引入了 web 表单,试图将桌面风格应用的事件和组件模型引入 web,从而使用户免受 HTML、HTTP 和状态维护等不太友好的方面的影响。在某种程度上,它是非常成功的;使用 VisualStudio,您可以在几分钟内轻松创建数据驱动的动态站点!很多东西仅仅通过标记就可以完成,不需要修改代码(读取或编译)。
几年后,版本 2 出现了,在所有其他好东西中,它以提供者模型的形式带来了可扩展性。它的许多功能都可以通过自定义提供者进行调整。后来,它又增加了 AJAX 扩展,这使得 AJAX 风格的效果非常简单。它为未来几年制定了标准,只为更多组件留下了空间。
老实说,以下版本 3.5、4 和 4.5 只提供了更多相同的功能,包括用于显示数据的新专用控件和用于检索和操作数据的图表,以及一些安全改进。一个巨大的变化是,一些框架库作为开源发布。
在 3.5 版和 4 版之间,微软发布了一个全新的框架,基于模型视图控制器(MVC模式),大部分是开源的。尽管它位于 ASP.NET 构建的基础设施之上,但它提供了一种全新的开发模式,这次完全采用了 HTTP 和 HTML。这似乎是当前跨技术 web 开发的趋势,PHP、Ruby 和 Java 等开发人员以及.NET 开发人员普遍对此感到满意。ASP.NET 开发人员现在有两种选择 Web 窗体和 MVC,它们都共享 ASP.NET 管道和.NET 库,但提供两种截然不同的方法将内容传送到浏览器。
与此同时,如今备受推崇的.NET 框架在一个不断变化的世界中成长起来。在现代企业中,需求已经发生了变化,像这样的句子只在 Windows 上运行或我们需要等待 XX 年才能获得下一个版本几乎无法接受。认识到这一点,微软开始着手开发一些新的、与众不同的东西,为未来几年制定日程。进入.NET Core!
2014 年末,微软发布了.NET Core。它是一个独立于平台、语言不可知、免费、开源的.NET 框架的完全重写。其主要特点如下:
- NET 的基类库将从零开始重写,同时保留相同的(简化的)公共 API,这意味着并非所有库最初都可用。
- 它还能够在非 Windows 操作系统上运行,特别是在几种 Linux 和 macOS 版本上,并在移动设备上运行,因此所有 Windows 特定的代码(和 API)都将被丢弃。
- 它的所有组件都将作为 NuGet 软件包交付,这意味着主机中只需要安装一个小的引导二进制文件。
- 与 IIS 之间不再存在依赖关系(或者说,关系非常密切),因此它可以自动托管或在托管进程内运行,就像 IIS 一样。
- 它将是开源的,开发者可以通过创建问题或提交请求来影响它。
这最终发生在 2016 年 7 月,.NET Core 的 1.0 版发布时。NET 开发人员现在可以编写一次并(几乎)部署到任何地方,他们最终对框架的发展方向有了发言权!
从头开始重写整个.NET 框架是一项艰巨的任务,因此微软必须做出决定并确定优先次序。其中之一就是抛弃 ASP.NETWeb 表单,只包括 MVC。ASP.NET 和 Web 表单是同义词的日子一去不复返了,ASP.NET Core 和 MVC 也是如此:它现在只是 ASP.NET Core!不仅是 ASP.NETWebAPI,它曾经是一种不同的项目类型,现在也与 ASP.NETCore 合并了(微软的明智决定,因为基本上 MVC 和 WebAPI 这两种技术有很多重叠,甚至出于几乎相同的目的有相同名称的类)。
那么,这对开发人员意味着什么呢?以下是我个人对科技发展的看法:
-
C#、Visual Basic 和 F#;F#在开发人员社区中获得了很多动力,他们为 VisualStudio 构建了模板以及许多有用的库。
-
开源是伟大的!如果您想更改任何内容,只需从 GitHub 获取代码并自己进行更改即可!如果它们足够好,那么其他人也可能对它们感兴趣,那么为什么不提交一个 pull 请求来集成它们呢?
-
我们不需要预先决定是否要使用 MVC 或 WebAPI。只需随时添加一两个 NuGet 包,并在
Startup.cs文件中添加几行即可;同一控制器可以无缝地为 API 和 web 请求提供服务。 -
属性路由是内置的,因此不需要任何显式配置。
-
ASP.NET Core 现在为基于.NET(OWIN)的中间件和配置使用开放式 Web 界面,因此您需要(显著地)更改您的模块和处理程序,使其适合此模型;MVC/WebAPI 过滤器基本相同。
-
它不依赖于 IIS 或 Windows,这意味着我们可以轻松地在旧的 Windows/Visual Studio 中编写应用,然后将它们部署到 Azure/AWS/Docker/Linux/macOS。从 VisualStudio 在 Docker/Linux 中调试我们的应用真的很酷!它也可以在控制台应用中自托管运行。
-
后者的结果是不再有IIS 管理器或
web.config/machine.config文件。 -
并非所有的库都可用于.NET Core,这意味着您需要找到替换库或自己实现这些功能。网站https://icanhasdot.net/Stats 有一个关于.NET Core 可用/不可用的列表,在的项目路线图中也有一个列表 https://github.com/dotnet/core/blob/master/roadmap.md 。
-
即使是核心类(双关语)。NET Core 类仍然缺少一些以前存在的方法;以
System.Environment类中的一些方法为例。 -
您需要为想要使用的库手工挑选 NuGet 包,包括您在过去认为理所当然的类。对于.NET;这包括,例如,
System.Collections(https://www.nuget.org/packages/System.Collections ),因为它们不是自动引用的。有时很难找到哪个 NuGet 包包含您想要的类;当这种情况发生时,http://packagesearch.azurewebsites.net 可能会派上用场。 -
不再有 Web 表单(以及 VisualStudio 中的 visual designer);现在是 MVC,也就是 Blazor,它提供了一些类似于 Web 表单的功能,并且也有一些优势!耶!
让我们先看看.NETCore 到底是关于什么的。
从.NETCore 开始
谈论 ASP.NET Core 而不解释.NET Core 有点麻烦。netcore 是每个人都在谈论的框架,这是有充分理由的。ASP.NETCore 可能是目前最有趣的 API,因为似乎一切都在向 web 转移。
为什么会这样?所有这些 API 都严重依赖 Windows 本机功能;事实上,Windows 窗体只是 Win32 API 的包装器,自 Windows 诞生之初就一直伴随着它。由于.NETCore 是多平台的,因此为所有受支持的平台提供这些 API 的版本将是一项巨大的努力。但当然,这绝不意味着它不会发生;只是它还没有发生。
对于.NETCore,主机运行应用只需要相对较小的引导代码;应用本身需要包含所有需要操作的参考库。有趣的是,可以将.NET Core 应用编译为本机格式,从而生成包含所有依赖项的特定于机器的可执行文件,甚至可以在没有.NET Core 引导程序的机器上运行。
正如我前面所说,.NETCore 是从头开始编写的,不幸的是,这意味着我们以前使用的所有 API 都没有被移植。具体而言,从版本 3 开始,以下功能仍然缺失:
- ASP.NET Web 表单(
System.Web.UI - XML Web 服务(
System.Web.Services - LINQ 到 SQL(
System.Data.Linq) - Windows 通信基础服务器端类(AutoT0)
- Windows 工作流基础(AutoT0 和 T1)
- .NET 远程处理(
System.Runtime.Remoting - Active Directory/LDAP(
System.DirectoryServices) - 企业服务(
System.EnterpriseServices - 电子邮件(
System.Net.Mail - XML 和 XSD(
System.Xml.Xsl和System.Xml.Schema) - 输入/输出端口(
System.IO.Ports) - 托管加载项框架(
System.Addin - 演辞(
System.Speech - 配置(
System.Configuration;这一个被一个新的配置 API(Microsoft.Extensions.Configuration替代) - Windows 管理工具(
System.Management) - Windows 以外的操作系统中的 Windows 注册表(【T0)】
这并不是一份详尽的清单。正如您所看到的,缺少了很多功能。尽管如此,只要我们以不同的方式做事并处理额外的负担,我们还是有可能实现我们所需要的一切!请注意,所有平台上都支持 Windows 窗体和 WPF。
以下 API 是新的或仍然存在,可以安全使用:
-
MVC 和 Web API(
Microsoft.AspNetCore.Mvc) -
实体框架核心(
Microsoft.EntityFrameworkCore -
Roslyn 用于代码生成和分析(
Microsoft.CodeAnalysis -
所有 Azure API
-
托管可扩展性框架(
System.Composition) -
文本编码/解码和正则表达式处理(
System.Text -
JSON 序列化(
System.Runtime.Serialization.Json) -
低层代码生成(
System.Reflection.Emit) -
大部分 ADO.NET(
System.Data、System.Data.Common、System.Data.SqlClient、System.Data.SqlTypes) -
LINQ 和并行 LINQ(
System.Linq -
集合,包括并发(
System.Collections、System.Collections.Generic、System.Collections.ObjectModel、System.Collections.Specialized、System.Collections.Concurrent) -
线程、进程间通信和任务原语(
System.Threading -
输入/输出、压缩、隔离存储、内存映射文件、管道(
System.IO -
XML(
System.Xml -
Windows 通信基础客户端类(AutoT0.)
-
密码学(
System.Security.Cryptography -
平台调用和 COM 互操作(
System.Runtime.InteropServices -
通用 Windows 平台(
Windows -
Windows 事件跟踪(
System.Diagnostics.Tracing -
数据注释(
System.ComponentModel.DataAnnotations) -
网络,包括 HTTP(
System.Net) -
反射(
System.Reflection) -
数学与数字(
System.Numerics -
无功扩展(
System.Reactive) -
全球化与本土化(
System.Globalization、System.Resources) -
缓存(包括内存和 Redis)(
Microsoft.Extensions.Caching -
日志记录(
Microsoft.Extensions.Logging -
配置(
Microsoft.Extensions.Configuration)
同样,这不是完整的列表,但您可以了解情况。这些只是为.NETCore 提供的 Microsoft API;显然还有成千上万的其他供应商。
And why are these APIs supported? Well, because they are specified in .NET Standard, and .NET Core implements this standard! More on this in a moment.
在.NET Core 中,不再有全局程序集缓存(GAC),而是有一个集中的位置(每个用户)来存储 NuGet 包,称为%HOMEPATH%.nugetpackages,它防止您在本地为所有项目复制包。NETCore2.0 引入了运行时存储,与 GAC 有些相似。本质上,它是本地机器上的一个文件夹,其中一些包可用并根据机器的体系结构进行编译。存储在那里的包永远不会从 NuGet 下载;它们被本地引用,不需要包含在应用中。我不得不说,这是一个值得欢迎的补充!您可以在上阅读有关元包和运行时存储的更多信息 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/metapackage 。
从 ASP.NET Core 2.1 开始,对以前的版本进行了更改:以前依赖于Microsoft.AspNetCore.All元包,现在依赖于Microsoft.AspNetCore.App。长话短说,这个版本的依赖性要小得多。具体而言,已删除以下依赖项:
Microsoft.Data.SqliteMicrosoft.Data.Sqlite.CoreMicrosoft.EntityFrameworkCore.SqliteMicrosoft.EntityFrameworkCore.Sqlite.CoreMicrosoft.Extensions.Caching.RedisMicrosoft.AspNetCore.DataProtection.AzureStorageMicrosoft.Extensions.Configuration.AzureKeyVaultMicrosoft.AspNetCore.DataProtection.AzureKeyVaultMicrosoft.AspNetCore.Identity.Service.AzureKeyVaultMicrosoft.AspNetCore.AzureKeyVault.HostingStartupMicrosoft.AspNetCore.ApplicationInsights.HostingStartup
从 3.0 版开始,Visual Studio 模板就已经引用了这个新的元包,一般来说,一切都应该正常工作;如果使用其中一个缺少的包,您可能需要添加显式引用。
有趣的是,从版本 3 开始,您不再需要在.csproj文件中引用此元包;当您引用.NET Core 3 framework 时,默认情况下会引用它。以下是最低限度的.NET Core 3.csproj文件:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
</Project>
对于.NET Core 3.1,您应该将netcoreapp3.0替换为netcoreapp3.1。稍后,我们将进一步了解这一点。
NuGet 软件包是.NETCore 的核心,大部分内容都需要从 NuGet 获得。甚至同一 VisualStudio 解决方案中的项目也作为 NuGet 包相互引用。使用.NET Core 时,需要显式添加包含您希望使用的功能的 NuGet 包。在您的一些项目中,您可能会遇到以下一些包:
| 包装 | 目的 |
| Microsoft.AspNetCore.Authentication.JwtBearer | JWT 认证 |
| Microsoft.AspNetCore.Mvc.TagHelpers | 标记助手 |
| Microsoft.EntityFrameworkCore | 实体框架核心 |
| Microsoft.Extensions.Caching.Memory | 内存缓存 |
| Microsoft.Extensions.Caching.Redis | Redis 缓存 |
| Microsoft.Extensions.Configuration | 一般配置类 |
| Microsoft.Extensions.Configuration.EnvironmentVariables | 从环境变量进行配置 |
| Microsoft.Extensions.Configuration.Json | 从 JSON 文件进行配置 |
| Microsoft.Extensions.Configuration.UserSecrets | 来自用户机密的配置(https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets ) |
| Microsoft.Extensions.Configuration.Xml | XML 中的配置 |
| Microsoft.Extensions.DependencyInjection | 内置 DI 框架 |
| Microsoft.Extensions.Logging | 日志基类 |
| Microsoft.Extensions.Logging.Console | 登录到控制台 |
| Microsoft.Extensions.Logging.Debug | 记录到调试 |
| System.Collections | 收藏 |
| System.ComponentModel | 组件和数据源定义中使用的类和接口 |
| System.ComponentModel.Annotations | 用于验证和元数据的数据注释 |
| System.Data.Common | ADO.NET |
| System.Globalization | 全球化和本地化 API |
| System.IO | 输入/输出 API |
| System.Linq.Parallel | 并行 LINQ |
| System.Net | 网络 API |
| System.Reflection | 反射 |
| System.Security.Claims | 基于债权的担保 |
| System.Threading.Tasks | 任务执行 |
| System.Xml.XDocument | XML API |
| System.Transactions | 环境事务 |
同样,这不是一个详尽的列表,但你可以了解情况。您可能看不到对所有这些包的引用,因为添加一个具有依赖项的包将带来所有这些依赖项,而大型包有很多依赖项。
没有更多的.exe文件;现在,所有程序集都是.dll,这意味着它们需要使用dotnet命令行实用程序运行。与.NET Framework 控制台和 Windows 窗体一样,所有.NET Core 应用都以静态的Main方法开始,但现在我们需要dotnet实用程序来运行它们。dotnet工具是一个非常通用的工具,可用于构建、运行、部署和恢复 NuGet 软件包,执行单元测试,以及从项目中创建 NuGet 软件包。正如我所说的,也可以将程序集编译为本机格式,但我们在这里不讨论这一点。
.NETCore 附带了内置的 DI、日志记录和灵活的配置框架,允许您插入自己的提供者(如果您愿意)。所有新 API(如 Entity Framework Core 和 ASP.NET Core)都统一使用这些服务。这是我们第一次看到 API 之间的一致行为。
此外,大多数生产 API(如 ASP.NET 和 Entity Framework)允许您用自定义版本替换它们构建的服务,允许您使它们完全按照您希望它们提供的方式工作,当然,您知道自己在做什么,并且这些服务通常基于接口。一切都更加模块化和透明。
单元测试在.NETCore 中获得了一流的公民资格。大多数新 API 的设计都考虑了可测试性(例如,考虑实体框架核心的新内存提供程序),并且工具(dotnet具有执行单元测试的显式选项,可以在任何框架中编写(当前,xUnit、NUnit、MbUnit和MSTest等发布了与.NET Core 兼容的单元测试框架)。我们将在第 13 章中介绍单元测试,了解测试的工作原理。
接下来,让我们看看支持.NETCore 的平台。
支撑平台
.NET Core 在以下平台上工作:
- Windows 7 SP1 或更高版本
- Windows Server 2008 R2 SP1 或更高版本
- Red Hat Enterprise Linux 7.2 或更高版本
- 软呢帽 23 或更高
- Debian 8.2 或更高版本
- Ubuntu 14.04 LTS/16.04 LTS 或更高版本
- Linux Mint 17 或更高版本
- openSUSE 13.2 或更高版本
- CentOS 7.1 或更高版本
- Oracle Linux 7.1 或更高版本
- macOS X 10.11 或更高版本
这涵盖了所有现代 Windows、Linux 和 macOS 发行版(Windows7SP1 于 2010 年发布)。它可能在其他发行版中也能正常工作,但这些发行版已经过微软的全面测试。
那么,这是如何工作的呢?事实证明,每当您请求需要操作系统中未包含的本机库的 NuGet 包时,这些库也会包含在.nupkg存档中。NET Core 使用平台调用(P/Invoke调用操作系统特定的库。这意味着您不必担心添加 NuGet 包和发布项目的过程是相同的,无论目标操作系统是什么。
请记住,平台独立性对您(开发人员)是透明的,当然,除非您碰巧也是库作者,在这种情况下,您可能需要关心它。
现在让我们看看过去组成.NET 的不同框架现在是如何得到支持的。
依赖关系和框架
在.NET Core 项目中,您可以指定希望针对的框架。这些框架是什么?是的,.NET Core 本身,但经典的.NET 框架也是如此,Xamarin、通用 Windows 平台(UWP)、便携类库(PCL)、Mono、Windows Phone 等等。
在.NETCore 的早期,您要么以.NETCore 本身为目标,要么/以及这些其他框架之一。现在,建议以标准为目标。现在我们有了.NET 标准,两者的区别如下:
- .NET 标准是一个规范(合同),涵盖了.NET 平台必须实现的 API。
- .NET Core 是一个具体的.NET 平台,实现了.NET 标准。
- 最新的.NET 标准将始终涵盖发布的最高.NET 完整框架。
大卫·福勒(https://twitter.com/davidfowl 微软的做出了如下类比:
interface INetStandard10
{
void Primitives();
void Reflection();
void Tasks();
void Collections();
void Linq();
}
interface INetStandard11 : INetStandard10
{
void ConcurrentCollections();
void InteropServices();
}
interface INetFramework45 : INetStandard11
{
// Platform specific APIs
void AppDomain();
void Xml();
void Drawing();
void SystemWeb();
void WPF();
void WindowsForms();
void WCF();
}
这应该很容易理解。如您所见,所有需要 Windows(WPF、Windows 窗体、绘图)的.NET API 仅在特定平台(.NET 4.5)中可用,而不是在标准中。标准是针对跨平台功能的。
For more information, please refer to https://docs.microsoft.com/en-us/dotnet/articles/standard/library.
因此,与其针对特定版本,如.NET 4.5.1、.NET Core 1.0、Mono、Universal Windows Platform 10,不如针对.NET 标准。您的项目保证在支持该标准(或更高标准)的所有平台上工作,无论是现有的还是等待创建的平台。如果这对你很重要的话,你应该尽量将你的依赖性保持在尽可能低的标准,以增加你的应用将使用的平台的数量。
在本书撰写时,不同的.NET 框架和它们实现的.NET 标准之间的当前映射在中始终可用 https://github.com/dotnet/standard/blob/master/docs/versions.md 。
.NET Core 2.0 和.NET Standard 2.0 于 2017 年 8 月推出,现在有四个框架针对.NET Standard 2.0:
- .NET Framework 完整版
- .NETCore2.x
- 沙马林
- 单声道
.NET Core 3.0 于 2019 年 9 月面世,并随.NET Standard 2.1 面世。
可以为每个目标或所有目标指定依赖项。在前一种情况下,所有依赖项都需要支持所有目标,而在后一种情况下,我们可以对每个目标具有不同的依赖项。您可能希望混合使用这两种方法,将公共库作为全局依赖项,并仅在可用的情况下指定更专业的库。如果您针对的是多个标准(或框架),那么请注意,因为您可能必须求助于条件定义(#if)来针对仅存在于其中一个标准(或框架)中的功能。让我们看看如何。
The .NET Standard FAQ is available in GitHub at https://github.com/dotnet/standard/blob/master/docs/faq.md.
以.NET Core 或完整的.NET 框架为目标
您必须知道,您可以在 ASP.NET Core 应用中针对完整的.NET framework!但是,如果您这样做,您将失去平台独立性,也就是说,您将只能在 Windows 上运行它。
默认情况下,ASP.NET Core 项目的目标是netcoreapp1.x、netcoreapp2.x或netcoreapp3.x,具体取决于您是针对 ASP.NET Core 1.x、2.x 还是 3.x,但您可以在.csproj文件中对其进行更改。如果只想针对一个框架,那么修改TargetFramework元素如下:
<TargetFramework>net461</TargetFramework>
或者,如果您想针对多个目标,请将TargetFramework替换为TargetFrameworks:
<TargetFrameworks>netcoreapp3.0;net461</TargetFrameworks>
有关更多信息,请参阅位于的 Microsoft 文档 https://docs.microsoft.com/en-us/dotnet/core/tools/csproj 。
对于.NET Core 和.NET 标准,您应该在TargetFramework或TargetFrameworks中使用以下名称:
| .NET Core/标准版 | 绰号 |
| .NET Core 1 | netcoreapp1.0 |
| .NET Core 1.1 | netcoreapp1.1 |
| .NET Core 2 | netcoreapp2.0 |
| .NET Core 2.1 | netcoreapp2.1 |
| .NET Core 2.2 | netcoreapp2.2 |
| .NET Core 3.0 | netcoreapp3.0 |
| .NET Core 3.1 | netcoreapp3.1 |
| .NET 标准 1.0 | netstandard1.0 |
| .NET 标准 1.1 | netstandard1.1 |
| .NET 标准 1.2 | netstandard1.2 |
| .NET 标准 1.3 | netstandard1.3 |
| .NET 标准 1.4 | netstandard1.4 |
| .NET 标准 1.5 | netstandard1.5 |
| .NET 标准 1.6 | netstandard1.6 |
| .NET 标准 2.0 | netstandard2.0 |
| .NET 标准 2.1 | netstandard2.1 |
请参见https://docs.microsoft.com/en-us/dotnet/standard/frameworks 获取最新列表。接下来,让我们看看通用托管是如何工作的。
了解通用主机
从 3.0 版开始,ASP.NET Core 现在使用通用主机进行引导。这意味着它没有专门绑定到 HTTP 或任何其他 web 协议,但它可能支持任何类型的协议,包括低级 TCP。模板已更改,现在引导看起来如下所示:
Host
.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
我们现在使用Host类来创建实现IHostBuilder而不是IWebHostBuilder的类的实例,尽管结果是相同的。
我们可以通过扩展方法干预引导过程。具体而言,我们可以配置以下各项:
- 服务注册
- 登录中
- 配置
- Web 主机默认设置(主机、启动类)
以下是更改配置的完整示例:
Host
.CreateDefaultBuilder(args)
.ConfigureHostConfiguration(builder =>
{
//host configuration (Kestrel or HTTP.sys)
builder.Properties["key"] = "value";
})
.ConfigureAppConfiguration(builder =>
{
//app configuration
builder.Add(new JsonConfigurationSource { Path =
"./configuration.json", Optional = true });
builder.Properties["key"] = "value";
})
.ConfigureLogging(builder =>
{
//add logging providers
builder.AddConsole();
})
.ConfigureServices(services =>
{
//register services
services.AddSingleton<IMyService, MyService>();
})
.ConfigureWebHostDefaults(webBuilder =>
{
builder.ConfigureKestrel(options =>
{
//set Kestrel options
});
//set the startup class
webBuilder.UseStartup<Startup>();
})
更改应用的IHostLifetime通常没有意义,因为这与我们正在构建的应用的类型有关。我们的选择如下:
ConsoleLifetime:默认跨平台主机;收听CTRL-C和SIGINT、SIGTERM停止信号SystemdLifetime:适用于使用systemd的操作系统,如 MacOS、Linux 等;收听SIGTERM信号WindowsServiceLifetime:仅适用于窗户;侦听 Windows 服务事件
当应用完成加载、即将停止或已停止时,主机负责调用IHostApplicationLifetime事件。您可以在第 18 章、gRPC 和其他主题中阅读。
在ConfigureServices中注册的服务将可被注入Startup类的构造函数中,并且也将出现在传递给其ConfigureServices方法的services参数中。日志提供商和应用配置也是如此。接下来,让我们转到 MVC 模式。
理解 MVC 模式
现在让我们回到 ASP.NET。对于那些仍在使用 Web 表单的人来说,MVC 到底是什么,它从何而来?
让我们面对现实吧:在 Web 表单中很容易做一些糟糕的事情,比如在页面中添加大量敏感代码(在浏览器访问页面之前,这些代码不会被编译)、向页面类添加复杂的业务逻辑、在每次请求时都有几兆字节处于视图状态的代码来回移动等等。除了开发人员的自由裁量权之外,根本没有任何机制来以正确的方式做事。另外,对它进行单元测试是很糟糕的,因为它依赖浏览器提交(POST和 JavaScript 使事情正常工作,例如将动作绑定到事件处理程序,并将值提交到控件。必须有一个不同的解决方案,事实上,确实有。
模型-视图-控制器(MVC)设计模式是在上个世纪 70 年代末和 80 年代初定义的(很可怕,不是吗?)。它被认为是一种适当分离概念上不应该在一起的事物的方法,例如呈现用户界面(UI的代码)和包含业务逻辑和数据访问的代码,这些代码将提供和控制该 UI。在 MVC 范式(及其后代)中,我们有公开公共行为的控制器。在每个操作中,控制器应用它需要的任何业务逻辑,然后决定应该呈现哪个视图,并向它传递足够的信息(模型),以便它能够完成它的工作。控制器对 UI 元素一无所知,它只获取在操作内部操作所需的数据和执行上下文,然后从那里开始。同样,视图对数据库、web 服务、连接字符串、SQL 等也一无所知,它只是呈现数据,可能只是简单地决定执行方式。至于模型,它基本上是包含视图所需信息的任何内容,包括记录列表、静态用户信息等等。这种严格的分离使事情更易于管理、测试和实现。当然,MVC 模式并不特定于 web,只要这种关注点分离是有用的,比如当我们有一个 UI 和一些代码来控制它时,就可以使用 MVC 模式。
下图显示了视图、控制器和模型之间的关系:

Image taken from https://docs.microsoft.com/en-us/archive/msdn-magazine/2013/november/asp-net-single-page-applications-build-modern-responsive-web-apps-with-asp-net
MVC 通常与面向对象编程(OOP)相关联,但也有多种语言的实现,包括 JavaScript 和 PHP。.NET MVC 实现具有以下基本特征:
-
控制器类可以是普通的旧 CLR 对象(POCOs)或继承自基类
Controller。继承Controller不是必需的(与以前的版本不同),但它确实使事情稍微容易一些。控制器类由 ASP.NET Core DI 框架实例化,这意味着它们可以将所依赖的服务传递给它们。 -
动作是控制器中的公共方法;它们可以接受参数,包括简单类型和复杂类型(POCO)。MVC 使用所谓的模型绑定将从浏览器发送的信息(查询字符串、标题、cookie、表单、DI 和其他位置)转换为方法参数。从请求 URL 和提交的参数中选择从哪个控制器调用哪个方法是通过路由表、约定和助手属性的混合实现的。
-
模型在动作方法的返回中从控制器发送到视图,基本上可以是任何东西(或什么都没有)。当然,API 调用的操作方法不返回视图,但可以返回带有 HTTP 状态代码的模型。还有其他向视图传递数据的方法,例如视图包,它本质上是一个非类型化的数据字典(一个大包);两者的区别在于模型通常是类型化的。模型将自动验证并绑定到动作方法参数。
-
视图由领域特定语言(DSL文件)组成,这些文件由视图引擎解释,并转化为浏览器可以解释的内容,如 HTML。ASP.NET Core 具有一个可扩展的视图引擎框架,但包含一个实现,Razor。Razor 提供了一种简单的语法,允许开发人员混合使用 HTML 和 C,以掌握传入的模型并决定如何使用它。视图可以受到布局的约束(Web 表单开发人员可以将布局视为母版页),并且可以包含其他局部视图(类似于 Web 表单中的 Web 用户控件)。Razor 视图引擎的视图具有
.cshtml扩展名,并且不能仅作为操作调用的结果直接访问。可以预编译视图,以便更快地检测到语法错误。 -
过滤器用于拦截、修改或完全替换请求;例如,内置筛选器使您能够在发生异常时阻止对未经身份验证的用户的访问或重定向到错误页面。
现在,还有其他类似于 MVC 的模式,如模型视图演示器(MVP)或模型视图视图模型(MVVM)。我们将只关注微软的 MVC 实现及其细节。特别是,ASP.NET Core 附带的 MVC 版本是版本 6,因为它构建在版本 5 上,该版本以前可用于.NET 完整框架,但添加和删除了两个功能。由于它现在位于新的.NET Core 框架上,它完全基于 OWIN,因此不再有Global.asax.cs文件。稍后将对此进行详细介绍。
在 ASP.NET 中实现 MVC 的方式主要包括以下几点:
- URL:它们现在更有意义,而且搜索引擎优化(SEO友好。
- HTTP 谓词:谓词现在准确地说明了操作应该做什么,例如,
GET用于幂等运算,POST用于新内容,PUT用于完整内容更新,PATCH用于部分内容更新,DELETE用于删除,等等。 - HTTP 状态码:用于返回操作结果码,在 Web API 的情况下更为重要。
例如,向http://somehost/Product/120发出GET请求可能会返回 ID 为120的产品视图,而向相同 URL 发出DELETE请求可能会删除该产品并返回 HTTP 状态码或尼斯视图,告知我们事实。
URL 及其与控制器和操作的绑定可通过路由进行配置,并且此 URL 可能由名为ProductController的控制器和配置为处理GET或DELETE请求的操作方法处理。无法从 URL 提取视图,因为它们是在 action 方法中确定的。
我们将在以下章节中深入介绍微软的 MVC 实现。当然,作为.NET 的核心功能,它的所有组件都可以作为 NuGet 包提供。您可能会发现以下几点:
| 包装 | 目的 |
| Microsoft.AspNetCore.Antiforgery | 防伪原料药 |
| Microsoft.AspNetCore.Authentication | 身份验证基类 |
| Microsoft.AspNetCore.Authentication.Cookies | 通过 cookie 进行身份验证 |
| Microsoft.AspNetCore.Authentication.JwtBearer | JWT 认证 |
| Microsoft.AspNetCore.Authorization | 授权 API |
| Microsoft.AspNetCore.Diagnostics | 诊断 API |
| Microsoft.AspNetCore.Hosting | 托管基类 |
| Microsoft.AspNetCore.Identity | 身份验证 |
| Microsoft.AspNetCore.Identity.EntityFrameworkCore | 以实体框架核心作为存储的标识 |
| Microsoft.AspNetCore.Localization.Routing | 路由本地化 |
| Microsoft.AspNetCore.Mvc | MVC 的核心特性 |
| Microsoft.AspNetCore.Mvc.Cors | 支持跨源请求脚本编制(CORS) |
| Microsoft.AspNetCore.Mvc.DataAnnotations | 通过数据注释进行验证 |
| Microsoft.AspNetCore.Mvc.Localization | 基于本地化的 API |
| Microsoft.AspNetCore.Mvc.TagHelpers | 标记助手功能 |
| Microsoft.AspNetCore.Mvc.Versioning | Web API 版本控制 |
| Microsoft.AspNetCore.ResponseCaching | 响应缓存 |
| Microsoft.AspNetCore.Routing | 路由 |
| Microsoft.AspNetCore.Server.IISIntegration | IIS 集成 |
| Microsoft.AspNetCore.Server.Kestrel | 红隼服务器 |
| Microsoft.AspNetCore.Server.WebListener(Microsoft.AspNetCore.Server.HttpSys在 ASP.NET Core 2 中) | WebListener 服务器(现在称为HTTP.sys。参见https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/httpsys 。 |
| Microsoft.AspNetCore.Session | 会话功能 |
| Microsoft.AspNetCore.StaticFiles | 能够为静态文件提供服务 |
您可能需要也可能不需要所有这些软件包,但您应该熟悉它们。
In ASP.NET Core 2.0, there was the Microsoft.AspNetCore.All NuGet metapackage, and since 2.1 there is Microsoft.AspNetCore.App. The former included lots of packages, so a decision was made to have another metapackage with far fewer dependencies. Since version 2.1, all projects will include Microsoft.AspNetCore.App, and you may need to add other dependencies, such as SQLite, Redis, Azure Storage, and ApplicationInsights. You can read a discussion about it at https://github.com/aspnet/Announcements/issues/287.
接下来,让我们看看上下文执行是如何工作的。
了解你的背景
您可能还记得 ASP.NET 中的HttpContext类。此类的当前实例将表示当前执行上下文,其中包括请求信息和响应通道。它无处不在,尽管在 Web 表单中它有时是隐藏的,但它是 Web 应用与客户端通信的方式。
当然,ASP.NET Core 也有一个HttpContext类,但有一个很大的区别:不再有Current静态属性让我们掌握当前上下文,相反,这个过程有点复杂。无论如何,所有的基础设施类中间件、控制器、视图、Razor 页面、视图组件、标记帮助程序和过滤器都允许轻松访问当前上下文。不使用的用户可以通过 DI 利用IHttpContextAccessor接口,并获得指向当前上下文的指针:
//this is required to register the IHttpContextAccessor
//services.AddHttpContextAccessor();
...
public MyType(IHttpContextAccessor httpContextAccessor)
{
var httpContext = httpContextAccessor.HttpContext;
}
因此,除了User、Request和Response属性,这些属性大多与堆芯前的对应属性相似外,我们还有以下几点:
- 一个
Features集合,公开当前托管服务器(Kestrel、WebListener/HTTP.sys等)实现的所有功能。 - 一个
RequestServices属性,它使我们能够访问内置的 DI 框架(在下面的章节中有更多关于这方面的内容)。 TraceIdentifier属性,唯一标识 ASP.NET Core 2.x 中的请求;在早期版本中,我们必须通过一个特性来访问它。- 一个
Connection对象,可以从中获取客户端连接的相关信息,如客户端证书,例如:Authentication对象,可轻松访问安全原语,如登录、注销、拒绝等。Session对象,由ISessionFeature特性实现,由HttpContext直接公开。ClientCertificate属性包含客户端作为握手协议的一部分发送的任何 SSL 证书。
我们将看到,上下文是 ASP.NET Core 应用的重要组成部分。
与环境合作
我们可能在上下文中执行的主要操作如下:
- 从请求中读取值
- 给答复写信
- 读写 cookies
- 获取当前用户
- 正在获取远程用户的地址
- 访问会话
- 从 DI 框架访问服务
以下是一些例子:
//writing to the response
HttpContext.Response.StatusCode = 200;
HttpContext.Response.ContentType = "text/plain";
HttpContext.Response.WriteAsync("Hello, World!");
//getting values from the request
var id = HttpContext.Request.Query["id"].Single();
var host = HttpContext.Request.Host;
var payload = HttpContext.Request.Form["payload"].SingleOrDefault();
//reading and writing cookies
var isAuthenticated = HttpContext.Request.Cookies["id"].Any();
HttpContext.Response.Cookies.Append("id", email);
//getting the current user
var user = HttpContext.User;
//getting the address of the remote user
var ip = HttpContext.Connection.RemoteIpAddress;
//accessing the session
HttpContext.Session.SetString("id", email);
var id = HttpContext.Session.GetString("id");
//getting services from DI
var myService = HttpContext.RequestServices.Get<IMyService>();
本质上,我们将通过构造(比如 MVC 的控制器和动作)所做的一切都是围绕这些和其他简单的HttpContext操作构建的。我们将研究的下一个主题是 OWIN 管道。
了解 OWIN 管道
ASP.NET 的早期版本与微软的旗舰 web 服务器互联网信息服务(IIS)有着非常密切的关系。事实上,IIS 是承载 ASP.NET 的唯一受支持的方式。
为了改变这一点,微软为.NET(OWIN规范)定义了开放式 Web 界面,您可以在上阅读 http://owin.org 。简而言之,它是解耦服务器和应用代码以及 web 请求执行管道的标准。因为它只是一个标准,对 web 服务器(如果有的话)一无所知,所以可以使用它来提取其功能。
.NET Core 大量借用了 OWIN 规范。没有更多的Global.asax、web.config或machine.config配置文件、模块或处理程序。我们所拥有的是:
Program.Main中的引导代码声明了一个包含约定定义方法的类(如果没有声明类,则使用Startup。- 这种传统的方法,应该称为
Configure,接收对IApplicationBuilder实例的引用(它可以从服务提供商处获取其他要注入的服务)。 - 然后开始向
IApplicationBuilder添加中间件;这个中间件将处理您的 web 请求。
下面是一个简单的例子。首先是引导类,默认名称为Program:
public class Program
{
public static void Main(string [] args) =>
CreateWebHostBuilder(args).Build().Run();
public static IHostBuilder CreateHostBuilder(string [] args) =>
Host
.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(builder =>
{
builder.UseStartup<Startup>();
});
}
事情可能会变得更复杂,但现在不要太担心。稍后,我将解释这一切意味着什么。目前,我们已经知道我们正在利用一个Host来托管 Kestrel(默认主机),并传递一个名为Startup的常规类。这个Startup类看起来像这样(以简化的方式):
public class Startup
{
public IConfiguration Configuration { get; }
{
this.Configuration = configuration;
}
public void Configure(IApplicationBuilder app)
{
app.Run(async (context) => {
await context.Response.WriteAsync("Hello, OWIN World!");
}
}
}
这里有几件事值得解释一下。首先,您会注意到,Startup类没有实现任何接口或从显式基类继承。这是因为Configure方法没有预定义的签名,除了它的名称之外,它的第一个参数是IApplicationBuilder。例如,还允许以下情况:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { ... }
这个版本甚至比你要求的还多。但我离题了。
IApplicationBuilder接口定义了一个Run方法。此方法接受一个RequestDelegate参数,它是一个委托定义,接受一个HttpContext(记住?)作为其唯一参数,并返回一个Task。在我的示例中,我们通过向其添加async和await关键字使其异步,但不必如此。你所要做的就是确保你从HttpContext中提取你想要的东西,然后写你想要的东西。这是你的网络管道。它包装了 HTTP 请求和响应对象,我们称之为middleware。
Run方法本身就是一个成熟的管道,但我们可以使用(pun-designed)Use方法将其他步骤(中间件)插入管道:
app.Use(async (context, next) =>
{
await context.Response.WriteAsync("Hello from a middleware!");
await next();
});
这样,我们可以添加多个步骤,它们都将按照定义的顺序执行:
app.Use(async (context, next) =>
{
await context.Response.WriteAsync("step 1!");
await next();
});
app.Use(async (context, next) =>
{
await context.Response.WriteAsync("step 2!");
});
请记住,这里的顺序很重要;下一个示例显示了这一点:
app.Use(async (context, next) =>
{
try
{
//step 1
await next();
}
catch (Exception ex)
{
await context.Response.WriteAsync($"Exception {ex.Message} was
caught!");
}
});
app.Use(async (context, next) =>
{
//step 2
throw new Exception();
});
因为第一个步骤是在第二个步骤之前添加的,所以它会对它进行包装,所以第二步抛出的任何异常都会被第一步捕获;如果按不同的顺序添加,则不会发生这种情况。
Use方法以HttpContext实例为参数,返回Func<Task>,通常是对下一个处理程序的调用,因此管道继续进行。
我们可以将 lambda 提取到它自己的方法中,如下所示:
async Task Process(HttpContext context, Func<Task> next)
{
await context.Response.WriteAsync("Step 1");
await next();
}
app.Use(Process);
甚至可以将中间件提取到自己的类中,并使用通用的UseMiddleware方法应用它:
public class Middleware
{
private readonly RequestDelegate _next;
public Middleware(RequestDelegate next)
{
this._next = next;
}
public async Task InvokeAsync(HttpContext context)
{
await context.Response.WriteAsync("This is a middleware class!");
}
}
//in Startup.Configure
app.UseMiddleWare<Middleware>();
在这种情况下,构造函数需要将指向管道中下一个中间件的指针作为其第一个参数,作为RequestDelegate实例。
我想现在你已经明白了:OWIN 定义了一个管道,你可以向其中添加处理程序,然后依次调用这些处理程序。Run和Use之间的区别在于前者结束了管道,也就是说,它不会在自身之后调用任何东西。
下图(来自 Microsoft)清楚地显示了这一点:

Image taken from https://docs.microsoft.com/en-us/dotnet/architecture/blazor-for-web-forms-developers/middleware
在某种程度上,第一个中间件封装了所有下一个中间件。例如,假设您希望将异常处理添加到管道中的所有步骤中。你可以这样做:
app.Use(async (context, next) =>
{
try
{
//log call
await next(context);
}
catch (Exception ex)
{
//do something with the exception
}
await context.Response.WriteAsync("outside an exception handler");
});
对next()的调用被包装在try...catch块中,因此管道中的另一个中间件可能引发的任何异常都将被捕获,只要该异常是在该异常之后添加的。
You can set the status code of a response, but be aware that, if an exception is thrown, it will be reset to 500 Server Error!
您可以在上阅读更多关于微软实施 OWIN 的信息 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/owin 。
为什么 OWIN 很重要?因为 ASP.NET Core(及其 MVC 实现)是建立在它之上的。稍后我们将看到,为了拥有一个 MVC 应用,我们需要在Startup类的Configure方法中将 MVC 中间件添加到 OWIN 管道中,通常如以下代码所示,使用新的端点路由和默认路由:
{
endpoints.MapDefaultControllerRoute();
});
正如你所知,这本书本质上讲的是 MVC 模式,但是我们可以同样使用这种中间件,而不需要任何 MVC 的东西;只是解决复杂性会困难得多,MVC 在这方面做得很好。
OWIN 本质上是 ASP.NET Core 中间件。我们在UseXXX扩展中添加的所有内容都是中间件。接下来,让我们看看如何主持 ASP.NET Core 项目。
托管 ASP.NET Core
当我们谈到 OWIN 时,您可能注意到,我提到示例应用是由 Kestrel 托管的。Kestrel 是完全用.NETCore 编写的独立于平台的 web 服务器的名称(当然,使用操作系统的本机库)。您需要在某个地方托管 web 应用,.NET Core 提供以下选项:
- 红隼:独立于平台,如果您想让代码在任何平台上运行,您可以选择主机。
- WebListener:只使用 Windows 的主机,与 Kestrel 相比具有显著的性能优势,但也存在需要 Windows 的缺点;从 ASP.NET Core 2 开始,它现在被称为
HTTP.sys。 - IIS:与过去一样,您可以继续在 Windows 上的 IIS 中托管您的 web 应用,受益于旧的管道和配置工具。
在此上下文中,服务器只是IServer的一个实现,它是Microsoft.AspNetCore.HostingNuGet 包中定义的一个接口。这定义了服务器提供的基本契约,可以描述如下:
- 一种
Start方法,所有的乐趣都从这里开始。负责创建HttpContext,设置Request和Response属性,调用常规Configure方法。 - 实现支持的
Features集合。有几十种功能,但至少服务器需要支持IHttpRequestFeature和IHttpResponseFeature。
这些服务器实现中的每一个都在 NuGet 软件包中提供:
| 服务器 | 包装 |
| 红隼 | Microsoft.AspNetCore.Server.Kestrel |
| WebListener/HTTP.sys | Microsoft.AspNetCore.Server.WebListener(Microsoft.AspNetCore.Server.HttpSys来自 ASP.NET Core 2) |
| 非法移民 | Microsoft.AspNetCore.Server.IISIntegration |
IIS 不能单独使用。当然,IIS 是 Windows 本机应用,因此无法通过 NuGet 获得,但Microsoft.AspNetCore.Server.IISIntegration软件包包含 IISASP.NET Core 模块,需要在 IIS 中安装该模块,以便它可以使用 Kestrel 运行 ASP.NET Core 应用(WebListener 与 IIS 不兼容)。当然,还有其他由第三方提供商实现的服务器(例如,Nowin,可在上获得)https://github.com/Bobris/Nowin )。ASP.NET Core 模块充当反向代理,通过 IIS 接收请求,然后在同一进程空间中调用 ASP.NET Core。其他反向代理是Apache和NGINX。反向代理非常有用,因为它们提供了 ASP.NET Core 以外的其他功能;他们接受请求,发挥他们的魔力,并将请求转发给 ASP.NET Core,以便它也能发挥其魔力。
那么,关于这些有什么需要了解的呢?我们如何选择其中一个托管服务器呢?
红隼
Kestrel 是默认多平台 web 服务器。它提供了可接受的性能,但缺少现实生活中所期望的许多功能:
- 不支持 Windows 身份验证(随着时间的推移,问题会越来越少)
- 无直接文件传输
- 没有强大的安全保护(大请求等)
由此可以清楚地看出,Kestrel 并不打算用于生产,除非它位于反向代理(如 NGINX、Apache 或 IIS)后面。它在引导时通过UseKestrel扩展方法进行配置,如果您需要配置它的选项,则需要提供额外的 lambda:
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.UseStartup<Startup>()
.UseKestrel(opt => { opt.Limits.MaxConcurrentConnections =
10; })
});
您可以在上了解更多信息 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel 。
WebListener/HTTP.sys
这只适用于 Windows,因为它是处理 web 请求的 Windows 子系统的包装器。它提供了迄今为止最好的性能,支持 HTTP/2、WebSockets、Windows 身份验证、直接文件传输、端口共享、响应缓存,以及您能想到的任何东西。当然,缺点是它需要 Windows 7 或 Windows Server 2008 R2 及更高版本。在引导时,使用UseWebListener扩展方法将其添加到主机生成器中,可能带有一个配置参数:
.UseWebListener(opt =>
{
opt.ListenerSettings.Authentication.AllowAnonymous = false;
})
Since ASP.NET Core 2.0, WebListener is called HTTP.sys.
非法移民
我们已经知道 IIS 了。IIS 可用作 Kestrel 的反向代理,或添加主机不支持的功能,如 Windows 身份验证。为此,我们应该通过调用UseIISIntegration来包括对 IIS 的支持。在这里,应该通过Web.config文件进行配置,在本例中,这是一个要求(VisualStudio 模板将此文件添加到项目的根目录中)。
NGINX
NGINX(发音为EngineX)是一种 UNIX 和 Linux 反向代理,可与 ASP.NET Core 一起使用。我们将在第 19 章应用部署中详细介绍 NGINX。
阿帕奇
流行的 UNIX 和 Linux 服务器 Apache(实际上也在 Windows 中运行)也可以充当反向代理。更多信息请参见第 17 章、部署。
配置
正如我们所看到的,通常使用Host实例选择服务器。至少,您需要告诉 it 使用哪台服务器以及根目录是什么:
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.UseStartup<Startup>()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory());
});
实际上,对UseKestrel和UseContentRoot(Directory.GetCurrentDirectory())的调用已经由ConfigureWebHostDefaults完成,所以您可以跳过它们。
特征
不同的服务器将提供不同的功能。本质上,此上下文中的功能只是一个配置,每个请求都可以使用该配置,并提供可以检查和更改的属性。以下是开箱即用的一些功能:
| 接口 | 特征 |
| IExceptionHandlerPathFeature | 如果使用集中式异常处理,请访问上次发生的错误和请求路径。 |
| IEndpointFeature | 访问端点路由。 |
| IHttpRequestFeature | 访问请求对象和集合(表单、标题、cookie、查询字符串等)。 |
| IHttpResponseFeature | 访问响应对象和集合(标题、cookie、内容等)。 |
| IHttpAuthenticationFeature | 基于声明和主体的身份验证。 |
| IHttpUpgradeFeature | 支持 HTTP 升级(参见https://tools.ietf.org/html/rfc2616.html#section-14.42。 |
| IHttpBufferingFeature | 响应缓冲。 |
| IHttpConnectionFeature | 本地主机调用的属性。 |
| IHttpRequestLifetimeFeature | 检测客户端是否已断开连接,以及实际断开连接的能力。 |
| IHttpResetFeature | 用于向支持重置消息的协议(HTTP/2)发送重置消息。 |
| IHttpSendFileFeature | 将文件作为响应直接发送的能力。 |
| IHttpWebSocketFeature | 网袋。 |
| IHttpRequestIdentifierFeature | 唯一标识请求。 |
| IHttpsCompressionFeature | 访问请求和响应压缩。 |
| IFormFeature | 访问请求表单数据。 |
| ISessionFeature | 提供会话功能。需要由会话中间件添加;否则不可用。 |
| IQueryFeature | 访问查询字符串。 |
| ITlsConnectionFeature | 正在检索客户端证书。 |
| ITlsTokenBindingFeature | 使用 TLS 令牌。 |
| IStatusCodePagesFeature | 根据 HTTP 状态代码重定向到错误。 |
这绝不是完整的列表,因为它可能会根据您的具体配置(您选择的主机等)发生变化。功能没有基本接口。所有这些特征都可以通过Server的Features属性获得,也可以通过请求HttpContext接口获得:
var con = HttpContext.Features.Get<IHttpConnectionFeature>();
这是访问该功能提供的功能的一种方法,但对于某些功能,存在变通方法。例如,ASP.NETSession对象可以直接从HttpContext获取。特性本质上是HttpContext类如何获得其公开的行为;例如,请求和响应对象、会话等。中间件类可以提供自己的功能,以便通过将它们直接添加到Features集合中,在下游可用:
HttpContext.Features.Set(new MyFeature());
每种类型只能有一个功能,例如,每IMyFeature1一个,每IMyFeature2一个,等等。
启动配置
Visual Studio 可以为每个项目提供多个配置,这意味着它可以通过多种方式启动项目,并且有一个工具栏按钮显示以下事实:

特别是,我们可以选择是使用 IIS(或 IIS Express)作为主机启动 web 应用,还是使用代码中指定的任何内容(Kestrel 或HTTP.sys)。启动设置存储在 Visual Studio 默认创建的PropertieslaunchSettings.json文件中。此文件包含以下(或类似)内容:
{
"iisSettings": {
"windowsAuthentication": true,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:24896/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Web": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Where I have "Web", you should have the name of your application.
在这里,我们可以看到默认端口和要使用的环境名称(稍后讨论)。此文件不需要手动更改(尽管可以更改);您可以使用项目属性以可视形式查看它:

现在让我们看一个需要动态设置端口的特殊情况。
设置侦听端口和地址
可能需要设置侦听端口。例如,您已经有一台或多台服务器在同一台计算机上运行。发生这种情况时,您可以选择一个确定未使用的端口,也可以让 ASP.NET Core 为您选择一个。如果要限制从何处接收请求,设置侦听地址也很重要。实现这一目标的方法有很多;让我们一个接一个地看一遍。
默认情况下,ASP.NET Core 在以下位置接受请求:
http://localhost:5000https://localhost:5001(使用本地证书时)
使用命令行
当您使用dotnet启动应用时,您可以传递--urls参数以指定应用应侦听的 URL:
dotnet run --urls "http://localhost:5000;https://localhost:5001"
当然,这是静态的。在这里,您指定仅在端口5000上将 HTTP 绑定到localhost,在端口5001上也将HTTPS绑定到localhost。如果要绑定到任何主机,应该使用0.0.0.0而不是localhost。
这种方法对于 Docker 部署是一种很好的方法。现在让我们看看如何使用环境变量来实现这一点。
使用环境变量
另一种选择是使用ASPNETCORE_URLS环境变量。这与前面的方法基本相同:
//Linux, MacOS
export ASPNETCORE_URLS="http://localhost:5000;https://localhost:5001"
//Windows
set ASPNETCORE_URLS="http://localhost:5000;https://localhost:5001"
这对 Docker 来说也是可以的。
接下来,让我们看看如何使用 VisualStudio 的配置文件。
使用 launchSettings.json
launchSettings.json是 VisualStudio 保存运行 web 解决方案的配置详细信息的地方。其结构如下所示:
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:7788",
"sslPort": 44399
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Web": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}
您可以看到在哪里用粗体指定 URL、地址和端口,以及在哪里可以更改它们。对于 IIS Express,您需要编辑位于根解决方案文件夹内的.vs\config\applicationhost.config。
当然,这种方法只适用于地方发展。
使用代码
我们还可以在代码中指定侦听地址,这对于动态情况更有用,因为我们希望动态构建地址:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host
.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.UseStartup<Startup>()
.UseUrls("http://localhost:5000",
"https://localhost.5001");
});
动态设置端口
如果我们需要使用动态分配的端口怎么办?当我们要使用的端口已被占用时,可能会发生这种情况。ASP.NET Core 通过将端口设置为0完全支持这一点,但这需要在实际主机级别完成。这只适用于红隼;HTTP.sys不支持这一点。让我们看看如何做到这一点:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host
.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.UseStartup<Startup>()
.UseKestrel(options =>
{
options.ListenAnyIP(0);
});
});
如果您想知道我们使用的地址,您必须使用名为IServerAddressesFeature的功能。一种方法是在Configure方法中查看,但仅在应用启动后:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime events)
{
events.ApplicationStarted.Register(() =>
{
var feature = app.ServerFeatures.Get<IServerAddressesFeature>();
var addresses = feature.Addresses;
});
//rest goes here
}
此示例演示了两个概念:服务器功能和主机应用事件。我向ApplicationStarted事件注册了一个处理程序,当它被触发时,我请求一个服务器功能IServerAddressesFeature,它包含我的应用当前绑定到的所有地址,包括端口。从这里,我可以看到选择的端口。
We read about server features in this chapter. Application events are discussed in Chapter 18, gRPC and Other Topics.
既然我们已经学习了托管的基础知识,现在让我们关注 ASP.NET Core 的另一个关键方面:控制反转和 DI 框架模式。
控制反转与依赖注入
控制反转****IoC和依赖注入****DI是两种相关但不同的模式。第一个告诉我们,我们不应该依赖于实际的、具体的类,而是依赖于指定我们感兴趣的功能的抽象基类或接口。
根据注册情况,IoC 框架将返回一个与我们所需的接口或抽象基类匹配的具体类。另一方面,DI 是一个过程,当构建一个具体的类时,它所需要的依赖项通过该过程传递给它的构造函数(构造函数注入,尽管还有其他选项)。这两种模式很好地结合在一起,在本书中,我将使用术语 IoC 或 DI 容器/框架来表示相同的含义。
NET 一直支持有限形式的国际奥委会;Windows 窗体设计器在设计时使用它来访问当前设计器的服务,例如,Windows 工作流基础还使用它在运行时获取注册扩展名。但在.NETCore 中,微软将其集中化,使其成为生态系统中的一流公民。现在,几乎所有事情都依赖于 IoC 和 DI 框架。它在Microsoft.Extensions.DependencyInjectionNuGet 软件包中提供。
IoC 和 DI 容器允许服务(类)通过其抽象基类或其实现的接口进行注册和访问。应用代码不需要关心实现契约的实际类,这使得在配置或运行时切换实际依赖项非常容易。除此之外,它还将依赖项注入到它正在构建的实际类中。例如,假设您有以下场景:
public interface IMyService
{
void MyOperation();
}
public interface IMyOtherService
{
void MyOtherOperation();
}
public class MyService : IMyService
{
private readonly IMyOtherService _other;
public MyService(IMyOtherService other)
{
this._other = other;
}
public void Operation()
{
//do something
}
}
如果您向 DI 容器注册一个MyService类,那么当它构建一个实际实例时,它将知道它还需要构建一个IMyOtherService实例来传递给MyService构造函数,这将针对实际IMyOtherService实现中的每个依赖项进行级联。
Host在构建主机时,初始化一个IServiceCollection实例,然后将其传递给Startup类的ConfigureServices方法。这是一种传统的方法,应该用于我们自己的注册。
现在,服务注册有三个组成部分:
- 将在其下注册的类型(注册的唯一密钥)
- 它的寿命
- 实例工厂
生命周期可以是以下情况之一:
Scoped:将为每个 web 请求(或作用域)创建一个新的服务实例,并且无论何时我们向 DI 框架请求,都会为相同的请求(作用域)返回相同的实例。Singleton:将要创建的实例保存在内存中,并始终返回。Transient:无论何时请求,都会创建一个新实例。
实例工厂可以是以下之一:
- 一个实际的实例,通常被视为一个
Singleton;当然,这不能用于Transient或Scoped寿命 - 具体的
Type,然后根据需要实例化 - 一个
Func<IServiceProvider, object>委托,知道如何在收到对 DI 容器的引用后创建具体类型的实例
您通过ConfigureServices方法的services参数注册服务及其实现,该参数为IServiceCollection实现:
//for a scoped registration
services.Add(new ServiceDescriptor(typeof(IMyService), typeof(MyService), ServiceLifetime.Scoped);
//for singleton, both work
services.Add(new ServiceDescriptor(typeof(IMyService), typeof(MyService),
ServiceLifetime.Singleton);
services.Add(new ServiceDescriptor(typeof(IMyService), newMyService());
//with a factory that provides the service provider as a parameter, from //which you can retrieve //other services
services.Add(new ServiceDescriptor(typeof(IMyService), (serviceProvider) =>
new MyService(), ServiceLifetime.Transient);
有几种扩展方法允许我们进行注册;以下各项完全相同:
services.AddScoped<IMyService, MyService>();
services.AddScoped<IMyService>(sp =>
new MyService((IMyOtherService) sp.GetService
(typeof(IMyOtherService))));
services.AddScoped(typeof(IMyService), typeof(MyService));
services.Add(new ServiceDescriptor(typeof(IMyService), typeof(MyService), ServiceLifetime.Scoped));
所有其他生命都是如此。
DI 容器还支持泛型类型,例如,如果您注册一个开放的泛型类型,例如MyGenericService<T>,您可以请求一个特定的实例,例如MyGenericService<ServiceProviderOptions>:
//register an open generic type
services.AddScoped(typeof(MyGenericService<>));
//build the service provider
var serviceProvider = services.BuildServiceProvider();
//retrieve a constructed generic type
var myGenericService = serviceProvider.GetService
<MyGenericService<string>>();
可以遍历IServiceCollection对象以查看已注册的内容。它只是ServiceDescriptor实例的集合。如果我们愿意,我们可以访问个人注册,甚至可以替换一个。
还可以删除特定基本类型或接口的所有注册:
services.RemoveAll<IMyService>();
RemoveAll扩展方法在Microsoft.Extensions.DependencyInjection.Extensions命名空间上可用。
One very important thing to bear in mind is that any services that implement IDisposable and are registered for either the Scoped or the Transient lifetimes will be disposed of at the end of the request.
DI 框架有作用域的概念,作用域注册与之绑定。我们可以创建新的作用域并将我们的服务与之关联。我们可以使用IServiceScopeFactory接口,该接口是自动注册的,它允许我们执行以下操作:
var serviceProvider = services.BuildServiceProvider();
var factory = serviceProvider.GetService<IServiceScopeFactory>();
using (var scope = factory.CreateScope())
{
var svc = scope.ServiceProvider.GetService<IMyService>();
}
从CreateScope内部范围内的服务提供者返回的任何范围绑定服务都将随范围一起销毁。有趣的是,如果任何作用域注册服务实现了IDisposable,那么它的Dispose方法将在作用域的末尾被调用。
你需要记住几件事:
- 同一个
Type可以多次注册,但只能在同一生命周期内注册。 - 您可以为同一个
Type注册多个实现,它们将在对GetServices的调用中返回。 GetService只返回给定Type的上一次注册的实现。- 您不能注册一个依赖关系为
Scoped的Singleton服务,因为它没有意义;根据定义,Scoped每次都会变化。 - 您不能将具体实例传递给
Scoped或Transient注册。 - 您只能从 factory 委托解析自身已注册的服务;但是,factory 委托仅在注册了所有服务之后才会被调用,因此您无需担心注册顺序。
- 如果给定的
Type没有注册服务,则决议返回null;不会引发任何异常。 - 如果注册的类型在其构造函数上具有不可解析的类型,即未在 DI 提供程序上注册的类型,则会引发异常。
一些.NET Core API 提供了执行注册的扩展方法,例如,AddMvc、AddDbContext或AddSession。默认情况下,ASP.NET Core 的引导自动注册以下服务:
| 服务类型 |
| Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory |
| Microsoft.AspNetCore.Hosting.IWebHostEnvironment |
| Microsoft.AspNetCore.Hosting.IStartup |
| Microsoft.AspNetCore.Hosting.IStartupFilter |
| Microsoft.AspNetCore.Hosting.Server.IServer |
| Microsoft.AspNetCore.Http.IHttpContextFactory |
| Microsoft.Extensions.Configuration.IConfiguration |
| Microsoft.Extensions.Hosting.IHostApplicationLifetime |
| Microsoft.Extensions.Logging.ILogger<T> |
| Microsoft.Extensions.Logging.ILoggerFactory |
| Microsoft.Extensions.Logging.ILoggerFactory |
| Microsoft.Extensions.ObjectPool.ObjectPoolProvider |
| Microsoft.Extensions.Options.IConfigureOptions<T> |
| Microsoft.Extensions.Options.IOptions<T> |
| Microsoft.Extensions.Options.IConfigureOptions<T> |
| Microsoft.Extensions.Options.IOptionsSnapshot<T> |
| Microsoft.Extensions.Options.IOptionsMonitor<T> |
| Microsoft.Extensions.Options.IOptionsChangeTokenSource<T> |
| Microsoft.Extensions.Options.IOptionsFactory<T> |
| System.Diagnostics.DiagnosticListener |
| System.Diagnostics.DiagnosticListener |
| System.Diagnostics.DiagnosticSource |
所有注册完成后,最终将从IServiceCollection实例构建实际的依赖关系框架。它的公共界面正是自.NET1.0 以来一直存在的古老的IServiceProvider。它公开了一个方法GetService,该方法将Type作为其单个参数进行解析。
但是,Microsoft.Extensions.DependencyInjection包和命名空间中有一些有用的通用扩展方法:
GetService<T>():如果已注册,则返回已正确转换的服务类型实例,否则返回nullGetRequiredService<T>():尝试检索给定服务类型的注册,如果未找到,则引发异常GetServices<T>():返回注册密钥与给定服务密钥匹配(相同、实现或是子类)的所有服务
您可以为同一Type注册多个服务,但只有最后注册的服务才能使用GetService()检索。有趣的是,所有这些都将使用GetServices()返回!
Keep in mind that the latest registration for a Type overrides any previous one, meaning that you will get the latest item when you use a GetService, but all of the registrations are returnable by GetServices.
尽管最常见的用法可能是构造函数注入,其中 DI 框架创建了一个具体类型,并在构造函数中传递其所有依赖项,但也可以在任何给定时间请求我们想要的服务实例,方法是使用对IServiceProvider的引用,如以下上下文中可用的引用:
var urlFactory = this.HttpContext.RequestServices.
GetService<IUrlHelperFactory>();
这被称为服务定位器模式,有些人认为它是反模式。我不想在这里讨论,因为我认为这次讨论毫无意义。
IServiceProvider实例本身已在 DI 提供程序上注册,这使其成为可能的注入候选对象!
如果您想构建一个类型的实例,该类型的构造函数服务应该来自 DI 提供者,那么您可以使用ActivatorUtilities.CreateInstance方法:
var instance = ActivatorUtilities.CreateInstance<MyType>(serviceProvider);
或者,如果我们引用了Type,您可以使用以下内容:
MyType instance = (MyType) ActivatorUtilities.CreateInstance(serviceProvider, typeof(MyType));
最后,我需要谈谈别的事情。多年来,人们一直在使用第三方 DI 和 IoC 框架。netcore 虽然灵活,但肯定允许我们使用自己的,这可能会提供内置内核所提供的附加功能。我们所需要的只是让我们选择的 DI 提供者也公开一个IServiceProvider实现;如果有,我们只需要从ConfigureServices方法返回:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
//AutoFac
var builder = new ContainerBuilder();
//add registrations from services
builder.Populate(services);
return new AutofacServiceProvider(builder.Build());
}
AutofacServiceProvider还实现了IServiceProvider,因此我们可以将其从ConfigureServices退回,作为开箱即用 DI 容器的替代品。
总之,很高兴看到国际奥委会和国际奥委会。这只是基础;我们将在本书余下的大部分内容中讨论 DI。
验证依赖项
通常,通过控制器(和其他组件)的构造函数将依赖项注入控制器(和其他组件)。问题是,我们可能不知道我们所依赖的服务丢失了注册,直到我们尝试访问依赖它的控制器时为时已晚,它才会崩溃。
当在Development环境中运行时,我们会对此进行检查。我们将所有控制器注册为服务:
services
.AddControllers()
.AddControllersAsServices();
然后,当访问控制器时,web 应用(任何 web 应用,而不是具有特定依赖项的 web 应用)将尝试验证其已注册的所有依赖项,并且,如果找到未找到依赖项的依赖项,将引发异常。此异常将确切地告诉您缺少什么服务。ASP.NET Core 还检查作用域服务的有效性,例如,不能从作用域之外检索注册为Scoped的服务(通常是 web 请求)。
通过在Program中的引导代码中添加以下内容,您实际上可以控制Development以外环境的此行为:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host
.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(builder =>
{
builder.UseStartup<Startup>();
builder.UseDefaultServiceProvider(options =>
{
options.ValidateOnBuild = true;
options.ValidateScopes = true;
});
});
注意ValidateOnBuild和ValidateScopes属性。ValidateOnBuild用于测试依赖关系图是否有效,ValidateScopes用于测试需要范围的服务是否从范围内检索。默认情况下,两者都是false,但Development环境中除外。
接下来,让我们继续了解我们工作的环境。
了解环境
.NET Core 具有环境的概念。环境基本上是一个运行时设置,其形式为一个名为ASPNETCORE_ENVIRONMENT的环境变量。此变量可以采用以下值之一(请注意,这些值区分大小写):
Development:一个开发环境,可能不需要太多解释Staging:用于测试的试生产环境Production:应用发布后将在其中生存的环境(或尽可能类似的环境)
具体来说,您可以传递任何值,但这些值对.NETCore 具有特殊意义。您可以通过多种方式访问当前环境,但您最有可能使用以下方法之一、IWebHostEnvironment接口的扩展方法和属性(向Microsoft.Extensions.Hosting名称空间添加using引用):
IsDevelopment()IsProduction()IsStaging()IsEnvironment("SomeEnvironment")EnvironmentName
IsDevelopment、IsProduction和IsStaging扩展方法只是使用IsEnvironment方法的方便方法。根据实际环境,您可以决定代码,例如选择不同的连接字符串、web 服务 URL 等等。需要指出的是,这与调试或版本编译器配置无关。
通常从Startup类的Configure方法的参数中得到IWebHostEnvironment的实例:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { ... }
但是您也可以从 DI 容器中获得它,它可以从HttpContext类中获得,以及其他地方,作为RequestServices属性:
var env = HttpContext.RequestServices.GetService<IWebHostEnvironment>();
或者您也可以将IWebHostEnvironment注入控制器,如下所示:
public IActionResult Index([FromServices] IWebHostEnvironment env) { ... }
这允许您随时检查当前环境,以便具有条件逻辑。
The IWebHostEnvironment replaces the old IHostingEnvironment interface available in pre-3 .NET Core, now deprecated.
最后一点注意:服务配置可以很好地适应环境。我们可以有多个方法,而不是单一的ConfigureServices方法,分别命名为ConfigureDevelopmentServices、ConfigureStagingServices和ConfigureProductionServices。为了清楚起见,可以在Configure前缀之后和Services之前添加任何环境名称。将调用特定于环境的方法(例如,ConfigureDevelopmentServices),而不是通用方法(ConfigureServices):
public void ConfigureDevelopmentServices(IServiceCollection services)
{
//WILL be called for environment Development
}
public void ConfigureServices(IServiceCollection services)
{
//will NOT be called for environment Development
}
而且,如果我们想更进一步,我们甚至可以对Startup类执行相同的操作:我们可以为每个环境创建一个类,并将其作为后缀:
public class StartupDevelopment
{
public StartupDevelopment(IConfiguration configuration) { ... }
public void ConfigureServices(IServiceCollection services) { ... }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { ... }
}
或者,如果我们想动态指定驻留在不同程序集中的类,我们必须稍微更改Program类中的代码,以便从程序集中引导:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host
.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(builder =>
{
builder.UseStartup(typeof(Startup).Assembly.FullName);
});
我们可以从程序集而不是从特定类执行此操作:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host
.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(builder =>
{
builder.UseStartup<Startup>();
});
这是一个很好的特性,可以帮助我们更好地组织代码!现在让我们看一看可以用来开始创建项目的标准项目模板。
了解项目模板
从 3.x 版开始,用于创建 ASP.NET Core 项目的 Visual Studio 模板向Program类添加了以下内容(或非常类似的内容):
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host
.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(builder =>
{
builder.UseStartup<Startup>();
});
这与以前的版本相比有了一些变化,现在更加固执己见;在本章前面讨论 OWIN 时,我已经展示了这一点。
Host类公开静态CreateDefaultBuilder,返回一个完整构建的IHostBuilder实例。CreateDefaultBuilder方法实际上是在我们背后做很多事情:
-
创建一个
ConfigurationBuilder并向其添加环境变量提供程序(有关更多详细信息,请参见第 2 章配置) -
将
appsettings.json(必需)和appsettings.<environment>.json(可选)JSON 文件和提供程序添加到配置生成器中 -
配置用户机密配置(如果在开发模式下运行)
-
如果传递了命令行参数,则配置命令行配置
-
将 Kestrel 设置为主机以使用和加载与 Kestrel 相关的配置
-
将内容根目录设置为当前目录
-
设置主机以使用作为
ASPNETCORE_SERVER.URLS环境变量传递的 URL(如果存在) -
配置控制台、调试、事件源和事件日志的日志记录(如果在 Windows 中)
-
添加 IIS 集成
-
将默认主机生存期设置为
ConsoleHostLifetime -
配置服务提供程序参数,以验证在
Development环境中运行的已注册服务的范围和生存期 -
注册一些服务,如
IConfiguration
这些是您获得的默认值,但是您可以通过在IHostBuilder接口上使用一些扩展方法来覆盖其中任何一个:
Host
.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, builder) =>
{
//add or remove from the configuration builder
})
.ConfigureContainer<MyContainer>((context, container) =>
{
//configure container
})
.ConfigureLogging((context, builder) =>
{
//add or remove from the logging builder
});
.ConfigureServices(services =>
{
//register services
})
.ConfigureWebHostDefaults(builder =>
{
builder.UseStartup<Startup>();
});
在实例化默认构建器之后,我们要求它使用Startup类,在这里我们可以配置我们想要的东西,比如注册服务、中间件组件等等
IHostBuilder然后构建一个IHost,然后我们要求它运行。这就是让我们的应用真正工作的原因。
我们以前谈过Startup课。基本上,它公开了两种方法,按惯例命名为ConfigureServices和Configure;第一个用于向默认 DI 提供程序注册服务及其实现(可能使用另一个),第二个用于向 ASP.NET Core 管道添加中间件组件。
您需要记住的主要事项如下:
- Kestrel 是默认的主机服务器。
- 自动添加 JSON 和环境的配置提供程序;如果在
Development环境中运行,则添加用户机密。应该有一个appsettings.json文件,可能还有一个appsettings.<environment>.json文件,每个环境都有覆盖。 - 已为 Visual Studio 的控制台和调试窗格启用日志记录。
现在我们已经了解了这些模板,让我们看看自 2.0 版以来发生了什么变化,以及不同的工具、模板、功能等是如何受其影响的。
2.0 版之后有什么新功能?
让我们看一下 2.0 版中的新功能,具体内容如下。
ASP.NET Core 2.1
ASP.NET Core 2.1 于 2018 年 5 月 30 日在网上发布。它没有包含大量的突破性的变化或奇妙的新特性,但我要强调以下几点。
信号员
ASP.NET Core 的实时通信库 SignalR 最终脱离了预发布版。它有很多在前核心版本中不存在的优点,我们将在它自己的章节中介绍。
Razor 类库
现在可以将 Razor UI 文件(.cshtml)打包为 NuGet 包。这为许多有趣的可能性打开了大门。在关于组件重用的一章中将有更多关于这方面的内容。
剃须刀页面的改进
ASP.NET Core 2.0 中引入的 Razor 页面现在也支持一些领域,并具有一些附加功能。我们将在“观点”一章中讨论这些问题。
新的部分标记辅助程序
有一个新的<partial>标记帮助器,它提供了一个比RenderPartial更干净的替代方案。同样,它将在关于组件重用的章节中讨论。
顶级参数验证
在以前版本的 ASP.NET Core 中,您必须明确检查模型的验证状态,通常是通过调用ModelState.IsValid。现在,情况不再如此,使用任何验证器配置的参数验证都是自动完成的。我们将在专门讨论表单和模型的章节中对此进行更多讨论。
标识 UI 库和脚手架
与新的 RazorUI 类库一起,VisualStudio 现在支持脚手架,ASP.NET Core 标识是一个很好的候选。这意味着,如果我们选择 ASP.NET Core Identity 作为身份验证提供程序,我们可以选择感兴趣的 UI 组件(登录页面、登录状态等),并提供其余的组件。这将在专门讨论安全性的章节中介绍。
虚拟认证方案
有一种新的机制,通过它我们可以抽象(并可能组合)不同的身份验证提供者:它被称为虚拟身份验证方案,我们将在关于安全性的一章中讨论它。
默认情况下为 HTTPS
我还能说什么?HTTPS 现在是默认的,但是可以通过 VisualStudio 向导进行配置。希望它既能使您的应用更加安全,又能防止一些只有在部署到生产环境时才会出现的微妙问题。这将在关于安全的一章中介绍。
与 GDPR 相关的模板更改
全球数据保护条例(GDPR在跟踪用户和存储其数据方面施加了许多限制。新的 Visual Studio 模板和 ASP.NET Core 2.1 API 引入了一些与 cookie 跟踪和明确用户同意相关的更改。我们将在安全章节中讨论所有这些。
If you want to know more about GDPR, please visit https://eugdpr.org.
MVC 功能测试改进
功能(或集成)测试现在更容易设置,因为.NETCore2.1 做出了一些通常可以接受的假设。在测试一章中将有更多关于这方面的内容。
API 约定和支持类型
在为 API 端点提供元数据和可发现性方面有了一些改进,所有这些都将在关于 API 控制器和操作的新章节中介绍。
通用主机生成器
这对于 ASP.NET Core 开发人员可能不太重要,但有一个新的主机生成器可用于构建非 HTTP 端点。因为这太具体了,我们不会在本书中讨论它。
更新 SPA 模板
对于一些最流行的 JavaScript 框架,单页应用(SPA)提供了新的模板:Angular、React 和 React with Redux。我将(简要地)在关于客户端开发的一章中介绍这些内容。
ASP.NET Core 2.2
ASP.NET Core 2.2 于 2018 年 12 月发布。以下各节概述了一些更改。
API 代码分析器
VisualStudio 现在可以根据约定自动添加描述 API 操作的返回类型和代码的属性。
健康检查 API
健康检查 API 以前是作为预发布代码提供的,但现在可以作为稳定且完全支持的多条件检查提供。
端点路由
现在有了一种更快的路由机制,它还允许在管道中更早地推断当前路由。它还包括参数变压器。
问题详细信息(RFC 7807)支持
RFC 7807 问题细节的实现提供了新的支持,用于表示 API 错误。
ASP.NET Core 3.0
ASP.NET Core 3.0 于 2019 年 9 月发布。以下是它的一些最大变化。
C#8.0
Visual Studio 2019 与.NET Core 3.0 一起进行了更新,以支持 C#8.0 的新语言功能。
.NET 标准 2.1
新的.NET 标准也发布了,具有更大的 API 界面。
布拉佐
Blazor(服务器托管模型)现在包含在.NETCore3 中。
内置 JSON 支持
.NET 现在拥有自己的 JSON 库System.Text.Json。
HTTP/2 支持
HttpClient现在支持 HTTP/2,默认在 Kestrel 中启用。
gRPC
用于.NET 的 gRPC 已发布。Visual Studio 和dotnet现在有了 gRPC 的模板。
IdentityServer 集成
身份验证现在能够与IdentityServer集成。
端点路由
端点路由现在是默认的。
迁移到 ASP.NET Core 3.x
将项目更新到版本 3 应该非常简单,只需将.csproj文件的TargetFramework属性更新为包含netcoreapp3.0(或netcoreapp3.1(对于.NET Core 3.1),而不是netcoreapp2.0,并删除对Microsoft.AspNetCore.App的任何引用。删除DotNetCliToolReference也是强制性的,因为它已被弃用,其用途已被全局工具取代。当然,当 VisualStudio 要求您更新解决方案的 NuGet 软件包时,您应该使用最新的功能。
For a detailed, step-by-step tutorial, please go to https://docs.microsoft.com/en-us/aspnet/core/migration/20_21.
版本集
ASP.NET Core 的某些功能只有在您明确要求时才可用。这是通过调用SetCompatibilityVersion扩展方法完成的:
services .AddMvc() .SetCompatibilityVersion
(CompatibilityVersion.Version_3_0);
您可以传递给SetCompatibilityVersion方法的值如下:
Latest:使用最新功能(撰写本书时,版本3)Version_2_0:仅使用 ASP.NET Core2.0支持的子集Version_2_1:使用版本2.1中引入的功能Version_3_0:使用3版本的特性
因为我们想探索 ASP.NET Core 可用的所有功能,所以我们可以用Latest或Version_3_0来称呼它。如果不指定值,则默认为最新主版本:3。
There is no flag for version 3.1 because this release does not contain breaking changes from version 3.
现在让我们继续看一些工具,它们将在本书末尾的两个附录中进行更深入的介绍。
NuGet 和 dotnet 工具
有两种工具与.NET Core SDK 密切相关:
dotnetnuget
这些工具是.NET 开发的必备工具:第一个,dotnet是什么,NuGet 库生态系统,安装、发布和管理 NuGet 包集。这个是
dotnet始终使用系统上可用的最新.NET Core 版本执行。在附录 1中,您将看到该工具及其用法的详细说明。
您可以从获取nuget工具 https://www.nuget.org/packages/NuGet.CommandLine 。
总结
在第一章中,我们介绍了 ASP.NET Core 和.NET Core 中一些最大的变化。我们将向您介绍.NET Core 中的一些关键概念:NuGet 分发模式、OWIN 管道、托管模型、环境、改进的上下文和内置的依赖关系框架,这些都是 ASP.NET Core 3 中新增的。我们还了解了nuget和dotnet工具,这是命令行.NET 开发的瑞士军刀,将在附录**x1中详细介绍。
在下一章中,我们将通过探索应用的配置开始.NET Core 之旅。
问题
现在,您应该能够回答以下问题:
- DI 的好处是什么?
- 什么是环境?
- MVC 是什么意思?
- 内置 DI 容器中支持的生存期是多少?
- NET Core 和.NET 标准之间有什么区别?
- 什么是元包?
- 奥文是什么?
二、配置
本章介绍 ASP.NET Core 应用的配置。每个应用都需要以一种或另一种形式进行配置,因为如果发生任何情况,都可以更轻松地更改底层行为—考虑连接字符串、凭据、互联网协议(IP地址、,或任何其他类型的数据,这些数据可能会随时间变化,因此不适合硬编码。
配置可以通过多种方式完成,其中一些甚至不需要重新部署应用,这是一个巨大的好处。幸运的是,.NETCore 的构思考虑到了这一点,并且具有很强的可扩展性,因此它可以覆盖大多数场景,包括基本场景和高级场景。它还可以很好地处理其他方面,比如安全性和依赖注入。
另外,一个非常典型的配置只是以切换或切换为特征:某些东西要么启用,要么不启用。NETCore3 引入了一个新的特性切换库,该库位于主配置框架之外,但这里将介绍它。
阅读本章后,您应该能够理解以下内容:
- 配置在.NET Core 框架上的工作方式
- 我们有哪些可用的配置源
- 如何扩展它,使它更有用,更符合您的需要
- 运行时主机配置
- .NETCore3 中引入的新功能切换机制
技术要求
为了实现本章介绍的示例,您需要.NET Core 3软件开发工具包(SDK和某种文本编辑器。当然,VisualStudio2019(任何版本)满足所有要求,但您也可以使用 VisualStudio 代码。
源代码可以在这里从 GitHub 检索:https://github.com/PacktPublishing/Modern-Web-Development-with-ASP.NET-Core-3-Second-Edition 。
开始
以前版本的.NET 有一个相对简单的配置系统,所有设置都进入扩展名为.config的可扩展标记语言(XML文件)。有一个基本的模式可以处理系统设置和非类型化的键值对,但它们都是字符串。还有某种程度的继承,因为有些设置可以在机器范围内定义,然后在每个应用中覆盖,甚至在互联网信息服务(IIS应用)下的虚拟应用中也可以覆盖。通过编写和注册.NET 类,可以使用类型化设置和复杂结构定义自定义节。
然而,尽管这看起来很方便,但它也有其局限性,即:
- 仅支持 XML 文件;不可能有其他现成的配置源。
- 很难在每个环境(分段、质量保证(质量保证)、生产等)中有不同的配置文件/配置部分。
- 配置更改时无法接收通知。
- 保存更改很棘手。
此外,由于依赖注入不是核心.NET 基础设施的一部分,因此无法将配置值自动注入到其服务中。让我们看看.NETCore3 如何帮助我们克服这些限制。
.NETCore 中的配置
认识到这一点,微软将配置作为.NET Core 的首要概念,并以一种非常灵活、可扩展的方式实现了这一点。这一切都从一个生成器实例开始;我们向它添加提供者,完成后,我们只要求它构建一个配置对象,该对象将在内存中保存从每个提供者加载的所有值。
此配置对象将能够透明地从任何添加的提供程序返回配置设置,这意味着无论源如何,我们都使用相同的语法查询配置选项。它将保存从所有注册提供程序加载的所有值的内存表示,并允许您更改这些值或添加新条目。
.NET Core 中配置应用编程接口(API的基类模型如下:

因此,提供程序机制分为两个基本接口及其实现,如下所示:
IConfigurationSource负责创建IConfigurationProvider的具体实例;每个可用的提供者(下一个)都实现这个接口。IConfigurationProvider指定实际检索值、重新加载等的合同;实现这一点的根类是ConfigurationProvider,还有一个特定的实现,作为所有基于文件的提供程序的根,FileConfigurationProvider。
ConfigurationBuilder本身只是IConfigurationBuilder接口的具体实现,没有其他实现。其合同规定了我们如何添加提供程序并从中构建配置,如以下代码块所示:
var builder = new ConfigurationBuilder() .Add(source1)
.Add(source2);
var cfg = builder.Build();
至于配置本身,有三个基本接口,如下所示:
IConfiguration:指定检索和设置配置节和值、监视更改等的方法。IConfigurationRoot:这增加了一种将配置重新加载到IConfiguration的方法,以及用于构建配置的提供者列表。IConfigurationSection:这是一个配置节,意味着它可以位于配置根目录下的某个位置,该位置由路径(所有父节的键,直到并包括其自己的键)和唯一标识父节中该节的键标识。
我们将很快看到使用配置值的方法,但现在值得一提的是,我们可以通过IConfiguration中的重载[]操作符检索和设置单个设置,如下所示:
cfg["key"] = "value";
string value = cfg["key"];
它将一个字符串作为key并返回一个字符串作为value,在接下来的部分中,我们将看到如何绕过这个限制。如果给定密钥不存在条目,则返回null。
All keys are case-insensitive. A path is composed of a colon (:)-combined set of keys and subkeys that can be used to get to a specific value.
NET Core 配置具有节的概念。通过运行以下代码,我们可以获得特定节,甚至可以检查它是否完全存在:
var section = cfg.GetSection("ConnectionStrings");
var exists = section.Exists();
按照惯例,各节之间用:分隔。使用特定于节的键从节中获取值与使用完全限定键从配置根中检索值相同。例如,如果您有一个A:B:C键,这与在A部分的B中有一个C键相同,如下面的屏幕截图所示:
var valueFromRoot = cfg["A:B:C"];
var aSection = cfg.GetSection("A");
var bSection = aSection.GetSection("B");
var valueFromSection = bSection["C"];
为便于记录,核心配置 API 在Microsoft.Extensions.Configuration和Microsoft.Extensions.Configuration.BinderNuGet 包中实现,这些包自动包含在其他包中,例如特定提供商的包中。现在让我们看看可用的提供者。
ASP.NET Core 2 and later automatically registers the IConfiguration instance in the dependency injection framework; for previous versions, you need to do this manually.
提供者
可用的 Microsoft 配置提供程序(及其 NuGet 软件包)如下所示:
-
JavaScript 对象表示法(JSON文件:
Microsoft.Extensions.Configuration.Json -
XML 文件:
Microsoft.Extensions.Configuration.Xml -
初始化(INI文件:
Microsoft.Extensions.Configuration.Ini -
用户机密:
Microsoft.Extensions.Configuration.UserSecrets -
Azure 密钥库:
Microsoft.Extensions.Configuration.AzureKeyVault -
环境变量:
Microsoft.Extensions.Configuration.EnvironmentVariables -
命令行:
Microsoft.Extensions.Configuration.CommandLine -
内存:
Microsoft.Extensions.Configuration -
码头工人秘密:
Microsoft.Extensions.Configuration.DockerSecrets
Some of these are based upon the FileConfigurationProvider class: JSON, XML, and INI.
引用这些包时,会自动使其扩展可用。因此,例如,如果您想添加 JSON 提供程序,您有两个选项,下面详细介绍。
您可以直接添加一个JsonConfigurationSource,如下:
var jsonSource = new JsonConfigurationSource {
Path = "appsettings.json" };
builder.Add(jsonSource);
或者,您可以使用AddJsonFile扩展方法,如下所示:
builder.AddJsonFile("appsettings.json");
很可能,扩展方法就是您所需要的。正如我所说,您可以同时拥有任意数量的提供者,如以下代码片段所示:
builder
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables()
.AddXmlFile("web.config");
您只需要记住,如果两个提供程序返回相同的配置设置,那么添加它们的顺序很重要;您得到的结果将来自最后添加的提供程序,因为它将覆盖以前的提供程序。例如,假设您正在添加两个 JSON 配置文件,一个在所有环境(开发、登台和生产)中通用,另一个用于特定环境;在这种情况下,您可能会遇到以下问题:
builder
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{env.EnvironmentName}.json");
This is so the environment-specific configuration file takes precedence.
当然,每一个提供者都会设置不同的属性;例如,所有基于文件的提供者都需要一个文件路径,但当我们讨论环境变量时,这是没有意义的。
基于文件的提供者
JSON、XML 和 INI 配置源都基于文件。因此,它们的类继承自FileConfigurationSource抽象基类。此类提供以下配置属性:
Path:查找文件的实际、完全合格的物理路径;这是必需的设置。Optional:一个布尔标志,用于指定缺少文件是否会导致运行时错误(false)或不(true);默认为false。ReloadOnChange:这里,您决定是否自动检测源文件(true)的更改(false);默认为false。ReloadDelay:检测到更改时重新加载文件之前的延迟,以毫秒为单位(ReloadOnChange设置为true;默认值为 250 毫秒。OnLoadException:解析源文件出错时调用的委托;默认情况下,这是空的。FileProvider:实际检索文件的文件提供者;默认为PhysicalFileProvider实例,设置为Path属性的文件夹。
所有扩展方法都允许您为每个属性提供值,除了OnLoadException。您还可以自由指定自己的IFileProvider具体实现,如果您有特定需求,例如从 ZIP 文件中获取文件,您应该这样做。ConfigurationBuilder有一个扩展方法SetBasePath,它设置一个指向文件系统上文件夹的默认PhysicalFileProvider,以便您可以将相对文件路径传递给配置源的Path属性。
如果您将ReloadOnChange设置为true,.NET Core 将启动一个操作系统特定的文件,用于监视源文件上的手表;因为这些东西是有成本的,所以尽量不要买太多手表。
一个典型的例子如下:
builder
.SetBasePath(@"C:\Configuration")
.AddJsonFile(path: "appsettings.json", optional: false,
reloadOnChange: true)
.AddJsonFile(path: $"appsettings.{env.EnvironmentName}.json",
optional: true, reloadOnChange: true);
这将导致从C:\Configuration文件夹加载appsettings.json文件(如果不存在,则引发异常),然后加载appsettings.Development.json(这次,如果文件不存在,则忽略它)。只要其中一个文件发生更改,就会重新加载它们并更新配置。
Very important: in operating systems or filesystems where the case matters, such as Linux, make sure that the name of the file that takes the environment name (for example, appsettings.Development.json) is in the right case—otherwise, it won't be found!
但是,如果要添加错误处理程序,则需要手动添加配置源,如下所示:
var jsonSource = new JsonConfigurationSource { Path = "filename.json" };
jsonSource.OnLoadException = (x) =>
{
if (x.Exception is FileNotFoundException ex)
{
Console.Out.WriteLine($"File {ex.FileName} not found");
x.Ignore = true;
}
};
builder.Add(jsonSource);
这样,我们就可以防止某些错误导致应用崩溃。
所有基于文件的提供者都是通过名为AddxxxFile的扩展方法添加的,其中xxx是实际类型—Json、Xml或Ini——并且始终采用相同的参数(path、optional、reloadOnChange)。
JSON 提供程序
我们通常使用AddJsonFile扩展名方法添加 JSON 配置文件。JSON 提供程序将加载一个包含 JSON 内容的文件,并使用点符号使其结构可用于配置。以下代码段中显示了一个典型示例:
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)mssqllocaldb;
Database=aspnetcore"
}
}
任何有效的 JSON 内容都可以使用。到目前为止,无法指定模式。节只是 JSON 内容的子元素。
用于加载配置值的代码示例如下:
var defaultConnection = cfg["ConnectionStrings:DefaultConnection"];
XML 提供者
XML 越来越不常见,而 JSON 则越来越流行;然而,仍然有充分的理由使用 XML。因此,我们使用AddXmlFile扩展方法添加了一个 XML 文件,就配置而言,我们需要将 XML 内容包装在settings节点中;XML 声明是可选的。请参阅以下示例:
<settings Flag="2">
<MySettings>
<Option>10</Option>
</MySettings>
</settings>
同样,到目前为止,无法指定验证模式。使用此提供程序,节将作为子元素实现。
这方面的两个例子如下:
var flag = cfg["Flag"];
var option = cfg["MySettings:Option"];
INI 提供程序
INI 文件已经成为过去,但由于历史原因,Microsoft 仍然支持它们(实际上,Linux 也使用 INI 文件)。如果您不熟悉它的语法,它看起来是这样的:
[SectionA]
Option1=Value1
Option2=Value2
[SectionB]
Option1=Value3
您可以通过AddIniFile扩展方法将 INI 文件添加到配置中。
One word of advice: both XML and JSON file formats support anything that INI files do, so unless you have a very specific requirement, you're better off with either JSON or XML.
INI 文件中的节只映射到 INI 文件规范提供的内部节。
一个例子如下:
var optionB2 = cfg["SectionB:Option1"];
其他提供者
除了基于文件的提供程序外,还有其他存储和检索配置信息的方法。在这里,我们列出了.NETCore 中当前可用的选项。
用户机密
.NET Core 引入了用户机密作为每个用户存储敏感信息的手段。这样做的好处是,它以安全的方式保存在配置文件之外,并且其他用户看不到它。用户机密存储由userSecretsId标识(针对给定用户),VisualStudio 模板将其初始化为字符串和全局唯一标识符(GUID)的组合,例如aspnet-Web-f22b64ea-be5e-432d-abc6-0275a9c00377。
存储中的秘密可以通过dotnet可执行文件列出、添加或删除,如以下代码段所示:
dotnet user-secrets list --lists all the values in the
store
dotnet user-secrets set "key" "value" --set "key" to be "value"
dotnet user-secrets remove "key" --remove entry for "key"
dotnet user-secrets clear --remove all entries
你需要Microsoft.Extensions.SecretManager.Tools包。dotnet user-secrets命令仅在存在指定userSecretsId存储 ID 的项目文件时才起作用。AddUserSecrets扩展方法是我们用于向配置添加用户机密的方法,它将自动拾取此userSecretsId设置,或者您可以在运行时提供自己的设置,如下所示:
builder.AddUserSecrets(userSecretdId: "[User Secrets Id]");
另一个选项是从程序集获取用户机密 ID,在这种情况下,需要使用UserSecretsIdAttribute属性进行修饰,如下所示:
[assembly: UserSecretsId("aspnet-Web-f22b64ea-be5e-432d-abc6-0275a9c00377")
在这种情况下,加载它的方法在以下代码段中演示:
builder.AddUserSecrets<Startup>();
Be warned: if you have more than one assembly with the same user secret ID (by mistake), the application will throw an exception when loading them.
另一种指定用户机密的方法(在 ASP.NET Core 2.x 中)是通过.csproj文件,使用UserSecretsId元素,如以下代码片段所示:
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<UserSecretsId>9094c8e7-0000-0000-0000-c26798dc18d2</UserSecretsId>
</PropertyGroup>
无论您如何指定用户机密 ID,与所有其他提供程序一样,加载值的方式如下:
var value = cfg["key"];
如果您感兴趣,可以在此处阅读更多有关.NET Core 用户机密的信息:https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets
Azure 密钥保险库
Azure Key Vault是一项 Azure 服务,您可以利用它进行企业级安全密钥值存储。完整的描述超出了本书的范围,但您可以在此处阅读:https://azure.microsoft.com/en-us/services/key-vault 。只需说明,您通过AddAzureKeyVault扩展方法添加 Azure 密钥保险库提供程序,如以下代码行所示:
builder.AddAzureKeyVault(vault: "https://[Vault].vault.azure.net/",
clientId: "[Client ID]", clientSecret: "[Client Secret]");
在这之后,所有的数据都被添加到配置对象中,您可以用通常的方式检索它们。
命令行
获取配置设置的另一种非常流行的方法是命令行。可执行文件通常期望在命令行中传递信息,以便指示应该做什么或控制应该如何做。
要使用的扩展方法是AddCommandLine,它需要一个必需的和可选的参数,如下所示:
builder.AddCommandLine(args: Environment.GetCommandLineArgs().Skip(1).ToArray());
args参数通常来自Environment.GetCommandLineArgs(),我们去掉第一个参数,因为这是条目程序集的名称。如果我们在Program.Main中构建配置对象,我们也可以使用它的args参数。
现在,有几种方法可以指定参数。下面的代码段说明了一种方法:
Key1=Value1
--Key2=Value2
/Key3=Value3
--Key4 Value4
/Key5 Value5
下面是另一个例子:
dotnet run MyProject Key1=Value1 --Key2=Value2 /Key3=Value3 --Key4 Value4 /Key5 Value5
如果该值中有空格,则需要将其括在引号中("。您不能使用-(单破折号),因为这将被解释为dotnet的参数。
AddCommandLine的可选参数switchMappings是一个字典,可用于创建与命令行中的键重复的新键,如下所示:
var switchMappings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{ { "--Key1", "AnotherKey" } };
builder.AddCommandLine(
args: Environment.GetCommandLineArgs().Skip(1).ToArray(),
switchMappings: switchMappings);
这些密钥甚至可以包含特殊字符,例如,--a:key和/some.key是有效密钥。
同样,使用相同的语法检索它们的值。
环境变量
环境变量存在于所有操作系统中,也可以被视为配置的来源。很多工具,比如 Docker,都依赖于环境变量来获取它们的操作上下文。
将环境变量添加到.NET Core 配置非常简单;你只需要打电话AddEnvironmentVariables。默认情况下,这会将所有现有环境变量带入配置中,但我们也可以指定前缀,并过滤掉所有不以前缀开头的变量,如下所示:
builder.AddEnvironmentVariables(prefix: "ASPNET_");
因此,这将同时添加ASPNET_TargetHost和ASPNET_TargetPort,但不添加PATH或COMPUTERNAME。
如果使用双下划线分隔名称(例如,__),则支持节。例如,假设您有以下环境变量:
ASPNETCORE__ADMINGROUP__USERS=rjperes,pm
您可以这样访问ADMINGROUP部分:
var group = cfg
.GetSection("ASPNETCORE")
.GetSection("ADMINGROUP");
var users = group["USERS"];
记忆力
内存提供程序是在运行时动态指定值和使用字典对象的方便方法。我们使用AddInMemoryCollection扩展方法添加提供者,如下所示:
var properties = new Dictionary<string, string> { { "key", "value" } };
builder.AddInMemoryCollection(properties);
这种方法的优点是很容易用我们想要的任何值填充字典,特别是在单元测试中。
码头工人
从 Docker 存储的文件中获取机密的功能在.NETCore 中相对较新。基本上,它将尝试加载 Docker 实例中特定目录中的文本文件作为值,其中键是文件名本身。这是 Docker 的一个实际功能,您可以在这里阅读更多内容:https://docs.docker.com/engine/swarm/secrets
AddDockerSecrets扩展方法采用两个可选参数:用户机密目录和该目录本身是否可选;换句话说,如果它不存在,就忽略它。以下代码段对此进行了说明:
builder.AddDockerSecrets(secretsPath: "/var/lib/secrets", optional: true);
如果我们使用获取配置对象的重载,则可以指定这两个参数加上一个ignore前缀和一个委托,用于按文件名筛选出文件,如以下代码块所示:
builder.AddDockerSecrets(opt =>
{
opt.SecretsDirectory = "/var/lib/secrets";
opt.Optional = true;
opt.IgnorePrefix = "ignore.";
opt.IgnoreCondition = (filename) => !filename.Contains($".{env.EnvironmentName}.");
});
在这里,我们将过滤掉以ignore.开头的两个文件,以及不包含当前环境名称的文件(例如.Development.。很酷!
默认提供者
默认应用模板(WebHostBuilder.CreateDefaultBuilder中包含的 ASP.NET Core 代码注册了以下提供程序:
- JSON
- 环境
- 命令行
- 用户机密
当然,您可以向 configuration builder 添加新的提供程序以满足您的需要。接下来,我们将了解如何为特定的配置需求创建自定义提供程序。
创建自定义提供程序
虽然我们有几种存储配置值的选项,但您可能有自己的特定需求。例如,如果您使用的是 Windows,则可能希望将配置设置存储在注册表中。为此,您需要一个自定义提供程序。让我们来看看如何构建一个。
首先,您需要将Microsoft.Win32.RegistryNuGet 包添加到您的项目中。然后,我们从实现IConfigurationSource开始,如下所示:
public sealed class RegistryConfigurationSource : IConfigurationSource
{
public RegistryHive Hive { get; set; } = RegistryHive.CurrentUser;
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new RegistryConfigurationProvider(this);
}
}
从前面的代码块中可以看到,唯一可配置的属性是Hive,通过它可以指定特定的注册表配置单元,其中CurrentUser (HKEY_CURRENT_USER)是默认值。
接下来,我们需要一个IConfigurationProvider实现。让我们从ConfigurationProvider类继承,因为它负责一些基本的实现,比如重新加载(当我们直接转到源代码时,我们不支持重新加载)。代码可以在这里看到:
public sealed class RegistryConfigurationProvider : ConfigurationProvider
{
private readonly RegistryConfigurationSource _configurationSource;
public RegistryConfigurationProvider(
RegistryConfigurationSource configurationSource)
{
_configurationSource = configurationSource;
}
private RegistryKey GetRegistryKey(string key)
{
RegistryKey regKey;
switch (_configurationSource.Hive)
{
case RegistryHive.ClassesRoot:
regKey = Registry.ClassesRoot;
break;
case RegistryHive.CurrentConfig:
regKey = Registry.CurrentConfig;
break;
case RegistryHive.CurrentUser:
regKey = Registry.CurrentUser;
break;
case RegistryHive.LocalMachine:
regKey = Registry.LocalMachine;
break;
case RegistryHive.PerformanceData:
regKey = Registry.PerformanceData;
break;
case RegistryHive.Users:
regKey = Registry.Users;
break;
default:
throw new InvalidOperationException($"Supplied hive
{_configurationSource.Hive} is invalid.");
}
var parts = key.Split('\\');
var subKey = string.Join("", parts.Where(
(x, i) => i < parts.Length - 1));
return regKey.OpenSubKey(subKey);
}
public override bool TryGet(string key, out string value)
{
var regKey = this.GetRegistryKey(key);
var parts = key.Split('\\');
var name = parts.Last();
var regValue = regKey.GetValue(name);
value = regValue?.ToString();
return regValue != null;
}
public override void Set(string key, string value)
{
var regKey = this.GetRegistryKey(key);
var parts = key.Split('');
var name = parts.Last();
regKey.SetValue(name, value);
}
}
此提供程序类利用RegistryAPI 从 Windows 注册表中检索值,当然,这在非 Windows 计算机上不起作用。ConfigurationProvider类中定义的TryGet和Set方法都委托给私有GetRegistryKey方法,该方法从注册表中检索键值对。
最后,让我们添加一个友好的扩展方法以简化注册,如下所示:
public static class RegistryConfigurationExtensions
{
public static IConfigurationBuilder AddRegistry(
this IConfigurationBuilder builder,
RegistryHive hive = RegistryHive.CurrentUser)
{
return builder.Add(new RegistryConfigurationSource { Hive = hive });
}
}
现在,您可以使用此提供程序,如下所示:
builder
.AddJsonFile("appsettings.json")
.AddRegistry(RegistryHive.LocalMachine);
又好又简单,你不觉得吗?现在,让我们看看如何使用我们注册的提供者的配置文件。
使用配置值
现在,我们已经了解了如何设置配置提供程序,但是我们如何准确地使用这些配置值呢?让我们看看下面几节。
显式获取和设置值
请记住,.NET 配置允许您使用[]符号设置读取和写入,如以下代码段所示:
var value = cfg["key"];
cfg["another.key"] = "another value";
当然,在配置对象中设置值并不意味着它将被持久化到任何提供者中;配置仅保存在内存中。
也可以尝试将值转换为特定类型,如下所示:
cfg["count"] = "0";
var count = cfg.GetValue<int>("count");
Don't forget that the value that you want to convert needs to be convertible from a string; in particular, it needs to have TypeConverter defined for that purpose, which all .NET Core primitive types do. The conversion will take place using the current culture.
配置部分
也可以使用配置部分。配置节通过冒号(:指定,如section:subsection中所示。可以指定节的无限嵌套。但是,我听到你问什么是配置部分,我们如何定义配置部分?这取决于您使用的配置源。
对于 JSON,配置部分基本上映射到一个复杂的属性。请查看以下代码段以查看此示例:
{
"section-1": {
"section-2": {
"section-3": {
"a-key": "value"
}
}
}
}
Not all providers are capable of handling configuration sections or handle them in the same way. In XML, each section corresponds to a node; for INI files, there is a direct mapping; and for the Azure Key Vault, user secrets, memory (dictionaries), and providers, sections are specified as keys separated by colons (for example, ASPNET:Variable, MyApp:Variable, Data:Blog:ConnectionString, and more). For environment variables, they are separated by double underscores (__). The example Registry provider I showed earlier does not, however, support them.
我们这里有几个部分,如下所示:
- 根部
section-1section-2section-3
因此,如果我们想要访问a-key键的值,我们将使用以下语法:
var aKey = cfg["section-1:section-2:section-3:a-key"];
或者,我们可以请求section-3部分并直接从中获取a-key值,如以下代码片段所示:
var section3 = cfg.GetSection("section-1:section-2:section-3");
var aKey = section3["a-key"];
var key = section3.Key; //section-3
var path = section3.Path; //section-1:section-2:section-3
节将包含从中获取它的路径。这是在继承自IConfiguration的IConfigurationSection接口中定义的,因此它的所有扩展方法也都可用。
顺便说一下,您可以请求任何配置部分,并且始终会返回一个值,但这并不意味着它存在。您可以使用Exists扩展方法检查这种可能性,如下所示:
var fairyLandSection = cfg.GetSection("fairy:land");
var exists = fairyLandSection.Exists(); //false
配置部分可能有子项,我们可以使用GetChildren列出它们,如下所示:
var section1 = cfg.GetSection("section-1");
var subSections = section1.GetChildren(); //section-2
.NET Core 包含典型配置部分和连接字符串的简写。这是GetConnectionString扩展方法,它基本上查找名为ConnectionStrings的连接字符串并从中返回命名值。您可以使用我们讨论 JSON 提供程序时引入的 JSON 模式作为参考,如下所示:
var blogConnectionString = cfg.GetConnectionString("DefaultConnection");
获取所有值
它可能没有那么有用,但可以获得配置对象中存在的所有配置值(及其键)的列表。我们使用AsEnumerable扩展方法来实现这一点,如以下代码片段所示:
var keysAndValues = cfg.AsEnumerable().ToDictionary(kv => kv.Key, kv => kv.Value);
还有一个makePathsRelative参数,默认情况下是false,可以在配置节中使用,将节的键从返回条目的键中去掉。例如,假设您正在section-3部分工作。如果您在makePathsRelative设置为true的情况下呼叫AsEnumerable,则a-key的条目将显示为a-key而不是section-1:section-2:section-3:a-key。
绑定到类
另一个有趣的选项是将当前配置绑定到类。绑定过程将获取配置中存在的任何节及其属性,并尝试将它们映射到.NET 类。假设我们有以下 JSON 配置:
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}
我们还有一些课程,比如:
public class LoggingSettings
{
public bool IncludeScopes { get; set; }
public LogLevelSettings LogLevel { get; set; }
}
public class LogLevelSettings
{
public LogLevel Default { get; set; }
public LogLevel System { get; set; }
public LogLevel Microsoft { get; set; }
}
LogLevel comes from the Microsoft.Extensions.Logging namespace.
您可以将两者绑定在一起,如下所示:
var settings = new LoggingSettings { LogLevel = new LogLevelSettings() };
cfg.GetSection("Logging").Bind(settings);
LoggingSettings的值将从当前配置中自动填充,使目标实例的任何属性在配置中没有值时保持不变。当然,对于任何配置部分都可以这样做,因此如果您的设置没有存储在根级别,它仍然可以工作。
请注意,当底层数据发生更改时,这些数据不会自动刷新。我们将在稍后看到如何做到这一点。
另一个选项是进行配置生成并返回自实例化实例,如下所示:
var settings = cfg.GetSection("Logging").Get<LoggingSettings>();
要使其工作,模板类不能是抽象的,需要定义一个公共的无参数构造函数。
Don't forget that an error will occur if—and only if—a configuration value cannot be bound, either directly as a string or through TypeConverter to the target property in the Plain Old CLR Object (POCO) class. If no such property exists, it will be silently ignored. The TypeConverter class comes from the System.ComponentModel NuGet package and namespace.
由于在使用基于文件的配置时,所有属性都存储为字符串,因此提供程序需要知道如何将这些属性转换为目标类型。幸运的是,包含的提供程序知道如何对大多数类型执行此操作,例如:
- 串
- 整数
- 浮点(前提是十进制字符与当前区域性相同)
- 布尔型(
true或false在任何外壳中) - 日期(格式必须符合当前文化或符合征求意见(RFC)3339/国际标准化组织(ISO)8601)
- 时间(hh:mm:ss或 RFC 3339/ISO 8601)
- 伪装
- 枚举
注入值
好了,我们现在知道了如何从多个源加载配置值,我们还知道了明确请求配置值的几种方法。然而,.NETCore 严重依赖依赖于依赖项注入,因此我们可能也希望将其用于配置设置。
首先,很明显,我们可以在依赖项注入框架中注册配置对象本身,如下所示:
var cfg = builder.Build();
services.AddSingleton(cfg);
无论我们在哪里要求一个IConfigurationRoot对象,我们都会得到这个。我们还可以将其注册为 baseIConfiguration,这也是安全的,尽管我们无法重新加载配置(稍后我们将更详细地介绍这一点)。这一点如下所示:
services.AddSingleton<IConfiguration>(cfg);
Since version 2.0, ASP.NET Core automatically registers the configuration object (IConfiguration) with the dependency injection framework.
我们可能还对向 POCO 类注入配置设置感兴趣。在这种情况下,我们使用Configure,如下所示:
services.Configure<LoggingSettings>(settings =>
{
settings.IncludeScopes = true;
settings.Default = LogLevel.Debug;
});
这里,我们使用的是Configure扩展方法,它允许我们为一个 POCO 类指定值,该类在运行时被请求时创建。我们可以要求配置对象执行此操作,而不是手动执行此操作,如下所示:
services.Configure<LoggingSettings>(settings =>
{
cfg.GetSection("Logging").Bind(settings);
});
更好的是,我们可以传递命名配置选项,如下所示:
services.Configure<LoggingSettings>("Elasticsearch", settings =>
{
this.Configuration.GetSection("Logging:Elasticsearch").Bind(settings);
});
services.Configure<LoggingSettings>("Console", settings =>
{
this.Configuration.GetSection("Logging:Console").Bind(settings);
});
稍后,我们将看到如何使用这些命名的配置选项。
我们甚至可以传入配置根本身,或配置根的一个子部分,这更简单,如以下代码段所示:
services.Configure<LoggingSettings>(cfg.GetSection("Logging"));
当然,我们也可以使用依赖注入框架注册我们的 POCO 类,如下所示:
var cfg = builder.Build();
var settings = builder.GetSection("Logging").Get<LoggingSettings>();
services.AddSingleton(settings);
如果我们使用Configure方法,那么依赖项注入框架中的配置实例将作为IOptions<T>的实例提供,其中T是传递给Configure类型的模板参数——根据本例,IOptions<LoggingSettings>。
IOptions<T>接口指定一个Value属性,通过该属性我们可以访问Configure中传递或设置的底层实例。好的方面是,只有在实际请求时,才会在运行时动态执行,这意味着除非我们明确需要,否则不会发生从配置到 POCO 类的绑定。
最后一点注意:在使用Configure之前,我们需要在services集合中添加对它的支持,如下所示:
services.AddOptions();
为此,首先需要添加Microsoft.Extensions.OptionsNuGet 包,这将确保正确注册所有必需的服务。
检索命名配置选项
当我们通过Configure方法家族注册 POCO 配置时,本质上我们是将其注册到依赖注入容器IOption<T>。这意味着无论何时我们想要注射它,我们都可以声明IOption<T>,比如IOption<LoggingSettings>。但是如果我们想使用命名的配置值,我们需要使用IOptionsSnapshot<T>。此接口公开了一个 niceGet方法,该方法将命名的配置设置作为其唯一参数,如下所示:
public HomeController(IOptionsSnapshot<LoggingSettings> settings)
{
var elasticsearchSettings = settings.Get("Elasticsearch");
var consoleSettings = settings.Get("Console");
}
您必须记住,我们通过调用Configure方法注册了LoggingSettings类,该方法使用 name 参数。
重新加载和处理更改通知
您可能还记得,当我们谈到基于文件的提供者时,我们提到了reloadOnChange参数。这将设置一个文件监视操作,当文件内容发生更改时,操作系统通过该操作通知.NET。即使我们不启用该功能,也可以要求提供商重新加载其配置。IConfigurationRoot接口为此目的公开了一个Reload方法,如以下代码片段所示:
var cfg = builder.Build();
cfg.Reload();
因此,如果我们显式地重新加载配置,我们很有信心,当我们请求配置密钥时,我们将得到更新的值,以防同时更改配置。但是,如果我们不这样做,我们已经看到的 API 就不能确保我们每次都得到更新版本。为此,我们可以执行以下任一操作:
- 注册更改通知回调,以便在基础文件内容更改时得到通知
- 注入数据的实时快照,其值在源更改时也会更改
对于第一个选项,我们需要获取重载令牌的句柄,然后在其中注册回调操作,如下所示:
var token = cfg.GetReloadToken();
token.RegisterChangeCallback(callback: (state) =>
{
//state will be someData
//push the changes to whoever needs it
}, state: "SomeData");
对于后一个选项,我们需要使用IOptionsSnapshot<T>,而不是注入IOptions<T>。只需更改这一点,我们就可以确保注入的值将来自当前最新的配置源,而不是创建配置对象时的配置源。请查看以下代码段以获取此示例:
public class HomeController : Controller
{
private readonly LoggingSettings _settings;
public HomeController(IOptionsSnapshot<LoggingSettings> settings)
{
_settings = settings.Value;
}
}
始终使用IOptionsSnapshot<T>而不是IOptions<T>是安全的,因为开销最小。
运行配置前和配置后操作
自 ASP.NET Core 2.0 以来,有一个新功能:为已配置的类型运行配置前和配置后操作。这意味着,在完成所有配置之后,在从依赖项注入中检索配置的类型之前,注册类的所有实例都有机会执行并修改配置。对于未命名配置选项和命名配置选项都是如此。
对于未命名的配置选项(Configure没有名称参数),有一个名为IConfigureOptions<T>的接口,如下面的代码片段所示:
public class PreConfigureLoggingSettings : IConfigureOptions<LoggingSettings>
{
public void Configure(LoggingSettings options)
{
//act upon the configured instance
}
}
对于命名的配置选项(Configure带 name 参数),我们有IConfigureNamedOptions<T>,如下面的代码片段所示:
public class PreConfigureNamedLoggingSettings : IConfigureNamedOptions<LoggingSettings>
{
public void Configure(string name, LoggingSettings options)
{
//act upon the configured instance
}
public void Configure(LoggingSettings options)
{
}
}
注册时,这些类将在委托传递给Configure方法之前激发。配置很简单,如以下代码段所示:
services.ConfigureOptions<PreConfigureLoggingSettings>();
services.ConfigureOptions<PreConfigureNamedLoggingSettings>();
但是还有更多:除了在配置委托之前运行操作之外,我们还可以在配置委托之后运行。输入IPostConfigureOptions<T>-这一次,命名和未命名配置选项的注册没有不同的接口,如以下代码段所示:
public class PostConfigureLoggingSettings : IPostConfigureOptions<LoggingSettings>
{
public void PostConfigure(string name, LoggingSettings options) { ... }
}
最后,这些类中的每一个都由依赖项注入容器实例化,这意味着我们可以使用构造函数注入!这就像一个符咒,可以在以下代码段中看到:
public PreConfigureLoggingSettings(IConfiguration configuration) { ... }
对于IConfigureOptions<T>、IConfigureNamedOptions<T>和IPostConfigureOptions<T>也是如此。
现在,让我们看看以前版本的一些更改。
对 2.x 版的更改
版本 2.0 的最大变化是,从 2.1 开始,配置是按照约定完成的,即添加appsettings.jsonJSON 文件的过程(每个环境的通用和可选)以及对用户隐藏的所有内容。
这在WebHost.CreateDefaultBuilder方法中定义。但是,您仍然可以构建自己的ConfigurationBuilder并添加任何您喜欢的内容。为此,您可以调用ConfigureAppConfiguration方法,如第 1 章、ASP.NET Core 入门所述,如下代码块所示:
Host
.CreateDefaultBuilder(args)
.ConfigureAppConfiguration(builder =>
{
var jsonSource = new JsonConfigurationSource { Path =
"appsettings.json" };
builder.Add(jsonSource);
})
.ConfigureWebHostDefaults(builder =>
{
builder.UseStartup<Startup>();
});
或者,如果您只想向默认构建的配置(或正在修改的配置)添加一个条目,则调用UseSettings扩展方法,如下所示:
Host
.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(builder =>
{
builder.UseSetting("key", "value");
builder.UseStartup<Startup>();
});
因此,当Startup类被实例化时,它将被传递一个IConfiguration对象,该对象是根据您在这里输入的代码构建的。
Warning: when using UseSetting, the value will be written to all registered configuration providers.
在了解了应用配置是如何完成的之后,让我们看看如何对主机执行同样的操作。
配置运行时主机
.NETCore3 引入了一种不太为人所知的配置机制,它仍然有一些用途:运行时主机配置。这里的想法是在.csproj文件中提供配置设置,作为键值对。您可以通过编程方式从AppContext类中检索它们。以下是一个示例项目文件:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<RuntimeHostConfigurationOption Include="Foo" Value="Bar" />
</ItemGroup>
</Project>
可以通过调用AppContext类的GetData方法来检索"Foo"设置,如下代码片段所示:
var bar = AppContext.GetData("Foo");
如果指定的条目不存在,GetData只返回null。请注意,GetData的原型是返回一个object,但在本例中,它将返回一个string。
通常情况下,您不希望这样做,但如果您希望创建或修改运行时主机配置设置的一个条目,您可以通过应用域执行此操作,如下所示:
AppDomain.CurrentDomain.SetData("Foo", "ReBar");
请注意,这并不能取代结构良好、定义正确的配置。NETCore 所做的是,在运行和部署时,它将RuntimeHostConfigurationOption部分(以及更多部分)的内容复制到生成的${project}.runtimeconfig.json文件中,该文件与生成的二进制文件放在一起。
我们现在将看到 ASP.NET Core 的一个新功能:功能切换。
理解特征切换
.NETCore3 引入了Microsoft.FeatureManagement.AspNetCore库,它非常方便进行功能切换。简而言之,功能要么启用,要么不启用,这是通过布尔开关通过配置(任何源)配置的。
对于更复杂的场景,您可以为特定功能定义一个可用的配置;在确定是否启用时,可以考虑这一点。
特征切换可通过应用具有任意数量特征名称的[FeatureGate]属性应用于动作方法,如下所示:
[FeatureGate("MyFeature1", "MyFeature2")]
public IActionResult FeactureEnabledAction() { ... }
当[FeatureGate]属性应用于某个操作方法且该功能被禁用时,任何试图访问它的尝试都将导致 HTTP 404 Not Found 结果。它可以采用任意数量的功能名称和可选需求类型,可以是All或Any,这意味着需要启用所有功能或至少启用一个功能。以下代码段对此进行了说明:
[FeatureGate(RequirementType.All, "MyFeature1", "MyFeature2")]
public IActionResult FeactureEnabledAction() { ... }
或者,可以通过注入的IFeatureManager实例明确地请求,如下所示:
public HomeController(IFeatureManager featureManager)
{
_featureManager = featureManager;
}
public async Task<IActionResult> Index()
{
var isEnabled = await _featureManager.IsEnabledAsync("MyFeature");
}
当然,你可以在任何地方注射IFeatureManager。以下代码段中可以看到这方面的示例:
@inject IFeatureManager FeatureManager
@if (await FeatureManager.IsEnabledAsync("MyFeature")) {
<p>MyFeature is enabled!</p>
}
但在视图上,另一个选项是使用<feature>标记辅助对象,如下所示:
<feature name="MyFeature">
<p>MyFeature is enabled!</p>
</feature>
与[FeatureGate]属性类似,您可以在name属性中指定多个特征名称,也可以在requirement中指定Any或All中的一个。您也可以negate该值,如下所示:
<feature name="MyFeature">
<p>MyFeature is enabled!</p>
</feature>
<feature name="MyFeature" negate="true">
<p>MyFeature is disabled!</p>
</feature>
正如您所看到的,这非常有用,因为您可以在功能启用和未启用时提供内容。
标签助手需要注册这通常发生在_ViewImports.cshtml文件上,如下所示:
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
至少,对于名为MyFeature的功能,我们需要在appsettings.json文件上进行以下配置:
{
"FeatureManagement": {
"MyFeature": true
}
}
默认值始终为false,表示该功能已禁用。运行时对配置文件所做的任何更改都会被功能管理库检测到。
ConfigureServices方法中的设置非常简单,只需调用AddFeatureManagement扩展方法即可。这是注册IFeatureManager接口的内容(加上我们稍后将看到的其他几个),如下所示:
services
.AddFeatureManagement()
.AddFeatureFilter<MyFeatureFilter>();
还有另一个重载AddFeatureManagement将IConfiguration对象作为参数,如果您希望构建自己的对象。接下来,您需要注册您想要使用的尽可能多的功能过滤器,并连续调用AddFeatureFilter。
包含的功能过滤器
功能过滤器包包括以下过滤器:
PercentageFilter:允许一定百分比的项目通过。TimeWindowFilter:功能仅在定义的日期和时间窗口内启用。
这些过滤器中的每一个都有自己的配置模式,让我们看看每一个。
百分比过滤器
百分比过滤器将我们感兴趣的百分比作为其唯一参数。每次调用它时,它将返回大约该百分比的启用次数。appsettings.json文件中的配置如下:
"FeatureManagement": {
"HalfTime": {
"EnabledFor": [
{
"Name": "Microsoft.Percentage",
"Parameters": {
"Value": 50
}
}
]
}
}
您可以看到,在本例中,您声明了特征门的名称"HalfTime"和百分比参数—50。
您还可以声明该属性,如下所示:
[FeatureGate("HalfTime")]
public IActionResult Action() { ... }
时间窗滤波器
这一功能允许在特定日期和时间到来时自动提供功能。圣诞节的配置如下所示:
"FeatureManagement": {
"Christmas": {
"EnabledFor": [
{
"Name": "Microsoft.TimeWindow",
"Parameters": {
"Start": "25 Dec 2019 00:00:00 +00:00",
"End": "26 Dec 2019 00:00:00 +00:00"
}
}
]
}
}
请注意,日期和时间的格式与文化无关。您需要声明开始和结束时间,以及特征门的名称:"Christmas"。
功能门声明如以下代码段所示:
[FeatureGate("Christmas")]
public IActionResult Action() { ... }
自定义功能过滤器
构建一个简单的特征过滤器很简单,只需实现IFeatureFilter,它只有一个方法,如下所示:
[FilterAlias("MyFeature")]
public class MyFeatureFilter : IFeatureFilter
{
public bool Evaluate(FeatureFilterEvaluationContext context)
{
//return true or false
}
}
然后在ConfigureServices上注册,如下:
services
.AddFeatureManagement()
.AddFeatureFilter<MyFeatureFilter>();
FeatureFilterEvaluationContext类只提供两个属性,如下所示:
FeatureName(string:当前特征的名称Parameters(IConfiguration:用于向功能过滤器提供信息的配置对象
但是,我们可以利用.NET Core 的内置依赖注入机制,将其注入到我们的功能过滤器中,例如IHttpContextAccessor,从中我们可以访问当前 HTTP 上下文,并从它访问您需要的几乎任何内容。这可以通过以下方式实现:
private readonly HttpContext _httpContext;
public MyFeatureFilter(IHttpContextAccessor httpContextAccessor)
{
this._httpContext = httpContextAccessor.HttpContext;
}
您也不限于配置中的“是/否”值,您可以拥有丰富的配置设置。例如,让我们看看如何在配置文件中拥有我们自己的模型——尽管为了简单起见,我们将使其成为一个简单的模型。想象一下下面的简单类:
public class MySettings
{
public string A { get; set; }
public int B { get; set; }
}
我们希望将此类保存在配置文件中,如下所示:
{
"FeatureManagement": {
"MyFeature": {
"EnabledFor": [
{
"Name": "MyFeature",
"Parameters": {
"A": "AAAAA",
"B": 10
}
}
]
}
}
此配置可以从Evaluate方法内的自定义功能读取,如下所示:
var settings = context.Parameters.Get<MySettings>();
MySettings类将从配置设置中自动反序列化,并可供.NET 类使用。
检查之间的一致性
您可能会注意到,对于某些功能(如百分比功能),如果在同一请求期间调用它两次,可能会得到不同的值,如以下代码段所示:
var isEnabled1 = await _featureManager.IsEnabledAsync("HalfTime");
var isEnabled2 = await _featureManager.IsEnabledAsync("Halftime");
通常,当您的功能执行复杂的计算或一些随机操作,并且希望在请求期间获得一致的结果时,您希望避免这种情况。在这种情况下,您希望使用IFeatureManagerSnapshot而不是IFeatureManager。IFeatureManagerSnapshot继承自IFeatureManager,但其实现将结果缓存在请求中,这意味着您总是得到相同的结果。而且IFeatureManagerSnapshot也注册在依赖注入框架上,所以您可以在任何时候使用IFeatureManager来使用它。
禁用的功能处理程序
当您尝试访问一个动作方法时,该动作方法被修饰为一个功能门,该功能门的目标是一个(或多个)被禁用的功能,那么该动作方法是不可访问的,默认情况下,我们将得到一个 HTTP 403 禁止错误。但是,这可以通过应用自定义禁用的功能处理程序来更改。
禁用的功能处理程序是实现IDisabledFeaturesHandler的具体类,例如:
public sealed class RedirectDisabledFeatureHandler : IDisabledFeaturesHandler
{
public RedirectDisabledFeatureHandler(string url)
{
this.Url = url;
}
public string Url { get; }
public Task HandleDisabledFeatures(IEnumerable<string> features,
ActionExecutingContext context)
{
context.Result = new RedirectResult(this.Url);
return Task.CompletedTask;
}
}
此类重定向到作为参数传递的统一资源定位器(URL)。您通过呼叫UseDisabledFeaturesHandler进行注册,如下所示:
services
.AddFeatureManagement()
.AddFeatureFilter<MyFeatureFilter>()
.UseDisabledFeaturesHandler(new
RedirectDisabledFeatureHandler("/Home/FeatureDisabled"));
您只能注册一个处理程序,仅此而已。每当我们试图访问一个动作方法,其中定义了一个计算结果为false的特征门时,就会调用它,最明显的响应是重定向到某个页面,正如我给出的示例中所示。
在本节中,我们了解了 ASP.NET Core 的一个新特性:特性切换。这是一个简化版本的配置,更适合于开/关开关,并具有一些相关的良好功能。愿你觉得它有用!
总结
因为 JSON 是当今的标准,所以我们应该坚持使用 JSON 提供程序,并支持在发生更改时重新加载配置。我们应该首先添加公共文件,然后为每个不同的环境添加可选覆盖(注意添加每个源的顺序)。我们了解了 ASP.NET Core 的默认配置如何加载 JSON 文件,包括不同环境下的不同文件。
然后,我们了解了如何使用配置部分来更好地组织设置,并且还了解了如何为它们使用 POCO 包装器。
因此,这让我们思考是应该使用IOptions<T>还是我们自己的 POCO 类来注入配置值。嗯,如果您不想引用.NET Core 配置包污染您的类或程序集,那么您应该坚持使用 POCO 类。我们对此不太担心,因此建议保留接口包装器。
我们将使用IOptionsSnapshot<T>而不是IOptions<T>,以便始终获得最新版本的配置设置。
在此之后,我们研究了功能切换,以快速启用或禁用刚刚打开或关闭的功能。
在本章中,我们了解了为 ASP.NET Core 应用提供配置的多种方法。我们学习了如何构建从 Windows 注册表获取配置的简单提供程序。然后,我们讨论了使用内置依赖项注入框架注入配置设置的多种方法,以及如何在配置源中收到更改通知。
问题
阅读本章后,您现在应该能够回答以下问题:
- 用于检索配置值的根接口是什么?
- .NET Core 中的内置基于文件的配置提供程序是什么?
- 是否可以直接将配置绑定到 POCO 类?
IOptions<T>和IOptionsSnapshot<T>接口之间有什么区别?- 我们是否需要在依赖项注入容器中显式注册配置对象?
- 我们如何拥有可选的配置文件?
- 是否可以在配置更改时获取通知?
三、路由
本章讨论路由,即 ASP.NET Core 将用户请求转换为 MVC 控制器和操作的过程。这可能是一个复杂的过程,因为请求中的细微更改可能导致调用不同的端点(控制器/操作对)。需要考虑几个方面:协议(HTTP 或 HTTPS)、发出请求的用户是否经过身份验证、HTTP 谓词、请求路径、查询字符串以及路径和查询字符串参数值的实际类型。
路由还定义了当一条路由不匹配时会发生什么情况,即 catch-all 路由,它可以用于需要定义自定义路由约束的复杂情况。
ASP.NET Core 提供了配置路由的不同方法,可分为基于约定的配置和显式配置。
在本章结束时,您将能够定义路由表并以 ASP.NET Core 为 MVC 应用提供的所有不同方式应用路由配置。
本章的目标如下所示:
-
理解端点路由
-
配置路由
-
理解路由表
-
使用管线模板
-
匹配路由参数
-
使用动态路由
-
基于属性的学习路径选择
-
从属性强制主机选择
-
设置路由默认值
-
路由到内联处理程序
-
应用路线约束
-
使用路由数据令牌
-
路由到区域
-
使用属性进行路由
-
使用路由进行错误处理
技术要求
为了实现本章中介绍的示例,您需要.NET Core 3 SDK 和文本编辑器。当然,VisualStudio2019(任何版本)满足所有要求,但您也可以使用 VisualStudio 代码。
源代码可以在这里从 GitHub 检索:https://github.com/PacktPublishing/Modern-Web-Development-with-ASP.NET-Core-3-Second-Edition
开始
在旧的 web 应用时代,事情很简单,如果你想要一个页面,你必须有一个物理页面。然而,事情已经发生了变化,ASP.NET Core 现在是一个 MVC 框架。这是什么意思?嗯,在 MVC 中,没有物理页面(尽管这并不完全正确);相反,它使用路由将请求定向到路由****处理程序。MVC 中最常见的路由处理程序是控制器动作。在本章之后,您将学习如何使用路由访问控制器操作。
请求只是一些相对 URL,例如:
/Search/Mastering%ASP.NET%Core
/Admin/Books
/Book/1
这会产生更可读的 URL,并且对谷歌等搜索引擎也有好处。为搜索引擎优化网站(包括其公共 URL)的主题称为搜索引擎优化(SEO。
当 ASP.NET Core 收到请求时,可能会发生以下两种情况之一:
- 存在与请求匹配的物理文件。
- 有一个接受请求的路由。
为了让 ASP.NET Core 服务物理文件,需要对此进行配置,我们使用Configure中的UseStaticFiles扩展方法,将静态文件、处理中间件添加到管道中;对UseStaticFiles的调用包含在 ASP.NET Core web 应用的 Visual Studio 模板中。如果我们不启用静态文件服务,或者如果不存在文件,则需要由路由处理程序处理请求。MVC 中最常见的路由处理程序是控制器****动作。
控制器是一个公开动作的类,该动作知道如何处理请求。动作是一种可以获取参数并返回动作结果的方法。我们使用路由表将请求引导至控制器****动作。
我们可以使用两个 API 来注册路由:
- Fluent API(代码)
- 属性
在以前的版本中,我们必须显式地添加对路由属性的支持,但它们现在是.NETCore 的一流公民。让我们从路由表的概念开始,来看看它们。
端点路由
端点路由是在 ASP.NET Core 2.2 中引入的,从 3.0 开始现在是默认机制。其主要优点是,它支持许多不同的机制,尽管这些机制利用了路由和中间件,但它们与 MVC、Razor Pages、gRPC、Blazor、SignalR 等机制截然不同。您仍然可以在ConfigureServices中注册所需的服务,然后使用Configure方法中的扩展方法将中间件添加到管道中。端点路由使框架更加灵活,因为它将路由匹配和解析与端点调度分离,而端点调度过去是 MVC 功能的一部分。
有三个必需的方法调用:
AddRouting:我们注册所需的服务,并可选择配置其部分选项(ConfigureServices)UseRouting:实际添加路由中间件的地方(Configure;这会将请求匹配到端点UseEndpoints:我们将端点配置为可用(Configure;这将执行匹配的端点
现在,在 Razor 视图(或页面)上,如果您想动态生成指向可寻址资源的超链接,不管它是什么(操作方法、Razor 页面或其他任何内容),您可以使用Url.RouteUrl重载方法:
<!-- a Razor page -->
<a href="@Url.RouteUrl(new { page = "Admin" })">Admin</a>
<!-- an action method on a controller -->
<a href="@Url.RouteUrl(new { action = "Contact", controller = "Home" })">Contact</a>
如果出于任何原因,您需要在中间件组件上生成链接,那么您可以注入一个LinkGenerator类。它公开了允许您检索多种不同类型 URL 信息的离散方法:
Get{Path,Uri}ByAction:返回控制器动作方法的完整路径(URL)Get{Path,Uri}ByAddress:返回基本路径的完整路径(URL)和指定的路由值Get{Path,Uri}ByName:返回端点名称和指定路由值的完整路径(URL)Get{Path,Uri}ByPage:返回页面名称的完整路径(URL)Get{Path,Uri}ByRouteValues:返回指定端点路由的完整路径(URL)和路由值
*Path版本与*Uri版本的区别在于前者返回绝对路径(如/controller/action),后者返回协议限定的完整路径(如http://host:8080/controller/action)。
如果您需要获取当前端点,有一个新的扩展方法,HttpContext之上的GetEndpoint,您可以使用它:
var endpoint = this.HttpContext.GetEndpoint();
var displayName = endpoint.DisplayName;
var metadata = endpoint.Metadata.ToArray();
除了DisplayName和Metadata集合之外,端点没有提供太多的功能。DisplayName是操作方法的完全限定名,包括类和程序集,除非设置了显示名,Metadata集合包含应用于当前操作方法的所有元数据,包括属性和约定。
您可以使用Get<T>通用方法请求特定的元数据接口;元数据特定接口如下所示:
IDataTokensMetadata:这用于获取数据令牌的访问权限(有关更多信息,请参阅下一节)。IEndpointNameMetadata:用于获取可选的端点名称。IHostMetadata:用于获取端点的主机限制。IHttpMethodMetadata:用于获取端点的方法限制。IRouteNameMetadata:获取定义路由表时指定的路由名称。ISuppressLinkGenerationMetadata:如果该接口的SuppressLinkGeneration属性设置为true,则在使用LinkGenerator类生成链接时不考虑该端点。ISuppressMatchingMetadata:如果该接口的SuppressMatching属性为true,则不考虑该端点的 URL 进行 URL 匹配。
例如,假设我们要获取当前路由名称:
var routeName = HttpContext.GetEndpoint().Metadata.Get<IRouteNameMetadata>();
Keep in mind that Get<> returns the first occurrence of any registered metadata that implements the passed type.
我们可以添加自定义元数据,并在构建时在端点上设置显示名称,如下所示:
app.UseEndpoints(endpoints =>
{
endpoints
.MapControllerRoute
(
name: "Default",
pattern: "{controller=Home}/{action=Index}/{id?}",
)
.WithDisplayName("Foo")
.WithMetadata(new MyMetadata1(), new MyMetadata2());
});
此示例显示了一个典型的控制器路由,该路由具有显示名称集(WithDisplayName)和自定义元数据(MyMetadata1和MyMetadata2);这些类仅用于演示目的。
在了解了端点路由的工作原理之后,现在让我们看看如何配置路由表。
路由配置
我们可以为路由生成配置几个选项,所有这些选项都是通过AddRouting扩展方法在服务定义上配置的:
services.AddRouting(options =>
{
options.LowercaseUrls = true;
options.AppendTrailingSlash = true;
options.ConstraintMap.Add("evenint", typeof(EvenIntRouteConstraint));
});
RouteOptions类支持以下属性:
AppendTrailingSlash:确定是否应在所有生成的 URL 中追加尾随斜杠(/);默认值为false(表示不应该)LowercaseUrls:确定生成的 URL 是否为小写;默认为falseConstraintMap:确定约束映射的位置;当我们讨论路由约束时,将对此进行更多介绍
但是路由配置并没有到此结束,下一节实际上是最重要的一节:创建路由表。
创建路由表
在第一章中,从 ASP.NET Core开始,我们讨论了 OWIN 管道,解释了我们使用中间件构建这个管道。事实证明,有一个 MVC 中间件负责解释请求并将其转换为控制器操作。为此,我们需要一个路由表。
只有一个路由表,如本例中默认 Visual Studio 模板所示:
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
我们在这里看到了什么?IApplicationBuilder的UseEndpoints扩展方法有一个参数是IEndpointRouteBuilder的一个实例,我们可以在其中添加路由。路线基本上包括以下组成部分:
- 名称(
default) - 模板模式(
{controller=Home}/{action=Index}/{id?}) - 每个路由参数的可选默认值(
Home、Index)
此外,我们还有一些默认设置:
- 如果没有为 URL 提供控制器,则使用
Home作为默认值。 - 如果没有提供任何操作,对于任何控制器,则使用
Index作为默认值。
本例中未显示一些可选参数:
- 可选路由参数约束
- 可选数据令牌
- 路径处理程序
- 路由约束解析器
我们将在本章中介绍所有这些内容。这是默认的 MVC 模板,此调用与以下调用相同:
endpoints.MapDefaultControllerRoute();
至于实际的路线,这个名字只是对我们有意义的东西,它没有以任何方式被使用。更有趣的是模板,稍后我们将看到它。
作为记录,如果希望仅映射控制器,则应包括以下调用:
endpoints.MapControllers();
这不包括对 Razor 页面的支持;为此,您需要:
endpoints.MapRazorPages();
话虽如此,我们可以定义多条路线:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute(
name: "admin",
pattern: "admin/{controller}/{action=Index}");
});
在本例中,我们有两个路由:第二个路由映射一个以admin开头的请求,它需要一个显式的控制器名称,因为它没有默认名称。该操作确实有一个(Index)。
Routes are searched in order; the first one that matches the request is used.
这里我们看到了如何将请求映射到确实存在的资源。下一节解释了当请求的资源不存在时该怎么做!
回退端点
要定义回退路由(如果没有其他路由匹配,则匹配的路由),我们可以回退到页面(任何相对 URL),包括或不包括区域:
endpoints.MapFallbackToPage("/Admin");
endpoints.MapFallbackToAreaPage("/", "Admin");
或者,我们可以有一个包含以下文件的回退页面:
endpoints.MapFallbackToFile("index.html");
我们可以有控制器动作,带或不带区域:
endpoints.MapFallbackToController("Index", "Home");
endpoints.MapFallbackToAreaController("Index", "Home", "Admin");
或者,最后,我们可以有一个委托,它接收作为其唯一参数的请求上下文(HttpContext,您可以从中做出决定:
endpoints.MapFallback(ctx =>
{
ctx.Response.Redirect("/Login");
return Task.CompletedTask;
});
这些MapFallback*扩展方法中的每一个都有一个重载,其第一个参数类型为string,称为pattern。如果使用此重载,pattern参数可用于将回退限制为与此模式匹配的请求。例如,请参见:
endpoints.MapFallbackToPage("/spa/{**path:nonfile}", "/Missing");
回退路由应该是端点路由表上的最后一个条目。
现在让我们看看如何通过在路由模板中使用特殊令牌来增强路由。
使用管线模板
模板是相对 URL,因此不能以斜杠(/开头)。在它中,您定义了站点的结构,或者更准确地说,定义了您打算提供的结构。由于 ASP.NET Core 是一个 MVC 框架,模板应该描述如何将请求映射到控制器中的操作方法。以下是模板:
{controller=Home}/{action=Index}/{id?}
它由斜线分隔的部分组成,每个部分都有一些标记(在花括号内)。
另一个例子是:
sample/page
这里不清楚我们想要什么,因为没有提到controller或action。但是,这是一个完全有效的模板,所需的信息需要来自其他地方。
模板可以包含以下元素:
- 字母数字文字
- 大括号(
{}中的字符串片段,它们是命名的标记,可以映射到操作方法参数 - 如果 URL 中未提供令牌,则具有相等分配的命名令牌(
=)具有默认值;让一个带有默认值的令牌后跟一个不带默认值的必需令牌是没有意义的 - 以问号(
?结尾的标记,可选,表示不需要;可选标记后面不能跟有必需的标记 - 以星星(
*开头的代币,完全是可选的,可以匹配任何东西;它们必须是模板中的最后一个元素
令牌始终是字母数字字符段,可以用分隔符符号(/、?、-、(、)等)分隔。但是,您不需要使用分隔符;请注意,action和id标记之间没有斜杠,以下内容完全有效:
{controller=Admin}/{action=Process}{id}
下面是另一个稍微复杂一点的示例,其中包括添加一个 catch-all 标记querystring:
{controller=Admin}/{action=Process}/{?id}?{*querystring}
此模板将与以下 URL 匹配:
| URL | 参数 |
| / | 控制器:Admin行动:Process编号:不适用查询字符串:不适用 |
| /Account | 控制器:Account行动:Process编号:不适用查询字符串:不适用 |
| /Admin/Process | 控制器:Admin行动:Process编号:不适用查询字符串:不适用 |
| /Admin/Process/1212 | 控制器:Admin行动:Processid:1212 |
| /Admin/Process/1212?force=true | 控制器:Admin行动:Processid:1212查询字符串:force=true |
另一个非常有效的例子是:
api/{controller=Search}/{action=Query}?term={term}
这将符合以下条件:
api?term=.net+core
api/Search?term=java
api/Search/Query?term=php
请注意,无论大小写如何,任何文本都必须以与 URL 中所示完全相同的方式呈现。
现在,让我们看看模板中指定的路由参数是如何匹配的。
匹配路由参数
记住模板需要有一个controller令牌和一个action令牌;这些是唯一必需的标记,具有特殊意义。控制器将匹配控制器类,操作将匹配其公共方法之一。任何其他模板参数都将与 action 方法中同名的参数匹配。例如,采用具有以下模板的路线:
{controller=Search}/{action=Query}/{phrase}
该路径将映射到名为SearchController的类中的Query方法:
public IActionResult Query(string phrase) { ... }
By convention, the name of the controller in a template does not take the Controller suffix.
如果路由令牌是可选的,则它必须映射到具有默认值的参数:
{controller=Account}/{action=List}/{page?}
匹配方法将具有以下签名:
public IActionResult List(int page = 0)
请注意,page参数是一个int实例,其默认值为0。例如,这可以用于分页,其中默认页面是第一个页面(从零开始)。这与拥有默认值为0的令牌并将其映射到没有默认值的参数相同。
到目前为止,我们只看到了如何映射字符串或基本类型的简单值;我们将很快看到如何使用其他类型。
我们已经提到,action参数是必需的,但是,尽管在某种程度上这是正确的,它的值可能会被跳过。在这种情况下,ASP.NET Core 将使用 HTTP 操作头中的值,例如GET、POST、PUT、DELETE等等。这在 web API 的情况下特别有用,并且通常非常直观。因此,例如,采用具有以下模板的路线:
api/{controller}/{id}
假设其有以下要求:
GET /api/Values/12
它可以映射到一个名为ValuesController的控制器中的如下方法:
public IActionResult Get(int id) { ... }
所以,我们刚刚了解了如何将模板参数从模板匹配到控制器类的方法。现在我们将学习动态路由,其中映射不是预定义的。
使用动态路由
到目前为止,我们已经看到了静态地将路由模板映射到控制器操作的路由表,但还有另一种:动态路由。在这种情况下,我们仍然使用路由模板,但问题是,我们可以动态更改它们。
通过调用MapDynamicControllerRoute注册动态路由处理程序。我将提供一个示例,使用翻译服务将用户提供的控制器和操作名称(以任何语言)翻译成项目中存在的纯英语。
让我们从头开始。我们为翻译服务定义了接口:
public interface ITranslator
{
Task<string> Translate(string sourceLanguage, string term);
}
如您所见,这有一个异步方法Translate,它接受两个参数:源语言和要翻译的术语。我们不要浪费太多时间在这上面。
核心动态路由功能作为继承自DynamicRouteValueTransformer的类实现。下面是一个此类的示例,并对其进行了说明:
public sealed class TranslateRouteValueTransformer : DynamicRouteValueTransformer
{
private const string _languageKey = "language";
private const string _actionKey = "action";
private const string _controllerKey = "controller";
private readonly ITranslator _translator;
public TranslateRouteValueTransformer(ITranslator translator)
{
this._translator = translator;
}
public override async ValueTask<RouteValueDictionary> TransformAsync(
HttpContext httpContext, RouteValueDictionary values)
{
var language = values[_languageKey] as string;
var controller = values[_controllerKey] as string;
var action = values[_actionKey] as string;
controller = await this._translator.Translate(
language, controller) ?? controller;
action = await this._translator.Translate(language, action)
?? action;
values[_controllerKey] = controller;
values[_actionKey] = action;
return values;
}
}
TranslateRouteValueTransformer类在其构造函数上接收ITranslator的实例,并将其保存为本地字段。在TransformAsync方法中,检索路由模板值、language、controller和action的值;对于controller 和action,将其翻译为ITranslator。结果值将再次存储在 route values dictionary 中,并在最后返回。
要使这项工作发挥作用,我们需要三件事:
- 我们需要在
ConfigureServices中将ITranslator注册为服务:
services.AddSingleton<ITranslator, MyTranslator>();
//MyTranslator is just for demo purposes, you need to roll out your own dictionary implementation
- 我们也需要将
TranslateRouteValueTransformer注册为一项服务:
services.AddSingleton<TranslateRouteValueTransformer>();
- 最后,我们需要注册一个动态路由:
app.UseEndpoints(endpoints =>
{
endpoints.MapDynamicControllerRoute<TranslateRouteValueTransformer>(
pattern: "{language}/{controller}/{action}/{id?}");
//now adding the default route
endpoints.MapDefaultControllerRoute();
});
如您所见,我们的动态路线寻找一个模式language/controller/action/id,其中id部分是可选的。任何可以映射到此模式的请求都将落入此动态路由。
请记住,动态路由的目的不是更改路由模式,而是更改路由模板令牌。这不会导致任何重定向,但实际上会确定如何处理请求、操作方法和控制器以及任何其他路由参数。
为了结束本节,本例允许解析这些路线,前提是词典支持法语(fr、德语(de)和葡萄牙语(pt):
/fr/Maison/Index至/Home/Index/pt/Casa/Indice至/Home/Index/de/Zuhause/Index至/Home/Index
You can have multiple dynamic routes with different patterns; this is perfectly OK.
了解了动态路由之后,让我们回到静态路由,这次使用类和方法中的属性来定义路由。
从属性中选择路由
ASP.NET Core,或者更确切地说,路由中间件,将获取请求 URL 并检查它知道的所有路由,以查看是否有与请求匹配的路由。它将在遵守路由插入顺序的情况下执行此操作,因此请注意,您的请求可能会意外地落入您预期的路由之外。总是先添加最具体的,然后再添加通用的。
在找到与请求匹配的模板后,ASP.NET Core 将检查目标控制器上是否存在一个可用的操作方法,该方法没有禁止将方法用作操作的NonActionAttribute实例,或者具有从HttpMethodAttribute继承的属性,该属性与当前 HTTP 动词相匹配。以下列出了:
HttpGetAttributeHttpPostAttributeHttpPutAttributeHttpDeleteAttributeHttpOptionsAttributeHttpPatchAttributeHttpHeadAttribute
它们都继承自HttpMethodAttribute:这是用于基于 HTTP 谓词进行过滤的根类。
如果找到其中任何一个,则仅当 HTTP 谓词与指定的谓词之一匹配时,才会选择路由。可以有许多属性,这意味着可以使用指定的任何 HTTP 谓词调用 action 方法。
There are other HTTP verbs, but ASP.NET Core only supports these out of the box. If you wish to support others, you need to subclass HttpMethodAttribute and supply your list or use ActionVerbsAttribute. Interestingly, ASP.NET Core—as before in the ASP.NET web API—offers an alternative way of locating an action method: if the action token is not supplied, it will look for an action method whose name matches the current HTTP verb, regardless of the casing.
您可以使用这些属性来提供不同的操作名称,这允许您使用方法重载。例如,如果有两个同名的方法采用不同的参数,则区分它们的唯一方法是使用不同的操作名称:
public class CalculatorController
{
//Calculator/CalculateDirectly
[HttpGet(Name = "CalculateDirectly")]
public IActionResult Calculate(int a, int b) { ... }
//Calculator/CalculateByKey
[HttpGet(Name = "CalculateById")]
public IActionResult Calculate(Guid calculationId) { ... }
}
如果不可能,则可以使用不同的目标 HTTP 谓词:
//GET Calculator/Calculate
[HttpGet]
public IActionResult Calculate(int a, int b) { ... }
//POST Calculator/Calculate
[HttpPost]
public IActionResult Calculate([FromBody] Calculation calculation) { ... }
当然,您可以限制一个操作方法或整个控制器,以便只有在使用AuthorizeAttribute对请求进行身份验证的情况下才能访问它。我们在这里不再赘述,这将在第 11 章、安全中讨论。
但值得注意的是,即使整个控制器上都标有AuthorizeAttribute,如果带有AllowAnonymousAttribute:
[Authorize]
public class PrivateController
{
[AllowAnonymous]
public IActionResult Backdoor() { ... }
}
另一个选项是根据请求的内容类型约束操作。您使用ConsumesAttribute用于此目的,您可以按如下方式应用它:
[HttpPost]
[Consumes("application/json")]
public IActionResult Process(string payload) { ... }
For an explanation of what content types are, please see https://www.w3.org/Protocols/rfc1341/4_Content-Type.html.
另一个有助于路由选择的属性是RequireHttpsAttribute。如果请求存在于方法或控制器类中,则只有通过 HTTPS 发出的请求才会被接受。
最后,还有路由约束。它们通常用于验证请求中传递的令牌,但也可以用于验证整个请求。我们将很快讨论这些问题。
因此,顺序如下:
- 查找与请求匹配的第一个模板。
- 检查是否存在有效的控制器。
- 通过操作名称或动词匹配,检查控制器中是否存在有效的操作方法。
- 检查存在的任何约束是否有效。
- 检查所有有助于路由选择的属性(
AuthorizeAttribute、NonActionAttribute、ConsumesAttribute、ActionVerbsAttribute、RequireHttpsAttribute和HttpMethodAttribute均有效。
我们将很快看到约束如何影响路线选择。
使用特殊路线
以下路由是特殊的,因为它们对 ASP.NET Core 具有特殊意义:
[HttpGet("")]:这是控制器的默认动作;只能定义一个。如果应用于没有所需参数的方法,它将是整个应用的默认操作。[HttpGet("~/")]:这是应用对默认控制器的默认操作:它映射到应用的根目录(例如,**/**。
因此,如果您在控制器的动作方法上设置[HttpGet("")]并且没有定义任何其他路由,那么它将是该控制器的默认动作,如果您在没有路由表的情况下设置[HttpGet("~/")],那么它将是默认动作和默认控制器。
下一节将解释如何基于调用主机和/或服务器端口限制路由。
从属性中选择主机
从 ASP.NET 3 开始,还可以基于主机头和端口限制路由。您可以通过属性或使用 fluent(基于代码)配置来实现这一点。
以下是使用属性的示例:
[Host("localhost", "127.0.0.1")]
public IActionResult Local() { ... }
[Host("localhost:80")]
public IActionResult LocalPort80() { ... }
[Host(":8080")]
public IActionResult Port8080() { ... }
这里有三个使用[Host]属性的示例:
- 第一种方法仅当本地头为
localhost或127.0.0.1时,才使Local动作方法可达;可以提供任意数量的主机头。 - 第二个示例需要主机头和端口的组合,在本例中为
80。 - 最后一个只需要端口
8080。
当然,[Host]属性可以与任何[Http*]或[Route]属性组合。
下面是如何通过代码执行此操作:
endpoints.MapControllerRoute("Local", "Home/Local").RequireHost("localhost", "127.0.0.1");
本例仅接受来自"localhost"或"127.0.0.1"(通常为同义词)的给定路由请求。
现在,下一个主题将是如何为管线模板参数指定默认值。
设置路由默认值
我们已经了解了如何在模板中为路由参数指定默认值,但还有另一种方法:通过重载接受包含默认值的对象的MapControllerRoute扩展方法。您可以使用以下内容,而不是将这些默认值作为字符串提供:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" });
});
即使路由中没有令牌,这也是有效的,如下所示:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "My/Route",
defaults: new { controller = "My", action = "Route" });
});
请记住,您必须提供controller和action;如果它们不在模板中,则需要作为默认值提供。
下一节将深入研究路由的内部工作机制,以及我们如何处理请求。
路由到内联处理程序
在 ASP.NET Core 中,可以直接处理请求,即不路由到控制器操作。我们使用扩展方法定义内联处理程序,该扩展方法指定要匹配的 HTTP 谓词和模板,如下所示:
MapGet:HTTPGetMapPost:HTTPPostMapPut:HTTPPutMapDelete:HTTPDeleteMapVerb:任何命名的 HTTP 动词;例如,Get与使用MapGet相同
实际上有两种扩展方法,MapXXX和MapXXXMiddleware,第一种采用委托,第二种采用中间件类。下面是一个例子。
这些方法提供了两种可能的签名(除了采用 HTTP 动词的Map<*verb*>之外),并采用以下参数:
pattern:这是一个路由模板。requestHandler:此处理程序接受当前上下文(HttpContext并返回任务。
这里有两个例子。在第一种情况下,我们只是设置响应内容类型并向输出中写入一些文本:
endpoints.MapGet(
pattern: "DirectRoute",
requestDelegate: async ctx =>
{
ctx.Response.ContentType = "text/plain";
await ctx.Response.WriteAsync("Here's your response!");
});
在这里,我们将向响应添加一个中间件:
var newAppBuilder = endpoints.CreateApplicationBuilder();
newAppBuilder.UseMiddleware<ResponseMiddleware>();
endpoints.MapGet(
pattern: "DirectMiddlewareRoute", newAppBuilder.Build());
ResponseMiddleware可能是这样的:
public class ResponseMiddleware
{
private readonly RequestDelegate _next;
public ResponseMiddleware(RequestDelegate next)
{
this._next = next;
}
public async Task InvokeAsync(HttpContext ctx)
{
await ctx.Response.WriteAsync("Hello, from a middleware!");
}
}
When using MapMiddlewareXXX, you can't return the next delegate, as it is meant to be the only response.
这两种方法(使用处理程序或应用生成器)是相似的,因为前者让我们直接访问请求上下文,而后者允许我们为特定路由模板向请求管道添加步骤。这完全取决于你想做什么。
You cannot mix direct handlers with controllers: the first handler that is picked up in the routing table will be processed, and no other. So, for example, if you have MapGet followed by MapControllerRoute for the same template, the handler or action specified in MapGet will be processed, but not the controller in MapControllerRoute.
现在我们了解了如何处理路由请求,接下来我们将学习如何约束路由的适用性。
应用路线约束
当我们定义一个路由模板或模式时,我们可能还想指定该路由应该如何匹配,这是对的约束。我们可以通过多种方式约束路线,例如:
- 请求需要匹配给定的 HTTP 方法。
- 请求需要与给定的内容类型匹配。
- 其参数需要与某些规则匹配。
约束可以在路由模板中表示,也可以使用MapControllerRoute方法表示为离散对象。如果选择使用路由模板,则需要在其应用的令牌旁边指定其名称:
{controller=Home}/{action=Index}/{id:int}
注意{id:int}:这将id参数约束为整数,这是我们稍后将讨论的提供的约束之一。另一个选项是使用defaults参数:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" },
constraints: new { id = new IntRouteConstraint() });
});
您应该能够猜测在constraints参数中传递的匿名类必须具有与路由参数匹配的属性。
根据本例,您还可以传递未绑定到任何管线参数的约束,而是执行某种定制验证,如下所示:
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" },
constraints: new { foo = new BarRouteConstraint() });
在这种情况下,BarRouteConstraint约束类仍将被调用,并可用于使路由选择无效。
HTTP 方法
如前所述,为了使操作方法仅对某些 HTTP 谓词或特定内容类型可用,可以使用以下方法之一:
HttpGetAttributeHttpPostAttributeHttpPutAttributeHttpDeleteAttributeHttpOptionsAttributeHttpPatchAttributeHttpHeadAttributeActionVerbsAttributeConsumesAttribute
这些名字应该是不言自明的。您可以为不同的谓词添加属性,如果存在任何谓词,则只有当其谓词与这些属性之一匹配时,路由才会匹配。ActionVerbsAttribute允许您传递希望支持的单个方法或方法列表。ConsumesAttribute采用有效的内容类型。
默认约束
ASP.NET Core 包含以下约束:
| 约束 | 目的 | 示例 |
| alpha (AlphaRouteConstraint) | 将文本限制为字母数字字符,即不包括符号 | {term:alpha} |
| bool (BoolRouteConstraint) | 仅为true或false | {force:bool} |
| datetime (DateTimeRouteConstraint) | 给出日期或日期和时间模式 | {lower:datetime} |
| decimal (DecimalRouteConstraint) | 包括十进制值 | {lat:decimal} |
| double (DoubleRouteConstraint) | 包括双精度浮点值 | {precision:double} |
| exists (KnownValueRouteConstraint) | 强制存在路由令牌 | {action:exists} |
| float (FloatRouteConstraint) | 包括单精度浮点值 | {accuracy:float} |
| guid (GuidRouteConstraint) | 包括 guid | {id:guid} |
| int (IntRouteConstraint) | 包括整数值 | {id:int} |
| length (LengthRouteConstraint) | 包括受约束的字符串 | {term:length(5,10) |
| long (LongRouteConstraint) | 包含一个长整数 | {id:long} |
| max (MaxRouteConstraint) | 这是整数的最大值 | {page:max(100)} |
| min (MinRouteConstraint) | 这是整数的最小值 | {page:min(1)} |
| maxlength (MaxLengthRouteConstraint) | 包括最大长度的任何字母数字字符串 | {term:maxlength(10)} |
| minlength (MinLengthRouteConstraint) | 包括任何最小长度的字母数字字符串 | {term:minlength(10)} |
| range (RangeRouteConstraint) | 包括一个整数范围 | {page:range(1,100)} |
| regex (RegexRouteConstraint) | 正则表达式 | {isbn:regex(^d{9}[d|X]$)} |
| required (RequiredRouteConstraint) | 包括必须实际存在的必需值 | {term:required} |
一个 route 参数可以同时接受多个约束,以:分隔,如下所示:
Calculator/Calculate({a:int:max(10)},{b:int:max(10)})
在本例中,a和b参数需要是整数,同时具有10的最大值。另一个例子如下:
Book/Find({isbn:regex(^d{9}[d|X]$)])
这将匹配一个 ISBN 字符串,该字符串以 9 位数字开头,后跟尾随数字或X字符。
还可以提供您自己的自定义约束,我们将在下面看到。
创建自定义约束
约束是实现IRouteConstraint的任何类。如果要在管线模板中内联使用,则必须对其进行注册。以下是验证偶数的路由约束示例:
public class EvenIntRouteConstraint : IRouteConstraint
{
public bool Match(
HttpContext httpContext,
IRouter route,
string routeKey,
RouteValueDictionary values,
RouteDirection routeDirection)
{
if ((!values.ContainsKey(routeKey)) || (values[routeKey] == null))
{
return false;
}
var value = values[routeKey].ToString();
if (!int.TryParse(value, out var intValue))
{
return false;
}
return (intValue % 2) == 0;
}
}
您应该能够知道所有路由参数都在values集合中提供,并且路由参数名称在routeKey中。如果没有实际提供路由参数,它将只返回false,如果无法将参数解析为整数,它将返回false。现在,要注册约束,您需要使用本章前面所示的AddRouting方法:
services.AddRouting(options =>
{
options.ConstraintMap.Add("evenint", typeof(EvenIntRouteConstraint));
});
这实际上与从注册的配置中检索RouteOptions相同:
services.Configure<RouteOptions>(options =>
{
//do the same
});
就这些。
如果希望使用路由约束验证 URL 或任何请求参数,可以使用未绑定到路由密钥的路由约束:
public class IsAuthenticatedRouteConstraint : IRouteConstraint
{
public bool Match(
HttpContext httpContext,
IRouter route,
string routeKey,
RouteValueDictionary values,
RouteDirection routeDirection)
{
return httpContext.Request.Cookies.ContainsKey("auth");
}
}
当然,还有其他(甚至更好的)方法可以做到这一点;这只是一个例子。
现在我们可以这样使用它,在一条路线中:
Calculator/Calculate({a:evenint},{b:evenint})
另一方面,如果您更愿意在MapControllerRoute调用中直接使用约束类,则不需要注册它们。无论如何,路由约束集合作为IInlineConstraintResolver服务提供:
var inlineConstraintResolver = routes
.ServiceProvider
.GetRequiredService<IInlineConstraintResolver>();
If you wish to specify custom route constraints in routing attributes, you will need to register them.
在本章中,我们了解了如何定义路由令牌的约束,包括创建我们自己的约束,这对于预先验证 URL 非常有用。下一节将解释什么是数据令牌。
路由数据令牌
路由数据令牌与路由令牌或路由参数相反,只是您在路由表条目中提供的一些任意数据,可用于路由处理管道,包括 MVC 操作方法。与路由令牌不同,路由数据令牌可以是任何类型的对象,而不仅仅是字符串。它们对 MVC 毫无意义,只会被忽略,但它们很有用,因为您可以有多个路由指向同一个操作方法,并且您可能希望使用数据令牌来找出触发调用的路由。
您可以按如下方式传递数据令牌:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" },
constraints: null,
dataTokens: new { foo = "bar" });
});
您还可以从IDataTokensMetatata元数据项中检索它们,如从控制器操作中检索:
public class HomeController : Controller
{
public IActionResult Index()
{
var metadata = this.HttpContext.GetEndpoint().Metadata.
GetMetadata<IDataTokensMetadata>();
var foo = metadata?.DataTokens["foo"] as string;
return this.View();
}
}
因为DataTokens值的原型是object,所以您需要知道将要检索什么。另外,请注意,如果没有设置数据令牌,GetMetadata<IDataTokensMetadata>()方法可能返回null!
无法更改数据标记的值。另外,ControllerBase类的旧RouteData属性和HttpContext上的GetRouteData扩展方法现在已经过时,可能会在未来版本的 ASP.NET Core 中删除。
最后,让我们继续,看看如何配置区域的路由。
路由到区域
MVC 长期以来一直支持区域的概念。从本质上讲,区域用于隔离和组织控制器和视图,因此,例如,可以在不同区域中具有相同名称的控制器。
VisualStudio 允许您在项目中创建文件夹,然后向其中添加控制器和视图。您可以将这些文件夹标记为区域。
在涉及路由的情况下,区域将另一个路由令牌(适当地命名为area)添加到controller和action。如果要使用区域,则模板中可能会有另一段,例如:
Products/Phones/Index
Reporting/Sales/Index
这里,Products和Reporting是区域。您需要将它们映射到路由,以便 MVC 能够识别它们。您可以使用MapControllerRoute扩展方式,但您需要提供area令牌,如下所示:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{area:exists}/{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" });
});
您也可以使用MapAreaControllerRoute扩展方法,该方法负责添加area参数:
endpoints.MapAreaControllerRoute(
name: "default",
areaName: "Products",
pattern: "List/{controller}/{action}/{id?}",
defaults: new { controller = "Phones", action = "Index" });
此路由将List/Phones/Index请求映射到Products区域内PhonesController控制器的Index动作方式。
这就是区域的问题。现在让我们看看路由属性。
使用路由属性
将路由添加到路由表的替代方法是使用路由属性。路由属性存在于 ASP.NET Core 之前,甚至存在于 ASP.NET MVC 和 Web API 中。如果希望 ASP.NET Core 自动识别路由属性,则需要执行以下操作:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
在以下部分中,我们将了解一些路由属性,并了解如何应用它们。
让我们看看如何使用属性定义路由。
定义路线
这些属性用于定义路线,可以组合在一起;如果我们给一个类添加一个路由属性,给它的一个方法添加另一个路由属性,实际路由将由这两个属性产生。
路由属性最明显的用途是修饰动作方法,如下所示:
[Route("Home/Index")]
public IActionResult Index() { ... }
例如,如果您在同一控制器中有多个操作,并且希望使用相同的前缀(Home来映射它们,则可以执行以下操作:
[Route("Home")]
public class HomeController
{
[Route("Index")]
public IActionResult Index() { ... }
[Route("About")]
public IActionResult About() { ... }
}
In previous (non-Core) versions of MVC and Web API, you could use RoutePrefixAttribute for this purpose. Now, RouteAttribute takes care of both cases.
路由是加法的,这意味着如果您在控制器中指定一个路由,然后在操作方法中,您将获得这两个路由,如Home/Index或Home/About。
如您所见,HomeController类中的 route 参数与控制器的常规名称(Home匹配。因此,我们也可以使用[controller]特殊代币:
[Route("[controller]")]
public class HomeController { ... }
对于 API 控制器,我们可以使用:
[Route("api/[controller]")]
public class ServicesController { ... }
此外,每个操作都映射了一个与方法名称完全匹配的名称。同样,我们可以使用[action]:
[Route("[action]")]
public IActionResult Index() { ... }
[Route("[action]")]
public IActionResult About() { ... }
可以传递多个路由属性,以便操作方法将响应不同的请求:
[Route("[action]")]
[Route("")]
[Route("Default")]
public IActionResult Index() { ... }
Index方法可通过以下任一请求调用:
/Home
/Home/Index
/Home/Default
注意,Home部分来自类级别应用的 route 属性。另一方面,如果在模板中指定斜杠,则使模板成为绝对模板;此模板的外观如下所示:
[Route("Default/Index")]
public IActionResult Index() { ... }
这只能通过以下方式访问:
/Default/Index
如果您想考虑控制器,您应该在模板中显式地命名它,或者使用[controller]特殊标记:
[Route("[controller]/Default/Index")]
public IActionResult Index() { ... }
可通过以下方式访问:
/Home/Default/Index
The [controller] and [action] tokens are for when we want to use constants for routes. These constants have the potential to be used in lots of places, as they are not stuck to specific actions and controllers. They were not available in previous versions of ASP.NET MVC or Web API.
默认路由
通过路由属性,您可以使用空白模板应用RouteAttribute来指定默认控制器:
[Route("")]
public class HomeController { ... }
控制器中的默认操作也将是具有空模板的操作,如下所示:
[Route("")]
public IActionResult Index() { ... }
如果没有具有空路由模板的方法,ASP.NET Core 将尝试查找名称与当前 HTTP 方法匹配的方法。
约束路线
您还可以指定管线约束,其语法与我们之前看到的相同:
[Route("Calculate({a:int},{b:int})")]
public IActionResult Calculate(int a, int b) { ... }
界定领域
通过将AreaAttribute应用于控制器,您也可以定义包含区域的路由:
[Area("Products")]
[Route("[controller]")
public class ReportingController { ... }
与[controller]和[action]类似,还有一个特殊的[area]标记,您可以在模板中使用它来指示当前区域,从文件系统推断:
[Route("[area]/Default")]
public IActionResult Index() { ... }
指定操作名称
您可以通过ActionNameAttribute为控制器方法指定操作名称,如下所示:
[ActionName("Default")]
public IActionResult Index() { ... }
您还可以通过 HTTP 动词选择属性(HttpGetAttribute、HttpPostAttribute、HttpPutAttribute、HttpOptionsAttribute、HttpPatchAttribute、HttpDeleteAttribute或HttpHeadAttribute中的任何一个)来执行此操作:
[HttpGet(Name = "Default")]
public IActionResult Index() { ... }
Please do remember that you cannot specify a route template and an action name at the same time, as this will result in an exception being thrown at startup time when ASP.NET Core scans the routing attributes. Also, do not specify ActionNameAttribute and a verb selection attribute at the same time as specifying the action name.
定义非行动
如果要防止控制器类中的公共方法被用作操作,可以使用NonActionAttribute对其进行修饰:
[NonAction]
public IActionResult Process() { ... }
限制路线
当我们谈到路由约束时,我们看到我们可以限制一个动作方法,使其仅在满足以下一个或多个条件时才可调用:
- 它与给定的 HTTP 动词(
ActionVerbsAttribute、Http*Attribute匹配。 - 使用 HTTPS(
RequireHttpsAttribute调用。 - 使用给定的内容类型(
ConsumesAttribute调用。
我们将不再详细讨论这个问题,因为前面已经解释过了。
设置路线值
可以在操作方法中提供任意路由值。这就是RouteValueAttribute抽象类的目的。您需要从中继承:
public class CustomRouteValueAttribute : RouteValueAttribute
{
public CustomRouteValueAttribute(string value) : base("custom", value) { }
}
然后,按如下方式应用和使用:
[CustomRouteValue("foo")]
public IActionResult Index()
{
var foo = this.ControllerContext.RouteData.Values["foo"];
return this.View();
}
AreaAttribute is an example of a class inheriting from RouteValueAttribute. There is no way to pass arbitrary route data tokens through attributes.
正如您所看到的,通过属性可以实现很多功能。这还包括错误处理;现在让我们来了解更多。
路由中的错误处理
对于在处理请求期间捕获的错误和异常,例如,当找不到资源时,我们该怎么办?您可以为此使用路由。在这里,我们将介绍一些策略:
- 路由
- 添加全面覆盖的路由
- 显示开发人员错误页面
- 使用状态代码页中间件
我们将在以下部分了解这些。
控制器路由的路由错误
当发生错误时,您可以通过调用UseExceptionHandler强制调用特定控制器的操作:
app.UseExceptionHandler("/Home/Error");
请注意,您在这个视图(Error中的内容完全取决于您。
您甚至可以做一些更有趣的事情,即注册中间件以在发生错误时执行,如下所示:
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var errorFeature = context.Features.Get<IException
HandlerPathFeature>();
var exception = errorFeature.Error; //you may want to check what
//the exception is
var path = errorFeature.Path;
await context.Response.WriteAsync("Error: " + exception.Message);
});
});
You will need to add a using reference for the Microsoft.AspNetCore.Http namespace in order to use the WriteAsync method.
IExceptionHandlerPathFeature功能允许您检索发生的异常和请求路径。使用这种方法,您必须自己生成输出;也就是说,您没有 MVC 视图的好处。
接下来,我们将介绍如何显示用户友好的错误页面。
使用开发人员异常页面
在开发模式下运行时,您可能需要一个显示开发人员相关信息的页面,在这种情况下,您应该调用UseDeveloperExceptionPage:
app.UseDeveloperExceptionPage();
这将基于也包含环境变量的默认模板显示异常消息,包括所有请求属性和堆栈跟踪。它通常仅用于Development环境,因为它可能包含攻击者可能使用的敏感信息。
由于.NETCore3,可以通过IDeveloperPageExceptionFilter实现来调整它的输出。我们在依赖项注入容器中注册了一个,或者在HandleExceptionAsync方法中提供我们自己的输出,或者只返回默认实现:
services.AddSingleton<IDeveloperPageExceptionFilter, CustomDeveloperPageExceptionFilter>();
此方法非常简单:它接收一个错误上下文和一个委托,该委托指向管道中的下一个异常筛选器,该筛选器通常生成默认错误页:
class CustomDeveloperPageExceptionFilter : IDeveloperPageExceptionFilter
{
public async Task HandleExceptionAsync(ErrorContext
errorContext, Func<ErrorContext, Task> next)
{
if (errorContext.Exception is DbException)
{
await errorContext.HttpContext.Response.WriteAsync("Error
connecting to the DB");
}
else
{
await next(errorContext);
}
}
}
这个简单的示例具有依赖于异常的条件逻辑,它要么发送自定义文本,要么只委托给默认处理程序。
使用“一网打尽”的路线
您可以通过添加一个操作方法来添加一个全面覆盖的路由,该方法的路由在没有其他路由匹配的情况下始终匹配(如回退端点部分离子中的回退页面)。例如,我们可以使用如下路由属性:
[HttpGet("{*url}", Order = int.MaxValue)]
public IActionResult CatchAll()
{
this.Response.StatusCode = StatusCodes.Status404NotFound;
return this.View();
}
当然,在Configure方法中,同样可以通过流畅的配置实现:
app.UseEndpoints(endpoints =>
{
//default routes go here
endpoints.MapControllerRoute(
name: "CatchAll",
pattern: "{*url}",
defaults: new { controller = "CatchAll", action = "CatchAll" }
);
});
在这里,您只需添加一个带有友好错误消息的漂亮视图!请注意,同一控制器中的其他操作也需要指定路由;否则,默认路由将变为CatchAll!
Fallback pages are a simpler alternative to catch-all routes.
使用状态代码页中间件
现在让我们看看如何使用 HTTP 状态码响应错误,这是向客户端返回高级响应的标准方法。
状态代码页
另一种选择是在400****坏请求和599****网络连接超时之间添加代码以响应特定的 HTTP 状态代码,该代码没有主体(尚未处理),我们通过UseStatusCodePages来实现:
app.UseStatusCodePages(async context => { context.HttpContext.Response.ContentType = "text/plain";
var statusCode = context.HttpContext.Response.StatusCode;
await context.HttpContext.Response.WriteAsync("HTTP status code: " + statusCode); });
该方法将中间件组件添加到管道中,该组件负责在异常发生后执行两项操作:
- 在
IStatusCodePagesFeature特性上填充Error属性 - 从那里处理死刑
这里有一个不同的重载,基本上与上一个重载相同:
app.UseStatusCodePages("text/plain", "Error status code: {0}");
下面是一些自动重定向到路由(找到一个 HTTP 代码为302)的东西,其中一个特定的状态代码作为路由值:
app.UseStatusCodePagesWithRedirects("/error/{0}");
相反,这一个在不发出重定向的情况下重新执行管道,从而使其更快:
app.UseStatusCodePagesWithReExecute("/error/{0}");
可通过IStatusCodePagesFeature功能禁用与特定状态代码相关的所有执行:
var statusCodePagesFeature = HttpContext.Features.Get<IStatusCodePagesFeature>();
statusCodePagesFeature.Enabled = false;
路由到特定状态代码页
您可以向控制器添加这样的操作,使其响应"error/404"请求(只需将错误代码替换为您想要的代码):
[Route("error/404")]
public IActionResult Error404()
{
this.Response.StatusCode = StatusCodes.Status404NotFound;
return this.View();
}
现在,要么添加一个Error404视图,要么调用一个通用视图,将404状态代码传递给它,可能通过视图包。同样,此路由可以流畅地配置,如下所示:
endpoints.MapControllerRoute(
name: "Error404",
pattern: "error/404",
defaults: new { controller = "CatchAll", action = "Error404" }
);
当然,这需要与UseStatusCodePagesWithRedirects或UseStatusCodePagesWithReExecute一起使用。
任何状态代码
要捕获单个方法中的所有错误,请执行以下操作:
[Route("error/{statusCode:int}")]
public IActionResult Error(int statusCode)
{
this.Response.StatusCode = statusCode;
this.ViewBag.StatusCode = statusCode;
return this.View();
}
在这里,我们调用一个名为Error(从动作名称推断)的通用视图,因此我们需要向其传递原始状态代码,我们通过视图包执行此操作,如下所示:
endpoints.MapControllerRoute(
name: "Error",
pattern: "error/{statusCode:int}",
defaults: new { controller = "CatchAll", action = "Error" }
);
对于/error/<statusCode>的请求,我们被引导到CatchAllController控制器和Error动作。同样,这需要UseStatusCodePagesWithRedirects或UseStatusCodePagesWithReExecute。
在这里,我们介绍了基于异常或状态代码的不同错误处理方法。挑一个最适合你的!
总结
在现实生活中,您可能会混合使用基于代码的路由配置和属性。在我们的示例中,我们将使用本地化功能,这需要大量配置,通常是基于代码的配置。属性路由也有它的位置,因为我们可以直接定义不需要受常规路由模板限制的可访问端点。路由约束非常强大,应该使用。
从包含的默认路线模板开始并从那里开始,总是很好的。它应该足以满足您大约 80%的需求。其他路由将通过自定义路由或路由属性定义。
我们在本章中看到,安全性是需要考虑的因素,为此目的使用路由属性似乎很理想,因为我们可以通过查看控制器方法立即了解安全限制。
我们已经看到了配置路由的不同方式,换句话说,将浏览器请求转化为操作。我们研究了基于代码和基于属性的路由,并了解了它们的一些优点和局限性。我们了解了如何将 URL 参数限制为特定类型或符合特定要求,以及如何防止调用动作方法,除非它符合特定动词、HTTPS 要求或请求内容类型。最后,我们研究了如何使用路由指向状态代码或特定于错误的操作,以便返回友好的错误页面。
本章中涉及的许多主题将在后面的章节中再次出现。在下一章中,我们将讨论 MVC 最重要的部分,这也是本章的主题:控制器和动作。
问题
现在,您已经完成了本章的学习,您应该能够回答以下问题:
- 什么是特殊路线代币?
- 我们如何防止根据请求的 HTTP 谓词选择路由?
- 除非请求使用 HTTPS,否则如何防止选择路由?
- 如何根据出现的 HTTP 错误代码提供不同的视图?
- 如何防止调用控制器中的方法?
- 如何强制路由值为特定类型(例如,数字)?
- 什么是路由处理程序?
四、控制器和动作
本章讨论 MVC 最重要的特性:逻辑存储在哪里。这是实现应用所做的事情的地方,也是业务逻辑的重要组成部分。
控制器和动作通过约定找到,并作为路由规则的结果调用,路由规则在前一章中介绍。但是事情会变得非常复杂,有很多方法可以让一个动作从请求中检索数据;它可以是异步的,也可以是同步的,并且可以返回许多不同类型的数据。可以缓存这些数据,以便在重复请求时基本上不会造成性能损失。
正如我们所知,HTTP 是无状态的,但这在我们感兴趣的现代应用中并不起作用,因此我们需要维护请求之间的状态。我们还希望根据发出请求的人的文化和语言返回数据、数字和文本。在本章中,我们将研究所有这些主题。
在本章中,我们将学习以下主题:
- 如何使用控制器
- 如何找到控制器
- 控制器的生命周期是什么?
- 什么是控制器动作?
- 如何进行错误处理
- 如何缓存响应
- 如何维护请求之间的状态
- 使用依赖注入
- 应用全球化和本地化
技术要求
为了实现本章中介绍的示例,您需要.NET Core 3 SDK 和文本编辑器。当然,VisualStudio2019(任何版本)满足所有要求,但您也可以使用 VisualStudio 代码。
源代码可在从 GitHub 检索 https://github.com/PacktPublishing/Modern-Web-Development-with-ASP.NET-Core-3-Second-Edition 。
开始
我们将在实际代码所在的位置工作,在哪里完成工作,在哪里处理来自浏览器的请求。我们将讨论 MVC 控制器返回视图,还将讨论跨请求持久化数据、将依赖项注入控制器和操作以及如何向代码添加本地化支持。总而言之,这是一个非常重要的章节,所以我请你们全神贯注。
在本章中,我们将讨论 MVC 应用最重要的方面:
- 控制器
- 行动
我们将在接下来的章节中研究每一个问题。
使用控制器
在 MVC 中,控制器负责处理请求。它是业务逻辑所在的位置、检索数据的位置、验证请求参数的位置等等。在面向对象的语言中,例如那些支持.NET Framework 的语言,这是在类中实现的。请记住,MVC 模式提倡强烈的责任分离,这使得其所有组件都特别重要;即使考虑到这一事实,控制器实际上也是 ASP.NET Core 中唯一需要的部分,因为您可以在没有视图的情况下生活。试想一下不返回任何用户界面或模型的 web 服务。这是 ASP.NET Core 的一个非常重要的方面。
控制器基类
ASP.NET Core(与其前身一样)提供了一个名为ControllerBase的基类,您可以从中继承,尽管严格来说这不是必需的。我们将在本章后面更详细地讨论这一点。然而,继承ControllerBase有几个优点:
-
轻松访问模型验证
-
用于返回不同结果的助手方法(重定向、JSON、视图、文本等)
-
直接访问请求和响应基础结构对象,包括头、cookie 等
-
拦截/覆盖操作事件的能力
实际上,还有另一个类,Controller,它依次继承自ControllerBase,如果您想处理视图,您应该继承该类。如果您正在编写 web 服务(web API),则不需要使用视图。
VisualStudio 中的模板始终生成从Controller类继承的控制器,但如果愿意,可以将其更改为 POCO。除非您想更改约定,否则唯一真正的要求是为所有控制器添加Controller后缀。名称空间或物理位置不相关。例如,您可以在不同的文件夹或名称空间中创建控制器。
ControllerBase类使以下属性可用:
-
ControllerContext(ControllerContext):当前控制器和请求的执行上下文,包括动作描述符(用于猜测应该调用哪个动作)和获取动作参数的值提供程序工厂;它是这个类的一个实例。 -
HttpContext(HttpContext):HTTP 上下文,包括请求和响应对象,我们可以从中获取和设置所有的头、cookie、状态码、认证信息、证书等;还提供对依赖项注入(DI)框架、框架功能、会话状态(如果已启用)和底层连接属性的访问。 -
MetadataProvider(IModelMetadataProvider:用于提取类模型的元数据验证器、文本描述符和编辑信息。 -
ModelBinderFactory(IModelBinderFactory):这是一个用于创建绑定器的对象,该绑定器反过来用于将提交的请求属性绑定到给定的类模型。 -
ModelState(ModelStateDictionary):这是提交的模型值和验证结果。 -
ObjectValidator(IObjectModelValidator:用于验证提交模型的实例。 -
Request****HttpRequest:处理指向HttpContext内相同对象的方便指针。 -
Response****HttpResponse:处理指向HttpContext内相同对象的方便指针。 -
Url(IUrlHelper):这是一个实例,使用方便的方法生成指向特定控制器动作的 URL 链接。 -
User(ClaimsPrincipal:该字段包含对当前 ASP.NET Core 用户的引用;根据实际使用的身份验证机制,它将持有不同的值和声明,即使它没有经过身份验证,也永远不会是null。
Controller类提供了前面所有的属性以及视图特定的属性:
RouteData(RouteData:包含 MVC 路由数据参数。ViewBag(dynamic):这是在视图中可用的数据的动态集合。ViewData(ViewDataDictionary):与ViewBag相同,但以键值字典的形式强类型输入。TempData(ITempDataDictionary):这是一个强类型字典,用于在下次提交表单之前维护数据。
It's safe and convenient to inherit from Controller, even if you do not use views; it won't cause any problems.
当然,控制器需要提供至少一个操作方法,用于执行操作并向调用者返回有意义的内容,可以是 HTML 视图、一些 JSON 内容,也可以只是 HTTP 状态代码。
您还可以重写许多虚拟方法,以便在调用某个操作方法之前、之后或代替该操作方法执行操作。这些在接口IActionFilter和IAsyncActionFilter中定义,由Controller实现:
OnActionExecuted在调用动作后被调用。- 在调用操作之前同步调用
OnActionExecuting。 - 在调用操作之前异步调用
OnActionExecutingAsync。
这些接口是过滤器的基础,我们将在后面更详细地讨论。
I almost forgot: if a controller class has the [NonController] attribute applied to it, then it is not considered and cannot be used as a controller.
POCO 控制器
在 ASP.NET Core 中,控制器不需要从任何基类继承或实现特定接口。正如我们前面提到的,根据惯例,他们只需要使用Controller后缀,并避免使用[NonController]属性。这种方法的问题是,您会丢失所有帮助器方法和上下文属性(HttpContext、ControllerContext、ViewBag和Url),但您可以将它们注入。让我们看看这是怎么回事。
If you add the [Controller] attribute to any POCO class, you can turn it into a controller, regardless of its name.
向 POCO 控制器添加上下文
例如,假设您有一个 POCO 控制器,HomeController。您没有各种与上下文和视图包相关的属性,但通过将两个属性应用于适当类型的属性,您可以让基础结构注入它们,如以下示例所示:
public class HomeController
{
private readonly IUrlHelperFactory _url;
public HomeController(IHttpContextAccessor ctx, IUrlHelperFactory url)
{
this.HttpContext = ctx.HttpContext;
this._url = url;
}
[ControllerContext]
public ControllerContext { get; set; }
public HttpContext HttpContext { get; set; }
[ActionContext]
public ActionContext ActionContext { get; set; }
[ViewDataDictionary]
public ViewDataDictionary ViewBag { get; set; }
public IUrlHelper Url { get; set; }
public string Index()
{
this.Url = this.Url ?? this._url.GetUrlHelper(this.ActionContext);
return "Hello, World!";
}
}
在这里,您会注意到一些有趣的事情:
- 只需将
[ActionContext]、[ControllerContext]和[ViewDataDictionary]属性添加到任何名称的属性,并分别添加ActionContext、ControllerContext和ViewDataDictionary类型,即可自动注入ActionContext、ControllerContext和ViewBag。 - 当 ASP.NET Core 基础设施实例化控制器时,依赖注入框架注入
IHttpContextAccessor和IUrlHelperFactory对象。 HttpContext对象需要从传递的IHttpContextAccessor实例中获取。- 为了构建一个
IUrlHelper,IUrlHelperFactory需要一个ActionContext实例;因为我们在构造函数的时候没有它,所以我们需要在以后构建它,例如,在一个 action 方法中(在这个例子中,Index)。
然而,为了使这项工作正常进行,我们需要告诉 ASP.NET Core 注册IHttpContextAccessor和IUrlHelperFactory的默认实现。这通常在Startup类的ConfigureServices方法中完成:
services.AddScoped<IHttpContextAccessor, HttpContextAccessor>();
//or, since version 2.1:
services.AddHttpContextAccessor();
services.AddScoped<IUrlHelperFactory, UrlHelperFactory>();
这些属性的行为方式与继承自ControllerBase和Controller的非 POCO 属性完全相同。
POCO 控制器中的拦截操作
如果需要,还可以实现一个过滤器接口,以便在调用操作之前或之后与请求进行交互,例如IActionFilter:
public class HomeController : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
//before the action is called
}
public void OnActionExecuted(ActionExecutedContext context)
{
//after the action is called
}
}
如果您希望有一个异步处理程序,请改为实现异步版本(IAsyncXXXFilter。我们将在第 10 章、理解过滤器中进一步讨论过滤器。
现在让我们看看框架是如何发现控制器的。
查找控制器
无论您选择 POCO 还是非 POCO 控制器,ASP.NET Core 都将应用相同的规则来发现控制器,如下所示:
- 他们需要有
Controller后缀(严格来说,这是可以更改的,但我们现在就不做了)。 - 它们需要是可实例化的类(非抽象、非泛化和非静态)。
- 它们不能应用
[NonController]属性。 - 如果它们是 POCO 并且没有
Controller后缀,您可以用[Controller]属性装饰它们。
By convention, the files that contain the controller classes are stored in a folder called Controllers, and also in a Controllers namespace, but this is just ignored.
控制器类通过路由中的名称和控制器参数进行查找,并在为此目的注册的程序集中进行搜索。默认情况下,搜索中包括当前正在执行的程序集,但注册为应用部件的所有程序集也包括在内。将 MVC 功能添加到依赖项注入框架(ConfigureServices方法)时,可以注册其他应用部件,如下所示:
services.AddMvc()
.AddApplicationPart(typeof(MyCustomComponent).GetTypeInfo().Assembly);
在这里,我们添加了对包含一个假设类MyCustomComponent的程序集的引用。完成此操作后,位于其中的所有控制器都可以使用。为了获得找到的控制器的完整列表,我们可以使用ControllerFeature并通过ApplicationPartManager填充它:
services.AddMvc()
.AddApplicationPart(typeof(MyCustomComponent).GetTypeInfo().Assembly)
.ConfigureApplicationPartManager(parts =>
{
var controllerFeature = new ControllerFeature();
parts.PopulateFeature(controllerFeature);
//controllerFeature.Controllers contains the list of discovered
//controllers' types
});
控制器只在启动时被发现一次,这在性能方面是一件好事。
如果有两个控制器具有相同的名称,但位于不同的命名空间中,并且它们都公开了一个与当前请求匹配的操作方法,则 ASP.NET 将不知道选择哪个控制器,并将引发异常。如果发生这种情况,我们需要通过应用[ControllerName]属性为其中一个类指定一个新的控制器名称,如下代码所示:
namespace Controllers
{
public class HomeController
{
}
namespace Admin
{
[ControllerName("AdminHome")]
public class HomeController
{
}
}
}
我们还可以更改动作名称,稍后我们将看到。现在,让我们看看找到控制器类型后会发生什么。
控制器生命周期
找到控制器的类型后,ASP.NET Core 将启动一个进程对其进行实例化。程序如下:
-
默认控制器工厂(
IControllerFactory来自依赖注入(DI框架)并调用其CreateController方法。 -
控制器工厂使用同样从 DI 获得的注册控制器激活器(
IControllerActivator来获取控制器的实例(IControllerActivator.Create。 -
使用 DI 的
IActionSelector定位动作方法。 -
如果控制器实现任何过滤器接口(
IActionFilter、IResourceFilter等),或者如果该操作具有任何过滤器属性,则会对其和全局过滤器调用适当的方法。 -
动作方法由
IActionInvokerProvider中的IActionInvoker调用,也从 DI 中获取。 -
对控制器、操作方法的过滤器属性和全局过滤器调用任何过滤器方法。
-
控制器工厂释放控制器(
IControllerFactory.ReleaseController。 -
控制器激活器释放控制器(
IControllerActivator.Release。 -
如果控制器实现了
IDisposable,则调用Dispose方法。
这些组件大部分可以通过内置的 DI 框架注册,例如,如果您想替换默认的IControllerFactory实现,那么您可以通过ConfigureServices方法进行注册:
services.AddSingleton<IControllerFactory, CustomControllerFactory>();
现在,假设您想编写一个操作选择器,将所有调用重定向到类的特定方法。您可以编写重定向操作选择器,如下所示:
public class RedirectActionSelector : IActionSelector
{
public ActionDescriptor SelectBestCandidate(
RouteContext context,
IReadOnlyList<ActionDescriptor> candidates)
{
var descriptor = new ControllerActionDescriptor();
descriptor.ControllerName = typeof(MyController).Name;
descriptor.MethodInfo = typeof(MyController).
GetMethod("MyAction");
descriptor.ActionName = descriptor.MethodInfo.Name;
return descriptor;
}
public IReadOnlyList<ActionDescriptor> SelectCandidates(
RouteContext context)
{
return new List<ActionDescriptor>();
}
}
这将把任何请求重定向到MyController类的MyAction方法。嘿,只是为了好玩,记得吗?
现在让我们来看看行动。
行动
动作方法是所有动作发生的地方(双关语)。它是处理您的请求的代码的入口点。从IActionInvoker实现调用找到的动作方法;它必须是控制器类的物理非通用公共实例方法。动作选择机制相当复杂,依赖于路由动作参数。
动作方法的名称应该与此参数相同,但这并不意味着它是物理方法名称;您还可以应用[ActionName]属性将其设置为不同的值,如果我们有重载方法,这一点特别有用:
[ActionName("BinaryOperation")]
public IActionResult Operation(int a, int b) { ... }
[ActionName("UnaryOperation")]
public IActionResult Operation(int a) { ... }
在以下部分中,我们将了解操作如何工作以及它们在控制器上下文中如何工作。
寻找行动
在发现一组用于处理请求的候选控制器后,ASP.NET Core 将检查它们是否提供与当前路由匹配的方法(请参见第 3 章、路由:
- 它必须是公共的、非静态的和非通用的。
- 其名称必须与路由的操作相匹配(物理名称可能不同,只要它具有
[ActionName]属性)。 - 其参数必须与路由中指定的非可选参数(未标记为可选且无默认值的参数)匹配;如果路由指定了
id值,则必须有id参数和类型,如果id有int的路由约束,如{id:int}中所述,则必须是int类型。 - 动作方法可以有
IFormCollection、IFormFile或IFormFileCollection类型的参数,因为这些参数总是可以接受的。 - 它不能应用
[NonAction]属性。
获取适用操作的实际规则如下所示:
- 如果 URL 中提供了操作名称,则暂时使用该名称。
- 如果在基于 fluent 配置或属性的路由中指定了默认操作,则暂时使用该操作。
当我的意思是暂时的,我的意思是说可能有约束属性(一分钟后会有更多关于这方面的内容)或需要检查的强制属性。例如,如果一个操作方法需要一个强制参数,并且在请求或任何源中都找不到它,那么该操作就不能用于服务当前请求。
同步和异步操作
动作方法可以是同步的,也可以是异步的。对于异步版本,其原型应如下所示:
public async Task<IActionResult> Index() { ... }
当然,您可以添加任意数量的参数,就像使用同步操作方法一样。然而,这里的关键是将方法标记为async并返回Task<IActionResult>,而不仅仅是IActionResult(或其他继承类型)。
为什么要使用异步操作?你需要了解以下事实:
- Web 服务器有许多线程用于处理传入的请求。
- 当一个请求被接受时,其中一个线程在等待处理时被阻塞。
- 如果请求花费的时间太长,则此线程无法响应其他请求。
输入异步操作。对于异步操作,只要线程接受传入请求,它就会立即将其传递给后台线程,由后台线程处理,释放主线程。这非常方便,因为它可以接受其他请求。这与性能无关,而是与可伸缩性有关;使用异步操作允许应用始终响应,即使它仍在后台处理请求。
获取上下文
我们已经了解了如何在 POCO 和基于控制器的控制器中访问上下文。根据上下文,我们将讨论与行动方法有关的三件事:
- HTTP 上下文,由
HttpContext类表示,从中可以访问当前用户、低级请求和响应属性,如 cookie、标头等。 - 控制器上下文,
ControllerContext的一个实例,允许您访问当前模型状态、路由数据、操作描述符等。 ActionContext类型的动作上下文,它提供的信息与您从ControllerContext获得的信息几乎相同,但在不同的地方使用;因此,如果将来只在一个功能上添加一个新功能,它将不会显示在另一个功能上。
访问上下文非常重要,因为您可能需要根据从上下文中获得的信息做出决策,或者直接设置响应头或 cookie。您可以看到 ASP.NET Core 已经删除了自 ASP.NET 开始以来一直存在的HttpContext.Current属性,因此您无法立即访问它;但是,您可以从ControllerContext或ActionContext中获取它,或者通过让构造函数获取IHttpContextAccessor的实例,将它注入到依赖项注入构建组件中。
动作约束
在应用于 action 方法的属性中实现以下属性和接口时,可能会阻止调用它:
-
[NonAction]:从未调用该操作。 -
[Consumes]:如果有许多候选方法,例如,在方法重载的情况下,则使用此属性检查是否有任何方法接受当前请求的内容类型。 -
[RequireHttps]:如果存在,则仅当请求协议为 HTTPS 时才会调用 action 方法。 -
IActionConstraint:如果应用于动作方法的属性实现了该接口,则调用其Accept方法,查看是否应该调用该动作。 -
IActionHttpMethodProvider:由[AcceptVerbs]、[HttpGet]、[HttpPost]等 HTTP 方法选择器属性实现;如果存在,则仅当当前请求的 HTTP 谓词与HttpMethods属性返回的值之一匹配时,才会调用 action 方法。 -
IAuthorizeData:将检查实现此接口的任何属性,其中最臭名昭著的是[Authorize],以查看分配给HttpContext's User属性的ClaimsPrincipal当前标识是否具有正确的策略和角色。 -
Filters:如果将筛选器属性(例如IActionFilter)应用于操作,或者如果调用了IAuthorizationFilter(例如)并可能引发异常或返回IActionResult,从而阻止调用操作(NotFoundObjectResult、UnauthorizedResult等)。
IActionConstraint的此实现将应用自定义逻辑来决定是否可以在其Accept方法中调用某个方法:
public class CustomAuthorizationAttribute: Attribute, IActionConstraint
{
public int Order { get; } = int.MaxValue;
public bool Accept(ActionConstraintContext context)
{
return
context.CurrentCandidate.Action.DisplayName
.Contains("Authorized");
}
}
context参数授予对路由上下文的访问权,并从路由上下文访问 HTTP 上下文和当前候选方法。这些应该足以做出决定。
应用约束的顺序可能是相关的,因为IActionConstraint接口的Order属性在属性中使用时,将决定应用于同一方法的所有属性的相对执行顺序。
动作参数
动作方法可以获取参数。例如,这些参数可以是提交的表单值或查询字符串参数。基本上有三种方法可以获得所有提交的值:
IFormCollection、IFormFile和IFormFileCollection:这些类型中的任何一个参数都将包含 HTML 表单提交的值列表;它们不会用于GET请求,因为无法使用GET上传文件。HttpContext:直接访问上下文并从Request.Form或Request.QueryString集合中检索值。- 添加与我们希望单独访问的请求中的值匹配的命名参数。
后者可以是基本类型,如string、int等,也可以是复杂类型。其值的注入方式是可配置的,并且基于提供者模型。IValueProviderFactory和IValueProvider用于获取这些属性的值。ASP.NET Core 为开发人员提供了通过AddMvc方法检查价值提供者工厂集合的机会:
services.AddMvc(options =>
{
options.ValueProviderFactories.Add(new CustomValueProviderFactory());
});
开箱即用,以下价值提供者工厂可用,并按以下顺序注册:
FormValueProviderFactory:从提交的表单中注入值,例如<input type="text" name="myParam"/>。RouteValueProviderFactory:路线参数,例如[controller]/[action]/{id?}。QueryStringValueProviderFactory:查询字符串值,例如?id=100。JQueryFormValueProviderFactory:jQuery 表单值。
但是,顺序很重要,因为它决定了将值提供程序添加到 ASP.NET Core 用于实际获取值的集合中的顺序。每个值提供程序工厂将调用其CreateValueProviderAsync方法,并且通常会填充一个值提供程序集合(例如,QueryStringValueProviderFactory将添加一个QueryStringValueProvider实例,等等)。
这意味着,例如,如果您提交了一个名为myField的表单值,并通过查询字符串传递myField的另一个值,那么将使用第一个值;但是,可以同时使用多个提供程序,例如,如果您的路由需要一个id参数,但也可以接受查询字符串参数:
[Route("[controller]/[action]/{id}?{*querystring}")]
public IActionResult ProcessOrder(int id, bool processed) { ... }
这将愉快地访问一个请求/Home/Process/120?processed=true,其中id来自路由,并由查询字符串提供程序处理。
某些发送值的方法允许它们是可选的,例如路由参数。在这种情况下,您需要确保 action 方法中的参数也允许以下操作:
- 引用类型,包括可以有
null值的引用类型 - 值类型,应具有默认值,如
int a = 0
例如,如果希望将路由中的值注入到操作方法参数中,如果该值是必需的,则可以这样做:
[Route("[controller]/[action]/{id}")]
public IActionResult Process(int id) { ... }
如果是可选的,您可以这样做:
[Route("[controller]/[action]/{id?}")]
public IActionResult Process(int? id = null) { ... }
值提供程序更有趣,因为它们实际返回操作方法参数的值。他们试图从名称中找到一个值,即 action 方法参数名称。ASP.NET 将迭代提供的值提供者列表,对每个参数调用其ContainsPrefix方法,如果结果为true,则调用GetValue方法。
即使提供的值提供程序很方便,您也可能希望从其他来源获取值。例如,我可以想到以下几点:
- 曲奇饼
- 标题
- 会话值
假设您希望将 cookie 值自动注入到操作方法的参数中。为此,您可以编写一个CookieValueProviderFactory,它很可能如下所示:
public class CookieValueProviderFactory : IValueProviderFactory
{
public Task CreateValueProviderAsync(
ValueProviderFactoryContext context)
{
context.ValueProviders.Add(new
CookieValueProvider(context.ActionContext));
return Task.CompletedTask;
}
}
然后你可以写一个CookieValueProvider来配合它:
public class CookieValueProvider : IValueProvider
{
private readonly ActionContext _actionContext;
public CookieValueProvider(ActionContext actionContext)
{
this._actionContext = actionContext;
}
public bool ContainsPrefix(string prefix)
{
return this._actionContext.HttpContext.Request.Cookies
.ContainsKey(prefix);
}
public ValueProviderResult GetValue(string key)
{
return new ValueProviderResult(this._actionContext.HttpContext
.Request.Cookies[key]);
}
}
之后,您将在MvcOptions的ValueProviders集合中的AddMvc方法中注册:
services.AddMvc(options =>
{
options.ValueProviderFactories.Add(new CookieValueProviderFactory());
}):
现在,您可以将 cookie 值透明地注入到您的操作中,而无需任何额外的工作。
Don't forget that, because of C# limitations, you cannot have variables or parameters that contain - or other special characters, so you cannot inject values for parameters that have these in their names out of the box. In this cookie example, you won't be able to have a parameter for a cookie with a name like AUTH-COOKIE.
但是,在相同的操作方法中,可以使用来自不同来源的参数,如下所示:
[HttpGet("{id}")]
public IActionResult Process(string id, Model model) { ... }
但是,如果目标操作方法参数不是字符串类型怎么办?答案在于模型绑定。
模型绑定
模型绑定是 ASP.NET Core 将部分请求(包括路由值、查询字符串、提交的表单等)转换为强类型参数的过程。与 ASP.NET Core 的大多数 API 一样,这是一种可扩展的机制。不要与模型值提供者混淆;模型绑定器的责任不是提供值,而仅仅是使它们适合我们告诉它们的任何类!
开箱即用,ASP.NET 可以转换为以下内容:
IFormCollection、IFormFile和IFormFileCollection参数- 基元/基类型(处理字符串之间的转换)
- 枚举
- POCO 类
- 辞典
- 收藏
- 取消令牌(稍后将对此进行详细介绍)
模型绑定器提供程序在MvcOptions类中配置,通常可通过AddMvc调用访问:
services.AddMvc(options =>
{
options.ModelBinderProviders.Add(new CustomModelBinderProvider());
});
应该已经支持您感兴趣的大多数场景。您还可以指定从中获取参数的源。那么,让我们看看如何使用这种能力。
身体
如果您正在使用 HTTP 谓词调用一个操作,该谓词允许您传递有效负载(POST、PUT和PATCH),您可以通过应用[FromBody]属性请求您的参数从该有效负载接收一个值:
[HttpPost]
public IActionResult Submit([FromBody] string payload) { ... }
除了使用字符串值外,您还可以提供自己的 POCO 类,如果配置的输入格式化程序之一支持该格式,则将从有效负载填充该类(稍后将对此进行详细介绍)。
类型
另一个选项是在提交的表单中使用来自特定命名字段的参数,为此,我们使用[FromForm]属性:
[HttpPost]
public IActionResult Submit([FromForm] string email) { ... }
有一个Name属性,如果提供该属性,将从指定的命名表单字段(例如,[FromForm(Name = "UserEmail")])获取值。
标题
标头也是检索值的一个很好的候选者,因此,[FromHeader]属性:
public IActionResult Get([FromHeader] string accept) { ... }
[FromHeader]属性允许我们指定实际的头名称(例如,[FromHeader(Name = "Content-Type")]),如果没有指定,它将查找应用它的参数的名称。
默认情况下,它只能绑定到字符串或字符串集合,但您可以强制它接受其他目标类型(前提是输入对该类型有效)。配置 MVC 时只需将AllowBindingHeaderValuesToNonStringModelTypes属性设置为true:
services.AddMvc(options =>
{
options.AllowBindingHeaderValuesToNonStringModelTypes = true;
});
查询字符串
我们还可以使用[FromQuery]属性通过查询字符串检索值:
public IActionResult Get([FromQuery] string id) { ... }
您还可以使用Name属性[FromQuery(Name = "Id")]指定查询字符串参数名称。请注意,按照惯例,如果不指定此属性,仍然可以从查询字符串传递值,这些值将传递给操作方法参数。
路线
路线参数也可以是数据源输入[FromRoute]:
[HttpGet("{id}")]
public IActionResult Get([FromRoute] string id) { ... }
与大多数其他绑定属性类似,您可以指定一个名称来指示值应该来自的路由参数(例如,[FromRoute(Name = "Id")]。
依赖注入
您还可以使用依赖项注入,例如([FromServices]:
public IActionResult Get([FromServices] IHttpContextAccessor accessor) { ... }
当然,您注入的服务需要提前在 DI 框架中注册。
定制活页夹
也可以指定您自己的活页夹。为此,您可以使用[ModelBinder]属性,该属性将可选Type作为其参数。有趣的是,它可以用于不同的场景,例如:
- 如果将其应用于控制器类上的属性或字段,则它将绑定到来自任何受支持的值提供程序(查询字符串、路由、表单等)的请求参数:
[ModelBinder]
public string Id { get; set; }
- 如果您传递了实现
IModelBinder的类的类型,那么您可以将该类用于实际的绑定过程,但仅用于应用该类的参数、属性或字段:
public IActionResult Process([ModelBinder(typeof(CustomModelBinder))] Model model) { ... }
进行 HTML 格式设置的简单模型活页夹可以编写如下:
public class HtmlEncodeModelBinder : IModelBinder
{
private readonly IModelBinder _fallbackBinder;
public HtmlEncodeModelBinder(IModelBinder fallbackBinder)
{
if (fallbackBinder == null)
throw new ArgumentNullException(nameof(fallbackBinder));
_fallbackBinder = fallbackBinder;
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var valueProviderResult = bindingContext.ValueProvider.
GetValue(bindingContext.ModelName);
if (valueProviderResult == ValueProviderResult.None)
{
return _fallbackBinder.BindModelAsync(bindingContext);
}
var valueAsString = valueProviderResult.FirstValue;
if (string.IsNullOrEmpty(valueAsString))
{
return _fallbackBinder.BindModelAsync(bindingContext);
}
var result = HtmlEncoder.Default.Encode(valueAsString);
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
The code for this was written by Steve Gordon and is available at https://www.stevejgordon.co.uk/html-encode-string-aspnet-core-model-binding.
代码做的不多:它在构造函数中使用一个回退绑定器,如果没有要绑定的值,或者该值是null或空字符串,则使用它;否则,它将对其进行 HTML 编码。
还可以将模型绑定提供程序添加到全局列表中。将拾取处理目标类型的第一个。模型绑定提供程序的接口由IModelBinderProvider(谁知道?)定义,它只指定一个方法GetBinder。如果返回非 null,则将使用活页夹。
让我们来看一个模型绑定器提供程序,它将此模型绑定器应用于具有自定义属性的字符串参数:
public class HtmlEncodeAttribute : Attribute { }
public class HtmlEncodeModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null) throw new
ArgumentNullException(nameof(context));
if ((context.Metadata.ModelType == typeof(string)) &&
(context.Metadata.ModelType.GetTypeInfo().
IsDefined(typeof(HtmlEncodeAttribute))))
{
return new HtmlEncodeModelBinder(new SimpleTypeModelBinder(
context.Metadata.ModelType));
}
return null;
}
}
之后,我们在AddMvc中将其注册到ValueProviderFactories集合中;迭代此集合,直到从GetBinder返回正确的模型绑定器,在这种情况下,它的使用方式如下:
services.AddMvc(options =>
{
options.ValueProviderFactories.Add(new
HtmlEncodeModelBinderProvider());
});
我们创建了一个简单的标记属性HtmlEncodeAttribute(以及一个模型绑定器提供程序),用于检查目标模型是否为字符串类型,并将[HtmlEncode]属性应用于该模型。如果是,则应用HtmlEncodeModelBinder。就这么简单:
public IActionResult Process([HtmlEncode] string html) { ... }
在本章后面讨论 HTML 表单时,我们将重新讨论模型绑定。
属性绑定
控制器中使用[BindProperty]属性修饰的任何属性也会从请求数据绑定。您也可以应用相同的绑定源属性([FromQuery]、[FromBody]等等),但要在GET请求中填充它们,您需要明确地告诉框架这样做:
[BindProperty(SupportsGet = true)]
public string Id { get; set; }
您还可以将其应用于控制器级属性验证属性(例如,[Required]、[MaxLength]等),它们将用于验证每个属性的值。[BindRequired]也有效,这意味着如果未提供属性值,则会导致错误。
输入格式化程序
当您通过应用[FromBody]属性从有效负载绑定 POCO 类时,ASP.NET Core 将尝试将有效负载中的 POCO 类型反序列化为字符串。为此,它使用了一个输入格式化程序。与输出格式化程序类似,它们用于在 JSON 或 XML 等常用格式之间进行转换。对 JSON 的支持是现成的,但是您需要显式地添加对 XML 的支持。您可以通过包含 NuGet 包Microsoft.AspNetCore.Mvc.Formatters.Xml并显式添加对管道的支持来实现这一点:
services
.AddMvc()
.AddXmlSerializerFormatters();
如果您感到好奇,那么这将向MvcOptions``InputFormatters集合添加一个XmlSerializerInputFormatter实例。列表将被迭代,直到一个格式化程序能够处理数据。包括的格式化程序如下所示:
JsonInputFormatter,可从任何 JSON 内容导入(application/json)JsonPatchInputFormatter,可从 JSON 补丁内容(application/json-patch+json导入)
显式绑定
您还可以通过应用属性来微调模型类的哪些部分被绑定,以及如何绑定。例如,如果您想从绑定中排除属性,可以应用[BindNever]属性:
public class Model
{
[BindNever]
public int Id { get; set; }
}
或者,如果要显式定义应该绑定哪些属性,可以将[Bind]应用于Model类:
[Bind("Name, Email")]
public class Model
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
如果将值传递给Prefix属性,则可以指示 ASP.NET Core 从具有该前缀的属性检索要绑定的值。例如,如果有多个具有相同名称的表单值(例如,option),则可以将它们全部绑定到一个集合:
[Bind(Prefix = "Option")]
public string[] Option { get; set; }
通常,如果源媒体中未提供属性值,例如POST有效负载或查询字符串,则属性不会获得值。但是,您可以强制执行此操作,如下所示:
[BindRequired]
public string Email { get; set; }
如果不传递Email参数,则ModelState.IsValid将为false,并抛出异常。
您还可以在类级别指定默认绑定行为,然后使用[BindingBehavior]逐个属性重写它:
[BindingBehavior(BindingBehavior.Required)]
public class Model
{
[BindNever]
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
因此,我们有三种情况:
- 如果请求中存在值,则将其绑定到模型(
[Bind]。 - 忽略模型中传递的任何值(
[BindNever]。 - 要求在请求中传递值(
[BindRequired]。
我们还应该提到,这些属性可以应用于动作方法参数,如下所示:
public IActionResult Process(string id, [BindRequired] int state) { ... }
取消请求
有时,客户端会取消请求,例如当有人关闭浏览器、导航到另一个页面或刷新页面时。问题是,您不知道它发生了,并且您继续执行您的操作方法,而不知道答案将被丢弃。为了在这些场景中提供帮助,ASP.NET Core 允许您添加一个CancelationToken类型的参数。这是允许取消.NET 和.NET Core 中异步任务的标准方法。其工作原理如下:
public async Task<IActionResult> Index(CancelationToken cancel) { ... }
如果 ASP.NET Core 主机(Kestrel、WebListener)出于任何原因检测到客户端已断开连接,则会触发取消令牌(其IsCancelationRequested设置为true,与HttpContext.RequestAborted相同)。一个好处是,您可以将此CancelationToken实例传递给您可能正在使用的任何异步方法(例如,HttpClient.SendAsync()、DbSet<T>.ToListAsync()等),它们也将随客户端请求一起取消!
模型验证
一旦正确构建了模型(传递给 action 方法的参数)并设置了其属性值,就可以对其进行验证。验证本身是可配置的。
从所有值提供程序获得的所有值都可以在ControllerBase类中定义的ModelState属性中使用。对于任何给定类型,IsValid属性将说明 ASP.NET 是否根据其配置的验证器认为模型有效。
默认情况下,注册的实现依赖于注册的模型元数据和模型验证器提供程序,其中包括DataAnnotationsModelValidatorProvider。这将针对System.ComponentModel.DataAnnotationsAPI 执行验证,即从ValidationAttribute(RequiredAttribute、RegularExpressionAttribute、MaxLengthAttribute等)派生的所有类,以及IValidatableObject实现。这是.NET 中事实上的验证标准,能够处理大多数情况。
填充模型时,它也会自动验证,但您也可以通过在操作中调用TryValidateModel方法明确请求模型验证,例如,如果您更改了其中的任何内容:
public IActionResult Process(Model model)
{
if (this.TryValidateModel(model))
{
return this.Ok();
}
else
{
return this.Error();
}
}
自 ASP.NET Core 2.1 以来,您可以将验证属性应用于动作参数本身,并且也可以对它们进行验证:
public IActionResult Process([Required, EmailAddress] string email) { ... }
如前所述,ModelState将根据验证结果设置IsValid属性,但我们也可以强制重新验证。如果要检查特定属性,可以使用TryValidateModel的重载,该重载包含一个额外的字符串参数:
if (this.TryValidateModel(model, "Email")) { ... }
在幕后,所有注册的验证器都被调用,该方法将返回一个带有所有验证结果的布尔标志。
我们将在下一章中重新讨论模型验证。现在,让我们看看如何插入自定义模型验证器。我们在ConfigureServices中使用AddMvc方法进行此操作:
services.AddMvc(options =>
{
options.ModelValidatorProviders.Add(new
CustomModelValidatorProvider());
});
CustomModelValidatorProvider如下所示:
public class CustomModelValidatorProvider : IModelValidatorProvider
{
public void CreateValidators(ModelValidatorProviderContext context)
{
context.Results.Add(new ValidatorItem { Validator =
new CustomModelValidator() });
}
}
主要逻辑简单地放在CustomModelValidator中:
public class CustomObjectModelValidator : IModelValidator
{
public IEnumerable<ModelValidationResult>
Validate(ModelValidationContext context)
{
if (context.Model is ICustomValidatable)
{
//supply custom validation logic here and return a collection
//of ModelValidationResult
}
return Enumerable.Empty<ModelValidationResult>();
}
}
亲爱的读者,ICustomValidatable接口(和实现)留给您作为练习。希望这不会太难理解。
此ICustomValidatable实现应查看其类的状态,并针对发现的任何问题返回一个或多个ModelValidationResults。
由于 ASP.NET Core 2.1,[ApiController]属性为控制器(通常为 API 控制器)添加了一个约定,该约定在调用动作方法时自动触发模型验证。您可以使用它,但它所做的是返回一个 400 HTTP 状态码(https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400 )和 JSON 格式的验证错误说明,这可能不是处理视图时需要的。您可以出于同样的目的使用动作过滤器;让我们看一个例子:
[Serializable]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
AllowMultiple = false,
Inherited = true)]
public sealed class ValidateModelStateAttribute : ActionFilterAttribute
{
public ValidateModelStateAttribute(string redirectUrl)
{
this.RedirectUrl = redirectUrl;
}
public ValidateModelStateAttribute(
string actionName,
string controllerName = null,
object routeValues = null)
{
this.ControllerName = controllerName;
this.ActionName = actionName;
this.RouteValues = routeValues;
}
public string RedirectUrl { get; }
public string ActionName { get; }
public string ControllerName { get; }
public object RouteValues { get; }
public override Task OnResultExecutionAsync(ResultExecutingContext
context, ResultExecutionDelegate next)
{
if (!context.ModelState.IsValid)
{
if (!string.IsNullOrWhiteSpace(this.RedirectUrl))
{
context.Result = new RedirectResult(this.RedirectUrl);
}
else if (!string.IsNullOrWhiteSpace(this.ActionName))
{
context.Result = new RedirectToActionResult
(this.ActionName, this.ControllerName,
this.RouteValues);
}
else
{
context.Result = new BadRequestObjectResult
(context.ModelState);
}
}
return base.OnResultExecutionAsync(context, next);
}
}
这是一个操作筛选器,也是一个属性,这意味着它可以全局注册:
services.AddMvc(options =>
{
options.AllowValidatingTopLevelNodes = true;
options.Filters.Add(new ValidateModelStateAttribute("/Home/Error"));
});
也可以通过将属性添加到控制器类或操作方法来注册它。此类提供两个控制器:
- 一个用于将重定向指定为完整 URL
- 另一个用于使用控制器名称、操作方法以及可能的路由参数
它继承自ActionFilterAttribute,后者依次实现IActionFilter和IAsyncActionFilter。在这里,我们对异步版本感兴趣——这是一个好的实践,意味着我们重写了OnResultExecutionAsync。在将控件传递给 action 方法之前调用此方法,这里我们检查模型是否有效。如果不是,则根据类的实例化方式将其重定向到正确的位置。
顺便说一下,只有当AllowValidatingTopLevelNodes属性设置为true时,控制器属性才会被验证,如本例所示;否则,将忽略任何错误。
行动结果
操作处理请求,通常向调用客户端返回内容或 HTTP 状态代码。在 ASP.NET Core 中,广义而言,有两种可能的返回类型:
IActionResult的一个实现- 任何.NETPOCO 类
IActionResult的实现包装了实际响应,加上内容类型头和 HTTP 状态码,通常很有用。此接口仅定义一个方法ExecuteResultAsync,该方法采用一个ActionContext类型的参数,该参数封装了描述当前请求的所有属性:
ActionDescriptor:描述要调用的动作方法HttpContext:描述请求上下文ModelState:描述提交的模型属性及其验证状态RouteData:描述路由参数
所以您可以看到,IActionResult实际上是命令设计模式(的一个实现 https://sourcemaking.com/design_patterns/command )在实际执行的意义上,它不仅仅存储数据。返回字符串和 HTTP 状态代码200的IActionResult的一个非常简单的实现可能如下所示:
public class HelloWorldResult : IActionResult
{
public async Task ExecuteResultAsync(ActionContext actionContext)
{
actionContext.HttpContext.Response.StatusCode = StatusCodes
.Status200OK;
await actionContext.HttpContext.Response.WriteAsync("Hello,
World!");
}
}
正如我们将很快看到的,IActionResult现在是描述 HTML 结果以及 API 样式结果的接口。ControllerBase和Controller类为返回IActionResult实现提供了以下方便的方法:
-
BadRequest(BadRequestResult、HTTP 代码400:请求无效。 -
Challenge(ChallengeResult、HTTP 代码401:身份验证的挑战。 -
Content(ContentResult、HTTP 代码200:任何内容。 -
Created(CreatedResult、HTTP 代码201:表示已创建资源的结果。 -
CreatedAtAction(CreatedAtActionResult、HTTP 代码201:表示某个操作创建了资源的结果。 -
CreatedAtRoute(CreatedAtRouteResult、HTTP 代码201:表示在命名路由中创建了资源的结果。 -
File(VirtualFileResult、FileStreamResult、FileContentResult、HTTP 码200)。 -
Forbid(ForbidResult、HTTP 代码403。 -
LocalRedirect(LocalRedirectResult、HTTP 代码302:重定向到本地资源。 -
LocalRedirectPermanent(LocalRedirectResult、HTTP 代码301:对本地资源的永久重定向。 -
NoContent(NoContentResult、HTTP 代码204:无需部署内容。 -
NotFound(NotFoundObjectResult、HTTP 代码404:未找到资源。 -
Ok(OkResult、HTTP 代码200:可以。 -
无方法(
PartialViewResult,HTTP 代码200:不支持请求的 HTTP 方法。 -
PhysicalFile(PhysicalFileResult、HTTP 代码200:物理文件的内容。 -
Redirect(RedirectResult、HTTP 代码302:重定向到绝对 URL。 -
RedirectPermanent(RedirectResult、HTTP 代码301:永久重定向到绝对 URL。 -
RedirectToAction(RedirectToActionResult、HTTP 代码302:重定向到本地控制器的动作。 -
RedirectToActionPermanent(RedirectToActionResult、HTTP 代码301:对本地控制器动作的永久重定向。 -
RedirectToPage(RedirectToPageResult、HTTP 代码302、来自 ASP.NET Core 2):重定向到本地 Razor 页面。 -
RedirectToPagePermanent(RedirectToPageResult、HTTP 代码301:永久重定向到本地剃须刀页面。 -
RedirectToPagePermanentPreserveMethod****RedirectToPageResult、HTTP 代码301:永久重定向到本地页面,保留原始请求的 HTTP 方法。 -
RedirectToPagePreserveMethod****RedirectToPageResult、HTTP 代码302:重定向到本地页面。 -
RedirectToRoute(RedirectToRouteResult、HTTP 代码302:重定向到指定路由。 -
RedirectToRoutePermanent(RedirectToRouteResult、HTTP 代码301:永久重定向到指定路由。 -
SignIn``SignInResult:签到。 -
SignOut``SignOutResult:退出。 -
StatusCode(StatusCodeResult、ObjectResult、任意 HTTP 代码)。 -
无方法(
UnsupportedMediaTypeResult、HTTP 代码415:接受的内容类型与可以返回的内容类型不匹配。 -
Unauthorized(UnauthorizedResult、HTTP 代码401:不允许请求资源。 -
View(ViewResult、HTTP 代码200、在Controller类中声明):一个视图。 -
ViewComponent(ViewComponentResult、HTTP 代码200:调用视图组件的结果。
其中一些结果还指定了内容类型,例如,ContentResult将默认返回text/plain(可以更改),JsonResult将返回application/json等等。有些名字是不言自明的;其他人可能需要一些澄清:
Redirect方法通常有四个版本,一个用于临时重定向,一个用于永久重定向,另外两个版本也保留了原始的请求 HTTP 方法。可以重定向到任意 URL、特定控制器操作的 URL、Razor 页面 URL 和本地(相对)URL。- 重定向中的 preserve 方法意味着浏览器发出的新请求将保留原始 HTTP 谓词。
File和Physical文件方法提供了几种返回文件内容的方法,可以通过 URL、Stream、字节数组或物理文件位置。Physical方法允许您直接从文件系统位置发送文件,这可能会提高性能。您还可以选择在希望传输的内容上设置ETag或LastModified日期。ViewResult和PartialViewResult的不同之处在于后者只寻找局部视图。- 某些方法可能会返回不同的结果,这取决于所使用的重载(当然还有它的参数)。
SignIn、SignOut、Challenge与认证相关,未配置无意义。SignIn将重定向到配置的登录 URL,SignOut将清除身份验证 cookie。- 并非所有这些结果都返回内容;其中一些只返回状态码和一些头(例如,
SignInResult、SignOutResult、StatusCodeResult、UnauthorizedResult、NoContentResult、NotFoundObjectResult、ChallengeResult、BadRequestResult、ForbidResult、OkResult、CreatedResult、CreatedAtActionResult、CreatedAtRouteResult以及所有的[T13 结果)。另一方面,JsonResult、ContentResult、VirtualFileResult、FileStreamResult、FileContentResult、ViewResult所有返回内容。
所有返回视图(ViewResult或部分视图(PartialViewResult的操作结果类)都采用Model属性,其原型为object。您可以使用它向视图传递任意数据,但请记住,视图必须声明兼容类型的模型。遗憾的是,您无法传递匿名类型,因为视图将无法定位其属性。在第 6 章使用表单和模型中,我将提出一个解决方案。
返回操作结果可能是控制器最典型的用法,但也可以返回任何.NET 对象。为此,必须声明方法以返回所需的任何类型:
public string SayHello()
{
return "Hello, World!";
}
这是一种完全有效的行动方法;但是,有几件事你需要知道:
- 在调用任何筛选器之前(例如,
IActionFilter、IResultFilter),将返回的对象包装在一个ObjectResult中。 - 该对象由配置的输出格式化程序之一格式化(序列化),第一个表示可以处理该对象。
- 如果要更改响应的状态代码或内容类型,则需要使用
HttpContext.Response对象。
为什么返回一个 POCO 类或一个ObjectResult?好的,ObjectResult给了你一些额外的优势:
- 您可以提供输出格式化程序的集合(
Formatters集合)。 - 您可以告诉它使用内容类型的选择(
ContentTypes。 - 您可以指定要返回的状态码(
StatusCode。
让我们更详细地了解有关 API 操作的输出格式化程序。现在,让我们看一个示例操作结果,它以 XML 形式返回内容:
public class XmlResult : ActionResult
{
public XmlResult(object value)
{
this.Value = value;
}
public object Value { get; }
public override Task ExecuteResultAsync(ActionContext context)
{
if (this.Value != null)
{
var serializer = new XmlSerializer(this.Value.GetType());
using (var stream = new MemoryStream())
{
serializer.Serialize(stream, this.Value);
var data = stream.ToArray();
context.HttpContext.Response.ContentType =
"application/xml";
context.HttpContext.Response.ContentLength = data.Length;
context.HttpContext.Response.Body.Write(data, 0,
data.Length);
}
}
return base.ExecuteResultAsync(context);
}
}
在这段代码中,我们实例化了一个绑定到要返回的值类型的XmlSerializer实例,并使用它将该值序列化为字符串,然后将其写入响应。您需要为XmlSerializer类添加对System.Xml.XmlSerializerNuGet 包的引用。这进一步导致操作的重定向和流化。让我们看看这些是什么。
重定向
当服务器在收到客户端(浏览器)的请求后指示客户端转到另一个位置时,会发生重定向:

至少有 10 种方法可以实现重定向。此处的更改是返回给客户端的 HTTP 状态代码以及重定向 URL 的生成方式。我们为以下内容提供了重定向:
- 特定 URL,完整或本地:
Redirect - 本地 URL:
LocalRedirect - 命名路由:
RedirectToRoute - 特定控制器及动作:
RedirectToAction - 剃须刀页面(更多信息参见第 7 章、实现剃须刀页面:
RedirectToPage
**所有这些方法都返回 HTTP 状态码 302(参见https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302 ),这是一个临时重定向。然后我们有发送 HTTP 301(的替代版本 https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/301 ),一个永久重定向,这意味着浏览器被指示缓存响应,并了解当被要求转到原始 URL 时,他们应该访问新的 URL。这些方法与前面的方法类似,但以Permanent结尾:
- 特定 URL:
RedirectPermanent - 本地 URL:
LocalRedirectPermanent - 命名路由:
RedirectToRoutePermanent - 特定控制器及动作:
RedirectToActionPermanent - 剃须刀页面(更多信息参见第 7 章、实现剃须刀页面:
RedirectToPagePermanent****
****然后还有另一个变体,它保留了原来的 HTTP 动词,并且基于 HTTP 308(https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/308 )。例如,可能是浏览器试图使用 HTTPPOST访问资源,服务器返回 HTTP 状态 308,并重定向到另一 URL;然后,客户端必须使用POST而不是GET再次请求此 URL,这是其他代码的情况。在这种情况下,我们还有其他变化:
- 特定 URL:
RedirectPermanentPreserveMethod - 本地 URL:
LocalRedirectPreserveMethod - 命名路由:
RedirectToRoutePermanentPreserveMethod - 特定控制器及动作:
RedirectToActionPermanentPreserveMethod - 剃须刀页面(更多信息请参见第 7 章、中的涂抹剃须刀页面
RedirectToPagePermanentPreserveMethod**
****### 流动
如果需要将内容流式传输到客户端,则应使用FileStreamResult类。在以下示例代码中,我们正在流式传输 MP4 文件:
[HttpGet("[action]/{name}")]
public async Task<FileStreamResult> Stream(string name)
{
var stream = await System.IO.File.OpenRead($"{name}.mp4");
return new FileStreamResult(stream, "video/mp4");
}
请注意,ControllerBase或Controller类中没有返回FileStreamResult的方法,因此您需要自己构建它,将流和所需的内容类型传递给它。这将保持客户端连接,直到传输结束或浏览器导航到另一个 URL。
现在让我们看看我们能做些什么来处理错误。
错误处理
在上一章中,我们看到了在发生错误时如何重定向到特定操作。另一种选择是利用IExceptionFilter和IAsyncExceptionFilter接口(过滤器类之一),让控制器本身或其他类直接实现错误处理。
在我们的控制器中,只需实现IExceptionFilter类,它只有一个方法OnException:
public void OnException(ExceptionContext context)
{
var ex = context.Exception;
//do something with the exception
//mark it as handled, so that it does not propagate
context.ExceptionHandled = true;
}
在异步版本IAsyncExceptionFilter中,OnExceptionAsync方法采用相同的参数,但必须返回一个Task。
在章节10了解过滤器中,我们将了解更多关于过滤器的概念。现在,只要说如果在实现IExceptionFilter的控制器中的操作引发任何异常,就会调用其OnException方法。
Don't forget to set ExceptionHandled to true if you don't want the exception to propagate!
下一个主题与性能相关:响应缓存。
响应缓存
任何类型的操作响应(例如 HTML 或 JSON)都可以缓存在客户机中以提高性能。不用说,只有当它返回的结果很少改变时,这种情况才会发生。这在RFC 7234、HTTP/1.1 缓存(中有规定 https://tools.ietf.org/html/rfc7234 。本质上,响应缓存是一种机制,通过该机制,服务器通知客户机(浏览器或客户机 API)将返回的 URL 响应(包括标题)保留一段时间,并在此期间将其用于 URL 的所有后续调用。只有GETHTTP 谓词可以缓存,因为它被设计为幂等:PUT、POST、PATCH或DELETE不能缓存。
我们在ConfigureServices中增加了对资源缓存的支持,具体如下:
services.AddResponseCaching();
我们在Configure中使用它,它基本上将响应缓存中间件添加到 ASP.NET Core 管道中:
app.UseResponseCaching();
我们还可以在对AddResponseCaching的调用中设置几个选项,例如:
MaximumBodySize(int):这是可以存储在客户端响应缓存中的响应的最大大小;默认值为 64 KB。UseCaseSensitivePaths(bool):可以配置缓存密钥的请求 URL 是否区分大小写;默认值为false。
这些可使用AddResponseCaching方法的过载:
services.AddResponseCaching(options =>
{
options.MaximumBodySize *= 2;
options.UseCaseSensitivePaths = true;
});
我们还可以通过将[ResponseCache]属性应用于动作或整个控制器类来缓存动作结果。接下来,我们有两个选项,可以直接在属性中指定每个缓存参数,也可以告诉它使用缓存配置文件。
方案如下:
Duration(int:缓存的秒数;默认值为0Location(ResponseCacheDuration):缓存的位置(Client、None、Any;默认值为AnyNoStore(bool:是否阻止结果的存储;默认值为falseVaryByHeader(string:缓存结果实例的头的逗号分隔列表;默认值为nullVaryByQueryKeys(string []:缓存结果实例的查询字符串参数列表;默认值为nullCacheProfileName(string:缓存配置文件名称,与其他选项不兼容;默认值为空
如前所述,您可以指定所有单个选项(或至少指定您需要的选项),也可以指定缓存配置文件名称。缓存配置文件通过AddMvc扩展方法在ConfigureServices方法的Startup定义,如下所示:
services.AddMvc(options =>
{
options.CacheProfiles.Add("5minutes", new CacheProfile
{
Duration = 5 * 60,
Location = ResponseCacheLocation.Any,
VaryByHeader = "Accept-Language"
});
});
此缓存配置文件指定结果保留五分钟,不同实例对应不同的Accept-Language头值。在此之后,您只需指定名称5minutes:
[ResponseCache(CacheProfileName = "5minutes")]
public IActionResult Cache() { ... }
VaryByHeader和VaryByQueryKeys属性(如果它们有值)将为请求头或查询字符串参数(或两者)的每个值保留相同缓存响应的不同实例。例如,如果您的应用支持多种语言,并且您使用Accept-LanguageHTTP 头来指示应该使用哪种语言,则结果会针对每种请求的语言保存在缓存中—一种用于pt-PT,另一种用于en-GB,依此类推。
通常最好使用缓存配置文件,而不是在属性中提供所有参数。
现在让我们看看如何在后续请求之间保持状态。
维护国家
如果您需要在同一请求中或跨多个请求维护从一个组件到另一个组件的状态,该怎么办?Web 应用传统上为此提供解决方案。让我们探讨一下我们的选择。
使用请求
存储在请求(内存中)中的任何对象都将在整个请求期间可用。条目是HttpContext类中的强类型词典:
this.HttpContext.Items["timestamp"] = DateTime.UtcNow;
您可以在访问该项之前检查该项是否存在;值得注意的是,以下内容区分大小写:
if (this.HttpContext.Items.ContainsKey("timestamp")) { ... }
当然,您也可以删除项目:
this.HttpContext.Items.Remove("timestamp");
使用表单数据
Form集合跟踪 HTMLFORM提交的所有值,通常在POST请求之后。要访问它,请使用HttpContext的Request对象的Form属性:
var isChecked = this.HttpContext.Request.Form["isChecked"].Equals("on");
您可以通过首先检查值的存在(不区分大小写)进行防御性编程:
if (this.HttpContext.Request.Form.ContainsKey("isChecked")) { ... }
可以获得多个值,在这种情况下,可以对它们进行计数并获得它们的所有值:
var count = this.HttpContext.Request.Form["isChecked"].Count;
var values = this.HttpContext.Request.Form["isChecked"].ToArray();
使用查询字符串
通常,您不会在查询字符串中存储数据,而是从中获取数据,例如,http://servername.com?isChecked=true。Query集合跟踪 URL 中作为字符串发送的所有参数:
var isChecked = this.HttpContext.Request.Query["isChecked"].Equals("true");
为了检查是否存在值,我们使用以下方法:
if (this.HttpContext.Request.Query.ContainsKey("isChecked")) { ... }
这也支持多个值:
var count = this.HttpContext.Request.Query["isChecked"].Count;
var values = this.HttpContext.Request.Query["isChecked"].ToArray();
使用路线
与查询字符串方法一样,您通常只从路由中获取值,而不向其写入;但是,IUrlHelper接口中确实有方法,通常可以通过生成动作 URL 的ControllerBase类的Url属性访问该接口,从中可以打包任意值。
路由参数如http://servername.com/admin/user/121所示,使用[controller]/[action]/{id}路由模板。
要获取路由参数(字符串),请执行以下操作:
var id = this.RouteData.Values["id"];
要检查它是否存在,请使用以下命令:
if (this.RouteData.ContainsKey("id")) { ... }
使用 cookies
Cookie 已经存在很长时间了,它是 web 上许多功能的基础,例如身份验证和会话。它们在 RFC 6265(中规定 https://tools.ietf.org/html/rfc6265 )。本质上,它们是在客户端存储少量文本的一种方式。
你可以读写 cookies。要读取 cookie 值,您只需要知道它的名称;其值将以字符串形式出现:
var username = this.HttpContext.Request.Cookies["username"];
当然,您还可以通过以下方式检查 cookie 是否存在:
if (this.HttpContext.Request.Cookies.ContainsKey("username")) { ... }
要将 cookie 作为响应的一部分发送到客户端,您需要更多的信息,即以下信息:
-
Name(string:一个名字(还有什么?) -
Value(string:字符串值 -
Expires(DateTime):可选的到期时间戳(默认情况下 cookie 基于会话,这意味着一旦浏览器关闭,cookie 将消失) -
Path(string:cookie 可用的可选路径(默认为/) -
Domain(string:可选域(默认为当前完全限定的主机名) -
Secure(bool:一个可选的安全标志,如果存在,将导致 cookie 仅在使用 HTTPS 服务请求时可用(默认为false) -
HttpOnly(bool:另一个可选标志,指示 cookie 是否可被客户端浏览器上的 JavaScript 读取(默认值也是false)
我们向请求对象添加 cookie,如下所示:
this.HttpContext.Response.Cookies.Append("username", "rjperes", new CookieOptions
{
Domain = "packtpub.com",
Expires = DateTimeOffset.Now.AddDays(1),
HttpOnly = true,
Secure = true,
Path = "/"
});
第三个参数,CookieOptions类型是可选的,在这种情况下,cookie 采用默认值。
撤销 cookie 的唯一方法是添加一个具有相同名称和过期日期的 cookie。
You mustn't forget that there is a limit to the number of cookies you can store per domain, as well as a limit to the actual size of an individual cookie value; these shouldn't be used for large amounts of data. For more information, please consult RFC 6265.
使用会话
会话是一种为每个客户端持久化数据的方法。通常,会话依赖于 cookie,但也可能(但容易出错)使用查询字符串参数,ASP.NET Core 不支持这种开箱即用的方式。在 ASP.NET Core 中,会话是选择性加入的;换句话说,它们需要明确添加。我们需要添加 NuGet 包Microsoft.AspNetCore.Session并在Startup类的Configure和ConfigureServices方法中显式添加支持:
public void ConfigureServices(IServiceCollection services)
{
services.AddSession();
//rest goes here
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseSession();
//rest goes here
}
之后,Session对象在HttpContext实例中可用:
var value = this.HttpContext.Session.Get("key"); //byte[]
更好的方法是使用GetString扩展方法并序列化/反序列化为 JSON:
var json = this.HttpContext.Session.GetString("key");
var model = JsonSerializer.Deserialize<Model>(json);
这里,Model只是一个 POCO 类,JsonSerializer 是System.Text.Json中的一个类,它有静态方法来序列化和反序列化 JSON 字符串。
要在会话中存储值,我们使用Set或SetString方法:
this.HttpContext.Session.Set("key", value); //value is byte[]
JSON 方法如下所示:
var json = JsonSerializer.Serialize(model);
this.HttpContext.Session.SetString("key", json);
通过将值设置为null或调用Remove来实现删除。与GetString和SetString类似,还有GetInt32和SetInt32扩展方法。使用最适合您需要的方法,但不要忘记数据始终存储为字节数组。
如果要检查会话中是否存在值,应使用TryGetValue方法:
byte[] data;
if (this.HttpContext.Session.TryGetValue("key", out data)) { ... }
这就是将会话用作通用词典的主要原因。现在是,配置时间!您可以在SessionOptions对象中设置一些值,主要围绕用于存储会话的 cookie 以及空闲时间间隔:
services.AddSession(options =>
{
options.CookieDomain = "packtpub.com";
options.CookieHttpOnly = true;
options.CookieName = ".SeSsIoN";
options.CookiePath = "/";
options.CookieSecure = true;
options.IdleTimeout = TimeSpan.FromMinutes(30);
});
也可以在Configure中的UseSession方法中配置:
app.UseSession(new SessionOptions { ... });
最后要注意的一点是,会话在默认情况下将使用内存存储,这不会使其在现实生活中的应用中具有过度的弹性或有用性;但是,如果在调用AddSession之前注册了分布式缓存提供程序,则会话将使用该提供程序!所以,让我们看看下一个主题,看看如何配置它。
在继续之前,我们需要记住以下几点:
- 在会话中存储对象会有一点性能损失。
- 如果达到空闲超时,对象可能会从会话中退出。
- 在会话中访问对象会延长其生存期,即重置其空闲超时。
使用缓存
与以前版本的 ASP.NET 不同,不再内置对缓存的支持;与.NETCore 中的大多数内容一样,它仍然可用,但作为一种可插入服务。.NET Core 中基本上有两种缓存:
- 内存缓存中,由
IMemoryCache接口表示 - 分布式缓存,使用
IDistributedCache接口
ASP.NET Core 包括一个默认的IMemoryCache实现和一个IDistributedCache实现。分布式实现需要注意的是,它也在内存中,只用于测试,但好的是有几种实现可用,比如 Redis(https://redis.io/ 或 SQL Server。
In-memory and distributed caches can be used simultaneously, as they are unaware of each other.
分布式缓存和内存缓存都将实例存储为字节数组(byte[]),但一个好的解决方法是首先将对象转换为 JSON,然后使用处理字符串的方法扩展,如下所示:
var json = JsonSerializer.Serialize(model);
var model = JsonSerializer.Deserialize<Model>(json);
内存缓存
要使用内存缓存,您需要使用以下默认选项在ConfigureServices中注册其服务:
services.AddMemoryCache();
如果愿意,还可以使用重载扩展方法对其进行微调,该方法采用MemoryCacheOptions实例:
services.AddMemoryCache(options =>
{
options.Clock = new SystemClock();
options.CompactOnMemoryPressure = true;
options.ExpirationScanFrequency = TimeSpan.FromSeconds(5 * 60);
});
这些财产的用途如下:
Clock(ISystemClock):这是ISystemClock的一个实现,将用于到期计算。它对于单元测试和模拟非常有用;没有默认设置。CompactOnMemoryPressure(bool:用于在可用内存过低时从缓存中删除最旧的对象;默认值为true。ExpirationScanFrequency(TimeSpan):设置.NET Core 用于确定是否从缓存中删除对象的时间间隔;默认值为一分钟。
为了使用内存缓存,我们需要从依赖项注入中检索一个IMemoryCache实例:
public IActionResult StoreInCache(Model model, [FromServices] IMemoryCache cache)
{
cache.Set("model", model);
return this.Ok();
}
我们将在依赖注入**部分中更详细地了解[FromServices]。
IMemoryCache支持您可能期望的所有操作,以及其他一些操作:
CreateEntry:在缓存中创建一个条目,并允许您访问 expirationGet/GetAsync:从缓存中同步或异步检索项目GetOrCreate/GetOrCreateAsync:从缓存中返回项目(如果存在),或同步或异步创建一个项目Set/SetAsync:同步或异步添加或修改缓存中的项目Remove:从缓存中删除项目TryGetValue:尝试同步从缓存中获取项目
差不多就是这样!内存缓存将可用于同一应用中的所有请求,并在应用重新启动或停止后消失。
分布式缓存
分布式缓存的默认开箱即用实现在现实场景中几乎毫无用处,但它可能是一个很好的起点。以下是如何在ConfigureServices中添加对它的支持:
services.AddDistributedMemoryCache();
没有其他选择,只是这样。要使用它,请向依赖项注入容器请求一个IDistributedCache实例:
private readonly IDistributedCache _cache;
public CacheController(IDistributedCache cache)
{
this._cache = cache;
}
public IActionResult Get(int id)
{
return this.Content(this._cache.GetString(id.ToString()));
}
所包含的实现将以与内存缓存完全相同的方式运行,但是对于更严重的用例,也有一些很好的替代方案。它提供的 API 具有以下功能:
Get/GetAsync:从缓存返回一个项目Refresh/RefreshAsync:刷新缓存中的项目,延长其生命周期Remove/RemoveAsync:从缓存中删除项目Set/SetAsync:将项目添加到缓存或修改其当前值
请注意,由于缓存现在是分布式的,同步可能需要一些时间,因此存储在其中的项目可能不会立即对所有客户端可用。
雷迪斯
Redis是一个开源的分布式缓存系统。它的描述超出了本书的范围,但可以说微软已经以Microsoft.Extensions.Caching.RedisNuGet 包的形式为它提供了一个客户端实现。添加此包后,您将获得两个扩展方法,您需要使用这些方法在ConfigureServices中注册两个服务,这将用正确的值替换Configuration和InstanceName属性:
services.AddDistributedRedisCache(options =>
{
options.Configuration = "servername";
options.InstanceName = "Shopping";
});
就这样!现在,无论何时您请求一个IDistributedCache实例,您都会得到一个在下面使用 Redis 的实例。
There is a good introduction to Redis available at https://redis.io/topics/quickstart.
SQL Server
另一种选择是将 SQL Server 用作分布式缓存。Microsoft.Extensions.Caching.SqlServer是增加对其支持的 NuGet 包。您可以在ConfigureServices中添加对它的支持,如下所示:
services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = @"Server=.; Database=DistCache;
Integrated Security=SSPI;";
options.SchemaName = "dbo";
options.TableName = "Cache";
});
其余的都是一样的,所以只要从 DI 那里抓住IDistributedCache就可以了。
ASP.NET Core no longer includes the HttpApplication and HttpApplicationState classes, which is where you could keep state applications. This mechanism had its problems, and it's better if you rely on either an in-memory or distributed cache instead.
使用临时数据
Controller类提供了ITempDataDictionary类型的TempData属性。临时数据是一种在请求中存储项目的方法,以便在下一个请求中仍然可用。它基于提供商,目前有两个提供商可用:
- 饼干(
CookieTempDataProvider) - 会话(
SessionStateTempDataProvider
对于后者,您需要启用会话状态支持。为此,您选择一个提供者,并使用依赖注入框架注册它,通常使用ConfigureServices方法:
//only pick one of these
//for cookies
services.AddSingleton<ITempDataProvider, CookieTempDataProvider>();
//for session
services.AddSingleton<ITempDataProvider, SessionStateTempDataProvider>();
从 ASP.NET Core 2 开始,CookieTempDataProvider已经注册。如果您使用SessionStateTempDataProvider,还需要启用会话。
选择其中一个提供者后,可以向TempData集合添加数据:
this.TempData["key"] = "value";
从以下代码中可以看到,检索和检查存在性非常简单:
if (this.TempData.ContainsKey("key"))
{
var value = this.TempData["key"];
}
通过注册其中一个提供者启用临时数据后,可以使用[SaveTempData]属性。当应用于操作结果返回的类时,它将自动保存在临时数据中。
[TempData]属性如果应用于model类中的属性,将自动将该属性的值保存在临时数据中:
[TempData]
public OrderModel Order { get; set; }
状态维修技术的比较
下表提供了可用于维护请求之间状态的所有不同技术的简单比较:
| 技术 | 可存储对象 | 是安全的 | 为共享 | 正在进行 | 到期日 |
| 要求 | object | 对 | 不 | 对 | 不 |
| 类型 | string | 是(如果使用 HTTPS) | 不 | 对 | 不 |
| 查询字符串 | string | 不 | 对 | 对 | 不 |
| 路线 | string | 不 | 对 | 对 | 不 |
| 曲奇饼 | string | 是(如果仅设置为 HTTPS) | 不 | 不 | 对 |
| 一场 | byte[] | 对 | 不 | 大概 | 对 |
| 隐藏物 | object | 对 | 对 | 大概 | 对 |
| 临时数据 | string | 对 | 不 | 不 | 对 |
不用说,并非所有这些技术都有相同的用途;相反,它们用于不同的场景。
在下一节中,我们将学习如何在控制器内使用依赖项注入。
依赖注入
NET Core 通过其内置的DI框架实例化控制器。由于它完全支持构造函数注入,您可以将任何已注册的服务作为参数注入构造函数:
//ConfigureServices
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
//HomeController
public HomeController(IHttpContextAccessor accessor) { ... }
但是,您也可以通过利用HttpContext.RequestServices属性以服务定位器的方式从 DI 请求服务,如下所示:
var accessor = this.HttpContext.RequestServices.GetService<IHttpContextAccessor>();
For the strongly typed GetService<T> extension method, you need to add a reference to the Microsoft.Extensions.DependencyInjection namespace.
在 action 方法中,您还可以通过使用[FromServices]属性修饰其类型化参数来注入服务,如下所示:
public IActionResult Index([FromServices] IHttpContextAccessor accessor) { ... }
下一个主题涉及一个非常重要的主题,特别是对于那些希望实现多语言站点的人。
全球化与本土化
如果您需要构建一个将由不同国家的人使用的应用,您可能需要将其全部或至少部分翻译。但不仅仅是这样:您可能还希望以用户期望的方式显示十进制数字和货币符号。应用支持不同文化的过程称为全球化,而本地化则是使其适应特定文化的过程,例如,通过以特定语言呈现文本。
与以前的版本一样,ASP.NET Core 完全支持这两个相互交织的概念,它将特定的区域性应用于请求并让其流动,并且能够根据请求者的语言提供字符串资源。
我们首先需要添加对全球化和本地化的支持,我们通过向项目中添加Microsoft.AspNetCore.Localization.Routing包来实现这一点。就本章而言,我们希望能够做到以下几点:
- 为当前请求设置区域性
- 与当前区域性匹配的手动资源字符串
让我们通过调用AddLocalization在ConfigureServices方法中配置本地化。我们将选择Resources文件夹作为资源文件的源,稍后我们将看到:
services.AddLocalization(options =>
{
options.ResourcesPath = "Resources";
});
我们创建这个Resources文件夹,并在其中创建一个Controllers文件夹。我们还可以使用 Visual Studio 创建两个资源文件,一个名为HomeController.en.resx,另一个名为HomeController.pt.resx。resx扩展名是资源文件的标准扩展名,这些文件基本上是包含键值对的 XML 文件。在这些文件中的每个文件上,添加一个带有键Hello和以下值的条目:
| 葡萄牙语 | 英语 |
| 奥拉! | 你好 |
它应该看起来像下面的屏幕截图。请注意,每个文件都有控制器类的名称加上两个字母的区域性标识符:

现在,让我们定义一系列要支持的文化和语言。简单来说,我们将支持葡萄牙语(pt)和英语(英):
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("pt"),
new CultureInfo("en")
};
We are using pt and en, generic culture descriptors, but we could have also used pt-pt and en-gb for specific cultures. Feel free to add these if you want.
然后配置RequestLocalizationOptions以获得默认语言:
services.Configure<RequestLocalizationOptions>(options =>
{
options.DefaultRequestCulture =
new RequestCulture(supportedCultures.First().Name,
supportedCultures.First().Name);
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
options.RequestCultureProviders = new[] {
new AcceptLanguageHeaderRequestCultureProvider { Options =
options } };
});
从浏览器获取区域性的过程基于提供者模型。以下提供程序可用:
AcceptLanguageHeaderRequestCultureProvider从Accept-Language头获取培养基。CookieRequestCultureProvider从饼干中获取文化。QueryStringRequestCultureProvider从查询字符串参数获取区域性。RouteDataRequestCultureProvider从路由参数获取区域性。
只需将前面代码中的RequestCultureProviders赋值替换为您想要的。如您所见,有许多可用选项,每个选项都具有需要设置的不同功能,例如 cookie 名称、查询字符串参数、路由参数名称等:
new CookieRequestCultureProvider { CookieName = "culture" }
new QueryStringRequestCultureProvider { QueryStringKey = "culture" }
new RouteDataRequestCultureProvider { RouteDataStringKey = "culture" }
在第二章中,我们研究了路线约束,因此在这里我们将介绍文化路线约束:
public sealed class CultureRouteConstraint : IRouteConstraint
{
public const string CultureKey = "culture";
public bool Match(
HttpContext httpContext,
IRouter route,
string routeKey,
RouteValueDictionary values,
RouteDirection routeDirection)
{
if ((!values.ContainsKey(CultureKey)) || (values
[CultureKey] == null))
{
return false;
}
var lang = values[CultureKey].ToString();
var requestLocalizationOptions = httpContext
.RequestServices
.GetRequiredService<IOptions<RequestLocalization
Options>>();
if ((requestLocalizationOptions.Value.SupportedCultures
== null)
|| (requestLocalizationOptions.Value.SupportedCultures.
Count == 0))
{
try
{
new System.Globalization.CultureInfo(lang);
//if invalid, throws an exception
return true;
}
catch
{
//an invalid culture was supplied
return false;
}
}
//checks if any of the configured supported cultures matches the
//one requested
return requestLocalizationOptions.Value.SupportedCultures
.Any(culture => culture.Name.Equals(lang, StringComparison
.CurrentCultureIgnoreCase));
}
}
Match方法仅在存在为区域性密钥指定的值时运行;如果是,它将提取其值并检查RequestLocalizationOptions是否为受支持的区域性或是否为有效的区域性。本质上,这允许验证路由值,例如{language:culture},如果该值不是有效的区域性,您将得到一个异常。此路由约束需要先注册,然后才能使用,如下所示:
services.Configure<RouteOptions>(options =>
{
options.ConstraintMap.Add(CultureRouteConstraint.CultureKey, typeof
(CultureRouteConstraint));
});
现在,我们希望控制器响应浏览器的语言设置。例如,在 Chrome 中,我们将在设置|语言|语言和输入设置中配置:

此设置的作用是配置浏览器在每次请求时发送的Accept-LanguageHTTP 头。我们将利用这一点来决定我们将展示什么语言。
我们希望了解本地化的每个控制器需要进行如下更改:
- 添加中间件过滤器属性以注入中间件组件。
- 注入一个字符串定位器,我们可以使用它来获取适当翻译的资源。
下面是它应该是什么样子:
[MiddlewareFilter(typeof(LocalizationPipeline))]
public class HomeController
{
private readonly IStringLocalizer<HomeController> _localizer;
public HomeController(IStringLocalizer<HomeController> localizer)
{
this._localizer = localizer;
}
}
LocalizationPipeline实际上是一个 OWIN 中间件组件,应该如下所示:
public class LocalizationPipeline
{
public static void Configure(
IApplicationBuilder app,
IOptions<RequestLocalizationOptions> options)
{
app.UseRequestLocalization(options.Value);
}
}
现在,如果我们想以特定于区域性的方式访问特定的资源,我们只需执行以下操作:
var hello = this._localizer["Hello"];
返回的字符串将来自源于浏览器的基于当前区域性的正确资源文件。您可以通过查看CultureInfo.CurrentCulture和CultureInfo.CurrentUICulture属性来检查这一点。
最后还有几件事需要注意:
- 每个语言可以有多个资源文件,或者更准确地说,每个特定语言(例如,
en、pt)和通用语言(例如,en-gb、en-us)可以有多个资源文件;如果浏览器请求特定语言(例如,en-gb、en-us),则本地化程序将尝试查找以该语言作为后缀的资源文件,如果找不到,则将尝试通用语言(例如,en。如果此操作也失败,它将返回提供的资源密钥(例如,Hello) - 本地化程序从不返回错误或空值,但您可以使用以下内容检查当前语言是否存在该值:
var exists = this._localizer["Hello"].ResourceNotFound;
这里讨论的主题是非常重要的,如果您要实现需要支持多种文化或语言的站点,但如果您希望在站点中的文本中包含文本(如资源),那么也可以考虑使用它,以便可以很容易地编辑和替换它们。
总结
在本章中,我们看到使用 POCO 控制器并不是真正需要的,它需要做的工作比我们从中获得的任何好处都多,因此我们应该让我们的控制器继承自Controller。
然后我们看到,使用异步操作有助于提高可伸缩性,因为它不会对性能产生太大影响,但您的应用的响应速度会更快。
您可以忘记 XML 格式,因为 JSON 可以完美地工作,并且是在 web 上发送和处理数据的标准方式。
我们学到了应该使用 POCO 类作为我们行为的模型。内置的模型绑定器工作得很好,我们将在后面的章节中看到,但是您可以添加 cookie 值提供程序,因为它可能很方便。
就模型验证而言,我们发现最好坚持使用好的旧数据注释 API。如果需要,您应该在您的模型中实现IValidatableObject。
Redis 分布式缓存系统非常流行,Azure 和 AWS 都支持它。您应该选择 Redis 作为分布式缓存来保存参考数据;换句话说,那些不经常改变的东西。
就性能而言,响应缓存也很有用。产品页面应该不会有太大的变化,所以至少我们可以将其保存在缓存中几个小时。
这是一个很长的章节,我们讨论了控制器和操作,可以说是 ASP.NET Core 最重要的方面。我们还讨论了模型概念的一些部分,如绑定、注入和验证。我们看到了如何维护状态以及可以从操作返回的可能值。我们还学习了如何使用资源进行翻译。其中一些概念将在以后的章节中重新讨论;在下一节中,我们将讨论视图。
问题
您现在应该能够回答以下问题:
- 模型状态的默认验证提供程序是什么?
- 什么是行动?
- 什么是全球化?全球化与本地化有何不同?
- 临时数据的用途是什么?
- 缓存有什么好处?
- 什么是会话?
- 从
Controller基类继承控制器有什么好处?**********
五、视图
在我们从服务器端讨论了应用是如何工作的之后,是时候来看看客户端了。在本章中,我们将介绍模型视图控制器(MVC)应用的视觉方面:视图。
此上下文中的视图是超文本标记语言(HTML和在服务器端执行的代码的组合,其输出在请求结束时被组合并发送给客户端。
为了帮助实现一致性和可重用性,ASP.NET Core 提供了一些非常方便的机制、页面布局和局部视图。此外,由于我们可能希望支持不同的语言和文化,我们有内置的本地化支持,这有助于提供更好的用户体验。
在本章中,我们将学习以下内容:
- 什么是剃须刀视图
- 什么是局部视图
- 什么是视图布局
- 什么是基本的 Razor 视图类
- Razor 如何查找视图文件
- 如何将服务注入到视图中
- 什么是位置扩展器
- 如何执行视图本地化
- 如何在视图上混合使用代码和标记
- 如何在发布时启用视图编译
技术要求
为了实现本章介绍的示例,您需要.NET Core 3软件开发工具包(SDK和某种形式的文本编辑器。当然,VisualStudio2019(任何版本)满足所有要求,但您也可以使用 VisualStudio 代码,例如,或 VisualStudioforMac。
源代码可在从 GitHub 检索 https://github.com/PacktPublishing/Modern-Web-Development-with-ASP.NET-Core-3-Second-Edition 。
开始
视图为MVC中的V。它们是应用的可视部分。通常,web 应用呈现 HTML 页面,即 HTML 视图。视图是一个模板,由 HTML 和一些可能的服务器端内容组成。
ASP.NET Core 使用视图引擎实际呈现视图,这是一种可扩展的机制。在 Core 时代之前,有几个视图引擎可用;尽管它们的目的总是生成 HTML,但它们在语法和支持的功能方面存在细微的差异。目前,ASP.NET Core 仅包含一个名为Razor的视图引擎,因为以前可用的另一个视图引擎 Web 表单已被删除。Razor 已经存在了相当长的一段时间,并且在将其添加到 ASP.NET Core 的过程中得到了改进。
Razor 文件具有cshtml扩展名(用于 C#HTML),按照惯例,它们保存在应用下方名为Views的文件夹中,以及应用它们的控制器名称的文件夹中,例如Home。可能会有全球和本地的观点,我们马上就会了解它们的区别。
让控制器操作返回视图的典型方法是返回执行Controller类的View方法的结果。这就创建了ViewResult,它可以有很多选项,如下所示:
-
ContentType(string:返回给客户端的可选内容类型;text/html是默认值 -
Model(object):就是我们想让视图使用的任何对象 -
StatusCode(int:可选返回状态码;如果没有提供,则为200 -
TempDataITempDataDictionary:强类型临时数据,在下一次请求前可用 -
ViewData(ViewDataDictionary:传递给视图的任意数据的键值集合 -
ViewName(string:要渲染的视图的名称
唯一需要的参数是ViewName,如果不提供,则使用当前动作名称;也就是说,如果我们在名为Index的操作方法中执行,并且我们想要返回一个视图,但不提供其名称,那么将使用Index,如下面的代码片段所示:
public IActionResult Index()
{
return this.View(); //ViewName = Index
}
View方法有一些重载,基本上采用viewName或模型,或两者兼有,如以下代码段所示:
return this.View(
viewName: "SomeView",
model: new Model()
);
Beware—if your model is of the string type, .NET may mistakenly choose the View overload that takes a view name!
现在,假设您想要返回一个具有特定内容类型或状态代码的视图。您可以从View方法调用中获取ViewResult对象,然后对其进行更改,如下所示:
var view = this.View(new Model());
view.ContentType = "text/plain";
view.StatusCode = StatusCodes.Status201Created;
return view;
或者,如果要设置一些视图数据,可以运行以下代码:
view.ViewData["result"] = "success";
有一件事你不能忘记,如果你没有在AddMvc注册你的 MVC 服务,你需要在AddControllersWithViews注册,如下所示:
services.AddControllersWithViews();
这将导致比AddMvc稍小的内存压力,因为它不会注册 Razor 页面所需的服务(不要将它们与 Razor 视图混淆,本章的范围!)。
Razor Pages and Razor views are not the same thing: Razor Pages are callable on their own, whereas Razor views are returned by controller action methods. Razor Pages will be discussed in their own chapter.
让我们继续探索 view 类。
理解观点
Razor 视图实际上是一个模板,它被转换为从RazorPage<T>继承的类。泛型参数实际上是模型的类型,稍后我们将看到。此类继承自RazorPage,它公开了一些有用的属性,如下所示:
IsLayoutBeingRendered(bool:当前是否呈现布局页面BodyContent(IHtmlContent:生成页面的正文内容;将仅在稍后时间可用TempData(ITempDataDictionary:临时数据字典ViewBag(dynamic):访问视图包,该视图包包含任意数据原型dynamicUser(ClaimsPrincipal:当前用户,如HttpContext.User所示Output(TextWriter):输出写入器,一旦页面被处理,HTML 结果将发送到该输出写入器DiagnosticSource(DiagnosticSource):允许记录诊断消息,此处介绍HtmlEncoder(HtmlEncoder:用于在响应中发送结果时对结果进行编码的 HTML 编码器Layout(string:当前布局文件ViewContext(ViewContext:视图上下文Path(string:当前查看文件路径Context(HttpContext:HTTP 上下文
所有这些属性都可以在视图中使用。
当然,我们可以定义我们自己的类,它派生自RazorPage<T>,并让我们的视图使用它,通过使用@inherits,如下所示:
public class MyPage : RazorPage<dynamic>
{
public override Task ExecuteAsync()
{
return Task.CompletedTask;
}
}
唯一需要的方法是ExecuteAsync,但你不必担心。如果现在从该类继承,我们将看到以下内容:
@inherits MyPage
或者,如果我们希望生成的类实现一些接口,我们可以使用@implements关键字,例如IDisposable,如下面的代码片段所示:
@implements IDisposable
@public void Dispose()
{
//do something
}
在这种情况下,我们当然必须自己实现所有接口成员。
理解视图生命周期
当动作发出应渲染视图的信号时,会发生以下情况(以简化的方式):
-
操作返回一个
ViewResult对象,因为ViewResult实现了IActionResult,其ExecuteResultAsync方法被异步调用。 -
默认实现尝试从依赖项注入(DI框架)中查找
ViewResultExecutor。 -
在
ViewResultExecutor上调用FindView方法,该方法使用注入的ICompositeViewEngine,也从 DI 框架中获取,从注册视图引擎列表中获取IView。 -
选择的视图引擎将是
IRazorViewEngine的一个实现(反过来,它扩展了IViewEngine。 -
IView实现使用注册的IFileProviders加载视图文件。 -
然后要求
ViewResultExecutor通过其ExecuteAsync方法调用视图,最终调用基础ViewExecutor的ExecuteAsync方法。 -
ViewExecutor构建并初始化一些基础设施对象,如ViewContext并最终调用IView RenderAsync方法。 -
另一个服务(
ICompilationService用于编译 C#代码。 -
注册的
IRazorPageFactoryProvider创建一个工厂方法,用于创建从IRazorPage继承的.NET 类。 -
IRazorPageActivator传递了新IRazorPage的实例。 -
调用
IRazorPage的ExecuteAsync方法。
在这里,我没有提到过滤器,但正如我所说,它们也在这里,除了动作过滤器。
为什么这很重要?您可能需要实现自己版本的 say-IRazorPageActivator,以便在 Razor 视图中执行一些自定义初始化或 DI,如以下代码块所示:
public class CustomRazorPageActivator : IRazorPageActivator
{
private readonly IRazorPageActivator _activator;
public CustomRazorPageActivator(
IModelMetadataProvider metadataProvider,
IUrlHelperFactory urlHelperFactory,
IJsonHelper jsonHelper,
DiagnosticSource diagnosticSource,
HtmlEncoder htmlEncoder,
IModelExpressionProvider modelExpressionProvider)
{
this._activator = new RazorPageActivator(
metadataProvider,
urlHelperFactory,
jsonHelper,
diagnosticSource, htmlEncoder,
modelExpressionProvider);
}
public void Activate(IRazorPage page, ViewContext context)
{
if (page is ICustomInitializable)
{
(page as ICustomInitializable).Init(context);
}
this._activator.Activate(page, context);
}
}
您只需在ConfigureServices中为IRazorPageActivator服务注册此实现,如下所示:
services.AddSingleton<IRazorPageActivator, CustomRazorPageActivator>();
现在,视图是如何定位的?
定位视图
当要求返回视图(ViewResult时,框架需要首先定位视图文件(.cshtml。
查找视图文件的内置约定如下:
- 视图文件以
cshtml扩展名结尾。 - 视图文件名应与视图名相同,减去扩展名(例如,
Index视图将存储在名为Index.cshtml的文件中)。 - 视图文件存储在
Views文件夹中,并存储在以控制器命名的文件夹中,这些文件从控制器返回,例如Views\Home。 - 全局视图或共享视图直接存储在
Views文件夹中,或存储在Shared文件夹中,例如Views\Shared。
实际上,这是由RazorViewEngineOptions类的ViewLocationFormats集合控制的(Razor 是唯一包含的视图引擎)。默认情况下,它具有以下条目:
/Views/{1}/{0}.cshtml/Views/Shared/{0}.cshtml
The {1} token is replaced by the current controller name and {0} is replaced by the view name. The / location is relative to the ASP.NET Core application folder, not wwwroot.
如果你想让剃须刀引擎看不同的地方,你所需要做的就是告诉它;所以,通过AddRazorOptions方法,也就是通常依次调用AddMvc,在ConfigureServices方法中,如下所示:
services
.AddMvc()
.AddRazorOptions(options =>
{
options.ViewLocationFormats.Add("/AdditionalViews/{0}.cshtml");
});
在ViewLocationFormats集合中按顺序搜索视图位置,直到找到一个文件。
实际查看文件内容通过IFileProviders加载。默认情况下,只注册了一个文件提供程序(PhysicalFileProvider,但可以通过配置添加更多文件提供程序。代码可以在以下代码段中看到:
services
.AddMvc()
.AddRazorOptions(options =>
{
options.FileProviders.Add(new CustomFileProvider());
});
添加自定义文件提供程序可能很有用,例如,如果您希望从非传统位置加载内容,例如数据库、ZIP 文件、程序集资源等。有多种方法可以做到这一点。让我们在下面的小节中尝试它们。
使用视图位置扩展器
有一个高级功能,我们可以根据请求控制搜索视图文件的位置:称为视图位置扩展器。视图位置扩展器是一个剃须刀,因此也可以通过AddRazorOptions进行配置,如下代码段所示:
services
.AddMvc()
.AddRazorOptions(options =>
{
options.ViewLocationExpanders.Add(new ThemesViewLocationExpander
("Mastering"));
});
视图位置扩展器只是实现IViewExpander约定的某个类。例如,假设您想要一个主题框架,该框架将向views搜索路径添加几个文件夹。你可以这样写:
public class ThemesViewLocationExpander : IViewLocationExpander
{
public ThemesViewLocationExpander(string theme)
{
this.Theme = theme;
}
public string Theme { get; }
public IEnumerable<string> ExpandViewLocations(
ViewLocationExpanderContext context,
IEnumerable<string> viewLocations)
{
var theme = context.Values["theme"];
return viewLocations
.Select(x => x.Replace("/Views/", "/Views/" + theme + "/"))
.Concat(viewLocations);
}
public void PopulateValues(ViewLocationExpanderContext context)
{
context.Values["theme"] = this.Theme;
}
}
如我们所见,默认搜索位置如下:
/Views/{1}/{0}.cshtml/Views/Shared/{0}.cshtml
通过添加此视图位置扩展器,对于名为Mastering的主题,这些内容将变为以下内容:
/Views/{1}/{0}.cshtml/Views/Mastering/{1}/{0}.cshtml/Views/Shared/Mastering/{0}.cshtml/Views/Shared/{0}.cshtml
IViewLocationExpander接口只定义了两种方法,如下:
PopulateValues:用于初始化视图位置扩展器;在本例中,我使用它在上下文中传递一些值。ExpandViewLocations:将调用它来检索所需的视图位置。
视图位置扩展器已排队,因此将按照注册顺序依次调用它们;每个ExpandViewLocations方法都将被调用,其中包含前一个方法返回的所有位置。
这两种方法都可以通过context参数访问所有请求参数(HttpContext、RouteData等),因此您可以随心所欲地发挥创意,并根据您能想到的任何理由定义视图的搜索位置。
使用视图引擎
在本章的开头提到 ASP.NET Core 只包括一个视图引擎 Razor,但没有任何东西阻止我们添加更多。这可以通过MvcViewOptions的ViewEngines集合来实现,如下面的代码片段所示:
services
.AddMvc()
.AddViewOptions(options =>
{
options.ViewEngines.Add(new CustomViewEngine());
});
视图引擎是IViewEngine的一个实现,唯一包含的实现是RazorViewEngine。
同样,当 ASP.NET Core 被要求呈现视图时,视图引擎会按顺序搜索,第一个返回视图的引擎就是所使用的引擎。IViewEngine定义的两种方法如下:
FindView(ViewEngineResult:尝试从ActionContext中查找视图GetView(ViewEngineResult):尝试从路径中查找视图
如果找不到视图,两种方法都返回null。
视图是IView的一个实现,RazorViewEngine返回的都是RazorView。IView合同中唯一值得注意的方法是RenderAsync,该方法负责实际呈现ViewContext的视图。
A view engine is not an easy task. You can find a sample implementation written by Dave Paquette in a blog post at: http://www.davepaquette.com/archive/2016/11/22/creating-a-new-view-engine-in-asp-net-core.aspx.
Razor 视图是一个基本上由 HTML 组成的模板,但它也接受相当大的片段,实际上是服务器端 C#代码。考虑它的要求,如下:
- 首先,您可能需要定义视图从控制器接收的模型类型。默认情况下,它是动态的,但您可以使用
@model指令对其进行更改,如下所示:
@model MyNamespace.MyCustomModel
- 这样做与指定视图的基类完全相同。这是通过使用
@inherits实现的,如下所示:
@inherits RazorPage<MyNamespace.MyCustomModel>
Remember: the default is RazorPage<dynamic>. Don't forget: you cannot have @inherits and @model at the same time with different types!
- 如果您不想编写完整的类型名,可以添加任意数量的
@using声明,如以下代码段所示:
@using My.Namespace
@using My.Other.Namespace
- 您可以将 HTML 与 Razor 表达式混合,这些表达式在服务器端处理。剃刀表情总是以
@字符开头。例如,如果要输出当前登录的用户,可以编写以下命令:
User: @User.Identity.Name
- 您可以直接输出任何返回
string或IHtmlContent的方法,如下所示:
@Html.Raw(ViewBag.Message)
- 如果需要计算一些简单代码,则需要将其包含在括号内,如下所示:
Last week: @(DateTime.Today - TimeSpan.FromDays(7))
请记住,如果表达式有空格,则需要将其包含在括号内,唯一的例外是await关键字,如以下代码段所示:
@await Component.InvokeAsync("Process");
- 您可以对 HTML 进行编码(隐式使用
HtmlEncoder属性中提供的HtmlEncoder实例),如下所示:
@("<span>Hello, World</span>")
这将输出一个 HTML 编码的字符串,如以下代码段所示:
<span>Hello, World</span>
更复杂的表达式,例如变量的定义、属性的设置值或不返回严格的结果(string、IHtmlContent)的方法的调用需要放在一个特殊的块中,在这个块中,您可以在.NET 方法中放入几乎任何您想要的内容,如以下代码段所示:
@{
var user = @User.Identity.Name;
OutputUser(user);
Layout = "Master";
}
Sentences inside @{} blocks need to be separated by semicolons.
当然,以这种方式定义的变量可以在声明之后的视图中的任何其他位置使用。
现在让我们来看条件句(if、else if、else和switch,它们没有什么特别之处。请查看以下代码段:
//check if the user issuing the current request is authenticated somehow
@if (this.User.Identity.IsAuthenticated)
{
<p>Logged in</p>
}
else
{
<p>Not logged in</p>
}
//check the authentication type for the current user
@switch (this.User.Identity.AuthenticationType)
{
case "Claims":
<p>Logged in</p>
break;
case null:
<p>Not logged in</p>
break;
}
第一个条件检查当前用户是否经过身份验证,并相应地显示 HTML 块。第二个是switch指令,可以指定多个可能的值;在本例中,我们只看两个,"Claims"和null,这两个条件产生的结果基本上与第一个条件相同。
循环使用一种特殊的语法,您可以将 HTML(任何有效的可扩展标记语言(XML)元素)和代码混合在一起,如以下代码片段所示:
@for (var i = 0; i < 10; i++)
{
<p>Number: @i</p>
}
请注意,这将不起作用,因为Number未包含在 XML 元素中,如以下代码段所示:
@for (var i = 0; i < 10; i++)
{
Number: @i
}
但以下语法(@:可以使用:
@:Number: @i
这使得该行的其余部分被视为 HTML 块。
在foreach和while中可以使用相同的语法。
现在,让我们看一看try/catch块,如下面的代码片段所示:
@try
{
SomeMethodCall();
}
catch (Exception ex)
{
<p class="error">An error occurred: @ex.Message</p>
Log(ex);
}
考虑下面代码片段中所示的 Tyr0T0 和 AuthT1-A.块:
@using (Html.BeginForm())
{
//the result is disposed at the end of the block
}
@lock (SyncRoot)
{
//synchronized block
}
现在,如果要输出@字符,该怎么办?你需要用另一个@来逃避它,就像这样:
<p>Please enter your username @@domain.com</p>
但是 Razor 视图可以识别电子邮件,并且不会强制对其进行编码,如以下代码片段所示:
<input type="email" name="email" value="nobody@domain.com"/>
最后,还支持单行或多行注释,如以下代码段所示:
@*this is a single-line Razor comment*@
@*
this
is a multi-line
Razor comment
*@
在@{}块中,您也可以添加 C#注释,如以下代码片段所示:
@{
//this is a single-line C# comment
/*
this
is a multi-line
C# comment
*/
}
当然,由于视图本质上是 HTML,因此也可以使用 HTML 注释,如以下代码段所示:
<!-- this is an HTML comment -->
The difference between C#, Razor, and HTML comments is that only HTML comments are left by the Razor compilation process; the others are discarded.
我们可以将函数(用面向对象的术语来说,实际上是方法)添加到 Razor 视图中;这些只是.NET 方法,仅在视图范围内可见。要创建它们,我们需要将它们分组到@functions指令中,如下所示:
@functions
{
int Count(int a, int b) { return a + b; }
public T GetValueOrDefault<T>(T item) where T : class, new()
{
return item ?? new T();
}
}
可以指定可见性。默认情况下,这发生在一个称为私有类的类中。指定可见性可能毫无意义,因为生成的类只有在运行时才知道,并且没有简单的方法访问它。
@functions名称实际上有点误导,因为您可以在其中声明字段和属性,如以下代码块所示:
@functions
{
int? _state;
int State
{
get
{
if (_state == null)
{
_state = 10;
}
return _state;
}
}
}
这个例子展示了一个简单的私有字段,它封装在一个属性后面,该属性后面有一些逻辑:第一次访问它时,它将字段设置为默认值;否则,它只返回当前值。
记录和诊断
通常,您可以从 DI 框架获得对ILogger<T>的引用,并在视图中使用它,如下所示:
@inject ILogger<MyView> Logger
但是还有另一个内置机制,DiagnosticSource类和属性,它们在RazorPage基类中声明。通过调用其Write方法,您可以将自定义消息写入诊断框架。这些消息可以是任何.NET 对象,甚至是匿名对象,无需担心其序列化。请查看以下代码段:
@{
DiagnosticSource.Write("MyDiagnostic", new { data = "A diagnostic" });
}
此诊断消息所发生的情况实际上是可配置的。首先,让我们添加Microsoft.Extensions.DiagnosticAdapterNuGet 包,然后为该诊断源生成的事件创建一个自定义侦听器,如下所示:
public class DiagnosticListener
{
[DiagnosticName("MyDiagnostic")]
public virtual void OnDiagnostic(string data)
{
//do something with data
}
}
我们可以针对不同的事件名称添加任意多的侦听器。实际的方法名并不重要,只要它应用了一个与事件名匹配的[DiagnosticName]属性。我们需要通过Configure方法注册并将其挂接到.NET Core 框架,方法是向DiagnosticListener服务添加一个引用,以便我们可以与之交互,如下所示:
public void Configure(IApplicationBuilder app, DiagnosticListener diagnosticListener)
{
var listener = new DiagnosticListener();
diagnosticListener.SubscribeWithAdapter(listener);
//rest goes here
}
请注意,[DiagnosticName]属性中的名称与DiagnosticSource.Write调用匹配,Write调用中匿名类型的名称data与OnDiagnostic方法的参数名称(和类型)匹配。
内置.NET Core 类为以下各项生成诊断:
-
Microsoft.AspNetCore.Diagnostics.HandledException -
Microsoft.AspNetCore.Diagnostics.UnhandledException -
Microsoft.AspNetCore.Hosting.BeginRequest -
Microsoft.AspNetCore.Hosting.EndRequest -
Microsoft.AspNetCore.Hosting.UnhandledException -
Microsoft.AspNetCore.Mvc.AfterAction -
Microsoft.AspNetCore.Mvc.AfterActionMethod -
Microsoft.AspNetCore.Mvc.AfterActionResult -
Microsoft.AspNetCore.Mvc.AfterView -
Microsoft.AspNetCore.Mvc.AfterViewComponent -
Microsoft.AspNetCore.Mvc.BeforeAction -
Microsoft.AspNetCore.Mvc.BeforeActionMethod -
Microsoft.AspNetCore.Mvc.BeforeActionResult -
Microsoft.AspNetCore.Mvc.BeforeView -
Microsoft.AspNetCore.Mvc.BeforeViewComponent -
Microsoft.AspNetCore.Mvc.Razor.AfterViewPage -
Microsoft.AspNetCore.Mvc.Razor.BeforeViewPage -
Microsoft.AspNetCore.Mvc.Razor.BeginInstrumentationContext -
Microsoft.AspNetCore.Mvc.Razor.EndInstrumentationContext -
Microsoft.AspNetCore.Mvc.ViewComponentAfterViewExecute -
Microsoft.AspNetCore.Mvc.ViewComponentBeforeViewExecute -
Microsoft.AspNetCore.Mvc.ViewFound -
Microsoft.AspNetCore.Mvc.ViewNotFound
希望这些名字能不言自明。你为什么要在基于ILogger的机制上使用这种机制?这使得使用强类型方法将侦听器添加到诊断源非常容易。我将在第 12 章、记录、跟踪和诊断中详细介绍两者之间的区别。
视图编译
通常,视图只有在首次使用时才被编译,即控制器操作返回ViewResult。这意味着,只有在框架呈现页面时,才会在运行时捕获任何最终的语法错误;另外,即使没有错误,ASP.NET Core 也需要一些时间(请注意,以毫秒为单位)来编译视图。然而,情况并非如此。
与以前的版本不同,默认情况下,当 Razor 文件更改时,ASP.NET Core 3 不会重新编译视图。为此,您必须重新启动服务器。如果要恢复此行为,需要添加对Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilationNuGet 包的引用,并将以下行添加到services配置中:
services
.AddMvc()
.AddRazorRuntimeCompilation();
或者,您可能更愿意只为应用的调试版本启用此功能,这会将其从生产版本中排除。在这种情况下,您可以这样做:
var mvc = services.AddMvc();
#if DEBUG
mvc.AddRazorRuntimeCompilation();
#endif
或者,对于特定的环境,您可以将IWebHostEnvironment注入您的Startup类中,存储它,并在调用AddRazorRuntimeCompilation之前检查当前环境,如下所示:
public IConfiguration Configuration { get; }
public IWebHostEnvironment Environment { get; }
public Startup(IConfiguration configuration, IWebHostEnvironment environment)
{
this.Configuration = configuration;
this.Environment = environment;
}
var mvc = services.AddMvc();
if (this.Environment.IsDevelopment())
{
mvc.AddRazorRuntimeCompilation();
}
Microsoft 提供了一个 NuGet 软件包,即Microsoft.AspNetCore.Mvc.Razor.ViewCompilation,您可以将其添加为项目的参考。之后,您可以在发布时启用视图编译,目前唯一的方法是手动编辑**.**csproj文件。查找其中声明的第一个<PropertyGroup>实例,即包含<TargetFramework>元素的实例,并添加一个<MvcRazorCompileOnPublish>和一个<PreserveCompilationContext>元素。结果应该如下所示:
<PropertyGroup>
<TargetFramework>netcoreapp3</TargetFramework>
<MvcRazorCompileOnPublish>true</MvcRazorCompileOnPublish>
<PreserveCompilationContext>true</PreserveCompilationContext>
</PropertyGroup>
现在,无论何时使用 Visual Studio 或dotnet publish命令发布项目,都会出现错误。
Do not forget that the precompilation only occurs at publish, not build, time!
为每个视图生成的类公开了一个名为Html的属性,该属性的类型为IHtmlHelper<T>,T是模型的类型。此属性有一些有趣的方法可用于呈现 HTML,如下所示:
- 正在生成链接(
ActionLink、RouteLink) - 给定模型或模型属性的生成表单(
BeginForm、BeginRouteForm、CheckBox、CheckBoxFor、Display、DisplayFor、DisplayForModel、DisplayName、DisplayNameFor、DisplayNameForInnerType、DisplayNameForModel、DisplayText、DisplayTextFor、DropDownList、DropDownListFor、Editor、EditorFor、EditorForModel、EndForm、Hidden、Id、DisplayNameForModel、Id,IdFor、IdForModel、Label、LabelFor、LabelForModel、ListBox、ListBoxFor、Name、NameFor、NameForModel、Password、PasswordFor、RadioButton、RadioButtonFor、TextArea、TextAreaFor、TextBox、TextBoxFor、Value、ValueFor、ValueForModel) - 显示验证消息(
ValidationMessage、ValidationMessageFor、ValidationSummary) - 提供防伪代币(
AntiForgeryToken - 输出原始 HTML(
Raw) - 包括局部视图(
Partial、PartialAsync、RenderPartial、RenderPartialAsync) - 访问上下文属性(
ViewContext、ViewBag、ViewData、TempData以及基类的属性(RazorPage、RazorPage<T>)和属性(UrlEncoder、MetadataProvider) - 一对配置属性(
Html5DateRenderingMode、IdAttributeDotReplacement)
我们将在第 13 章中更详细地研究这些方法,了解测试如何工作。现在,让我们看看如何添加我们自己的扩展(helper)方法。最简单的方法是在IHtmlHelper<T>上添加一个扩展方法,如下面的代码片段所示:
public static HtmlString CurrentUser(this IHtmlHelper<T> html)
{
return new HtmlString(html.ViewContext.HttpContext.
User.Identity.Name);
}
现在,您可以在每个视图中使用它,如下所示:
@Html.CurrentUser()
确保您从它返回string或IHtmlContent;否则,您将无法使用此语法。
我们已经看到,ViewResult类提供了以下三个属性,可用于将数据从操作传递到视图:
- 模型(
Model):在 ASP.NET MVC 的早期,这是唯一可以使用的机制;我们需要定义一个可能相当复杂的类,其中包含我们希望提供的所有数据。 - 视图数据(
ViewData):现在我们有了一个强类型的随机值集合,这在模型中得到了普及。 - 临时数据(
TempData):只有在下一次请求之前才可用的数据。
这些属性最终会传递给RazorPage<T>类中同名的属性。
甚至可以通过设置ViewEngine属性的值来指定视图渲染过程应该使用的视图引擎(一个IViewEngine实例),但这并不常见。通常,这是自动处理的。
向视图传递数据
接下来我们将讨论将数据传递给视图的不同方式。
使用模型
默认情况下,Razor 视图继承自RazorPage<dynamic>,这意味着模型原型为dynamic。
这将是Model属性的类型。这是一个灵活的解决方案,因为您可以在模型中传递您想要的任何内容,但您无法获得 IntelliSense Visual Studio 对它的完整支持。
但是,您可以通过inherits指定强类型模型,如下所示:
@inherits RazorPage<ProcessModel>
这也可以通过使用model指令来实现,如下所示:
@model ProcessModel
这些基本上是相同的。Visual Studio 可帮助您找到其属性和方法,如以下屏幕截图所示:

One thing to keep in mind is that you cannot pass an anonymous type on your controller, as the view won't be able to access its properties. See the next chapter for a solution to this.
使用 ViewBag 属性
视图包(ViewBag属性)是对模型的补充,但在我看来,它早已占据了主导地位。为什么呢?好吧,我想问题在于,每当需要更多属性时,都需要更改模型类,而且在视图包中粘贴新项要容易得多。
使用查看包有两个选项,如下所示:
- 通过运行时不安全的
ViewBag动态属性,如下所示:
<script>alert('@ViewBag.Message');</script>
- 通过
ViewData强类型字典,如下所示:
<script>alert('@ViewData["Message"]');</script>
ViewBag只是ViewData的一个包装器——添加到其中一个的任何内容都可以从另一个中检索,反之亦然。选择ViewData的一个很好的理由是,如果存储数据的名称包含空格或其他特殊字符,如-、/、@等。
使用临时数据
在第 4 章、控制器和动作中解释的临时数据,如果我们需要,可以按照与ViewData类似的方式检索,如下所示:
<script>alert('@TempData["Message"]');</script>
记住,临时数据只存在于next请求的范围内,顾名思义。
接下来,我们将探讨为视图定义公共结构的机制。
了解视图布局
视图布局类似于旧 ASP.NET Web 表单中的母版页。它们定义了一个基本布局,可能还有几个视图可以使用的默认内容,以便最大化、重用并提供一致的结构。在以下屏幕截图中可以看到示例视图布局:

Image taken from https://docs.microsoft.com/en-us/aspnet/core/mvc/views/layout
视图布局本身也是 Razor 视图,可以通过在视图中设置Layout属性来控制它们,该属性在RazorPage基类中定义,如下所示:
@{ Layout = "_Layout"; }
Layout属性只是一个视图的名称,可以用通常的方式发现它。
布局视图中唯一需要的是调用RenderBody方法;这将导致渲染使用它的实际视图。还可以定义节占位符,实际视图可以使用这些占位符来提供内容。一个节由一个RenderSection调用定义,如下面的代码块所示:
<!DOCTYPE html>
<html>
<head><title></title>
@RenderSection("Head", required: false)
</head>
<body>
@RenderSection("Header", required: false)
<div style="float:left">
@RenderSection("LeftNavigation", required: false)
</div>
@RenderBody
<div style="float:right">
@RenderSection("Content", required: true)
</div>
@RenderSection("Footer", required: false)
</body>
</html>
如您所见,RenderSection采用以下两个参数:
- 名称,在布局中必须是唯一的
- 根据区段是否需要,需要参数(默认为
true)
还有异步版本的RenderSection,恰当地命名为RenderSectionAsync。
Unlike ASP.NET Web Forms content placeholders, it is not possible to supply default content on a view layout.
如果某个节是按要求定义的,则使用布局视图的视图页面必须为其声明一个section,如下所示:
@section Content
{
<h1>Hello, World!</h1>
}
如果没有定义节,Razor 编译系统只接受已编译视图并将其内容插入调用RenderBody的位置。
通过执行以下代码,可以检查是否定义了节:
@if (IsSectionDefined("Content")) { ... }
IsLayoutBeingRendered属性告诉我们是否定义、找到布局视图,以及当前是否正在渲染布局视图。
如果您知道某个剖面在视图布局中已按要求定义,但仍不希望渲染该剖面,则可以调用IgnoreSection,如下所示:
@IgnoreSection(sectionName: "Content")
如果出于任何原因,您决定不在视图布局中包含实际视图的任何内容,您可以调用IgnoreBody。
Layouts can be nested—that is, a top-level view can define one layout that also has its own layout, and so on.
接下来,让我们探讨视图类型及其使用方式。
理解局部视图
部分视图类似于常规视图,但其目的是包含在一个中间视图中。语法和功能集完全相同。这个概念类似于 ASP.NET Web 表单中的用户控件,基本上是干式(简称不要重复自己)。通过在局部视图中包装公共内容,我们可以在不同的位置引用它。
有三种方法,可以以同步和异步的方式将部分视图包含在视图的中间。第一种方法涉及到Partial和PartialAsync方法,如下面的代码片段所示:
@Html.Partial("LoginStatus")
@await Html.PartialAsync("LoginStatus")
如果视图中有任何需要异步运行的代码,则可以使用异步版本。
另一种包含部分内容的方式是通过RenderPartial和RenderPartialAsync,如下面的代码片段所示:
@{ Html.RenderPartial("LoginStatus"); }
@{ await Html.RenderPartialAsync("LoginStatus"); }
两者有什么区别?我听到你问。那么,Partial/PartialAsync返回IHtmlContent,这本质上是一个编码字符串,RenderPartial/RenderPartialAsync直接写入底层输出写入程序,可能会导致(稍微)更好的性能。
第三个是使用 ASP.NET Core 2.1 中出现的<partial>标记帮助程序,如以下代码片段所示:
<partial name="Shared/_ProductPartial.cshtml" />
Partial views and view layouts are two different, complementary, mechanisms to allow reuse. They should be used together, not one instead of the other.
让我们看看局部视图是如何工作的。
向局部视图传递数据
Partial和RenderPartial都提供了允许我们传递模型对象的重载,如以下代码片段所示:
@Html.Partial("OrderStatus", new { Id = 100 })
@{ Html.RenderPartial("OrderStatus", new { Id = 100 }); }
当然,OrderStatus视图中声明的模型必须与传递的模型兼容,如果声明为动态(默认),则总是会发生这种情况;如果不是,那么它将抛出异常,所以要小心!
对于Partial/PartialAsync,我们也可以传递其ViewBag的值,如下所示:
@Html.Partial("OrderStatus", new { Id = 100 }, ViewData)
@await Html.PartialAsync("OrderStatus", new { Id = 100 }, ViewData)
这里,我们只是传递当前视图包,但不一定是这样。
Partial views can be nested, meaning that a partial view can include other partial views.
寻找局部视图
局部视图的发现略有不同,原因如下:
- 如果只提供了一个名称(例如,
LoginStatus),则使用与全局视图相同的规则查找视图文件。 - 如果视图名称以
.cshtml(例如,LoginStatus.cshtml)结尾,则仅在与包含视图相同的文件夹中查找视图文件。 - 如果视图名称以
~/或/开头(例如,~/Views/Status/LoginStatus.cshtml),则视图文件将在相对于 web 应用根目录的文件夹中查找(请注意,不是wwwroot文件夹)。 - 如果视图名称以
../开头(例如,../Status/LoginStatus.cshtml),则视图引擎将尝试在相对于调用视图之一的文件夹中查找它。
如果位于不同的文件夹中,则可以存在多个具有相同名称的局部视图。
理解特殊视图文件
ASP.NET Core 可识别两个特殊的视图文件,如果存在,将按如下方式进行特殊处理:
_ViewImports.cshtml:用于指定应用于所有视图的 Razor 指令(@addTagHelper、@removeTagHelper、@tagHelperPrefix、@using、@model、@inherits、@inject,如以下代码片段所示:
@using Microsoft.AspNetCore.Mvc.Razor
@using My.Custom.Namespace
@inject IMyService Service
_ViewStart.cshtml:此处放置的任何代码都将对所有视图执行;因此,它是设置全局通用布局(当然,每个视图都可以覆盖该布局)、通用模型或基本视图页面的好地方,如下所示:
@{ Layout = "_Layout"; }
但也有其他用途,例如:
- 添加
@using指令,以便所有视图都可以访问相同的名称空间 - 添加
@inject指令 - 通过
@addTagHelper注册标签助手 - 定义静态方法(对于 Razor 页面最有用)
VisualStudio 模板将这些文件添加到应用的Views文件夹中。这意味着它们不能正常引用,因为此文件夹位于视图的默认搜索位置之外。
Special files are aware of areas, meaning that if you are using areas and you add one of these files to an area, it will be executed after the global one.
让我们看看可以为视图配置的一些选项。
了解视图选项
作为开发人员,我们会影响视图的某些工作方式,尤其是 Razor 视图。通常,这是通过配置完成的,通过ConfigureServices方法中通常依次调用AddViewOptions和AddRazorOptions扩展方法来完成的,如下面的代码片段所示:
services
.AddMvc()
.AddViewOptions(options =>
{
//global view options
})
.AddRazorOptions(options =>
{
//razor-specific options
});
通过AddViewOptions可以配置MvcViewOptions类的以下属性:
ClientModelValidatorProviders(IList<IClientModelValidatorProvider>:客户端模型验证程序提供者的集合,在客户端验证模型时使用;这将在第 11 章、安全中讨论,但默认情况下,它包括DefaultClientModelValidatorProvider、DataAnnotationsClientModelValidatorProvider和NumericClientModelValidatorProvider。HtmlHelperOptions(HtmlHelperOptions:几个与 HTML 生成相关的选项;下面将讨论这一点。ViewEngines(IList<IViewEngine>:注册的视图引擎;默认情况下,它只包含一个RazorViewEngine实例。
HtmlHelperOptions具有以下特性:
-
ClientValidationEnabled(bool:是否启用客户端验证;默认值为true。 -
Html5DateRenderingMode(Html5DateRenderingMode:在 HTML5 表单字段中将DateTime值呈现为字符串的格式;默认值为Rfc3339,表示DateTime为2017-08-19T12:00:00-01:00。 -
IdAttributeDotReplacement(string):当 MVC 呈现模型的输入字段时,要使用的字符串代替点(.);默认为_。 -
ValidationMessageElement(string:将用于呈现其特定验证消息的 HTML 元素;默认值为span。 -
ValidationSummaryMessageElement(string:用于呈现全局验证摘要的 HTML 元素;默认值为span。
AddRazorOptions方法提供了更特定于 Razor 视图的功能,如下所示:
AdditionalCompilationReferences(IList<MetadataReference>:可以从中加载 ASP.NET Core 元素(控制器、视图组件、标记帮助程序等)的程序集引用集合;默认为空AreaViewLocationFormats(IList<string>:区域文件夹内要搜索的文件夹列表,用于查看;与ViewLocationFormats类似,但适用于区域CompilationCallback(Action<RoslynCompilationContext>:编译每个元素后调用的回调方法;可以安全地忽略,因为它只能由高级开发人员使用CompilationOptions(CSharpCompilationOptions:一组 C#编译选项FileProviders(IList<IFileProvider>:文件提供者的集合;默认情况下,只包含一个PhysicalFileProvider实例ParseOptions(CSharpParseOptions:一组 C#解析选项ViewLocationExpanders(IList<IViewLocationExpander>:视图位置扩展器集合ViewLocationFormats(IList<string>:要搜索视图文件的位置,前面讨论过
通常,MetadataReference是使用MetadataReference类的一种静态方法获得的,如下所示:
var asm = MetadataReference.CreateFromFile("\Some\Folder\MyAssembly.dll");
CSharpCompilationOptions和CSharpParseOptions类非常广泛,主要包括编译器支持的所有设置,甚至包括一些在 Visual Studio 中不容易找到的设置。解释所有这些都会很枯燥,而且非常离题,但我这里只举两个例子:
services
.AddMvc()
.AddRazorOptions(options =>
{
//enable C# 7 syntax
options.ParseOptions.WithLanguageVersion(LanguageVersion.CSharp7);
//add a using declaration for the System.Linq namespace
options.CompilationOptions.Usings.Add("System.Linq");
});
这段代码作为引导过程的一部分运行,它为 Razor 页面设置了一个使用 C#Version7 的选项。它还为System.Linq名称空间添加了一个隐式using语句。
现在,我们将了解如何逻辑(和物理)组织我们的站点功能:区域。
引用应用的基本路径
在第 2 章、配置中描述了基本路径,作为在/以外的路径中托管我们的应用的一种方式。如果需要在视图中获取应用的基本路径,可以使用以下方法:
<script>
var basePath = '@Url.Content("~/")';
</script>
在本例中,我们将配置的基本路径(映射到特殊的~文件夹)存储到 JavaScript 变量。
现在我们了解了布局的工作原理,让我们看看如何使用网站上的区域。
使用区域
区域是您在网站内隔离功能的一种方式。例如,与管理区域相关的任何内容都放在一个地方,例如,一个物理文件夹,包括它自己的控制器、视图等。就视图而言,唯一值得一提的是如何配置可以找到视图文件的路径。这是通过RazorViewEngineOptions类的AreaViewLocationFormats集合控制的,如下面的代码片段所示:
services
.AddMvc()
.AddRazorOptions(options =>
{
options.AreaViewLocationFormats.Add("/SharedGlobal
/Areas/{2}.cshtml");
});
包含的值如下所示:
/Areas/{2}/Views/{1}/{0}.cshtml/Areas/{2}/Views/Shared/{0}.cshtml/Views/Shared/{0}.cshtml
这里,{2}标记代表区域名称,{0}代表视图名称,{1}代表控制器名称,如前所述。基本上,您的结构与非区域视图类似,但现在您有了全局共享或按区域共享的视图。
如前所述,您可以向区域添加特殊文件。现在,让我们看看 DI 在视图中是如何工作的。
依赖注入
视图类(RazorPage<T>)支持在其构造函数中注入服务,如以下代码片段所示:
public class MyPage : RazorPage<dynamic>
{
public MyPage(IMyService svc)
{
//constructor injection
}
}
视图还支持将服务注入其中。只需在.cshtml文件中声明一个@inject元素,其中包含要检索的服务类型和保存它的局部变量,可能在视图的开头,如下所示:
@inject IHelloService Service
在此之后,您可以使用注入的Service变量,如下所示:
@Service.SayHello()
There may be the need to either fully qualify the type name or add a @using declaration for its namespace.
现在让我们看看如何让我们的应用以不同的语言响应。
使用翻译
我们在上一章中已经看到,ASP.NET Core 包括用不同语言显示资源的内置机制;这当然包括观点。实际上,有两种方式显示翻译文本,如下所示:
- 资源
- 翻译观点
让我们从资源开始。
利用资源
那么,假设我们有两个资源文件(.resx),用于语言PT和EN。让我们将它们存储在Resources文件夹下(这可以配置,稍后我们将看到),一个名为Views的文件夹下,以及一个以控制器命名的文件夹内,视图将从该文件夹提供服务(例如,Home)。文件名本身必须与操作名匹配,因此,例如,我们可能有以下内容:
Resources\Views\Home\Index.en.resxResources\Views\Home\Index.pt.resx
在使用之前,我们需要在ConfigureServices中配置本地化服务,如下所示:
services
.AddMvc()
.AddMvcLocalization(
format: LanguageViewLocationExpanderFormat.Suffix,
localizationOptionsSetupAction: options =>
{
options.ResourcesPath = "Resources";
});
AddMvcLocalization 的两个参数表示如下:
format(LanguageViewLocalizationExpanderFormat:用于说明资源文件的区域性的格式localizationOptionsSetupAction(Action<LocalizationOptions>:配置定位机制需要采取的动作,比如指定资源的路径(目前只有ResourcesPath属性)
LanguageViewLocalizationExpanderFormat的两个可能值如下:
SubFolder:这意味着每个资源文件都应该存储在以区域性命名的文件夹下(例如,Resources\Views\Home\en、Resources\Views\Home\en-gb、Resources\Views\Home\pt、Resources\Views\Home\pt-pt等等)。Suffix:区域性是文件名的一部分(例如,Index.en.resx、Index.pt.resx等等)。
对于LocalizationOptions结构,其ResourcePath属性已经默认为Resources。
注册后,我们需要实际添加负责设置区域性和 UI 区域性的中间件:
var supportedCultures = new[] { "en", "pt" };
var localizationOptions = new RequestLocalizationOptions()
.SetDefaultCulture(supportedCultures[0])
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
app.UseRequestLocalization(localizationOptions);
这应该采用Configure方法。以下是对此的一点解释:
- 我们必须确定可供选择的文化;这些应该映射到我们拥有的资源文件。
- 如果浏览器未设置特定区域性,则其中一个区域性将是默认的(回退)区域性。
- 这里我们既要设置当前区域性(
CultureInfo.CurrentCulture),也要设置当前 UI 区域性(CultureInfo.CurrentUICulture);它们很有用,因为我们可能希望在将字符串值发送到视图之前在服务器上格式化字符串值,在这种情况下,我们希望服务器代码使用适当的区域性。
对于资源提供者,ASP.NET Core 包含三个,默认情况下都包含在RequestLocalizationOptions类中,顺序如下:
QueryStringRequestCultureProvider:查找culture查询字符串键CookieRequestCultureProvider:从.AspNetCore.Culturecookie 获取要使用的培养基AcceptLanguageHeaderRequestCultureProvider:查找Accept-LanguageHTTP 头
提供者列表(实现IRequestCultureProvider的类)存储在RequestLocalizationOptions.中
RequestCultureProviders这个列表被交叉,直到它找到一个返回值的提供者。
当涉及到实际使用资源文件中的值时,我们需要向视图中注入一个IViewLocalizer实例并从中检索值,如下所示:
@inject IViewLocalizer Localizer
<h1>@Localizer["Hello"]</h1>
IViewLocalizer接口扩展了IHtmlLocalizer,因此继承了它的所有属性和方法。
您也可以使用共享资源。共享资源是一组.resx文件,加上一个空类,它们与特定的操作或控制器无关。这些应存储在Resources文件夹中,但此类的命名空间应设置为程序集默认命名空间,如以下代码段所示:
namespace chapter05
{
public class SharedResources { }
}
对于本例,资源文件应称为SharedResources.en.resx,或任何其他区域性的名称。
然后,在您看来,插入对IHtmlLocalizer<SharedResources>的引用,如下所示:
@inject IHtmlLocalizer<SharedResources> SharedLocalizer
<h1>@SharedLocalizer["Hello"]</h1>
接下来,我们将翻译视图。
使用翻译视图
另一种选择是转换整个视图;通过翻译,我的意思是 ASP.NET Core 将在返回到通用视图之前寻找与当前语言匹配的视图。
要激活此功能,您需要调用AddViewLocalization,如下所示:
services
.AddMvc()
.AddViewLocalization();
这样做的目的是添加一个名为LanguageViewLocationExpander的视图位置扩展器(还记得吗?)。这将复制已注册的视图位置,以便包括以当前语言作为文件后缀的位置。例如,我们可能有以下初始视图位置格式:
/Views/{1}/{0}.cshtml/Views/Shared/{0}.cshtml
对于pt语言,这些将成为以下内容:
/Views/{1}/{0}.pt.cshtml/Views/{1}/{0}.cshtml/Views/Shared/{0}.pt.cshtml/Views/Shared/{0}.cshtml
因为顺序很重要,这实际上意味着 ASP.NET Core 将首先尝试查找以.pt结尾的视图(例如Index.pt.cshtml),然后,如果找不到,它将求助于查找通用视图(例如Index.cshtml。很酷,你不觉得吗?当然,翻译后的视图可能与一般视图完全不同,尽管这主要是为了翻译而设计的。
总结
视图布局是必不可少的;尽量避免嵌套(或太嵌套)视图布局,因为可能很难理解最终结果。局部视图也非常方便,但请确保使用它们以避免重复代码。
我们还应该避免在视图中使用代码,例如,通过指定自定义视图类;为此目的使用过滤器。我们认为我们应该考虑你的应用的本地化需求;重构不使用本地化引入的现有应用非常困难且容易出错。
然后,接下来,我们看到,为了安全,可以使用代码或标记帮助程序来保护视图的敏感部分。
坚持文件夹名称等方面的约定。这将使团队中的每个人,无论是现在还是将来,都能更轻松地完成任务。
我们了解到,_ViewImports.cshtml和_ViewStart.cshtml是您的朋友,它们用于您希望应用于所有页面的通用代码。
考虑视图编译它确实有助于发现一些问题之前,他们咬你。
在本章中,我们使用内置的 Razor 引擎介绍了 ASP.NET Core 的视图功能。我们了解了如何使用视图布局来引入一致的布局和部分视图,以便进行封装和重用。我们学习了将数据从控制器传递到视图的方法。
在下一章中,我们将继续使用视图,特别是 HTML 表单。我们将深入讨论这里介绍的一些主题。
问题
您现在应该能够回答以下问题:
- 视图的基类是什么?
- 如何将服务注入到视图中?
- 什么是视图位置扩展器?
- 什么是视图布局?
- 什么是局部视图?
- 哪些功能可以替换局部视图?
_ViewStart.cshtml特殊文件的作用是什么?
六、表单和模型
在本章中,我们将学习如何构建表单以显示和捕获应用中使用的数据,如何将控件绑定到模型,以及如何使用验证技术排除无效数据。我们将介绍客户端提交的数据,即 HTML 表单及其服务器端对应项、模型和文件。通过这些,我们将学习如何处理用户提交的数据。
具体来说,我们将讨论以下内容:
- 使用表单上下文
- 使用模型
- 了解模型元数据并使用元数据影响表单生成
- 我们如何使用 HTML 助手生成 HTML
- 使用模板
- 将表单绑定到对象模型
- 验证模型
- 使用 AJAX
- 上传文件
技术要求
为了实现本章中介绍的示例,您需要.NET Core 3 SDK 和文本编辑器。当然,VisualStudio2019(任何版本)满足我们的所有要求,但您也可以使用 VisualStudio 代码。
本章的源代码可从 GitHub 的检索 https://github.com/PacktPublishing/Modern-Web-Development-with-ASP.NET-Core-3-Second-Edition 。
开始
因为视图本质上是 HTML,所以没有什么可以阻止您手动将标记添加到视图中,其中可以包括通过模型、视图包或临时数据从控制器获得的值。但是,与以前的版本一样,ASP.NET Core 具有内置的方法来帮助您生成与模型(结构和内容)匹配的 HTML,并显示模型验证错误和其他有用的模型元数据。
因为所有这些都在模型之上工作,为了使框架能够提取任何相关信息,我们需要使用强类型视图,而不是动态视图;这意味着向具有适当模型类型的视图添加@model或@inherits指令。需要明确的是,模型是您传递给从控制器返回的ViewResult对象的对象,可能是从View方法返回的,它必须匹配视图中声明的@model指令或其@inherit声明。
让我们先看看表单上下文,然后看看如何获得有关模型的信息。
使用表单上下文
视图上下文对象(ViewContext在视图组件(将在第 9 章、可重用组件中讨论)中可用,并且作为 Razor Pages 的属性(IRazorPage),这意味着您可以在视图中访问它。在其中,除了通常的上下文属性(例如HttpContext、ModelStateDictionary、RouteData和ActionDescriptor之外,您还可以访问表单上下文(FormContext对象)。此对象提供以下属性:
CanRenderAtEndOfForm(bool):表示表单最后是否可以呈现额外内容(EndOfFormContent)。EndOfFormContent(IList<IHtmlContent>):在表单末尾(在</form>标记之前)添加的内容集合。FormData(IDictionary<string, object>:提交的表单数据。HasAntiforgeryToken(bool):表示表单是否呈现防伪令牌,具体取决于BeginForm方法的调用方式。默认为true。HasEndOfFormContent(bool):表示是否添加了表单结尾内容。HasFormData(bool):表示FormData字典是否已经使用,是否包含数据。
此外,它还提供了一个方法RenderedField,带有两个重载:
- 返回是否已在当前视图中呈现表单字段的指示的一种
- 为特定字段设置此标志的另一个字段(通常由基础结构调用)
开发人员可以利用表单上下文来呈现表单中的其他数据,例如验证脚本或其他字段。
现在我们已经看到了全局上下文的样子,让我们看看如何提取有关模型的信息。
使用模型
ASP.NET Core 框架使用模型元数据提供程序从模型中提取信息。此元数据提供程序可以通过Html的MetadataProperty访问,并公开为IModelMetadataProvider。默认设置为DefaultModelMetadataProvider实例,可通过依赖注入框架进行更改,其合约只定义了两种相关方法:
GetMetadataForType(ModelMetadata:返回模型类型本身的元数据GetMetadataForProperties(IEnumerable<ModelMetadata>:所有公共模型属性的元数据
你通常不会调用这些方法;它们由框架在内部调用。他们返回的ModelMetadata类(实际上可能是派生类,例如DefaultModelMetadata)应该是我们更感兴趣的。此元数据返回以下内容:
-
类型或属性的显示名称和说明(
DisplayName -
数据类型(
DataType) -
文本占位符(
Placeholder -
空值情况下显示的文本(
NullDisplayText -
显示格式(
DisplayFormatString) -
是否需要该属性(
IsRequired -
属性是否为只读(
IsReadOnly -
绑定是否需要该属性(
IsBindingRequired -
型号活页夹(
BinderType) -
活页夹型号名称(
BinderModelName -
模型的绑定源(
BindingSource -
属性的包含类(
ContainerType)
HTML 助手在为模型生成 HTML 时使用这些属性,它们会影响模型的生成方式。
默认情况下,如果未提供模型元数据提供程序且不存在属性,则元数据属性将假定为安全值或空值。但是,可以覆盖它们。让我们了解如何使用这些属性。
我们将首先查看显示名称(DisplayName和说明(Description)。这些可以由来自System.ComponentModel.DataAnnotations名称空间的[Display]属性控制。此属性还设置属性(Placeholder的占位符/水印):
[Display(Name = "Work Email", Description = "The work email",
Prompt = "Please enter the work email")]
public string WorkEmail { get; set; }
通过[Required]实现按要求标记属性(IsRequired)。还可以提供从ValidationAttribute继承的所有其他验证属性(例如Required和MaxLength,如下所示:
[Required]
[Range(1, 100)]
public int Quantity { get; set; }
属性是否可编辑(IsReadOnly)由属性是否有 setter 和是否应用[Editable]属性控制(默认值为true:
[Editable(true)]
public string Email { get; set; }
字符串中包含的数据类型(DataType)可以通过应用[DataType]属性或从中继承的属性来定义:
[DataType(DataType.Email)]
public string Email { get; set; }
有几个从DataTypeAttribute继承的属性类可以替代它使用:
[EmailAddress]:同DataType.EmailAddress[CreditCard]:DataType.CreditCard[Phone]:DataType.PhoneNumber[Url]:DataType.Url[EnumDataType]:DataType.Custom[FileExtensions]:DataType.Upload
DataType has several other possible values; I advise you to have a look into it.
显示值是否为null(NullDisplayText)的文本和显示格式(DisplayFormatString)都可以通过[DisplayFormat]属性设置:
[DisplayFormat(NullDisplayText = "No birthday supplied", DataFormatString = "{0:yyyyMMdd}")]
public DateTime? Birthday { get; set; }
当涉及到将表单字段绑定到类属性时,[ModelBinder]可以用于指定自定义模型绑定器类型(BinderType属性)以及要绑定到的模型中的名称(ModelBinderName;通常,您不提供模型的名称,因为假定该名称与特性名称相同:
[ModelBinder(typeof(GenderModelBinder), Name = "Gender")]
public string Gender { get; set; }
这里,我们指定一个自定义模型绑定器,它将尝试从请求中检索一个值并将其转换为适当的类型。下面是一个可能的实现:
public enum Gender
{
Unspecified = 0,
Male,
Female
}
public class GenderModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var modelName = bindingContext.ModelName;
var valueProviderResult = bindingContext.
ValueProvider.GetValue(modelName);
if (valueProviderResult != ValueProviderResult.None)
{
bindingContext.ModelState.SetModelValue(modelName,
valueProviderResult);
var value = valueProviderResult.FirstValue;
if (!string.IsNullOrWhiteSpace(value))
{
if (Enum.TryParse<Gender>(value, out var gender))
{
bindingContext.Result = ModelBindingResult.
Success(gender);
}
else
{
bindingContext.ModelState.TryAddModelError
(modelName, "Invalid gender.");
}
}
}
return Task.CompletedTask;
}
}
这样做的目的是使用当前值提供程序查找传递的表单名称,如果设置了,则检查它是否与Gender枚举匹配。如果是,则设置为返回值(bindingContext.Result;否则,它会添加一个模型错误。
如果通过设置[Bind]、[BindRequired]、[BindingBehavior]或[BindNever]需要一个属性,则IsBindingRequired将是true:
[BindNever] //same as [BindingBehavior(BindingBehavior.Never)]
public int Id { get; set; }
[BindRequired] //same as [BindingBehavior(BindingBehavior.Required)]
public string Email { get; set; }
[BindingBehavior(BindingBehavior.Optional)] //default, try to bind if a
//value is provided
public DateTime? Birthday { get; set; }
[Bind]应用于类本身或参数,以指定哪些属性应该绑定或从绑定中排除。在这里,我们要提到的是,哪些应该受到约束:
[Bind(Include = "Email")]
public class ContactModel
{
public int Id { get; set; }
public string Email { get; set; }
}
如果我们使用IBindingSourceMetadata属性之一,则设置BindingSource属性:
[FromBody][FromForm][FromHeader][FromQuery][FromRoute][FromServices]
默认的模型元数据提供程序可以识别这些属性,但是您当然可以推出自己的提供程序并以任何其他方式提供属性。
有时不应将属性应用于模型属性,例如,自动生成模型类时。在这种情况下,您可以应用一个[ModelMetadataType]属性,通常在另一个文件中,您可以在其中指定用于从以下文件检索元数据属性的类:
public partial class ContactModel
{
public int Id { get; set; }
public string Email { get; set; }
}
可以从另一个文件向同一类添加属性:
[ModelMetadataType(typeof(ContactModelMetadata))]
public partial class ContactModel
{
}
在以下示例中,我们指定了要绑定的各个属性:
public sealed class ContactModelMetadata
{
[BindNever]
public int Id { get; set; }
[BindRequired]
[EmailAddress]
public string Email { get; set; }
}
Besides using the model, it is also possible to bind properties on the controller itself. All that is said also applies, but these properties need to take the [BindProperty] attribute. See Chapter 4, Controllers and Actions, for more information.
现在让我们看看如何处理匿名类型。
使用匿名类型的模型
与以前版本的 ASP.NET MVC 一样,您不能将匿名类型作为模型传递给视图。即使可以,该视图也无法访问其属性,即使该视图设置为使用dynamic作为模型类型。您可以使用这样的扩展方法将您的匿名类型转换为ExpandoObject,这是dynamic的常见实现:
public static ExpandoObject ToExpando(this object anonymousObject)
{
var anonymousDictionary = HtmlHelper.
AnonymousObjectToHtmlAttributes(anonymousObject);
IDictionary<string, object> expando = new ExpandoObject();
foreach (var item in anonymousDictionary)
{
expando.Add(item);
}
return expando as ExpandoObject;
}
您可以在控制器中使用此选项:
return this.View(new { Foo = "bar" }.ToExpando());
在视图文件中,按如下方式使用它:
@model dynamic
<p>@Model.Foo</p>
我们现在已经完成了模型绑定,所以让我们继续使用 HTML 帮助程序。
使用 HTML 助手
HTML 助手是视图的Html对象(IHtmlHelper对象)的方法,其存在是为了帮助生成 HTML。我们可能不知道路由的确切语法和 URL 可能很难生成,但我们使用它们还有两个更重要的原因。HTML 帮助程序根据模型元数据生成用于显示和编辑目的的适当代码,并且还包括错误和描述占位符。重要的是要记住,它们始终基于模型。
通常,内置 HTML 帮助程序有两个重载:
- 采用强类型模型的模型(例如,
EditorFor(x => x.FirstName)) - 另一种以字符串形式获取动态参数的方法(例如,
EditorFor("FirstName"))
此外,它们都采用可选参数htmlAttributes,可用于向呈现的 HTML 元素添加任何属性(例如,TextBoxFor(x => x.FirstName, htmlAttributes: new { @class = "first-name" }))。由于这个原因,当我们浏览不同的 HTML 帮助程序时,我将跳过htmlAttributes参数。
形式
为了提交值,我们首先需要一个表单;HTMLform元素可用于此目的。BeginForm助手为我们生成一个:
@using (Html.BeginForm())
{
<p>Form goes here</p>
}
返回一个IDisposable实例;因此,应在using块中使用。这样,我们可以确保它被正确终止。
此方法有多个重载,其中,它可以采用以下参数:
-
actionName(string:控制器动作的可选名称。如果存在,controllerName参数也必须提供。 -
controllerName(string:控制器的可选名称;它必须与actionName配合使用。 -
method(FormMethod:可选的 HTML 表单方法(GET或POST);如果未提供,则默认为POST。 -
routeName(string:可选路由名称(通过 fluent 配置注册的路由名称)。 -
routeValues(object:可选对象实例,包含routeName特有的路由值。 -
antiForgery(bool?):表示表单中是否应该包含防伪令牌(后面会详细介绍);如果未提供,则默认情况下会包含它。
还有另一种表单生成方法BeginRouteForm,它更关注路由,因此它总是采用routeName参数。它所做的任何事情都可以通过BeginForm实现。
有两种方法可用于定义表单提交的目标:
actionName和controllerName:将表单提交到的操作和可选控制器名称。如果省略控制器名称,它将默认为当前名称。routeName:路由表中定义的路由名称,由控制器和动作组成。
必须选择其中之一。
单行文本框
所有基本的.NET 类型都可以通过文本框进行编辑。我所说的文本框是指具有适当的type属性的<input>元素。为此,我们有TextBoxFor和TextBox方法,前者用于强类型版本(基于模型使用 LINQ 表达式的版本),另一个用于基于字符串的版本。这些方法可按如下方式使用:
@Html.TextBoxFor(x => x.FirstName)
@Html.TextBox("FirstName")
这些方法具有多个重载,这些重载采用了format参数。
format(string:用于呈现类型实现IFormattable的情况的可选格式字符串
例如,如果要呈现的值表示金钱,我们可以有一行,如下所示:
@Html.TextBoxFor(model => model.Balance, "{0:c}");
此处,c用于设置货币格式。
TextBox和TextBoxForHTML 帮助程序呈现一个值为type的<input>标记,该值取决于属性的实际类型及其数据类型元数据(DefaultModelMetadata.DataTypeName:
text:对于没有任何特定DataType属性的字符串属性date和datetime:对于DateTime属性,取决于DataType的存在,其值为Date或DateTimenumber:用于数字属性email:与EmailAddress的DataType属性关联时的字符串属性url:具有DataType属性Url的字符串属性time:TimeSpan属性或DataType属性为Time的字符串属性tel:具有DataType属性PhoneNumber的字符串属性
<input>标记的类型是 HTML5 支持的值之一。您可以在上了解更多信息 https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input 。
多行文本框
如果我们想要呈现多行文本框,我们必须使用TextArea和TextAreaFor方法。这些呈现 HTMLtextarea元素及其参数:
rows(int):要生成的行(textarea``rows属性)columns(int)cols属性
在此之后,我们继续了解密码是如何工作的。
密码
密码(<input type="password">由Password和PasswordFor方法之一产生。他们可以接受的唯一可选值是初始密码,即初始密码value(string。
接下来是下拉列表。
下拉菜单
DropDownList和DropDownListFor方法使用SelectListItem项集合形式指定的值呈现<select>元素。参数如下:
selectList(IEnumerable<SelectListItem>:要显示的项目列表optionLabel(string:默认为空项
SelectListItem类公开了以下属性:
Disabled(bool):表示该项目是否可用。默认值为false。Group(SelectListGroup:可选组。Selected(bool):表示是否选中该项目。只能有一个项目标记为选中;因此,默认值为false。Text(string:要显示的文本值。Value(string:要使用的值。
SelectListGroup类提供两个属性:
Name(string:必填组名,用于将多个列表项分组。Disabled(bool):表示该组是否禁用。默认为false。
有两个助手方法,GetEnumSelectList和GetEnumSelectList<>,它们以IEnumerable<SelectListItem>的形式返回枚举字段的名称和值。如果我们希望使用它们来提供下拉列表,这将非常有用。
列表框
ListBox和ListBoxFor方法与下拉列表类似。唯一的区别是生成的<select>元素的multiple属性设置为true。只需要一个参数selectList(IEnumerable<SelectListItem>)就可以显示项目。
单选按钮
对于单选按钮,我们有RadioButton和RadioButtonFor两种方法,它们将<input>呈现为radio类型:
value(object:用于单选按钮的值isChecked(bool?):表示单选按钮是否勾选(默认)
单选按钮组名称将是生成该组的属性的名称,例如:
@Html.RadioButtonFor(m => m.Gender, "M" ) %> Male
@Html.RadioButtonFor(m => m.Gender, "F" ) %> Female
复选框
也可以通过CheckBox、CheckBoxFor和CheckBoxForModel方法来考虑复选框。这一次,他们呈现了一个类型为checkbox的<input>标记。唯一参数如下所示:
isChecked(bool?):表示复选框是否勾选。默认值为false。
同样,组名将来自属性,单选按钮也是如此。
使用复选框时需要记住的一点是,我们通常会将复选框值绑定到一个bool参数。在这种情况下,我们不能忘记为复选框提供一个值true;否则,表单将不包含其字段的数据。
隐藏值
Hidden、HiddenFor和HiddenForModel呈现一个<input type="hidden">元素。可以使用以下参数显式重写模型或其属性:
value(object):要包含在隐藏字段中的值
另一个选项是使用[HiddenInput]属性装饰模型类属性,如下例所示:
[HiddenInput(DisplayValue = false)]
public bool IsActive { get; set; } = true;
使用自动模型编辑器时,DisplayValue参数会导致属性不作为标签输出。
链接
如果我们想要生成特定控制器动作的超链接(<a>,我们可以使用ActionLink方法。它有多个重载,可接受以下参数:
linkText(string:链接文本actionName(string:动作名称controllerName(string:控制器名称,必须与actionName一起提供routeValues(object:包含路由值的可选值(POCO 类或字典)protocol(string):可选的 URL 协议(如http、https等)hostname(string:可选的 URL 主机名fragment(string:可选的 URL 锚(例如#anchorname)port(int:可选的 URL 端口
如我们所见,此方法可以为 web 应用的同一主机或不同主机生成链接。
另一种选择是使用路由名称,为此目的,有RouteLink方法;唯一的区别是,它不使用actionName和controllerName参数,而是使用routeName参数,如routeName(string),即为其生成链路的路由的名称。
接下来,我们有标签。
标签
Label、LabelFor和LabelForModel使用模型的文本表示或可选文本呈现<label>元素:
labelText(string:要添加到标签的文本
在标签之后,我们有原始的 HTML。
原始 HTML
这将呈现 HTML 编码的内容。其唯一参数如下:
value(string、object:HTML 编码后显示的内容
我们将要学习的下一个特性是 ID、名称和值。
ID、名称和值
从生成的 HTML 元素、生成的 ID 和名称中提取某些属性时,这些属性通常很有用。JavaScript 通常需要这样做:
Id、IdFor、IdForModel:返回id属性的值Name、NameFor、NameForModel:name属性的值DisplayName、DisplayNameFor、DisplayNameForModel:给定属性的显示名称DisplayText和DisplayTextFor:属性或模型的显示文本Value、ValueFor、ValueForModel:视图包中的第一个非空值
通用编辑器和显示
我们已经看到,我们可以对单个模型属性或模型本身使用模板。要呈现显示模板,我们有Display、DisplayFor和DisplayForModel方法。它们都接受以下可选参数:
templateName(string:将覆盖模型元数据中模板的名称(DefaultModelMetadata.TemplateHint)additionalViewData(object):合并到视图包中的对象或IDictionaryhtmlFieldName(string:生成的 HTML<input>字段的名称
属性只有在其元数据声明为显示模式时才会呈现(DefaultModelMetadata.ShowForDisplay。
对于编辑模板,方法类似:Editor、EditorFor和EditorForModel。这些参数与相应的显示参数完全相同。值得一提的是,仅为根据元数据定义的可编辑属性生成编辑器(DefaultModelMetadata.ShowForEdit。
效用方法与性质
IHtmlHelper类还公开了一些其他实用方法:
Encode:HTML 使用配置的 HTML 编码器对字符串进行编码FormatValue:呈现传递值的格式化版本
此外,它还公开了以下上下文属性:
-
IdAttributeDotReplacement:用于生成 ID 值的点替换字符串(来自MvcViewOptions.HtmlHelperOptions.IdAttributeDotReplacement) -
Html5DateRenderingMode:HTML5 日期呈现模式(从MvcViewOptions.HtmlHelperOptions.Html5DateRenderingMode开始) -
MetadataProvider:模型元数据提供者 -
TempData:临时数据 -
ViewData或ViewBag:强/松类型视图包 -
ViewContext:视图的所有上下文,包括 HTTP 上下文(HttpContext)、路由数据(RouteData)、表单上下文(FormContext)和解析模型(ModelStateDictionary)
接下来是验证消息。
验证消息
可以显示单个已验证属性的验证消息,也可以显示所有模型的摘要。对于显示单个消息,我们使用ValidationMessage和ValidationMessageFor方法,它们接受以下可选属性:
message(string:覆盖验证框架中的错误消息的错误消息
对于验证总结,我们有ValidationSummary,它接受以下参数:
excludePropertyErrors(bool):如果设置,则仅显示模型级(顶部)错误,而不显示单个属性的错误message(string):显示单个错误的消息tag(string:用于覆盖MvcViewOptions.HtmlHelperOptions.ValidationSummaryMessageElement的 HTML 标记)
在验证之后,我们继续下一个特性,即自定义帮助程序。
海关助理
一些 HTML 元素没有相应的 HTML 帮助程序,例如,button。不过,添加一个是很容易的。那么,让我们在IHtmlHelper上创建一个扩展方法:
public static class HtmlHelperExtensions
{
public static IHtmlContent Button(this IHtmlHelper html, string text)
{
return html.Button(text, null);
}
public static IHtmlContent Button(this IHtmlHelper html, string
text, object htmlAttributes)
{
return html.Button(text, null, null, htmlAttributes);
}
public static IHtmlContent Button(
this IHtmlHelper html,
string text,
string action,
object htmlAttributes)
{
return html.Button(text, action, null, htmlAttributes);
}
public static IHtmlContent Button(this IHtmlHelper html, string
text, string action)
{
return html.Button(text, action, null, null);
}
public static IHtmlContent Button(
this IHtmlHelper html,
string text,
string action,
string controller)
{
return html.Button(text, action, controller, null);
}
public static IHtmlContent Button(
this IHtmlHelper html,
string text,
string action,
string controller,
object htmlAttributes)
{
if (html == null)
{
throw new ArgumentNullException(nameof(html));
}
if (string.IsNullOrWhiteSpace(text))
{
throw new ArgumentNullException(nameof(text));
}
var builder = new TagBuilder("button");
builder.InnerHtml.Append(text);
if (htmlAttributes != null)
{
foreach (var prop in htmlAttributes.GetType()
.GetTypeInfo().GetProperties())
{
builder.MergeAttribute(prop.Name,
prop.GetValue(htmlAttributes)?.ToString() ??
string.Empty);
}
}
var url = new UrlHelper(new ActionContext(
html.ViewContext.HttpContext,
html.ViewContext.RouteData,
html.ViewContext.ActionDescriptor));
if (!string.IsNullOrWhiteSpace(action))
{
if (!string.IsNullOrEmpty(controller))
{
builder.Attributes["formaction"] = url.Action(
action, controller);
}
else
{
builder.Attributes["formaction"] = url.Action(action);
}
}
return builder;
}
}
此扩展方法使用所有其他 HTML 帮助程序的通用准则:
- 每个可能参数的多个重载
- 有一个名为
htmlAttributes的object类型的参数,用于我们希望添加的任何自定义 HTML 属性 - 使用
UrlHelper类为控制器操作生成正确的路由链接(如果提供) - 返回一个
IHtmlContent的实例
使用它很简单:
@Html.Button("Submit")
它还可以与特定动作和控制器一起使用:
@Html.Button("Submit", action: "Validate", controller: "Validation")
它甚至可以与一些自定义属性一起使用:
@Html.Button("Submit", new { @class = "save" })
由于 ASP.NET Core 不提供任何用于提交表单的 HTML 帮助程序,我希望您觉得这很有用!
我们对定制助手的研究到此结束。现在让我们重点讨论为常用的标记编写模板。
使用模板
当调用Display、DisplayFor<T>或DisplayForModelHTML 助手方法时,ASP.NET Core 框架会以特定于该属性(或模型类)的方式呈现目标属性(或模型)值,并且会受到其元数据的影响。例如,ModelMetadata.DisplayFormatString用于以所需格式呈现属性。然而,假设我们想要一个稍微复杂一点的 HTML,例如,在复合属性的情况下。输入显示模板!
显示模板是一个剃须刀功能;基本上,它们是部分视图,存储在Views\Shared下名为DisplayTemplates的文件夹中,它们的模型被设置为以.NET 类为目标。让我们想象一下,我们有一个Location类存储Latitude和Longitude值:
public class Location
{
public decimal Latitude { get; set; }
public decimal Longitude { get; set; }
}
如果我们想为此使用自定义显示模板,可以使用局部视图,如下所示:
@model Location
<div><span>Latitude: @Model.Latitude</span> - <span>Longitude:
@Model.Longitude</span></div>
因此,该文件存储在Views/Shared/DisplayTemplates/Location.cshtml中,但现在您需要将Location类与之关联,您可以通过将[UIHint]应用于该类型的属性来实现:
[UIHint("Location")]
public Location Location { get; set; }
The [UIHint] attribute accepts a view name. It is searched in the Views\Shared\DisplayTemplates folder.
与显示模板类似,我们有编辑器模板。编辑器模板由Editor、EditorFor或EditorForModel呈现,它们与显示模板的主要区别在于部分视图文件存储在Views\Shared\EditorTemplates中。当然,在这些模板中,您可能会添加 HTML 编辑器元素,即使使用自定义 JavaScript 也是如此。对于Location类,我们可以有以下内容:
@model Location
<div>
<span>Latitude: @Html.TextBoxFor(x => x.Latitude)</span>
<span>Longitude: @Html.TextBoxFor(x => x.Longitude)</span>
</div>
There can be only one [UIHint] attribute specified, which means that both templates—display and editor—must use the same name. Also, custom templates are not rendered by EditorForModel or DisplayForModel; you need to explicitly render them using EditorFor and DisplayFor.
好的,我们已经了解了如何为常用的标记元素使用模板,这从重用的角度来看非常有用。现在让我们看一看模型绑定。
强制模型绑定
ASP.NET Core 尝试自动填充(设置其属性和字段的值)操作方法的任何参数。这是因为它有一个内置的(虽然是可配置的)模型绑定器提供程序,它创建了一个模型绑定器。这些模型绑定器知道如何将来自多个绑定源(前面讨论过)的数据以多种格式绑定到 POCO 类。
模型粘合剂
模型绑定器提供程序接口是IModelBinderProvider,模型绑定器是IModelBinder,这并不奇怪。模型装订商在MvcOptions的ModelBinderProviders集合中注册:
services.AddMvc(options =>
{
options.ModelBinderProviders.Add(new CustomModelBinderProvider());
});
包括的供应商如下:
BinderTypeModelBinderProvider:定制型号活页夹(IModelBinder)ServicesModelBinderProvider:[FromServices]BodyModelBinderProvider:[FromBody]HeaderModelBinderProvider:[FromHeader]SimpleTypeModelBinderProvider:使用类型转换器的基本类型CancellationTokenModelBinderProvider:CancellationTokenByteArrayModelBinderProvider:将 Base64 字符串反序列化为字节数组FormFileModelBinderProvider:[FromForm]FormCollectionModelBinderProvider:IFormCollectionKeyValuePairModelBinderProvider:KeyValuePair<TKey, TValue>DictionaryModelBinderProvider:IDictionary<TKey, TValue>ArrayModelBinderProvider:对象数组CollectionModelBinderProvider:对象集合(ICollection<TElement>、IEnumerable<TElement>或IList<TElement>)ComplexTypeModelBinderProvider:嵌套属性(例如TopProperty.MidProperty.BottomProperty)
这些提供程序帮助将值分配给以下类型:
- 使用类型转换器的简单属性
- POCO 类
- 嵌套的 POCO 类
- POCO 类的数组
- 辞典
- POCO 类集合
例如,以以下类别的模型为例:
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public OrderState State { get; set; }
public DateTime Timestamp { get; set; }
public List<OrderDetail> Details { get; set; }
}
public enum OrderState
{
Received,
InProcess,
Sent,
Delivered,
Cancelled,
Returned
}
public class OrderDetail
{
public int ProductId { get; set; }
public int Quantity { get; set; }
}
public class Location
{
public int X { get; set; }
public int Y { get; set; }
}
这里,我们有不同类型的属性,包括基本类型、枚举和 POCO 类集合。当我们为这样的模型生成表单时,可能使用前面描述的 HTML 帮助程序,您将获得包含以下值的 HTML 表单元素:
Id=43434
CustomerId=100
State=InProcess
Timestamp=2017-06-15T20:00:00
Details[0]_ProductId=45
Details[0]_Quantity=1
Details[1]_ProductId=47
Details[1]_Quantity=3
X=10
Y=20
请注意分隔子属性名称的_字符,默认情况下,它被配置为替换MvcViewOptions.HtmlHelper.IdAttributeDotReplacement属性中的点(.。如您所见,ASP.NET Core 甚至可以绑定一些复杂的情况。
模型绑定源
因此,我们将模型(或单个基类型参数)声明为操作方法的参数,并且可以应用模型绑定源属性来指示 ASP.NET Core 从特定位置获取值。同样,这些措施如下:
[FromServices]:将从依赖项注入容器插入对象。[FromBody]:该值将来自POST请求的有效负载,通常为 JSON 或 XML。[FromForm]:该值将来自已发布的表单。[FromQuery]:从查询字符串中获取值。[FromHeader]:该值将从请求头读取。[FromRoute]:该值将作为命名模板项来自路由。
可以在同一方法上混合不同的模型绑定源属性,如下所示:
public IActionResult Process(
[FromQuery] int id,
[FromHeader] string contentType,
[FromBody] Complex complex) { ... }
[FromPost]将采用multipart/form-data或application/x-www-form-urlencoded格式的键值对。
您需要记住的一点是,只有一个参数具有[FromBody]属性,这是有意义的,因为主体是唯一的,将其绑定到两个不同的对象是没有意义的。将其应用于 POCO 类也是有意义的。[FromBody]与注册的输入格式化程序一起工作;它试图通过遍历每个输入格式化程序来反序列化发送的任何有效负载(通常由POST或PUT)。第一个响应为非空值的函数将生成结果。输入格式化程序查看请求的Content-Type头(例如,application/xml或application/json,以确定是否可以处理请求并将其反序列化为目标类型。我们将在第 8 章、API 控制器中更详细地介绍输入格式化程序。
您可以使用[FromQuery]从查询字符串构造 POCO 对象。如果您在查询字符串上为 POCO 的每个属性提供一个值,ASP.NET Core 足够聪明,可以做到这一点,如下所示:
//call this with: SetLocation?X=10&Y=20
public IActionResult SetLocation([FromQuery] Location location) { ... }
其中一些属性采用可选的Name参数,可用于显式声明源名称,如下所示:
[FromHeader(Name = "User-Agent")]
[FromQuery(Name = "Id")]
[FromRoute(Name = "controller")]
[FromForm(Name = "form_field")]
如果不指定源名称,它将使用参数的名称。
如果未指定任何属性,ASP.NET Core 在尝试绑定值时将采用以下逻辑:
- 如果请求是一个
POST值,它将尝试绑定表单中的值(与[FromForm]一样)。 - 然后,它将路由值(
[FromRoute]。 - 然后查询字符串(
[FromQuery]。
因此,[FromBody]、[FromServices]和[FromHeader]从未自动使用。您始终需要应用属性(或定义约定)。
如果在使用默认逻辑或任何属性的操作方法中找不到参数的值,则该值将接收默认值:
- 值类型的默认值(
0表示整数,false表示布尔值,等等) - 类的实例化对象
如果要在找不到参数值时强制模型状态无效,请对其应用[BindRequired]属性:
public IActionResult SetLocation(
[BindRequired] [FromQuery] int x,
[BindRequired] [FromQuery] int y) { ... }
在这种情况下,在不提供X和Y参数的情况下,尝试调用此操作时将出现错误。您还可以将其应用于模型类,在这种情况下,需要提供其所有属性,如下所示:
[BindRequired]
public class Location
{
public int X { get; set; }
public int Y { get; set; }
}
这也有一些限制,因为您无法绑定到抽象类、值类型(struct)或没有公共无参数构造函数的类。如果您想绑定到一个抽象类或一个没有公共、无参数构造函数的类,您需要展开自己的模型绑定器并自己返回一个实例。
动态绑定
如果您事先不知道请求将包含什么内容,例如,如果您希望接受任何已发布的内容,该怎么办?你基本上有三种方式来接受它:
- 如果有效负载可以表示为字符串,请使用字符串参数。
- 使用自定义模型活页夹。
- 使用支持 JSON 的参数类型之一。
如果使用字符串参数,它将只按原样包含有效负载,但 ASP.NET Core 还支持将 JSON 有效负载绑定到dynamic或System.Text.Json.JsonElement参数。如果您不熟悉的话,JsonElement是新的System.Text.JsonAPI 的一部分,它取代了JSON.NET(Newtonsoft.Json)作为包含的 JSON 序列化程序。ASP.NET Core 可以将内容类型为application/json的POST绑定到这些参数类型之一,而无需任何额外配置,如下所示:
[HttpPost]
public IActionResult Process([FromBody] dynamic payload) { ... }
动态参数实际上是JsonElement的一个实例。除非使用自己的模型绑定器并从中返回构造的实例,否则不能将参数声明为接口或抽象基类。
现在,让我们继续验证绑定它的模型帖子。
JSON.NET is still available as an open source project from GitHub at https://github.com/JamesNK/Newtonsoft.Json. You can use it instead of the built-in JSON serializer. To do this, have a look at https://docs.microsoft.com/en-us/aspnet/core/migration/22-to-30?view=aspnetcore-3.1.
模型验证
我们都知道,通过验证页面而不必发布其内容的客户端验证是我们现在对 web 应用的期望。但是,对于禁用 JavaScript 的情况来说,这可能是不够的。在这种情况下,我们需要确保在实际使用数据之前在服务器端验证数据。ASP.NET Core 支持这两种方案;让我们看看如何。
服务器端验证
验证提交的模型的结果(通常通过POST进行验证)始终在ControllerBase类的ModelState属性中可用,并且在ActionContext类中也存在。考虑下面的代码片段:
if (!this.ModelState.IsValid)
{
if (this.ModelState["Email"].Errors.Any())
{
var emailErrors = string.
Join(Environment.NewLine, this.ModelState
["Email"].Errors.Select(e => e.ErrorMessage));
}
}
如您所见,我们有全局验证状态(IsValid)和单个属性错误消息(例如,["Email"].Errors)。
使用所有基于System.ComponentModel.DataAnnotationsAPI 的内置验证程序,执行以下验证:
- 基于属性的验证(
ValidationAttribute-派生) - 基于
IValidatableObject接口的验证
如果您碰巧更改了模型,则在发布表单或通过调用TryValidateModel显式调用表单时执行验证。ModelState属性属于ModelStateDictionary类型,它公开了以下属性:
Item(ModelStateEntry:访问单个模型属性的状态Keys(KeyEnumerable:模型属性名称的集合Values(ValueEnumerable:模型属性值Count(int:模型属性的计数ErrorCount(int:错误计数HasReachedMaxErrors(bool:发现的错误是否达到配置的最大值MaxAllowedErrors(int:配置的最大错误数(参见配置部分)Root(ModelStateEntry:根对象的模型状态IsValid(bool:模型是否有效ValidationState(ModelValidationState:模型的验证状态(Unvalidated、Invalid、Valid或Skipped)
基于属性的验证与验证属性所在的属性相关联(某些验证属性也可以应用于类)。属性的名称将是键,属性的值将是ModelStateDictionary中的值。对于每个属性,一旦验证器失败,将不会触发任何其他最终验证器,并且模型状态将立即无效。每个属性公开一个或多个ModelError对象的集合:
IEnumerable<ModelError> errors = this.ModelState["email"];
此类有两个属性:
ErrorMessage(string):由属性验证器生成的消息(如果有)Exception(Exception:验证此特定属性时产生的任何异常
在这之后,我们转到它的配置。
配置
作为MvcOptions类的一部分,AddMvc方法提供了两个配置选项:
MaxModelValidationErrors(int:不再执行验证之前的最大验证错误数(默认为200)。ModelValidatorProviders(IList<IModelValidatorProvider>:注册的模型验证提供商。默认情况下,它包含一个DefaultModelValidatorProvider实例和一个DataAnnotationsModelValidatorProvider实例。
这些内置提供程序基本上执行以下操作:
DefaultModelValidatorProvider:如果属性具有实现IModelValidator的属性,则使用该属性进行验证。DataAnnotationsModelValidatorProvider:钩住要验证的属性可能具有的任何ValidatorAttribute实例。
数据注释验证
System.ComponentModel.DataAnnotations提供以下验证属性:
-
[Compare]:比较两个属性的值是否相同。 -
[CreditCard]:字符串属性必须具有有效的信用卡格式。 -
[CustomValidation]:通过外部方法进行自定义验证。 -
[DataType]:根据特定数据类型(DateTime、Date、Time、Duration、PhoneNumber、Currency、Text、Html、MultilineText、EmailAddress、Password、Url、ImageUrl、CreditCard、PostalCode或Upload验证属性。 -
[EmailAddress]:检查字符串属性是否为有效的电子邮件地址。 -
[MaxLength]:字符串属性的最大长度。 -
[MinLength]:字符串属性的最小长度。 -
[Phone]:检查字符串属性是否具有类似电话的结构(仅限美国)。 -
[Range]:属性的最大值和最小值。 -
[RegularExpression]:使用正则表达式验证字符串属性。 -
[Remote]:使用控制器动作验证模型。 -
[Required]:检查属性是否设置了值。 -
[StringLength]:检查字符串的最大和最小长度;与一个[MinLength]值和一个[MaxLength]值相同,但是使用这个,您只需要一个属性。 -
[Url]:检查字符串属性是否为有效的 URL。
所有这些属性都由注册的DataAnnotationsModelValidatorProvider自动挂钩。
对于自定义验证,我们有两个选项:
- 继承
ValidationAttribute并实现其IsValid方法:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class IsEvenAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
if (value != null)
{
try
{
var convertedValue = Convert.ToDouble(value);
var isValid = (convertedValue % 2) == 0;
if (!isValid)
{
return new ValidationResult(this.ErrorMessage,
new[] { validationContext.MemberName });
}
}
catch { }
}
return ValidationResult.Success;
}
}
- 实施验证方法:
[CustomValidation(typeof(ValidationMethods), "ValidateEmail")]
public string Email { get; set; }
在此ValidationMethods类中,添加以下方法:
public static ValidationResult ValidateEmail(string email, ValidationContext context)
{
if (!string.IsNullOrWhiteSpace(email))
{
if (!Regex.IsMatch(email, @"^([\w\.\-]+)@([\w\-]+
)((\.(\w){2,3})+)$"))
{
return new ValidationResult("Invalid email",
new[] { context.MemberName });
}
}
return ValidationResult.Success;
}
需要注意的几点:
- 此验证属性仅检查有效电子邮件;它没有检查所需的值。
ValidationContext属性具有一些有用的属性,例如当前正在验证的成员名称(MemberName)、其显示名称(DisplayName)和根验证对象(ObjectInstance)。ValidationResult.Success为null。
验证方法的签名可能会有所不同:
- 第一个参数可以是强类型(例如,
string)或松散类型(例如,object),但必须与要验证的属性兼容。 - 它可以是
static或实例。 - 可以接受也可以不接受
ValidationContext参数。
为什么选择其中一个呢?[CustomValidation]属性通过拥有一组可在不同上下文中使用的共享方法,潜在地促进了重用。此属性中还有一条错误消息。
[CustomValidation] can be applied to either a property or the whole class.
错误消息
有三种方法可用于设置在发生验证错误时显示的错误消息:
ErrorMessage:一个普通的旧错误消息字符串,没有附加任何魔法。ErrorMessageString:一个格式字符串,可以根据实际的验证属性获取令牌(例如,{0}、{1});令牌{0}通常是正在验证的属性的名称。ErrorMessageResourceType和ErrorMessageResourceName:可以请求错误消息来自外部类型(ErrorMessageResourceType中声明的字符串属性(ErrorMessageResourceName);如果您想本地化错误消息,这是一种常见的方法。
在此之后,我们继续下一个功能。
自我验证
如果您需要的验证涉及一个类的多个属性,那么您将实现IValidatableObject(也由DataAnnotationsValidatorProvider支持),类似于您将[CustomValidation]应用于整个类所实现的。我们说这个类是自验证的。IValidatableObject接口指定了一个方法Validate,下面是一个可能的实现:
public class ProductOrder : IValidatableObject
{
public int Id { get; set; }
public DateTime Timestamp { get; set; }
public int ProductId { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext
context)
{
if (this.Id <= 0)
{
yield return new ValidationResult("Missing id", new []
{ "Id" });
}
if (this.ProductId <= 0)
{
yield return new ValidationResult("Invalid product",
new [] { "ProductId" });
}
if (this.Quantity <= 0)
{
yield return new ValidationResult("Invalid quantity",
new [] { "Quantity" });
}
if (this.Timestamp > DateTime.Now)
{
yield return new ValidationResult("Order date
is in the future", new [] { "Timestamp" });
}
}
}
自我验证之后,让我们继续进行自定义验证。
自定义验证
自定义验证的另一个选项涉及挂钩一个新的模型验证器提供者和一个定制的模型验证器。模型验证程序提供程序是IModelValidatorProvider的实例,例如:
public sealed class IsEvenModelValidatorProvider : IModelValidatorProvider
{
public void CreateValidators(ModelValidatorProviderContext context)
{
if (context.ModelMetadata.ModelType == typeof(string)
|| context.ModelMetadata.ModelType == typeof(int)
|| context.ModelMetadata.ModelType == typeof(uint)
|| context.ModelMetadata.ModelType == typeof(long)
|| context.ModelMetadata.ModelType == typeof(ulong)
|| context.ModelMetadata.ModelType == typeof(short)
|| context.ModelMetadata.ModelType == typeof(ushort)
|| context.ModelMetadata.ModelType == typeof(float)
|| context.ModelMetadata.ModelType == typeof(double))
{
if (!context.Results.Any(x => x.Validator is
IsEvenModelValidator))
{
context.Results.Add(new ValidatorItem
{
Validator = new IsEvenModelValidator(),
IsReusable = true
});
}
}
}
}
这将检查目标属性(context.ModelMetadata是否为预期类型(数字或字符串)之一,然后添加一个IsEvenModelValidator属性。触发验证时,将调用此验证程序。
为了便于完成,以下是其代码:
public sealed class IsEvenModelValidator : IModelValidator
{
public IEnumerable<ModelValidationResult>
Validate(ModelValidationContext context)
{
if (context.Model != null)
{
try
{
var value = Convert.ToDouble(context.Model);
if ((value % 2) == 0)
{
yield break;
}
}
catch { }
}
yield return new ModelValidationResult(
context.ModelMetadata.PropertyName,
$"{context.ModelMetadata.PropertyName} is not even.");
}
}
此验证程序代码尝试将数字转换为double值(因为它更通用),然后检查数字是否为偶数。如果值为null或不可转换,则只返回空结果。
阻止验证
如果您不希望您的模型验证整个类或一个或多个属性,可以对其应用[ValidateNever]属性。这实现了IPropertyValidationFilter接口,该接口可用于在验证过程中选择性地包括或排除属性。然而,我发现[ValidateNever]属性的实现方式没有多大意义,因为它迫使您将其包含在模型类中,而不是模型参数中,我认为这更有意义。
自动验证
在第 4 章、控制器和动作中,我们看到了如何注册一个过滤器,用于触发自动模型验证,当您使用POST时,这种情况已经发生了——并相应地执行动作。更多信息请参阅本章。
客户端模型验证
因为服务器端验证需要 post,所以有时它更有用,并提供更好的用户体验,以便在客户端执行验证。让我们看看怎么做。
所有内置验证器还包括客户端行为;这意味着,如果使用默认情况下包含在 ASP.NET Core 模板中的 jQuery 的非干扰性验证,则会自动获得它。不引人注目的验证需要以下 JavaScript 模块:
- jQuery 本身(
jquery-xxx.js:https://jquery.com/ - jQuery 验证(
jquery.validate.js:https://jqueryvalidation.org/ jquery.validate.unobtrusive.js:https://github.com/aspnet/jquery-validation-unobtrusive
实际的文件名可能略有不同(最小化版本或包含版本号),但仅此而已。它们默认安装为wwwroot\lib\jquery、wwwroot\lib\jquery-validation和wwwroot\lib\jquery-validation-unobtrusive。
在幕后,包含的验证程序向每个属性添加 HTML5 属性(data-*,以验证 HTML 表单元素,并且在表单即将提交时,强制进行验证。客户端验证仅在启用时执行(下一主题将对此进行详细介绍)。
配置
客户端验证提供程序通过AddViewOptions方法配置,该方法采用一个 lambda 函数,该函数公开MvcViewOptions:
ClientModelValidatorProviders(IList<IClientModelValidatorProvider>):注册的客户模型验证人;默认情况下,它包含一个DefaultClientModelValidatorProvider属性、一个DataAnnotationsClientModelValidatorProvider属性和一个NumericClientModelValidatorProvider属性。HtmlHelperOptions.ClientValidationEnabled(bool:是否启用客户端验证。默认为true,表示已启用。ValidationMessageElement(string:用于插入每个已验证属性的验证错误消息的 HTML 元素。默认值为span。ValidationSummaryMessageElement(string:用于插入模型的验证错误消息摘要的 HTML 元素。默认值为span。
包含的IClientModelValidatorProvider属性具有以下目的:
DefaultClientModelValidatorProvider:如果验证属性实现了IClientModelValidator,则无论是否有特定的客户机模型验证程序提供程序,都会使用它进行验证。NumericClientModelValidatorProvider:限制文本框仅包含数值。DataAnnotationsClientModelValidatorProvider:添加对所有包含的数据注释验证程序的支持。
自定义验证
您当然可以推出自己的客户端验证程序;它的核心是IClientModelValidator和IClientModelValidatorProvider接口。从前面看到的IsEvenAttribute属性开始,让我们看看如何在客户端实现相同的验证。
首先,让我们注册一个客户端模型验证程序提供程序:
services
.AddMvc()
.AddViewOptions(options =>
{
options.ClientModelValidatorProviders.Add(new
IsEvenClientModelValidatorProvider());
});
IsEvenClientModelValidatorProvider属性的代码如下:
public sealed class IsEvenClientModelValidatorProvider : IClientModelValidatorProvider
{
public void CreateValidators(ClientValidatorProviderContext context)
{
if (context.ModelMetadata.ModelType == typeof(string)
|| context.ModelMetadata.ModelType == typeof(int)
|| context.ModelMetadata.ModelType == typeof(uint)
|| context.ModelMetadata.ModelType == typeof(long)
|| context.ModelMetadata.ModelType == typeof(ulong)
|| context.ModelMetadata.ModelType == typeof(short)
|| context.ModelMetadata.ModelType == typeof(ushort)
|| context.ModelMetadata.ModelType == typeof(float)
|| context.ModelMetadata.ModelType == typeof(double))
{
if (context.ModelMetadata.ValidatorMetadata.
OfType<IsEvenAttribute>().Any())
{
if (!context.Results.Any(x => x.Validator is
IsEvenClientModelValidator))
{
context.Results.Add(new ClientValidatorItem
{
Validator = new IsEvenClientModelValidator(),
IsReusable = true
});
}
}
}
}
}
这需要一些解释。调用CreateValidators基础结构方法,为客户机模型验证程序提供程序提供添加自定义验证程序的机会。如果当前正在检查的属性(context.ModelMetadata)属于受支持的类型(context.ModelMetadata.ModelType)、数字或字符串之一,并且同时包含IsEvenAttribute属性且不包含任何IsEvenClientModelValidator属性,我们将以包含IsEvenClientModelValidator属性的ClientValidatorItem形式向 validators 集合(context.Results中添加一个属性,可以安全地重复使用(IsReusable,因为它不保持任何状态。
现在,让我们看看IsEvenClientModelValidator属性是什么样子的:
public sealed class IsEvenClientModelValidator : IClientModelValidator
{
public void AddValidation(ClientModelValidationContext context)
{
context.Attributes["data-val"] = true.ToString().
ToLowerInvariant();
context.Attributes["data-val-iseven"] = this.GetErrorMessage
(context);
}
private string GetErrorMessage(ClientModelValidationContext context)
{
var attr = context
.ModelMetadata
.ValidatorMetadata
.OfType<IsEvenAttribute>()
.SingleOrDefault();
var msg = attr.FormatErrorMessage(context.
ModelMetadata.PropertyName);
return msg;
}
}
它的工作原理如下:
- 将向用于编辑模型属性的 HTML 元素添加两个属性:
data-val:这意味着该元素应该被验证。data-val-iseven:元素无效时用于iseven规则的错误消息。
- 从
IsEvenAttribute属性的FormatErrorMessage方法检索错误消息。我们知道有IsEvenAttribute;否则,我们就不会在这里了。
最后,我们需要以某种方式添加 JavaScript 验证代码,可能在一个单独的.js文件中:
(function ($) {
var $jQval = $.validator;
$jQval.addMethod('iseven', function (value, element, params) {
if (!value) {
return true;
}
value = parseFloat($.trim(value));
if (!value) {
return true;
}
var isEven = (value % 2) === 0;
return isEven;
});
var adapters = $jQval.unobtrusive.adapters;
adapters.addBool('iseven');
})(jQuery);
我们在这里所做的是在iseven名称下注册一个自定义 jQuery 验证函数,该函数在启动时检查值是否为空,并尝试将其转换为浮点数(这适用于整数和浮点数)。最后,它检查该值是否为偶数并适当返回。不用说,这个验证函数是由不引人注目的验证框架自动连接的,因此您不必担心它没有验证。
The error message is displayed in both the element-specific error message label and in the error message summary if it is present in the view.
您可能会发现这个过程有点复杂,在这种情况下,您会很高兴知道您可以将 validation 属性和IClientModelValidator实现添加到一起;它将同样工作,这是可能的,因为包含了DefaultClientModelValidatorProvider属性。然而,由于单一责任原则(SRP)和关注点分离(SoC),因此建议将其分开。
在本节中,我们了解了如何编写在客户端或服务器端工作的自定义验证器。现在,让我们看看如何实现 AJAX 体验。
使用 AJAX 进行验证
AJAX 是一个很久以前创造的术语,用来表示现代浏览器的一个特性,通过它,可以通过 JavaScript 或浏览器完成异步 HTTP 请求,而无需重新加载整个页面。
ASP.NET Core 不提供对 AJAX 的任何支持,这并不意味着您不能使用它,只是您需要手动操作。
下面的示例使用 jQuery 检索表单中的值,并将它们发送给操作方法。确保 jQuery 库包含在视图文件或布局中:
<form>
<fieldset>
<div><label for="name">Name: </label></div>
<div><input type="text" name="name" id="name" />
<div><label for="email">Email: </label></div>
<div><input type="email" name="email" id="email" />
<div><label for="gender">Gender: </label></div>
<div><select name="gender" id="gender">
<option>Female</option>
<option>Male</option>
</select></div>
</fieldset>
</form>
<script>
$('#submit').click(function(evt) {
evt.preventDefault();
var payload = $('form').serialize();
$.ajax({
url: '@Url.Action("Save", "Repository")',
type: 'POST',
data: payload,
success: function (result) {
//success
},
error: function (error) {
//error
}
});
});
</script>
本节 JavaScript 代码执行以下操作:
- 将单击事件处理程序绑定到 ID 为
submit的 HTML 元素。 - 序列化所有的
form元素。 - 在名为
RepositoryController的控制器中,创建对名为Save的控制器操作的POSTAJAX 请求。 - 如果 AJAX 调用成功,则调用
success函数;否则,将调用一个error函数。
The URL to the controller action is generated by the Action method. It is important not to have it hardcoded but to instead rely on this HTML helper to return the proper URL.
现在让我们看看如何使用内置机制执行 AJAX 风格的验证。
验证
其中一个包含的验证属性[Remote]使用 AJAX 透明地在服务器端执行验证。当应用于模型的属性时,它采用一个控制器和一个必须引用现有控制器操作的action参数:
[Remote(action: "CheckEmailExists", controller: "Validation")]
public string Email { get; set; }
此控制器操作必须具有与此类似的结构,当然减去操作的参数:
[AcceptVerbs("Get", "Post")]
public IActionResult CheckEmailExists(string email)
{
if (this._repository.CheckEmailExists(email))
{
return this.Json(false);
}
return this.Json(true);
}
本质上,如果验证成功,它必须返回 JSON 格式的值true,否则返回false。
This validation can not only be used for a simple property of a primitive type (such as string) but also for any POCO class.
实施限制
在 ASP.NET MVC 的早期(预核心)版本中,有一个属性[AjaxOnly],可用于限制只能由 AJAX 调用的操作。虽然它不再存在,但通过编写一个资源过滤器很容易将其恢复,如下所示:
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class AjaxOnlyAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
public void OnResourceExecuting(ResourceExecutingContext context)
{
if (context.HttpContext.Request.Headers["X-Requested-With"]
!= "XMLHttpRequest")
{
context.Result = new StatusCodeResult
((int)HttpStatusCode.NotFound);
}
}
}
此属性实现资源过滤器接口IResourceFilter,这将在第 10 章理解过滤器中讨论,基本上,它所做的是检查是否存在特定标头(X-Requested-With),如果其值为XMLHttpRequest,则表示当前请求正在由 AJAX 执行。如果不是,则设置响应结果,从而使任何其他可能的过滤器短路。要应用它,只需将其放置在要限制的操作旁边:
[AjaxOnly]
public IActionResult AjaxOnly(Model model) { ... }
For an overview of AJAX and the XMLHttpRequest object, please see https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest.
在此之后,我们继续学习如何从 AJAX 返回内容。
从 AJAX 返回内容
根据最佳实践,AJAX 端点应该返回数据;在现代世界,当涉及到 web 应用时,这些数据通常是 JSON 格式的。因此,您最有可能使用JsonResult类将内容返回到客户机代码。至于向服务器发送数据,如果您使用 jQuery,它将为您处理所有事情,并且可以正常工作。否则,您将需要将数据序列化为适当的格式,可能还有 JSON。设置适当的内容类型标题,然后退出。
上传文件
文件上传是一个过程,在这个过程中,我们将文件从计算机发送到一个运行 ASP.NET Core 的服务器。在 HTTP 中上载文件需要两件事:
- 你必须使用
POST动词。 - 必须在表单上设置
multipart/form-data编码。
就 ASP.NET Core 而言,包含的模型绑定器知道如何将任何发布的文件绑定到IFormFile对象(或对象集合)。例如,采取如下形式:
@using (Html.BeginForm("SaveForm", "Repository", FormMethod.Post,
new { enctype = "multipart/form-data" }))
{
<input type="file" name="file" />
<input type="submit" value="Save"/>
}
您可以使用以下操作方法检索文件:
[HttpPost("[controller]/[action]")]
public IActionResult SaveForm(IFormFile file)
{
var length = file.Length;
var name = file.Name;
//do something with the file
return this.View();
}
但是,HTML 文件上传规范(https://www.w3.org/TR/2010/WD-html-markup-20101019/input.file.html 还提到了使用multiple属性一次提交多个文件的可能性。在这种情况下,您可以将参数声明为IFormFile实例数组(集合也可以使用):
public IActionResult SaveForm(IFormFile[] file) { ... }
IFormFile界面为您提供了操作这些文件所需的一切:
-
ContentType(string:投递文件的内容类型 -
ContentDisposition(string:包含 HTML 输入名称和所选文件名的内部内容处置头 -
Headers(IHeaderDictionary:随文件发送的任何头文件 -
Length(long:已发布文件的长度,以字节为单位 -
Name(string:发起文件上传的输入元素的 HTML 名称 -
FileName(string:保存发布文件的文件系统中的临时文件名
通过使用CopyTo和CopyToAsync,您可以轻松地将发布文件的内容作为字节数组从Stream源复制到另一个源。OpenReadStream允许您查看实际文件内容。
默认的文件上传机制使用文件系统中的临时文件,但是您可以推出您的机制。有关更多信息,请参阅 Microsoft 发布的以下帖子:
https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads 。
直接访问提交的文件
还可以直接访问HttpContext.Request.Form.Files集合。此集合的原型为IFormFileCollection,它公开了一个IFormFile集合。
本章将结束介绍如何使用文件。大多数复杂的应用都需要这一点,因此了解这一点很有用。
总结
本章涉及来自用户的数据以及因此需要验证的数据;否则,即使格式不正确,也可能提交无效信息。读完本章后,您应该能够设计一个表单来接收复杂的数据结构并对其进行验证。
对于验证,如果需要,您可能应该坚持使用数据注释属性和IValidatableObject实现。这些 API 在大量其他.NET API 中使用,几乎是验证的标准。
最好实现客户端验证和 AJAX,因为它提供了更好的用户体验,但千万不要忘记在服务器端进行验证!
可能不需要定制模型活页夹,因为包含的活页夹似乎涵盖了大多数情况。
显示和编辑器模板非常方便,因此您应该尝试使用它们,因为它可能会减少每次需要添加的代码,特别是如果您希望重用它的话。
在本章中,我们了解了如何使用模型,为模型生成 HTML,包括在前端和后端使用模板验证模型,查看验证错误消息,以及如何将模型与 HTML 表单元素绑定。
在下一章中,我们将讨论一个完全不同的主题!
问题
您应该能够回答这些问题,答案见评估部分:
- 默认的验证提供程序是什么?
- 我们将用于呈现 HTML 字段的方法称为什么?
- 什么是模型元数据?
- ASP.NET Core 是否支持客户端验证?
- 可以绑定到上传文件的基本接口是什么?
- 什么是不引人注目的验证?
- 我们如何执行服务器端验证?
七、实现 Razor 页面
本章介绍 Razor Pages,这是 ASP.NET Core 2.0 中引入的一项功能,它提供了一个不使用控制器的简化开发模型。
通过学习本章,我们将能够开发由数据驱动的动态网站。
我们将讨论以下内容:
- 资产搜查令
- 使用页面模型
- 剃刀观点的共性
- 强制执行安全
技术要求
要实现本章中介绍的示例,您需要.NET Core 3 SDK 和文本编辑器。当然,VisualStudio2019(任何版本)满足所有要求,但您也可以使用 VisualStudio 代码。
源代码可以在这里从 GitHub 检索:https://github.com/PacktPublishing/Modern-Web-Development-with-ASP.NET-Core-3-Second-Edition
开始
剃须刀页面是在 ASP.NET Core 2.0 中引入的,它们采用了与 ASP.NET Core 其他部分完全不同的方法。Razor 页面不是 MVC 模式,而是自包含的文件,类似于 XAML 控件或 ASP.NET Web 表单,因为它们也可以有代码隐藏文件。不再存在控制器/视图分离,因为 Razor 页面在一个文件中拥有它们所需的所有内容,尽管我们也可以为它们指定一个类。
要使用 Razor 页面,您需要一个兼容的 Visual Studio 版本,从 2017 Update 3 开始,另外还需要安装 ASP.NET Core 2.0 或更高版本:

Razor 页面物理上存储在文件系统中,位于Pages文件夹下(这是惯例),页面应具有与常规 Razor 视图相同的.cshtml扩展名。与之不同的是新的@page指令。显示的代码如下所示:
@page
@model HelloWorldModel
<!DOCTYPE html>
<html>
<head><title>Hello World</title></head>
<body>
<h1>@Html.Raw("Hello, World!")</h1>
</body>
</html>
添加一个@page指令(最好作为第一行)会自动将.cshtml文件变成一个页面。不需要引用任何特定的 NuGet 包或执行任何配置,因为它在默认情况下处于启用状态。
访问 Razor 页面非常简单;由于不涉及路由,可以直接调用,不需要.cshtml分机:
/HelloWorld/Admin/Settings
唯一的要求是页面位于Pages根文件夹中的某个位置。Index.cshtml文件默认为送达,即如果其中一个文件位于Pages\Admin文件夹内,则无需明确请求即可送达;/Admin将提供\Pages\Admin\Index.cshtml文件。
要路由到 Razor 页面,需要使用新的端点路由机制显式启用它:
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
您甚至可以使用新的RequireHost扩展方法来确保只有在使用某些主机头或端口时才能访问 Razor 页面。不要忘记注册 it 所需的服务:
services.AddRazorPages();
Leave the Pages prefix and the .cshtml extension out; they cannot be used in the request. Also, Razor pages cannot start with an underscore (_).
让我们从资产搜索顺序开始,了解 Razor 页面的位置。
资产搜查令
Razor Pages 资产(.cshtml文件)将按以下文件夹和顺序进行搜索:
Pages中的当前文件夹/Pages/Shared//Views/Shared/
这意味着用户请求的视图名称或布局将首先在当前文件夹(根据请求的路径)中查找,然后在/Pages/Shared文件夹中查找,最后在/Views/Shared中查找—所有这些都与应用的根文件夹相关。
因此,在学习了基础知识之后,让我们跳转到页面模型,这是一个非常重要的概念。
使用页面模型
您可以使用与 Razor 视图完全相同的语法,但还有更多的东西;Razor 页面本身就有一个与之关联的PageModel类,请注意指向HelloWorldModel的@model指令。该类必须继承自PageModel,在该类中可以定义处理 HTTP 方法的方法,如GET或POST。包含页面模型类定义的文件必须与具有.cs扩展名的 Razor 页面具有相同的物理名称,位于同一文件夹中,并从PageModel继承。因此,例如,如果前一个文件名为HelloWorld.cshtml,则其页面模型将放在HelloWorld.cshtml.cs文件中:
public class HelloWorldModel : PageModel
{
}
如果您不希望指定自定义页面模型类,系统会自动为您提供一个,您仍然可以在.cshtml文件中直接指定处理程序方法:
@functions
{
public async Task<IActionResult> OnGetAsync()
{
if (!this.User.Identity.IsAuthenticated)
{
return this.RedirectToAction(actionName: "Login",
controllerName: "Account");
}
return this.Page();
}
}
考虑下面的属性,例如,您可以在 AutoT0-派生类中声明:
public string Message { get; set; }
public void OnGet()
{
this.Message = "Hello, World!";
}
然后可以在.cshtml文件中使用这些选项:
<p>Message: @Model.Message</p>
您甚至可以在此处声明该类:
@page
@model IndexModel
@functions
{
public class IndexModel : PageModel
{
public async Task<IActionResult> OnGetAsync()
{
//whatever
}
}
}
PageModel类提供以下属性:
-
HttpContext(HttpContext:通常的上下文 -
ModelState(ModelStateDictionary:模型状态,由所有值提供者填写 -
PageContext(PageContext):提供对当前处理程序方法(如果有)的访问,以及值提供程序和查看开始工厂集合 -
Request(HttpRequest):与请求对象HttpContext.Request相同的值 -
Response(HttpContext.Response中的HttpResponse:响应对象 -
RouteData(RouteData:路由数据,一般不需要 -
TempData``ITempDataDictionary:临时数据 -
Url(IUrlHelper):用于生成指向路由动作的 URL,例如User(ClaimsPrincipal来自HttpContext.User:当前用户,由使用中的认证机制确定 -
ViewData(ViewDataDictionary):视图包,如第 4 章、控制器和动作中介绍的
这是关于页面模型的一般信息,现在让我们详细了解这些特性;首先,让我们看看如何实现页面处理程序。
了解页面处理程序
HTTP 方法处理程序可以有多个签名:
- 名称必须以
On开头,后跟 HTTP 方法名称(Get、Post、Put、Delete等)。 - 返回类型必须为
void或IActionResult。 - 如果我们要使用异步版本,则该方法必须返回
Task或Task<IActionResult>,并可选地应用async关键字,并且应该以Async后缀结尾。 - 它们可以采用参数(具有默认值的基本类型或复杂类型),完全不采用参数,也可以采用
IFormCollection参数。
现在,您可以添加处理请求的方法,可以是同步的,如下所示:
public IActionResult OnGet()
{
if (this.HttpContext.Request.Headers["HTTP-
Referer"].SingleOrDefault().Contains("google.com") == true)
{
//hey, someone found us through Google!
}
return this.Page();
}
或者,它们可以异步处理请求:
public async Task<IActionResult> OnGetAsync()
{
//...
return this.Page();
}
You cannot have both a synchronous and an asynchronous handler method or multiple overloads for the same HTTP verb, as it will result in a runtime error.
您甚至可以拥有不遵循这些模式的自定义处理程序。实现这一目标的几种方法如下:
- 在查询字符串中传递一个
handler参数,例如?handler=MyHandler。 - 改为传递路由中的
handler参数,例如@page "{handler?}"。 - 在
<form>、<input>或<button>标记中,设置asp-page-handler属性,例如asp-page-handler="MyHandler"(这使用标记处理程序功能)。
通过这种方式,您可以使用以下方法:
public async Task<IActionResult> OnPostMyHandlerAsync() { ... }
不管您给它起什么名字,如果它是一个异步处理程序,您总是会有On前缀和Async后缀。
如果希望将页面发布到多个处理程序,根据单击的内容,很容易:
<form method="post">
<input type="submit" value="One Handler" asp-page-handler="One" />
<input type="submit" value="Another Handler" asp-page-handler="Two" />
</form>
要使其工作,两个按钮必须位于具有POST方法的表单中,并且默认标记帮助程序必须在_ViewImports.cshtml中注册:
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
处理程序的名称必须遵循约定。对于本例,您可以按如下方式使用它们:
public void OnPostOne() { ... }
public async Task<IActionResult> OnPostTwoAsync() { ... }
这只是一个示例,它们可以是异步的,也可以不是异步的,还可以返回值。在内部,他们可以通过返回IActionResult执行重定向等任务:
public async Task<IActionResult> OnPostTwoAsync()
{
return this.RedirectToPage("/Pages/Success");
}
但并非所有的行动结果都有意义;例如,返回ViewResult没有意义,因为 Razor 页面不会在控制器的上下文中执行。如果不需要,您甚至不需要返回IActionResult:
public void OnGet()
{
//initialize everything
}
这些可以作为返回IActionResults的助手,其方式与ControllerBase和Controller类基本相同:
-
Challenge(ChallengeResult) -
Content(ContentResult) -
File(FileContentResult、FileStreamResult、VirtualFileResult) -
Forbid(ForbidResult) -
LocalRedirect(LocalRedirectResult) -
LocalRedirectPermanent(LocalRedirectResult) -
LocalRedirectPermanentPreserveMethod(LocalRedirectResult) -
LocalRedirectPreserveMethod(LocalRedirectResult) -
NotFound(NotFoundResult、NotFoundObjectResult) -
Page(PageResult) -
PhysicalFile(PhysicalFileResult) -
Redirect(RedirectResult) -
RedirectPermanent(RedirectResult) -
RedirectPermanentPreserveMethod(RedirectResult) -
RedirectPreserveMethod(RedirectResult) -
RedirectToAction(RedirectToActionResult) -
RedirectToActionPermanent(RedirectToActionResult) -
RedirectToActionPermanentPreserveMethod(RedirectToActionResult) -
RedirectToActionPreserveMethod(RedirectToActionResult) -
RedirectToPage(RedirectToPageResult) -
RedirectToPagePermanent(RedirectToPageResult) -
RedirectToPagePermanentPreserveMethod(RedirectToPageResult) -
RedirectToPagePreserveMethod(RedirectToPageResult) -
RedirectToRoute(RedirectToRouteResult) -
RedirectToRoutePermanent(RedirectToRouteResult) -
RedirectToRoutePermanentPreserveMethod(RedirectToRouteResult) -
RedirectToRoutePreserveMethod(RedirectToRouteResult) -
SignIn(SignInResult) -
SignOut(SignOutResult) -
StatusCode(StatusCodeResult、ObjectResult) -
Unauthorized(UnauthorizedResult)
其中一些方法提供重载,每个方法都可以返回不同的结果类型。
最后,如果需要,可以将参数传递给处理程序:
<input type="submit" value="Third Handler" asp-page-handler="Three" asp-route-foo="bar" />
只需在处理程序上声明一个参数:
public void OnPostThree(string foo)
{
//do something with the value of foo
}
在了解了如何实现页面处理程序之后,现在让我们看看如何将请求绑定到类模型。
做模型绑定
如果您在页面模型类(或@functions块中声明一个属性,并使用[BindProperty]属性对其进行修饰,它将自动绑定,使用与前一章中所述相同的规则(绑定源提供程序和绑定属性):
[BindProperty]
public Order Order { get; set; }
然后,您将能够访问和更改它的任何属性,可能是在 HTTP 处理程序方法中。您也可以通过BinderType属性提供自己的活页夹。如果BindProperty的SupportsGet属性设置为true,则BindProperty也可以绑定GET调用。
如果愿意,还可以将[BindProperties]属性应用于整个类,其所有属性将自动绑定:
[BindProperties]
public class Model
{
public int OneProperty { get; set; }
public string AnotherProperty { get; set; }
}
Do notice that properties bound this way will only be so for non-GET calls (typically POST) unless you set its SupportsGet property (both [BindProperty] and [BindProperties] have SupportsGet). It works pretty much the same as [ModelBinder], but the latter never binds on GET requests.
此外,与控制器操作类似,HTTP 处理程序方法中的参数也会自动绑定:
public void OnGet(int? id = null)
{
//?id=1212
}
您可以选择不将模型声明为处理程序方法签名的一部分,而是动态更新它:
public void OnPost()
{
var model = new OrderModel();
this.TryUpdateModel(model);
}
这样做的一个可能原因是,同一个页面处理不同的请求,从而处理不同的模型。
现在我们已经了解了如何将请求转换为类,现在是学习如何验证它的时候了!
进行模型验证
模型验证的工作方式与控制器中的工作方式基本相同:
public IActionResult OnPost()
{
var model = new OrderModel();
this.TryUpdateModel(model);
if (this.TryValidateModel(model))
{
return this.RedirectToPage("/Pages/Error");
}
return this.Page();
}
与控制器类似,ModelState属性还跟踪所有注入值及其验证状态。
维持状态
所有常用的数据持久化方法也适用于 Razor 页面,因此这里没有什么特别值得一提的。
使用视图布局
Razor 页面可以使用与视图相同的布局功能,但建议您将布局页面保留在Views\Shared文件夹之外,因为这是为视图保留的。
使用局部视图
与视图布局一样,也以完全相同的方式支持局部视图。
现在让我们看看如何支持这些区域。
使用区域
从 ASP.NET Core 2.1 开始,Razor 页面还支持以下区域。区域是一种在应用内部、文件夹中物理分离模块的方法。这仅仅意味着 Razor 页面可以在这些文件夹中寻址,如以下示例所示:
请注意,必须在项目根目录上的Areas文件夹下创建这些文件夹,如下所示:

在每个命名区域内,我们必须创建一个Pages文件夹。在它里面,你可以放任何你喜欢的东西,比如.cshtml文件、_ViewStart.cshml和许多其他文件。默认情况下,区域处于启用状态。
是时候提一下 Razor 视图和 Razor 页面中存在的特殊文件了。
特殊文件
Razor 页面尊重_ViewStart.cshtml和_ViewImports.cshtml文件,并以与常规 Razor 视图相同的方式处理它们,即在实际页面之前调用它们。它们也适用于不同的区域,这意味着您可以拥有不同的文件,每个区域一个。
接下来,让我们讨论过滤器。
使用过滤器
Razor 页面可以使用除操作过滤器之外的任何过滤器。这些过滤器不会被触发,因为您没有操作。还有一个新的过滤器IPageFilter,还有一个异步版本IAsyncPageFilter。我已经在“使用过滤器”一节中谈到了它们,所以我在这里不再重复。
我们将在下面看到,依赖注入也受到支持。
使用依赖注入
您可以按照通常的方式将依赖项注入页面模型类的构造函数中:
public class HelloWorldModel : PageModel
{
public HelloWorldModel(IMyService svc)
{
//yes, dependency injection in the constructor also works!
}
}
如果您在自定义页面模型中用[FromServices]修饰一个属性,它将被接受,并且该属性的值将从依赖项注入框架及其声明的类型中设置。
您也可以使用@inject指令,就像在 Razor 视图中一样。
现在,我们将了解如何配置特定于 Razor 页面的选项。
配置选项
AddRazorPagesOptions扩展方法可以在AddMvc之后调用,这样我们可以配置 Razor 页面的一些选项:
services
.AddMvc()
.AddRazorPagesOptions(options =>
{
options.RootDirectory = "/Pages";
});
RazorPagesOptions类提供以下属性:
AllowAreas(bool:是否允许区域默认为falseAllowMappingHeadRequestsToGetHandler(bool):如果剃须刀页面(或其型号)没有为HEAD提供处理程序,HEAD请求是否会变成GET请求-默认为falseConventions(IList<IApplicationModelConvention>):将在未来的一章中讨论使用此选项的约定RootDirectory(string:相对于应用根目录的根目录,通常设置为/Pages
此外,还有一些扩展方法是通过RazorPagesOptions配置的,基本上增加了一个或多个约定:
AllowAnonymousToFolder:允许匿名请求特定文件夹下的所有页面AllowAnonymousToPage:允许对给定页面进行匿名请求AuthorizeFolder:为特定文件夹下的所有页面定义授权策略(这将在第 11 章、安全中进行更深入的讨论)AuthorizePage:定义特定页面的授权策略ConfigureFilter:允许全局过滤器的配置(添加和删除)
请查看以下示例:
services
.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AllowAnonymousToPage("/Pages/HelloWorld");
});
页面路由是一种特殊的配置,我们将在下面看到。
了解页面路由
除了直接调用 Razor 页面外,您还可以让它们应答路由。RazorPagesOptions有一种新的AddPageRoute扩展方法,您可以利用它向页面添加友好路由:
services
.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AddPageRoute("/Order", "My/Order/{id:int}");
});
有趣的是,我们可以看到 Razor 页面在某种程度上依赖于 MVC 框架。
AddPageRoute的参数如下:
pageName(string):指向的页面名称,以/开头,不带.cshtml后缀route(string:常规路由,可能有一些路由或查询字符串参数
在视图中,您可以使用HttpContext.RouteData或HttpContext.Request.Query访问任何路由或查询字符串参数。
有趣的是,下面是如何将页面(/HelloWorld设置为默认页面的:
.AddRazorPagesOptions(options =>
{
options.Conventions.AddPageRoute("/HelloWorld", "");
});
此外,通过向page指令添加路由模板参数,您可以让 Razor 页面监听特定路由:
@page "{id:int}"
在这种情况下,如果调用 Razor 页面时没有使用id参数,该参数也必须是int类型,则不会找到该页面,而是返回 HTTP404错误。
接下来,如何在 Razor 页面中强制执行安全规则。
强制执行安全
我们可以通过两种方式在 Razor 页面上强制执行安全规则:
- 通过将
[Authorize]属性应用于页面模型或页面处理程序 - 通过定义约定
让我们从属性方法开始。
使用[Authorize]属性
整个页面都很简单:
[Authorize]
public class AdminIndexModel: PageModel
{
}
也可以将其用于单个处理程序:
public class AdminIndexModel: PageModel
{
[Authorize]
public void OnGet() { ... }
}
现在,让我们继续讨论公约。
习俗
使用AddRazorPagesOptions扩展方法,我们可以控制如何将安全性应用于一个或多个页面或文件夹。可用的方法如下:
AllowAnonymousToPage:授予对单个页面的匿名访问权限AllowAnonymousToFolder:授予对给定文件夹下所有页面的匿名访问权限AuthorizePage:定义页面的授权策略AuthorizeFolder:为文件夹下的所有页面定义授权策略
下面是一个例子:
services
.AddMvc()
.AddRazorPagesOptions(options =>
{
//adds an AuthorizeAttribute with a named Policy property
options.Conventions.AuthorizePage("/ShoppingBag",
"Authenticated");
//adds an AuthorizeAttribute
options.Conventions.AuthorizeFolder("/Products");
//adds an AllowAnonymousAttribute
options.Conventions.AllowAnonymousToPage("/Login");
options.Conventions.AllowAnonymousToFolder("/Images");
});
在这里,我们确保"/ShoppingBag"端点仅适用于"Authenticated"策略,并确保尝试访问"/Products"的任何人都需要授权。最后,任何人都可以使用"/Login"和"/Images"URL,包括匿名用户。
现在,我们可以从 XSS 攻击中学到什么?
跨站点请求脚本
服务器上的 Razor 页面默认情况下会检查跨站点请求脚本(XSS攻击。如果您想在 Razor 页面中使用 AJAX,请确保在页面中包含防伪令牌,并在每个 AJAX 请求中发送标题,如第 11 章、**安全中的防伪保护部分所述。
*# 总结
首先,在常规视图和 Razor 页面之间进行选择是一个需要提前做出的决定,因为它们太不一样了。拥有控制器和视图对以前使用过 MVC 的人来说可能更具吸引力,我想说这可以带来更好的耦合和组织,但是 Razor 页面非常容易使用,不需要服务器端代码,也不需要重新编译(如果不使用页面模型的话)。
继续使用局部视图和视图布局,因为它们是提高重用性的良好机制。
与控制器操作一样,Razor 页面也存在同样的安全问题。最好选择约定而不是属性,因为我们有一个存储安全信息的中心位置。
在本章中,我们介绍了 ASP.NET Core 2 的新 Razor Pages 功能,该功能虽然与普通视图不同,但共享相当多的功能。它可以用于更简单的解决方案,而不需要控制器和操作的所有麻烦。
在下一章中,我们将了解如何提取有关 ASP.NET Core 中发生的事情的信息。
问题
您现在应该能够回答以下问题:
- Razor 页面是否使用代码隐藏?
- 页面模型的用途是什么?
- 什么是页面处理程序?
- 如何限制匿名用户调用 Razor 页面?
- 我们可以通过哪两种方式将服务注入 Razor 页面?
- 剃须刀页面是否使用页面布局?
- 默认情况下,Razor 页面在哪里提供服务?*
八、API 控制器
本章介绍 API 控制器。API 控制器只是一个 MVC 控制器,它不返回 UI,而是处理请求和有效负载,并以机器可读的格式(如 JSON 或 XML)返回响应。我们将讨论与 API 控制器相关的许多方面,从安全性到版本控制。
本章将介绍以下主题:
- 休息导论
- 模型绑定
- 授权访问资源
- 应用 OpenAPI 约定
- 返回验证结果
- 执行内容协商
- 处理错误
- 理解 API 版本控制
- 生成 API 文档
- 小田
到本章结束时,我们将能够在不需要太多人工交互的情况下,全面使用身份验证和验证。
技术要求
为了实现本章中介绍的示例,您需要.NET Core 3 SDK 和文本编辑器。当然,VisualStudio2019(任何版本)满足所有要求,但您也可以使用 VisualStudio 代码。
本章的源代码可从的 GitHub 中检索 https://github.com/PacktPublishing/Modern-Web-Development-with-ASP.NET-Core-3-Second-Edition 。
web API 入门
并非所有操作都要返回 HTML(如视图)。某些返回内容仅适用于非人工处理,例如某些客户端 API。在这种情况下,其他内容比 HTML 更合适,即一种表示语言,如 JSON 或 XML。有时,只需要返回 HTTP 状态码或一些响应头。在过去,这是通过 ASP.NET MVC 之外的 API 实现的,比如微软的ASP.NET Web API()https://www.asp.net/web-api 、南希(http://nancyfx.org 或服务栈(https://servicestack.net 。
让我们看看 ASP.NET web API。它与 MVC 有许多相同的概念和类似的命名(和用途)API,但它是一个完全不同的项目,使用不同的程序集和不同的引导机制,如Open Web Interface for.NET(OWIN)。毫不奇怪,微软决定用 ASP.NET Core 统一 MVC 和 web API;现在,不再有 web API,只有 MVC。但是,可以在 MVC 上找到 API 的所有功能。
有一个称为代表性状态转移(REST的概念,它是事实上的标准,用于编写完全包含 HTTP 的 web 服务和 API,包括其动词、头和 URL。NET Core 允许我们编写符合 REST 建议的 web 服务。
API 控制器不同于非 API 控制器,因为前者不返回 UI-HTML 或其他,而是消耗和返回数据。这些数据基本上是机器可读的,并使用企业标准格式,如 XML 和 JSON。有时,可以协商可接受和返回的协议是什么。在任何情况下,无论何时收到数据,都应该对其进行验证。
ASP.NET Core 的 API 功能构建在 MVC 功能的基础上,但并不需要所有功能。信息技术因此,您可能需要添加 MVC 功能,如下所示:
services.AddMvc();
或者,您可以只使用 API 的最小值,这可能足以满足您的需要,并且使用更少的内存:
services.AddControllers();
现在我们已经了解了一些基本知识,让我们进一步深入了解 REST。
理解休息
REST是一种风格,而不是规定使用有意义的 URL 和 HTTP 动词的架构模式。
动词表示操作。以以下为例:
| HTTP 动词 | 意思是 |
| GET | 阅读 |
| PUT | 更新或替换 |
| POST | 创造 |
| PATCH | 部分更新 |
| DELETE | 删除 |
正如您所看到的,这类似于我们在 ASP.NET MVC 中所做的,但是 HTML 表单只使用POST和GET。
另一方面,URL 公开实体和标识符。举以下例子:
http://store.com/products/1http://profile.net/users/rjpereshttp://search.pt/term/rest+api
所有这些 URL 都有不同的含义;例如,如果使用GET动词调用每个 URL,它应该返回结果,并且不会产生任何副作用。对于POST,应创建新记录。PUT更新现有记录并DELETE删除基础记录。
可以想象,POST、PUT和PATCH所需的实际内容不能总是通过 URL 发送;如果内容复杂,则需要将其作为有效负载发送。GET和DELETE通常不采用复杂参数。
REST 对于创建创建、检索、更新和删除(CRUD)样式的应用特别有用。这些应用用于添加记录,例如博客文章。
最后,HTTP 状态代码表示操作的结果,例如:
-
200 OK:已成功检索实体。 -
201 Created:已成功创建实体。 -
202 Accepted:接受删除或更新实体。 -
204 No Content:请求已成功处理,但未返回任何内容。 -
404 Not Found:请求的实体不存在。 -
409 Conflict:保存与持久化版本冲突的实体。 -
422 Unprocessable Entity:实体验证失败。 -
501 Bad Request:发出了错误的请求。
For more information about the REST and RESTful APIs, please read https://searchmicroservices.techtarget.com/definition/RESTful-API.
在本节中,我们了解了 REST,它本质上是一种同意机制。现在让我们看一看 ASP.NET Core,特别是如何将请求转换为.NET 类。
模型绑定
通常,在使用 RESTAPI 时,我们使用POST、PUT或PATCH动词将内容作为有效负载发送。然后将该内容转换为 POCO 类,这些类被定义为动作方法的参数。
事实证明,如果从请求主体或查询字符串绑定,ASP.NET Core 可以将有效负载绑定到 POCO,但不能使用[Bind]、[BindNever]和[BindRequired]属性排除(或包括)特定属性。典型示例如下所示:
[ApiController]
public class PetController : ControllerBase
{
[HttpPost]
public IActionResult Post([FromBody] Pet pet) { ... }
}
这是因为 ASP.NET Core 使用输入格式化程序将请求绑定到模型,并且由于这些请求可能会更改,因此由它们决定是否应跳过哪些属性。例如,某个 JSON 序列化程序可能使用某些属性来配置属性序列化,而其他人可能会忽略这些属性。
授权访问资源
虽然表单通常使用用户名和密码对来实施身份验证,但 API 通常不是这样。然而,认证和授权的概念也适用;授权通过角色、声明或自定义规则来确保,但身份验证通常通过JSON Web 令牌(JWTs来实现。JWT 与 Cookie 类似,但 Cookie 存储在浏览器中,web API 通常不是由浏览器调用的,而是由 API 客户端调用的。ASP.NET Core 提供了一种机制,用于检查请求的身份验证以及检查请求者是否有权执行其想要执行的操作。本章的目的是解释如何做到这一点。
使用 JWTs
JWT 是 RFC 7519 中定义的开放标准,安全地表示使用 HTTP 进行通信的两个连接方之间的声明。该规范可在上获得 https://tools.ietf.org/html/rfc7519 。
使用 JWTs 类似于使用 Cookie 进行身份验证,但 Cookie 通常与人机交互相关,而 JWTs 在机器对机器的场景(如 web 服务)中更常见。使用 cookie 需要一个 cookie 容器,该容器可以容纳 cookie 并在每次请求时发送 cookie。正常情况下,浏览器会为我们这样做。但是,对于 web API,请求通常不是由浏览器发出的。
让我们看一个完整的例子。在深入研究代码之前,请确保将Microsoft.AspNetCore.Authentication.JwtBearerNuGet 包添加到代码中。
让我们看看如何通过查看一个简单的GenerateToken方法来生成令牌,例如,为给定用户名生成令牌:
private string GenerateToken(string username)
{
var claims = new Claim[]
{
new Claim(ClaimTypes.Name, username),
new Claim(JwtRegisteredClaimNames.Nbf,
new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds()
.ToString()),
new Claim(JwtRegisteredClaimNames.Exp,
new DateTimeOffset(DateTime.UtcNow.AddDays(1))
.ToUnixTimeSeconds().ToString()),
};
var token = new JwtSecurityToken(
new JwtHeader(new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes("<at-least-16-
character-secret-key>")),
SecurityAlgorithms.HmacSha256)),
new JwtPayload(claims));
return new JwtSecurityTokenHandler().WriteToken(token);
}
此代码允许任何拥有有效用户名/密码对的人请求持续 1 天的 JWT(DateTime.UtcNow.AddDays(1)。当然,密钥(<at-least-16-character-secret-key>可以通过配置生成,不应该真正硬编码。
现在,要设置认证,我们需要进入ConfigureServices;这是它在 ASP.NET Core 2.x 及更高版本中的外观:
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.
AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.
AuthenticationScheme;
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes("<at-least-16-character
-secret-key>")),
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5)
};
});
ClockSkew允许服务器与连接到服务器的任何客户端之间的时钟同步存在差异。在这种情况下,我们允许 5 分钟的容差,这意味着令牌过期时间少于 5 分钟的客户端仍将被接受。需要进入Configure将认证中间件添加到管道中:
app.UseAuthentication();
现在,JWT 将检查对具有[Authorize]属性的操作方法的任何请求,并且只有在该请求有效时才会被接受。为了确保实现这一点,您需要在所有请求(包括 AJAX 调用)中发送授权令牌;这是Authorization标题,看起来像这样:
Authorization: Bearer <my-long-jwt-authorization-token>
<my-long-jwt-authorization-token>值是根据前面显示的GenerateToken方法生成的值。
您可以使用多个公共站点(如【T0)】使用并生成有效的 JWT 令牌 https://jwt.io 。当然,您需要找到在请求期间存储令牌的方法(HTML 本地存储,例如参见https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage 了解更多信息)。如果令牌被篡改或达到其超时,您将获得授权错误。
如果愿意,可以指示 ASP.NET Core 使用不同的身份验证提供程序。例如,可以同时使用基于 cookie 和 JWT 的授权提供程序。对于 JWT,您只需要使用[Authorize]属性的AuthenticationSchemes属性,如下所示:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
以下内容可用于使用 cookie:
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
您可以在同一 ASP.NET Core 应用上混合使用不同的身份验证方案。
现在我们已经完成了身份验证,让我们看看 VisualStudio 提供的强制 REST 约定的机制。
应用 OpenAPI REST 约定
ASP.NET Core 2.2 将 API web 分析器引入 Visual Studio。这些分析器用于强制执行 REST 约定。简单地说,我们声明一个程序集或类应该遵循某种约定;然后,VisualStudio 检查其方法是否为 OpenAPI(Swagger)声明了正确的响应类型和状态代码,并在需要时通过添加正确的属性来修复此问题。这纯粹是一个设计时特性,而不是为更改而编写的代码。
Microsoft.AspNetCore.Mvc.Api.AnalyzersNuGet 包以DefaultApiConventions类的形式包含 RESTAPI 的一些标准约定。如果要确保当前程序集中的所有类型都遵循这些约定,我们将在程序集级别应用以下属性:
[assembly: ApiConventionType(typeof(DefaultApiConventions))]
如果我们只想在类级别上执行此操作,我们将去掉assembly修饰符,并将其应用于类,通常是控制器,如下所示:
[ApiConventionType(typeof(DefaultApiConventions))]
public class PerController : ControllerBase { }
或者,我们可以在方法级别执行此操作:
[ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Put))]
[HttpPut("{id}")]
public async Task<ActionResult<Pet>> PutPet(int id, Pet pet) { ... }
请注意,在本例中,我们指定了方法的名称(Put,该方法包含我们希望用于此方法的约定。在所有其他情况下,VisualStudio 都会查找约定方法的提示,这些方法被指定为属性,用于匹配当前上下文中的方法。
DefaultApiConventions类对以下几种方法有约定(与 HTTP 谓词相同):
Get或FindPost或CreatePut、Edit或UpdateDelete
对于其中的每一个,VisualStudio 都提供添加适当的状态代码([ProducesResponseType]属性。所以,它是这样的:
| 方法 | 命令 |
| GET | 200 OK:找到内容,返回成功。404 Not Found:未找到内容。 |
| POST | 201 Created:内容创建成功。400 Bad Request:发出错误或无效的请求。 |
| PUT | 204 No Content:未发布内容。404 Not Found:未找到要更新的内容。400 Bad Request:发出了错误或无效的请求。 |
| DELETE | 200 OK:内容删除成功。404 Not Found:未找到内容。400 Bad Request:发出了错误或无效的请求。 |
以下是使用 Visual Studio 添加与GET约定匹配的方法缺少的响应类型的示例:

我们可以推出自己的约定,但这些约定是 RESTAPI 的默认约定,因此我们可能应该坚持这些约定。如果您想了解更多信息,请查看https://docs.microsoft.com/en-us/aspnet/core/web-api/advanced/conventions 。
返回验证结果
在 ASP.NET 2.1 之前,您需要明确查看模型验证结果,例如,通过检查ModelState.IsValid-并相应地采取行动,例如返回BadRequestResult。此后,对于任何具有[ApiController]属性的控制器,ASP.NET Core 将添加一个名为ModelStateInvalidFilter的操作过滤器,该过滤器在实际运行操作方法之前,检查模型的有效性并为我们返回BadRequestResult。伪代码如下所示:
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
发送给客户端的响应包括模型验证错误,默认情况下,还包括一个特殊的内容类型application/problem+json。在本章后面讨论错误处理时,我们将更详细地讨论这一点。
您可以通过将ApiBehaviorOptions.SuppressModelStateInvalidFilter属性的值设置为false来完全禁用此行为:
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
您只需隐藏验证详细信息,从安全角度看,这可能很有用:
options.SuppressUseValidationProblemDetailsForInvalidModelStateResponses = true;
另一种选择是明确说明如何将模型验证错误转换为响应。ApiBehaviorOptions类提供了一个名为InvalidModelStateResponseFactory的属性,该属性的委托仅用于此目的。它以ActionContext为唯一参数,我们可以从中检查ModelState以及其他可能有用的属性。下面的代码示例显示了如何根据模型验证错误的数量返回不同的结果:
options.InvalidModelStateResponseFactory = (ctx) =>
{
if (ctx.ModelState.ErrorCount > 1)
{
return new JsonResult(new { Errors = ctx.ModelState.ErrorCount });
}
return new BadRequestObjectResult(ctx.ModelState);
};
在本例中,如果错误计数大于1,则返回一个 JSON 结果;否则,我们将返回默认的错误请求。
现在,让我们看看当客户端请求时内容协商是如何工作的。
执行内容协商
内容协商是应用以客户端请求的格式返回数据的过程。这通常用于 API 样式的调用,而不是服务于 HTML 的请求。例如,某个客户机可能希望以 JSON 格式返回数据,而其他客户机可能更喜欢 XML。ASP.NET Core 支持这一点。
基本上有两种方法可以实现这一点:
- 通过路由或查询字符串参数
- 通过
Accept请求头
第一种方法允许您在 URL 上指定感兴趣的格式。让我们先看看它是如何工作的:
- 假设您有以下操作方法:
public Model Process() { ... }
- 让我们忘记
Model实际上是什么,因为它只是一个包含您感兴趣的属性的 POCO 类。可以这么简单:
public class Model
{
public int A { get; set; }
public string B { get; set; }
}
- 开箱即用的 ASP.NET Core 包含一个 JSON 格式设置程序,但您也可以添加一个 NuGet 包,该包也来自 Microsoft,它增加了对 XML 的支持-
Microsoft.AspNetCore.Mvc.Formatters.Xml。除了将其添加到服务中,您还需要告诉 ASP.NET 要使用什么映射;在这种情况下,.xml格式转换为application/xml内容类型:
services
.AddMvc(options =>
{
options.FormatterMappings.SetMediaTypeMappingForFormat("xml",
"application/xml");
})
.AddXmlSerializerFormatters();
呼叫AddXmlSerializerFormatters已执行以下操作:
services
.AddMvc()
.AddXmlSerializerFormatters();
- 已经有一个从
json到application/json的映射,因此不需要添加它,因为它将是默认值。然后,您需要使用指定format参数的路由来装饰您的操作方法:
[Route("[controller]/[action]/{format}")]
public Model Process() { ... }
- 您还需要使用
[FormatFilter]属性装饰控制器类,如下所示:
[FormatFilter]
public class HomeController { }
- 现在,如果您使用
json或xml作为format路由值来调用您的操作,您将得到一个按照您指定的格式正确格式化的答案,例如对于 XML:
<Model xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<A>1</A>
<B>two</B>
</Model>
- 对于 JSON,您将获得以下信息:
{"a":1,"b":"two"}
- 另一种方法是使用请求的
Accept头,这是指定我们感兴趣接收的内容的标准方法。API 客户端通常不使用此功能,但浏览器使用。在AddMvc调用中,您需要激活RespectBrowserAcceptHeader属性:
services
.AddMvc(options =>
{
options.RespectBrowserAcceptHeader = true;
})
.AddXmlSerializerFormatters();
现在,如果您发送一个application/xml或application/json的Accept头(这是默认值),您将获得所需格式的结果。
For more information about the Accept header, please consult https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept.
为了完整起见,JSON 格式化程序允许我们通过使用AddJsonOptions扩展方法指定其他选项:
services
.AddMvc()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy =
JsonNamingPolicy.CamelCase;
});
这将解析程序配置为使用camelCasing而不是默认选项。这里有太多的选项需要讨论,因为它们实际上并不相关,我们将不讨论它们。
现在我们已经了解了请求接受,现在让我们看看响应格式。
输出格式化程序
在 HTTP 响应中返回对象意味着什么?嗯,这个物体需要转换成可以通过电线传输的东西。一些典型的响应类型如下:
text/html:用于 HTML 内容text/plain:针对一般文本内容application/json:用于 JSON 内容application/xml:用于 XML 内容binary/octet-stream:对于任何二进制内容
因此,您返回的对象需要转换为可以使用这些内容类型之一发送的内容。为此,ASP.NET Core 使用了输出格式化程序的概念。输出格式化程序本质上是IOutputFormatter的一个实现。
现成的 ASP.NET Core 包括以下输出格式化程序:
HttpNoContentOutputFormatter根本不写任何内容;仅返回一个204HTTP 状态码。StringOutputFormatter按原样输出字符串。StreamOutputFormatter将流写入一系列字节。JsonOutputFormatter将对象序列化为 JSON。
还有几种类型的 XML 格式化程序可以使用Microsoft.AspNetCore.Mvc.Formatters.XmlNuGet 包安装,并通过AddXmlDataContractSerializerFormatters(对于DataContractSerializer)或AddXmlSerializerFormatters(对于XmlSerializer)注册。
Data contracts and XML serializers use different approaches; for example, different attributes to control the output.
可以使用AddMvc扩展方法重载来配置输出格式化程序,该方法采用一个参数,如下所示:
services.AddMvc(options =>
{
options.OutputFormatters.Insert(0, new MyOutputFormatter());
});
那么,如何选择输出格式化程序呢?ASP.NET Core 迭代已配置格式化程序的列表并调用其IOutputFormatter.CanWriteResult方法。返回true的第一个格式化程序是用于将对象序列化到输出流的格式化程序(即WriteAsync方法)。
处理空值
当 ASP.NET Core API 控制器返回通常包装在IActionResult中的null值并获取值时,ASP.NET Core 会自动将返回值切换到NoContentResult(HTTP 204 无内容)。这种行为在大多数情况下可能是正常的,但在其他情况下可能是不可取的。幸运的是,它可以被我们控制;这实际上是通过HttpNoContentOutputFormatter输出格式化程序完成的,默认情况下,该格式化程序已注册。
因此,如果要禁用它,只需删除此格式化程序:
services.AddMvc(options =>
{
options.OutputFormatters.RemoveType<HttpNoContentOutputFormatter>();
});
在这种情况下,请注意,如果您不验证返回的响应,您可能会返回一个响应200 OK和一个null响应。如果您愿意,您可以实现一个结果过滤器,在响应为null的情况下返回其他内容,例如NotFoundResult。这将如下所示:
public sealed class NoResultToNotFoundFilterAttribute : Attribute, IAlwaysRunResultFilter
{
public void OnResultExecuting(ResultExecutingContext context)
{
if ((context.Result is ObjectResult result) && (result.Value
== null))
{
context.Result = new NotFoundResult();
}
}
public void OnResultExecuted(ResultExecutedContext context) { }
}
请注意,我们将其实现为一个always run result过滤器,如第 10 章理解过滤器所述。您只需要将此筛选器注册为全局筛选器,就可以开始了。
内容协商部分到此结束。在下一节中,我们将研究错误处理。
处理错误
当我们谈论 API 时,错误处理意味着返回非人工端点在发生错误时可能使用的信息,这些信息可以提供有用的信息。W3C(即万维网联盟)在 RFC 7807(上)上对此进行了说明 https://tools.ietf.org/html/rfc7807 ),作为“一种在 HTTP 响应
中携带机器可读的错误详细信息的方式”。
这里的想法是,当错误发生时,我们收集所有有用的信息并返回一个响应,该响应以适当的详细程度描述发生的情况。
截获 web 请求上发生的任何异常的一种方法是通过异常处理程序—通过调用UseExceptionHandler添加的中间件。让我们看一个例子:
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var errorFeature = context.Features.Get
<IExceptionHandlerPathFeature>();
var exception = errorFeature.Error;
var path = errorFeature.Path;
var problemDetails = new ProblemDetails
{
Instance = $"urn:my:error:{Guid.NewGuid()}",
Detail = exception.Message
};
if (exception is BadHttpRequestException badHttpRequestException)
{
problemDetails.Title = "Invalid request!";
problemDetails.Status = StatusCodes.Status400BadRequest;
}
else
{
problemDetails.Title = "An unexpected error occurred!";
problemDetails.Status = StatusCodes
.Status500InternalServerError;
}
context.Response.ContentType = "application/problem+json";
context.Response.StatusCode = problemDetails.Status.Value;
await context.Response.WriteAsync(JsonSerializer.
Serialize(problemDetails));
});
});
我们这里有一个在异常事件中调用的处理程序;我们通过IExceptionHandlerPathFeature检索当前异常并检查它是什么。还有另一个要素类IExceptionHandlerFeature,但IExceptionHandlerPathFeature对其进行了扩展,并将Path属性添加到现有的Error属性中。
然后,我们构建一个ProblemDetails类的实例(由.NETCore 提供;如果您希望提供自己的属性,您可以从中继承),并填写适当的属性。然后,我们将响应内容类型设置为 RFC 接口定义的application/problem+json,并将该实例序列化为响应流的 JSON。
ProblemDetails类的属性具有以下含义(来自 RFC):
Type(string):标识问题类型的 URI 引用(RFC3986)。如果未提供,则默认为about:blank。Title(string:问题类型的简短易读摘要。这不应随着问题的出现而改变,除非出于本地化的目的。Detail(string):一种人类可读的解释,专门针对该问题的发生。Instance(string):标识问题具体发生情况的 URI 引用。如果取消引用,它可能会或可能不会产生进一步的信息。Status(int:HTTP 状态码。
当然,具体的细节级别取决于所发生的消息和您想向客户端公开的内容,因此必须仔细检查异常、请求 URL 和任何其他可能的参数。
由于 ASP.NET Core 3,我们还可以在控制器操作方法中创建这样一个ProblemDetails对象:
var problemDetails = ProblemDetailsFactory.CreateProblemDetails(HttpContext);
这包括有关发生的异常的一般信息,如果发生更具体的模型验证错误(不是服务器端错误,通常不会引发任何异常),我们可以这样做:
var validationProblemDetails = ProblemDetailsFactory.
CreateValidationProblemDetails(HttpContext, ModelState);
这包括生成的对象中的所有验证错误。控制器中可能有如下代码:
if (!this.ModelState.IsValid)
{
var validationProblemDetails = ProblemDetailsFactory
.CreateValidationProblemDetails(HttpContext,
ModelState);
return BadRequest(validationProblemDetails);
}
这就是错误处理的全部内容;下一节将解释如何拥有多个 API 版本,并让客户选择他们感兴趣的版本。
理解 API 版本控制
与 API(web 服务)风格的方法调用相关的还有版本控制。通过对 API 进行版本控制,您可以同时获得多个版本的 API,方法可能是获取不同的有效负载并返回不同的结果。ASP.NET Core 通过Microsoft.AspNetCore.Mvc.Versioning库支持 API 版本控制。
开箱即用,您可以应用以下技术来指定您感兴趣的版本:
- URL 查询字符串参数
- 标题
- 前面的任何选项都可以是查询字符串或标题
假设在两个不同的命名空间中有两个同名的控制器类:
namespace Controllers.V1
{
[ApiVersion("1.0")]
public class ApiController
{
[ApiVersion("1.0", Deprecated = true)]
[HttpGet("[controller]/[action]/{version:apiversion}")]
public Model Get() { ... }
[ApiVersion("2.0")]
[ApiVersion("3.0")]
public Model GetV2() { ... }
]
}
在这里,您可以看到我们对每个应用了几个[ApiVersion]属性,每个属性都指定了控制器支持的 API 版本。让我们看看如何实现版本控制,从路由方法开始。
使用标题值
我们将配置 API 版本控制以从标题字段推断所需的版本。我们在ConfigureServices方法中配置版本控制。注意HeaderApiVersionReader类:
services.AddApiVersioning(options =>
{
options.ApiVersionReader = new HeaderApiVersionReader("api-version");
});
在这里,我们说版本应该来自名为api-version的头字符串。这不是一个标准值;只是我们捡到的一些绳子。
现在,当在/Api/Get调用 API 时,在传递一个值为1.0的api-version头时,请求将由Controllers.V1.ApiController类处理。但是,如果传递的值为2.0或3.0,则Controllers.V2.ApiController类将拾取该值。
使用标题是最透明的技术,但也不能轻易强制使用,例如,使用浏览器。让我们看看另一种技术。
使用查询字符串
为了从 URL 推断版本,我们需要使用QueryStringApiVersionReader类,如下所示:
services.AddApiVersioning(options =>
{
options.ApiVersionReader = new QueryStringApiVersionReader("api-
version");
});
我们还需要配置一个考虑到这一点的路由:
[Route("[controller]/{version:apiversion}")]
public Model Get() { ... }
现在,如果我们向/api/1.0发出请求,我们会得到版本1.0,同样的版本也适用于2.0和3.0。
If we want to be more flexible, we can use the QueryStringOrHeaderApiVersionReader class as ApiVersionReader; both approaches will work.
我们已经了解了如何使用查询字符串或标题指定版本。现在让我们看看如何将版本标记为已弃用。
弃用版本
通过设置Deprecated属性的标志,可以说某个版本已过时:
[ApiVersion("1.0", Deprecated = true)]
现在,如果您将ReportApiVersions标志设置为true,您将收到响应中支持和不支持的版本:
services.AddApiVersioning(options =>
{
options.ReportApiVersions = true;
options.ApiVersionReader = new QueryStringApiVersionReader("
api-version");
});
这将生成以下响应头:
api-deprecated-versions: 1.0
api-supported-versions: 2.0, 3.0
现在,让我们继续了解默认版本是如何工作的。
默认版本
您还可以指定默认版本:
services.AddApiVersioning(options =>
{
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(2, 0);
options.ApiVersionReader = new QueryStringApiVersionReader("
api-version");
});
在这种情况下,如果您没有指定版本,它将假定您想要版本2.0。
版本映射
正如我们所看到的,Controllers.V2.ApiController类被映射到两个版本——2.0和3.0。但如果您想单独处理版本3.0,会发生什么?那么,您只需向新方法添加一个[MapToApiVersion]属性:
[MapToApiVersion("3.0")]
public Model GetV3() { ... }
此后,所有版本3.0的请求都将通过此方法处理。
无效版本
如果请求的是不受支持的版本,而不是不推荐的版本,则会引发异常并将其返回到客户端,如下所示:
{
"error":
{
"code": "ApiVersionUnspecified",
"message":"An API version is required, but was not specified."
}
}
这就是 ASP.NET Core web API 的版本控制。正如您所看到的,您有几个选项,从使用 URL 到使用标题,并且您可以同时拥有多个 API 版本。这有望帮助您将运行旧版本的客户端迁移到新版本。
接下来,我们将看到如何为 API 生成文档,甚至创建一个 UI,以便从浏览器中调用它。
生成 API 文档
Web API 有一个最初称为Swagger的规范,但现在改名为OpenAPI(https://github.com/OAI/OpenAPI-Specification )。它用于描述某些 API 提供的端点和版本。Swagger v3.0 规范为 OpenAPI 计划做出了贡献,因此 Swagger 与 OpenAPI 合并。
在一些地方,它仍然通俗地被称为 Swagger,还有一个名为swashback的.NET 开源实现,在 NuGet 上作为Swashbuckle.AspNetCore(提供 https://github.com/domaindrivendev/Swashbuckle.AspNetCore )。这个包的作用是检查控制器的操作方法,并生成一个描述它们的 JSON 文档。它还提供了一个简单的 web 界面来调用这些操作方法,这有多酷?
为了使用Swashbuckle.AspNetCore,我们需要添加几个 NuGet 包——Swashbuckle.AspNetCore、.SwaggerGen、Swashbuckle.AspNetCore.SwaggerUI和Microsoft.OpenApi。后者由前者自动添加。要使用 Swashback,就像大多数 ASP.NET Core API 一样,我们首先需要将所需的服务注册到依赖项注入框架(ConfigureServices。具体做法如下:
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo {
Title = "My API V1",
Version = "v1",
OpenApiContact = new Contact {
Email = "rjperes@hotmail.com",
Name = "Ricardo Peres",
Url = "http://weblogs.asp.net/ricardoperes"
}
});
});
我们通过Configure方法将 Swagger 中间件添加到管道中:
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
两个调用UseSwagger和UseSwaggerUI表示两种不同的功能;第一个用于实际的 API 文档,第二个用于调用控制器操作的 UI。
您可以使用不同的 API 名称或版本向AddSwaggerGen添加任意多个调用。每个版本将生成不同的 API 版本文档。
Swashback 通过内省所有控制器及其操作方法来工作,但它只会找到您明确希望它找到的那些控制器,例如:
- 用
[Route]属性标记的控制器 - 标有
[Route]、[HttpGet]、[HttpPost]、[HttpPut]、[HttpDelete]、[HttpOptions]、[HttpPatch]或[HttpMethod]且带有明确路线模板的动作方式
Swashback 还将介绍以下内容:
[Produces]:可能由动作方法生成的 POCO 类的内容类型和契约。[ProducesResponseType]:可以通过动作方式返回的合同和状态码。对于不同的状态代码,您可以有任意数量的代码。[ProducesDefaultResponseType]:针对[ProducesResponseType]属性未明确提及的任何状态代码返回的合同。[ProducesErrorResponseType]:发生错误时返回的合同。[Consumes]:动作方法将接受的内容类型
这些属性可以应用于类(控制器)或方法(操作)级别;因此,例如,如果控制器中的所有操作都使用并生成 JSON,那么您可以有:
[Produces("application/json")]
[Consumes("application/json")]
public class HomeController : Controller { }
当您访问/swagger/v1/swagger.jsonURL 时,您会得到如下内容:
{
"swagger": "2.0",
"info": {
"version": "v1",
"title": "My API V1",
"contact": {
"name": "Ricardo Peres",
"url": "http://weblogs.asp.net/ricardoperes",
"email": "rjperes@hotmail.com"
}
},
"basePath": "/",
"paths": {
"/Home": {
"get": {
"tags": [ "Home" ],
"operationId": "HomeIndexGet",
"consumes": ["application/json"],
"produces": ["application/json"],
"responses": {
"200": {
"description": "Success"
}
}
}
}
},
"definitions": {},
"securityDefinitions": {}
}
由于空间限制,我们在这个示例输出中只包含了Home控制器的一种动作方法Index。但是,您可以看到一个名为My API V1版本V1的文档。对于每个操作方法,Swashback 描述了它接受的 HTTP 方法、它接受的任何内容类型(可以通过使用[Consumes]属性指定)和返回(由[Produces]设置)以及返回状态代码([ProducesResponseType]属性)。如果未指定,则使用默认值,即状态代码、无接受或返回内容类型的200。
This version has nothing to do with the versioning schema discussed in the previous topic.
如果单个方法可以返回多个文档类型或状态代码,则可以根据需要应用任意多个[Produces]和[ProducesResponseType]属性:
[ProducesResponseType(typeof(Model), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(Model), StatusCodes.Status202Accepted)]
[ProducesResponseType(typeof(Model), StatusCodes.Status304NotModified)]
[ProducesDefaultResponseType]
public IActionResult AddOrUpdate(Model model) { ... }
在这种情况下,我们的意思是:
- 如果模型已经存在,那么我们返回
HTTP 303 Not Modified。 - 如果模型改变了,我们返回
HTTP 202 Accepted。 - 如果添加了模型,则返回
HTTP 201 Created。
请注意,所有这些都是根据其他惯例推断出来的!
A word of caution—the difference between [Produces] and [ProducesResponseType] is that the former is a result filter that sets the response type to be of a specific value and the latter only declares it! This may be relevant if you wish to use content negotiation, so it's something that you must keep in mind!
对于使用AddSwaggerGen添加的每个文档,您都会得到不同的 URL,例如/swagger/v1/swagger.json、/swagger/v2/swagger.json等等。
更有趣的是生成的 UI,可以通过/swagger端点访问:

在这里,我们可以看到两个动作(称为操作)-/Home和/Home/Process。它们是Home控制器中的两种动作方式,这些是访问每种方式的路径。为了清楚起见,让我们考虑一下 AutoT3A.Action 方法有以下签名:
[HttpGet("Process")]
[ProducesResponseType(typeof(Model), StatusCodes.Status200OK)]
public IActionResult Process(string id, int state) { ... }
现在,扩展操作将产生以下结果:

在这里,您会得到一个表单,该表单要求将参数设置为Process,甚至会向您显示一个格式为 JSON 的示例响应。明亮的此示例响应来自应用于[Produces]属性的Type属性。如果您填写此表格并单击“试用”!,这就是你得到的:

在这里,您可以得到响应负载和所有响应头。很酷吧?
剩下要展示的是我们如何定制 JSON 文档和 UI 的 URL。我们通过UseSwagger和UseSwaggerUI扩展方法实现,如下所示:
app.UseSwagger(options =>
{
options.RouteTemplate = "api-doc/{documentName}/swagger.json";
});
RouteTemplate属性只需要取一个{documentName}令牌,默认为swagger/{documentName}/swagger.json。此令牌将替换为您在AddSwaggerGenlambda 中作为SwaggerDoc调用的第一个参数添加的任何内容。不要忘记,如果更改一个,则需要同时更改两个,如图所示:
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/api-doc/v1/swagger.json", "My API V1");
});
There are lots of other configuration options, so we advise you to take a look at the documentation available at https://github.com/domaindrivendev/Swashbuckle.AspNetCore.
生成之后,让我们看看如何添加文档。
添加 API 文档
Swashback 可以添加随代码提供的文档,只要我们让 MSBuild 为其生成 XML 文件。使用 Visual Studio,这只是在.csproj文件上设置属性的问题:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
这将导致代码中的每个文档注释都包含在一个 XML 文件中,该文件适合输入 Swashback。要加载此文件,我们需要执行以下操作:
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo {
Title = "My API V1",
Version = "v1",
Contact = new OpenApiContact {
Email = "rjperes@hotmail.com",
Name = "Ricardo Peres",
Url = "http://weblogs.asp.net/ricardoperes"
}
});
//assume that the XML file will have the same name as the current
//assembly
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath);
});
现在,公共类型及其公共成员和参数的所有注释都将显示在 Swashback 用户界面上。
小田
OData是一个通过 web 查询数据的开放标准。它允许我们公开关于域模型的元数据,并只使用 HTTP 动词和查询字符串查询它。例如,它可以用于将任何数据模型公开为 RESTAPI。
早期版本的 ASP.NET 已经支持它,从 2.0 版开始,ASP.NET Core 通过Microsoft.AspNetCore.ODataNuGet 包也支持它。
For additional information about the OData spec, please check out https://www.odata.org.
对于 ASP.NET Core,OData 允许我们通过 URL 查询实现IQueryable<T>或IEnumerable<T>的任何集合;在第一种情况下,这意味着查询不会在内存中执行,但会被转换为数据源特定的方言。对于对象关系映射器,例如实体框架(EF)核心或 NHibernate,这是 SQL。
在以下各节的整个过程中,我们将了解如何使用 OData 向 web 公开对象模型,以便只需使用浏览器即可轻松查询对象模型。让我们从设置开始。
建立 OData
因此,在添加Microsoft.AspNetCore.ODataNuGet 包之后,我们需要注册所需的服务并声明我们将公开的模型。让我们看看一些代码:
private static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
//register an entity set of type Order and call it Orders
builder.EntitySet<Order>("Orders");
//same for products
builder.EntitySet<Product>("Products");
//add other entity sets here
return builder.GetEdmModel();
}
public void ConfigureServices(IServiceCollection services)
{
//rest goes here
services.AddOData();
services
.AddControllers()
.SetCompatibilityVersion(CompatibilityVersion.Latest);
}
public void Configure(IApplicationBuilder app)
{
//rest goes here
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
//add a route named odata for an endpoint /odata using the EDM
//model
endpoints.MapODataRoute("odata", "odata", GetEdmModel());
});
}
请注意,GetEdmModel方法返回将在本例中提供给 OData 端点的实体集Products和Orders,作为实体数据模型(EDM)。还有更高级的功能,例如声明函数,但是我们在这里不讨论它们。
我们必须通过调用ConfigureServices中的AddOData扩展方法来注册服务,然后在Configure中,当我们包含 MVC 中间件时,我们需要在本例中声明 OData 端点将侦听的路由,这是odata。
MapODataRoute扩展方法将路由名称作为第一个参数,实际 URL 路径作为第二个参数,最后是 EDM 模型。
现在,我们需要添加一个控制器来实际返回集合或单个项目;该控制器必须继承自ODataController,该类继承自ControllerBase,因此继承其方法(不需要Controller类,因为该类基本上添加了与视图相关的方法):
[ODataRoutePrefix("Products")]
public class ProductsController : ODataController
{
private readonly ProductContext _ctx;
public ProductsController(ProductContext ctx)
{
this._ctx = ctx;
}
[EnableQuery]
public IQueryable<Product> Get() => _ctx.Products;
public async Task<Product> Get(int id) => await _ctx.Products.
FindAsync(id);
}
[ORouteDataPrefix]属性表示如果我们不希望使用控制器名称减去Controller后缀的默认约定,那么该控制器将使用的前缀。否则,可以安全地忽略它。
注意返回集合的方法中的[EnableQuery]属性;这是一个属性,它通过允许通过 URL 查询来实现真正的魔力。另外,名为Get的两个方法也有一个ODataRoute属性,但是接受id参数的重载也在其构造函数中提到它;这样就可以将其映射到参数。
此设置允许以下 HTTP 调用:
GET /odata/$metadata#Product:用于获取元数据,Product实体的一组公共属性GET /odata/Products至ProductController.Get():返回所有产品,允许查询GET/ odata/Products(1)至ProductController.Get(1):返回单个产品
现在我们已经了解了如何为 OData 准备 ASP.NET Core,让我们看看如何查询我们的模型。
获取元数据
不需要声明返回类型IQueryable<T>,只要实际返回值是这种类型,例如IActionResult、ActionResult<IQueryable<T>>甚至Task<IQueryable<T>>,其中T是实际类型。如果您不返回IQueryable<T>类型,而是返回实现IEnumerable<T>的内容,那么也可以进行查询,但查询不是在服务器端(如数据库服务器中),而是在内存中。
列表集合
调用 OData 端点(odata,如前所述),我们可以看到公开的实体列表。在我们的示例中,这是Products和Orders集合:

前面的屏幕截图显示了我们的模型在我们定义的端点中公开的集合。现在让我们看看这些实体的元数据。
实体元数据
实体的元数据显示该实体的所有属性,并可在以下端点处使用:
/odata/$metadata
以下是我们运行此操作时得到的输出:

这将显示所有实体属性及其所有集合。
现在让我们看看如何过滤元数据。
质疑
查询实体只需访问该实体的端点:
/odata/Products
这将返回该实体的所有记录,但如果我们只需要其中的一部分呢?
过滤实体
通过添加一个名为$filter的查询参数,可以实现对 URL 的查询,如下所示:
/odata/Products?$filter=Price gt 100
以下是我们在运行查询时得到的输出:

对于不同的属性类型,可以使用常规运算符,例如:
- 大于/小于:
gt或lt - 大于或等于/小于或等于:
gte或lte - 等于/不等于:
eq或ne - 和/或/或非:
and、or或not - 枚举标志:
has
如您所见,我们可以使用and、or和not组合表达式,甚至包括括号来分组条件。
对于字符串属性,我们可以使用其他运算符:
-
concat -
contains -
endswith -
indexof -
length -
startswith -
substring -
tolower -
toupper -
trim
使用字符串文字时,请确保用'括起来,如下所示:
/odata/Products?$filter=Name eq 'Expensive Product'
对于集合,我们有以下内容:
inhassubsethassubsequence
我们还有一些日期和时间功能:
datedayfractionalsecondshourmaxdatetimemindatetimeminutemonthnowsecondtimetotaloffsetminutestotalsecondsyear
可用的一些数学函数如下所示:
ceilingfloorround
一些类型函数如下所示:
castisof
一些地理功能(底层数据访问层支持该功能)如下所示:
geo.distancegeo.intersectsgeo.length
一些 lambda 运算符如下所示:
anyall
我不会试图解释所有这些。有关每个功能的详细说明,请参阅 OData 规范参考文档,可在上获得 https://www.odata.org/documentation 。
投影
投影允许您仅检索感兴趣的属性。例如,对于Products,您可能只需要Name和Price属性。您可以通过在查询字符串上包含一个$select参数来实现:
/odata/Products?$select=Price
以下是查询的输出:

可以为投影指定多个字段,每个字段用逗号分隔。
寻呼
我们可以通过添加$top和$skip来指定页面大小和可选起始位置:
/odata/Products?$top=2&$skip=2
以下是输出:

从 21 日开始,最多可返回 10 条记录。但是,千万不要忘记,在没有显式排序顺序的情况下不应该应用分页,否则结果可能不是您所期望的。
分类
通过$orderby实现分拣:
/odata/Products?$orderby=Price
以下是输出:

要按降序查看这些结果,请使用desc:
/odata/Products?$orderby=Price desc
以下是降序结果的输出:

也可以按几个属性进行排序:
/odata/Products?$orderby=Price desc,Name asc
asc值为默认值,可以省略。
膨胀
使用扩展,我们可以通过导航属性强制遍历相关实体。在本例中,我们要使用的是$expand参数:
/odata/Orders?$expand=Products
请注意,这与为子集合调用 EF Core LINQ 查询中的Include扩展方法相同,强制 EF Core 将其包括在查询中,并将其所有记录实例化为实体。
计数
即使使用过滤器,我们也可以通过包含一个值为true的$count关键字来返回记录总数:
/odata/Products?$count=true
之后,让我们看看配置选项。
配置选项
OData 选项在UseMVC扩展方法中配置。默认情况下,不允许任何选项,因此必须显式设置它们。可允许的可用操作如下所示:
- 实体选择(
Select) - 子属性和集合的扩展(
Expand - 过滤(
Filter) - 排序(
OrderBy) - 计数(
Count)
多个选项可以链接在一起,如下所示:
public void Configure(IApplicationBuilder app)
{
//rest goes here
app.UseMvc(options =>
{
options.Select().Expand().Filter().OrderBy().Count();
options.MapODataServiceRoute("odata", "odata", GetEdmModel());
});
}
这告诉 OData 允许选择、扩展、筛选、排序和计数。
限制
通常建议您对查询设置一些限制,以最小化资源消耗。
最大可返回记录数
可以并建议设置查询返回的最大记录数。这样做是为了节省资源。我们可以在使用MaxTop扩展方法配置 OData 选项时执行此操作:
app.UseMvc(options =>
{
options.MaxTop(10);
options.MapODataServiceRoute("odata", "odata", GetEdmModel());
});
这将要检索的最大实体数定义为 10。
膨胀
您还可以配置$expand命令是否允许扩展:
options.Expand();
如果提供了此选项,查询可能会更复杂,并且可以返回更多的结果,因为它们可以将子实体和主实体结合在一起。
If you wish to play with OData with a complex model, please go to https://www.odata.org/odata-services.
总结
在本章中,我们了解了如何使用 JWTs 来验证端点,而无需人工交互。
现在我们知道了如何使用数据验证 API 来执行自动模型验证。然后,我们了解到,如果您希望以不同的格式返回数据,内容协商可能非常有用;但是,在现实中,JSON 是当今网络上数据交换的事实上的标准。
OpenAPI 规范在开发模式下也有助于检查端点并针对它们发出请求。
接下来,我们看到 OData 是对 ASP.NET Core 的一个很好的补充,尤其是与 EF Core 或任何其他将数据公开为IQueryable<T>的数据提供程序集成时。
在本章中,我们还学习了如何在 ASP.NET Core 中实现 REST 以公开 API。在此之前,我们研究了如何执行模型绑定和验证数据。我们还介绍了用于授权端点的 JWT 机制,以及如何执行版本控制和内容协商。最后,我们研究了如何利用 OData 向 web 公开数据模型。
在下一章中,我们将介绍创建可重用组件的方法。
问题
您现在应该能够回答以下问题:
- 什么是小田?
- 什么是内容协商?
- 为什么不适合在 web API 中使用 cookie 进行身份验证?
- 对于特定版本的 API,我们可以采用哪些不同的方式?
- 关于行动方法的公约的目的是什么?
- 问题的细节是什么?
- 什么是休息?
九、可复用的组件
本章介绍 ASP.NET Core 的可重用组件。所谓可重用,我的意思是,它们可能会在不同的项目中使用,或者在同一个项目中的不同位置使用不同的参数,从而产生可能不同的结果。在本章中,我们将介绍视图组件和标记帮助程序(ASP.NET Core 新增)、标记帮助程序组件(ASP.NET Core 2 新增)以及我们的老朋友部分视图。
在本章中,我们将介绍以下主题:
- 视图组件
- 标记助手
- 标记辅助对象组件
- 局部视图
- Razor 类库
- 添加外部内容
技术要求
为了实现本章中介绍的示例,您需要.NET Core 3 SDK 和文本编辑器。当然,VisualStudio2019(任何版本)满足所有要求,但您也可以使用 VisualStudio 代码。
本章中介绍的所有技术都有助于构建代码并最小化全局代码库的大小。
深入到视图组件中
视图组件是 ASP.NET Core 的新组件,它们在 ASP.NET 预核心中不存在。您可以将它们视为局部视图(仍然存在)和返回子操作(不再可用)的RenderAction方法的替代品。不再与控制器绑定;它们是可重用的,因为它们可以从外部程序集(即,不是 web 应用的程序集)加载,并且比局部视图更适合呈现复杂的 HTML。在以下部分中,我们将了解什么是视图组件、它们如何工作以及在何处使用它们,并将它们与局部视图进行比较。
发现视图组件
可以通过以下方式之一查找视图组件:
- 通过继承
ViewComponent类 - 通过添加一个
[ViewComponent]属性 - 通过向类添加
ViewComponent后缀
您很可能会从ViewComponent类继承组件,因为该类提供了两个有用的方法。如果 web 应用引用外部程序集或将其注册为应用部件,则可以从外部程序集加载视图组件:
services
.AddMvc()
.ConfigureApplicationPartManager(options =>
{
options.ApplicationParts.Add(new AssemblyPart(Assembly.Load
("ClassLibrary"));
})
对于 POCO 视图组件,您将无法轻松访问请求的环境上下文。如果您必须在前面的选项中进行选择,请选择从ViewComponent继承,否则,您将需要投入额外的工作来获取所需的所有引用(如HttpContext等)。我们将在后面的依赖注入一节中对此进行更详细的描述。
视图组件只需声明一个方法,InvokeAsync:
public class MyViewComponent : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync()
{
return this.Content("This is a view component");
}
}
您还可以使用参数,我们将看到。
[ViewComponent]属性可用于更改视图组件的名称,但您应注意,这样做意味着您在加载视图组件时需要使用此名称:
[ViewComponent(Name = "MyView")]
public class SomeViewComponent : ViewComponent { ... }
不要给它一个带有连字符(“-”)的名称,因为它会影响使用!我们将在下一节中看到这一点。
使用视图组件
视图组件是从视图调用的,有两种不同的语法:
- 代码语法允许您传递复杂类型的参数,但代码必须在语法上有效:
@await Component.InvokeAsync("MyViewComponent", new { Parameter
= 4, OtherParameter = true })
InvokeAsync方法采用视图组件名称(默认情况下,类的名称减去ViewComponent后缀)和一个可选参数,其中包含要传递给视图组件的InvokeAsync方法的参数;此方法可以接受任意数量的参数并返回一个IViewComponentResult实例。
- 标记使用标记助手语法(稍后将对此进行详细介绍);注意
vc名称空间:
<vc:my-view-component parameter="4" otherParameter="true"/>
同样,这是不带ViewComponent后缀但使用连字符大小写的类的名称。这里您还需要使用[ViewComponent]属性中指定的Name,如果有的话。不要在命名中使用连字符。
Pascal-cased class and method parameters for tag helpers are translated into lower-kebab case, which you can read about at http://stackoverflow.com/questions/11273282/whats-the-name-for-dash-separated-case/12273101#12273101.
如果您的参数很复杂,无法用属性轻松表示,则应选择代码语法。此外,名称空间是可配置的。
另一个选项是以ViewComponentResult的形式从控制器操作返回视图组件:
public IActionResult MyAction()
{
return this.ViewComponent("MyViewComponent");
}
这与返回部分视图非常相似,只有视图中的组件,所有内容都需要由代码生成。也就是说,如果要返回自定义 HTML,可能需要通过连接字符串来构建。
查看组件结果
View components 返回一个IViewComponentResult实例,该实例在 ASP.NET Core 中有三个实现,每个实现通过ViewComponent类的方法返回:
Content(ContentViewComponentResult:返回字符串内容。View(ViewViewComponentResult):返回局部视图。HtmlViewComponentResult:与ContentViewComponentResult类似,但返回编码的 HTML。没有创建此类实例的方法,但您可以自己实例化一个。
The rules for discovering partial view files are identical to the ones described earlier in Chapter 5, Views.
IViewComponentResult接口在异步(ExecuteAsync和同步(Execute版本中只指定了一个方法。它以ViewComponentContext的一个实例作为其唯一参数,该参数具有以下属性:
Arguments(IDictionary<string, object>:传递给InvokeAsync方法的对象的命名属性HtmlEncoder(HtmlEncoder:HTML 输出编码器ViewComponentDescriptor(ViewComponentDescriptor):描述当前视图组件ViewContext(ViewContext:所有视图上下文,包括当前视图对象、HTTP 上下文、路由数据、模型、表单上下文和动作描述符ViewData(ViewDataDictionary:来自控制器的视图数据Writer(TextWriter):直接写入输出流
因为您可以访问所有这些上下文,所以您可以做您想做的事情,例如访问头、cookie 和请求参数,但是您不会将视图组件结果用于重定向,只用于呈现 HTML。
依赖注入
您可以通过调用ConfigureServices中AddMvc方法之上的AddViewComponentsAsServices扩展方法,将视图组件注册为服务:
services
.AddMvc()
.AddViewComponentsAsServices();
视图组件支持构造函数注入,因此您可以在构造函数中声明任何已注册的类型:
public class MyViewComponent : ViewComponent
{
private readonly ILoggerFactory loggerFactory;
public MyViewComponent(ILoggerFactory loggerFactory)
{
this._loggerFactory = loggerFactory;
}
}
一个共同的需求是掌握当前的HttpContext;如果在 POCO 控制器中需要它,则需要注入一个IHttpContextAccessor实例:
public class MyViewComponent : ViewComponent
{
public MyViewComponent(IHttpContextAccessor httpAccessor)
{
this.HttpContext = httpAccessor.HttpContext;
}
public HttpContext HttpContext { get; }
}
在本例中,我们注入了IHttpContextAccessor接口,从中我们可以提取请求的当前HttpContext实例。不要忘记,要使其工作,以下行必须出现在ConfigureServices中:
services.AddHttpContextAccessor();
视图组件与局部视图
如您所见,视图组件和局部视图有一些相似之处;它们都是生成标记的可重用机制。两者之间的区别在于局部视图继承了包含视图的大部分上下文,例如模型和视图数据集合,因此这两个视图和局部视图必须兼容。例如,它们必须具有兼容的模型。视图组件的情况并非如此,您可以使用自己喜欢的任何数据调用它们。
接下来,我们将讨论一个非常类似的主题,该主题使用了与 HTML 标记帮助程序更接近的不同语法。
探索标记助手
标记帮助程序也是 ASP.NET Core 的新功能。标记助手是一种向常规 HTML/XML 标记添加服务器端处理的机制;您可以将其视为类似于 ASP.NET Web 窗体的服务器端控件,尽管存在一些差异。标记帮助器在 Razor 视图上注册,当视图上的任何标记与标记帮助器匹配时,它将被触发。它们是 HTML 助手的另一种选择(可以说更简单),因为它们可以在没有代码块的情况下生成更清晰的标记。
标记助手的功能通过ITagHelper接口指定,其中TagHelper抽象基类提供了一个基本实现。其生命周期包括两种方法:
Init:在初始化标记助手时,在任何可能的子对象之前调用ProcessAsync:标签助手的实际处理
视图侧的标记辅助对象只不过是常规标记,因此,它可以包含其他标记,这些标记本身也可能是标记辅助对象。让我们看一个例子:
<time></time>
正如您所看到的,它只不过是一个普通的 XML 标记而不是 HTML,因为在任何版本的 HTML 上都没有这样的标记。
为了添加自定义服务器端行为,我们定义了一个标记帮助器类,如下所示:
public class TimeTagHelper : TagHelper
{
public override Task ProcessAsync(TagHelperContext context,
TagHelperOutput output)
{
var time = DateTime.Now.ToString();
output.Content.Append(time);
return base.ProcessAsync(context, output);
}
}
标记帮助器是递归的,这意味着在其他标记帮助器中声明的标记帮助器都将被处理。
我们将很快了解 ASP.NET Core 需要做些什么才能认识到这一点,但现在,让我们看看ProcessAsync方法的参数。
TagHelperContext包含上下文,如标记帮助器中所示。它包括以下属性:
AllAttributes(ReadOnlyTagHelperAttributeList):视图中为此标记帮助器声明的所有属性Items(IDictionary<string, object>:一个自由形式的项目集合,用于在当前请求中将上下文传递给其他标记帮助程序UniqueId(string:当前标记帮助器的唯一标识符
至于TagHelperOutput,它不仅允许将内容返回到视图,还允许返回标记内声明的任何内容。它公开了以下属性:
IsContentModified(bool):只读标志,表示内容是否已修改Order(int:标签助手的处理顺序PostElement(TagHelperContent):以下标签元素PostContent(TagHelperContent:当前标签后的内容Content(TagHelperContent:当前标签的内容PreContent(TagHelperContent:当前标签前的内容PreElement(TagHelperContent:前一个标签元素TagMode(TagMode):标签模式(SelfClosing、StartTagAndEndTag、StartTagOnly),用于定义标签在标记中应如何进行验证(允许内部内容为SelfClosing,仅无自身内容的标签为StartTagOnly)TagName(string:视图中标签的名称Attributes(TagHelperAttributeList:标签的原始属性列表,可以修改
例如,假设您有以下标签:
<time format="yyyy-MM-dd">Current date is: {0}</time>
在这里,您需要访问属性(format和<time>标记的内容。让我们看看如何实现这一点:
public class TimeTagHelper : TagHelper
{
public string Format { get; set; }
public override async Task ProcessAsync(TagHelperContext
context, TagHelperOutput output)
{
var content = await output.GetChildContentAsync();
var stringContent = content.GetContent();
var time = DateTime.Now.ToString(this.Format);
output.TagName = "span";
output.Content.Append(string.Format(CultureInfo.Invariant
Culture, stringContent, time));
return base.ProcessAsync(context, output);
}
}
在这里,我们可以看到我们正在做一些事情:
- 获取
Format属性的值 - 获取所有标记的内容
- 将目标标签的名称设置为
span - 使用内容和格式输出带有格式化时间戳的字符串
这基本上就是获取内容和属性的方法。您还可以向输出添加属性(通过向output.Attributes添加值)、更改输出标记名称(output.TagName)或阻止生成任何内容(通过使用output.SuppressOutput方法)。
在输出内容时,我们可以返回按照视图的HtmlEncoder实例编码的普通字符串,也可以返回已经编码的内容,在这种情况下,我们将调用AppendHtml,而不是Append:
output.Content.Append("<p>hello, world!</p>");
除了附加,我们还可以替换所有的内容;为此,我们称之为\t或SetHtmlContent,甚至清除所有内容(Clear或SuppressOutput。
[OutputElementHint]属性可用于提供关于哪个标记将输出的提示。这非常有用,以便 Visual Studio 知道如何提供有关其他一些元素的属性的提示,例如img:
[OutputElementHint("img")]
这样,当您在标记中添加自定义标记时,VisualStudio 将建议所有img元素的属性,例如SRC。
我们可以使用context.Items集合将数据从一个标记帮助器传递到另一个标记帮助器。记住Order属性定义了将首先处理的标记。
现在让我们看看标记帮助程序公开的属性。
了解标记辅助对象的属性
可以通过视图设置标记帮助器类中的任何公共属性。默认情况下,使用小写或大小写相同的名称,但我们可以通过应用[HtmlAttributeName]属性为属性指定不同的名称,以便在视图中使用:
[HtmlAttributeName("time-format")]
public string Format { get; set; }
在这种情况下,属性现在必须声明为time-format。
另一方面,如果我们不希望通过视图的标记设置属性的值,我们可以对其应用[HtmlAttributeNotBound]属性。
可以在标记中指定基本类型的属性,以及可以从字符串(例如Guid、TimeSpan和DateTime)和任何枚举转换的其他两个属性。
我们可以使用 Razor 表达式将代码生成的值传递给标记属性:
<time format="GetTimeFormat()">Time is {0}</format>
最后,值得一提的是,如果我们使用一个ModelExpression类型的属性,我们可以为视图的模型获得 Visual Studio 的 IntelliSense:
public ModelExpression FormatFrom { get; set; }
这就是它的样子:

为了实际检索属性的值,我们需要分析ModelExpression的Name和Metadata属性。
限制标记辅助对象的适用性
可以通过以下几种方式限制标记辅助对象的适用性:
- 它可以针对特定的元素,既可以是已知的 HTML 标记,也可以是自定义的 XML 标记。
- 其目标标记必须包含在另一个标记中。
- 其目标标记必须具有某些属性,可能具有特定格式。
- 其标记必须具有特定的结构。
可以根据几个[HtmlTargetElement]属性指定许多限制:
//matches any a elements
[HtmlTargetElement("a")]
//matches any a elements contained inside a div tag
[HtmlTargetElement("a", ParentTag = "div")]
//matches any a elements that target a JavaScript file ending in .js
[HtmlTargetElement("a", Attributes = "[href$='.js']")]
//matches any a elements that target a link starting with ~
[HtmlTargetElement("a", Attributes = "[href^='~']")]
//matches any a elements with a value for the name attribute
[HtmlTargetElement("a", Attributes = "name")]
//matches any a elements with a specific id
[HtmlTargetElement("a", Attributes = "id='link'")]
//matches any a elements that do not have any inner contents (for example, <a/>)
[HtmlTargetElement("a", TagStructure = TagStructure.WithoutEndTag)]
因此,我们具有以下特性:
ParentTag(string:父标签的名称Attributes(string:以逗号分隔的属性和可选值列表TagStructure(TagStructure:标签的格式,默认为Unspecified
TagStructure指定标签是自动关闭(WithoutEndTag)还是可能有内容(NormalOrSelfClosing)。
如果找到与其适用性规则不匹配的标记帮助器,则会在运行时引发异常。可以同时指定多个规则,不同的标记帮助器可以匹配相同的规则。
如果您以*为目标,它将应用于任何元素。
发现标记帮助程序
标签助手需要实现ITagHelper接口。需要显式地添加标记帮助程序,这样做的好地方是_ViewImports.cshtml文件。此处放置的任何内容都将应用于所有视图。让我们看看每一个是如何工作的:
addTagHelper指令添加了一个特定的组件和标记辅助程序:
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
The syntax is @addTagHelper <types>, <assembly>, where * stands for all types.
- 如果我们想阻止使用特定的标记帮助器,我们将应用一个或多个
removeTagHelper指令:
@removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.AnchorTagHelper, Microsoft.AspNetCore.Mvc.TagHelpers
- 如果我们想明确使用标记帮助器,我们可以使用
tagHelperPrefix指令强制它们使用前缀:
@tagHelperPrefix asp:
- 最后,我们还可以选择禁用针对特定标记的任何可能的标记帮助程序。我们只是在它前面加上
!字符:
<!a href="...">link</a>
依赖注入
标记帮助程序由依赖项注入机制实例化,这意味着它们可以在构造函数中接受已注册的服务。
Init、Process或ProcessAsync方法都不提供对执行上下文的访问,但类似于我们可以注入ViewContext的 POCO 控制器和视图组件:
[ViewContext]
public ViewContext ViewContext { get; set; }
从这里,我们可以访问HttpContext和ActionDescriptor,以及路由数据、视图数据等。
研究包含的标记助手
Microsoft.AspNetCore.Mvc.TagHelpers组件中包括以下标签辅助件:
AnchorTagHelper(<a>):呈现锚CacheTagHelper(<cache>):定义一个内存缓存区ComponentTagHelper(<component>):呈现 Blazor 组件DistributedCacheTagHelper(<distributed-cache>:呈现分布式缓存区域EnvironmentTagHelper(<environment>:根据当前环境决定是否渲染FormActionTagHelper(<form>:呈现一个表单,该表单呈现给控制器动作FormTagHelper(<form>):呈现表单ImageTagHelper(<img>):渲染图像InputTagHelper(<input>):呈现一个输入元素LabelTagHelper(<label>):呈现标签LinkTagHelper(<link>):呈现内部链接OptionTagHelper(<option>):呈现选择选项PartialTagHelper(<partial>):渲染剃刀局部视图RenderAtEndOfFormTagHelper(<form>):在表单末尾呈现内容ScriptTagHelper(<script>):呈现脚本SelectTagHelper(<select>):呈现select元素TextAreaTagHelper(<textarea>):呈现文本区域ValidationMessageTagHelper(<span>):呈现验证消息占位符ValidationSummaryTagHelper(<div>):呈现验证摘要占位符
其中一些将以~开头的 URL 转换为服务器特定地址,或添加控制器和操作属性,这些属性反过来转换为控制器操作 URL:
<a href="~">Root</a>
<a asp-controller="Account" asp-action="Logout">Logout</a>
<form asp-controller="Account" asp-action="Login">
</form>
例如,您可以将应用部署在/或虚拟路径下,如/admin。如果您事先不知道这一点,您不能将链接硬编码为指向/,而是可以使用~,ASP.NET Core 框架将确保它设置为正确的路径。
然而,其他一些标签功能非常强大,提供了非常有趣的特性。
<a>标记帮助器为锚提供了一些属性,允许您针对特定控制器、页面或命名路由的特定操作:
<a
asp-action="ActionName"
asp-controller="ControllerName"
asp-page="RazorPageName"
asp-route="RouteName"
asp-area="AreaName">...</a>
请注意,我们如何始终指定区域名称,而不管我们是以控制器的操作、Razor 页面还是以名称为目标的特定路由。
如果添加asp-action而不是asp-controller属性,则默认为当前控制器。
其性质如下:
asp-action(string:控制器动作的名称。asp-area(string:区域名称。asp-controller(string:控制器名称,应与asp-action配合使用。如果未提供,则默认为当前控制器。asp-page(string:剃须刀页面的名称。asp-route(string):端点定义中指定的路由名称。
您可以使用"~/"而不是"/"启动超链接,这意味着可以根据应用的基本路径映射具有的本地路径-例如,如果应用部署到"/app",则"~/file"的 URL 将变为"/app/file"。
标签
此标记帮助器将其中声明的内容缓存在内存缓存中(在依赖项注入框架中注册的任何IMemoryCache实例)。我们唯一的选择是保持缓存的持续时间,以及它是相对的、绝对的还是滑动的。让我们来看一个最基本的例子:
<cache expires-after="TimeSpan.FromMinutes(5)">
@DateTime.Now
</cache>
这将在内存缓存中的<cache>标记中保留字符串一段时间。我们还具有以下属性:
enabled(bool:是否启用(默认)expires-after(TimeSpan:相对到期时间的值expires-on``DateTime:绝对有效期expires-sliding(TimeSpan):滑动过期,与相对过期几乎相同,只是每次命中缓存时都会重新启动priority(CacheItemPriority:缓存的优先级,默认为Normalvary-by(string):用于改变缓存的任意(可能是动态)字符串值vary-by-cookie(string):以逗号分隔的 cookie 名称列表,可根据其改变缓存vary-by-header(string):以逗号分隔的头名称列表,用于根据不同的头名称改变缓存vary-by-query(string):一个以逗号分隔的query字符串参数名称列表,可根据其改变缓存vary-by-route(string):一个以逗号分隔的路由数据参数列表,用于改变缓存vary-by-user(bool:是否根据登录用户名改变缓存(默认为false)
必须提供expires-after、expires-on或expires-sliding,但默认值为 20 分钟。对于vary-by,通常设置一个模型值,如订单或产品 ID,如下所示:
<cache vary-by="@ProductId">
...
</cache>
标签
此标记助手仅介绍给.NET Core 3.0,与 Blazor 相关,我们将在第 17 章、介绍 Blazor中详细介绍。本质上,它呈现 Blazor 组件(一个.razor文件)。它接受以下参数:
type(string).razor文件的名称。render-mode(RenderMode):在第 17 章、介绍 Blazor中讨论的一种可能的渲染模式。param-XXX(string:传递给 Blazor 组件的可选参数;XXX应与组件上的属性名称匹配。
标签如下:
<component type="typeof(SomeComponent)" render-mode="ServerPrerendered" param-Text="Hello, World"/>
标签
<distributed-cache>标记与<cache>标记帮助器相同,只是它使用分布式缓存(IDistributedCache。它将另一个属性添加到由<cache>-name(string)提供的属性中,这是分布式缓存项的唯一名称。每个条目都应有自己的标签:
<distributed-cache name="redis" />
标签
<environment>标记也非常方便,它提供了根据正在运行的环境添加内容的功能(例如,Development、Staging和Production:
<environment names="Development,Staging">
<script src="development/file.js"></script>
</environment>
<environment names="Production">
<script src="production/file.js"></script>
</environment>
从 ASP.NET Core 2 开始,除了names之外,我们还有两个新属性—include和exclude。include与names完全相同,而exclude的功能与您期望的完全相同,它显示了所有环境的内容,但命令后面列出的环境除外(逗号分隔)。
这些属性的属性如下所示:
include:提供用于渲染的环境列表exclude:提供要从渲染中排除的环境列表
exclude始终优先。
可以使用表单标记帮助器代替IHtmlHelper.BeginForm()。两者都提供相同的功能,包括发布到特定的控制器操作和添加防伪令牌作为隐藏字段(有关更多信息,请参阅第 11 章、安全)。让我们看一下以下示例:
<form asp-controller="Home" asp-antiforgery="false" asp-action="Process">
默认情况下,防伪造功能处于启用状态。其特性如下:
asp-controller:如果不提供控制器名称,则默认为当前名称(如果使用 MVC)。asp-action:控制器的动作方式- asp 区域:目标控制器所在区域的名称
asp-page:处理表单的刮胡刀页面- asp 页面处理程序:Razor 页面中处理表单的页面处理程序方法的名称
asp-route:指定路线asp-antiforgery:决定是否检测默认打开的请求伪造
请注意,asp-controller、asp-area和asp-action可以一起使用,但将它们与asp-route或asp-page结合使用没有任何意义,因为它们是指定目的地的不同方式。
<script>标记帮助器允许为源属性指定测试、默认和回退值。测试是一个 JavaScript 表达式;让我们看一个例子:
<!-- if the current browser does not have the window.Promise property load a polyfill -->
<script asp-fallback-test="window.Promise" src="file.js" asp-fallback-src="polyfill.js"></script>
它还可用于一次将所有文件加载到文件夹中(某些可能的例外情况除外):
<script asp-src-include="~/app/**/*.js" asp-src-exclude="~/app/services/**/*.js"></script>
最后,它还可以通过向本地脚本添加版本号来阻止缓存;此版本反映文件的时间戳:
<script src="~/file.js" asp-append-version="true"></script>
您可能已经注意到了src属性中的前导~符号;它将自动替换为应用的根文件夹。例如,如果您的应用部署在/中,它将使用/,但如果部署在/virtualPath中,则~将被/virtualPath替换。这是第 2 章、配置中描述的基本路径。
标签
<link>标记帮助器可以在本地 URL 后面加上一个版本的后缀,使其对缓存友好:
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true"/>
与<script>标记帮助器类似,它有条件地包含内容:
<link rel="stylesheet" href="file.css" asp-fallback-href="otherfile.css"
asp-fallback-test-class="hidden" asp-fallback-test-property
="visibility"
asp-fallback-test-value="hidden" />
<select>标记帮助器知道如何从可枚举类型的模型属性或SelectListItem对象集合检索项:
@functions
{
IEnumerable<SelectListItem> GetItems()
{
yield return new SelectListItem { Text = "Red", Value = "#FF0000" };
yield return new SelectListItem { Text = "Green",
Value = "#00FF00" };
yield return new SelectListItem { Text = "Blue",
Value = "#0000FF",
Selected = true };
}
}
<select asp-items="GetItems()"/>
有两个重要特性:
asp-for:从中检索当前所选项目(或项目列表)的属性或方法asp-items:从中检索要填充列表的项目的属性或方法
标签
此标记已引入 ASP.NET 2.1;它呈现了一个局部视图,这与RenderPartial(Async)和Partial(Async方法的功能非常相似。
<partial name="_PartialFile" for="ModelProperty" model="Model" view-data="ViewData"></partial>
除了name部分视图(这是唯一必需的属性)之外,我们还可以向其传递一个视图数据对象(view-data和一个模型(一个for属性或model)。for属性可用于传递相对于包含视图的模型的表达式;例如,模型的属性。如果愿意,可以通过为model属性指定一个值,将全新模型传递给局部视图。请注意,for和model是相互排斥的;您只能使用其中一个。如果不使用其中任何一个,则将当前模型传递给局部视图。
其性质如下:
for(Expression):相对于当前模型传递到局部视图的可选表达式。不能与model一起使用。model(object:传递到局部视图的可选模型。如果已设置,则它必须与其声明的模型类型匹配。不能与for一起使用。name``string:必填项。这是要渲染的局部视图的名称。
验证消息和摘要
ValidationMessageTagHelper和ValidationSummaryTagHelper标记帮助程序仅为整个模型的<span>标记和<div>标记中的任何模型属性添加验证消息。例如,假设您想要获取Email模型属性的当前验证消息。您将执行以下操作:
<span asp-validation-for="Email"/>
对于整个模型,请执行以下操作:
<div asp-validation-summary/>
下一个主题将标记帮助器的概念介绍到更高的层次。我们将看到标记帮助器组件是如何工作的。
标记辅助对象组件
将标签助手组件引入 ASP.NET Core 2.0。它们是使用 DI 在输出中插入标记的一种方法。例如,想象一下,在视图中的特定位置插入 JavaScript 或 CSS 文件。
标记助手组件必须实现ITagHelperComponent并在 DI 框架中注册(方法ConfigureServices:
services.AddSingleton<ITagHelperComponent, HelloWorldTagHelperComponent>();
ITagHelperComponent接口只指定了一种方法ProcessAsync。每个注册的标记帮助器组件都有其ProcessAsync方法,用于当前视图(包括布局)中找到的每个标记,使其有机会注入自定义标记帮助器:
public class HelloWorldTagHelperComponent : TagHelperComponent
{
public override Task ProcessAsync(TagHelperContext context,
TagHelperOutput output)
{
if (context.TagName.ToLowerInvariant() == "head")
{
output.Content.AppendHtml("<script>window.alert('Hello,
World!')</script>");
}
return Task.CompletedTask;
}
}
如果此标记为head,则此示例在当前标记内容的末尾插入自定义 JavaScript。
TagHelperComponent类实现ITagHelperComponent并提供虚拟方法,我们可以随意重写这些方法。我们有与ITagHelper接口相同的两种方法—Init和ProcessAsync,并且它们的使用方式相同。
ProcessAsync取两个参数,与ITagHelper接口ProcessAsync方法取的参数相同。
Tag helper components, as they are instantiated by the DI framework, fully support constructor injection.
现在让我们讨论一下局部视图,它是 Razor MVC 的构建块之一。
局部视图
我们已经在第 8 章、API 控制器中介绍了部分视图。尽管它们是重用内容的一种非常有趣的机制,但它们在历史上存在一个问题,即它们不能跨程序集重用。总有办法解决这个问题;想法是将视图的.cshtml文件标记为嵌入式资源:

然后,我们只需要使用一个文件提供程序,它知道如何从程序集嵌入的资源中检索文件内容。为本例添加Microsoft.Extensions.FileProviders.EmbeddedNuGet 包。
在ConfigureServices中注册 MVC 服务时,我们需要注册另一个文件提供程序EmbeddedFileProvider,将其传递给包含嵌入式资源的程序集:
services
.AddMvc()
.AddRazorOptions(options =>
{
var assembly = typeof(MyViewComponent).GetTypeInfo().Assembly;
var embeddedFileProvider = new EmbeddedFileProvider(assembly,
"ReusableComponents");
options.FileProviders.Add(embeddedFileProvider);
});
在本例中,MyViewComponent类位于嵌入视图的同一程序集上,其默认名称空间为ReusableComponents。尝试加载文件时,ASP.NET Core 将遍历所有已注册的文件提供程序,直到其中一个返回非空结果。
幸运的是,我们现在有了Razor 类库,我们将很快介绍这些类库。
局部视图与视图组件
这两种机制是相似的,但如果要在视图组件中呈现较大的 HTML 块,则可以选择使用部分视图,因为需要操作字符串并通过代码返回它们。另一方面,局部视图是外部文件,这可能具有优势。
接下来,我们将讨论一些完全不同的代码库,您可以在项目中重用它们。
理解 Razor 类库
Razor 类库已引入 ASP.NET Core 2.2。这意味着所有基于代码或文件的组件都可以添加到程序集中,然后由应用引用。例如,如果这个类库包含多个.cshtml文件,我们可以在控制器中引用它们,或者在应用中为它们提供覆盖,只要遵循相同的路径。例如,想想 Identity 提供的身份验证和注册视图;如果你不喜欢其中任何一个,你可以提供一个替代方案,同时保留其他方案。
可以使用 Visual Studio 创建 Razor 类库:

它实质上生成一个.csproj文件,该文件使用Microsoft.NET.Sdk.RazorSDK(Razor 类库),而不是Microsoft.NET.Sdk.Web(用于 web 应用)或Microsoft.NET.Sdk(用于.NET Core 程序集):
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>netstandard3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc" />
</ItemGroup>
</Project>
当引用 Razor 类库程序集时,ASP.NET Core 知道该做什么,并且您无需任何其他工作即可引用其组件。
引用静态内容
如果您在 Razor 类库项目中创建一个wwwroot文件夹,您可以在引用该文件夹的项目中访问其中存储的任何静态内容(如.js、.css和图像)。只需使用以下格式创建链接:
<script src="_content/<ClassLib>/<File>"></script>
这里,<ClassLib>是引用的 Razor 类库的名称,<File>是静态文件的名称。需要记住的一点是,您需要支持加载静态文件,例如,在Configure中,添加以下内容:
app.UseStaticFiles();
引用外部组件
可以通过引用包含视图零部件和标记辅助对象的部件来添加视图零部件和标记辅助对象,这是在创建或处理项目时完成的。但是,我们也可以在运行时添加引用。这些被称为应用部件。
为了为 Razor 类库注册应用部件,我们将执行以下操作:
services
.AddMvc()
.ConfigureApplicationManager(options =>
{
var path = "<path-to-razor-class-library-dll>";
var asm = Assembly.LoadFrom(path);
options.ApplicationParts.Add(new CompiledRazorAssemblyPart(asm));
});
CompiledRazorAssemblyPart应用于 Razor 类库,其中还包括静态(基于文件的)资源。我们也可以对类型执行此操作,在这种情况下,要使用的类是AssemblyPart。
在这里,我们看到了如何从外部组件引用零件,外部组件可以包括任何可重用组件。这是本章的最后一个主题。在下一章中,我们将介绍过滤器。
总结
在本章中,我们看到我们总是为视图组件、标记帮助器和标记帮助器组件使用提供的基类,因为它们使我们的生活更加轻松。
在可能的情况下,最好使用标记帮助程序而不是 HTML 帮助程序,并编写我们自己的标记帮助程序,因为它们比代码更容易阅读。标记帮助器组件对于在特定位置自动插入代码非常有用。<cache>、<distributed-cache>和<environment>标记助手非常有趣,它们将为您提供很多帮助。
然后,我们看到,当您有一个更易于用 HTML 编码的模板时,局部视图比视图组件更可取。视图组件都是关于代码的,通过字符串连接实现 HTML 比较困难。另一方面,视图组件使您可以更轻松地传递参数。
Razor 类库是在项目之间分发静态资产的一种新方法。一定要使用它们!
我们还了解到,标记帮助器组件是一种非常好的方法,可以从集中的位置向任何地方注入 HTML 元素。将它们用于常见的 CSS 和 JavaScript。
在本章中,我们研究了跨项目重用组件的技术。代码重用几乎总是一个好主意,您可以使用带有参数的视图组件来帮助实现这一点。在下一章中,我们将介绍过滤器,它是一个拦截并可能修改请求和响应的过程。
问题
您现在应该能够回答以下问题:
- 如何从其他部件加载局部视图?
- 渲染局部视图的两种方式是什么?
- 标记帮助器和标记帮助器组件之间有什么区别?
- 如何根据环境限制视图上显示的内容?
- Razor 类库和类库之间有什么区别?
- 什么是嵌入式资源?
- 执行视图组件的两种语法是什么?
十、理解过滤器
过滤器是 ASP.NET Core 提供的一种机制,用于应用横切关注点,如日志记录、异常处理、强制授权和身份验证等。从 ASP.NETMVC 的早期开始,它们就已经存在,但在核心部分得到了扩展。
ASP.NET Core 中的过滤器是一种拦截机制,通过它我们可以在处理请求之前而不是之后执行代码。可以将它们视为一种向管道中添加自定义步骤的方法,而不必实际这样做;它保持不变,但我们对截取的内容有更细粒度的控制。它们对于实现横切操作非常有用,例如访问控制、缓存或日志记录。在这里,我们将讨论以下内容:
- 了解不同的过滤器类型
- 了解授权过滤器
- 了解资源过滤器
- 了解动作过滤器
- 了解结果过滤器
- 了解异常过滤器
- 了解页面过滤器
- 了解始终运行结果过滤器
技术要求
为了实现本章中介绍的示例,您需要.NET Core 3 SDK 和文本编辑器。当然,VisualStudio2019(任何版本)满足所有要求,但您也可以使用 VisualStudio 代码。
管道中的过滤器
过滤器是管道的一部分。它们在 ASP.NET Core 选择要运行的控制器(或 Razor 页面)后执行。下图对此进行了说明:

Image obtained from https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters
过滤器是一种拦截机制。过滤器在控制器(或页面)操作之前、之后或代替控制器(或页面)操作执行某些操作。下一节将解释不同的过滤器类型。
了解过滤器类型
在 ASP.NET Core 3(以及版本 2)中,我们有以下过滤器:
- 授权
IAuthorizationFilter和IAsyncAuthorizationFilter:控制执行当前请求的用户是否具有访问指定资源的权限;如果没有,则管道的其余部分短路,并返回错误消息。 - 资源(
IResourceFilter和IAsyncResourceFilter:在请求被授权后但在动作选择和模型绑定之前执行。这些是 ASP.NET Core 的新特性。 - 动作(
IActionFilter和IAsyncActionFilter:在调用动作方法前后执行。 - 结果(
IResultFilter和IAsyncResultFilter:这些发生在实际执行动作结果之前和之后(即IActionResult.ExecuteResultAsync方法)。 - 异常(
IExceptionFilter和IAsyncExceptionFilter:在处理动作的过程中抛出异常时调用。 - 页面(
IPageFilter和IAsyncPageFilter:这些发生在调用 Razor 页面处理程序方法之前和之后,对于 ASP.NET Core 2 来说是新的。 - 始终运行结果(
IAlwaysRunResultFilter和IAsyncAlwaysRunResultFilter):这是 ASP.NET Core 2.1 的新功能,类似于动作过滤器,但与此不同的是,始终运行结果始终运行,即使出现异常:

Image obtained from https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters
这类筛选器具有在目标事件之前和之后调用的 pre 方法和 post 方法,分别是授权、资源、操作、结果和页面。前方法版本始终以执行结束,后方法版本以执行结束。例如,对于动作过滤器,这些方法被称为OnActionExecuting和&OnActionExecuted。当然,授权和异常过滤器只提供一个方法-OnAuthorization和OnException,但您可以将它们视为事件后方法。
所有过滤器的唯一基类是IFilterMetadata,它不提供方法或属性,只是作为一个标记接口。因此,ASP.NET Core 框架必须检查过滤器的具体类型,以尝试识别其实现的已知接口。
让我们从两种横切类型的过滤器开始,它们有两种类型。
同步与异步
每种过滤器类型都提供同步和异步版本,后者有一个Async前缀。两者的区别在于,在异步版本中,只定义 pre 方法,并异步调用它;对于动作过滤器,同步版本提供OnActionExecuting/OnActionExecuted,异步版本提供单一OnActionExecutionAsync方法。只有异常筛选器不提供异步版本。
选择异步或同步筛选器,但不能同时选择两者!现在,让我们看看过滤器的适用范围。
过滤范围
过滤器可应用于不同级别:
- 全局:全局筛选器适用于所有控制器和操作,因此它们还捕获抛出的任何异常。全局过滤器通过
AddMvc方法添加到MvcOptions类的Filters集合中:
services.AddMvc(options =>
{
options.Filters.Add(new AuthorizeAttribute());
});
- 控制器:控制器级过滤器通常通过应用于控制器类的资源添加,并应用于对其调用的任何操作:
[Authorize]
public class HomeController : Controller { ... }
- 动作:这些过滤器仅适用于声明它们的动作方法:
public class HomeController
{
[Authorize]
public IActionResult Index() { ... }
}
MvcOptions的Filters集合可以采用过滤器类型或过滤器实例。如果希望使用 DI 框架构建过滤器,请使用过滤器类型。
现在让我们看看过滤器的执行顺序。
执行令
过滤器按以下顺序调用:
- 批准
- 资源
- 行动
- 页面(仅适用于剃须刀页面)
- 后果
- 始终运行结果
当然,异常和页面过滤器是特殊的,因此它们仅在发生异常时或分别在调用 Razor 页面时被调用。
因为大多数过滤器都有 pre 方法和 post 方法,所以实际顺序如下所示:
IAuthorizationFilter.OnAuthorizationIResourceFilter.OnResourceExecutingIActionFilter.OnActionExecuting<controller action>IActionFilter.OnActionExecutedIResultFilter.OnResultExecutingIAlwaysRunResultFilter.OnResultExecutingIAlwaysRunResultFilter.OnResultExecutedIResultFilter.OnResultExecutedIResourceFilter.OnResourceExecuted
<Controller action>当然是控制器上的动作方式,如果我们使用 MVC(剃须刀页面请参见第 7 章、实现剃须刀页面)。
有可能使某些滤波器短路;例如,如果在资源或授权筛选器上,我们通过将值设置为上下文的Result属性来返回结果,则不会调用操作筛选器或设置为在其之后执行的任何其他筛选器。但是,任何已注册的始终运行结果筛选器都将始终运行。
根据滤波器的应用方式,我们可以影响该顺序;例如,对于全局过滤器,根据MvcOptions.Filters集合中的索引对相同类型的过滤器进行排序,如下所示:
options.Filters.Insert(0, new AuthorizeAttribute()); //first one
对于属性过滤器,IOrderedFilter接口提供一个Order属性,该属性可用于排序相同范围(全局、控制器或操作)的属性:
[Cache(Order = 1)]
[Authorize(Order = 0)]
[Log(Order = 2)]
public IActionResult Index() { ... }
现在让我们看看如何通过属性应用和排序过滤器。
通过属性应用过滤器
过滤器接口可以通过一个常规属性(即Attribute类)实现,然后它将充当过滤器;有一些抽象的基本属性类ActionFilterAttribute(操作和结果过滤器)、ResultFilterAttribute(结果过滤器)和ExceptionFilterAttribute(异常过滤器)可以子类化以实现此行为。这些类实现了同步和异步版本,还支持通过实现IOrderedFilter对调用它们的顺序进行排序。因此,如果您想拥有一个处理操作和结果的过滤器属性,您可以从ActionFilterAttribute继承并实现其一个或多个虚拟方法:
-
OnActionExecuting -
OnActionExecuted -
OnActionExecutionAsync -
OnResultExecuting -
OnResultExecuted -
OnResultExecutionAsync
例如,如果您希望在调用某个操作之前覆盖 abstractActionFilterAttributefilter 属性上的某些行为以执行某些操作,则可以尝试以下操作:
public class LogActionAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext
context)
{
var loggerFactory = context.HttpContext.RequestServices.
GetRequiredService<ILoggerFactory>();
var logger = _loggerFactory.CreateLogger
(context.Controller.GetType());
logger.LogTrace($"Before {context.ActionDescriptor.
DisplayName}");
}
}
在这里,我们通过属性的构造函数注入记录器工厂,该构造函数继承自ActionFilterAttribute,并从中获取记录器。
过滤器排序
应根据以下任一要求订购相同类型的过滤器:
- 插入到
MvcOptions.Filters集合中的顺序 - 如果过滤器实现了
IOrderedFilter,则其Order属性
例如,授权类型的所有全局筛选器将根据这些规则进行排序,然后所有控制器级筛选器将应用于控制器,然后所有操作级筛选器。
所有的ActionFilterAttribute、MiddlewareFilterAttribute、ServiceFilterAttribute和TypeFilterAttribute类都实现了IOrderedFilter;这些是将过滤器注入控制器和操作的最常用方法。
现在让我们看看如何创建过滤器。
工厂和供应商
过滤器工厂只是创建过滤器的类的实例;唯一需要的是它实现了IFilterFactory,因为它继承了IFilterMetadata,所以也可以用作全局过滤器或自定义属性。你为什么要那样做?好吧,因为当过滤器工厂运行时,您可能会从当前的执行上下文中学到更多东西。让我们看一个例子:
public class CustomFilterFactory : IFilterFactory
{
public bool IsReusable => true;
public IFilterMetadata CreateInstance(IServiceProvider
serviceProvider)
{
//get some service from the DI framework
var svc = serviceProvider.GetRequiredService<IMyService>();
//create our filter passing it the service
return new CustomFilter(svc);
}
}
该过滤器工厂依赖于需要注册的特定服务。为了全局注册此自定义筛选器工厂,我们使用以下方法:
services.AddMvc(options =>
{
options.Filters.Insert(0, new CustomFilterFactory());
});
在一个属性中实现IFilterFactory同样简单,所以我不在这里展示。
过滤器工厂的合同很简单:
IsReusable(bool):告诉框架跨请求重用过滤器工厂是否安全。CreateInstance:此方法返回一个过滤器。
CreateInstance方法将IServiceProvider实例作为其唯一参数,并返回IFilterMetadata对象,这意味着您可以返回您想要的任何类型的过滤器(甚至其他过滤器工厂)。
过滤器提供程序(IFilterProvider是作为 MVC 配置的一部分在 DI 框架中注册的实际实现,它触发所有不同的过滤器行为。默认实现为DefaultFilterProvider。IFilterProvider接口只有一个属性:
Order(int:执行提供者的顺序。这提供了以下两种方法:
DI 呢?有没有办法将其与过滤器一起使用?哦,是的,有,我们看看刚才是怎么回事!
DI
迄今为止,我们通过Filters集合或通过属性在全球范围内看到的添加过滤器的方法不是 DI 友好的;在第一种情况下,添加一个已经实例化的对象,对于属性,它们是 DI 框架未实例化的静态数据。但是,我们有[ServiceFilter]属性,它接受过滤器类(任何类型)的类型作为其唯一必需的参数,并使用 DI 框架对其进行实例化;更重要的是,它甚至允许订购:
[ServiceFilter(typeof(CacheFilter), Order = 2)]
[ServiceFilter(typeof(LogFilter), Order = 1)]
public class HomeController : Controller { ... }
例如,LogFilter类可能如下所示:
public class LogFilter : IAsyncActionFilter
{
private readonly ILoggerFactory _loggerFactory;
public LogFilter(ILoggerFactory loggerFactory)
{
this._loggerFactory = loggerFactory;
}
public Task OnActionExecutionAsync(ActionExecutingContext
context, ActionExecutionDelegate next)
{
var logger = this._loggerFactory.CreateLogger
(context.Controller.GetType());
logger.LogTrace($"{context.ActionDescriptor.DisplayName}
action called");
return next();
}
}
与往常一样,ILoggerFactory由 DI 框架传入控制器,LogFilter类本身必须注册:
services.AddSingleton<LogFilter>();
还有另一个特殊属性[TypeFilter],它在给定某个类型和一些可选参数的情况下,尝试实例化它:
[TypeFilter(typeof(CacheFilter), Arguments = new object[] { 60 * 1000 * 60 })]
这些参数作为参数传递给筛选器类型的构造函数。这一次,没有使用 DI;在尝试构建具体类型时,它将只传递它接收到的任何值,方式与Activator.CreateInstance相同。
如果需要,您可以通过为IFilterProvider服务提供自己的实现来更改默认过滤器提供程序:
services.AddSingleton<IFilterProvider, CustomFilterProvider>();
这个过程很复杂,因为您需要返回来自全局存储库(MvcOptions)的过滤器、应用于类的属性、方法等等,所以您最好知道自己在做什么。如果有疑问,请保留现有的实现。
另一种方式是使用RequestServices服务定位器:
var svc = context.HttpContext.RequestServices.GetService<IMyService>();
这在每个公开HttpContext对象的过滤器中都可用。
访问上下文
您可以使用HttpContext.Items集合将上下文从一个筛选器传递到另一个筛选器,如下所示:
public class FirstFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context) { }
public void OnActionExecuted(ActionExecutedContext context)
{
context.HttpContext.Items["WasFirstFilterExecuted"] = true;
}
}
public class SecondFilter : IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context) { }
public void OnActionExecuting(ActionExecutingContext context)
{
if (context.HttpContext.Items["WasFirstFilterExecuted"]
is bool parameter && parameter)
{
//proceed accordingly
}
}
}
被调用的第一个过滤器在当前请求项中设置一个标志,第二个过滤器检查其存在并相应地执行一个操作。我们只需要确定应用过滤器的顺序,这可以通过ActionFilterAttribute、ServiceFilterAttribute和TypeFilterAttribute公开的IOrderedFilter.Order属性实现。
现在,让我们看看过滤器是如何工作的。
应用授权筛选器
这种过滤器用于授权当前用户。最臭名昭著的授权属性是[Authorize],它可以用于常见检查,例如被验证、属于给定角色或实现给定策略。
此属性既不实现IAuthorizationFilter也不实现IAsyncAuthorizationFilter,而是实现IAuthorizeData,它允许我们指定角色名称(Roles属性)、自定义策略名称(Policy或身份验证方案(AuthenticationSchemes)。该属性由名为AuthorizeFilter的内置过滤器处理,在添加授权中间件(AddAuthorization时默认添加该过滤器。
可以在授权属性中签入的其他内容包括,例如:
- 正在验证客户端的源 IP 或域
- 验证给定 cookie 是否存在
- 正在验证客户端证书
所以,对于定制授权,我们要么需要实现IAuthorizationFilter要么需要实现IAsyncAuthorizationFilter;第一个公开了一个方法,OnAuthorization。传递给OnAuthorization方法的上下文对象为当前请求和 MVC 动作公开HttpContext、ModelState、RouteData和ActionDescriptor;您可以使用其中任何一个来执行自己的自定义授权。如果您不希望授权访问,可以在上下文的Result属性中返回UnauthorizedResult,如下所示:
public void OnAuthorization(AuthorizationFilterContext context)
{
var entry = Dns.GetHostEntryAsync(context.HttpContext.
Connection.RemoteIpAddress)
.GetAwaiter()
.GetResult();
if (!entry.HostName.EndsWith(".MyDomain",
StringComparison.OrdinalIgnoreCase))
{
context.Result = new UnauthorizedResult();
}
}
在这种情况下,如果请求不是来自已知域,则拒绝访问。
AuthorizationFilterContext类具有以下属性:
ActionDescriptor(ActionDescriptor:要调用的操作的描述符Filters(IList<IFilterMetadata>:绑定到此请求的筛选器HttpContext(HttpContext:HTTP 上下文ModelState(ModelStateDictionary:模型状态(不用于授权过滤器)Result(IActionResult:绕过请求管道返回客户端的可选结果RouteData(RouteData:请求的路由数据
您可能想添加一个全局过滤器,要求用户在任何地方都经过身份验证;在这种情况下,请记住,至少条目页面和获取凭据的操作需要允许匿名访问。
至于IAsyncAuthorizationFilter,它的OnAuthorizationAsync方法也需要一个AuthorizationFilterContext参数,唯一的区别是它是异步调用的。
现在,让我们来看一些需要遵循的授权策略。
授权策略
在第 9 章可重用组件中,我们讨论了授权处理程序。它们也可以通过AuthorizeFilter类作为全局过滤器添加,该类是一个过滤器工厂。这里有一个例子:
services.AddMvc(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAssertion(ctx => true) //let everything pass
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
在这里,我们正在用一个特定的断言构建一个策略(在本例中,我们允许所有内容都是true,并且我们正在添加一个从该策略构建的全局AuthorizeFilter参数。这将适用于所有请求。
好了,我们已经完成了授权过滤器,现在让我们看看资源过滤器。
资源过滤器
在资源过滤器中,您可以应用与授权过滤器类似的逻辑,但它会在授权过滤器之后稍微执行,您可以获得更多信息。例如,当执行资源筛选器时,用户已登录(如果使用身份验证)。资源筛选器的一些常见用途如下:
- 登录中
- 缓存
- 节流
- 修改模型绑定
IResourceFilter接口定义了两种方式:
OnResourceExecuting:在请求到达操作之前调用OnResourceExecuted:动作执行后调用
这些方法中的每一种都分别为事件前和事件后使用一个ResourceExecutingContext和ResourceExecutedContext类型的参数。ResourceExecutingContext提供以下属性,反映处理资源之前的上下文:
Result(IActionResult):如果您希望短接请求管道,您可以在这里设置一个值,所有其他过滤器和中间件将被绕过(除了OnResourceExecuted方法),返回此结果;如果要返回 POCO 值,请将其包装在ObjectResult中。ValueProviderFactories(IList<IValueProviderFactory>:在这里,您可以检查、添加或修改向目标操作的参数提供值时要使用的值提供程序工厂集合。
关于ResourceExecutedContext,我们有以下内容:
-
Canceled(bool):是否在OnResourceExecuting中设置了结果。 -
Exception(Exception:资源处理过程中抛出的任何异常。 -
ExceptionDispatchInfo(ExceptionDispatchInfo):异常调度对象,用于捕获异常的堆栈跟踪,并在保留此上下文的同时,可以选择重新抛出异常。 -
ExceptionHandled(bool:是否处理异常(如果有),默认为false;如果没有处理,那么框架将重新抛出它。 -
Result(IActionResult):通过OnExecuting方法设置的动作,也可以在这里设置。
如果在处理资源的过程中(在 action 方法或另一个筛选器中)引发异常,并且该异常没有被资源筛选器显式标记为已处理(ExceptionHandled),则框架将引发该异常,从而导致错误。如果您想了解更多信息,请参阅ExceptionDispatchInfo的文档,网址为https://msdn.microsoft.com/en-us/library/system.runtime.exceptionservices.exceptiondispatchinfo.aspx 。
异步备选方案IAsyncResourceFilter只声明了一个方法OnResourceExecutionAsync,采用两个参数ResourceExecutingContext(与OnResourceExecuting方法相同)和ResourceExecutionDelegate;这一个很有趣,因为您可以使用它在运行时将其他中间件注入管道。
以下是缓存筛选器的示例:
[AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public sealed class CacheResourceFilter : Attribute, IResourceFilter
{
public TimeSpan Duration { get; }
public CacheResourceFilter(TimeSpan duration)
{
this.Duration = duration;
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
var cacheKey = context.HttpContext.Request.Path.ToString()
.ToLowerInvariant();
var memoryCache = context.HttpContext.RequestServices.
GetRequiredService<IMemoryCache>();
var result = context.Result as ContentResult;
if (result != null)
{
memoryCache.Set(cacheKey, result.Content, this.Duration);
}
}
public void OnResourceExecuting(ResourceExecutingContext context)
{
var cacheKey = context.HttpContext.Request.Path.ToString()
.ToLowerInvariant();
var memoryCache = context.HttpContext.RequestServices.
GetRequiredService<IMemoryCache>();
if (memoryCache.TryGetValue(cacheKey, out var cachedValue))
{
if (cachedValue != null && cachedValue
is string cachedValueString)
{
context.Result = new ContentResult() {
Content = cachedValueString };
}
}
}
}
此筛选器获取请求路径并检查它从当前上下文检索的IMemoryCache服务是否具有该路径的值,如果有,则从该路径设置内容。这是在执行请求之前(OnResourceExecuting)。执行资源后,过滤器只将内容存储在内存缓存中。为了实现这一点,我们需要注册IMemoryCache服务。
同样,不要同时实现同步和异步接口,因为只会使用异步接口。
这就是资源过滤器;让我们转到动作过滤器。
了解动作过滤器
在调用操作方法之前和之后都会调用操作筛选器,因此可以使用它们执行以下操作(例如):
- 缓存结果
- 修改参数
- 修改结果
现在,我们已经有了 action 方法的参数,这些参数来自于值提供者。这里,过滤器接口为IActionFilter和IAsyncActionFilter。同步方式提供两种方式,OnActionExecuting和OnActionExecuted。它们用于事件前和事件后通知。OnActionExecuting采用ActionExecutingContext类型的单个参数,提供以下属性:
Result(IActionResult):在此处设置一个值,以使请求处理管道短路并向客户端返回一个值,而不实际执行操作。ActionArguments(IDictionary<string, object>:动作方式的参数。Controller(object:目标控制器实例。
您可能想知道为什么Controller属性不是原型为ControllerBase或Controller:别忘了我们可以有 POCO 控制器!
ActionArguments参数为目标操作方法的每个参数都有条目,其值由注册值提供程序提供。
事件后方法OnActionExecuted采用ActionExecutedContext类型的参数,该参数公开了以下属性:
Canceled(bool):是否在OnActionExecuting中设置了结果。Controller(object:控制器实例。Exception(Exception:资源处理过程中抛出的任何异常。ExceptionDispatchInfo(ExceptionDispatchInfo):异常调度对象,用于捕获异常的堆栈跟踪,并在保留此上下文的同时,可以选择重新抛出异常。ExceptionHandled(bool:是否处理异常(如果有),默认为false;如果没有处理,那么框架将重新抛出它。Result(IActionResult):通过OnExecuting方法设置的动作,也可以在这里设置。
对于IAsyncActionFilter,它提供了一个单一的方法,即OnActionExecutionAsync。它以与OnResourceExecutionAsync相同的方式接受两个参数:ActionExecutingContext和ActionExecutionDelegate。ActionExecutionDelegate实例指向管道中的下一个动作过滤器方法。
现在,我们将继续了解结果过滤器以及它们如何在代码中使用。
结果过滤器
如果操作执行成功,则结果筛选器允许您在处理结果之前和之后执行自定义操作。动作结果由IActionResult表示,我们可以在调用ExecuteResultAsync之前和之后运行代码。结果过滤器的一些常见用途包括:
- 缓存(与以前一样)
- 拦截(修改响应)
- 添加响应头
- 结果格式
IResultFilter接口定义了OnResultExecuting和OnResultExecuted方法。第一个参数以ResultExecutingContext的一个实例作为其唯一参数,该参数提供以下属性:
Cancel(bool:是否取消对结果的处理Result(IActionResult:当我们想要绕过返回结果的执行时,要处理的结果Controller(object:控制器实例
对于事件后方法OnResultExecuted,我们在ResultExecutedContext中有以下属性:
Canceled(bool):是否在OnResultExecuting中设置了结果。Controller(object:控制器实例。Exception(Exception:资源处理过程中抛出的任何异常。ExceptionDispatchInfo(ExceptionDispatchInfo):异常调度对象,用于捕获异常的堆栈跟踪,并在保留此上下文的同时,可以选择重新抛出异常。ExceptionHandled(bool:是否处理异常(如果有),默认为false;如果没有处理,那么框架将重新抛出它。Result(IActionResult):通过OnResultExecuting方法设置的动作,也可以在这里设置。
这些与ResourceExecutedContext完全相同。与往常一样,我们还有一个异步版本的结果过滤器IAsyncResultFilter,它遵循相同的模式,提供了一个名为OnResultExecutionAsync的方法,该方法有两个参数ResultExecutingContext类型,具有以下属性:
Cancel(bool:是否取消对结果的处理Result(IActionResult:当我们想要绕过返回结果的执行时,要处理的结果Controller(object:控制器实例
另一个参数是ResultExecutionDelegate,它将指向管道中IAsyncResultFilter类型的下一个委托。下面是一个简单的结果过滤器示例:
public class CacheFilter : IResultFilter
{
private readonly IMemoryCache _cache;
public CacheFilter(IMemoryCache cache)
{
this._cache = cache;
}
private object GetKey(ActionDescriptor action)
{
//generate a key and return it, for now, just return the id
return action.Id;
}
public void OnResultExecuted(ResultExecutedContext context)
{
}
public void OnResultExecuting(ResultExecutingContext context)
{
var key = this.GetKey(context.ActionDescriptor);
string html;
if (this._cache.TryGetValue<string>(key, out html))
{
context.Result = new ContentResult { Content = html,
ContentType = "text/html" };
}
else
{
if (context.Result is ViewResult)
{
//get the rendered view, maybe using a TextWriter, and
//store it in the cache
}
}
}
}
当这个过滤器运行时,它会检查缓存中是否有当前操作和参数的条目,如果有,它会将其作为结果返回。
现在让我们看看处理异常的过滤器。
异常过滤器
这些是最容易理解的;每当异常过滤器(操作、控制器或全局)的作用域下出现异常时,就会调用其OnException方法。可以想象,这对于记录错误非常有用。
OnException方法采用ExceptionContext类型的参数:
Exception(Exception:资源处理过程中抛出的任何异常。ExceptionDispatchInfo(ExceptionDispatchInfo):异常调度对象,用于捕获异常的堆栈跟踪,并在保留此上下文的同时,可以选择重新抛出异常。ExceptionHandled(bool:是否处理异常(如果有),默认为false;如果没有处理,那么框架将重新抛出它。Result(IActionResult):可能是一个动作结果(如果设置了),也可以在这里设置。
没有Controller属性,因为异常可能已在控制器外部引发。
异步接口IAsyncExceptionFilter声明了一个方法OnExceptionAsync,它还接收一个ExceptionContext类型的参数。该行为与其同步对应的行为完全相同,但它是在另一个线程中调用的。
捕获的异常会传播,除非ExceptionHandled属性设置为true。如果您确实处理了异常,则您有责任返回一个结果(Result属性)或向输出中写入一些内容,如本例所示:
public sealed class ErrorFilter : IAsyncExceptionFilter
{
public async Task OnExceptionAsync(ExceptionContext context)
{
context.ExceptionHandled = true;
await context.HttpContext.Response.WriteAsync($"An
error occurred: {context.Exception.Message}");
}
}
此筛选器应注册为全局筛选器:
services.AddMvc(options =>
{
options.Filters.Insert(0, new ErrorFilter());
});
关于异常过滤器的部分到此结束。现在让我们看看 Razor 页面的特定过滤器。
剃须刀页面过滤器
这是一个新的剃须刀页面过滤器。基本上,我们可以在 Razor Pages 模型方法之前或之后启动自定义操作。至于其他过滤器,该过滤器有同步(IPageFilter和异步(IAsyncPageFilter两种版本)。
从同步版本开始,它声明了以下三种方法:
OnPageHandlerSelected:在框架选择处理请求的目标处理程序方法后调用,让开发人员有机会更改此方法OnPageHandlerExecuting:在调用处理程序之前调用OnPageHandlerExecuted:调用处理程序后调用
OnPageHandlerSelected接受PageHandlerSelectedContext类型的参数,此类提供以下属性:
ActionDescriptor(CompiledPageActionDescriptor:描述处理程序和模型类HandlerMethod(HandlerMethodDescriptor:将被调用的方法,可以更改HandlerInstance(object:处理请求的实例
前置事件处理程序OnPageHandlerExecuting接受一个PageHandlerExecutingContext类型的参数,该参数具有以下属性:
ActionDescriptor(CompiledPageActionDescriptor:处理程序和模型类Result(IActionResult:如果要覆盖页面的默认处理,则返回的结果HandlerArguments(IDictionary<string, object>:传递给 handler 方法的参数HandlerMethod(HandlerMethodDescriptor:将在处理程序实例上调用的方法HandlerInstance(object:处理请求的实例
对于 post 事件OnPageHandlerExecuted,我们有一个PageHandlerExecutedContext类型的参数,它的属性与PageHandlerExecutingContext类似:
ActionDescriptor(CompiledPageActionDescriptor:处理程序和模型类。Canceled(bool:通过在 pre 事件中设置结果,当前处理是否被取消。HandlerMethod(HandlerMethodDescriptor:将在处理程序实例上调用的方法。HandlerInstance(object:处理请求的实例。Exception(Exception:资源处理过程中抛出的任何异常。ExceptionDispatchInfo(ExceptionDispatchInfo):异常分派对象,用于捕获异常的堆栈跟踪,并可以选择重新抛出异常。ExceptionHandled(bool:是否处理异常;默认值为 false,这意味着框架将重新抛出它。Result(IActionResult:如果要覆盖页面的默认处理,则返回的结果。
最后,异步接口提供了两种异步方法,分别对应于OnPageHandlerSelected(现称OnPageHandlerSelectionAsync)和OnPageHandlerExecuted(现称OnPageHandlerExecutionAsync)OnPageHandlerSelectionAsync的单参数为PageHandlerSelectedContext实例,OnPageHandlerExecutionAsync取PageHandlerExecutingContext和PageHandlerExecutionDelegate两个参数。PageHandlerExecutionDelegate也是一个委托,它指向管道中相同类型的下一个方法(如果存在的话)。
这就是 Razor Pages 过滤器的全部功能,现在让我们看看另一种特殊的过滤器。
始终运行结果过滤器
总是运行的结果过滤器(IAlwaysRunResultFilter和IAsyncAlwaysRunResultFilter是最近才引入的有趣过滤器(ASP.NET Core 2.1)。它的目的是始终让某些内容运行,即使某个操作没有运行,例如出现异常或授权或资源筛选器使管道短路并直接返回某些内容时。它提供两种方法,一种在处理结果之前调用,另一种在处理结果之后调用(或者在请求结束时短路之后)。这些方法分别采用一个ResultExecutingContext或ResultExecutedContext参数,这是我们在处理结果过滤器时讨论的。
例如,始终运行结果过滤器的一个可能用途是检查控制器是否返回了一个null值,如果是,则将其替换为NotFoundResult。我们可以通过以下代码实现这一点:
[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
public sealed class NotFoundAttribute : Attribute, IAlwaysRunResultFilter
{
public void OnResultExecuted(ResultExecutedContext context)
{
if (context.Result is ObjectResult objectResult &&
objectResult.Value == null)
{
objectResult.Value = new {}; //anonymous method,
//add whatever properties you like
}
}
public void OnResultExecuting(ResultExecutingContext context)
{
}
}
此属性应用于类(仅在控制器中有意义)时,检查结果是否为null,在这种情况下,它将其设置为NotFoundResult。
总结
在本章中,我们看到,通常情况下,每个筛选器方法的异步版本都是首选的,因为它们本质上更具可伸缩性—在调用筛选器时线程不会阻塞,而且在同一个类上,线程不会混合筛选器接口的同步版本和异步版本,因为只调用异步版本。最好不要混合使用同步和异步过滤器!在本节中,我们还了解了过滤器类型的基础。
一个重要的观察结果是,如果需要将依赖项注入到过滤器中,我们可以通过[ServiceFilter]属性使用 DI。对于全局筛选器,将筛选器类型添加到AddMvc中的MvcOptions.Filters集合,而不是筛选器实例。
然后,我们发现我们需要了解每个过滤器的预期用途,而不是使用资源过滤器进行授权。如果需要截取操作参数或执行缓存,请使用操作筛选器,并使用结果筛选器修改输出或结果格式。然后,我们看到异常过滤器对于记录失败至关重要;在全球范围内,这些都是安全的。我们还了解到,我们需要应用授权过滤器来保护任何敏感资源,并选择可能的最佳授权(角色、策略或仅进行身份验证)。
接下来,我们了解到关注过滤器的范围是至关重要的,仔细选择全局、控制器或操作,无论什么最适合您的需要。
总的来说,在本章中,我们研究了 ASP.NET Core 的拦截机制,在下一章中,我们将讨论如何保护访问以及如何使用视图和表单。
问题
您现在应该能够回答以下问题:
- 用于控制资源授权的两个接口是什么?
- 为什么每种过滤器都有两种版本?
- 如何通过在操作方法上指定其类型来应用筛选器?
- 我们如何将排序应用于过滤器的应用?
- 我们可以应用过滤器的不同级别是什么?
- 我们如何将上下文从一个过滤器传递到另一个过滤器?
- 过滤器如何使用 DI?
十一、安全
安全是当今非常热门的话题;没有一家公司能够像最近这样暴露客户的数据,这是非常不幸的。安全不仅仅是数据;它涵盖了很多方面。这不仅仅是限制对网站或其特定部分的访问;它是关于防止上传恶意内容、存储配置(和其他)数据、允许访问特定来源的脚本,以及最重要的是,创建客户端和服务器之间通信的安全通道。
阅读本章后,您将对围绕 ASP.NET Core 应用的安全性的许多方面有很好的理解。
本章将介绍以下主题:
- 验证用户
- 授权请求
- 检查伪造请求
- 应用超文本标记语言(HTML编码
- 使用超文本传输协议安全(HTTPS)
- 理解跨源资源共享(CORS)
- 使用数据保护
- 保护静态文件
- 应用HTTP 严格传输安全(HSTS)
- 了解通用数据保护条例(GDPR)
- 绑定安全
我们将从两个主题开始:认证-谁是谁;和授权-谁能做什么。这些是任何安全 web 应用的构建块。让我们在以下各节中逐一研究。
技术要求
为了实现本章介绍的示例,您需要.NET Core 3软件开发工具包(SDK和某种形式的文本编辑器。当然,VisualStudio2019(任何版本)满足所有要求,但您也可以使用 VisualStudio 代码。
源代码可以在这里从 GitHub 检索:https://github.com/PacktPublishing/Modern-Web-Development-with-ASP.NET-Core-3-Second-Edition 。
验证用户
认证是你告诉你的申请者你是谁的过程;从这一刻起,应用至少会了解您一段时间。
身份验证与身份验证不同,尽管它与授权有关。如果您有需要授权才能访问的资源,则可能需要身份验证。
一般授权流程如下所示:
- 有人请求访问受保护的资源。
- 框架检查用户是否未经授权,并将其重定向到登录页面,并发出
302代码。这是挑战阶段。 - 用户提供其凭据。
- 检查凭证,如果凭证有效,用户将被引导到请求的资源(
HTTP 302),并使用 cookie(通常)将其标识为已登录。 - 否则,框架将重定向到失败的登录页面。
- 现在已授予对受保护资源的访问权限。
以下屏幕截图描述了客户端浏览器和应用之间的 HTTP 流:

Image taken from https://docs.microsoft.com/en-us/aspnet/web-api/overview/security/basic-authentication
在 ASP.NET Core 中,我们使用[Authorize]属性或某种形式的过滤器来限制对资源的访问,无论是通过整个控制器还是通过某些特定的操作方法,如以下代码片段所示:
//whole controller is protected
[Authorize]
public class AdminController { }
public class SearchController
{
//only this method is restricted
[Authorize]
public IActionResult Admin() { ... }
}
除此之外,当我们试图访问其中一个资源时,我们将得到一个401 Unauthorized错误代码。我们需要的是某种形式的中间件,它能够拦截错误代码并相应地进行处理。
下一节仅与 Windows 开发人员相关。我们将首先了解 Windows 的授权工作原理。
使用索赔
现代身份验证和授权使用声明的概念来存储登录用户将有权访问的信息。例如,这将包括角色,但它可以是身份验证提供商(Windows 或第三方)指定的任何其他信息。
在.NET Core 中,所有身份信息都可用的根类是ClaimsPrincipal。HttpContext类中提供了对当前标识的引用,如HttpContext.User。在其中,我们可以找到三个重要属性,具体如下:
Identity(IIdentity:与当前登录用户关联的主标识Identities(IEnumerable<ClaimsIdentity>:与当前登录用户关联的身份集合;它通常只包含一个标识Claims(IEnumerable<Claim>:与当前登录用户关联的索赔的集合
Identity属性包含以下内容:
Name(string:登录用户的姓名,如有IsAuthenticated(bool:当前用户是否经过身份验证AuthenticationType(string:当前认证类型,如果使用
不要忘记,正如我们将看到的,我们可以在同一个应用上使用多种身份验证类型,每种类型都有不同的名称,用户将根据其中一种类型进行身份验证。
对于Claims类,一个典型的索赔集合可能包含以下索赔类型,这些类型将映射到Claim类的Type属性:
-
ClaimTypes.Authentication -
ClaimTypes.Country -
ClaimTypes.DateOfBirth -
ClaimTypes.Email -
ClaimTypes.Gender -
ClaimTypes.GivenName -
ClaimTypes.HomePhone -
ClaimTypes.MobilePhone -
ClaimTypes.Name -
ClaimTypes.Role -
ClaimTypes.Surname -
ClaimTypes.WindowsAccountName
但是,这将取决于身份验证提供程序。实际上有更多的标准化声明,正如您从ClaimTypes类中看到的,但是没有任何东西阻止任何人添加他们自己的声明。请记住,一般来说,索赔并不意味着什么,但也有一些例外:Name和Role可用于安全检查,我们稍后将看到。
因此,Claim类具有以下主要属性:
Issuer``string:索赔人Type(string):权利要求的类型通常是ClaimTypes中的一种,但也可能是其他类型Value(string:索赔的价值
让我们从讨论 Windows 身份验证开始讨论身份验证。
Windows 身份验证
ASP.NET Core 不受平台影响,因此不支持 Windows 身份验证。如果我们确实需要的话,可能实现这一点的最佳方法是使用Internet Information Server(IIS)/IIS Express 作为反向代理,处理所有请求并将它们定向到 ASP.NET Core。
对于 IIS Express,我们需要在项目的Properties\launchSettings.json文件中配置启动设置如下,更改为粗体:
"iisSettings": {
"windowsAuthentication": true, "anonymousAuthentication": false, "iisExpress": {
"applicationUrl": "http://localhost:5000/",
"sslPort": 0
}
}
对于 IIS,我们需要确保我们的网站启用了AspNetCoreModule。
在任何情况下,我们都需要在ConfigureServices方法中配置 Windows 身份验证,如下所示:
services.AddAuthentication(IISDefaults.AuthenticationScheme);
最后,AspNetCoreModule使用了 ASP.NET Core 本身不需要或不使用的Web.config文件;它用于部署,包括以下内容:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<aspNetCore forwardWindowsAuthToken="true"
processPath="%LAUNCHER_PATH%"
arguments="%LAUNCHER_ARGS%" />
<handlers>
<add name="aspNetCore" path="*" verb="*"
modules="AspNetCoreModule"
resourceType="Unspecified" />
</handlers>
</system.webServer>
</configuration>
就这样。[Authorize]属性将要求经过身份验证的用户,并且将对 Windows 身份验证感到满意。HttpContext.User将被设置为WindowsPrincipal的一个实例,ClaimsPrincipal的一个子集,任何 Windows 组都可以作为角色和声明使用(ClaimTypes.Role。Windows 名称将以domain\user的形式设置在ClaimsIdentity.Name中。
在要获取当前 Windows 身份验证的任何位置,都可以使用以下代码:
var identity = WindowsIdentity.GetCurrent();
此外,例如,如果您想知道当前用户是否属于特定角色,例如内置管理员,则可以使用以下代码:
var principal = new WindowsPrincipal(identity);
var isAdmin = principal.IsInRole(WindowsBuiltInRole.Administrator);
如果当前用户是 Windows 内置管理员组的一部分,则此代码将返回true。
Don't forget that, although this code will compile on any platform, you can only use Windows authentication on Windows. You can check that by using System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows).
接下来,让我们看看如何为所有非 Windows 开发人员烘焙我们自己的身份验证机制。
自定义身份验证
ASP.NET Core 不包括任何身份验证提供程序,这与以前版本的 ASP.NET 不同,ASP.NET 支持 Windows 和基于结构化查询语言(SQL)的身份验证提供程序。这意味着我们必须手动或不完全地实现所有内容,稍后我们将看到这一点。
注册服务的方式为AddAuthentication,后面可以跟AddCookie,如下代码所示:
services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options
=>
{
options.LoginPath = "/Account/Login/";
options.AccessDeniedPath = "/Account/Forbidden/";
options.LogoutPath = "/Account/Logout";
options.ReturnUrlParameter = "ReturnUrl";
});
我们在Configure中增加UseAuthentication方法,如下:
app.UseAuthentication();
AccountController的变化很小,我们必须调用HttpContext实例上的SignInAsync和SignOutAsync扩展方法,而不是调用HttpContext.Authorization中的旧版本,如下代码块所示:
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> PerformLogin(string username, string password, string returnUrl,
bool isPersistent)
{
//...check validity of credentials
await this.HttpContext.SignInAsync(CookieAuthenticationDefaults.
AuthenticationScheme, new ClaimsPrincipal(user), new
AuthenticationProperties { IsPersistent = isPersistent });
return this.LocalRedirect(returnUrl);
}
[HttpGet]
public async Task<IActionResult> Logout()
{
await this.HttpContext.SignOutAsync(CookieAuthenticationDefaults
.AuthenticationScheme);
//...
}
在使用这些新方法之前,为Microsoft.AspNetCore.Authentication名称空间添加一个using语句。
最小登录页面(Views/Account/Login可能如下所示:
using (Html.BeginForm(nameof(AccountController.PerformLogin), "Account", FormMethod.Post))
{
<fieldset>
<p>Username:</p>
<p><input type="text" name="username" /></p>
<p>Password:</p>
<p><input type="password" name="password" /></p>
<p>Remember me: <input type="checkbox" name="isPersistent"
value="true" /></p>
<input type="hidden" name="ReturnUrl" value="@Context.Request.
Query["ReturnUrl"]"/>
<button>Login</button>
</fieldset>
}
与实现我们自己的身份验证机制不同,使用现有的和完善的身份验证机制通常更方便,这正是我们接下来要讨论的。
身份
因为您不必自己处理低级身份验证,所以有许多包可以帮助您完成这项任务。微软推荐的是微软身份(http://github.com/aspnet/identity 。
Identity 是一个可扩展的库,用于进行用户名密码身份验证和存储用户属性。它是模块化的,默认情况下,它使用实体框架(EF核心进行数据存储持久化。当然,因为 EF 本身是非常可扩展的,所以它可以使用它的任何数据提供程序(SQLServer、SQLite、Redis 等等)。用于 Identity with EF Core 的 NuGet 软件包有Microsoft.AspNetCore.Identity.EntityFrameworkCore、Microsoft.EntityFrameworkCore.Tools和Microsoft.AspNetCore.Diagnostics.EntityFramework,如果我们选择通过个人用户帐户进行身份验证,您还应该知道,默认情况下,Identity 是通过 Visual Studio 模板为ASP.NET Core Web 应用安装的。以下屏幕截图显示了 Visual Studio 屏幕,我们可以在其中选择身份验证方法:

标识同时支持用户属性和角色。为了使用 Identity,我们首先需要注册其服务,如下所示:
services
.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(this.Configuration.
GetConnectionString("DefaultConnection")))
.AddDefaultIdentity<IdentityUser>(options => options.SignIn.
RequireConfirmedAccount = false)
.AddEntityFrameworkStores<ApplicationDbContext>();
无论如何,请将配置中的连接字符串键(Data:DefaultConnection:ConnectionString)替换为最适合您的键,并确保它指向有效的配置值。
它将是这样的:
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;
Database=aspnet-chapter07-2AF3F755-0DFD-4E20-BBA4-9B9C3F56378B;
Trusted_Connection=True;MultipleActiveResultSets=true"
},
当涉及到安全性时,Identity 支持大量选项;这些可以在调用AddDefaultIdentity时配置,如下所示:
services.AddDefaultIdentity<IdentityUser>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequiredUniqueChars = 0;
options.Password.RequiredLength = 0;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
});
此示例为登录设置了许多选项,例如禁用电子邮件确认、简化密码要求以及设置超时和失败登录尝试次数。我不会详细介绍所有可用的选项;请参考身份网站了解全貌:https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity
如果需要更改路径和 cookie 选项,则需要使用ConfigureApplicationCookie,如下例:
services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(20);
options.SlidingExpiration = true;
options.LoginPath = "/Account/Login";
options.AccessDeniedPath = "/Account/Forbidden";
options.LogoutPath = "/Account/Logout";
options.ReturnUrlParameter = "ReturnUrl";
});
此简单示例将路径设置为与前面在“自定义身份验证”主题中提供的路径相同,并设置一些 cookie 属性,如下所示:
HttpOnly:要求发送 cookie 时设置HttpOnly标志(参见https://owasp.org/www-community/HttpOnlyExpireTimeSpan:认证 cookie 的持续时间SlidingExpiration:将 cookie 到期时间设置为滑动,即每次访问应用时,cookie 到期时间将被更新相等的时间
身份注册码(本小节中列出的第一个代码)提到了ApplicationDbContext和IdentityUser类。当我们使用使用使用自定义身份验证的 Visual Studio 模板创建项目时,会自动添加这些类的框架,但我在此处添加它们以供参考,如下所示:
public class ApplicationDbContext : IdentityDbContext
{
public ApplicationDbContext(DbContextOptions options) : base(options) { }
}
现在,这非常重要,您需要在使用 Identity 之前创建数据库。为此,打开Package Manager Console并运行以下命令:
Add-Migration "Initial"
Update-Database
在此之后,我们可以向模型添加一些附加属性。
添加自定义属性
正如你所看到的,这里没有什么特别的东西。唯一值得一提的是,您可以将自己的自定义属性添加到IdentityUser和IdentityRole类中,这些属性将作为登录过程的一部分进行持久化和检索。你为什么要这么做?因为这些基类不包含任何有用的属性,所以只包含用户名、电子邮件和电话(对于用户)以及角色名。这些类分别映射一个用户和一个角色,其中一个用户可以有一个角色,而每个角色可以有多个与其关联的用户。您只需要创建新类并让上下文使用它们,如以下代码块所示:
public class ApplicationUser : IdentityUser
{
public ApplicationUser() {}
public ApplicationUser(string userName) : base(userName) {}
//add other properties here, with public getters and setters
[PersonalData]
[MaxLength(50)]
public string FullName { get; set; }
[PersonalData]
public DateTime? Birthday { get; set; }
}
public class ApplicationRole : IdentityRole
{
public ApplicationRole() {}
public ApplicationRole(string roleName) : base(roleName) {}
//add other properties here, with public getters and setters
}
请注意[PersonalData]属性,该属性用于标记正在添加的新属性:这是一个要求,因此它可以自动下载和删除。这是GDPR的要求,将在本章后面讨论。如果你不在乎它,你可以忽略它。
You can add validation attributes to this model.
您还需要修改上下文以使用新属性,如下所示:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string>
{
public ApplicationDbContext(DbContextOptions options) : base(options) {}
}
名称ApplicationUser和ApplicationRole是身份和角色数据自定义类的典型名称。请注意ApplicationDbContext的三个通用参数:标识用户和角色的类型,以及主键的类型,即string。
您还必须更改Startup类中的注册码以引用新的 Identity user 类,如下所示:
services
.AddDefaultIdentity<ApplicationUser>(options =>
{
//...
});
最后,我们必须在 Visual Studio(Package Manager Console中创建迁移并更新数据库以反映更改,如下所示:
Add-Migration "PersonalData"
Update-Database
或者,我们可以从命令行执行此操作,如下所示:
dotnet ef migrations add PersonalData
dotnet ef database update
当然,如果我们有自定义数据,我们还需要更新注册表,使其包含新属性。
更新用户界面
幸运的是,ASP.NET Core 标识完全支持这一点:可以提供全部或部分表单,它们将替换提供的表单!
右键单击 web 项目并选择“新建脚手架项目…”,如以下屏幕截图所示:

然后,在它之后,选择 Identity,如以下屏幕截图所示:

然后,我们可以选择在当前项目中覆盖哪些页面,如以下屏幕截图所示:

请注意,您必须选择要使用的上下文(DbContext-派生类)。默认情况下,将在新文件夹Areas/Identity下创建文件,该文件夹将对应于模型视图控制器(MVC区域。这些页面本身就是剃刀页面,这意味着它们不使用控制器,但它们使用代码隐藏文件(一个.cshtml和一个.cshtml.cs文件)。
因此,如果您按照我的示例,将FullName和Birthday属性添加到ApplicationUser类中,并为账户注册生成页面,我们需要将它们添加到Areas/Identity/Pages/Account/Manage/Register.cshtml文件中(粗体更改),如下所示:
...
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.FullName"></label>
<input asp-for="Input.FullName" class="form-control" />
<span asp-validation-for="Input.FullName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Birthday"></label>
<input type="date" asp-for="Input.Birthday" class="form-control" />
<span asp-validation-for="Input.Birthday" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Email"></label>
<input asp-for="Input.Email" class="form-control" />
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
...
在Register.cshtml.cs中,我们需要添加代码来持久化数据,如下所示:
...
[BindProperty]
public InputModel Input { get; set; }
public class InputModel
{
[Display(Name = "Full Name")]
[DataType(DataType.Text)]
[MaxLength(50)]
public string FullName { get; set; }
[Display(Name = "Birthday")] [DataType(DataType.Date)]
public DateTime? Birthday { get; set; }
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }
...
}
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = Input.Email,
Email = Input.Email,
Birthday = Input.Birthday, FullName = Input.FullName };
var result = await _userManager.CreateAsync(user, Input.Password);
...
}
...
本质上,我们只是将新属性添加到InputModel,它只是一个普通的旧 CLR 对象(POCO)类,用于绑定表单数据,并从那里添加到ApplicationUser类,然后传递到CreateAsync方法。
使用身份提供程序
现在,继续上一个身份验证示例,让我们看看它与标识的关系:
public class AccountController : Controller
{
private readonly IOptions<IdentityOptions> _options;
private readonly UserManager<ApplicationUser> _userManager;
private readonly RoleManager<ApplicationRole> _roleManager;
private readonly SignInManager<ApplicationUser> _signInManager;
public AccountController(
IOptions<IdentityOptions> options,
UserManager<ApplicationUser> userManager,
RoleManager<ApplicationRole> roleManager,
SignInManager<ApplicationUser> signInManager)
{
this._options = options;
this._signInManager = signInManager;
this._userManager = userManager;
this._roleManager = roleManager;
}
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> PerformLogin(string username,
string password, string returnUrl)
{
var result = await this._signInManager.PasswordSignInAsync
(username, password,
isPersistent: true,
lockoutOnFailure: false);
if (result.Succeeded)
{
return this.LocalRedirect(returnUrl);
}
else if (result.IsLockedOut)
{
this.ModelState.AddModelError("User", "User is locked out");
return this.View("Login");
}
return this.Redirect(this._options.Value.Cookies.
ApplicationCookie.AccessDeniedPath);
}
[HttpGet]
public async Task<IActionResult> Logout()
{
await this._signInManager.SignOutAsync();
return this.RedirectToRoute("Default");
}
private async Task<ApplicationUser> GetCurrentUserAsync()
{
//the current user properties
return await this._userManager.GetUserAsync
(this.HttpContext.User);
}
private async Task<ApplicationRole> GetUserRoleAsync(string id)
{
//the role for the given user
return await this._roleManager.FindByIdAsync(id);
}
}
用于管理身份验证过程的类有UserManager<T>、SignInManager<T>和RoleManager<T>,这些类都是泛型的,以具体身份用户或身份角色类作为参数。这些类通过对AddDefaultIdentity的调用注册到依赖项注入(DI框架中,因此可以在任何需要它们的地方注入。对于记录,调用AddDefaultIdentity与添加以下服务相同:
services
.AddIdentity() //adds core functionality
.AddDefaultUI() //adds self-contained Razor Pages UI in
// an area called /Identity
.AddDefaultTokenProviders(); //for generating tokens for new
// passwords, resetting operations
我们调用UserManager<T>类的以下三种方法:
PasswordSignInAsync:实际验证用户名和密码,返回用户状态的方法;可选地,它将 cookie 设置为持久性(isPersistent),这意味着用户将在一段时间内保持身份验证,如配置设置中所指定,并且还指示在多次尝试失败的情况下是否锁定用户(lockoutOnFailure)——同样,可配置。SignOutAsync:通过设置身份验证 cookie 的过期时间来注销当前用户RefreshSignInAsync:通过延长认证 cookie 的到期时间来刷新认证 cookie(此处未显示)
UserManager<T>类公开了一些有用的方法,如下所示:
-
GetUserAsync:检索当前用户的数据(或者IdentityUser或者子类) -
CreateAsync:创建用户(此处未显示) -
UpdateAsync:更新用户(此处未显示) -
DeleteAsync:删除用户(此处未显示) -
AddClaimAsync/RemoveClaimAsync:向用户添加/删除索赔(此处未显示) -
AddToRoleAsync/RemoveFromRoleAsync:在角色中添加/删除用户(此处未显示) -
ConfirmEmailAsync:为最近创建的用户确认电子邮件(此处未显示) -
FindByEmailAsync/FindByIdAsync/FindByNameAsync:尝试通过电子邮件/ID/姓名查找用户(此处未显示)
对于RoleManager<T>,它在这里的唯一用途是通过FindByIdAsync方法(此处未显示)检索当前用户的角色(IdentityRole-派生)。
正如您所看到的,该代码与前面的代码非常相似,但这只是一个玩笑,因为 Identity 支持许多其他功能,包括以下功能:
- 用户注册,包括电子邮件激活码
- 为用户分配角色
- 多次登录尝试失败后帐户锁定
- 双因素认证
- 密码检索
- 外部身份验证提供程序
有关更多信息,请咨询身份网站:https://www.asp.net/identity
现在,让我们来看一个非常流行的服务器,用于集成数据源并向多个客户端提供身份验证请求。
使用 IdentityServer
IdentityServer是针对 ASP.NET 的OpenID Connect和OAuth 2.0协议的开源实现。我们感兴趣的版本IdentityServer4是专门为 ASP.NET Core 设计的;其源代码可在上获得 https://github.com/IdentityServer/IdentityServer4 及其文件位于http://docs.identityserver.io/ 。它非常流行,事实上,它是微软推荐的服务联合和单点登录(SSO的实现。
这是用于授予对资源访问权限的 OAuth 2.0 流程:

Image taken from https://docs.microsoft.com/en-us/aspnet/web-api/overview/security/external-authentication-services
粗略地说,IdentityServer 可以作为服务用于身份验证,这意味着它可以接受身份验证请求,根据任意数量的数据存储验证请求,并授予访问令牌。
我们将不深入讨论设置 IdentityServer 的细节,因为它可能非常复杂,并且具有大量的功能。我们感兴趣的是如何使用它对用户进行身份验证。为此,我们需要Microsoft.AspNetCore.Authentication.OpenIdConnect和IdentityServer4.AccessTokenValidationNuGet 包。
我们在ConfigureServices方法中设置了所有配置,如下代码块所示:
services.AddCookieAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);
services.AddOpenIdConnectAuthentication(options =>
{
options.ClientId = "MasteringAspNetCore";
//change the IdentityServer4 URL
options.Authority = "https://servername:5000";
//uncomment the next line if not using HTTPS
//options.RequireHttpsMetadata = false;
});
然后,在Configure中添加认证中间件,如下所示:
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
app.UseAuthentication();
这两行将首先删除JSON Web 令牌(JWT的声明映射,然后添加认证中间件。
For additional information, consult the wiki article at https://social.technet.microsoft.com/wiki/contents/articles/37169.secure-your-netcore-web-applications-using-identityserver-4.aspx and the IdentityServer Identity documentation at http://docs.identityserver.io/en/release/quickstarts/6_aspnet_identity.html.
以下各节介绍针对第三方提供商的身份验证。
使用 Azure Active Directory
随着一切都转移到云上,ASP.NET Core 也支持使用Azure Active Directory(Azure AD进行身份验证也就不足为奇了。创建新项目时,您可以选择工作或学校帐户进行身份验证,然后输入 Azure 云的详细信息,如以下屏幕截图所示:

You must enter a valid domain!
实际上,向导将以下两个 NuGet 包添加到项目Microsoft.AspNetCore.Authentication.Cookies和Microsoft.AspNetCore.Authentication.OpenIdConnect(Azure 身份验证基于 OpenID)。它还将以下条目添加到appsettings.json配置文件中:
"Authentication": {
"AzureAd": {
"AADInstance": "https://login.microsoftonline.com/",
"CallbackPath": "/signin-oidc",
"ClientId": "<client id>",
"Domain": "mydomain.com",
"TenantId": "<tenant id>"
}
}
身份验证使用 cookies,因此在ConfigureServices方法中添加了类似的条目,如以下代码片段所示:
services.AddAuthentication(options =>
options.SignInScheme = CookieAuthenticationDefaults
.AuthenticationScheme
);
最后,OpenID 中间件被添加到Configure中的管道中,如下面的代码片段所示:
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = this.Configuration["Authentication:AzureAd:ClientId"],
Authority = this.Configuration["Authentication:AzureAd:AADInstance"] +
this.Configuration["Authentication:AzureAd:TenantId"],
CallbackPath = this.Configuration["Authentication:AzureAd:
CallbackPath"]
});
在AccountController类中登录(SignIn)、注销(Logout)和显示注销页面(SignedOut)的相关方法(来自本章开头的原始列表)如下代码块所示:
[HttpGet]
public async Task<IActionResult> Logout()
{
var callbackUrl = this.Url.Action("SignedOut", "Account",
values: null,
protocol: this.Request.Scheme);
return this.SignOut(new AuthenticationProperties {
RedirectUri = callbackUrl },
CookieAuthenticationDefaults.AuthenticationScheme,
OpenIdConnectDefaults.AuthenticationScheme);
}
[HttpGet]
public IActionResult SignedOut()
{
return this.View();
}
[HttpGet]
public IActionResult SignIn()
{
return this.Challenge(new AuthenticationProperties { RedirectUri = "/" },
OpenIdConnectDefaults.AuthenticationScheme);
});
}
现在,我们将了解如何使用知名社交网络应用作为应用的身份验证提供商。
使用社交登录
另一种自行保存和维护用户凭据的方法是使用第三方的身份验证信息,如社交网络应用。这是一个有趣的选项,因为您不需要用户完成帐户创建过程;您只需为此信任外部身份验证提供程序。
所有外部身份验证提供程序都遵循以下流程:

Image taken from https://docs.microsoft.com/en-us/aspnet/web-api/overview/security/external-authentication-services For more information, please consult https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/
该机制基于提供者,微软提供了许多提供者;您必须知道所有这些都依赖于标识,因此您需要首先配置它(UseIdentity。创建项目时,请确保选择使用身份验证并选择个人帐户。这将确保使用正确的模板,并且项目中存在所需的文件。让我们在接下来的部分中学习一些。
脸谱网
Facebook其实并不需要介绍。其提供商可作为Microsoft.AspNetCore.Authentication.FacebookNuGet 包提供。您需要先在 Facebook 上创建一个开发者帐户,然后在Configure方法中注册提供商时使用应用 ID 和用户密码,如下所示:
app.UseFacebookAuthentication(new FacebookOptions()
{
AppId = Configuration["Authentication:Facebook:AppId"],
AppSecret = Configuration["Authentication:Facebook:AppSecret"]
});
Facebook login details are available here: https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins
啁啾
推特是另一个流行的社交网站,其提供商可作为Microsoft.AspNetCore.Authentication.TwitterNuGet 软件包提供。您还需要在 Twitter 开发者网站上注册您的应用。其配置如下所示:
app.UseTwitterAuthentication(new TwitterOptions()
{
ConsumerKey = Configuration["Authentication:Twitter:ConsumerKey"],
ConsumerSecret = Configuration["Authentication:Twitter:
ConsumerSecret"]
});
Twitter login details are available here: https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/twitter-logins.
谷歌
谷歌提供商包含在Microsoft.AspNetCore.Authentication.GoogleNuGet 包中。同样,您需要创建一个开发者帐户,并事先注册您的应用。Google 提供程序的配置如下:
app.UseGoogleAuthentication(new GoogleOptions()
{
ClientId = Configuration["Authentication:Google:ClientId"],
ClientSecret = Configuration["Authentication:Google:ClientSecret"]
});
For more information about the Google provider, please consult https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/google-logins.
微软
当然,微软为自己的认证服务提供了一个提供商;这包含在Microsoft.AspNetCore.Authentication.MicrosoftAccountNuGet 包中,配置如下:
app.UseMicrosoftAccountAuthentication(new MicrosoftAccountOptions()
{
ClientId = Configuration["Authentication:Microsoft:ClientId"],
ClientSecret = Configuration["Authentication:Microsoft:ClientSecret"]
});
Go to https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/microsoft-logins for more information.
所有这些机制都依赖于 cookie 来持久化身份验证,因此我们有必要稍微讨论一下 cookie 安全性。
Cookie 安全性
CookieAuthenticationOptions类有几个属性可用于配置额外的安全性,如下所示:
Cookie.HttpOnly(bool:cookie 是否应该是 HTTP 唯一(参见)https://www.owasp.org/index.php/HttpOnly ;默认值为false。如果未设置,则不发送HttpOnly标志。Cookie.Secure(CookieSecurePolicy:cookie 是否应该只通过 HTTPS(Always)发送,始终(None),还是根据请求(SameAsRequest)发送,这是默认设置;如果未设置,则不发送Secure标志。Cookie.Path(string:cookie 应用的可选路径;如果未设置,则默认为当前应用路径。Cookie.Domain(string:cookie 的可选域;如果未设置,将使用站点的域。DataProtectionProvider(IDataProtectionProvider:可选的数据保护提供者,用于对 cookie 值进行加密解密;默认为null。CookieManager(ICookieManager):可选的 cookie 存储区;例如,在应用之间共享 cookie 可能很有用(请参见https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/compatibility/cookie-sharing )。IsEssential(bool:就 GDPR 而言,cookie 是否重要(ss)。ClaimsIssuer(string:谁发布了饼干的声明。ExpireTimeSpan(TimeSpan:认证 cookie 的有效性。SlidingExpiration(bool):是否在每次请求时更新ExpireTimeSpan中指定的 cookie 的有效性(默认)。AccessDeniedPath(string:质询阶段后,如果验证失败,浏览器将重定向到的路径。LoginPath(string:如果需要验证,浏览器将重定向到的登录路径(质询阶段)。LogoutPath(string):注销路径,其中验证 cookie(以及其他内容)被清除。ReturnUrlParameter(string):查询字符串参数,质询阶段保留原统一资源定位器(URL);默认为ReturnURL。
滑动过期意味着每次服务器接收到请求时,在过期中指定的时间段都将延长:返回具有相同名称的 cookie,其过期时间与覆盖前一个 cookie 的相同。
所有这些属性都在 Identity 中可用。为了设置值,您可以在ConfigureServices中调用AddAuthentication后,构建CookieAuthenticationOptions实例或使用AddCookie扩展方法中可用的委托,如下所示:
services
.AddAuthentication()
.AddCookie(options =>
{
//set global properties
options.LoginPath = "/Account/Login";
options.AccessDeniedPath = "/Account/Forbidden";
options.LogoutPath = "/Account/Logout";
options.ReturnUrlParameter = "ReturnUrl";
});
The HTTP cookie specification is available at https://tools.ietf.org/html/rfc6265.
支持 SameSite cookies
SameSite是征求意见(RFC)6265 的扩展,称为 RFC 6265bis,定义 HTTP cookies,其目的是缓解跨站点请求伪造(CSRF)通过选择性地仅允许从同一站点上下文设置 cookie 进行攻击。例如,假设您的站点位于www.abc.com;那么,dev.abc.com也被认为是同一站点,而xpto.com被认为是跨站点。
SameSite 与其他 cookie 参数一起由浏览器发送,它有三个选项,如下所示:
Strict:只有当 cookie 的站点与当前在浏览器上查看的站点匹配时,才会发送 cookie。Lax:仅当浏览器 URL 中的域与 cookie 的域匹配时,才会设置 cookie。None:必须通过 HTTPS 发送
对于 Edge、FireFox 和 Chrome,默认值现在为Lax,这意味着第三方 cookie 现在被阻止。
SameSite 安全性可以在CookieOptions类上设置,这意味着当我们显式设置 cookie 时,它可以一起设置,或者当使用基于 cookie 的身份验证机制时,可以在CookieAuthenticationOptions上可用的 cookie 生成器上设置,如下代码所示:
services
.AddAuthentication()
.AddCookie(options =>
{
options.Cookie.SameSite = SameSiteMode.Strict;
});
我们可以传递给SameSite的可能值如下:
Lax:客户端浏览器应发送具有相同站点和跨站点顶级请求的 cookie。None:未对客户进行相同的现场验证。Strict:客户端浏览器只发送具有相同站点请求的 cookie。Unspecified:默认设置,由客户端浏览器指定。
在添加身份验证之前,我们不能忘记将 cookie 中间件添加到管道中,如下所示:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseCookiePolicy();
app.UseAuthentication();
//rest goes here
}
现在我们已经讨论了身份验证,也就是说,构建一个定制的身份验证提供者,并使用IdentityServer或社交网站进行身份验证和一些 cookie 安全性,让我们谈谈授权。
授权请求
在这里,我们将看到如何控制对应用部分的访问,无论是控制器还是更细粒度的应用。
因此,假设您想要将整个控制器或特定操作标记为需要身份验证。最简单的方法是向控制器类添加一个[Authorize]属性,就像这样。如果您试图访问受保护的控制器或资源,将返回一个401 authorization Required错误。
为了增加授权支持,我们必须在UseAuthentication调用之后向Configure方法添加所需的中间件,如下所示:
app.UseRouting();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
//...
});
不要忘记这个订单-UseAuthorization之前的UseAuthentication-这是强制性的!
以下各节描述了为 web 应用上的资源声明授权规则的不同方法。让我们从角色开始,这是定义用户组的常用方法。
基于角色的授权
如果我们想在授权过程中更进一步,我们可以请求仅当经过身份验证的用户处于给定角色时才能访问受保护的资源控制器或操作。角色只是声明,受任何身份验证机制的支持,如下所示:
[Authorize(Roles = "Admin")]
public IActionResult Admin() { ... }
可以指定多个角色,用逗号分隔。在以下情况下,如果当前用户至少担任其中一个角色,则将授予访问权限:
[Authorize(Roles = "Admin,Supervisor")]
如果您想通过代码知道当前用户是否属于特定角色,可以使用ClaimsPrincipal实例的IsInRole方法,如下面的代码片段所示:
var isAdmin = this.HttpContext.User.IsInRole("Admin");
如果当前用户是Admin组的成员,则返回true。
定义授权的另一种方法是通过策略,策略允许对权限进行更细粒度的控制。现在让我们看看这是怎么回事。
基于策略的授权
策略是一种更灵活的授权方式;在这里,我们可以使用我们想要的任何规则,而不仅仅是属于某个角色或正在验证的规则。
要使用策略,我们需要使用[Authorize]属性和Policy属性来修饰要保护的资源(控制器、操作),如下面的代码片段所示:
[Authorize(Policy = "EmployeeOnly")]
策略是通过AddAuthorization方法在AuthorizationOptions类中配置的,如下面的代码片段所示
services.AddAuthorization(options =>
{
options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim
("EmployeeNumber"));
});
这段代码要求当前用户具有特定的声明,但我们可以考虑其他示例,例如仅允许本地请求。RequireAssertion允许我们指定任意条件,如下代码块所示:
options.AddPolicy("LocalOnly", builder =>
{
builder.RequireAssertion(ctx =>
{
var success = false;
if (ctx.Resource is AuthorizationFilterContext mvcContext)
{
success = IPAddress.IsLoopback(mvcContext.HttpContext.
Connection.RemoteIpAddress);
}
return success;
});
});
注意,在这里,我们假设Resource属性是AuthorizationFilterContext。记住,只有在[Authorize]过滤器的上下文中,这才是真的;否则,情况就不会如此。
您还可以将策略用于特定声明(RequireClaim)或角色(RequireRole),用于身份验证(RequireAuthenticatedUser),甚至用于拥有特定用户名(RequireUserName),甚至可以将所有这些策略组合在一起,如下所示:
options.AddPolicy("Complex", builder =>
{
//a specific username
builder.RequireUserName("admin");
//being authenticated
builder.RequireAuthenticatedUser();
//a claim (Claim) with any one of three options (A, B or C)
builder.RequireClaim("Claim", "A", "B", "C");
//any of of two roles
builder.RequireRole("Admin", "Supervisor");
});
天空是您可以使用任何逻辑授予访问权限的限制。Resource属性原型为object,表示可以接受任何值;如果作为 MVC 授权过滤器的一部分调用,它将始终是AuthorizationFilterContext的一个实例。
现在我们来看一种将这些策略封装在可重用类中的方法。
授权处理程序
授权处理程序是在类中封装业务验证的一种方法。有一个由以下内容组成的授权 API:
IAuthorizationService:所有授权检查的入口点IAuthorizationHandler:授权规则的实现IAuthorizationRequirement:单一授权需求的合同,传递给授权处理人AuthorizationHandler<TRequirement>:绑定到特定IAuthorizationRequirement的IAuthorizationHandler抽象基实现
我们实现了一个IAuthorizationHandler(可能是AuthorizationHandler<TRequirement>的子类),并在其中定义了我们的规则,如下所示:
public sealed class DayOfWeekAuthorizationHandler : AuthorizationHandler<DayOfWeekRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
DayOfWeekRequirement requirement)
{
if ((context.Resource is DayOfWeek requestedRequirement) &&
(requestedRequirement == requirement.DayOfWeek))
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
return Task.CompletedTask;
}
}
public sealed class DayOfWeekRequirement : IAuthorizationRequirement
{
public DayOfWeekRequirement(DayOfWeek dow)
{
this.DayOfWeek = dow;
}
public DayOfWeek DayOfWeek { get; }
}
此处理程序响应DayOfWeekRequirement类型的需求。当一个这样的需求被传递到AuthorizeAsync方法时,它会自动绑定到它。
授权管道可以接受许多需求,为了使授权成功,所有需求也必须成功。这是一个非常简单的示例,我们需要一周中的某一天,授权处理程序要么成功,要么失败,这取决于一周中的当前日期是否符合给定的要求。
IAuthorizationService类在 DI 框架中注册;默认实例为DefaultAuthorizationService。我们将使用以下代码启动权限检查:
IAuthorizationService authSvc = ...;
if (await (authSvc.AuthorizeAsync(
user: this.User,
resource: DateTime.Today.DayOfWeek,
requirement: new DayOfWeekRequirement(DayOfWeek.Monday))).Succeeded)
) { ... }
授权处理程序还可以绑定到策略名称,如以下代码段所示:
services.AddAuthorization(options =>
{
options.AddPolicy("DayOfWeek", builder =>
{
builder.AddRequirements(new DayOfWeekRequirement
(DayOfWeek.Friday));
});
});
在这种情况下,前一个调用将改为以下调用:
if ((await (authSvc.AuthorizeAsync(
user: this.User,
resource: DateTime.Today.DayOfWeek,
policyName: "DayOfWeek"))).Succeeded)
) { ... }
这两个重载的参数如下所示:
user(ClaimsPrincipal:当前登录用户policyName(string:已注册的保单名称resource(object:将传递到授权管道的任何对象requirement(IAuthorizationRequirement:将传递给授权处理程序的一个或多个需求
如果我们想要覆盖默认授权处理程序,我们可以在ConfigureServices中非常轻松地完成,如下所示:
services.AddSingleton<IAuthorizationHandler, DayOfWeekAuthorizationHandler>();
这将注册一个自定义授权处理程序,我们需要对其执行自己的检查。在替换默认处理程序时要小心,因为这可能很棘手,而且很容易忘记某些东西!
现在,如果我们需要使用上下文进行更复杂的验证,我们需要将其注入到处理程序中。以下示例将允许访问仅来自本地主机的请求:
public sealed class LocalIpRequirement : IAuthorizationRequirement
{
public const string Name = "LocalIp";
}
public sealed class LocalIpHandler : AuthorizationHandler<LocalIpRequirement>
{
public LocalIpHandler(IHttpContextAccessor httpContextAccessor)
{
this.HttpContext = httpContextAccessor.HttpContext;
}
public HttpContext HttpContext { get; }
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
LocalIpRequirement requirement)
{
var success = IPAddress.IsLoopback(this.HttpContext.Connection
.RemoteIpAddress);
if (success)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
return Task.CompletedTask;
}
}
为此,我们需要执行以下操作:
- 注册
IHttpContextAccessor服务,如下:
services.AddHttpContextAccessor();
- 将
LocalIpHandler注册为作用域服务,如下所示:
services.AddScoped<IAuthorizationHandler, LocalIpHandler>();
- 当我们想检查当前请求是否与策略匹配时,我们会这样做:
var result = await authSvc.AuthorizeAsync(
user: this.User,
requirement: new LocalIpRequirement(),
policyName: LocalIpRequirement.Name
);
if (result.Succeeded) { ... }
我们应该很好。
现在,让我们来看一种查询定义为策略的当前权限的方法。
基于资源的授权
我们可以利用授权处理程序进行基于资源的授权。基本上,我们要求授权服务检查访问给定资源和策略的权限。我们调用IAuthorizationService实例的AuthorizeAsync方法之一,如下面的代码片段所示:
IAuthorizationService authSvc = ...;
if ((await authSvc.AuthorizeAsync(this.User, resource, "Policy")).Succeeded) { ... }
IAuthorizationService实例通常从 DI 框架获取。AuthorizeAsync方法采用以下参数:
user(ClaimsPrincipal:当前用户resource(object:用于检查对policyName的权限的资源policyName(string:检查resource权限的策略名称
可以在控制器和视图中调用此方法以检查细粒度权限。它将执行在策略名称下注册的AuthorizationPolicy并将资源传递给它,然后调用所有注册的授权处理程序。
细粒度授权检查的一个典型示例是请求对给定记录的编辑权限,例如,在视图中,如下所示:
@inject IAuthorizationService authSvc
@model Order
@{
var order = Model;
}
@if ((await (authSvc.AuthorizeAsync(User, order, "Order.Edit"))).Succeeded)
{
@Html.EditorForModel()
}
else
{
@Html.DisplayForModel()
}
这里,我们正在检查一个名为Order.Edit的策略,该策略需要一个Order类型的资源。它的所有需求都已运行,如果它们都成功,那么我们有权编辑订单;否则,我们只显示它。
如果我们需要允许任何用户访问受保护的资源控制器操作或 Razor 页面,该怎么办?
允许匿名访问
在使用访问控制时,如果出于任何原因,您希望允许访问特定控制器或控制器中的特定操作,则可以对其应用[AllowAnonymous]属性。这将绕过任何安全处理程序并执行操作。当然,在 action 或 view 中,您仍然可以通过检查HttpContext.User.Identity属性来执行显式安全检查。
授权是两个构建块之一,我们讨论了为 web 资源或命名策略定义规则的不同方式。在下一节中,我们将讨论安全性的其他方面,从请求伪造开始。
检查伪造请求
CSRF(或XSRF)攻击是最常见的黑客攻击之一,用户被诱骗在其登录的某个站点执行某些操作。例如,假设你刚刚访问了你的电子银行网站,然后你去了一个恶意网站,没有注销;恶意网站上的一些 JavaScript 可能会让浏览器向电子银行网站发布一条指令,将一定数量的资金转移到另一个帐户。意识到这是一个严重的问题,微软一直支持一个防伪软件包Microsoft.AspNetCore.Antiforgery,它实现了开放式 Web 应用安全项目(OWASP中描述的双提交 Cookie和加密令牌模式的混合备忘单:https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)(预防)(欺诈)单(CSRF)特定(防御)
OWASP旨在提供一个非盈利的网络安全最佳实践库。它列出了常见的安全问题,并解释了如何解决这些问题。
防伪框架执行以下操作:
- 在每个表单上生成一个带有防伪标记的隐藏字段(也可以是标题)
- 发送具有相同令牌的 cookie
- 回发时,检查它是否收到了作为有效负载一部分的防伪令牌,以及它是否与防伪 cookie 相同
BeginForm方法默认在生成<form>标记时输出防伪令牌,除非在antiforgery参数设置为false的情况下调用。
您需要通过调用AddAntiforgery来注册所需的服务,如以下代码片段所示:
services.AddAntiforgery(options =>
{
options.FormFieldName = "__RequestVerificationToken";
});
可能的选择如下:
-
CookieName(string:替换默认 cookie 的 cookie 名称;这是自动生成的,前缀为.AspNetCore.Antiforgery。 -
CookiePath(PathString?:限制 cookie 适用性的可选路径;默认值为null,这意味着不会随 cookie 发送任何路径设置 -
CookieDomain(string:限制(或增加)cookie 适用性的可选域;默认为null,不设置域设置 -
FormFieldName(string:存储防伪令牌的隐藏表单字段名称;默认为__RequestVerificationToken,为必填项 -
HeaderName(string:将存储令牌的头名称;默认值为RequestVerificationToken -
RequireSsl(bool:True如果防伪 cookie 仅使用 HTTPS 发送;默认为false -
SuppressXFrameOptionsHeader(bool):是否发送X-Frame-Options头;默认为false,表示发送SAMEORIGIN的值
防伪服务在IAntiforgery界面下注册。
有许多属性可用于控制默认行为,如下所示:
[ValidateAntiforgeryToken]:向特定控制器或操作添加防伪验证[IgnoreAntiforgeryToken]:禁用特定控制器或操作上的防伪验证(如果已全局启用)[AutoValidateAntiforgeryToken]:将防伪验证添加到任何不安全的请求中(POST、PUT、DELETE、PATCH)
所有这些都可以作为全局过滤器添加到属性旁边,如以下代码段所示:
services.AddMvc(options =>
{
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});
[ValidateAntiforgeryToken]与[AutoValidateAntiforgeryToken]的区别在于后者被设计成一个全局滤波器;没有必要在任何地方都明确地应用它。
Check out https://docs.microsoft.com/en-us/aspnet/core/security/anti-request-forgery for a more in-depth explanation of the anti-forgery options available.
如果您想将它与 AJAX 结合使用,同时保护这些类型的请求,该怎么办?首先,您需要从服务器获取一个令牌和要使用的头的名称,以便将其添加到每个 AJAX 请求中。您可以将令牌注入视图,然后将其添加到 AJAX 头中(例如,使用 jQuery),如以下代码块所示:
@using Microsoft.AspNetCore.Antiforgery
@inject IAntiforgery AntiForgery;
var headers = {};
headers['RequestVerificationToken'] = '@AntiForgery.GetAndStoreTokens
(HttpContext).RequestToken';
$.ajax({
type: 'POST',
url: url,
headers: headers}
)
.done(function(data) { ... });
在这里,我们无缝地将防伪令牌与每个 AJAX 请求一起发送,因此 ASP.NET Core 框架捕获了防伪令牌并对此感到满意。
接下来,我们将看到如何使用 HTML 编码防止脚本注入。
应用 HTML 编码
ASP.NET Core 中的视图引擎使用 HTML 编码器呈现 HTML,以防止脚本注入攻击。RazorPage类是所有 Razor 视图的基础,具有HtmlEncoder类型的HtmlEncoder属性。默认情况下,它作为DefaultHtmlEncoder 从 DI 获得,但您可以将其设置为不同的实例,尽管可能不需要它。我们要求使用@("...")Razor 语法对内容进行显式编码,如下所示:
@("<div>encoded string</div>")
这将呈现以下 HTML 编码字符串:
<div>encoded string</div>
您还可以使用IHtmHelper对象的Encode方法显式地执行此操作,如下所示:
@Html.Encode("<div>encoded string</div>")
最后,如果您有一个 helper 方法返回一个值IHtmlContent,它将使用注册的HtmlEncoder自动呈现。
If you want to learn more about script injection, please consult https://www.owasp.org/index.php/Code_Injection.
脚本注入保护就到此为止。现在,让我们转到 HTTPS。
使用 HTTPS
如今,HTTPS 的使用越来越普遍,不仅早期存在的性能损失现在已经消失,而且获得证书的成本也显著降低;在某些情况下,它甚至可能是免费的,例如,让我们加密(https://letsencrypt.org 提供此类证书。此外,谷歌(Google)等搜索引擎通过 HTTPS 为网站提供搜索结果。当然,ASP.NET Core 完全支持 HTTPS。现在,我们将了解如何添加证书,以便使用 HTTPS 为我们的站点提供服务,以及如何仅限制对 HTTPS 的访问。
让我们从证书开始。
证书
为了使用 HTTPS,我们需要一个浏览器接受为有效的有效证书。我们可以从根证书提供商处获取一个证书,也可以出于开发目的生成一个证书。这不会被识别为来自可信来源。
为了生成证书并将其安装到计算机的存储(在 Windows 和 macOS 上),我们运行以下代码:
dotnet dev-certs https --clean
dotnet dev-certs https --trust
如果需要,我们可以将证书文件导出到文件系统,如下所示:
dotnet dev-certs https --trust -ep .\certificate.pfx
请记住,该证书有以下两个用途:
- 加密通信
- 确保 web 服务器是可信的
dotnet工具生成的开发证书仅用于第一个目的。
获得证书后,我们现在必须使用它,这取决于我们的主机选择。下面将介绍这一点。
托管我们的应用
继续的方式取决于我们是直接连接到 ASP.NET Core 主机(如 Kestrel)还是通过反向代理(如 IIS Express)连接。IIS Express 是 IIS 的轻型版本,可以在本地运行以进行开发。它提供了成熟 IIS 的所有功能,但性能和可伸缩性并不完全相同。让我们看看什么是 IIS Express。
服务器
如果我们要使用 IIS Express,我们只需要将其设置配置为启用安全套接字层(SSL,如下所示:

红隼
另一方面,如果我们和红隼一起去,事情就有点不同了。首先,我们需要Microsoft.AspNetCore.Server.Kestrel.HttpsNuGet 包和一个证书文件。在引导代码中,它是隐式使用的。我们需要运行以下代码:
Host
.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(builder =>
{
builder.ConfigureKestrel(options =>
{
options.ListenAnyIP(443, listenOptions =>
{
listenOptions.UseHttps("certificate.pfx", "<password>");
});
});
builder.UseStartup<Startup>();
});
您将遵守以下规定:
- 证书从名为
certificate.pfx的文件加载,受密码<password>保护。 - 我们在端口
443上侦听任何本地 IP 地址。
如果我们只想更改默认端口和宿主服务器(Kestrel),而不想使用证书,那么可以通过代码轻松完成,如下所示:
builder.UseSetting("https_port", "4430");
这也可以通过ASPNETCORE_HTTPS_PORT环境变量实现。
HTTP.sys
对于HTTP.sys,我们需要Microsoft.AspNetCore.Server.HttpSys包,而不是ConfigureKestrel,我们称之为UseHttpSys,如下所示:
.UseHttpSys(options =>
{
options.UrlPrefixes.Add("https://*:443");
});
需要在 Windows 上为您希望提供服务的特定端口和主机头配置与HTTP.sys一起使用的证书。
在现代网络中,我们可能只想使用 HTTPS,所以让我们看看如何实施这一点。
强制 HTTPS
有时,我们可能要求所有呼叫都通过 HTTPS 进行,而所有其他请求都被拒绝。为此,我们可以使用全局过滤器RequireHttpsAttribute,如以下代码块所示:
services.Configure<MvcOptions>(options =>
{
options.SslPort = 443; //this is the default and can be omitted
options.Filters.Add(new RequireHttpsAttribute());
});
我们还需要告诉 MVC 我们在 HTTPS 中使用的端口,只是在我们使用非标准端口的情况下(443是标准端口)。
另一个选项是逐个控制器执行,如下所示:
[RequireHttps]
public class SecureController : Controller
{
}
或者,这可以一个接一个地发生,比如:
public class SecureController : Controller
{
[HttpPost]
[RequireHttps]
public IActionResult ReceiveSensitiveData(SensitiveData data) { ... }
}
Mind you, using [RequireHttps] in web APIs might not be a good idea—if your API client is not expecting it, it will fail and you may not know what the problem is.
如果我们有两个版本,HTTP 和 HTTPS,并且希望以静默方式引导客户机使用 HTTPS,该怎么办?
重定向到 HTTPS
ASP.NET Core 包括一个重定向中间件。它的功能与 ASP.NET IIS 重写模块类似(请参见https://www.iis.net/learn/extensions/url-rewrite-module )。它的描述超出了本章的范围,但足以解释如何强制从 HTTP 重定向到 HTTPS。请查看以下代码段:
var options = new RewriteOptions()
.AddRedirectToHttps();
app.UseRewriter(options);
Configure中的这段简单代码注册重定向中间件,并指示它将所有到 HTTP 的流量重定向到 HTTPS 协议。就这么简单,但它甚至可以更简单:由于 ASP.NET Core 的 2.1 版,我们只需要在Configure方法中调用UseHttpsRedirection,如下所示:
app.UseHttpsRedirection();
如果我们想指定其他信息,我们调用AddHttpsRedirection,并在ConfigureServices中添加选项,如下所示:
services.AddHttpsRedirection(options =>
{
options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect;
options.HttpsPort = 4430;
});
Again, redirecting to HTTPS with web APIs might not be a good idea because API clients may be configured to not follow redirects.
仍然在 HTTPS 方面,现在让我们研究另一种引导用户使用 HTTPS 的机制。
使用 HST
HSTS是一种网络安全策略机制,有助于保护网站免受协议降级攻击(HTTPS->HTTP)和 cookie 劫持。它允许 web 服务器声明 web 浏览器只能使用安全的 HTTPS 连接与之交互,而不能通过不安全的 HTTP 协议。浏览器会记住这个定义。
To learn more about HSTS, please consult https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security.
将 HSTS 添加到Configure方法中,如下所示:
app.UseHsts();
它向响应中添加一个标题,如下所示:
Strict-Transport-Security: max-age=31536000
如您所见,它有一个max-age参数。我们通过调用AddHsts在ConfigureServices中进行配置,如下所示:
services.AddHsts(options =>
{
options.MaxAge = TimeSpan.FromDays(7);
options.IncludeSubDomains = true;
options.ExcludedHosts.Add("test");
options.Preload = true;
});
HSTS 预载
如果站点在 HSTS 标头中发送preload指令,则视为请求包含在预加载列表中,并可通过上的表格提交 https://hstspreload.org 地点。
因此,在本节中,我们了解了如何使用 HTTPS,从构建证书到使用证书,以及强制从 HTTP 重定向到 HTTPS。现在,让我们转到安全的其他方面,从 CORS 开始。
理解 CORS
CORS本质上是从一个域从另一个域提供服务的页面请求资源的能力:例如,想想http://mysite.com的一个页面从http://javascriptdepository.com请求 JavaScript 文件。这是在所有大型门户网站中完成的,例如,包括访客跟踪或广告脚本。现代浏览器默认情况下不允许这样做,但可以逐个启用。
If you want to learn more about CORS, please consult https://developer.mozilla.org/en/docs/Web/HTTP/Access_control_CORS.
ASP.NET Core 支持 CORS 服务。您首先需要注册所需的服务(在ConfigureServices中),如下所示:
services.AddCors();
或者,一个稍微复杂一点的示例涉及定义策略,如下所示:
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", builder =>
builder
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
);
});
策略可以采用特定的 URL;不需要支持任何来源。请查看以下代码示例:
builder
.WithOrigins("http://mysite.com", "http://myothersite.com")
包含标题、方法和来源的更完整示例如下:
var policy = new CorsPolicy();
policy.Headers.Add("*");
policy.Methods.Add("*");
policy.Origins.Add("*");
policy.SupportsCredentials = true;
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", policy);
});
Headers、Methods和Origins集合包含所有应明确允许的值;向其添加*与调用AllowAnyHeader、AllowAnyMethod、AllowAnyOrigin相同。将SupportsCredentials设置为true意味着将返回Access-Control-Allow-Credentials头,这意味着应用允许从不同的域发送登录凭据。请注意此设置,因为它意味着不同域中的用户可以尝试登录到您的应用,甚至可能是恶意代码的结果。明智地使用这个。
然后,在Configure中添加 CORS 中间件,这将导致全局允许 CORS 请求。代码可以在以下代码段中看到:
app.UseCors(builder => builder.WithOrigins("http://mysite.com"));
或者,使用特定的策略执行此操作,例如:
app.UseCors("CorsPolicy");
注意,所有这些都需要Microsoft.AspNetCore.CorsNuGet 软件包。您可以使用WithOrigins方法添加任意数量的 URL,并且可以使用要授予访问权限的所有地址顺序调用它。您也可以将其限制为特定的头和方法,如下所示:
app.UseCors(builder =>
builder
.WithOrigins("http://mysite.com", "http://myothersite.com")
.WithMethods("GET")
);
需要记住的一点是,UseCors必须在UseMvc之前调用!
另一方面,如果希望逐个控制器或逐个操作在控制器上启用 CORS,则可以使用[EnableCors]属性,如以下代码段所示:
[EnableCors("CorsPolicy")]
public class HomeController : Controller { ... }
在这里,您需要指定策略名称,而不是单个 URL。同样,您可以通过应用[DisableCors]属性来禁用特定控制器或操作的 CORS。这一个没有策略名称;它只是完全禁用了 CORS。
现在来看看完全不同的东西。让我们研究 ASP.NET Core 可用于动态加密和解密数据的提供程序。
使用数据保护
ASP.NET Core 使用数据保护提供商来保护暴露给第三方的数据,如 cookie。IDataProtectionProvider接口定义了它的合约,ASP.NET Core 附带了一个在KeyRingBasedDataProtectorDI 框架中注册的默认实例,如下面的代码片段所示:
services.AddDataProtection();
cookie 的身份验证和 cookie 临时数据提供程序 API 使用数据保护提供程序。数据保护提供程序公开了一个方法CreateProtector,该方法用于检索保护器实例,然后可用于保护字符串,如以下代码段所示:
var protector = provider.CreateProtector("MasteringAspNetCore");
var input = "Hello, World";
var output = protector.Protect(input); //CfDJ8AAAAAAAAAAAAAAAAAAAAA...uGoxWLjGKtm1SkNACQ
您当然可以将其用于其他目的,但对于前面介绍的两个目的,您只需要在ConfigureServices方法中将提供程序实例传递给CookiesAuthenticationOptions实例,如下代码片段所示:
services.AddCookieAuthentication(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.DataProtectionProvider = instance;
});
CookieTempDataProvider类在其构造函数中已经接收到一个IDataProtectionProvider实例,因此当 DI 框架构建它时,它会传入已注册的实例。
如果您使用的是群集解决方案,并且希望以安全的方式在群集的不同计算机之间共享状态,那么数据保护提供程序非常有用。在这种情况下,您应该同时使用数据保护和分布式缓存提供程序(IDistributedCache实现),例如 Redis,您将在其中存储共享密钥。如果出于某种原因,您需要在没有分布式提供程序的情况下运行,则可以在本地存储共享密钥文件。事情是这样的:
services
.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("<location>"));
如果您愿意,您可以在配置文件上设置<location>,如下所示:
{
"DataProtectionSettings": {
"Location": "<location>"
}
}
这里,<location>是指数据文件存储的路径。
Data protection providers is a big topic and one that is outside the scope of this book. For more information, please consult https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/.
我们已经了解了如何保护任意数据,现在让我们看看如何保护静态文件。
保护静态文件
无法保护 ASP.NET Core 上的静态文件。然而,不用说,这并不意味着你做不到。基本上,您有以下两个选项:
- 将要提供服务的文件保存在
wwwroot文件夹之外,并使用控制器操作检索它们;此操作应强制执行您想要的任何安全机制 - 使用中间件组件检查对文件的访问,并有选择地限制对文件的访问
我们将在下一节中看到每个过程。
使用操作检索文件
因此,您需要使用操作方法来检索文件。可以用一个[Authorize]属性装饰这个动作方法,或者检查它内部的细粒度访问(IAuthorizationService.AuthorizeAsync。请查看以下代码:
private static readonly IContentTypeProvider _contentTypeProvider =
new FileExtensionContentTypeProvider();
[Authorize]
[HttpGet]
public IActionResult DownloadFile(string filename)
{
var path = Path.GetDirectoryName(filename);
//uncomment this if fine-grained access is not required
if (this._authSvc.AuthorizeAsync(this.User, path, "Download"))
{
_contentTypeProvider.TryGetContentType("filename",
out var contentType);
var realFilePath = Path.Combine("ProtectedPath", filename);
return this.File(realFilePath, contentType);
}
return this.Challenge();
}
这将只允许通过身份验证的用户发出GET请求,并检查下载策略以获取文件的路径。然后,将请求的文件与ProtectedPath组合,以获得真实的文件名。FileExtensionContentTypeProvider实例用于根据文件扩展名推断文件的内容类型。
使用中间件加强安全性
您从第 1 章、开始使用 ASP.NET Core了解 ASP.NET Core/开放式 Web 界面用于.NET(OWIN管道。其中的每个中间件组件都会影响其他组件,甚至会阻止它们的执行。此其他选项将拦截任何文件。让我们添加一个配置类和一个扩展方法,如下所示:
public class ProtectedPathOptions
{
public PathString Path { get; set; }
public string PolicyName { get; set; }
}
public static IApplicationBuilder UseProtectedPaths(
this IApplicationBuilder app, params ProtectedPathOptions [] options)
{
foreach (var option in options ??
Enumerable.Empty<ProtectedPathOptions>())
{
app.UseMiddleware<ProtectedPathsMiddleware>(option);
}
return app;
}
接下来,实际中间件组件的代码需要在管道的早期添加(Configure方法),如下所示:
public class ProtectedPathsMiddleware
{
private readonly RequestDelegate _next;
private readonly ProtectedPathOptions _options;
public ProtectedPathsMiddleware(RequestDelegate next,
ProtectedPathOptions options)
{
this._next = next;
this._options = options;
}
public async Task InvokeAsync(HttpContext context)
{
using (context.RequestServices.CreateScope())
{
var authSvc = context.RequestServices.GetRequiredService
<IAuthorizationService>();
if (context.Request.Path.StartsWithSegments
(this._options.Path))
{
var result = await authSvc.AuthorizeAsync(
context.User,
context.Request.Path,
this._options.PolicyName);
if (!result.Succeeded)
{
await context.ChallengeAsync();
return;
}
}
}
await this._next.Invoke(context);
}
}
该中间件检查所有注册的路径保护选项,并检查请求路径是否满足它们指定的策略。如果没有,它们将质询响应,从而影响到登录页面的重定向。
要激活它,您需要在Configure方法中将此中间件添加到管道中,如下所示:
app.UseProtectedPaths(new ProtectedPathOptions { Path = "/A/Path", PolicyName = "APolicy" });
If, by any chance, you need to lock down your app—meaning bring it offline—you can do so by adding an app_offline.htm file to the root of your app (not the wwwroot folder!). If this file exists, it will be served, and any other requests will be ignored. This is an easy way to temporarily disable access to your site, without actually changing anything.
我们已经了解了如何为静态文件应用授权策略。在下一节中,我们将看到 GDPR 是什么的解释。
了解 GDPR
欧盟(欧盟于 2018 年采用 GDPR。虽然这主要针对欧洲国家,但所有在那里可用的网站也应遵守这一规定。我将不讨论该法规的技术方面,但从本质上讲,它确保用户允许他人访问其个人数据,并可以自由撤销该访问权限,从而让他们在任何时候销毁这些信息。这可能在许多方面影响应用,甚至迫使采用特定的需求。至少,对于所有使用 cookie 跟踪个人信息的应用,它们都必须警告用户并征得用户的同意。
Read more about the GDPR here: https://gdpr-info.eu/
所需曲奇
从 3.x 版开始,默认的 ASP.NET Core 模板包括获得用户对使用 cookie 的批准的支持。用于提供诸如到期等 cookie 数据的CookieOptions类现在有了一个新属性IsEssential,这取决于应用的 cookie 策略,由其CookiePolicy实例的CheckConsentNeeded属性决定。这实际上是一个函数,如果它返回true但用户没有明确授予权限,则某些事情将无法工作:TempData和Sessioncookies 将无法工作。
通过ITrackingConsentFeature功能设置客户端 cookie(谁能说出?)来获得实际同意,如以下代码片段所示:
HttpContext.Features.Get<ITrackingConsentFeature>().GrantConsent();
或者,如果我们希望拒绝此同意,我们将运行以下代码:
HttpContext.Features.Get<ITrackingConsentFeature>().WithdrawConsent();
在任何时候,我们都可以通过运行以下代码来检查授权的当前状态:
var feature = HttpContext.Features.Get<ITrackingConsentFeature>();
var canTrack = feature.CanTrack;
var hasConsent = feature.HasConsent;
var isConsentNeeded = feature.IsConsentNeeded;
这些属性的含义如下:
CanTrack:是否已经同意或不需要同意HasConsent:是否同意IsConsentNeeded:申请是否要求 cookies 同意
配置应该在ConfigureServices方法中完成,如下面的代码片段所示:
services
.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = (context) => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
options.HttpOnly = HttpOnlyPolicy.Always;
options.Secure = CookieSecurePolicy.SameAsRequest;
});
如您所见,CheckConsentNeeded是一个委托,它将HttpContext实例作为其唯一参数,并返回一个布尔值;这样,您就可以根据具体情况决定要做什么。
MinimumSameSitePolicy、HttpOnly和Secure的行为与CookieOptions类中的行为完全相同,用于设置单个 Cookie 的选项。
配置完成后,我们需要通过向管道中添加中间件来实现这一点;在Configure方法中是这样的:
app.UseCookiePolicy();
个人资料
我们已经讨论过的另一件事是,在使用身份验证提供程序时,您应该使用[PersonalData]属性标记添加到用户模型中的任何个人属性。这是一个提示,如果用户要求,这些属性将需要提供给用户,同样,如果用户要求,这些属性将与其他用户数据一起删除。
请记住,GDPR 是欧洲的一项要求,一般来说,是全世界都期待的事情,因此这绝对是你应该做好准备的事情。
现在,安全性的另一个方面与模型绑定有关。
绑定安全
现在是一个完全不同的主题。我们知道 ASP.NET Core 会自动将提交的值绑定到模型类,但是如果我们劫持了一个请求并要求 ASP.NET 绑定一个不同于我们现有的用户或角色,会发生什么情况?例如,考虑是否有一种使用以下模型更新用户配置文件的方法:
public class User
{
public string Id { get; set; }
public bool IsAdmin { get; set; }
//rest of the properties go here
}
如果将此模型提交到数据库,很容易看出,如果我们传递一个值IsAdmin=true,那么我们将立即成为管理员!为防止这种情况,我们应采取以下措施之一:
- 从
public模型中移出敏感属性,该模型从用户发送的数据中检索 - 将
[BindNever]属性应用于这些敏感属性,如下所示:
[BindNever]
public bool IsAdmin { get; set; }
在后一种情况下,我们需要使用正确的逻辑自己填充这些属性。
As a rule of thumb, never use as the MVC model the domain classes that you use in your object-relational mapping (O/RM); it is better to have a clear distinction between the two and map them yourself (even if with the help of a tool such as AutoMapper), taking care of sensitive properties.
请小心绑定的属性,因为您不希望用户能够访问所有内容。仔细检查您的模型和绑定规则。
总结
本章讨论了安全的许多方面。在这里,我们学习了如何使我们的应用更安全,更能抵御攻击。
我们了解如何使用授权属性来保护应用的敏感资源。使用策略比使用实际命名的声明或角色更好,因为更改策略配置要容易得多,而且您几乎可以做任何事情。
然后,我们了解了如何使用身份进行身份验证,而不是推出自己的机制。如果您的需求允许,请使用社交登录,因为这可能被广泛接受,因为大多数人都使用社交网络应用。
在将敏感数据绑定到模型时要小心;防止它自动发生,并为 MVC 和实际数据存储使用不同的模型。我们看到,我们总是对来自数据库的数据进行 HTML 编码,以防止恶意用户向其中插入 JavaScript 的可能性。
我们看到,我们需要警惕静态文件,因为它们在默认情况下不受保护。最好检索这些文件。
最后,在本章的最后一部分,我们理解我们应该考虑将站点的整体移动到 HTTPS,因为它显著地减少了窃听数据的机会。
这是一个相当广泛的主题,涵盖了安全的许多方面。如果你坚持这些建议,你的应用会更安全,但这还不够。始终遵循您使用的 API 的安全建议规则,并确保您知道它们的含义。
在下一章中,我们将看到如何提取 ASP.NET Core 中发生的事情的信息
问题
因此,在本章结束时,您应该知道以下问题的答案:
- 我们可以使用什么属性来标记方法或控制器,以便只能通过 HTTPS 调用它?
- 基于角色的授权和基于策略的授权有什么区别?
- CORS 的目的是什么?
- HSTS 的目的是什么?
- 身份验证过程的挑战阶段是什么?
- 为什么在将请求绑定到模型类时要小心?
- 饼干的滑动过期时间是多少?
十二、日志,跟踪和诊断
日志记录、跟踪和度量是任何非平凡应用的基本功能,原因如下:
- 日志记录告诉我们系统正在做什么,它将要做什么,它遇到的错误,等等。
- 跟踪是关于收集关于旅程的事务信息,以及它们在分布式系统中的流动方式。
- 指标包括实时获取有关正在发生的事情的信息,并可能从中生成警报。
在本章中,我们将了解我们手头上的一些选项,从最简单到最复杂。
本章将介绍以下主题:
- 介绍.NET Core 通用日志框架
- 编写自定义日志中间件
- 使用跟踪和诊断
- 使用性能(事件)计数器获取指标
- 通过 Microsoft Azure AppInsights、亚马逊网络服务(AWS)CloudWatch 和 New Relic 使用遥测技术
- 执行 ASP.NET Core 运行状况检查
技术要求
为了实现本章介绍的示例,您需要.NET Core 3软件开发工具包(SDK和某种形式的文本编辑器。当然,VisualStudio2019(任何版本)满足所有要求,但您也可以使用 VisualStudio 代码。
要使用 Azure、AWS 或 New Relic,您需要这些提供商的工作帐户。
源代码可以在这里从 GitHub 检索:https://github.com/PacktPublishing/Modern-Web-Development-with-ASP.NET-Core-3-Second-Edition 。
介绍.NET Core 通用日志框架
日志记录是.NETCore 的一个组成部分,它提供了几个抽象来支持它;不用说,它是完全可插拔和可扩展的。基础结构类、接口、抽象基类、枚举等包含在Microsoft.Extensions.Logging.AbstractionsNuGet 包中,内置实现包含在Microsoft.Extensions.Logging包中。记录消息时,它将路由到所有已注册的日志提供程序。
在这里,我们将看到以下内容:
- 使用日志服务
- 定义日志级别
- 使用日志提供程序
- 过滤日志
- 编写自定义日志提供程序
- 使用依赖注入****DI与日志提供者
- 使用日志属性
我们将在接下来的章节中研究每一个问题。
使用日志服务
我们通过在ConfigureServices方法中调用AddLogging来注册日志服务。这实际上是通过其他方法完成的,例如AddMvc,因此通常不需要手动调用它,当然,如果我们不使用模型视图控制器(MVC)。不过,这样做没有害处。手动操作,可按如下方式进行:
services.AddLogging();
为了登录到.NET Core,您需要一个ILogger(或ILogger<T>接口的实例。您通常使用完全支持它的 DI 框架将其注入类控制器、视图组件、标记帮助器和中间件中。以下代码段对此进行了说明:
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger)
{
this._logger = logger;
}
}
但您也可以通过ILoggerFactory方法请求接口实例,如下所示:
var logger1 = loggerFactory.CreateLogger<MyClass>();
//or
var logger2 = loggerFactory.CreateLogger("MyClass");
类别名称取自泛型类参数的完整类型名称。
ILogger接口只提供三种方法,加上一些扩展方法。以下列出了核心方法:
BeginScope:启动一个与它内部的所有日志相关的块IsEnabled:检查是否允许记录给定的日志级别Log:将日志消息写入特定的日志级别,带有事件 ID 和可选的格式化程序
定义日志级别
日志级别在LogLevel枚举中定义如下:
| 级别 | 数值 | 目的 |
| Trace | 0 | 包含最详细消息的日志。这些消息可能包含敏感的应用数据。 |
| Debug | 1. | 在开发过程中用于交互调查的日志。这些日志应该主要包含对调试有用的信息,并且没有长期价值。 |
| Information | 2. | 跟踪应用一般流程的日志。这些日志应该具有长期价值。 |
| Warning | 3. | 突出显示应用流中异常或意外事件,但不会导致应用执行停止的日志。 |
| Error | 4. | 当当前执行流由于故障而停止时突出显示的日志。这些应该指示当前活动中的故障,而不是应用范围的故障。 |
| Critical | 5. | 描述无法恢复的应用或系统崩溃或需要立即注意的灾难性故障的日志。 |
| None | 6. | 指定日志记录类别不应写入任何消息。 |
如您所见,这些日志级别具有升序数值,从最冗长且可能无趣的(调试目的除外)开始,到最严重的结束。日志框架的设计使我们能够过滤掉低于给定级别的级别,从而避免日志中不必要的混乱。为了检查给定级别是否启用,我们使用IsEnabled。
Log通用方法通常是最有趣的方法,它采用以下参数:
logLevel(LogLevel:所需的日志级别eventId(EventId:事件 IDstate(TState:要记录的状态exception(Exception:日志异常formatter(Func<TState, Exception, string>:基于状态和可能的异常的格式化函数
每个日志条目都有以下信息:
-
日志级别
-
时间戳
-
类别
-
例外状态
-
事件 ID
-
作用域名称(如果从作用域内部调用)
当我们从ILoggerFactory请求 logger 实例时,T参数是类别名称,这通常是通过在类的构造函数中声明ILogger<T>实例自动完成的;它与完全限定类型名相同。
到目前为止,我们想要记录的最常见的消息类型是字符串(或异常),因此有一些扩展方法可以做到这一点,如下所示:
LogTraceLogDebugLogInformationLogWarningLogErrorLogCritical
如您所见,所有这些方法都绑定到特定的日志级别,并且每个方法都有三个重载,用于获取这些参数的组合,如下所示:
message(string:要记录的消息,带有可选的参数占位符(例如,{0}、{1}等)parameters(params object []:要记录的消息的可选参数eventId(EventId:一个相关 IDexception(Exception:如果存在异常,则为日志异常
三个重载中的每一个取message加其可选的parameters,另一个取eventId,另一个取exception。
事件 ID 的用途是什么?我听到你问。它是一个相关 ID,一个在请求中可能是唯一的标识符,它将多个日志消息关联在一起,以便分析它们的人能够发现它们是否相关。事件 ID 本质上是一个数字加上一个可选名称。如果未提供,则不使用。
作用域仅在所有日志消息中包含给定的作用域名称,直到作用域结束。由于BeginScope返回IDisposable实例,调用其Dispose方法结束作用域。
使用日志提供程序
日志提供程序是实现ILoggerProvider接口的类。日志提供程序需要在日志框架中注册,以便使用。通常,这可以通过ILoggerFactory.AddProvider方法或提供者提供的扩展方法来完成。
Microsoft 随以下提供商提供.NET Core:
| 供应商 | NuGet 套餐 | 目的 |
| Azure 应用服务 | Microsoft.Extensions.Logging.AzureAppServices | 将日志记录到 Azure Blob 存储或文件系统 |
| 安慰 | Microsoft.Extensions.Logging.Console | 登录到控制台 |
| 调试 | Microsoft.Extensions.Logging.Debug | 使用Debug.WriteLine的日志 |
| 事件日志 | Microsoft.Extensions.Logging.EventLog | 记录到 Windows 事件日志 |
| 事件源 | Microsoft.Extensions.Logging.EventSource | Windows(ETW的事件跟踪日志 |
| 痕迹资源 | Microsoft.Extensions.Logging.TraceSource | 使用TraceSource的日志 |
通常,我们在 ASP.NET Core 的Startup类的引导过程中注册这些提供程序,但我们也可以在Program类中更早注册这些提供程序;这样做的好处是可以捕捉到在Startup出现之前可能出现的一些早期事件。为此,我们需要在IHostBuilder调用中添加一个额外步骤,如以下代码片段所示:
Host
.CreateDefaultBuilder(args)
.ConfigureLogging(builder =>
{
builder
.AddConsole()
.AddDebug();
})
//rest goes here
从 ASP.NET Core 2 开始,在ConfigureServices方法中,当我们向 DI 框架注册日志提供程序时,日志机制的配置在前面完成,如以下代码片段所示:
services
.AddLogging(options =>
{
options
.AddConsole()
.AddDebug();
});
Azure App Service logging has, of course, much more to it than the other built-in providers. For a good introduction to it, outside the scope of this book, please have a look at https://blogs.msdn.microsoft.com/webdev/2017/04/26/asp-net-core-logging.
还有其他几个.NET Core 提供商,包括:
| 供应商 | NuGet 套餐 | 来源 |
| 自动气象站 | AWS.Logger.AspNetCore | https://github.com/aws/aws-logging-dotnet |
| 埃尔玛·伊奥 | Elmah.Io.Extensions.Logging | https://github.com/elmahio/Elmah.Io.Extensions.Logging |
| Log4Net | log4net | https://github.com/apache/logging-log4net |
| Loggr | Loggr.Extensions.Logging | https://github.com/imobile3/Loggr.Extensions.Logging |
| NLog | NLog | https://github.com/NLog |
| Serilog | Serilog | https://github.com/serilog/serilog |
在这些包中,Serilog可以进行结构化日志记录,除了字符串之外还有其他功能。
过滤日志
我们可以根据以下标准限制(过滤)日志记录:
- 日志级别
- 类别名称
这意味着,对于特定的提供者,我们可以为以特定名称开头的类别记录高于或等于给定级别的所有级别的事件。
由于 ASP.NET Core 2,我们可以根据类别和日志级别配置默认全局筛选器;lambda 表达式允许我们返回一个布尔值,以指示是否应处理日志,如以下代码段所示:
services
.AddLogging(options =>
{
options.AddFilter((category, logLevel) => logLevel >=
LogLevel.Warning);
});
或者,只需按类别过滤,如本例所示,其中 lambda 仅采用类别名称:
services
.AddLogging(options =>
{
options.AddFilter("Microsoft", LogLevel.Warning);
});
如果我们想过滤特定提供者的日志输出,我们将其作为通用模板方法添加到AddFilter,如下所示:
services
.AddLogging(options =>
{
options.AddFilter<ConsoleLoggerProvider>("Microsoft",
LogLevel.Warning);
//or, with a lambda
options.AddFilter<ConsoleLoggerProvider>((categoryName,
logLevel) => true);
});
也可以按提供程序、类别和日志级别执行此操作,如下所示:
services
.AddLogging(options =>
{
options.AddFilter<ConsoleLoggerProvider>("System", logLevel =>
logLevel >= LogLevel.Warning);
//same as this
options.AddFilter((provider, category, logLevel) =>
{
//you get the picture
});
});
此外,作为主机构建过程的一部分,可以在Program类中配置提供程序,如以下代码片段所示:
Host
.CreateDefaultBuilder(args)
.ConfigureLogging((hostingContext, builder) =>
{
builder.AddConfiguration(hostingContext.Configuration.
GetSection("Logging"));
builder.AddConsole(LogLevel.Warning);
builder.AddDebug();
})
//rest goes here
编写自定义日志提供程序
ASP.NET Core 中不包含的非现成的提供程序是一个写入文件的提供程序。(有些简单)文件日志记录提供程序可能如下所示:
public sealed class FileLoggerProvider : ILoggerProvider
{
private readonly Func<string, LogLevel, bool> _func;
public FileLoggerProvider(Func<string, LogLevel, bool> func)
{
this._func = func;
}
public FileLoggerProvider(LogLevel minimumLogLevel) :
this((category, logLevel) => logLevel >= minimumLogLevel)
{
}
public ILogger CreateLogger(string categoryName)
{
return new FileLogger(categoryName, this._func);
}
public void Dispose()
{
}
}
public sealed class FileLogger : ILogger
{
private readonly string _categoryName;
private readonly Func<string, LogLevel, bool> _func;
public FileLogger(string categoryName, Func<string, LogLevel,
bool> func)
{
this._categoryName = categoryName;
this._func = func;
}
public IDisposable BeginScope<TState>(TState state)
{
return new EmptyDisposable();
}
public bool IsEnabled(LogLevel logLevel)
{
return this._func(this._categoryName, logLevel);
}
public void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception exception,
Func<TState, Exception, string> formatter)
{
if (this.IsEnabled(logLevel))
{
var now = DateTime.UtcNow;
var today = now.ToString("yyyy-MM-dd");
var fileName = $"{this._categoryName}_{today}.log";
var message = formatter(state, exception);
File.AppendAllText(fileName, $"{message}\n");
}
}
}
internal sealed class EmptyDisposable : IDisposable
{
public void Dispose() { }
}
public static class LoggerFactoryExtensions
{
public static ILoggerFactory AddFile(this ILoggerFactory
loggerFactory,
Func<string, LogLevel, bool> func)
{
loggerFactory.AddProvider(new FileLoggerProvider(func));
return loggerFactory;
}
public static ILoggerFactory AddFile(this ILoggerFactory
loggerFactory, LogLevel minimumLogLevel)
{
return AddFile(loggerFactory, (category, logLevel) => logLevel >=
minimumLogLevel);
}
public static ILoggingBuilder AddFile(this ILoggingBuilder
loggingBuilder,
Func<string, LogLevel, bool> func)
{
return loggingBuilder.AddProvider(new FileLoggerProvider(func));
}
public static ILoggingBuilder AddFile(this ILoggingBuilder
loggingBuilder, LogLevel minimumLogLevel)
{
return AddFile(loggingBuilder, (category, logLevel) =>
logLevel >= minimumLogLevel);
}
}
此示例由以下内容组成:
- 记录器工厂类
FileLoggerFactory,负责创建实际记录器 FileLogger类,它记录到一个文件- 用于模拟作用域的帮助器类
EmptyDisposable LoggerFactoryExtensions类中的一些扩展方法,可以通过ILoggerFactory实例或ILoggingBuilder实例更轻松地注册文件提供程序。
FileLoggerFactory类需要接受一个参数,该参数是要接受的最低日志级别,然后传递给任何创建的记录器。要创建的文件的名称格式为{categoryName}-{yyyy-MM-dd}.log,其中categoryName是传递给CreateLogger方法的值,yyyy-MM-dd是当前日期。很简单,你不觉得吗?
在日志提供程序中使用 DI
正如我们所看到的,我们可以将记录器或记录器工厂本身注入到我们的类中。传递记录器是目前最常见的场景,但是如果我们想要进行一些额外的配置,例如注册一个新的日志提供程序,我们也可以传递记录器工厂。
Be warned: you cannot inject an ILogger instance, only an ILogger<T> instance, where T is an actual type—class or struct, abstract or concrete; it doesn't matter. Since ASP.NET Core 2, you do not need to call AddLogging explicitly in your ConfigureServices method, as the logging services are automatically registered.
使用日志属性
在第 7 章实现剃须刀页面中解释的过滤器机制的一个有趣用途是通过过滤器属性添加日志记录。根据我们想要添加日志的位置,我们可以使用资源、结果或操作过滤器,但我将给出一个涉及操作过滤器的示例,因为这些过滤器能够检查将要传递给操作方法的模型及其调用的结果。
以下代码块显示了一个属性,当该属性应用于类或方法时,将导致发出日志消息:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true,
Inherited = true)]
public sealed class LoggerAttribute : ActionFilterAttribute
{
public LoggerAttribute(string logMessage)
{
this.LogMessage = logMessage;
}
public string LogMessage { get; }
public LogLevel LogLevel { get; set; } = LogLevel.Information;
private EventId _eventId;
private string GetLogMessage(ModelStateDictionary modelState)
{
var logMessage = this.LogMessage;
foreach (var key in modelState.Keys)
{
logMessage = logMessage.Replace("{" + key + "}",
modelState[key].RawValue?.ToString());
}
return logMessage;
}
private ILogger GetLogger(HttpContext context,
ControllerActionDescriptor action)
{
var logger = context
.RequestServices
.GetService(typeof(ILogger<>)
.MakeGenericType(action.ControllerTypeInfo.
UnderlyingSystemType)) as ILogger;
return logger;
}
public override void OnActionExecuted(ActionExecutedContext context)
{
var cad = context.ActionDescriptor as ControllerActionDescriptor;
var logMessage = this.GetLogMessage(context.ModelState);
var logger = this.GetLogger(context.HttpContext, cad);
var duration = TimeSpan.FromMilliseconds(Environment.
TickCount - this._eventId.Id);
logger.Log(this.LogLevel, this._eventId,
$"After {cad.ControllerName}.{cad.ActionName} with
{logMessage} and result {context.HttpContext.Response.StatusCode}
in {duration}", null, (state, ex) => state.ToString());
base.OnActionExecuted(context);
}
public override void OnActionExecuting(ActionExecutingContext context)
{
var cad = context.ActionDescriptor as ControllerActionDescriptor;
var logMessage = this.GetLogMessage(context.ModelState);
var logger = this.GetLogger(context.HttpContext, cad);
this._eventId = new EventId(Environment.TickCount, $"{cad.
ControllerName}.{cad.ActionName}");
logger.Log(this.LogLevel, this._eventId, $"Before {cad.
ControllerName}.{cad.ActionName} with {logMessage}", null,
(state, ex) => state.ToString());
base.OnActionExecuting(context);
}
}
此示例描述了可应用于方法或类的属性。它继承自ActionFilterAttribute,这意味着它是一个过滤器(请参见第 10 章、理解过滤器,了解过滤器的更新)。这意味着在动作执行之前(OnActionExecuting)和之后(OnActionExecuted),该属性执行一些动作。在本例中,它从 DI 检索当前控制器的记录器(如果您不使用 MVC,请对此进行调整),并向其记录一条消息。此属性需要从其构造函数中获取一个logMessage参数。此参数可以采用括号中的型号名称(例如,{email}),该名称将在日志消息中替换。它使用自系统重新启动以来经过的毫秒数(Environment.TickCount作为事件 ID,并使用控制器和操作名称的组合作为事件名称;此事件 ID 在事件前和事件后重复使用。在调用每个操作方法之前和之后,将使用提供的日志级别记录一条消息。下面是一个示例声明:
[Logger("Method called with {email}", LogLevel = LogLevel.Information)]
public IActionResult AddToMailingList(string email) { ... }
如果我们希望将自定义日志透明地添加到某些操作方法以注销模型值,这可能很方便。
现在,我们将看到另一种更高级的方法来记录所有请求。
编写自定义日志中间件
在上一节中,我们已经了解了如何编写自定义属性以在控制器操作之前和之后执行操作,以及在第 1 章中,开始使用 ASP.NET Core,中,我们如何编写中间件。现在,可以编写一个简单的中间件类来记录所有请求,如下所示:
public class LoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILoggerFactory _loggerFactory;
public LoggingMiddleware(RequestDelegate next, ILoggerFactory
loggerFactory)
{
this._next = next;
this._loggerFactory = loggerFactory;
}
public async Task InvokeAsync(HttpContext context)
{
var logger = this._loggerFactory.CreateLogger<LoggingMiddleware>
();
using (logger.BeginScope<LoggingMiddleware>(this))
{
logger.LogInformation("Before request");
await this._next.Invoke(context);
logger.LogInformation("After request");
}
}
}
请注意,记录器的类别设置为LoggingMiddleware类型的全名,我们为每个调用启动一个作用域,这里只是一个示例。注册此中间件的方法是在Configure中调用UseMiddleware,如下所示:
app.UseMiddleware<LoggingMiddleware>();
这会将我们的中间件添加到管道中,但我们必须确保在我们希望监视的任何其他内容之前添加它,否则我们将无法捕获它。
这就完成了关于编写日志中间件的简短部分。现在,让我们看看一些工具。
使用跟踪和诊断
我们在第 5 章、视图中提到了 ASP.NET Core 的诊断功能。诊断与日志记录相当,但它有许多优点,如下所示:
- 追踪在更高的层次上运行,捕捉整个旅程,而不仅仅是瞬间。
- 它可以进行结构化日志记录,也就是说,它可以调用跟踪记录器中的方法,这些方法接受参数,而不仅仅是字符串。
- 插入新适配器很容易,只需向类添加一个属性;甚至可以使用引用程序集中的类。
请参考第 5 章、视图,了解更深入的解释。在这里,我们将介绍一个微软软件包Microsoft.AspNetCore.MiddlewareAnalysis。当使用它时,它通过诊断功能跟踪在管道上执行的所有中间件组件。通过简单调用AddMiddlewareAnalysis进行配置,如下所示:
services.AddMiddlewareAnalysis();
然后,我们注册一些新事件的侦听器,如下所示:
Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareStarting:中间件启动时调用Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareFinished:中间件完成后调用Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareException:执行中间件时发生异常时调用
以下是如何注册诊断源侦听器:
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env,
DiagnosticListener diagnosticListener)
{
var listener = new TraceDiagnosticListener();
diagnosticListener.SubscribeWithAdapter(listener);
//rest goes here
}
TraceDiagnosticListener类具有将自动连接到这些事件的方法,如以下代码块所示:
public class TraceDiagnosticListener
{
[DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.
MiddlewareStarting")]
public virtual void OnMiddlewareStarting(HttpContext
httpContext, string name)
{
//called when the middleware is starting
}
[DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.
MiddlewareException")]
public virtual void OnMiddlewareException(Exception exception,
string name)
{
//called when there is an exception while processing
//a middleware component
}
[DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.
MiddlewareFinished")]
public virtual void OnMiddlewareFinished(HttpContext
httpContext, string name)
{
//called when the middleware execution finishes
}
}
请注意,这两个中间件类(请参阅第 1 章、ASP.NET Core 入门)或作为自定义委托添加到管道中的中间件都会调用。代码显示在以下代码段中:
app.Properties["analysis.NextMiddlewareName"] = "MyCustomMiddleware";
app.Use(async (context, next) =>
{
//do something
await next();
});
注意我们设置analysis.NextMiddlewareName属性的那一行,因为这个中间件没有名称,它是一个匿名委托;该属性用于TraceDiagnosticListener类的每个方法的name参数。
如果您想直观地分析所有这些活动,可以添加另一个 Microsoft 软件包Microsoft.AspNetCore.Diagnostics.Elm。ELM代表错误记录中间件,通常先通过注册服务(ConfigureServices方法)添加,如下图:
services.AddElm();
然后,将中间件添加到管道中(Configure,如下所示:
app.UseElmPage();
app.UseElmCapture();
通过检查当前环境并有条件地添加此中间件,您可能希望添加这些只是为了开发。完成后,当您访问/elm时,您将得到一个很好的跟踪,如以下屏幕截图所示:

您可以看到处理请求时发生的所有事件。所有这些都来自诊断功能。
还有最后一步:您必须启用同步输入/输出(I/O),因为 ELM 使用它。在Program类中,将以下内容添加到ConfigureWebHostDefaults方法中:
.ConfigureWebHostDefaults(builder =>
{
builder.UseKestrel(options =>
{
options.AllowSynchronousIO = true;
});
builder.UseStartup<Startup>();
});
如果您想设置 ELM 使用的统一资源定位器(URL),或者过滤结果,您当然可以这样做,如下所示:
services.Configure<ElmOptions>(options =>
{
options.Path = "/_Elm";
options.Filter = (name, logLevel) =>
{
return logLevel > LogLevel.Information;
};
});
此小代码片段设置 ELM 的配置,如下所示:
- 请求路径设置为
/_Elm(必须以/开头)。 - 仅显示日志级别高于
Information的事件。
现在,我们将了解一个非常重要的功能,该功能自 Windows 诞生之初就存在,现在可供.NET Core 使用:事件计数器。
使用性能(事件)计数器获取指标
性能(事件)计数器自 Windows 诞生以来就存在,但它们没有以相同的方式在其他操作系统中实现,这使得它们无法跨平台。其思想是,应用发出轻量级、不引人注目的代码,由操作系统拾取,可用于在应用工作时实时监控应用,或生成转储文件进行事后分析。
.NET Core 3.0 通过引入dotnet-trace、dotnet-dump和dotnet-counters跨平台全局工具,开始全面支持事件计数器。我们将在下面几节中了解这些功能。
包括柜台
事件计数器是为命名计数器发出值的类。NET 包括以下计数器,分为两个提供程序:
System.Runtime (default)Microsoft.AspNetCore.Hosting
每个提供程序中的可用计数器如下所示:
| 系统运行时 |
| cpu-usage | 进程使用中央处理****单元(CPU)的时间量(毫秒)(平均值) |
| working-set | 进程使用的工作集数量(MB)(平均值) |
| gc-heap-size | 垃圾收集器(GC)报告的总堆大小(MB)(平均值) |
| gen-0-gc-count | 第 0 代 GCs/秒的数量(总和) |
| gen-1-gc-count | 第 1 代地面军事系统/秒的数量(总和) |
| gen-2-gc-count | 第 2 代地面军事系统/秒的数量(总和) |
| time-in-gc | %自上次 GC 以来的 GC 时间(平均值) |
| gen-0-size | 第 0 代堆大小(平均值) |
| gen-1-size | 第 1 代堆大小(平均值) |
| gen-2-size | 第 2 代堆大小(平均值) |
| loh-size | 大对象堆(LOH)堆大小(平均值) |
| alloc-rate | 分配率(总和) |
| assembly-count | 加载的程序集数(平均值) |
| exception-count | 每秒异常数(总和) |
| threadpool-thread-count | ThreadPool螺纹数(平均值) |
| monitor-lock-contention-count | 监视器锁争用计数 |
| threadpool-queue-length | ThreadPool工作项队列长度(平均值) |
| threadpool-completed-items-count | ThreadPool已完成工作项的计数 |
| active-timer-count | 活动计时器计数 |
| Microsoft.AspNetCore.Hosting |
| requests-per-second | 请求率(平均值) |
| total-requests | 请求总数(总和) |
| current-requests | 当前请求数(总和) |
| failed-requests | 失败的请求数(总和) |
System.Runtime提供程序包含独立于应用 web、控制台、服务等类型的计数器。当然,Microsoft.AspNetCore.Hosting只包含特定于 web 的计数器。
您可以想象,通过监视这些计数器,我们可以从系统的角度很好地了解 ASP.NET Core 应用内部的情况。在接下来的部分中,我们将看到如何做到这一点。
海关柜台
我们可以编写代码来创建自己的计数器。以下是一个例子:
[EventSource(Name = SourceName)]
public sealed class LogElapsedUrlEventSource : EventSource
{
private readonly EventCounter _counter;
public static readonly LogElapsedUrlEventSource Instance = new LogElapsedUrlEventSource();
private const int SourceId = 1;
private const string SourceName = "LogElapsedUrl";
private LogElapsedUrlEventSource() : base(EventSourceSettings.
EtwSelfDescribingEventFormat)
{
this._counter = new EventCounter(SourceName, this);
}
[Event(SourceId, Message = "Elapsed Time for URL {0}: {1}",
Level = EventLevel.Informational)]
public void LogElapsed(string url, float time)
{
this.WriteEvent(SourceId, url, time);
this._counter.WriteMetric(time);
}
}
您需要添加System.Diagnostics.PerformanceCounterNuGet 包才能编译此文件。
此示例代码记录打开任意 URL 所花费的时间。它将 URL 作为参数,time作为浮点值。它本质上是一个单例,因为它有一个私有构造函数,只能通过其公共静态Instance字段访问。LogElapsed方法同时写入基础EventCounter实例(可接受任意数量的参数)和基础EventSource类(仅接受数值)。最后,对于性能计数器,使用的是数值。
要使用此示例,我们将执行以下操作:
LogElapsedUrlEventSource.Instance.LogElapsed("http://google.com", 0.1F);
这引用了我们新创建的事件源的单个实例,并为http://google.comURL 记录了一个0.1值。它将被写入下面的事件计数器。
性能监测
Windows 有一个名为PerfView(的免费工具 https://github.com/microsoft/perfview ),可用于可视化性能计数器,但不幸的是,它不是跨平台的。相反,我们需要另一种解决方案。让我们安装dotnet-counters全局工具。为此,请运行以下命令:
dotnet tool install -g dotnet-counters
安装dotnet-counters工具后(根据此命令,在全局范围内),我们现在可以使用它监视现有的应用。首先,让我们看看那些可以监控的(运行.NET Core 应用),如下所示:
dotnet-counters ps
这将提供正在运行的.NET Core 应用的进程 ID 列表,无论它们是什么。找到感兴趣的对象后,将其输入工具,如下所示:
dotnet-counters monitor -p 3527
如果在进程 ID 之后未指定提供者名称,则默认为System.Runtime。你应该得到这样的东西:

例如,如果您想监视我们的示例计数器,您可以运行以下代码:
dotnet-counters monitor -p 3527 LogElapsedUrl
dotnet-counters工具将持续更新,直到您退出它(Ctrl+C)。
For additional information on dotnet-counters and real-time performance monitoring of .NET Core apps, please refer to https://github.com/dotnet/diagnostics/blob/master/documentation/dotnet-couunters-instructions.md. The documentation is available at https://docs.microsoft.com/dotnet/core/diagnostics/dotnet-counters.
在这里,我们看到了如何在控制台中实时监控事件。现在,让我们看看如何将它们收集到一个文件中。
追踪
与.NET Core 3 一起引入的另一个工具是dotnet-trace。它与dotnet-counter类似,但不是将当前事件计数器值输出到控制台,而是将其写入跟踪文件。这样做的好处是可以在以后读取它,以便从中提取运行时信息,包括它与有意义的事件(上下文信息)的关联。它也可以在后台运行,不需要有人观看控制台。
要安装此跟踪,请使用以下命令:
dotnet tool install -g dotnet-trace
与dotnet-counters类似,dotnet-trace可以检测系统上运行的.NET Core 进程,还需要连接到一个进程,如下所示:
dotnet-trace collect -p 3527
现在,在进程 ID 之后,dotnet-trace 命令可以采用跟踪配置文件的名称。默认情况下,定义的跟踪配置文件包括许多事件计数器,但您也可以定义自己的事件计数器。包括以下供应商:
- Microsoft Windows DotNETRuntime(默认)
- 微软 DotNETCore 采样档案器
I won't go into the details of creating trace profiles, as this is a complex topic.
要为Microsoft-Windows-DotNETRuntime概要文件创建跟踪文件,我们运行以下命令:
dotnet-trace collect -p 3527 --providers Microsoft-Windows-DotNETRuntime
这将导致如下结果:

除了配置文件(包括性能计数器以外的许多信息项)之外,您还可以通过指定提供程序名称作为跟踪配置文件来请求各个性能计数器提供程序,如下所示:
dotnet-trace collect -p 3527 --providers System.Runtime:0:1:EventCounterIntervalSec=1
在这种情况下,dotnet-trace将每秒输出属于System.Runtime提供程序的所有计数器的性能计数器值。
跟踪文件具有trace.nettrace名称,默认情况下,在运行dotnet-trace命令的同一文件夹中创建。要查看这些文件,我们有两个选项。
在 Windows 系统上,我们可以使用 Visual Studio,如以下屏幕截图所示:

对于 PerfView,我们可以使用以下内容:

对于支持.NET Core(Linux 和 macOS)的非 Windows 系统,目前还没有用于读取跟踪文件的本机解决方案,但一个可能的替代方案是免费的Speedscope站点(http://speedscope.app )。但是,为了使用它,我们必须通过运行以下代码告诉dotnet-trace以适当的格式生成跟踪文件:
dotnet-trace collect -p 3527 --providers Microsoft-Windows-DotNETRuntime --format speedscope
或者,如果我们有一个.nettrace格式的文件,我们可以随后将其转换,以便将其传递给 Speedscope 应用。为此,您需要运行以下代码:
dotnet-trace convert trace.nettrace --format speedscope
输出文件将被称为trace.speedscope.json,如以下屏幕截图所示:

For additional information on dotnet-trace and the tracing of .NET Core apps, please refer to https://github.com/dotnet/diagnostics/blob/master/documentation/dotnet-trace-instructions.md. The documentation is available at https://docs.microsoft.com/dotnet/core/diagnostics/dotnet-trace.
我们现在来看另一个工具,但这一次,一个用于更高级用户的工具。
跟踪转储
.NET Core 3 提供的第三个工具是dotnet-dump。这一个更适合高级用户,因为它需要深入了解IL(代表中间语言)和.NET 自己的汇编语言。简而言之,它可以收集正在运行的.NET Core 进程的内存转储并将其写入磁盘,以便脱机分析。这可能有助于发现应用在内存使用方面的行为。
dotnet-dump only works on Windows and Linux, not macOS, for generating dumps.
要安装它,请运行以下命令:
dotnet tool install -g dotnet-dump
与之前的工具不同,它不能列出正在运行的进程,因此,如果您需要此功能,则需要依赖于dotnet-counters或dotnet-trace。找到所需的后,要求它创建进程当前运行状态的转储,如下所示:
dotnet-dump collect -p 3527
转储文件将在同一文件夹上创建,并命名为core_YYYYMMDD_hhmmss。
生成转储文件后,还可以使用dotnet-dump进行分析,如下图所示:
dotnet-dump analyze core_20191103_234821
这将启动一个交互式 shell(在支持它的操作系统中),该 shell 允许对转储进行探索,并允许运行Strike 之子(SOS)命令(https://docs.microsoft.com/dotnet/core/diagnostics/dotnet-dump#analyze-sos 命令。
dotnet-dump only allows dumps to be analyzed on Linux. For additional information on dotnet-dump, please refer to https://github.com/dotnet/diagnostics/blob/master/documentation/dotnet-dump-instructions.md. The documentation is available at https://docs.microsoft.com/dotnet/core/diagnostics/dotnet-dump.
现在让我们离开控制台,探索一下远程监控应用的一些选项。
使用遥测
遥测技术包括通过互联网从软件中透明地收集使用数据。它非常有用,因为您的所有应用都是集中监控的,遥测软件包通常提供方便的工具,例如丰富的用户界面,用于选择我们想要看到的内容或创建警报。这里有一些替代方案,我们将在下面的部分中只讨论几个最流行的方案。
使用跟踪标识符
ASP.NET Core 提供了一个IHttpRequestIdentifierFeature功能,可为每个请求生成唯一的 ID。此 ID 可以帮助您关联在请求上下文中发生的事件。以下是获取此 ID 的三种方法:
//using the TraceIdentifier property in ASP.NET Core 2.x
var id1 = this.HttpContext.TraceIdentifier;
//accessing the feature in earlier versions of ASP.NET Core
var id2 = this.HttpContext.Features.Get<IHttpRequestIdentifierFeature>().TraceIdentifier;
//another way
var id3 = Activity.Current.Id;
跟踪标识符只是一个不透明的引用,如0HL8VHQLUJ7CM:00000001——保证在请求中是唯一的。有趣的是,这实现了跟踪上下文万维网联盟m(W3C)规范(https://www.w3.org/TR/trace-context ),这意味着 ASP.NET Core 3 尊重Request-IdHTTP 头。这是映射到这些属性的值。
确保使用以下行启用标准 W3C 格式,在应用的开头,可能在Main或Startup中:
Activity.DefaultIdFormat = ActivityIdFormat.W3C;
Activity.ForceDefaultIdFormat = true;
现在,每当您向某个微服务发出 HTTP 请求时,请确保包含此跟踪标识符。例如,如果您正在使用HttpClient,则可以使用DelegatingHandler,如下代码块所示:
public class TraceIdentifierMessageHandler : DelegatingHandler
{
private readonly IHttpContextAccessor _httpContextAccessor;
public TraceIdentifierMessageHandler(IHttpContextAccessor
httpContextAccessor)
{
this._httpContextAccessor = httpContextAccessor;
}
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var httpContext = this._httpContextAccessor.HttpContext;
request.Headers.Add("Request-Id", httpContext.TraceIdentifier);
request.Headers.Add("X-SessionId", httpContext.Session.Id);
return base.SendAsync(request, cancellationToken);
}
}
调用委托处理程序来处理消息的发送,在本例中,它用于设置头-Request-Id。它从注入构造函数的IHttpContextAccessor服务中获得的上下文中获取此头。
我们注册一个或多个HttpClient实例,我们将在ConfigureServices方法中调用的每个微服务一个实例,如下所示:
//this is required for the TraceIdentifierMessageHandler
services.AddHttpContextAccessor();
services
.AddHttpClient("<service1>", options =>
{
options.BaseAddress = new Uri("<url1>");
})
.AddHttpMessageHandler<TraceIdentifierMessageHandler>();
当然,您需要用适当的值替换<service1>和<url1>。
当您想要获取HttpClient的实例向该微服务发送消息时,注入IHttpClientFactory实例并请求您注册客户端时使用的名称,如下代码块所示:
public class HomeController : Controller
{
private readonly IHttpClientFactory _clientFactory;
public HomeController(IHttpClientFactory clientFactory)
{
this._clientFactory = clientFactory;
}
public async Task<IActionResult> Index()
{
var client = clientFactory.CreateClient("<service1>");
var result = await client.GetAsync("GetData");
//do something with the response
return this.Ok();
}
}
使用这种方法,我们可以确保在所有请求上都保持相同的跟踪标识符,这是在创建上下文。这对于维护状态非常重要,我将在下面介绍的日志框架中使用它。
Azure 应用洞察
当您使用 Visual Studio 创建 ASP.NET 项目时,您将看到添加对应用洞察(AI的支持的选项。AI 是一种 Azure 服务,可用于监视 web 应用的可用性、性能和使用情况,包括错误。当您向 web 应用添加对 AI 的支持时,您可以转到 AI 控制台,实时监控应用的行为,查看其最近的行为,甚至在发生异常情况时获得通知。
这里我们不会全面介绍人工智能,但只会概述如何在 ASP.NET Core 项目中使用人工智能。
以下屏幕截图显示了典型的组合视图,显示了响应和页面加载时间:

在使用 AI 之前,您需要有一个可用的 Azure 帐户(https://portal.azure.com 并且您需要创建一个 AI 资源。
您可以从 Visual Studio 内部创建一个,如以下屏幕截图所示:

详细说明请参考https://github.com/Microsoft/ApplicationInsights-aspnetcore/wiki/Getting-Started-with-Application-Insights-for-ASP.NET-Core 。AI 依赖于Microsoft.ApplicationInsights.AspNetCoreNuGet 包,创建 AI 资源后,您需要使用 AI 控制台中提供给您的检测密钥添加配置设置。如果您使用JavaScript 对象表示法(JSON)配置文件,您将需要如下内容:
{
"ApplicationInsights": {
"InstrumentationKey": "11111111-2222-3333-4444-555555555555"
}
}
在任何情况下,您都需要直接从配置中注册 AI 服务,如下所示:
services.AddApplicationInsightsTelemetry(this.Configuration);
或者,您可以直接传递检测键,如下所示:
services.AddApplicationInsightsTelemetry(
instrumentationKey: "11111111-2222-3333-4444-555555555555");
对于更高级的选项,您可以传入一个ApplicationInsightsServiceOptions实例,如以下代码段所示:
services.AddApplicationInsightsTelemetry(new ApplicationInsightsServiceOptions
{
InstrumentationKey = "11111111-2222-3333-4444-555555555555",
DeveloperMode = true,
EnableDebugLogger = true
});
在开发模式下运行时,使用 AI 的开发者模式通常很有用,因为您可以立即看到结果(而不是成批)。
您不需要显式启用异常和请求遥测;因为这是自动完成的,_ViewImports.cshtml的代码如下:
@using Microsoft.ApplicationInsights.AspNetCore
@inject JavaScriptSnippet snippet
在布局视图(可能是_Layout.cshtml)中,我们必须使用以下代码行呈现脚本:
@Html.Raw(snippet.FullScript)
这说明了如何启用 AI 功能,但下一个主题将展示如何向其发送实际事件。
发送自定义事件
您可以使用TelemetryClient应用编程接口(API发送自定义数据。基本上,您构建了一个TelemetryClient实例,如下所示:
var client = new TelemetryClient();
client.InstrumentationKey = this.Configuration["ApplicationInsights:InstrumentationKey"];
您有以下方法,取自文档(https://docs.microsoft.com/azure/application-insights/app-insights-api-custom-events-metrics :
TrackPageView:页数TrackEvent:用户操作和自定义事件TrackMetric:绩效衡量TrackException:例外情况TrackRequest:性能分析服务器请求的频率和持续时间TrackTrace:诊断日志消息TrackDependency:您的应用所依赖的外部组件调用的持续时间和频率
您可以调用前面列表中提到的Track方法之一。现在,让我们从TrackEvent开始,如下所示:
client.TrackEvent("Product added to basket");
您还可以为单个值调用特定度量,如下所示:
client.TrackMetric("TotalCost", 100.0);
您可以请求信息,如下所示:
var now = DateTimeOffset.Now;
var timer = Stopwatch.StartNew();
//issue call
client.TrackRequest("Searching for product", now, timer.Elapsed, "OK", true);
要发送异常,请执行以下代码:
client.TrackException(ex);
要在经过的时间(类似于请求跟踪)发送依赖项(非我们自己的任何调用代码),请执行以下代码:
var success = false;
var startTime = DateTime.UtcNow;
var timer = Stopwatch.StartNew();
var id = Guid.NewGuid();
try
{
success = orderService.ProcessOrder();
}
finally
{
timer.Stop();
telemetry.TrackDependency("Order Service", $"Order id {id}", startTime, timer.Elapsed, success);
}
要发送自定义跟踪消息,请执行以下代码:
client.TrackTrace("Processing order", SeverityLevel.Warning, new Dictionary<string,string> { {"Order", id} });
对于页面视图,执行以下代码:
client.TrackPageView("ShoppingBag");
您还可以将范围内的几个相关事件组合在一起,如下所示:
using (var operation = telemetry.StartOperation<RequestTelemetry>("Order Processing"))
{
//one or more of Track* methods
}
因为 AI 会批量发送数据,所以在任何时候,您都可以通过运行以下命令强制将其刷新到 Azure:
client.Flush();
所有这些事件都将在 AI 控制台中可用,如以下屏幕截图所示:

AppInsights 仪表板可以显示不同的事件可视化(例如,按名称分组事件),如以下屏幕截图所示:

AI 可以使用前面讨论过的跟踪标识符跟踪分布式调用。
在下一节中,我将介绍另一个主要云提供商的日志服务:AWS CloudWatch。
AWS 云表
AWS CloudWatch 是一种 AWS 应用和基础架构监控服务。它允许您将半结构化内容写入日志,然后可以在线查询和监视日志。
为了从.NET Core 应用写入,您需要添加对AWSSDK.Core和AWSSDK.CloudWatchNuGet 包的引用。不需要添加自定义日志框架;内置的日志记录就可以了。使用需要的应用配置在ConfigureServices中注册其服务,以便我们可以注入服务。这可以通过运行以下代码来完成:
services.AddDefaultAWSOptions(this.Configuration.GetAWSOptions());
services.AddAWSService<IAmazonCloudWatch>();
GetAWSOptions扩展名从appsettings.json文件中可能提供的配置中返回所需的条目。这些应该如下所示:
{
"AWS": {
"Profile": "local-profile",
"Region": "eu-west-1"
}
}
当然,您将分别替换local-profile和eu-west-1,替换 AWS 配置文件中的配置文件名称和要使用的区域名称,该区域应是距离您所在位置最近的区域。
您还需要启用到控制台的日志记录。添加Microsoft.Extensions.Logging.ConsoleNuGet 包(如果您还没有),并运行以下代码:
services.AddLogging(options =>
{
options.AddConsole();
});
一旦 AWS 服务注册,只要您登录到控制台,您也将登录到 AWS CloudWatch。
以下是来自 AWS 站点的 AWS CloudWatch 日志示例:

在下一节中,我将介绍一种非常流行的商业工具,用于实时监控 web 应用,它可以应用于任何站点,甚至是云端托管的站点。
新遗迹
New Relic 是一款软件分析产品,用于监控 web 应用的应用性能。它提供有关 web 应用行为的实时数据,如同时用户数、内存使用和错误。您需要在部署计算机上安装 New Relic 代理,无论是 Windows 还是 Linux。
Please follow the instructions available at https://docs.newrelic.com/docs/agents/net-agent/installation/introduction-net-agent-install to set up the New Relic agent on your platform of choice.
安装后,New Relic 会截获并检测您的.NET Core 在计算机上的调用,并向其添加遥测功能。New Relic 还可以安装在 Docker 上,这使得它非常适合于自包含的自动化部署。
所有生成的调用都可以在 NewRelic 控制台上使用,如 NewRelic 站点的以下屏幕截图所示:

请注意,《新文物》是非常强大的,我甚至不敢看它,因为这需要一本书。请一定去看看这个新遗址,看看它是否适合你的需要。
接下来,我们将看一看 ASP.NET Core 新增的用于监视应用内服务状态的功能。
进行健康检查
微软一直在开发一个名为Microsoft.AspNetCore.Diagnostics.HealthChecks的健康检查框架,并将其作为 ASP.NET Core 2.2 的一部分发布。它提供了一种标准的可插拔方式来检查应用所依赖的服务状态。这不是 ASP.NET 本身的一项功能,但由于任何复杂的 web 应用通常都有外部依赖项,因此在采取任何操作之前检查其状态可能很方便。
在添加核心 NuGet 包之后,您将需要添加您感兴趣的检查,例如 SQL Server 的检查。我们在ConfigureServices方法中注册了以下内容:
services
.AddHealthChecks()
.AddCheck("Web Check", new WebHealthCheck("http://
google.com"), HealthStatus.Unhealthy)
.AddCheck("Sample Lambda", () => HealthCheckResult.Healthy
("All is well!"))
.AddDbContextCheck<MyDbContext>("My Context"); //check
//a database through EF Core
在本例中,我们登记了三张支票,如下所示:
- 一个自定义检查类
WebHealthCheck,它实现了IHealthCheck接口 - 基于 lambda 的检查
- 使用实体框架(EF核心上下文检查数据库访问权限的检查
对于 EF 核心检查,您需要添加对Microsoft.Extensions.Diagnostics.HealthChecks的.EntityFrameworkCoreNuGet 包的引用。
WebHealthCheck类看起来像这样:
public class WebHealthCheck : IHealthCheck
{
public WebHealthCheck(string url)
{
if (string.IsNullOrWhiteSpace(url))
{
throw new ArgumentNullException(nameof(url));
}
this.Url = url;
}
public string Url { get; }
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default(CancellationToken))
{
var client = new HttpClient();
var response = await client.GetAsync(this.Url);
if (response.StatusCode < HttpStatusCode.BadRequest)
{
return HealthCheckResult.Healthy("The URL is up and running");
}
return HealthCheckResult.Unhealthy("The URL is inaccessible");
}
}
此类针对提供的 URL 发出 HTTP 请求,并根据 HTTP 响应状态代码返回Healthy或Unhealthy。
在Configure上,我们注册了一个端点以监控这些检查,如下所示:
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints
.MapHealthChecks("/health")
.RequireHost("localhost")
.RequireAuthorization();
});
注意对MapHealthChecks的调用,端点名称为/health。
然后我们可以将一个IHealthCheckService实例注入到我们的类中,看看是否所有内容都是OK,如下所示:
var timedTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(3));
var checkResult = await _healthCheck.CheckHealthAsync(timedTokenSource.Token);
if (checkResult.CheckStatus != CheckStatus.Healthy)
{
//Houston, we have a problem!
}
检查所有已注册健康检查的状态时,此代码最多等待 3 秒钟。结果可能是以下情况之一:
Unknown:状态未知,可能是超时。Unhealthy:至少有一项服务不正常。Healthy:一切似乎都很好。Warning:一切正常,但有很多警告。
现在,健康检查框架启用了一个端点,您可以从中找到所有检查的全局状态;如前所述,我们将其设置为/health。如果我们使用浏览器访问它,就会得到其中一个值。
但是,如果我们希望获得已执行的所有检查的详细信息及其状态,我们可以将端点的注册码修改为如下内容:
endpoints
.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = async (context, report) =>
{
var result = JsonSerializer.Serialize(new
{
Status = report.Status.ToString(),
Checks = report.Entries.Select(e => new
{
Check = e.Key,
Status = e.Value.Status.ToString()
})
});
context.Response.ContentType = MediaTypeNames.Application.Json;
await context.Response.WriteAsync(result);
}
});
这样,我们将发送一个 JSON 响应,其中包含除全局状态之外的所有单个检查的状态,如以下代码片段所示:
{"Status":"Healthy","Checks":[{"Check":"Web Check","Status":"Healthy"},{"Check":"Sample Lambda","Status":"Healthy"},{"Check":"My Context","Status":"Healthy"}]}
在本章中,我们介绍了 ASP.NET Core 的新运行状况检查功能。它是一个可插拔的系统,允许您编写自己的健康检查,并从一个集中的端点进行检查。它是检查 web 应用状态的宝贵资产,我希望它有用!
总结
本章从通用日志开始,它是.NET Core 中的必备功能。该基础设施虽然有限,但不支持结构化的即时登录。例如,它是内置的、可插入的和 DI 友好的。将其用于最常见的日志记录用途。请浏览所有可用的日志记录提供程序,查看是否有一个符合您的要求。特殊的主机提供商,如 Azure 或 AWS,提供自己的软件包,您应该利用这些软件包获得最佳效果。
接下来,我们看到诊断跟踪提供了一个优势,您可以使用离散参数调用方法,这是一个优势,因为它可以生成更有意义的日志。您可以使用它精确地查看正在执行的中间件以及每个步骤所需的时间。
显示的其他选项(添加中间件或操作过滤器)也可能值得探索,尤其是操作过滤器。
然后,我们看到遥测技术对于全天候工作的企业应用至关重要,因为它为您提供了在长时间内的行为概述,并且您可以设置警报以响应紧急情况。
最后,我们还查看了 ASP.NET Core 中可用的不同日志记录和诊断选项,以帮助我们对问题进行排序。
在下一章中,我们将看到如何对前面章节中讨论的所有特性执行单元测试。
问题
您现在应该能够回答以下问题:
- 什么是事件计数器?
- 遥测的好处是什么?
- 我们如何过滤日志记录?
- 什么是健康检查?
- 中间件在日志记录中是如何有用的?
- 什么是榆树?
- 与普通日志相比,诊断有哪些好处?
十三、理解测试如何工作
在前几章中,我们介绍了如何在 ASP.NET Core 中构建内容。我们知道,我们应该在测试我们的应用之前,我们认为他们完成了。发生的情况是,应用不断发展,过去在某个时间点工作的东西现在可能不再工作了。因此,为了确保这些应用不会在我们身上失败,我们设置了可以自动运行的测试,并检查是否一切都正常工作。
本章将介绍以下主题:
- 单元测试原则
- 使用 xUnit、NUnit 和 MSTest
- 模拟对象
- 断言
- 用户界面测试
- 进行集成测试
在本章中,我们将了解执行这些测试的两种方法,以及它们如何帮助我们确保编写集成良好的代码。
技术要求
为了实现本章中介绍的示例,您需要.NET Core 3 SDK 和文本编辑器。当然,VisualStudio2019(任何版本)满足所有要求,但您也可以使用 VisualStudio 代码。
开始单元测试
单元测试并不新鲜。从本质上讲,单元测试旨在单独测试系统的一个功能,以证明它能够正常工作。F.I.R.S.T 单元测试原则规定单元测试应如下所示:
- 快速:他们应该快速执行,这意味着他们不应该执行任何复杂或冗长的操作。
- 隔离/独立:单元测试不应依赖于其他系统,应提供独立于任何特定上下文的结果。
- 可重复:如果在实现上没有任何更改,则无论何时执行单元测试,单元测试都应产生相同的结果。
- 自我验证:应该是自给自足的,即不需要任何人工检查或分析。
- 彻底/及时:他们应该涵盖所有重要的东西,即使 100%的代码不需要。
简而言之,单元测试应该运行得很快,这样我们就不必等待很长时间的结果,并且应该进行编码,这样就可以测试基本特性,而不依赖于外部变量。此外,单元测试不应该产生任何副作用,并且应该能够重复它们,并始终获得相同的结果。
有些人甚至主张在实际代码之前就开始实现单元测试。这样做的好处是使代码可测试——毕竟,它的设计考虑到了测试,一旦我们实现了它,我们就已经有了单元测试。这被称为测试驱动开发(TDD。虽然我不是 TDD 的铁杆后卫,但我看到了它的优势。
使用 TDD 的开发通常会经历一个称为红绿重构的循环,这意味着测试首先是红色的(意味着测试失败),然后是绿色的(通过),只有到那时,当一切正常工作时,我们才需要重构代码来改进它。有关 TDD 的更多信息,请访问https://technologyconversations.com/2014/09/30/test-driven-development-tdd 。
我们通常依赖单元测试框架来帮助我们执行这些测试并获得结果。.NET Core 有几种框架,包括:
- MSTest:这是微软自己的测试框架;它是开源的,可在上获得 https://github.com/Microsoft/testfx 。
- xUnit:一个甚至被微软使用的流行框架,可在上找到 https://xunit.github.io 。
- NUnit:最古老的单元测试框架之一,从 Java 的JUnit移植而来,可在上获得 http://nunit.org 。
这些都是开源的,它们的特性是相似的。您可以在找到三个框架的良好比较 https://xunit.github.io/docs/comparisons.html 。
如果您更喜欢从控制台而不是 Visual Studio 启动项目,dotnet有 MSTest、NUnit 和 xUnit 的模板,请选择一个:
dotnet new mstest
dotnet new xunit
dotnet new nunit
现在让我们来掌握代码。
编写单元测试
在这里,我们将看到如何将一些最流行的单元测试框架与.NET Core 结合使用。这将不是对框架的深入介绍,只是让您开始学习的基础知识。
单元测试是.NETCore 中的一流公民,有自己的项目类型。基本上,单元测试项目使用Microsoft.NET.SdkSDK,但必须引用Microsoft.NET.Test.Sdk。正如我们将看到的,dotnet工具了解这些项目,并为它们提供了特殊的选项。
首先,我们需要使用一个受支持的框架创建一个单元测试项目。
单元测试框架
有很多单元测试框架,但我选择了最常用的,包括微软。
MSTest
MSTest 是微软自己的测试框架,最近开源。要使用 MSTest,您需要添加对以下 NuGet 包的引用:
-
MSTest.TestFramework -
MSTest.TestAdapter -
Microsoft.NET.Test.Sdk
第一个参考是框架本身,第二个是允许 VisualStudio 与 MSTest 交互的参考;是的,所有这些框架都与 VisualStudio 很好地集成!
Visual Studio 为 MSTest 项目提供了一个模板项目,但您也可以使用dotnet创建一个模板项目:
dotnet new mstest
添加对 web app 项目的引用,并创建一个名为ControllerTests的类,其内容如下:
[TestClass]
public class ControllerTests
{
[TestMethod]
public void CanExecuteIndex()
{
var controller = new HomeController();
var result = controller.Index();
Assert.IsNotNull(result);
Assert.IsInstanceOfType(result, typeof(ViewResult));
}
}
这是一个简单的单元测试,将检查在HomeController类上调用Index方法的结果是否为null。
注意CanExecuteIndex方法中的[TestMethod]属性;这表示此方法包含单元测试,并由 Visual Studio 的测试资源管理器功能捕获:

如果满足以下条件,Visual Studio 将能够找到任何测试方法:
- 它是在非抽象类中声明的。
- 该类用一个
[TestClass]属性修饰。 - 这是公开的。
- 它具有
[TestMethod]或[DataRow]属性(稍后将对此进行详细介绍)。
从这里,您可以运行或调试您的测试;尝试在CanExecuteIndex方法中放置断点并调试测试。它由 VisualStudio 自动调用,并考虑在没有引发异常的情况下测试是否通过。控制器通常是很好的单元测试候选者,但您也应该对服务和业务对象进行单元测试。请记住,首先关注最关键的类,然后,如果您有资源、时间和开发人员,继续编写不太关键的代码。
除了[TestMethod]之外,您还可以使用一个或多个[DataRow]属性来装饰您的单元测试方法。这允许您传递参数的任意值,并返回由单元测试框架自动提供的方法的值:
[TestClass]
public class CalculatorTest
{
[DataTestMethod]
[DataRow(1, 2, 3)]
[DataRow(1, 1, 2)]
public void Calculate(int a, int b, int c)
{
Assert.AreEqual(c, a + b);
}
}
在本例中,我们可以看到我们提供了两组值-1、2和3以及1、1和2。这些都是自动测试的。
如果要在同一类中的任何测试之前或之后执行代码,可以应用以下内容:
[TestClass]
public class MyTests
{
[ClassInitialize]
public void ClassInitialize()
{
//runs before all the tests in the class
}
[ClassCleanuç]
public void ClassCleanup()
{
//runs after all the tests in the class
}
}
或者,对于在每次测试之前和之后运行的代码,应用以下内容:
[TestInitialize]
public void Initialize()
{
//runs before each test
}
[TestCleanup]
public void Cleanup()
{
//runs after each test
}
如果要忽略特定的测试方法,请应用以下方法:
[Ignore("Not implemented yet")]
public void SomeTest()
{
//will be ignored
}
Assert类提供了许多在单元测试中使用的实用方法:
AreEqual:要比较的项目相同。AreNotEqual:要比较的项目不相同。AreNotSame:要比较的项目没有得到相同的参考。AreSame:要比较的项目具有相同的参考。Equals:项目相等。Fail:断言失败。Inconclusive:该断言没有结论性。IsFalse:该条件预计为假。IsInstanceOfType:实例应为给定类型。IsNotInstanceOfType:实例不应为给定类型。
Please refer to the MSTest documentation at https://docs.microsoft.com/en-us/visualstudio/test/using-microsoft-visualstudio-testtools-unittesting-members-in-unit-tests?view=vs-2019 for more information.
接下来,我们有 NUnit。
单元测试
NUnit 是最古老的单元测试框架之一。要在代码中使用它,您需要添加以下 NuGet 包作为引用:
-
nunit -
NUnit3TestAdapter -
Microsoft.NET.Test.Sdk
同样,第一个是框架本身,第二个是与 VisualStudio 的集成。要创建 NUnit 项目,除了使用 Visual Studio 模板外,还可以使用dotnet创建一个 NUnit 项目:
dotnet new nunit
添加对 web 应用项目的引用,并将此类类添加到名为ControllerTests.cs的文件中:
[TestFixture]
public class ControllerTests
{
[Test]
public void CanExecuteIndex()
{
var controller = new HomeController();
var result = controller.Index();
Assert.AreNotEqual(null, result);
}
}
Visual Studio 能够自动查找单元测试,前提是满足以下条件:
- 它们在非抽象类中声明。
- 该类用一个
[TestFixture]属性修饰。 - 它们是公开的、非抽象的。
- 它们具有
[Test]或[TestCase]属性。
[TestCase]允许我们自动传递多个参数,以及预期的返回值(可选):
public class CalculatorTest
{
[TestCase(1, 1, ExpectedResult = 2)]
public int Add(int x, int y)
{
return x + y;
}
}
注意,在这个示例中,我们甚至不需要指定断言,如果您指定了ExpectedResult,那么断言将自动推断出来。
现在,如果您希望在代码中的任何测试之前运行某些内容,则需要在代码中包含以下内容:
[SetUpFixture]
public class InitializeTests
{
[OneTimeSetUp]
public void SetUpOnce()
{
//run before all tests have started
}
[OneTimeTearDown]
public void TearDownOnce()
{
//run after all tests have finished
}
}
类和方法的名称是不相关的,但是类需要是公共的,并且有一个公共的无参数构造函数,方法也需要是公共的和非抽象的(可以是静态的或实例的)。请注意类和SetUp(在前面运行)和TearDown(在后面运行)方法上的属性。并非所有这些都需要提供,只有一个。
同样,如果希望在每次测试之前运行代码,则需要将方法标记为:
[SetUp]
public void BeforeTest()
{
//runs before every test
}
两者的区别在于标记为[SetUp]的方法在每次测试之前运行,而[OneTimeSetUp]和[OneTimeTearDown]只在每个测试序列(所有测试)中运行一次。
如果出于某种原因(例如,测试失败或尚未完成)希望忽略测试,则可以使用另一个属性对其进行标记:
[Ignored("Not implemented yet")]
public void TestSomething()
{
//will be ignored
}
与其他框架一样,有一个名为Assert的类,它包含一些 helper 方法:
-
IsFalse:给定条件为假。 -
IsInstanceOf:传递的实例是给定类型的实例。 -
IsNaN:传递的表达式不是数字。 -
IsNotAssignableFrom:传递的实例不可从给定类型分配。 -
IsNotEmpty:集合不为空。 -
IsNotInstanceOf:传递的实例不是给定类型的实例。 -
IsNotNull:实例不是null。 -
IsNull:实例为null。 -
IsTrue:条件为真。
For more information, please consult the NUnit documentation at https://github.com/nunit/docs/wiki/NUnit-Documentation.
接下来是 xUnit。
单元测试
为了使用 xUnit,您需要添加几个 NuGet 包:
xunitxunit.runner.visualstudioMicrosoft.NET.Test.Sdk
第一个是框架本身,另外两个是 VisualStudio 集成所必需的。VisualStudio2019 甚至提供了 xUnit 测试项目模板,这甚至更好!
让我们创建一个单元测试项目;因为我们将以 ASP.NET Core 功能和.NET Core 应用为目标,所以我们需要创建一个也以.NET Core 应用为目标的单元测试项目-netcoreapp3.0。如前所述,您可以通过 Visual Studio 或使用dotnet工具来创建模板项目:
dotnet new xunit
在这个项目中,我们向 web 应用添加一个引用,并创建一个类。我们叫它ControllerTests。在这个类中,我们添加了以下代码:
public class ControllerTests
{
[Fact]
public void CanExecuteIndex()
{
var controller = new HomeController();
var result = controller.Index();
Assert.NotNull(result);
}
}
这是一个非常简单的测试。我们正在创建一个HomeController实例,执行其Index方法,并检查是否未引发异常(隐式,否则测试将失败),以及其结果是否不是null。
Unlike other frameworks, with xUnit, you do not need to decorate a class that contains unit tests.
Visual Studio 会自动发现单元测试,并在测试资源管理器窗口中显示它们,前提是满足以下条件:
- 它们在非抽象类中声明。
- 它们是公开的。
- 它们具有
[Fact]或[Theory]属性。
[Theory]更有趣,因为您可以为您的测试方法提供参数,xUnit 将负责调用这些参数!你自己看看:
[Theory]
[InlineData(1, 2, 3)]
[InlineData(0, 10, 10)]
public void Add(int x, int y, int z)
{
Assert.Equals(x + y, z);
}
这个例子有点简单,但我想你明白了![InlineData]应具有与其声明的方法一样多的参数。因为我们有两个[InlineData]属性,所以我们有两个数据集,所以对于其中一个[InlineData]属性中的每个值,该方法将被调用两次。
或者,如果您想测试动作方法模型,可以使用以下方法:
var controller = new ShoppingController();
var result = controller.ShoppingBag();
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsType<ShoppingBag>(viewResult.ViewData.Model);
您可以有任意多个测试方法,并且可以从 VisualStudioTestExplorer 窗口运行一个或多个。您的每个方法都应该负责测试一个特性,所以请确保不要忘记这一点!通常,单元测试是根据排列 Act Assert(AAA)设置的,这意味着我们首先设置(排列)对象,然后调用它们上的一些代码(Act),然后检查其结果(Assert)。一定要记住这个助记符!
如果您进行单元测试的类实现了IDisposable,那么在所有测试结束时将自动调用其Dispose方法。当然,类的构造函数也将运行,因此它需要是公共的,并且没有参数。
如果您让您的测试类实现IClassFixture<T>,xUnit 将期望它包含一个公共构造函数,该构造函数接受T的实例(因此,该实例必须是公共的可实例化类型),并将其实例传递给实现相同接口的所有单元测试类:
public class MyTests : IClassFixture<SharedData>
{
private readonly SharedData _data;
public MyTests(SharedData data)
{
this._data = data;
}
}
最后,如果希望忽略单元测试,只需在[Fact]或[Theory]属性上设置Skip属性:
[Fact(Skip = "Not implemented yet")]
public void TestSomething()
{
//will be ignored
}
xUnitAssert类中有几种实用程序方法,如果不满足条件,它们将引发异常:
-
All:集合中的所有项目都符合给定条件。 -
Collection:集合中的所有项目都符合所有给定条件。 -
Contains:集合包含一个给定的项目。 -
DoesNotContain:集合不包含给定项。 -
DoesNotMatch:字符串与给定的正则表达式不匹配。 -
Empty:集合为空。 -
EndsWith:字符串以一些内容结尾。 -
Equal:两个集合相等(包含完全相同的元素)。 -
Equals:两项相等。 -
False:表示为假。 -
InRange:可比值在一定范围内。 -
IsAssignableFrom:对象可从给定类型进行赋值。 -
IsNotType:对象不是给定类型。 -
IsType:对象为给定类型。 -
Matches:字符串与给定的正则表达式匹配。 -
NotEmpty:集合不为空。 -
NotEqual:两个对象不相等。 -
NotInRange:可比值不在范围内。 -
NotNull:该值不是null。 -
NotSame:两个参照物不是同一个对象。 -
NotStrictEqual:使用默认比较器(Object.Equals验证两个对象是否不相等。 -
Null:检查值是否为null。 -
ProperSubset:验证一个集合是否是另一个集合的适当子集(包含)。 -
ProperSuperset:验证一个集合是否是(包含)另一个集合的正确超集。 -
PropertyChanged/PropertyChangedAsync:验证属性是否已更改。 -
Raises/RaisesAsync:验证某个操作是否引发事件。 -
RaisesAny/RaisesAnyAsync:验证某个操作是否引发给定事件之一。 -
Same:两个引用指向同一个对象。 -
Single:集合包含且仅包含一项。 -
StartsWith:验证一个字符串是否以另一个字符串开头。 -
StrictEqual:使用默认比较器(Object.Equals验证两个对象是否相等。 -
Subset:验证一个集合是否是另一个集合的子集(包含)。 -
Superset:验证一个集合是否是另一个集合的超集(包含)。 -
Throws/ThrowsAsync:验证操作是否引发异常。 -
ThrowsAny/ThrowsAnyAsync:验证某个操作是否引发给定的异常之一。 -
True:这句话是真的。
本质上,所有这些方法都是True的变体;您想断言一个条件是否为真。在单元测试方法中不要有太多断言;确保只测试 essentials,例如,检查方法在测试中是否返回非空或非 null 集合,并让其他测试检查返回值的正确性。如果要测试不同的场景或返回值,请创建另一个单元测试。
For more information, please consult the xUnit documentation at https://xunit.net/#documentation.
现在让我们看看如何使用 xUnit 准备单元测试。
测试设置
本章中的示例都将使用 xUnit 作为单元测试框架。
注入依赖项
它可能并不总是简单的;例如,要测试的类可能包含依赖项。到目前为止,将依赖项注入控制器的最佳方法是通过其控制器。以下是执行日志记录的控制器示例:
ILogger<HomeController> logger = ...;
var controller = new HomeController(logger);
幸运的是,HttpContext的RequestServices属性本身是可设置的,这意味着您可以使用所需的服务构建自己的实例。检查以下代码:
var services = new ServiceCollection();
services.AddSingleton<IMyService>(new MyServiceImplementation());
var serviceProvider = services.BuildServiceProvider();
controller.HttpContext.RequestServices = serviceProvider;
如果您的代码依赖于当前正在进行身份验证或拥有某些声明的用户,则需要设置一个HttpContext对象,您可以这样做:
controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext
{
User = new ClaimsPrincipal(new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, "username")
}))
}
};
这样,在控制器内部,HttpContext和User属性将被正确初始化。在DefaultHttpContext类的构造函数中,还可以传递一组特性(即HttpContext.Features集合):
var features = new FeatureCollection();
features.Set<IMyFeature>(new MyFeatureImplementation());
var ctx = new DefaultHttpContext(features);
通过使用自定义要素集合,可以为许多要素注入值,例如:
Sessions:ISessionFeatureCookies:IRequestCookiesFeature、IResponseCookiesFeatureRequest:IHttpRequestFeatureResponse:IResponseCookiesFeatureConnections:IHttpConnectionFeatureForm:IFormFeature
通过在 features 集合中提供您自己的实现,或者通过将值分配给现有的实现,您可以为测试注入值,以便模拟真实场景。例如,假设您的控制器需要特定的 cookie:
var cookies = new RequestCookieCollection(new Dictionary<string, string> { { "username", "dummy" } });
var features = new FeatureCollection();
features.Set<IRequestCookiesFeature>(new RequestCookiesFeature(cookies));
var context = new DefaultHttpContext(features);
RequestCookieCollection过去是公开的,但现在是内部的,这意味着要模拟 cookies,我们需要自己实现它们。以下是最简单的实现:
class RequestCookieCollection : IRequestCookieCollection
{
private readonly Dictionary<string, string> _cookies;
public RequestCookieCollection(Dictionary<string, string> cookies)
{
this._cookies = cookies;
}
public string this[string key] => _cookies[key];
public int Count => _cookies.Count;
public ICollection<string> Keys => _cookies.Keys;
public bool ContainsKey(string key)
{
return _cookies.ContainsKey(key);
}
public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
{
return _cookies.GetEnumerator();
}
public bool TryGetValue(string key, out string value)
{
return _cookies.TryGetValue(key, out value);
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
现在,您应该注意,您不能更改ControllerBase上的HttpContext对象—它是只读的。然而,事实证明它实际上来自ControllerContext属性,该属性本身是可设置的。以下是一个完整的示例:
var request = new Dictionary<string, StringValues>
{
{ "email", "rjperes@hotmail.com" },
{ "name", "Ricardo Peres" }
};
var formCollection = new FormCollection(request);
var form = new FormFeature(formCollection);
var features = new FeatureCollection();
features.Set<IFormFeature>(form);
var context = new DefaultHttpContext(features);
var controller = new HomeController();
controller.ControllerContext = new ControllerContext { HttpContext = context };
此示例允许我们设置表单请求的内容,以便可以在控制器内部的单元测试中访问它们,如下所示:
var email = this.Request.Form["email"];
为此,我们必须创建一个表单集合(IFormCollection和一个功能(IFormFeature),使用该功能构建一个 HTTP 上下文(HttpContext),用 HTTP 上下文分配一个控制器上下文(ControllerContext),并将其分配给我们想要测试的控制器(HomeController。这样,它的所有内部属性-HttpContext和Request都将具有我们作为请求传递的伪值。
依赖性的挑战之一是,因为我们执行的是系统的有限子集,所以可能不容易获得正常运行的对象;我们可能需要用替代品来取代它们。我们现在来看看如何解决这个问题。
嘲笑
模拟、伪造和存根是类似的概念,本质上意味着一个对象被另一个模仿其行为的对象替代。我们为什么要这样做?好的,因为我们是孤立地测试我们的代码,并且我们假设第三方代码按照广告的方式工作,所以我们不关心它,所以我们可以用假人替换这些其他依赖项。
For a comparison of these terms, please refer to https://blog.pragmatists.com/test-doubles-fakes-mocks-and-stubs-1a7491dfa3da.
为此,我们使用模拟框架,对于.NET Core 也有一些可用的框架,例如:
让我们选择最小起订量。为了使用它,将MoqNuGet 包添加到您的项目中。然后,当需要模拟给定类型的功能时,可以创建该类型的模拟并设置其行为,如图所示:
//create the mock
var mock = new Mock<ILogger<HomeController>>();
//setup an implementation for the Log method
mock.Setup(x => x.Log(LogLevel.Critical, new EventId(), "", null, null));
//get the mock
ILogger<HomeController> logger = mock.Object;
//call the mocked method with some parameters
logger.Log(LogLevel.Critical, new EventId(2), "Hello, Moq!", null, null);
通过传递一个表达式来设置方法,该表达式由具有适当参数类型的方法或属性调用组成,而不考虑其实际值。您可以将模拟对象作为服务或控制器的依赖项传递,运行测试,然后确保调用了模拟方法:
mock.Verify(x => x.Log(LogLevel.Critical, new EventId(), "", null, null));
也可以设置响应对象,例如,如果我们正在模拟HttpContext:
var mock = new Mock<HttpContext>();
mock.Setup(x => x.User).Returns(new ClaimsPrincipal(new ClaimsIdentity(new[] { new
Claim(ClaimTypes.Name, "username"), new Claim(ClaimTypes
.Role, "Admin") }, "Cookies")));
var context = mock.Object;
var user = context.User;
Assert.NotNull(user);
Assert.True(user.Identity.IsAuthenticated);
Assert.True(user.HasClaim(ClaimTypes.Name, "username"));
在这里,您可以看到,我们正在为调用User属性提供返回值,并且我们正在返回一个预构建的ClaimsPrincipal对象,其中包含所有的铃铛和哨子。
当然,Moq 还有很多,但我认为这应该足以让你开始。
断言
如果抛出异常,单元测试将失败。因此,您可以推出自己的异常抛出代码,也可以依赖单元测试框架提供的断言方法之一,该方法实际上抛出异常本身;它们都提供了类似的方法。
For more complex scenarios, it may be useful to use an assertion library. FluentAssertions is one such library that happens to work nicely with .NET Core. Get it from NuGet as FluentAssertions and from GitHub at https://github.com/fluentassertions/fluentassertions.
使用代码,您可以有如下断言:
int x = GetResult();
x
.Should()
.BeGreaterOrEqualTo(100)
.And
.BeLessOrEqualTo(1000)
.And
.NotBe(150);
如您所见,您可以组合许多与同一对象类型相关的表达式;数值有比较,字符串有匹配,等等。您还可以加入属性更改检测:
svc.ShouldRaisePropertyChangeFor(x => x.SomeProperty);
此外,还可以添加执行时间:
svc
.ExecutionTimeOf(s => s.LengthyMethod())
.ShouldNotExceed(500.Milliseconds());
There's a lot more to it, so I advise you to have a look at the documentation, available at http://fluentassertions.com.
接下来是用户界面。
用户界面
到目前为止,我们看到的单元测试是用于测试 API 的,比如业务方法和逻辑。但是,也可以测试用户界面。让我们看看使用硒对有何帮助。Selenium 是一个可移植的 web 应用软件测试框架,其.NET 端口为Selenium.WebDriver。除此之外,我们还需要以下方面:
Selenium.Chrome.WebDriver:对于铬Selenium.Firefox.WebDriver:针对 FirefoxSelenium.WebDriver.MicrosoftWebDriver:用于 Internet Explorer 和 Edge
我们首先创建一个驱动程序:
using (var driver = (IWebDriver) new ChromeDriver(Environment.CurrentDirectory))
{
//...
}
注意Environment.CurrentDirectory参数;这指定了驱动程序在 Firefox 中可以找到chromedriver.exe文件-geckodriver.exe的路径,在 Internet Explorer/Edge 中可以找到MicrosoftWebDriver.exe(当然是 Windows!)。这些可执行文件由 NuGet 包自动添加。此外,如果不处理驱动程序,单元测试完成后窗口将保持打开状态。您也可以随时拨打Quit关闭浏览器。
现在,我们可以导航到任何页面:
driver
.Navigate()
.GoToUrl("http://www.google.com");
我们可以从其名称中找到一个元素:
var elm = driver.FindElement(By.Name("q"));
除了名称,我们还可以通过以下参数进行搜索:
-
ID:
By.Id -
CSS 类:
By.ClassName -
CSS 选择器:
By.CssSelector -
标签名称:
By.TagName -
链接文本:
By.LinkText -
部分链接文本:
By.PartialLinkText -
XPath:
By.XPath
找到元素后,我们可以访问其属性:
var attr = elm.GetAttribute("class");
var css = elm.GetCssValue("display");
var prop = elm.GetProperty("enabled");
我们还可以发送击键:
elm.SendKeys("asp.net");
我们也可以单击以下按钮,而不是按键:
var btn = driver.FindElement(By.Name("btnK"));
btn.Click();
正如我们所知,页面加载可能需要一些时间,因此我们可以配置默认时间以等待加载,可能在我们执行GoToUrl之前:
var timeouts = driver.Manage().Timeouts();
timeouts.ImplicitWait = TimeSpan.FromSeconds(1);
timeouts.PageLoad = TimeSpan.FromSeconds(5);
ImplicitWait是硒在寻找元素之前等待的时间;我相信你能猜出PageLoad的作用。
如果我们需要等待一段时间,例如直到 AJAX 请求完成,我们可以这样做:
var waitForElement = new WebDriverWait(driver, TimeSpan.FromSeconds(5));
var logo = waitForElement.Until(ExpectedConditions.ElementIsVisible(By.Id("hplogo")));
传递给ExpectedConditions的条件可以是以下条件之一:
-
AlertIsPresent -
AlertState -
ElementExists -
ElementIsVisible -
ElementSelectionStateToBe -
ElementToBeClickable -
ElementToBeSelected -
FrameToBeAvailableAndSwitchToIt -
InvisibilityOfElementLocated -
InvisibilityOfElementWithText -
PresenceOfAllElementsLocatedBy -
StalenessOf -
TextToBePresentInElement -
TextToBePresentInElementLocated -
TextToBePresentInElementValue -
TitleContains -
TitleIs -
UrlContains -
UrlMatches -
UrlToBe -
VisibilityOfAllElementsLocatedBy
正如你所看到的,你可以使用大量的条件。如果在计时器到期前不满足该条件,Until返回的值为null。
希望有了它,您能够编写单元测试来检查站点的用户界面。当然,它们需要指向一个活动环境,因此在这种情况下,测试不会是自包含的。当我们讨论集成测试时,我们将看到如何克服这一问题。
For more information about Selenium, please refer to https://selenium.dev.
这就是我们将要讨论的关于用户界面的内容。现在让我们看看如何从命令行运行测试。
使用命令行
dotnet命令行工具是.NET Core 开发的瑞士军刀,因此,它完全支持运行单元测试。如果您在有单元测试的项目文件夹中,只需运行dotnet test即可:
C:\Users\Projects\MyApp\UnitTests>dotnet test
Build started, please wait...
Build completed.
Test run for C:\Users\Projects\MyApp\UnitTests\bin\Debug\netcoreapp3.0\UnitTests.dll(.NETCoreApp,Version=v3.0)
Microsoft (R) Test Execution Command Line Tool Version 16.3.0
Copyright (c) Microsoft Corporation. All rights reserved.
Starting test execution, please wait...
[xUnit.net 00:00:00.3769248] Discovering: UnitTests
[xUnit.net 00:00:00.4364853] Discovered: UnitTests
[xUnit.net 00:00:00.4720996] Starting: UnitTests
[xUnit.net 00:00:00.5778764] Finished: UnitTests
Total tests: 10\. Passed: 10\. Failed: 0\. Skipped: 0.
Test Run Successful.
Test execution time: 1,0031 Seconds
Since the project is set up to use xUnit (the xunit.runner.visualstudio package), dotnet is happy to use it automatically.
如果您希望查看已定义的所有测试,请改为运行dotnet test --list-tests:
Test run for C:\Users\Projects\MyApp\UnitTests\bin\Debug\netcoreapp3.0\UnitTests.dll(.NETCoreApp,Version=v3.0)
Microsoft (R) Test Execution Command Line Tool Version 16.3.0
Copyright (c) Microsoft Corporation. All rights reserved.
The following Tests are available:
CanExecuteIndex
Add(1, 2, 3)
Add(0, 10, 10)
现在让我们看看单元测试的一些局限性。
单元测试的局限性
尽管单元测试很有用,但请记住它们基本上用于回归测试,并且它们有一些局限性:
- 它们通常不涉及用户界面,尽管存在一些可以这样做的框架(在撰写本文时,没有针对.NETCore 的框架)。
- 无法测试某些 ASP.NET 功能,例如筛选器或视图。
- 外部系统是模拟的,因此您只能有限地查看系统的一小部分。
我们之前看到了如何在用户界面上执行测试。在下一节中,我们将看到如何克服最后两个限制。
进行集成测试
在这里,我们将不仅仅把集成测试看作是一种执行带有一些输入参数的测试方法并断言结果或是否抛出异常的测试,还将其看作是执行真实代码的测试。集成测试将不同的代码模块测试在一起,而不仅仅是单个模块。
作为 ASP.NET Core 的一部分,微软已经推出了Microsoft.AspNetCore.Mvc.TestingNuGet 软件包。本质上,它允许我们托管一个 web 应用,这样我们就可以像在现实服务器中一样在它上执行测试,当然,这就消除了性能和可伸缩性问题。
在单元测试项目中,创建如下类(同样,我们使用的是 xUnit):
public class IntegrationTests : IClassFixture<WebApplicationFactory<Startup>>
{
private readonly WebApplicationFactory<Startup> _factory;
public IntegrationTests(WebApplicationFactory<Startup> factory)
{
this._factory = factory;
}
[Theory]
[InlineData("/")]
public async Task CanCallHome(string url)
{
//Arrange
var client = this._factory.CreateClient();
//Act
var response = await client.GetAsync(url);
//Assert
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("Welcome", content);
}
}
那么,我们这里有什么?如果您还记得在xUnit部分中,我们有一个单元测试类,在该类中,我们使用用于适当应用的相同Startup类注入WebApplicationFactory。然后,我们发出一个 URL 请求,该 URL 作为内联数据注入。在我们得到响应后,我们验证它的状态代码(EnsureSuccessStatusCode检查我们没有4xx或5xx,并且我们实际上查看了返回的内容。请注意,在这里,我们不使用IActionResults或类似工具,而是使用 HTTP 响应。因为WebApplicationFactory使用约定,所以它知道从何处加载配置文件和程序集。
这种方法的优点是,我们在一个类似 web 的场景中真正测试我们的控制器(及其所有服务),这意味着将运行过滤器、检查身份验证、像往常一样加载配置等等。这与单元测试配合得很好,从本例中可以看出。
Notice that in this example, the unit test method is asynchronous; this is supported by xUnit and the other unit test frameworks.
您会注意到,我们将响应读取为字符串(response.Content.ReadAsStringAsync。这意味着我们得到的响应是纯 HTML,这可能是我们想要的,也可能不是。我们可以使用像AngleSharp这样的库来解析这个 HTML 并从中构建 DOM。然后,您可以使用类似于浏览器上的方法进行查询。AngleSharp 作为 NuGet 套装提供。
最后一句话,您可能需要调整WebApplicationFactory类以添加一些额外的配置或行为。这只是从它继承并重写其虚拟方法的问题。例如,假设要禁用在依赖项注入框架上注册的所有后台服务:
class MyCustomWebApplicationFactory : WebApplicationFactory<Startup>
{
protected override IHostBuilder CreateHostBuilder()
{
return base
.CreateHostBuilder()
.ConfigureServices(services =>
{
services.RemoveAll<IHostedService>();
});
}
}
如您所见,在运行基类的CreateHostBuilder参数后,我们将删除IHostBuilder的所有注册实例。我们还可以将 start-up 类更改为其他类,或者执行任何其他类型的定制。只是在IClassFixture<T>上指定这个类而不是WebApplicationFactory<Startup>的问题。
我们已经了解了如何使用单元测试框架来实际执行集成测试。当然,请注意,以自动化的方式执行此操作将导致您的测试运行更长时间,并可能产生副作用,这与单元测试的理念背道而驰。
总结
你绝对应该对你的应用进行单元测试。无论您是否严格遵循 TDD,它们都非常有用,特别是对于回归测试。大多数持续集成工具完全支持运行单元测试。只是不要试图掩盖一切;关注应用的关键部分,如果时间允许,然后继续其他部分。认为我们将在大多数项目中拥有 100%的覆盖率是不合理的,因此我们需要做出决策。模拟框架在这里起着至关重要的作用,因为它们允许我们很好地模拟第三方服务。
正如我们在这里看到的,自动化集成测试允许我们测试单元测试中不可用的特性,这些特性涵盖了我们需要的其他部分。
本章介绍了测试我们的应用的方法,可以是单独测试应用的一部分,也可以是整个系统。单元测试很有用,因为它们可以确保我们的应用仍然按照它应该的方式工作,即使我们正在对它进行更改。
在下一章中,我们将讨论使用 ASP.NET Core 进行客户端开发。
问题
因此,在本章结束时,您应该知道以下问题的答案:
- 什么是比较流行的单元测试框架?
- 嘲笑的好处是什么?
- 单元测试和集成测试之间的区别是什么?
- 什么是 TDD?
- 单元测试有哪些限制?
- 我们如何将数据自动传递给单元测试?
- 红绿重构是什么意思?
十四、客户端部署
尽管这本书是关于 ASP.NETCore 的,这是一个服务器端开发框架,但如今,没有客户端技术几乎什么也做不到。幸运的是,ASP.NET Core 还包括许多应用编程接口(API)和库,可以帮助我们使用第三方客户端库构建现代应用。这些应用包括单页应用(SPA),与需要从一个页面导航到另一个页面的旧式网站相比,这些应用提供了更加友好的用户体验。还有 TypeScript,一个 JavaScript 超集,可用于构建强类型、面向对象的代码,非常类似于用 C#编写的代码。
Visual Studio 还包括一些使我们的生活更轻松的功能,ASP.NET Core 引入了与 Node.js 的互操作性,这在以前是不可能的,包括用于节点包管理器(npm的内置包管理器)。例如,这包括从.NET 代码运行 npm 脚本的能力。
本章将介绍以下主题:
- 介绍客户端开发
- 使用图书馆管理员(利伯曼)
- 使用 Node.js
- 使用打字脚本
技术要求
为了实现本章介绍的示例,您需要.NET Core 3软件开发工具包(SDK和某种形式的文本编辑器。当然,VisualStudio2019(任何版本)满足所有要求,但您也可以使用 VisualStudio 代码。
源代码可以在这里从 GitHub 检索:https://github.com/PacktPublishing/Modern-Web-Development-with-ASP.NET-Core-3-Second-Edition 。
介绍客户端开发
客户端开发与服务器端开发相对应。在现代 web 应用中,一个应用离不开另一个应用。虽然这本书主要是关于 ASP.NET Core,一种服务器端技术,但我们很可能会使用 JavaScript 或级联样式表(CSS)。Visual Studio(以及 Visual Studio 代码)包含了一些使我们的生活更轻松的功能,ASP.NET Core 引入了与 Node.js 的互操作性,这在以前是不可能的,包括内置的包管理器(https://docs.microsoft.com/en-us/visualstudio/javascript/npm-package-management )。
让我们在下面几节中了解它的功能。
使用 LibMan
LibMan是微软推出的一款新的开源工具,用于管理客户端库。它由一个 VisualStudio 扩展和一个命令行工具组成。两者都从libman.json配置文件中读取信息。示例文件如下所示:
{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": [{
"library": "jquery@3.4.1",
"destination": "wwwroot/lib"
},
{
"library": "jquery-validation-unobtrusive@3.2.11",
"destination": "wwwroot/lib"
}]
}
正如您可以从前面的代码片段中看到的,在文件中,我们在特定版本中指定了一个或多个库(在本例中为jQuery和jQuery Validation Unobtrusive),并且,对于每个库,我们告诉库在哪里安装它(对于这两个库,wwwroot/lib)。在 Visual Studio 内部(仅适用于 Windows 的 Mac 不支持 LibMan),我们甚至可以完成库及其版本的代码,如以下屏幕截图所示:

然后,我们可以使用命令行工具检索这些库并按配置安装它们。命令行工具是一个dotnet全局工具,首先需要通过运行以下命令来安装:
dotnet tool install -g microsoft.web.librarymanager.cli
之后,从包含libman.json文件的文件夹中,我们只需恢复所有配置的库,如下所示:
libman restore
For additional information, please consult the Microsoft official documentation at https://devblogs.microsoft.com/aspnet/library-manager-client-side-content-manager-for-web-apps and the GitHub project page at https://github.com/aspnet/LibraryManager.
在了解了用于检索客户端库的新包管理器之后,现在是时候讨论一下 Node.js,服务器端 JavaScript 引擎,并了解如何从.NET Core 与之交互。
使用 Node.js
到现在为止,你们大多数人都会熟悉Node.js,它本质上是服务器端的 JavaScript。这是一个开源项目,至少目前为止,它使用 Chrome 的 V8 JavaScript 引擎在浏览器上下文之外运行 JavaScript。它已经变得非常流行,可以说是因为它使用了 JavaScript 语言(有些人可能不同意),但本质上是因为它的速度和通过 npm 提供的大量库,目前超过 55 万个。
You can find more information about Node.js and npm on their respective sites, https://nodejs.org and https://www.npmjs.com, and about the Visual Studio support for npm here: https://docs.microsoft.com/en-us/visualstudio/javascript/npm-package-management
您可以通过 VS installer 工具安装对 Visual Studio 的 Node.js 支持,但您还需要安装 Node.js 本身,您可以从获得该支持 https://nodejs.org 。您可以获得用于创建 Node.js 项目的 Visual Studio 模板,如以下屏幕截图所示:

您还可以将 Node.js 文件添加到 ASP.NET Core 项目中,但在添加package.json文件并引用某些包之前,Visual Studio 中没有明显的 npm explorer。npm节点出现在依赖项项目下,如以下屏幕截图所示:

Node.js 文件保存在wwwroot之外的node_modules文件夹中;这是因为这些文件通常不会提供给浏览器。您需要显式还原包,然后才能使用它们。
下一节将解释如何从.NET Core 代码中调用 Node.js 代码。
从.NET Core 调用节点
Steve Sanderson,来自 Knockout.js(http://knockoutjs.com/ 成名,几年前开始了一个名为NodeServices的宠物项目。它在几年前由 NuGet 以Microsoft.AspNetCore.NodeServices的形式提供,现在是 ASP.NET Core 生态系统的一部分。简而言之,它允许我们从 ASP.NET Core 调用 Node.js 代码。想想看,我们可以同时获得 ASP.NET Core 和 Node.js(以及 npm)的所有好处!
为了使用NodeServices,我们需要在ConfigureServices中注册其服务,如下所示:
services.AddNodeServices();
在此之后,我们可以将INodeServices的实例注入到我们的组件、控制器、视图组件、标记帮助器、中间件类等中。这个接口公开了一个方法InvokeAsync,我们可以使用它调用我们在本地安装的一个模块。一个例子可能是:
var result = await nodeServices.InvokeAsync<int>("Algebra.js", 10, 20);
然后,Algebra.js文件需要导出一个默认模块,如下所示:
module.exports = function(callback, a, b) {
var result = a + b;
callback(null, result);
};
NodeServices期望默认导出返回一个函数,该函数接受InvokeAsync隐式传递的回调和任意数量的参数。注意:这些参数应该是基元类型,但您可以传递一个JavaScript 对象符号(JSON)格式的对象,并让 Node.js 代码对其进行转换。您可以从 Node.js 代码中执行任何您想要的操作,包括引用其他模块。在默认export实例的末尾,您使用可选的错误参数和返回到.NET Core 的值调用隐式回调函数;如果传递错误,.NET 代码将引发异常。
还有另一种调用 Node.js 代码的方法,这通常很有用,这就是运行 npm 脚本。下一节将对此进行解释。
提供水疗档案
Microsoft.AspNetCore.SpaServices.ExtensionsNuGet 包中存在NodeServices的部分替换。请注意,Web 包中间件不再包含在 ASP.NET Core 中。我们必须使用开发服务器并启动 npm 脚本,以便为 SPA 项目提供文件服务,如下所示:
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
注意,UseSpaStaticFiles必须在UseEndpoints之前,而UseSpa必须在UseEndpoints之后。UseReactDevelopmentServer对反应确实没有特异性;它用于启动进程中的节点脚本。对于start脚本,您需要将其注册为package.json下的脚本-如下所示:
"scripts": { "start": "webpack-dev-server --config webpack.development.js --hot --inline",
如果需要代理请求(将请求转发到正在运行的服务器),则必须具有以下功能:
// Ensure that you start your server manually
spa.UseProxyToSpaDevelopmentServer("http://localhost:8088");
不要忘记先在ConfigureServices中注册所需的服务,执行以下代码:
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
.csproj文件中的配置取决于正在创建的项目类型;每个文件/项目的设置略有不同。让我们看看可以创建哪些类型的项目(模板)。
使用 SPA 模板
Microsoft 为许多流行的 JavaScript SPA 框架提供了模板,如下所示:
| 模板 | 框架 | 绰号 |
| Microsoft.AspNetCore.SpaTemplates | 奥雷利亚 | aurelia |
| Knockout.js | knockout |
| Vue.js | vue |
| Microsoft.DotNet.Web.Spa.ProjectTemplates | 有棱角的 | angular |
| React.js | react |
| React.js+Redux | reactredux |
您可以在看到模板的完整列表 https://dotnetnew.azurewebsites.net 。要安装模板,请使用以下代码:
dotnet new -i Microsoft.AspNetCore.SpaTemplates
dotnet new -i Microsoft.DotNet.Web.Spa.ProjectTemplates
您将获得列出的所有模板。然后,例如,运行以下代码:
mkdir AngularProject
cd AngularProject
dotnet new angular
dotnet restore
npm install
因此,您将使用 Angular 获得一个很好的项目框架,等待完成!
要将所有本地模板更新为最新版本,只需运行以下命令:
dotnet new --update-apply
在此之后,让我们继续了解 TypeScript 是如何工作的。
使用打字脚本
TypeScript 是一个 JavaScript 面向对象的超集。它是由 Microsoft 开发的一种开源语言,提供其他非脚本化、面向对象语言中存在的功能,如模块、类、接口、强类型、模板、不同的可见性级别和方法重载。
通过使用 TypeScript 编码,您可以获得这些语言的所有好处,但在代码传输(跨语言编译)后,您仍然可以获得您父亲的 JavaScript,尽管它是一个现代版本,您可以在客户端和服务器端(Node.js)中使用它。有关 TypeScript 的更多信息,请参见https://www.typescriptlang.org 并在从 GitHub 获取 https://github.com/Microsoft/TypeScript 。或者,如果你想先玩一下,你应该试试打字脚本:http://www.typescriptlang.org/play
Visual Studio 有两个扩展,为 Microsoft Visual Studio 构建的 TypeScript和为 Microsoft Visual Studio 构建的 TypeScript,都可以使用TypeScript SDK(安装 https://marketplace.visualstudio.com/items?itemName=TypeScriptTeam.typescript-331-vs2017,它可以帮助您创建 TypeScript 代码并将其转换为 JavaScript。您可以通过单击“添加新项”| Visual C#| ASP.NET Core | Web |脚本|类型脚本文件”将类型脚本文件添加到项目中。添加 TypeScript 内容并保存文件时,VisualStudio 会自动将其传输到相应的 JavaScript 文件。请记住,您不直接使用 TypeScript(.ts)文件,而是使用 JavaScript 文件(.js),如以下屏幕截图所示:

也可以自己创建一个 TypeScript 项目,但只针对 Node.js;在 ASP.NET 项目范围之外有一个 web 的 TypeScript 项目是没有意义的。
您可以使用默认模板从 Visual Studio 创建 Node.js 控制台应用 TypeScript 项目,如以下屏幕截图所示:

对于中型和大型项目,建议使用 TypeScript 而不是普通 JavaScript,因为它使事情更易于组织,并适当支持类型、接口、类、枚举等。此外,一般来说,它有助于防止错误,因为它具有强大的键入功能和更严格的检查。
如果您只想在Microsoft Build(MSBuild项目)上编译 TypeScript 文件,只需添加Microsoft.TypeScript.MSBuildNuGet 包并相应配置.csproj file,如下例:
<PropertyGroup>
<TypeScriptToolsVersion>3.7<TypeScriptToolsVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<TypeScriptRemoveComments>false</TypeScriptRemoveComments>
<TypeScriptSourceMap>true</TypeScriptSourceMap>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<TypeScriptRemoveComments>true</TypeScriptRemoveComments>
<TypeScriptSourceMap>false</TypeScriptSourceMap>
</PropertyGroup>
<Import
Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\
Microsoft.TypeScript.targets"
Condition="Exists('$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\
TypeScript\Microsoft.TypeScript.targets')" />
在本例中,我们首先定义目标 TypeScript 版本(3.7。另一个选项是跳过它或将其设置为Latest。然后,我们有两种配置,一种用于Debug,另一种用于Release。两者之间的区别在于优化一个删除注释而不创建源映射(Release,另一个则相反(Debug。有关所有选项的列表,请参阅https://www.typescriptlang.org/docs/handbook/compiler-options-in-msbuild.html 。
总结
您已经看到 Node.js 和 npm 变得越来越重要,甚至对于我们这些使用 ASP.NET 和 ASP.NET Core 的人来说,这是因为它有丰富的软件包。我们在本章中讨论的一些工具都依赖于它。因为您现在可以从 ASP.NET Core 调用 Node.js,所以您可以从它的许多可用包和蓬勃发展的社区中获益。即使你不是一个喜欢 JavaScript 的人,我也真诚地建议你尝试使用它。
确保在任何中大型项目中使用 TypeScript—任何比单个 JavaScript 文件大的文件—因为它有很多优点,可以帮助您提高生产率和速度。
在本章中,我们介绍了 VisualStudio 提供一流支持的一些客户端技术。我们没有详细讨论,因为这是一个巨大的话题,而且变化似乎非常快,但我给你们留下了一些线索,亲爱的读者,让你们自己去探索和发现更多。
问题
因此,在本章结束时,您应该知道以下问题的答案:
- TypeScript 的好处是什么?
- JavaScript 是否只在浏览器上运行?
- 什么是水疗?
- 利伯曼的目的是什么?
- dotnet SPA 框架的模板是否已硬编码?
- 我们如何从.NET Core 运行 JavaScript 代码?
- 列举一些具有 dotnet 模板的 SPA 框架。
十五、提升性能和可扩展性
本章讨论我们可以应用于 ASP.NET Core 应用的不同优化,以便它们执行得更快,能够处理更多的并发连接。我们将关注的性能和可伸缩性这两个概念是不同的,事实上,它们在某种程度上相互冲突。您必须应用正确的优化级别才能找到最佳点。
阅读本章后,您应该能够应用技巧,首先要了解应用中出现的错误或可以改进的地方,其次是如何改进。在接下来的章节中,我们将介绍一些可用的技术。
本章将介绍以下主题:
- 分析如何深入了解应用正在做什么
- 主机选择和调整主机以获得最佳性能
- 捆绑和最小化
- 使用异步操作
- 缓存
- 压缩响应
技术要求
为了实现本章中介绍的示例,您需要.NET Core 3 SDK 和文本编辑器。当然,VisualStudio2019(任何版本)满足所有要求,但您也可以使用 VisualStudio 代码。
开始
正如开尔文勋爵曾经说过的一句名言:“如果你不能测量它,你就无法改善它。”。考虑到这一点,我们需要衡量我们的应用,看看它的问题在哪里。有一些被称为分析器的应用可以为我们提供实现这一点的方法。让我们看看我们的一些选择。
微型剖面仪
一个开源分析器是MiniProfiler,可从获得 http://miniprofiler.com/dotnet/AspDotNetCore 和来自 NuGet 的MiniProfiler.AspNetCore.Mvc。还有其他软件包,如实体框架核心 SQL Server 提供程序的Microsoft.EntityFrameworkCore.SqlServer和Microsoft.EntityFrameworkCore.Sqlite for SQLite,您也应该添加这些软件包。
以下屏幕截图显示了控制台,以及有关请求和数据库调用的详细信息:

此屏幕显示加载页面后的一些指标,包括响应时间、加载 DOM 所用的时间以及执行 action 方法所用的时间。要使用 MiniProfiler,您需要注册其服务(ConfigureServices:
services
.AddMiniProfiler()
.AddEntityFramework();
添加中间件组件(Configure:
app.UseMiniProfiler()
然后,添加客户端 JavaScript 代码:
<mini-profiler position="@RenderPosition.Right" max-traces="5" color-scheme="ColorScheme.Auto" />
由于这是标记帮助器,您需要先注册它(_ViewImports.cshtml:
@addTagHelper *, MiniProfiler.AspNetCore.Mvc
还有其他选项,例如格式化 SQL 查询和着色,等等,所以我建议您看看 GitHub 上提供的示例应用。
堆叠前缀
Stackify Prefix不是开源产品,而是由知名的Stackify(维护的产品 https://stackify.com )。可从下载 https://stackify.com/prefix ,目前,NuGet 不提供。它提供了比其他两个更多的功能,因此可能值得一看:

此屏幕截图显示了调用操作方法(POST to order)的结果,并显示了在其中执行的 SQL。我们可以看到.NET 代码、数据库连接和SQL SELECT执行所花的时间。
现在让我们看看 ASP.NET Core 中可用的托管选项。
托管 ASP.NET Core
托管是用于运行 ASP.NET Core 应用的进程。在 ASP.NET Core 中,您有两种现成的托管选择:
- 红隼:跨平台主机,默认设置
- HTTP.sys(ASP.NET Core pre-2.x中的 WebListener:仅限 Windows 的主机
**如果您希望应用在不同的平台上运行,而不仅仅是在 Windows 上,那么 Kestrel 应该是您的选择,但是如果您只需要针对 Windows,那么 WebListener/HTTP.sys 可能会提供更好的性能,因为它使用本机 Windows 系统调用。你必须做出这个选择。默认情况下,VisualStudio 模板(或dotnet命令使用的模板)使用 Kestrel,这适用于大多数常见场景。让我们了解如何选择最适合我们的目标。
选择最佳主持人
你应该比较这两位主持人,看看他们在紧张的情况下表现如何。Kestrel 是默认的,包含在Microsoft.AspNetCore.Server.KestrelNuGet 包中。如果您想尝试 HTTP.sys,您需要添加对Microsoft.AspNetCore.Server.HttpSys包的引用。
Kestrel 是默认主机,但如果您希望明确说明,它如下所示:
public static IHostBuilder CreateWebHostBuilder(string[] args) =>
Host
.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(builder =>
{
builder
.ConfigureKestrel((KestrelServerOptions options) =>
{
//options go here
})
.UseStartup<Startup>();
});
要在 ASP.NET Core 3.x 中使用 HTTP.sys,则应使用以下命令:
public static IHostBuilder CreateWebHostBuilder(string[] args) =>
Host
.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(builder =>
{
builder
.UseHttpSys((HttpSysOptions options) =>
{
//options go here
})
.UseStartup<Startup>();
});
此示例显示如何启用 HTTP.sys 主机,以及可以在其中定义一些与性能相关的设置。
配置调整
Kestrel 和 HTTP.sys 这两个主机都支持对其某些参数进行调优。让我们看看其中的一些。
同时连接的最大数量
对于红隼来说,它看起来是这样的:
.ConfigureKestrel(options =>
{
options.Limits.MaxConcurrentConnections = null;
options.Limits.MaxConcurrentUpgradedConnections = null;
})
MaxConcurrentConnections指定可接受的最大连接数。如果设置为null,则没有限制,当然,系统资源耗尽除外。MaxConcurrentUpgradedConnections是可以从 HTTP 或 HTTPS 迁移到 WebSocket 的最大连接数(例如)。null是默认值,表示没有限制。
对该代码的解释如下:
MaxAccepts:相当于MaxConcurrentConnections。默认为0,表示没有限制。RequestQueueLimit:也可以在 HTTP.sys 中指定最大排队请求数。
对于 ASP.NET Core 3.x 中 WebListener 的替代品 HTTP.sys,类似于:
.UseHttpSys(options =>
{
options.MaxAccepts = 40;
options.MaxConnections = null;
options.RequestQueueLimit = 1000;
})
此代码为 HTTP.sys 主机设置一些常见的性能相关选项,如下表所示:
MaxAccepts指定并发接受的最大数量。MaxConnections是使用注册表中的机器全局设置的最大并发接受次数(默认为null。-1表示有无限多个连接。RequestQueueLimit是 HTTP.sys 可以排队的最大请求数。现在让我们看看限制是如何起作用的。
限制
与 HTTP.sys 类似,Kestrel 还允许设置一些限制,甚至比 HTTP.sys 多一点:
.ConfigureKestrel(options =>
{
options.Limits.MaxRequestBodySize = 30 * 1000 * 1000;
options.Limits.MaxRequestBufferSize = 1024 * 1024;
options.Limits.MaxRequestHeaderCount = 100;
options.Limits.MaxRequestHeadersTotalSize = 32 * 1024;
options.Limits.MaxRequestLineSize = 8 * 1024;
options.Limits.MaxResponseBufferSize = 64 * 1024;
options.Limits.MinRequestBodyDataRate.BytesPerSecond = 240;
options.Limits.MaxResponseDataRate.BytesPerSecond = 240
})
解释此代码很简单:
-
MaxRequestBodySize:请求正文允许的最大大小 -
MaxRequestBufferSize:请求缓冲区的大小 -
MaxRequestHeaderCount:请求头的最大数量 -
MaxRequestHeadersTotalSize:请求头的总可接受大小 -
MaxRequestLineSize:请求中的最大行数 -
MaxResponseBufferSize:响应缓冲区的大小 -
MinRequestBodyDataRate.BytesPerSecond:最大请求吞吐量 -
MaxResponseDataRate.BytesPerSecond:最大响应吞吐量
超时
每当一个应用正在等待一个外部事件等待一个完整的请求到达、一个表单被提交、一个连接被建立等等,它只能等待一段时间;这样就不会影响应用的全局功能。当它过去时,我们有一个超时,在此之后,应用要么放弃并失败,要么重新启动。Kestrel 允许指定若干超时:
.ConfigureKestrel(options =>
{
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);
})
对于正在设置的两个属性,以下是一些信息:
KeepAliveTimeoutkeep alive connections 中的客户端连接超时;0为默认值,表示无限期。RequestHeadersTimeout等待接收报头的时间;默认值也是0。
对于 HTTP.sys,属性如下所示:
DrainEntityBodykeep alive connections 中允许读取所有请求主体的时间。EntityBody是每个个体到达的最长时间。HeaderWait是解析所有请求头的最长时间。IdleConnection是空闲连接关闭前的时间。MinSendBytesPerSecond是以字节/秒为单位的最小发送速率。RequestQueue是允许排队请求留在队列中的时间。
下面是一个示例代码,演示了这些选项:
.UseHttpSys(options =>
{
options.Timeouts.DrainEntityBody = TimeSpan.FromSeconds(0);
options.EntityBody = TimeSpan.FromSeconds(0);
options.HeaderWait = TimeSpan.FromSeconds(0);
options.IdleConnection = TimeSpan.FromSeconds(0);
options.MinSendBytesPerSecond = 0;
options.RequestQueue = TimeSpan.FromSeconds(0);
})
在本节中,我们探讨了 ASP.NET Core 主机中的一些调整,这些调整可以提高资源利用率,并最终提高性能和可扩展性。在下一节中,我们将研究改进静态资源传输的技术。
理解捆绑和缩小
捆绑意味着可以组合多个 JavaScript 或 CSS 文件,以尽量减少浏览器发送到服务器的请求数量。缩小是一种从 CSS 和 JavaScript 文件中删除不必要的空白并更改函数和变量名的技术,使其更小。当这两种技术结合使用时,传输的数据会少得多,这将导致更快的加载时间。
Visual Studio 创建的默认项目在运行或部署应用时自动执行绑定。实际流程由bundleConfig.json文件配置,其结构类似于:
[
{
"outputFileName": "wwwroot/css/site.min.css",
"inputFiles": [
"wwwroot/css/site.css"
]
},
{
"outputFileName": "wwwroot/js/site.min.js",
"inputFiles": [
"wwwroot/js/site.js"
],
"minify": {
"enabled": true,
"renameLocals": true
},
"sourceMap": false
}
]
我们可以看到两个不同的组,一个用于 CSS,另一个用于 JavaScript,每个组生成一个文件(outputFileName。每个文件都有一组文件,其中可以包含通配符(inputFiles),并且可以指定是否缩小结果(enabled),以及重命名函数和变量以使其更小(renameLocals)。对于 JavaScript 文件,可以自动生成源映射文件(sourceMap。您可以在上阅读源地图 https://developer.mozilla.org/en-US/docs/Tools/Debugger/How_to/Use_a_source_map 。请注意,这种行为实际上不是 Visual Studio 固有的,而是由 Mads Kristensen 的Bundler & Minifier扩展生成的,可从 Visual Studio 图库的获取 https://marketplace.visualstudio.com/items?itemName=MadsKristensen.BundlerMinifier 。
还存在其他选项,例如添加同样来自 Mads Kristensen 的BuildBundlerMinifierNuGet 包,它向dotnet添加了一个命令行选项,允许我们在构建时从命令行执行绑定和缩小。还有一种选择是使用 Gulp、Grunt 或 WebPack,但由于这些是 JavaScript 解决方案,而不是 ASP.NET Core 解决方案,因此我将不在这里讨论它们。网页包、吞咽、咕噜请参考第 14 章、客户端**开发。
接下来,我们将学习异步操作如何帮助应用。
使用异步操作
异步调用是提高应用可伸缩性的一种方法。通常,处理请求的线程在处理过程中被阻塞,这意味着该线程将无法接受其他请求。通过使用异步操作,来自不同池的另一个线程被分配请求,侦听线程返回到池中,等待接收其他请求。控制器、Razor 页面、标记帮助程序、视图组件和中间件类可以异步执行。每当您有执行输入/输出(IO的操作时,始终使用异步调用,因为这可以带来更好的可伸缩性。
对于控制器,只需将动作方法的签名更改为如下所示(注意async关键字和Task<IActionResult>返回类型):
public async Task<IActionResult> Index() { ... }
在 Razor 页面中,它是类似的(注意Async后缀、Task<IActionResult>返回类型和async关键字):
public async Task<IActionResult> OnGetAsync() { ... }
对于标记帮助器和标记帮助器组件,重写ProcessAsync方法而不是Process:
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { ... }
针对组件,实现一个InvokeAsync方法,如下所示:
public async Task<IViewComponentResult> InvokeAsync(/* any parameters */) { ... }
还要确保在视图中异步调用它:
@await Component.InvokeAsync("MyComponent", /* any parameters */)
最后,在中间件类中,执行以下操作:
public async Task Invoke(HttpContext httpContext) { ... }
或者,在 lambdas 中,执行以下代码:
app.Use(async (ctx, next) =>
{
//async work
await next();
});
更好的是,对于控制器操作,包括一个CancellationToken参数,并将其传递给在其内部调用的任何异步方法。这将确保,如果客户端取消请求(通过关闭浏览器或以任何其他方式终止呼叫),所有呼叫也将关闭:
public async Task<IActionResult> Index(CancellationToken token) { ... }
请注意,此参数与您从HttpContext.RequestAborted获得的参数相同。
这还不是全部;您还应该更喜欢异步 API 方法,而不是阻塞 API 方法,尤其是那些执行 I/O、数据库或网络调用的方法。例如,如果需要发出 HTTP 调用,请始终查找其方法的异步版本:
var client = new HttpClient();
var response = await client.GetStreamAsync("http://<url>");
如果您想传递取消令牌,它会稍微复杂一些,但不会太复杂:
var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "<url>");
var response = await client.SendAsync(request, token);
或者,如果您需要上传可能较大的文件,请始终使用以下代码(安装Microsoft.AspNetCore.WebUtilitiesNuGet 软件包):
app.Use(async (ctx, next) =>
{
using (var streamReader = new HttpRequestStreamReader
(ctx.Request.Body, Encoding.UTF8))
{
var jsonReader = new JsonTextReader(streamReader);
var json = await JObject.LoadAsync(jsonReader);
}
});
这样做的好处是,在读取所有有效负载内容时不会阻止 post,在本例中,它也异步构建 JSON 对象。
在 ASP.NET3 中,主机现在一直是异步的,这意味着默认情况下禁用同步 API,调用它们会导致异常。如果您不希望这样做,您需要通过使用中间件组件打开功能上的标志来更改此行为:
var synchronousIOFeature = HttpContext.Features.Get<IHttpBodyControlFeature>();
synchronousIOFeature.AllowSynchronousIO = true;
或者,单独针对 Kestrel 和 HTTP.sys,您可以在服务配置上执行此操作:
//Kestrel
services.Configure<KestrelServerOptions>(options =>
{
options.AllowSynchronousIO = true;
});
//HTTP.sys
services.Configure<HttpSysOptions>(options =>
{
options.AllowSynchronousIO = true;
});
//if using IIS
services.Configure<IISServerOptions>(options =>
{
options.AllowSynchronousIO = true;
});
在这里,我们看到了如何使用异步操作来提高解决方案的可伸缩性。在下一节中,我们将研究一种提高性能的解决方案:缓存。
Keep in mind, however, that asynchronicity is not a panacea for all your problems; it is simply a way to make your application more responsive.
通过缓存提高性能
缓存是可以对站点性能产生更大影响的优化之一。通过缓存响应和数据,您不必再次获取它们、处理它们并将它们发送给客户端。让我们看看实现这一目标的两种方法。
缓存数据
通过缓存数据,您无需在需要时反复检索数据。你需要考虑一些方面:
- 它将在缓存中保存多长时间?
- 如果需要,如何使缓存无效?
- 您是否需要将其分布到不同的机器上?
- 需要多少内存?它会永远成长吗?
通常有三种方法指定缓存持续时间:
- 绝对:缓存将在预定义的时间点过期。
- 相对:缓存将在创建后一段时间过期。
- 滑动:缓存创建后会过期一段时间,但如果被访问,这段时间会延长相同的时间。
内存缓存
实现缓存的最简单方法是使用内置的IMemoryCache实现,可在Microsoft.Extensions.Caching.MemoryNuGet 包中获得(它也在Microsoft.AspNetCore.All元包中提供)。正如您所猜测的,它是一个仅内存的缓存,适用于单服务器应用。为了使用它,您需要在ConfigureServices中注册它的实现:
services.AddMemoryCache();
之后,您可以将IMemoryCache实现注入到任何类控制器、中间件、标记帮助器、视图组件等中。基本上有三个操作:
- 向缓存中添加一个条目(
CreateEntry或Set。 - 从缓存中获取条目(
Get、GetOrCreate或TryGetValue。 - 从缓存中删除条目(
Remove。
添加条目需要您为其指定名称、优先级和持续时间。名称可以是任何对象,持续时间可以指定为相对时间、滑动到期时间或绝对时间。下面是一个例子:
//relative expiration in 30 minutes
cache.Set("key", new MyClass(), TimeSpan.FromMinutes(30));
//absolute expiration for next day
cache.Set("key", new MyClass(), DateTimeOffset.Now.AddDays(1));
//sliding expiration
var entry = cache.CreateEntry("key");
entry.SlidingExpiration = TimeSpan.FromMinutes(30);
entry.Value = new MyClass();
您还可以将这两种策略结合起来:
//keep item in cache as long as it is requested at least once every 5
//minutes
// but refresh it every hour
var options = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromMinutes(5))
.SetAbsoluteExpiration(TimeSpan.FromHours(1));
var entry = cache.CreateEntry("key");
entry.SetOptions(options);
使用滑动过期选项时,每当访问缓存项时,它都将被续订。使用Set将创建一个新项目或用相同的密钥替换任何现有项目。如果不存在具有给定密钥的项,您也可以使用GetOrCreate添加一个,或者按如下方式返回现有项:
var value = cache.GetOrCreate("key", (entry) =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
return new MyClass();
});
优先级控制从缓存中逐出项的时间。只有两种方法可以从缓存中删除项:手动或在内存不足时删除。术语优先级是指当机器内存不足时应用于项目的行为。可能的值如下所示:
High:尽可能长时间地保存该项目。Low:必要时可以从内存中删除该项。NeverRemove:除非达到项目的持续时间,否则切勿从内存中逐出该项目。Normal:使用默认算法。
可以传递到期令牌集合;这本质上是一种拥有缓存依赖项的方法。您可以通过多种方式创建缓存依赖项,例如通过取消令牌:
var cts = new CancellationTokenSource();
var entry = cache.CreateEntry("key");
entry.ExpirationTokens.Add(new CancellationChangeToken(cts.Token));
您还可以通过配置更改创建一个:
var ccts = new ConfigurationChangeTokenSource<MyOptions>(this.Configuration);
var entry = cache.CreateEntry("key");
entry.ExpirationTokens.Add(ccts.GetChangeToken());
您甚至可以通过文件(或目录)中的更改创建一个:
var fileInfo = new FileInfo(@"C:\Some\File.txt");
var fileProvider = new PhysicalFileProvider(fileInfo.DirectoryName);
var entry = cache.CreateEntry("key");
entry.ExpirationTokens.Add(fileProvider.Watch(fileInfo.Name));
如果您想要组合多个,以便在任何更改令牌过期时缓存项过期,您可以使用CompositeChangeToken:
var entry = cache.CreateEntry("key");
entry.ExpirationTokens.Add(new CompositeChangeToken(new List<IChangeToken> {
/* one */,
/* two */,
/* three */
}));
您还可以注册一个回调,该回调将在从缓存中逐出项时自动调用,如下所示:
var entry = cache.CreateEntry("key");
entry.RegisterPostEvictionCallback((object key, object value, EvictionReason reason, object state) =>
{
/* do something */
}, "/* some optional state object */");
这可以用作一种简单的调度机制:您可以使用相同的回调添加另一个项,以便当该项过期时,它将一次又一次地添加该项。key和value参数明显;reason参数将告诉您项目被逐出的原因,这可能是由于以下原因之一:
-
None:原因不明。 -
Removed:该项目已明确删除。 -
Replaced:该项目已被替换。 -
Expired:已达到到期时间。 -
TokenExpired:已触发过期令牌。 -
Capacity:已达到最大容量。
state参数将包含您传递给RegisterPostEvictionCallback的任意对象,包括null。
为了从缓存中获取项目,存在两个选项:
//return null if it doesn't exist
var value = cache.Get<MyClass>("key");
//return false if the item doesn't exist
var exists = cache.TryGetValue<MyClass>("key", out MyClass value);
至于删除,再简单不过了:
cache.Remove("key");
这将从缓存中永久删除命名缓存项。
A side note: it is not possible to iterate through the items in the cache from the IMemoryCache instance, but you can count them by downcasting to MemoryCache and using its Count property.
分布式缓存
ASP.NET Core 附带两个分布式缓存提供程序:
- Redis:在
Microsoft.Extensions.Caching.Redis提供 NuGet 套餐 - SQL Server:可从
Microsoft.Extensions.Caching.SqlServer获取
核心功能通过IDistributedCache接口提供。您需要在ConfigureServices中注册其中一个实现。对于 Redis,请使用以下命令:
services.AddDistributedRedisCache(options =>
{
options.Configuration = "serverName";
options.InstanceName = "InstanceName";
});
对于 SQL Server,请使用以下命令:
services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = @"<Connection String>";
options.SchemaName = "dbo";
options.TableName = "CacheTable";
});
完成后,您将能够注入一个IDistributedCache实例,该实例提供四个操作:
- 添加或删除项目(
Set、SetAsync) - 检索项目(
Get、GetAsync) - 刷新项目(
Refresh、RefreshAsync) - 移除一个项目(
Remove、RemoveAsync)
如您所见,它类似于IMemoryCache,但有一点不同,它为所有操作提供异步和同步版本。此外,它并不具有内存缓存的所有选项,例如优先级、过期回调和过期令牌。但最重要的区别是,所有项都需要存储为字节数组,这意味着您必须事先序列化要存储在缓存中的任何对象。一种特殊情况是字符串,其中有直接处理字符串的扩展方法。
因此,要添加项目,您需要执行以下操作:
using (var stream = new MemoryStream())
{
var formatter = new BinaryFormatter();
formatter.Serialize(stream, new MyClass());
cache.Set("key", formatter.ToArray(), new DistributedCacheEntryOptions
{
//pick only one of these
//absolute expiration
AbsoluteExpiration = DateTimeOffset.Now.AddDays(1),
//relative expiration
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(60),
//sliding expiration
SlidingExpiration = TimeSpan.FromMinutes(60)
});
}
如您所见,它确实支持绝对、相对和滑动过期。如果要使用字符串,则更简单:
cache.SetString("key", str, options);
要检索项目,还需要随后对其进行反序列化:
var bytes = cache.Get("key");
using (var stream = new MemoryStream(bytes))
{
var formatter = new BinaryFormatter();
var data = formatter.Deserialize(stream) as MyClass;
}
对于字符串,使用以下代码:
var data = cache.GetString("key");
提神容易;如果项目使用滑动过期,则会续订:
cache.Refresh("key");
删除以下内容也是如此:
cache.Remove("key");
异步版本是相同的,只是它们以Async后缀结尾并返回一个Task对象,您可以等待它。
您可能知道,BinaryFormatter仅在.NET Core 2.0 之后可用,因此,对于在此之前的.NET Core 版本,您需要提出自己的序列化机制。一个好的可能是MessagePack,可从 NuGet 获得。
Both distributed caches and in-memory caches have their pros and cons. A distributed cache is obviously better when we have a cluster of machines, but it also has a higher latency—the time it takes to get results from the server to the client. In-memory caches are much faster, but they take up memory on the machine on which it is running.
在本节中,我们讨论了缓存数据的替代方案,无论是在内存中还是在远程服务器中。下一节解释如何缓存操作方法执行的结果。
缓存操作结果
通过缓存操作结果,可以指示浏览器在第一次执行后将提供的结果保留一段时间。这可以导致显著的性能改进;由于不需要运行代码,因此响应直接来自浏览器的缓存。该过程在的 RFC 中规定 https://tools.ietf.org/html/rfc7234#section-5.2。我们可以通过将[ResponseCache]属性应用于控制器或动作方法,将缓存应用于动作方法。它可以采用以下一些参数:
Duration(int:缓存持续时间,以秒为单位;映射到Cache-control头中的max-age值Location(ResponseCacheLocation):缓存的存储位置Any、Client或None中的一个)NoStore(bool):不缓存响应VaryByHeader(string):使缓存发生变化的标头,例如,Accept-Language导致针对每个请求的语言缓存响应(请参见https://www.w3.org/International/questions/qa-accept-lang-locales )VaryByQueryKeys(string[]):使缓存发生变化的任意数量的查询字符串键CacheProfileName(string:缓存配置文件的名称;稍后再谈
缓存位置具有以下含义:
Any:缓存在客户端和任何代理中;将Cache-control标题设置为publicClient:仅缓存在客户端;Cache-control设置为privateNone:未缓存任何内容;Cache-control和Pragma均设置为no-cache
但在使用之前,我们需要在ConfigureServices中注册所需的服务:
services.AddResponseCaching();
可以通过传递委托来配置某些选项:
services.AddResponseCaching(options =>
{
options.MaximumBodySize = 64 * 1024 * 1024;
options.SizeLimit = 100 * 1024 * 1024;
options.UseCaseInsensitivePaths = false;
});
可供选择的方案如下:
MaximumBodySize(int):最大可缓存响应;默认值为 64 KBSizeLimit(int:所有缓存响应的最大大小;默认值为 100 MBUseCaseInsensitivePaths(bool:路径是否应区分大小写;默认值为false
为了做到这一点,以及注册服务,我们需要添加响应缓存中间件(方法Configure):
app.UseResponseCaching();
与其传递持续时间、位置等参数,不如使用缓存配置文件。缓存配置文件是在我们注册 MVC 服务时通过添加以下条目定义的:
services
.AddMvc(options =>
{
options.CacheProfiles.Add("Public5MinutesVaryByLanguage",
new CacheProfile
{
Duration = 5 * 60,
Location = ResponseCacheLocation.Any,
VaryByHeader = "Accept-Language"
});
});
这里,我们正在注册名为Public5MinutesVaryByLanguage的缓存配置文件的一些选项,如下所示:
Duration(int:缓存项的持续时间,以秒为单位Location(ResponseCacheLocation:缓存项的存放位置;它可以在服务器上,也可以在客户端(浏览器)上VaryByHeader(string:可选的请求头,可根据需要改变缓存;在本例中,我们使用浏览器的语言更改缓存
如果愿意,可以从配置文件加载配置。假设你有这样的结构:
{
"CacheProfiles": {
"Public5MinutesVaryByLanguage": {
"Duration": 300,
"Location": "Any",
"VaryByHeader" : "Accept-Language"
}
}
}
您可以使用配置 API 在ConfigureServices中加载它:
services
.Configure<Dictionary<string, CacheProfile>>(this.Configuration.
GetSection("CacheProfiles"))
.AddMvc(options =>
{
var cacheProfiles = this.Configuration.GetSection<Dictionary
<string, CacheProfile>();
foreach (var keyValuePair in cacheProfiles)
{
options.CacheProfiles.Add(keyValuePair);
}
});
使用缓存配置文件可以让我们拥有一个集中的位置,在那里我们可以更改将在所有应用中使用的配置文件设置。简单如下:
[ResponseCache(CacheProfileName = "Public5MinutesVaryByLanguage")]
public IActionResult Index() { ... }
响应缓存还取决于默认启用的 HTTP.sys 设置。它被称为EnableResponseCaching:
.UseHttpSys(options =>
{
options.EnableResponseCaching = true;
})
这将为 HTTP.sys 主机启用响应缓存。请记住,如果没有此选项,[ResponseCache]属性将无法工作。这是发送适当的缓存响应头所必需的。
在本节中,我们看到了如何缓存来自动作方法的响应。现在让我们看看如何缓存视图标记。
缓存视图
通过使用附带的标记帮助程序<cache>和<distributed-cache>,您将能够缓存部分视图。从他们的名字可以推断,<cache>需要一个IMemoryCache的注册实例,<distributed-cache>需要IDistributedCache。我已经在第 9 章、可重用组件中谈到了这两个标签助手,所以我不再重复。我们只看两个例子。这一个用于内存缓存:
<cache expires-sliding="TimeSpan.FromMinutes(30)">
...
</cache>
这一个用于分布式缓存:
<distributed-cache name="redis" expires-sliding="TimeSpan.FromMinutes(30)">
...
</distributed-cache>
放置在<distributed-cache>中的任何内容都将存储在命名的分布式缓存(在本例中为redis)中,存储时间为从第一次渲染视图开始的一段时间(30分钟),在随后的情况下,它将直接从那里来,而无需任何额外处理。
Do not forget that you need to register an instance of either IMemoryCache or IDistributedCache. These tag helpers, unfortunately, cannot take cache profiles.
缓存是任何真实 web 应用的必备工具,但必须仔细考虑,因为它可能会给系统带来内存压力。在接下来的两部分中,我们将学习如何优化响应。
压缩响应
响应压缩可从Microsoft.AspNetCore.ResponseCompression包获得。本质上,对于支持它的浏览器,它可以在通过网络发送响应之前对响应进行压缩,从而最大限度地减少将要发送的数据量,但需要花费一些时间来压缩响应。
如果浏览器支持响应压缩,则应发送Accept-Encoding: gzip, deflate头。让我们看看如何:
- 我们首先需要在
ConfigureServices中注册响应压缩服务:
services.AddResponseCompression();
- 更详细的版本允许您指定实际的压缩提供程序(
GzipCompressionProvider包括在内)和可压缩文件类型:
services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<GzipCompressionProvider>();
options.MimeTypes = ResponseCompressionDefaults.
MimeTypes.Concat(new[] { "img/svg+xml" });
});
services.Configure<GzipCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest;
});
GzipCompressionProviderOptions的唯一选项是压缩级别,其中有三个选项:
您可以看到,还可以配置要压缩的文件类型。请注意,以下内容类型将自动压缩:
- 最后,您需要将响应压缩中间件添加到
Configure方法中:
app.UseResponseCompression();
现在,只要响应是配置的 mime 类型之一,它就会被自动压缩,并且响应头将包括一个Content-Encoding: gzip头。
请注意,您可以通过实现ICompressionProvider接口并将其注册到采用 lambda 的AddResponseCompression方法重载中来推出自己的压缩实现。除了 GZip,微软还有一个基于Brotli的实现(BrotliCompressionProvider和BrotliCompressionProviderOptions类)。Brotli 是一种开源压缩算法,受多个浏览器支持,提供比 GZip 更好的压缩。
The Deflate compression method is not supported in ASP.NET Core 2.x,—only GZip. Read about Deflate at its RFC (https://tools.ietf.org/html/rfc1951) and about GZip at https://tools.ietf.org/html/rfc1952. Read about Brotli in RFC 7932 (https://tools.ietf.org/html/rfc7932) and see the list of supported browsers at https://www.caniuse.com/#feat=brotli.
压缩可以极大地提高响应的延迟,但需要在服务器上进行一些额外的处理。现在我们已经了解了压缩,让我们看看如何通过使用缓冲来提高响应时间。
缓冲响应
我们将在这里介绍的最后一种技术是响应缓冲。通常,web 服务器流式传输响应,这意味着一旦有了块,它就会发送响应。另一种选择是获取所有这些块,组合它们,并立即发送它们:这称为缓冲。
缓冲提供了一些优势:它可以带来更好的性能,并提供在内容(包括标题)发送到客户端之前更改内容的能力。
微软通过Microsoft.AspNetCore.BufferingNuGet 软件包提供缓冲功能。使用它很简单例如,您可以在中间件 lambda 中使用它:
app.UseResponseBuffering();
app.Run(async (ctx) =>
{
ctx.Response.ContentType = "text/html";
await ctx.Response.WriteAsync("Hello, World!);
ctx.Response.Headers.Clear();
ctx.Response.Body.SetLength(0);
ctx.Response.ContentType = "text/plain";
await ctx.Response.WriteAsync("Hello, buffered World!");
});
在本例中,我们首先注册响应缓冲中间件(基本上是包装响应流),然后在中间件 lambda 上,您可以看到我们可以写入客户端,通过将其长度设置为0来清除响应,然后再次写入。如果没有响应缓冲,这是不可能的。
如果您想禁用它,可以通过其功能IHttpBufferingFeature:
var feature = ctx.Features.Get<IHttpBufferingFeature>();
feature.DisableResponseBuffering();
在本节中,我们学习了缓冲、它的优点以及如何启用它,并以此结束本章。
总结
在本章中,我们了解到在操作方法和视图中使用响应缓存是必要的,但必须谨慎使用,因为您不希望内容过时。缓存配置文件是操作方法的首选配置文件,因为它们提供了一个集中的位置,从而更容易进行更改。您可以拥有所需的任意多个配置文件。
如果需要在服务器集群之间共享数据,分布式缓存可能会有所帮助,但需要注意的是,通过有线传输数据可能需要一些时间,例如,即使它比从数据库检索数据快得多。它还可能占用大量内存,因此可能导致其他不可预见的问题。
然后,我们看到捆绑和缩小也非常方便,因为它们可以大大减少要传输的数据量,这对于移动浏览器来说更为重要。
异步操作也应该是您的首选;一些现代 API 甚至不允许您有任何其他选择。这可以大大提高应用的可伸缩性。
最后,我们看到需要使用分析器来识别瓶颈。Stackify 前缀是一个很好的选择。
主机的选择在很大程度上取决于部署需要。如果是非 Windows 主机,那么除了 Kestrel 之外,我们别无选择。在 Kestrel 和 HTTP.sys 上,有大量的参数可以根据需要进行调整,但请注意,使用这些参数可能会导致性能低下。
在本章中,我们介绍了一些提高应用性能和可伸缩性的方法。这不是一个详尽的列表,在代码中可以做很多事情,特别是在获取数据时。在将其应用于生产之前,请使用您的最佳判断并进行试验。
在下一章中,我们将介绍实时通信。
问题
因此,在本章结束时,您应该知道以下问题的答案:
- ASP.NET Core 3 可用的两台主机是什么?
- 有哪两种缓存可用?
- 压缩响应的好处是什么?
- 缓存响应的目的是什么?
- 异步操作是否提高了性能?
- 什么是捆绑?
- 剖析器有什么好处?**
十六、实时通信
在本章中,我们将了解 MicrosoftS****ignalR,这是一个用于在客户端和服务器之间进行实时通信的库。它允许服务器主动调用客户机,而不是作为请求的结果。它以众所周知的技术为基础,例如AJAX、WebSocket、和服务器发送事件,但采用透明的方式。你不需要知道它在使用什么,它基本上就是工作的,不管你有什么浏览器。它还支持很多浏览器,包括手机。让我们探索一下这项技术,看看它能提供什么,基本上如下所示:
- 设置信号机
- 将消息从客户端发送到服务器
- 从服务器向所有/某些客户端广播消息
- 从集线器外部发送消息
阅读本章后,您将学习如何从服务器实时通信到连接到页面的客户端,无论客户端是在 PC 上还是在移动设备上。
技术要求
要实现本章中介绍的示例,您需要.NET Core 3 SDK 和文本编辑器。当然,VisualStudio2019(任何版本)满足所有要求,但您也可以使用 VisualStudio 代码。
源代码可以在这里从 GitHub 检索:https://github.com/PacktPublishing/Modern-Web-Development-with-ASP.NET-Core-3-Second-Edition 。
设置信号机
在开始使用 signar 之前,首先需要解决几个问题,即在本地安装库。
执行以下步骤以开始此设置:
- 首先,安装
Microsoft.AspNetCore.SignalRNuGet 包。 - 您还需要通过npm(简称节点包管理器)作为
@microsoft/signalr提供的 JavaScript 库。 - 安装后,需要将 JavaScript 文件(最小化版本或调试版本)复制到
wwwroot下的某个文件夹中,因为浏览器需要检索该文件。 - 包含信号器库的文件称为
signalr.js或signalr.min.js(对于最小化版本),可在node_modules/@aspnet/signalr/dist/browser下找到。 - 如果您希望使用
MessagePack序列化,您还需要@aspnet/signalr-protocol-msgpack包(稍后将对此进行详细介绍),但这不是严格需要的。
与以前的预核心版本不同,signar 不需要任何其他库,例如jQuery,但它可以愉快地与之共存。只需在使用代码之前添加对signalr.js文件的引用。
对于 npm,添加一个与此类似的package.json文件:
{
"name": "chapter16",
"version": "1.0.0",
"description": "",
"main": "",
"scripts": {
},
"author": "",
"license": "ISC",
"dependencies": {
"@microsoft/signalr": "^3.1.4",
"@aspnet/signalr-protocol-msgpack": "^1.1.0",
"msgpack5": "^4.2.1"
}
}
npm 文件存储在node_modules文件夹中,但需要复制到wwwroot中的某个位置,从那里可以公开这些文件,例如,提供给浏览器。将文件从node_modules文件夹复制到应用中的一个好方法是利用MSBuild构建系统。打开您的.csproj文件并添加以下行:
<ItemGroup>
<SignalRFiles Include="node_modules/@microsoft/signalr
/dist/browser/*.js" />
<SignalRMessagePackFiles
Include="node_modules/@aspnet/signalr-protocol-msgpack
/dist/browser/*.js" />
<MessagePackFiles Include="node_modules/msgpack5/dist/*.js" />
</ItemGroup>
<Target Name="CopyFiles" AfterTargets="Build">
<Copy SourceFiles="@(SignalRFiles)"
DestinationFolder="$(MSBuildProjectDirectory)\wwwroot\lib
\signalr" />
<Copy SourceFiles="@(SignalRMessagePackFiles)"
DestinationFolder="$(MSBuildProjectDirectory)\wwwroot\lib
\signalr" />
<Copy SourceFiles="@(MessagePackFiles)"
DestinationFolder="$(MSBuildProjectDirectory)\wwwroot\lib\msgpack5" />
</Target>
这将把所需的 JavaScript 文件从 npm 提供的目录复制到wwwroot内的文件夹中,该文件夹适合包含在网页上。另一种选择是使用 Libman,如第 14 章、客户端开发中所述。一定要看一看!不要忘记,因为您是在服务静态文件,所以必须在Configure方法中添加适当的中间件:
app.UseStaticFiles();
我们将从信号机的核心概念开始,并从那里开始。
学习核心概念
signar 的吸引力来自于它隐藏了处理(近)实时网络通信的不同技术。这些措施如下:
- 服务器发送事件(参见https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
- 网袋(见https://developer.mozilla.org/en-US/docs/Glossary/WebSockets
- 长轮询(参见https://en.wikipedia.org/wiki/Push_technology#Long_polling
每个都有自己的优点和缺点,但对于 Signaler,您不需要真正关心这一点,因为 Signaler 会自动为您选择最好的。
那么,这是关于什么的?基本上,使用 Signal,您可以做两件事:
- 让客户端应用(如网页)向服务器发送消息,并将其路由到也连接到同一 web 应用的所有(或某些)方
- 让服务器主动向所有(或部分)连接方发送消息
消息可以是纯字符串或具有某种结构。我们不需要关心它;信号员为我们负责序列化。当服务器向连接的客户机发送消息时,它会引发一个事件,并让他们有机会对收到的消息进行处理。
SignalR 可以将连接的客户端分组,并要求进行身份验证。核心概念是集线器:客户端聚集在集线器周围,从集线器发送和接收消息。集线器由 URL 标识。
您可以创建到 URL 的 HTTP 连接,从 URL 创建集线器连接,将事件侦听器添加到集线器连接(系统事件,例如关闭和集线器消息),然后开始从 URL 接收消息,还可以开始发送消息。
在设置了 SignalR 之后,我们现在将看到如何托管集线器。
托管中心
集线器是信号员用来让客户在一个众所周知的位置聚集在一起的概念。在客户端,它被标识为一个 URL,如http://<servername>/chat。在服务器上,它是从Hub继承的类,必须在 ASP.NET Core 管道中注册。下面是聊天中心的一个简单示例:
public class ChatHub : Hub
{
public async Task Send(string message)
{
await this.Clients.All.SendAsync("message", this.Context.User.
Identity.Name, message);
}
}
Send消息只能由 JavaScript 调用。这个Send方法是异步的,因此我们必须在一个众所周知的端点中注册这个集线器,在Configure方法中,我们注册端点:
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<ChatHub>("chat");
});
您可以选择您想要的任何名称,而不需要受中心类名称的限制。
您也可以通过ConfigureServices方式注册其服务,如下所示:
services.AddSignalR();
Hub类公开了两个虚拟方法OnConnectedAsync和OnDisconnectedAsync,每当客户端连接或断开连接时,都会触发这两个方法。OnDisconnectedAsync以Exception为参数,只有在断开时发生错误时才不为空。
要调用hub实例,我们必须首先从 JavaScript 初始化 SignalR 框架,为此,我们需要引用~/lib/signalr/signalr.js文件(用于开发)或~/lib/signalr/signalr.min.js(用于生产)。实际代码如下所示:
var logger = new signalR.ConsoleLogger(signalR.LogLevel.Information);
var httpConnection = new signalR.HttpConnection('/chat', { logger: logger });
这里,我们在同一个主机上调用一个名为chat的端点,该主机提供请求服务。现在,在客户端和服务器本身的通信方面,我们需要start它:
var connection = new signalR.HubConnection(httpConnection, logger);
connection
.start()
.catch((error) => {
console.log('Error creating the connection to the chat hub');
});
如您所见,start方法返回一个承诺,您还可以链接一个catch方法调用,以便在连接时捕获任何异常。
我们可以通过挂接到onclose事件检测到连接意外关闭:
connection.onclose((error) => {
console.log('Chat connection closed');
});
连接成功后,您将钩住自定义事件("message"):
connection.on('message', (user, message) => {
console.log(`Message from ${user}: ${message}`);
});
使用事件名称("message")对on的调用应与SendAsync方法中在集线器上调用的事件名称相匹配。它还应采用相同数量(和类型)的参数。
我还使用了arrow函数,这是现代 JavaScript 的一个特性(您可以通过阅读本文了解更多信息)https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions 。这只是语法,可以通过匿名函数实现。
作为说明,您可以传递其他查询字符串参数,这些参数稍后可能会在中心中捕获。还有另一种方法,使用HubConnectionBuilder:
var connection = new signalR.HubConnectionBuilder()
.configureLogging(signalR.LogLevel.Information)
.withUrl('/chat?key=value')
.build();
它有时很有用,因为除了调用其方法之外,将数据从客户机传递到集线器的方法不多。检索这些值的方法如以下代码所示:
var value = this.Context.GetHttpContext().Request.Query["key"].SingleOrDefault();
现在,我们可以开始发送消息:
function send(message) {
connection.invoke('Send', message);
}
本质上,该方法异步调用ChatHub类的Send方法,任何响应都将由'message'侦听器接收,我们之前注册过该侦听器(请参见hub.on('message')。
简而言之,流程如下所示:
- 使用集线器地址(
signalR.HttpConnection)在客户端(signalR.HubConnection上创建连接,该地址必须映射到从Hub继承的.NET 类。 - 为某个事件(
connection.on())添加了事件处理程序。 - 连接已启动(
start()。 - 客户端使用在
Hub类上声明的方法的名称将消息发送到集线器(connection.invoke()。 Hub类上的方法向所有/部分连接的客户端广播消息。- 当客户端收到消息时,它会向该事件名称的所有订阅者引发一个事件(与
connection.on()中声明的相同)。
The client can call any method on the Hub class, and this, in turn, can raise any other event on the client.
但首先,让我们看看如何定义客户机和服务器之间的协议,以便两者可以对话。
通信协议的选择
signer 需要让客户端和服务器使用相同的语言—协议。它支持以下通信协议(或消息传输,如果您愿意):
- WebSocket:在支持它的浏览器中,这可能是性能最好的,因为它是基于二进制的,而不是基于文本的。阅读更多关于 WebSocket 的信息:https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API 。
- 服务器发送事件:这是另一个 HTTP 标准;它允许客户端不断轮询服务器,给人一种服务器直接与之通信的印象;参见https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events 。
- AJAX 长轮询:也称为AJAX Comet,这是一种技术,通过该技术,客户端可以使一个 AJAX 请求保持活动状态,可能持续很长一段时间,直到服务器提供应答为止,即当它返回并发出另一个长请求时。
通常,signalR确定要使用的最佳协议,但您可以从客户端强制使用:
var connection = new signalR.HubConnectionBuilder()
.configureLogging(signalR.LogLevel.Information)
.withUrl('/chat', { skipNegotiation: false, transport: signalR.
TransportType.ServerSentEvents })
这也可以从服务器检索,但一般来说,建议对所有协议保持打开状态:
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<ChatHub>("chat", opt =>
{
opt.Transports = HttpTransportType.ServerSentEvents;
});
});
例如,在不支持 WebSocket 的操作系统(如 Windows 7)中,可能需要强制使用一个协议。也可能是因为路由或防火墙可能不允许某些协议,例如 WebSocket。客户端和服务器端配置必须匹配,也就是说,如果服务器没有启用特定的协议,那么在客户端设置它将不起作用。如果您不限制运输,信号员将尝试所有这些方法,并自动选择最有效的方法。如果存在某种限制,例如客户端和服务器之间的协议不兼容,则可能需要选择特定的协议。如果你这样做了,别忘了也跳过谈判,因为这会节省一些时间。
Do not restrict the transport types unless you have a very good reason for doing so, such as browser or operating system incompatibilities.
我们已经了解了如何配置连接,现在让我们看看如何在出现故障时自动重新连接。
自动重联
您可以捕获close事件并对其作出响应,或者在连接意外断开时让信号器自动重新连接(这些事情发生在互联网上,您知道!)。为此,在客户端代码上调用withAutomaticReconnect扩展方法:
var connection = new signalR.HubConnectionBuilder()
.withAutomaticReconnect()
.withUrl('/chat')
.build();
调用此方法时可以不使用参数,也可以使用表示每次尝试重新连接之前等待的时间(以毫秒为单位)的数值数组。默认值为[0, 2000, 10000, 30000, null],即首先立即尝试,然后等待两秒钟,然后等待一秒钟,然后等待三秒钟,然后停止尝试(null。第三个选项是一个函数,它接受一些参数并返回下一个延迟,如本例所示:
var connection = new signalR.HubConnectionBuilder()
.withAutomaticReconnect({
nextRetryDelayInMilliseconds: (retryContext) => {
//previousRetryCount (Number)
//retryReason (Error)
return 2 * retryContext.elapsedMilliseconds;
})
.build();
在本例中,我们返回一个对象,该对象使用 lambda 函数,该函数将上一次返回计数、重试原因(异常)和自上次重试以来经过的时间作为其参数,如果要取消重新连接,则希望您返回下一次延迟或 null。
使用自动重新连接时,会引发一些事件:
connection.onreconnecting((error) => {
//a reconnect attempt is being made
});
connection.onreconnected((connectionid) => {
//successful reconnection
});
这些事件不言自明:
- 当信号器由于错误而试图重新连接时,会引发
onreconnecting,该错误作为其唯一参数传递给回调 - 成功重新连接后引发
onreconnected,并将新连接 ID 传递给回调
发送到集线器和从集线器发送的消息需要序列化,这是下一个主题的主题。
消息序列化
开箱即用,signer 以明文 JSON 发送消息,但还有一种替代方法,即MessagePack。这是一种压缩格式,可以提供更好的性能,特别是对于更大的有效负载。
如前所述,我们需要安装@aspnet/signalr-protocol-msgpacknpm 包和Microsoft.AspNetCore.SignalR.Protocols.MessagePackNuGet 包。
我们可以指定MessagePack协议的示例如下:
var connection = new signalR.HubConnectionBuilder()
.withUrl('/chat')
.withHubProtocol(new signalR.protocols.msgpack.
MessagePackHubProtocol())
.build();
如果您选择使用MessagePack,在注册信号机服务时还需要添加对MessagePack的支持:
services
.AddSignalR()
.AddMessagePackProtocol();
现在,我们已经了解了如何开始对话,让我们看看信号器上下文,在这里我们可以从当前信号器会话中获取信息。
探索信号员语境
信号器上下文帮助我们了解我们在哪里以及谁在发出请求。它通过Hub类的Context属性提供。在其中,您将发现以下属性:
Connection(HubConnectionContext:这是低级连接信息;您可以从它(GetHttpContext()中获取对当前HttpContext的引用以及元数据内容(Metadata,并且可以终止连接(Abort()。ConnectionId(string):这是唯一一个唯一标识此集线器上客户端的连接 ID。User(ClaimsPrincipal):这是当前用户(使用身份验证时有用)及其所有声明。
Context属性在调用hub方法时可用,包括OnConnectedAsync和OnDisconnectedAsync。不要忘记,对于上下文,用户总是通过其ConnectionId标识;只有使用身份验证,它才会与用户名(User.Identity.Name关联。
如果我们需要向中心传递任意参数?下一个!
使用查询字符串
通过Request的Query集合,可以在服务器端访问 URL 上传递的任何查询字符串参数(如"/chat?key=value":
var value = Context.GetHttpContext().Request.
Query["key"].SingleOrDefault();
现在,让我们了解消息可以发送到哪些实体。
了解消息目标
信号器消息可发送至以下任何一个:
- 全部:所有连接的客户端都会收到
- 组:只有某一组的客户才会收到;组由名称标识
- 除之外的组:除特定客户端以外的特定组中的所有客户端,由其连接 ID 标识
- 客户端:只有特定的客户端,由其连接 ID 标识
客户端由连接 ID 标识,该 ID 可从Hub类的Context属性中获取:
var connectionId = this.Context.ConnectionId;
用户可以添加到任意数量的组(当然也可以删除):
await this.Groups.AddAsync(this.Context.ConnectionId, "MyFriends");
await this.Groups.RemoveAsync(this.Connection.ConnectionId, "MyExFriends");
要向组发送消息,请将All属性替换为Group调用:
await this.Clients.Group("MyFriends").InvokeAsync("Send", message);
或者,类似地,对于特定客户端,请使用以下命令:
await this.Clients.Client(this.Context.ConnectionId).InvokeAsync("Send", message);
组由信号器在内部维护,但是,当然,没有什么可以阻止您拥有自己的助手结构。这就是为什么:
- 要向所有连接的客户端(集线器)发送消息,请执行以下操作:
await this.Clients.All.SendAsync("message", message);
- 要仅将消息发送到由其连接 ID 标识的特定客户端,请使用以下命令:
await this.Clients.Client("<connectionid>").SendAsync("message", message);
- 对于命名组,我们可以使用以下方法:
await this.Clients.Group("MyFriends").SendAsync("message", message);
- 或仅针对组,可使用以下内容:
await this.Clients.Groups("MyFriends", "MyExFriends").SendAsync("message", message);
- 对于组中除一个或两个连接 ID 以外的所有成员,我们使用以下方法:
await this.Clients.GroupExcept("MyFriends", "<connid1>", "<connid2>").SendAsync("message", message);
如果我们需要从 web 应用外部与中心通信,该怎么办?这就是下一节的主题。
从外部沟通
正如您所想象的,可以与集线器通信,这意味着向集线器发送消息。有两种可能性:
- 来自同一 web 应用
- 来自不同的应用
让我们逐一研究一下。
来自同一 web 应用的通信
可以从信号机外部向集线器发送消息。这并不意味着访问ChatHub类的实例,而是只访问其连接的客户端。您可以使用内置的依赖注入框架注入IHubContext<ChatHub>实例,如下所示:
public class ChatController : Controller
{
private readonly IHubContext<ChatHub> _context;
public ChatController(IHubContext<ChatHub> context)
{
this._context = context;
}
[HttpGet("Send/{message}")]
public async Task<IActionResult> Send(string message)
{
await this._context.Clients.All.SendAsync("message", this
.User.Identity.Name, message);
}
}
如您所见,您负责将所有参数发送到客户端。当然,也可以发送到组或直接发送到客户端。
假设您想向所有客户发送一条重复消息;您可以在Configure方法中编写这样的代码(或者从您引用服务提供商的地方):
public class TimerHub : Hub
{
public async Task Notify()
{
await this.Clients.All.SendAsync("notify");
}
}
//in Configure
TimerCallback callback = (x) =>
{
var hub = app.ApplicationServices.GetService<IHubContext<TimerHub>>();
hub.Clients.All.SendAsync("notify");
};
var timer = new Timer(callback);
timer.Change(
dueTime: TimeSpan.FromSeconds(0),
period: TimeSpan.FromSeconds(1));
前面的代码显示timerHub和notify事件的注册。引发事件时,会向控制台写入一条消息。如果启动订阅时发生错误,也会记录该错误。
Timer将每秒发射一次,并将当前时间广播给一个假设的TimerHub类。此TimerHub类需要注册为端点:
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<TimerHub>("timer");
});
它还需要在客户端注册:
var notificationConnection = new signalR.HubConnectionBuilder()
.withUrl('/timer')
.withAutomaticReconnect()
.configureLogging(signalR.LogLevel.Information)
.build();
notificationConnection.on('notify', () => {
console.log('notification received!');
});
notificationConnection
.start()
.catch((error) => {
console.log(`Error starting the timer hub: ${error}`);
});
接下来,让我们看看如何从不同的应用进行通信
从不同的应用进行通信
这是一种不同的方法:我们需要实例化一个连接到承载 SignalR hub 的服务器的客户端代理。我们需要Microsoft.AspNet.SignalR.ClientNuGet 套餐。HubConnectionBuilder用于实例化HubConnection,如下图所示:
var desiredProtocols = HttpTransportType.WebSockets | HttpTransportType.LongPolling |
HttpTransportType.ServerSentEvents;
var connection = new HubConnectionBuilder()
.WithUrl("https://<servername>:5000/chat?key=value", options =>
{
options.Transports = desiredProtocols;
})
.WithAutomaticReconnect()
.ConfigureLogging(logging =>
{
logging.SetMinimumLevel(LogLevel.Information);
logging.AddConsole();
})
.AddMessagePackProtocol()
.Build();
connection.On<string>("message", (msg) =>
{
//do something with the message
});
connection.Closed += (error) =>
{
//do something with the error
};
await connection.StartAsync();
await connection.SendAsync("message", message);
这个例子做了几件事:
- 定义可接受的通信协议(WebSocket、长轮询和服务器发送事件)
- 注册两个事件处理程序(
Closed和On("message")) - 使用
MessagePack协议创建连接,将重新连接、日志记录设置为Information和控制台,并传递查询字符串值"key"="value" - 异步启动连接
- 调用集线器上的
Send方法,向其传递字符串消息
可以从对承载 SignalR hub 的服务器具有 HTTP 访问权限的任何位置调用此代码。注意所需协议的设置以及WithAutomaticReconnect和AddMessagePackProtocol扩展方法。AddConsole扩展方法来自Microsoft.Extensions.Logging.ConsoleNuGet 包。
我们已经了解了如何从托管它的应用的外部将消息发送到 SignalR hub。以下主题解释了身份验证如何与 SignalR 一起工作。
使用用户身份验证
SignalR 使用与封装 web 应用相同的用户身份验证机制,这意味着如果用户通过该应用的身份验证,则该用户将通过 SignalR 的身份验证。也可以在每次请求时发送 JWT 令牌,操作如下:
var connection = new signalR.HubConnectionBuilder()
.withUrl('/chat', { accessTokenFactory: () => '<token>' })
.build();
注意accessTokenFactory参数;这里,我们传递一个 lambda(它可能是一个函数),它返回一个 JWT 令牌。在客户端代码中,如果您从外部应用调用 SignalR,则需要执行以下操作:
var connection = new HubConnectionBuilder()
.WithUrl("http://<servername>/chat", options =>
{
options.AccessTokenProvider = () => Task.FromResult("<token>");
})
.Build();
就 SignalR 而言,用户的身份由其连接 ID 决定。因此,根据您的要求,您可能需要在该 ID 和应用使用的用户 ID 之间建立映射。
我们已经了解了如何启用身份验证;现在让我们看看如何记录信号器的工作。
登录中
日志记录可以帮助我们诊断故障,并实时了解系统中发生的情况。我们可以在配置中或通过代码为 SignalR 启用详细的日志记录。对于第一个选项,将最后两行添加到您的appsettings.json文件中(对于"Microsoft.AspNetCore.SignalR"和"Microsoft.AspNetCore.Http.Connections":
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information",
"Microsoft.AspNetCore.SignalR": "Trace",
"Microsoft.AspNetCore.Http.Connections": "Trace"
}
}
}
对于后者,请将配置添加到引导代码:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(builder =>
{
builder
.ConfigureLogging(logging =>
{
logging.AddFilter("Microsoft.AspNetCore.SignalR",
LogLevel.Trace);
logging.AddFilter("Microsoft.AspNetCore.Http.
Connections", LogLevel.Trace);
})
.UseStartup<Startup>();
});
请注意,这只是为了启用使代码输出调试信息的标志。Trace是最详细的级别,因此它将输出几乎所有内容,包括低级网络调用。要真正进行日志记录,您需要为服务器端代码或您自己的自定义提供程序添加日志记录程序,如控制台(请注意,这只是一个示例):
.ConfigureLogging(logging => {
logging.AddFilter("Microsoft.AspNetCore.SignalR", LogLevel.Trace)
logging.AddFilter("Microsoft.AspNetCore.Http.Connections",
LogLevel.Trace);
logging.AddConsole();
logging.AddProvider(new MyCustomLoggingProvider());
})
对于客户端,您需要注册一个接受两个参数的自定义函数:
function myLog(logLevel, message) {
//do something
}
var connection = new signalR.HubConnectionBuilder()
.configureLogging({ log: myLog })
.withUrl('/chat')
.build();
第一个参数logLevel是一个表示可能的日志级别之一的数字:
signalR.LogLevel.Critical(5)signalR.LogLevel.Error(4)signalR.LogLevel.Warning(3)signalR.LogLevel.Information(2signalR.LogLevel.Debug(1)signalR.LogLevel.Trace(0:一切,包括网络消息
第二个参数message是描述事件的文本消息。
在本节中,我们了解了如何以不同的粒度级别在客户端和服务器端启用日志记录。
总结
在本章中,我们看到可以使用 signar 执行 AJAX 用于调用服务器端代码和异步获取响应的任务。其优点是,当服务器需要广播某些信息时,您可以使用它让服务器自己接触连接的客户端。
SignalR 是一种非常强大的技术,因为它本质上适应您的服务器和客户端支持。它使服务器到客户端的通信变得轻而易举。虽然当前版本不是发布质量,但它足够稳定,可以在项目中使用。
signar 的一些高级方面,比如流式处理或集群,还没有讨论过,因为它们更多的是一本专门的书。
本书即将结束,因此,在下一章中,我们将研究一些在前几章中未涉及的 API。
问题
您现在应该能够回答以下问题:
- Signal 支持哪两种序列化格式化程序?
- 信号员支持什么运输?
MessagePack协议的好处是什么?- 我们可以向哪些目标发送消息?
- 为什么我们要限制信号员使用交通工具?
- 我们是否可以从承载 Signal 的 web 应用外部向其发送消息?
- 我们可以用信号器进行身份验证吗?
十七、Blazor 简介
本章将介绍 Blazor,一种在.NETCore3 中首次亮相的新技术。它用于构建在浏览器上运行的用户界面(UI应用,使用相同的语言和您将在服务器上使用的应用编程接口(API)。本章不会是 Blazor 的深入指南,因为这可能需要一整本书,但它应该足以让您开始学习。在此,您将了解以下内容:
- 不同的托管模式
- 如何创建组件
- 组件的生命周期
- 绑定的工作原理
- 如何与 JavaScript 交互
- 如何保护 Blazor 页面
- 如何对 Blazor 组件执行单元测试
技术要求
为了实现本章介绍的示例,您需要.NET Core 3软件开发工具包(SDK和某种形式的文本编辑器。当然,VisualStudio2019(任何版本)满足所有要求,但您也可以使用 VisualStudio 代码。
您应该已经阅读了第 16 章、实时通信,以了解信号机。
源代码可从 GitHub 检索,网址为:https://github.com/PacktPublishing/Modern-Web-Development-with-ASP.NET-Core-3-Second-Edition 。
Blazor 入门
Blazor 是一项全新的技术,由Steve Sanderson创建,与创建KnockoutJS和节点服务的人是同一个人。它是一个允许使用.NETCore 为客户端 web 和服务器端创建 UI 的框架。这样做的方法是将.NET 编译成WebAssembly,一种可以在浏览器上运行的语言,在运行 JavaScript 的同一解释器上运行。这使您可以在 Razor 中编写 UI 组件,在.NET 中编写事件处理代码,还可以在服务器端和客户端重用在.NET 中编写的代码。简而言之,使用 Blazor 的优点如下:
- 您可以使用一种好的旧的强类型语言,如 C#(或.NETCore 支持的任何其他语言)来构建 web UI。
- 利用.NET Core 框架和所有可用库公开的丰富 API。
- 在不同层(客户端和服务器端)之间重用代码。
与所有 ASP.NET Core 元素一样,Blazor 作为源代码可从 GitHub 获得,网址为:https://github.com/aspnet/AspNetCore/tree/master/src/Components 。
我们将从探索可用的托管模型开始。
托管模型
Blazor 中有两种基本的托管模型,如下所示:
- WebAssembly:将.NET Core 代码编译成一个 web 程序集,并在沙盒内,由浏览器在客户端执行,由执行 JavaScript 代码的同一虚拟机执行。这意味着所有引用的程序集都需要发送到客户端,这在下载大小和安全性方面带来了挑战,因为代码可以在客户端进行反汇编。由于沙箱的存在,应用的功能受到一些限制,例如向任意主机打开套接字。
- 服务器:在服务器上运行.NET Core 代码,生成的代码通过信令器传输到客户端。由于所有代码都在服务器上运行,因此它的功能几乎没有限制。
WebAssembly 模式是最新发布到.NETCore 的模式,但实际上却吸引了大多数人的注意力。原因是它完全在客户端运行,在标准兼容的浏览器中运行。下面的屏幕截图显示了它的显示方式:

使用 WebAssembly 托管模型时,需要将所有内容下载到客户端。过程如下:
- 加载了一些执行.NET 仿真器的 JavaScript。
- 将项目组件和所有引用的.NET动态链接库(DLL)加载到客户端。
- 用户界面已更新。
然后,客户端可以断开与服务器的连接,因为不再需要永久连接。
Keep in mind that because applications deployed using the WebAssembly hosting model run on the client browser, in a sandbox, this means that it may not be able to open arbitrary sockets to any host, which prevents it from running Entity Framework (EF) Core, for example. Only connections to the originating host are allowed.
对于服务器模型,Blazor 执行以下操作:
- 呈现一些标记和一些 JavaScript
- 使用信号器向服务器发送事件
- 按照我们应用中的.NET 代码处理它们
- 将响应发送到客户端
- 更新用户界面
因为只使用 signer,所以没有回发和 AJAX 调用(当然,除非 signer 回退到 AJAX,否则它通常使用 WebSockets,如果有的话)。就所有目的而言,Blazor 应用只是一个单页应用(SPA。以下屏幕截图显示 Blazor 的服务器托管模型:

Image taken from https://docs.microsoft.com/en-us/aspnet/core/blazor
在服务器模式下,Blazor 需要连接到服务器,但在 WebAssembly 模式下,在将所有程序集下载到客户端后,Blazor 只能在客户端工作。在这种情况下,它在浏览器中以独立方式运行,无需服务器,如以下屏幕截图所示:

Image taken from https://docs.microsoft.com/en-us/aspnet/core/blazor
从本质上讲,Blazor 的主要优点是使用.NET 作为语言来更新 UI 和执行任何业务逻辑,而不是 JavaScript,这是针对客户端和服务器端的。Blazor 从 UI 获取输入,然后进入服务器,然后返回 UI 的过程称为电路。
实现 Blazor
根据您通过服务器或 WebAssembly 模型托管 Blazor 的方式,实现是完全不同的。Visual Studio 支持创建 Blazor 服务器或 WebAssembly 项目,如以下屏幕截图所示:

接下来,在前面的部分中,让我们看看这是如何工作的。
Blazor 服务器的实现
Blazor 服务器也有dotnet工具,可以按如下方式使用:
dotnet new blazorserver
这将在服务器模式下创建一个 Blazor 示例项目,该项目将与代表性状态传输(RESTweb API 进行通信。这与从 Visual Studio 生成 Blazor 项目并选中 ASP.NET Core hosted 复选框相同。如果您查看生成的项目,您会注意到,首先,Blazor 服务需要注册到ConfigureServices中的依赖项注入(DI框架中,如下所示:
services.AddServerSideBlazor();
在这里,您还可以指定与 Blazor 服务器模型固有使用的 SignalR 服务相关的其他选项,如以下代码段所示:
services
.AddServerSideBlazor()
.AddHubOptions(options =>
{
//options go here
});
这里我不详细介绍,因为它们与第 16 章、实时通信中描述的选项完全相同。
还需要在Configure中注册 Blazor Signal r 集线器的端点,以便 ASP.NET Core 知道如何处理请求,如下所示:
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
正如我们将看到的,需要回退到页面端点。
It is perfectly possible to mix Model-View-Controller (MVC), Razor Pages, and Blazor on the same project.
浏览器必须加载包含调用 Signal 的所有逻辑的 JavaScript 文件,并在收到响应后执行所有的文档对象模型(DOM)操作,如下所示:
<script src="_framework/blazor.server.js"></script>
如果需要,我们可以配置 SignalR 的某些方面。在这种情况下,我们需要告诉 Blazor 不要自动加载默认值,方法是将autostart设置为false并显式调用Blazor.start函数,如下面的代码片段所示:
<script src="_framework/blazor.server.js" autostart="false"></script>
<script>
Blazor.start({
configureSignalR: function (builder) {
builder.configureLogging("information");
}
});
</script>
我们可以在这里设置的选项与第 16 章、实时通信中描述的相同。防止自动加载的另一个原因是检测浏览器是否支持 Blazor WebAssembly(请记住,它在浏览器中运行)。如果不行,我们可以优雅地后退。
一个简单的测试可以是:
<script>
if (typeof(WebAssembly) === 'object' && typeof(WebAssembly.
instantiate) === 'function') {
//the browser supports WebAssembly
Blazor.start();
} else {
location.href = 'BlazorNotSupported.html';
}
</script>
实现 Blazor WebAssembly
对于 Blazor WebAssembly,dotnet工具可用于从模板生成基本项目,如下所示:
dotnet new blazorwasm
创建了一个只包含客户端部分而没有服务器端代码的示例项目。如果你看一下代码,你会发现它与我们所看到的完全不同,因为没有Startup类,但是有Program类。这个类实际上注册了一个名为app的根组件。然后在wwwroot文件夹内的index.html文件中引用该组件。没有任何控制器,只有 Blazor 组件,我们将在后面讨论。
浏览器必须加载两个 JavaScript 文件并运行一些代码,如下所示:
<script src="_framework/blazor.webassembly.js"></script>
<script>
//some code here
</script>
<script src="_framework/wasm/dotnet.3.2.0.js" defer="" integrity="sha256-mPoqx7XczFHBWk3gRNn0hc9ekG1OvkKY4XiKRY5Mj5U=" crossorigin="anonymous"></script>
第一个文件实例化.NET Core 仿真器,第二个文件加载实际的.NET Core JavaScript 代码。请注意,第二个文件中的版本号与我们正在加载的.NET Core 版本相匹配。然后,它加载所有程序集,如下所示:

现在让我们比较两种托管模型。
比较服务器和 WebAssembly
服务器和 WebAssembly 模型有很大不同,原因如下:
- WebAssembly 模型要求将所有代码下载到客户端;这可能会对性能造成一些影响,但当然也可以从缓存中获益。
- WebAssembly 需要一个能够呈现
webassembly的浏览器,这是大多数现代浏览器所能做到的;请注意,它不需要在客户端计算机中安装.NET Core。 - WebAssembly 可以在断开连接的模式下工作,当然,下载应用除外,而服务器模式不能。
See https://caniuse.com/#feat=wasm for browser support for WebAssembly.
除非另有明确说明,否则我们接下来讨论的所有内容都将应用于服务器和 WebAssembly。
接下来,我们将看到 Blazor 的构建块,从页面开始。
页
页面是一种特殊类型的 Blazor 组件,可以通过浏览器直接访问(这并不完全正确,但我们可以这样认为)。它们有.razor扩展名,按照惯例,应该放在我们应用根文件夹下名为Pages的文件夹中(或者放在它下面的文件夹中)。文件的第一行应该有一个@page指令(类似于 Razor Pages)——类似这样:
@page "/Home"
这似乎不必要,但应该包含页面接受的路由,该路由可能与文件名相同,没有.razor扩展名,但不必如此。如果页面没有@page指令,Blazor 将无法直接访问该页面。在本章后面讨论路由时,我们将对此进行更多讨论。
所有 Blazor 组件(页面就是组件)都必须实现一个IComponent接口,其中ComponentBase是最明显的、已经实现的选择。您不需要声明此继承;默认情况下,所有 Blazor 组件都隐式继承自ComponentBase。页面被编译为.NET 类,因此您可以始终通过将typeof运算符应用于要获取页面类型的文件名来引用页面的类型,如以下代码段所示:
@code
{
var mainAppType = typeof(App); //App comes from the App.razor file
}
页面通常具有标记,但也可以具有声明为标记的其他 Blazor 组件。现在我们将讨论页面的语法。
剃刀语法
该语法与您在 Razor 视图中使用的语法完全相同,只是有一些细微的更改,具体如下:
@page:用于 Blazor 页面(非组件)@code:用于代码块而不是@functions,此处不适用@namespace:用于设置生成的.NET 类的名称空间@attribute:用于添加将添加到生成类中的其他属性@attributes:将键值字典呈现为标签中的超文本标记语言(HTML属性@typeparam:用于声明通用/模板组件@:用于.NET 事件处理程序的语法(例如@onclick),不要与 JavaScript 事件混淆
此外,需要注意的是,标记帮助程序不起作用,Blazor 组件将被声明为标记。
除此之外,Razor 页面的所有其他关键字和功能也在 Blazor 页面中工作,包括@inject、@inherits和@implements。它还具有 IntelliSense,可提供代码完成,如以下屏幕截图所示:

我们将继续 Blazor 页面的构建,以及与类相关的内容。
名称空间导入
我们需要为 Blazor 页面中要使用的所有类型导入所有名称空间,方法是添加@using声明:如果我们不添加它们,我们的类型将找不到,因此将不可用。如果我们不希望在每个页面上都这样做,我们可以将所有需要的@using声明添加到名为_Imports.razor的文件中,该文件应放在 web 应用的根目录下,并将自动加载。Blazor 模板已经提供了一些最常见的名称空间。
部分类
因为 Blazor 文件被编译为类,所以您还可以将生成的类拆分为多个文件。例如,对于一个名为SimpleComponent.razor的文件,我们还可以创建一个名为SimpleComponent.razor.cs的分部类,如下所示:
public partial class SimpleComponent : ComponentBase
{
}
如果名称空间和类名相同,并且使用partial关键字,则可以跨多个文件生成类。
页面作为代码
页面不必写为.razor文件,带有标记和代码;它们可以实现为.NET 类。他们只需要实现IComponent(或从ComponentBase继承)。代替@page指令,它们需要有[Route]属性,而不是@layout,它们可以有[Layout]属性,这就是 Blazor 页面的转换方式。代码可以在以下代码段中看到:
[Route("/")]
[Layout(typeof(MainLayout))]
public partial class Index : ComponentBase
{
}
Blazor 在引导程序集上查找页面/组件类,并在决定路由时检查它们的[Route]属性。不要忘记 Blazor 页面/组件需要一个公共的无参数构造函数。
作为组件的页面
请记住,页面是一个组件。因此,如果您愿意,可以将一个页面完美地包含在另一个页面或组件中。只需像包含任何其他组件一样包含它,如下所示:
<MyPage />
接下来,我们将看到如何通过页面布局向页面添加一些结构。
页面布局
Blazor 的页面布局工作方式与 Razor 页面和视图类似。Blazor 页面通常有一个布局,布局需要通过@Body声明声明在何处呈现页面内容,如以下代码段所示:
@inherits LayoutComponentBase
<div>This is a layout</div>
<div>Body goes here:</div>
@Body
但是,Blazor 布局不支持多个部分,只支持单个部分(主体)。
布局在_Imports.razor文件中的路线(见下一节)上声明,这是一种将布局应用于多个页面的方式,如下所示:
@layout MyLayout
或者,它们在组件的代码文件中使用[Layout]属性声明,如下所示:
[Layout(typeof(MainLayout))]
通常布局应该继承LayoutComponentBase,但需要声明;否则,作为组件,它将从ComponentBase继承。LayoutComponentBase的优点是它为内容定义了一个Body属性,您可以在任何地方渲染它。布局通常是从路线定义的(我们稍后会看到),但也可以通过应用@layout指令来定义特定页面的布局,如下所示:
@layout MyLayout
@page "/Home"
最后,与 Razor 布局一样,Blazor 布局也可以嵌套,这意味着布局本身可以有一个布局。
对于下一个主题,我们从页面跳转到全局路由。
路由
对于由 Visual Studio 或dotnet工具生成的默认 Blazor 项目,您会注意到没有进入页面、没有index.html,没有控制器,也没有默认 Razor 页面。正因为如此,当你通过浏览器访问你的应用时,你将进入后备页面,我在实现ing Blazor 服务器部分中提到的页面。如果你看这个页面,你会注意到它本质上是一个简单的 Razor 页面,带有一个<component>标记帮助器—一个刚刚在.NETCore3 中引入的帮助器。这个标记帮助器正在呈现App组件,如果您查看App.razor文件的内部,您会发现一些奇怪的标记,如下所示:
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout=
"@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
这个文件实际上是 Blazor 的主要应用,尽管它只是进行路由。本质上,它定义了两种可能的路线,如下所示:
- 一条
found路线(我们接下来看看这意味着什么) not found路线
如果查看Router根,它定义了一个指向启动程序集的AppAssembly属性;如果需要,可以指定一个附加的AdditionalAssemblies属性,其中包含要加载的附加 Blazor 组件的附加程序集(Razor 类库)列表。
对于found路线,会发生以下情况:
- Blazor 通过查看
@page指令,找到与统一资源定位器(URL的请求相匹配的页面。 - 为其定义了页面布局(在本例中为
MainLayout)。 - 一个视图被实例化,它实际上是一个名为
RouteView的类的实例,该类被传递给所有路由参数(即@routeData。
因此,本质上,我们从来没有直接访问 Blazor 页面(.razor,而是访问一个回退页面,当它检测到请求的页面(或响应该 URL 的页面)存在时,显示该页面。
对于not found路线,以下情况适用:
LayoutView以MainLayout的布局进行实例化。- 默认内容设置为带有错误消息。
当然,可以模仿与found路由相同的行为,也就是说,当找不到请求的页面时,也可以显示特定的页面。只需将该页面添加为<SomePage />Blazor 组件。
在下一节中,我们将讨论页面路由以及如何映射和强制路由参数。
页面路由
页面可以接受许多不同的路由,如以下代码段所示:
@page "/Home"
@page "/"
@page "/My"
当试图从请求 URL 中找到正确的页面时,Blazor 不会查看页面的名称(以.razor结尾的名称),而是查看一个或多个指令。
路线约束
如果您还记得我们在第 3 章、路由中讨论路由时,我们也讨论了路由约束。如果页面需要某些参数,您也需要在此处列出,如下所示:
@page "/Product/{id}"
但是,您还可以指定此参数的类型,以便在提供的请求 URL 与预期 URL 不匹配时,请求将失败。这些是路由约束,如果您还记得的话,语法如下:
@page "/Product/{id:int}"
此处列出了开箱即用约束:
booldatetimedecimaldoublefloatguidintlong
请记住,其中一些(如float和datetime)是特定于区域性的,因此必须注意根据应用使用的区域性提供有效的值。例如,如果应用使用任何英语,则数字小数分隔符为**.**,而如果使用葡萄牙语,则为,。有关路线约束的更多信息,请参考第 13 章、了解测试工作原理。
包罗万象的路线
一网打尽的路线是始终满足的路线。一定是这样的:
@page "/{**path}"
在这里,path没有任何作用;仅用于指示/之后的任何内容。
来自路由的参数绑定
如果您有一个带有参数的路由,并且您以相同的名称(不区分大小写)声明了一个参数,则它将自动绑定到路由参数的值,如下所示:
@page "/Search/{product}"
@code
{
[Parameter]
public string Product { get; set; }
}
页面导航
Blazor 中的页面导航是通过NavigationManager服务实现的,该服务通常从 DI 获得(参见DI部分)。此服务将NavigateTo方法作为其主要方法公开,该方法可以采用相对 URL、Blazor 路由和可选参数来强制加载页面,如以下代码段所示:
[Inject]
public NavigationManager NavigationManager { get; set; }
@code
{
void GoToPage(int page)
{
NavigationManager.NavigateTo($"Page/{page}", forceLoad: false);
}
}
别忘了路线不是以.razor结尾的!
通常,我们不需要强制加载页面;这意味着 Blazor 使用 Signal 异步加载内容(当使用服务器托管模型时),URL 将添加到浏览器的历史记录中,但浏览器不会加载任何新页面。如果我们强制它,则会发生整页加载。
NavigationManager类公开了两个属性,如下所示:
BaseUri(string):Blazor 应用的绝对基数统一资源标识符(URI)Uri(string:当前页面的绝对 URI
而且,当我们通过调用NavigateTo方法请求导航到新页面时,会引发LocationChanged(EventHandler<LocationChangedEventArgs>事件,forceLoad参数设置为false。
然后是<NavLink>组件。此组件使用CSS :active类呈现所提供内容的页面链接,具体取决于此链接是否与当前 URL 匹配。它的行为与<a>元素完全相同,但它具有以下附加属性:
Match(Match:默认前缀(Prefix)或整体(All如何考虑链接
Match属性确定所传递的链接是否被认为是活动的。例如,考虑当前 URL 是 OUTT1TY,并且 OUTT2AY 属性是 OutT3。如果Match属性设置为Prefix,则它将显示为活动,但如果它设置为All,则不会显示。代码可以在以下代码段中看到:
<NavLink href="/someother/page" Match="All">
Jump to some other page
</NavLink>
<NavLink>没有什么大不了的;它的存在只是为了帮助我们将当前页面呈现为活动页面。
建筑构件
组件只是一个扩展名为.razor且符合 Blazor 语法的文件,没有@page指令,因此无法直接访问。但是,它可以包含在其他 Blazor 文件、页面或组件中。
所有组件都隐式继承自ComponentBase,但您可以通过@inherits指令将其修改为其他类。包含组件就像在文件中声明为标记一样简单。例如,如果您的组件被称为SimpleComponent.razor,您将按照如下方式声明它:
<SimpleComponent />
就是这样,但是有一种新的方法来嵌入 Blazor 组件,我们将在下面看到。
标记助手
有一个新的标签助手,AuthT0},它允许我们将一个 BLAZOR 组件嵌入到剃须刀视图的中间。第 9 章、可重用组件中的标记帮助程序部分也对其进行了介绍,但为了完整起见,我将在这里展示一个示例:
<component type="typeof(SomeComponent)" render-mode="ServerPrerendered" param-Text="Hello, World"/>
如果您还记得,SomeComponent只是一些具有Text属性的.razor文件。
呈现组件的第二个选项是通过代码,在 Razor 页面或视图中使用RenderComponentAsync方法,如以下代码片段所示:
@(await Html.RenderComponentAsync<SomeComponent>(RenderMode.Static, new { Text = "Hello, World" }))
RenderComponentAsync的第二个参数是可选的,它应该是一个匿名对象,其属性在要呈现的组件所期望的属性之后命名(并键入)。
Blazor 组件属性
Blazor 组件属性在代码中声明为公共属性,并用[Parameter]属性修饰,如以下代码片段所示:
@code
{
[Parameter]
public string Text { get; set; } = "Initial value";
}
然后,可以使用标记或标记帮助器语法设置值,如以下代码段所示:
<!-- in a .razor file -->
<SomeComponent Text="Hello, World" />
<!-- in a .cshtml file -->
<component type="typeof(SomeComponent)" param-Text="Hello, World" />
注意属性的param-XXX格式。
Properties are case-insensitive—for example, Text is identical to text.
不从字符串值解析属性;如果属性不是数字、布尔值或字符串,则必须传递实际对象。例如,对于TimeSpan属性,必须传递一个实际的TimeSpan对象,如下所示:
@code
{
public class Clock : ComponentBase
{
[Parameter]
public TimeSpan Time { get; set; }
}
}
<Clock Time="@DateTime.Now.TimeOfDay" />
或者,必须传递适当时间的变量,如下所示:
@code
{
TimeSpan now = DateTime.Now.TimeOfDay;
}
<Clock Time="@now" />
尝试传递字符串将导致错误,因为与 Web 表单不同,不会进行解析。
级联特性
级联属性总是从父组件注入子组件。级联属性是不需要显式提供值的参数,因为它们是由框架从包含组件自动设置的。它们可以是任何类型,包括复杂类型。想象你有这样一个:
<CascadingValue Value="Something">
<ProfileComponent />
</CascadingValue>
您在<CascadingValue>中放入的任何内容都将接收Value集合,无论嵌套程度如何(其他组件中的组件也会接收)。接收属性声明如下,例如,在假设的ProfileComponent实例中:
[CascadingParameter]
private string Text { get; set; } = "<not set>";
请注意,这些参数/属性应设置为private,因为手动设置它们的值没有意义。
当然,在设置初始值后,可以在子组件上更改级联属性,但它不会将级联回发起方。实现这一点的最佳方法是在子组件上声明事件并将其与父组件挂钩。
父级根据子级级联参数的匹配类型自动定位子级级联参数,但如果您有多个相同类型的级联参数,则可能需要通过设置名称告诉框架哪个级联参数映射到哪个级联参数,如下所示:
<CascadingValue Name="FirstName" Value="Ricardo">
<ProfileComponent />
</CascadingValue>
他们的声明也应出现在ProfileComponent内,如下所示:
[CascadingParameter(Name = "FirstName")]
private string FirstName { get; set; }
[CascadingParameter(Name = "LastName")]
private string LastName { get; set; }
您还可以按如下方式嵌套多个<CascadingValue>组件,它们的所有值都将传递给任何子组件:
<CascadingValue Name="FirstName" Value="Ricardo">
<CascadingValue Name"LastName" Value="Peres">
<ProfileComponent />
</CascadingValue>
</CascadingValue>
如果级联属性的值从未更改,则可以将其声明为只读;这对性能有好处,因为 Blazor 不必钩住它,也不必随时更新 UI。在以下代码段中可以看到这方面的代码:
[CascadingParameter(Name = "FirstName", IsFixed = true)]
private string FirstName { get; set; }
这是通过IsFixed实现的,默认为false。
捕获所有属性
如果为不存在的参数提供值,则在编译时会出现异常,除非您声明一个 catch all 属性,该属性将捕获与现有参数不匹配的任何参数的值,如下所示:
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object> Attributes { get; set; } = new Dictionary<string, object>();
使用这种方法,您可以传递任意数量的参数,如果它们没有找到匹配的参数,它们只会以InputAttributes结束。此外,还可以将所有属性传递给组件或 HTML 元素。使用@attributes关键字将每个字典元素展平并转换为键值对,如下所示:
<input @attributes="@Attributes" />
这是传递任意数量属性的通用方法,不必担心为每个属性定义属性。
子内容属性
一些组件可以在其中设置标记,包括其他组件;这称为子内容。以下是一个例子:
<ContainerComponent>
<p>This is the child content's markup</p>
</ContainerComponent>
可以在属性中捕获此内容以供以后使用,如下所示:
[Parameter]
public RenderFragment ChildContent { get; set; }
此属性必须被称为ChildContent。它的内容将是呈现的标记,包括父组件内声明的任何组件的内容。现在,在您的组件中,您可以随时随地输出(或者不输出,如果愿意),如以下代码段所示:
<pre>
<!-- child content goes here -->
@ChildContent
</pre>
具有通用参数的组件
可以声明通用(或模板化)组件,即具有通用参数的组件。这样做的好处是使您的组件完全适合您想要的类型。为此,我们使用@typeparam声明声明泛型模板参数,然后将一个或多个字段或属性声明为泛型并具有相同的模板参数,如下所示:
//MyComponent
@typeparam TItem
@code
{
[Parameter]
public IEnumerable<TItem> Items { get; set; }
}
使用此组件的一种方法是为泛型属性声明一个值,并让 Blazor 推断泛型参数类型,如以下代码段所示:
@code
{
var strings = new [] { "A", "B", "C" };
}
<MyComponent Items="strings" />
另一种方法是,如果无法推断类型或我们希望强制执行特定类型,则设置TItem属性,如下所示:
@code
{
var strings = new [] { "A", "B", "C" };
}
<MyComponent Items="strings" TItem="IComparable" />
使用这种方法,Items属性将被原型化为IEnumerable<IComparable>。因为字符串的数组也是IComparables的数组,所以类型匹配。
As of now, it is not possible to define constrained generic types.
组件列表
在呈现其内容可能会更改的组件列表(添加、删除或修改列表中的项目)时,告诉 Blazor 哪些内容标识列表中的每个项目非常重要,以防止出现不必要的行为,例如 Blazor 不知道更新哪些内容。为此,有一个@key属性应用于每个列表项,该属性具有唯一的值,可能是一个复杂的对象。让我们看一个例子。假设您有一个绑定到订单列表的列表,如下所示:
@code
{
foreach (var order in Orders)
{
<Order
@key="order.Id"
Product="order.Product"
Customer="order.Customer"
Date="order.Date" />
}
}
在这里,我们都向一个假设的Order组件传递它需要的数据—Product、Customer、Date——并将订单 ID 设置为其密钥,这意味着每个组件都有一个唯一的标识符。
定位组件
组件有一个与之关联的隐式类,该类位于与其所在文件夹相匹配的命名空间中。例如,如果SomeComponent.razor位于 web app 根文件夹下名为Components的文件夹中,我们需要添加一个@using声明才能包含它,如下所示:
@using MyProject.Components
位于使用它们的同一文件夹或Shared文件夹中的组件将自动找到,而无需@using指令。
渲染模式
组件可以在三种模式(RenderMode中的一种模式)下渲染,必须按如下方式指定:
Static:加载页面时,使用 Blazor 组件的任何参数静态呈现 Blazor 组件。这是最快的选项,但组件无法引发事件,这使得它不适用于任何高级用途;但是,这是默认设置。Server:在服务器上呈现页面,页面加载后才发送给客户端;这是最慢的选项,不能使用任何参数。ServerPrerendered:这是其他两种模式的折衷;Blazor 预先呈现页面,并在页面加载时发送组件,但随后使其成为交互式的。它也不支持参数。
当我们谈论与 DOM 交互和引发事件时,呈现模式是相关的,我们将在稍后介绍。
组件生命周期
每个组件(不要忘记页面和页面布局也是组件)都经历了一个生命周期,在这个生命周期中调用了一些虚拟方法。这些措施依次如下:
SetParametersAsync:设置查询字符串中的参数并绑定任何需要绑定的属性时;如果重写此方法,请确保调用基本实现。OnInitialized/OnInitializedAsync:组件初始化时,有机会更改/设置其他组件或 DOM 元素的属性。OnParametersSet/OnParametersSetAsync:组件初始化且组件父级的所有参数均已设置时。OnAfterRender/OnAfterRenderAsync:组件渲染完成时。
正如您所看到的,其中一些虚拟方法同时具有同步和异步版本。最好覆盖异步版本。
OnAfterRender/OnAfterRenderAsync方法采用一个firstRender参数,该参数指示组件是否是第一次渲染。这可能有助于您进行某种初始化。
还有一种ShouldRender方法值得一提。正如您所想象的,当 Blazor 需要决定组件是否需要更新其 UI 时,就会调用该函数;它不接受任何参数并返回布尔值。由您来实现其逻辑。StateHasChanged方法总是导致ShouldRender被调用,但是第一次呈现组件时(OnAfterRender/OnAfterRenderAsync被调用且firstRender参数设置为true时),无论ShouldRender返回什么,它总是如此。
如果一个组件实现了IDisposable,那么它的Dispose方法将在其生命周期结束时被调用——例如,当它从 UI 中删除时,或者当连接关闭时。但我们必须明确地告诉 Blazor,如下所示:
@implements IDisposable
@code
{
public void Dispose()
{
//dispose the component
//this method will be called automatically by the framework
}
}
从前面的代码片段中,我们可以看到该方法将被框架自动调用。
在不同项目中重用组件
如果 Blazor 组件是在 Razor 类库项目中创建的,那么它们可以跨项目重用。这是一个特殊的项目,可以由 Visual Studio 或使用 dotnet 的razorclasslib模板创建。这在第 9 章、可重用组件中有描述,但本质上,这只是一个 SDK 设置为Microsoft.NET.Sdk.Razor的项目文件,如以下代码段所示:
<Project Sdk="Microsoft.NET.Sdk.Razor">
...
</Project>
其中包含的任何.razor文件都可以从引用该项目的另一个项目中访问;只需为正确的名称空间添加一个@using语句(考虑项目的根名称空间,以及可能嵌套.razor文件的任何文件夹)。
访问 HTTP 上下文
如果需要从组件(或页面)内部访问 HTTP 上下文,只需将IHttpContextAccessor服务注入到类中,如下所示:
@code
{
[Inject]
public IHttpContextAccessor HttpContextAccessor { get; set; }
HttpContext HttpContext => HttpContextAccessor.HttpContext;
}
有关这方面的更多信息,请参阅DI部分。
样品成分
让我们考虑下面的组成部分:
@using System
@using System.Timers
@implements IDisposable
@code
{
private System.Timers.Timer _timer;
[Parameter]
public TimeSpan Delay { get; set; }
[Parameter]
public Action OnElapsed { get; set; }
[Parameter]
public bool Repeat { get; set; }
protected override void OnParametersSet()
{
this._timer = new System.Timers.
Timer(this.Delay.TotalMilliseconds);
this._timer.Elapsed += this.OnTimerElapsed;
this._timer.Enabled = true;
base.OnParametersSet();
}
private void OnTimerElapsed(object sender, ElapsedEventArgs e)
{
this.OnElapsed?.Invoke();
if (!this.Repeat)
{
this._timer.Elapsed -= this.OnTimerElapsed;
this._timer.Enabled = false;
}
}
void IDisposable.Dispose()
{
if (this._timer != null)
{
this._timer.Dispose();
this._timer = null;
}
}
}
这是一个计时器组件,它在一定时间后启动。它公开了以下属性:
Delay(TimeSpan:计时器触发的时间。OnElapsed(Action:定时器触发时要调用的回调。Repeat(bool:是否重复回调;默认值为false。
我们可以看到该组件公开了三个参数,并私下实现了IDisposable接口。它覆盖了OnParametersSet方法,因为当基础设施调用它时,属性已经设置好了;在这种情况下,现在是利用它们的好时机,用Delay参数的值实例化内部计时器。当计时器第一次触发时,组件决定是否继续引发事件,这取决于是否设置了Repeat参数。当处理组件时,它也处理内部计时器。
我们可以按如下方式使用此组件:
<Timer Delay="@TimeSpan.FromSeconds(20)" OnElapsed="OnTick" Repeat="true" />
@code
{
void OnTick()
{
//timer fired
}
}
现在我们来看另一个组件,它只为具有特定角色的用户呈现内容,如下所示:
@code
{
[Inject]
public IHttpContextAccessor HttpContextAccessor { get; set; }
HttpContext HttpContext => HttpContextAccessor.HttpContext;
ClaimsPrincipal User => HttpContext.User;
[Parameter]
public string Roles { get; set; } = "";
[Parameter]
public RenderFragment ChildContent { get; set; }
}
@if (string.IsNullOrWhitespace(Roles) || Roles.Split(",").Any(role => User.IsInRole(role)))
{
@ChildContent
}
本例注入了IHttpContextAccessor服务,然后从中提取当前HttpContext,并从中提取当前User。我们拥有Roles物业和ChildContent。仅当当前用户是Roles属性中提供的任何角色的成员或为空时,才会呈现ChildContent。
正如您所见,构建有用且可重用的组件非常容易!现在,让我们看看如何使用表单——这是我们谈论 web 时的一个常见需求。
使用表单
Blazor 支持使用绑定到模型的表单。有几个组件知道如何绑定到给定类型的属性并相应地将它们显示为 HTMLDOM 元素,还有一个表单组件负责绑定到模型并对其进行验证。
表单编辑
为了验证模型并允许对其进行编辑,要使用的组件为EditForm。其用法如以下代码段所示:
<EditForm
Model="@model"
OnSubmit="@OnSubmit"
OnInvalidSubmit="@OnInvalidSubmit"
OnValidSubmit="@OnValidSubmit">
...
<button>Submit</button>
</EditForm>
@code
{
var model = new Order(); //some model class
}
EditForm组件公开了两个属性,如下所示:
Model(object):包含绑定到表单组件的属性的POCO(简称普通旧公共语言运行库(CLR)对象);它通常是唯一需要的属性。EditContext(EditContext:一种形式语境;通常,它不是显式提供的,而是由EditForm组件为我们生成的。
它还公开了以下三个事件:
OnInvalidSubmit(EventCallback<EditContext>):表单尝试提交但存在验证错误时引发OnSubmit(EventCallback<EditContext>):明确提交表单时提出,无自动验证OnValidSubmit(EventCallback<EditContext>:表单提交成功时提出,无验证错误
如您所见,EditForm需要Model(强制),并且可能需要一个或多个事件处理程序来处理OnSubmit、OnInvalidSubmit或OnValidSubmit事件。在它里面,一定有一些导致提交的 HTML 元素,比如带有type="submit"的button或input——这实际上会触发表单提交。请注意,实际提交将是与OnSubmit或OnValidSubmit处理程序关联的操作。
形式语境
表单上下文是EditContext的实例,由与EditForm类同名的级联属性公开。上下文公开以下属性和事件:
Model(object):模型OnFieldChanged(EventHandler<FieldChangedEventArgs>:字段更改时引发的事件OnValidationRequested(EventHandler<ValidationRequestedEventArgs>:请求验证时引发的事件OnValidationStateChanged(EventHandler<ValidationStateChangedEventArgs>:验证状态改变时引发的事件
上下文还公开了一些方法,这些方法可用于强制验证、检查模型是否已更改或获取验证错误,其中最相关的方法如下:
GetValidationMessages:获取所有验证消息或仅针对某个字段IsModified:检查给定模型属性的值是否已更改MarkAsUnmodified:将特定模型属性标记为未修改NotifyFieldChanged:引发一个事件,通知字段属性更改NotifyValidationStateChanged:引发事件,通知验证状态更改Validate:根据使用中的验证 API 检查当前模型值的有效性
还有一些活动,如下所示:
OnFieldChanged:模型字段值已更改。OnValidationRequested:已请求验证。OnValidationStateChanged:验证状态已更改。
表单上下文可从EditForm或其内部的任何表单组件获得。
表单组件
Blazor 附带的表单组件如下所示:
InputText:以type="text"呈现inputInputTextArea:呈现textareaInputSelect:呈现selectInputNumber(对于int、long、float、double或decimal:用type="number"呈现inputInputCheckbox:以type="checkbox"呈现inputInputDate(对于DateTime和DateTimeOffset:用type="date"呈现input
这些都是非常方便的助手,可以帮助我们避免编写 HTML。这些应该放在EditForm内,并绑定到模型的属性。以下代码段中显示了一个示例:
<EditForm Model="@model">
<InputSelect @bind-Value="@model.Color">
<option></option>
<option>Red</option>
<option>Green</option>
<option>Blue</option>
</InputSelect>
</EditForm>
在本例中,InputSelect有几个选项,并且绑定到模型的Color属性,这可能是这些选项之一。
表单验证
到目前为止,唯一可用的验证器是DataAnnotationsValidator,它使用数据注释 API。为了在表单上进行验证,您需要在EditForm中声明一个验证器,如下所示:
<EditForm Model="@model">
<DataAnnotationsValidator />
...
</EditForm>
如果您为EditForm实例的OnSubmit事件提供了一个处理程序,那么您必须通过调用EditContext.Validate()来强制验证,这将反过来触发数据注释 API 验证,或者您可以自己进行验证。
而且,如果您希望显示验证错误的摘要,还可以包括一个ValidationSummary组件,如下所示:
<EditForm Model="@model">
<DataAnnotationsValidator />
<ValidationSummary Model="@model" />
...
</EditForm>
虽然很方便,但是除了在生成的标记中调整级联样式表(CSS类)之外,您没有什么可以自定义错误消息的显示方式。当然,您也可以自己处理EditForm上的OnInvalidSubmit事件,并为无效数据添加自己的消息,而无需使用ValidationSummary组件。
接下来,使用 DOM 元素!
使用 DOM 元素
在本节中,我们将学习如何使用 DOM 元素:访问它们、绑定属性、添加事件和处理程序,等等。
双向装订
将属性设置为元素值的一种方法是在元素的属性中声明它,如下所示:
<input type="text" name="name" value="@Name" />
命名元素将接收Name属性的值,但它不会将元素绑定到该属性。但是,以下属性将绑定:
<input type="text" name="name" @bind="Name" />
注意@bind关键字的用法。它用于将元素绑定到传递的属性或字段。默认情况下,它钩住元素的value属性,因为这是 DOM 表单元素(input、select、textarea的常用属性。这实际上是双向绑定:当元素的值更改时,属性的值也会更改!
如果我们希望绑定到组件的其他属性,我们只需在bind-之后指定其名称,如下所示:
<MyComponent @bind-Text="Name" />
如果需要为要绑定到的属性指定格式,则有一种特殊语法,如以下代码段所示:
<input type="text" @bind="StartDate" @bind:format="yyyy-MM-dd" />
本例使用特定格式将输入字段绑定到名为StartDate的属性。日期将按照该格式显示。
最后,我们可以指定要绑定到的替代事件,如下所示:
<input type="text" name="name" @bind-value="Name" @bind-value:event="oninput" />
DOM 表单事件的默认事件为onchange,但其他候选事件为oninput或onblur。对于自己组件的自定义事件,必须指定自己的事件。
事件处理
您还可以响应元素引发的事件,如下所示:
<button @onclick="OnButtonClick">Click Me</button>
@code
{
void OnButtonClick(MouseEventArgs e)
{
//button was clicked!
}
}
除了添加一个函数来处理事件外,我们还可以内联执行,这会产生一些难看的代码,如以下代码段所示:
<button @onclick="(evt) => Console.WriteLine("Clicked")>Click Me</button>
为了防止事件的默认行为,有一个特殊的关键字,如以下代码段所示:
<button @onsubmit:preventDefault>Click me</button>
这也可以通过使用布尔属性或字段(如本例中所示)来设置条件,如下所示:
<button @onsubmit:preventDefault="_preventDefault">Click me</button>
还有一个用于停止事件传播的方法,如以下代码段所示:
<button @onclick:stopPropagation>Click me</button>
它还允许使用条件运算符,如以下代码段所示:
<button @onclick:stopPropagation="_stopPropagation">Click me</button>
通过运行以下代码,也可以将我们自己的事件处理程序公开为组件的参数:
[Parameter]
public EventCallback<ChangeEventArgs> OnChange { get; set; }
声明自定义事件处理程序有两个选项,如下所示:
EventCallback<T>:强类型事件处理程序;需要与之匹配的委托EventCallback:接受object参数的委托
对于那些使用过它的人来说,这看起来非常类似于 Web 表单!通过处理程序,我们几乎可以做任何我们想做的事情,比如访问属性和其他组件的值,调用服务器,等等。
Blazor 拥有可由浏览器 DOM 引发的所有事件的类。其中每一项都包含与所发生事件相关的信息,如下表所示:
| 型 | 参数类 | DOM 事件 |
| 剪贴板 | ClipboardEventArgs | oncut、oncopy、onpaste |
| 拖放 | DragEventArgs | ondrag、ondragstart、ondragenter、ondragleave、ondragover、ondrop、ondragend |
| 错误 | ErrorEventArgs | onerror |
| 通用 | EventArgs | onactivate、onbeforeactivate、onbeforedeactivate、ondeactivate、onended、onfullscreenchange、onfullscreenerror、onloadeddata、onloadedmetadata、onpointerlockchange、onpointerlockerror、onreadystatechange、onscroll``onbeforecut、onbeforecopy、onbeforepaste``oninvalid、onreset、onselect、onselectionchange、onselectstart、onsubmit``oncanplay、oncanplaythrough、oncuechange、ondurationchange、onemptied、onpause、onplay、onplaying、onratechange、onseeked、onseeking、onstalled、onstop、onsuspend、ontimeupdate、onvolumechange、onwaiting |
| 集中 | FocusEventArgs | onfocus、onblur、onfocusin、onfocusout |
| 输入 | ChangeEventArgs | onchange、oninput |
| 键盘 | KeyboardEventArgs | onkeydown、onkeypress、onkeyup |
| 老鼠 | MouseEventArgs | onclick、oncontextmenu、ondblclick、onmousedown、onmouseup、onmouseover、onmousemove、onmouseout |
| 鼠标指针 | PointerEventArgs | onpointerdown、onpointerup、onpointercancel、onpointermove、onpointerover、onpointerout、onpointerenter、onpointerleave、ongotpointercapture、onlostpointercapture |
| 鼠标滚轮 | WheelEventArgs | onwheel、onmousewheel |
| 进步 | ProgressEventArgs | onabort、onload、onloadend、onloadstart、onprogress、ontimeout |
| 触摸 | TouchEventArgs | ontouchstart、ontouchend、ontouchmove、ontouchenter、ontouchleave、ontouchcancel |
所有这些类都继承自 PytT0::对于事件类,考虑继承,并且添加席 T1 后缀也被认为是一个很好的实践。
引用元素
元素或自定义组件可以与字段或属性关联。这样,您就可以通过编程方式访问它的公共 API。实现这一点的方法是向其添加一个指向适当类型的字段或属性的@ref属性,如以下代码片段所示:
<MyComponent @ref="_cmp" />
@code
{
MyComponent _cmp;
}
如果我们讨论的是通用 DOM 元素,那么字段或属性的类型必须为ElementReference。您还可以声明此类型的参数属性,并将属性从一个组件传递到另一个组件;这样,就可以传递 DOM 元素引用。顺便说一下,ElementReference不公开Id以外的任何属性或方法。与它引用的元素交互的唯一方法是通过 JavaScript 互操作性(没有可以在此对象上调用的属性或方法)。
但要注意:ElementReferences仅在调用OnAfterRender/OnAfterRenderAsync方法时设置;在此之前,他们只是null。
更新状态
在对组件的属性或绑定到组件或 DOM 元素的属性进行更改后,需要告诉 Blazor 更新 UI:为此,我们有StateHasChanged方法。调用时,Blazor 将重新呈现该组件,该组件可以是整个页面,也可以只是一个子组件。
接下来,让我们看看 Blazor 是如何支持 DI 的。
DI
当然,Blazor 对 DI 有着丰富的支持。如您所知,这提高了代码的可重用性、隔离性和可测试性。服务注册通常通过Startup类的ConfigureServices方法(对于服务器模型)或Program类的WebAssemblyHostBuilder.Services集合(对于 WebAssembly)完成。
注射服务
Blazor 可以使用在 DI 框架上注册的任何服务。这些可以通过.razor文件上的@inject指令检索,其工作方式与 Razor 视图中完全相同,如以下代码段所示:
@inject IJSRuntime JSRuntime
或者,在代码(一个@code块或一个分部类)上,您还可以使用[Inject]属性装饰属性,以便从 DI 填充它,如以下代码片段所示:
@code
{
[Inject]
IJSRuntime JSRuntime { get; set; }
}
在这种情况下,属性可以具有任何可见性(例如,公共、私有或受保护)。
您不能忘记的一件事是,如果对页面使用分部类,则不能在构造函数中注入依赖项。Blazor 要求其页面和组件使用公共的无参数构造函数。
注册服务
一些服务已经为我们预先注册,如下所示:
-
IJSRuntime:用于 JavaScript 互操作性检查(Scoped用于服务器,Singleton用于 WebAssembly)。 -
NavigationManager:用于导航和路由(Scoped用于服务器,Singleton用于 WebAssembly)。 -
AuthenticationStateProvider:用于认证(Scoped)。 -
IAuthorizationService:对于授权(Singleton)-这当然不是 Blazor 特有的。
您可以使用@inject或[Inject]方法访问它们。
作用域寿命
Scoped生存期有一个区别:在服务器托管模型中,它映射到当前连接(即,它持续到连接断开或浏览器刷新),而在 WebAssembly 中,它与Singleton相同。
接下来,我们将继续了解如何使用 JavaScript。
JavaScript 互操作性
由于 Blazor 在浏览器上运行,因此在某些情况下,我们可能需要执行浏览器本机功能。因此,没有办法避免 JavaScript!JavaScript 和 Blazor(.NET)可以通过两种方式进行互操作,如下所示:
- .NET 调用 JavaScript 函数。
- JavaScript 调用.NET 方法。
从.NET 调用 JavaScript 函数
Blazor 可以调用托管网页上的任何 JavaScript 函数。它通过IJSRuntime对象来实现这一点,当您注册 Blazor 时,DI 框架会自动提供该对象。
例如,在.razor文件中,添加以下代码:
@inject IJSRuntime JSRuntime;
function add(a, b) { return a + b; }
@code
{
var result = await JSRuntime.InvokeAsync<int>("add", 1, 1);
}
IJSRuntime允许您通过调用InvokeAsync以名称调用任何函数,传递任意数量的参数并接收强类型结果。如果 JavaScript 函数不返回任何内容,可以通过InvokeVoidAsync调用,如下所示:
await JSRuntime.InvokeVoidAsync("alert", "Hello, World!");
现在让我们看看如何做相反的事情,即从 JavaScript 调用.NET 代码!
从 JavaScript 调用.NET 方法
从网页上,JavaScript 可以调用 Blazor 组件上的方法,只要它们是public、static并用[JSInvokable]属性修饰,如以下代码片段所示:
[JSInvokable]
public static int Calculate(int a, int b) { return a + b; }
调用实例函数(如前面代码段中所示)的语法如下:
var result = DotNet.invokeMethod('Blazor', 'Calculate', 1, 2);
或者,如果希望异步执行操作,请执行以下代码:
[JSInvokable]
public static async Task<int> CalculateAsync(int a, int b) { return a + b; }
DotNet
.invokeMethodAsync('Blazor', 'CalculateAsync', 1, 2)
.then((result) => {
console.log(`Result: ${result}`);
});
这里,Blazor是我的 Blazor 项目/应用的名称;不一定是这样。
如果我们需要在某个类上调用实例方法,我们需要将其包装在DotNetObjectReference对象中,并将其返回给 JavaScript,如下所示:
public class Calculator
{
[JSInvokable]
public int Calculate(int a, int b) { return a + b; }
}
var calc = DotNetObjectReference.Create(new Calculator());
await JSRuntime.InvokeVoidAsync("calculate", calc);
然后,在 JavaScript 端,调用invokeMethod或invokeMethodAsync对接收到的对象调用公共实例方法,如下所示:
function calculate(calc) {
var result = calc.invokeMethod('Calculate', 1, 2);
}
因此,在前面的代码片段中,我们通过DotNetObjectReference.Create创建了一个Calculator类型的.NET 对象,并将对它的引用存储在局部变量中。然后通过JSRuntime.InvokeVoidAsync将该变量传递给 JavaScript 函数,在该函数(calculate中,我们最终使用invokeMethod调用带有一些参数的.NETCalculate方法。一个相当复杂但必要的方法!
接下来,我们将看到如何保持状态。
维持状态
在状态管理方面,有以下几种选择:
- 使用 DI 管理的对象保持状态
- 使用 ASP.NET Core 会话(仅适用于服务器托管模型)
- 使用 HTML 元素中保留的状态
- 在浏览器上保存状态
对于 DI 选项,这应该很简单:如果我们注入一个具有Singleton或Scoped生存期的容器服务,保存到它的任何数据都将达到该生存期的边界。第 4 章、控制器和动作中也描述了会话存储。将数据保存在 HTML 元素中非常简单,并且由于不需要回发,也不需要重新填充表单元素,因此这比传统的 web 编程更容易实现。
使用localStorage或sessionStorage在浏览器上保存状态是另一个主题。一种方法是使用 JavaScript 互操作性来直接调用这些浏览器对象中的方法,这很麻烦,但也有可能。假设我们公开了一组简单的函数,如下所示:
window.stateManager = {
save: (key, value) => window.localStorage.setItem(key, value),
load: (key) => window.localStorage.getItem(key),
clear: () => window.localStorage.clear(),
remove: (key) => window.localStorage.removeItem(key)
};
然后,我们可以很容易地使用 JavaScript 互操作性调用这些函数,正如我们前面看到的,如下所示:
var value = await JSRuntime.InvokeAsync<string>("stateManager.load", "key");
但也存在以下问题:
- 我们需要自己包装 JavaScript 调用。
- 任何复杂类型之前都需要序列化为 JSON。
- 没有数据保护。
另一种选择是使用第三方库为我们完成这项工作。微软目前有一个名为Microsoft.AspNetCore.ProtectedBrowserStorage的预览版 NuGet 库,它不仅可以通过数据保护 API以安全的方式访问浏览器存储设施。这意味着,如果您查看使用浏览器工具存储的值,您将无法从中获益,因为它们是加密的。微软确实警告说,这个库还没有准备好用于生产,但最终,它会到达那里,所以我将向您展示如何使用它。
因此,在添加对Microsoft.AspNetCore.ProtectedBrowserStoragepreview NuGet 包的引用后,您需要确保每次使用脚本文件时都将其加载到浏览器中;只需在_Host.cshtml文件中添加以下代码,例如:
<script src="_content/Microsoft.AspNetCore.ProtectedBrowserStorage/
protectedBrowserStorage.js"></script>
现在,您需要在 DI 框架(ConfigureServices中注册一些服务,如下所示:
services.AddProtectedBrowserStorage();
嘿,普雷斯托,您现在注册了两个附加服务,ProtectedSessionStorage(用于sessionStorageDOM 对象)和ProtectedLocalStorage(用于localStorage),这两个服务都使用相同的公共 API,基本上提供了三种方法,如下所示:
ValueTask SetAsync(string key, object value):将值保存到存储ValueTask<T> GetAsync<T>(string key):从存储中检索值ValueTask DeleteAsync(string key):从存储中删除密钥
设置复杂值(POCO 类)时,首先将其序列化为 JSON。现在,您可以将所需的服务注入 Blazor 页面或组件,并开始使用它以安全的方式在客户端持久化数据。
For more information about sessionStorage and localStorage, please see https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage and https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage.
下一节将介绍从 Blazor 应用进行 HTTP 调用的推荐方法。
进行 HTTP 调用
Blazor 应用的一个典型需求是进行 HTTP 调用。想想 AJAX 风格(XMLHttpRequest或fetch操作,它们是 spa 的主要业务。为此,我们需要一个 HTTP 客户端,最方便的是HttpClient。
我们首先需要在ConfigureServices方法中为其注册服务(对于服务器托管模型),如下所示:
services.AddHttpClient();
然后,我们可以在 Blazor 应用中注入IHttpClientFactory服务,并从中构建HttpClient,如下面的代码片段所示:
[Inject]
public IHttpClientFactory HttpClientFactory { get; set; }
HttpClient HttpClient => HttpClientFactory.CreateClient();
AddHttpClient有不同的重载,因为当我们需要使用特定设置配置命名客户机时,会出现默认标题、超时,然后在CreateClient中创建该客户机,但我在这里不做详细介绍。
HttpClient可以发送POST、GET、PUT、DELETE和PATCH请求,但您需要提供文本等内容,这意味着您可能需要将一些类序列化为 JSON,因为这是目前最常见的格式。你可以考虑的另一个选择是,To.T6.预览 NuGuT 包,它会处理这个问题,但当然它仍然不在最终版本中,这意味着它可能仍然包含 bug,或者它的 API 将来可能会改变,所以请警告。此包公开了HttpClient上的扩展方法,这些扩展方法已经允许您对POST、GET、PUT、DELETE和PATCH内部序列化为 JSON 的任何内容进行扩展。
要序列化为 JSON,最好的方法是通过执行以下代码,使用新的System.Text.JsonNuGet 包,这是一种轻量级的、更高性能的 JSON.NET 方法(Newtonsoft.Json:
var json = JsonSerializer.Serialize(item);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await HttpClient.PostAsync(relativeUrl, content);
再简单不过了:我们将一些负载序列化为 JSON,然后用它创建一个字符串内容消息,然后异步地将其发布到某个 URL。
应用安全性
在这里,我们将看到如何在 Blazor 应用中强制执行安全规则。在本文中,我们将介绍身份验证和授权这两个主要的安全主题,并简要介绍跨源资源共享(CORS)。
请求授权
Blazor 使用与 ASP.NET Core 相同的身份验证机制,即基于 Cookie 的身份验证:如果我们通过了 ASP.NET Core 的身份验证,那么我们就通过了 Blazor 的身份验证。至于授权,Blazor 资源(页面)通过应用一个[Authorize]属性进行保护,该属性可以有属性,也可以没有属性(角色或策略更通用)。属性可以通过在.razor文件或.cs代码隐藏文件上应用@attribute指令应用于页面,如下所示:
@attribute [Authorize(Roles = "Admin")]
Mind you, it is pointless to apply [Authorize] attributes to components—they only make sense in pages.
如果我们想要执行授权规则,我们必须修改App.razor文件并使用AuthorizeRouteView,如下所示:
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout
="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
如果您将此路由定义与前一个路由定义进行比较,您会注意到唯一的区别是我们将RouteView替换为AuthorizeRouteView。当使用AuthorizeRouteView组件时,我们可以使用AuthorizeView组件,在我们的页面和组件中选择性地显示以下状态:
Authorized:用户通过身份验证和授权后显示内容。Authorizing:仅对于 WebAssembly 模型,当 Blazor 应用使用外部端点进行授权时,显示此内容。NotAuthorized:用户未授权时显示内容。
例如,请查看以下代码段:
<AuthorizeView>
<Authorized>
<p>Welcome, authenticated user!</p>
</Authorized>
<NotAuthorized>
<p>You are not authorized to view this page!</p>
</NotAuthorized>
</AuthorizeView>
AuthorizeView组件还可以将以下内容作为属性:
Roles:以逗号分隔的角色列表,用于检查成员资格Policy:用于检查授权的策略的名称Resource:可选资源
如果没有提供这些属性,则表示它需要经过身份验证的用户。
组件使用IAuthorizationService.AuthorizeAsync方法作为 DI 框架自动注入的IAuthorizationService安全检查的真实来源。
获取当前用户
我们可以通过以下三种方式之一以编程方式检查当前用户的身份:
- 通过注入
AuthenticationStateProvider并检查其身份验证状态,如下代码段所示:
@inject AuthenticationStateProvider AuthenticationStateProvider
@code
{
var authState = await AuthenticationStateProvider
.GetAuthenticationStateAsync();
var user = authState.User;
if (user.Identity.IsAuthenticated)
{
//authenticated
}
}
- 通过使用
<CascadingAuthenticationState>级联值组件注入身份验证状态任务作为级联参数,如以下代码段所示:
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData"
DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
- 通过注入
IHttpContextAccessor服务,提取当前HttpContext,并从中提取当前User,如下代码片段所示:
@code
{
[Inject]
public IHttpContextAccessor HttpContextAccessor { get; set; }
HttpContext HttpContext => HttpContextAccessor.HttpContext;
ClaimsPrincipal User => HttpContext.User;
}
包装整个<Router>没有问题;我们得到的只是所有应用中名为AuthenticationStateTask的级联参数,如以下代码片段所示:
[CascadingParameter]
private Task<AuthenticationState> AuthenticationStateTask { get; set; }
@code
{
var authState = await AuthenticationStateTask;
var user = authState.User;
if (user.Identity.IsAuthenticated)
{
//authenticated
}
}
这三种方法非常相似。AuthenticationState类型仅公开属于ClaimsPrincipal类型的User属性;这提供了对身份验证过程提供的所有声明的访问。
显式检查权限
一旦我们掌握了ClaimsPrincipal,我们就可以通过利用IAuthorizationService(可从 DI 库获得)来评估它是否符合给定的策略,如下所示:
@inject IAuthorizationService AuthorizationService
@code
{
async Task<bool> IsAuthorized(ClaimsPrincipal user, string
policyName, object resource = null)
{
var result = await AuthorizationService.AuthorizeAsync(
user: user,
policyName: policyName,
resource: resource);
return result.Succeeded;
}
}
如果您记得第 11 章、安全中的内容,则会触发注册的授权处理程序并返回相应的结果。
或者,如果我们只需要检查当前用户是否属于某个角色,我们只需要调用IsInRole,如下所示:
var isInRole = user.IsInRole(roleName);
请记住,角色通常映射到声明。
科尔斯
建议您通过添加 CORS 中间件并将[DisableCors]属性应用于控制器,或通过创建适当的策略,在希望仅对 Blazor 可用的端点上禁用 CORS。更多信息请参见第 11 章、安全。
现在让我们看看如何对 Blazor 组件进行单元测试。
单元测试
我们可以使用我们在第 13 章中看到的单元测试概念和框架,理解测试是如何工作的,但是微软(同样,与 Steve Sanderson 合作)也一直在努力,如果我们与 Blazor 打交道,我们的生活会变得更轻松。
Steve 有一个项目,可在 GitHub 上访问https://github.com/SteveSandersonMS/BlazorUnitTestingPrototype ,其中包含一个单元测试框架的原型,可用于轻松测试 Blazor 组件。它被称为Microsoft.AspNetCore.Components.Testing,不幸的是,它在 NuGet 上仍然不可用,但您可以克隆代码并直接使用它。然后,您可以编写如下代码:
var host = new TestHost();
//Counter is a Blazor component
var component = host.AddComponent<Counter>();
//count is a named element inside Counter
var count = component.Find("#count");
Assert.NotNull(count);
var button = component.Find("button");
Assert.NotNull(button);
button.Click();
正如你所看到的,它很容易使用。希望史蒂夫和微软能尽快在 NuGet 上发布,让我们能够更轻松地使用它。
总结
在本章中,我们看到了 Blazor,这是微软在.NETCore3.0 中提供的一种新的、很酷的技术。它仍处于非常早期的阶段,在功能、社区采用和库方面,人们对它有很多期望。
建议将工作拆分为组件,并使用页面布局,这是 Razor 页面和视图的常见做法。
在本章中,我们看到需要在服务器上保留复杂的逻辑。请记住,当 WebAssembly 托管模型出现时,所有程序集都需要发送到客户端,因此需要保持小规模,并尽可能使用最小的逻辑。
从一开始就考虑安全问题,并为您希望保持安全的应用的关键部分定义策略和角色。
强制路由约束很重要,因为它们将使代码更具弹性和容错性。下一章将在这个版本的 ASP.NET Core 中包含一些新主题。
You can see more examples for the Server hosting model of Blazor from Microsoft at https://github.com/aspnet/AspNetCore.Docs/tree/master/aspnetcore/blazor/common/samples/3.x/BlazorServerSample and also from the Blazor workshop (also from Microsoft) at https://github.com/dotnet-presentations/blazor-workshop.
问题
您现在应该能够回答以下问题:
- 页面和组件之间的区别是什么?
- 服务器和 WebAssembly 托管模型之间的区别是什么?
- 我们可以在 Blazor 页面中使用标记帮助器吗?
- 可以从 Blazor 内部访问包含的网页吗?
- Blazor 支持 DI 吗?
- Blazor 页面布局是否支持区域?
- 组件的不同渲染模式之间有什么区别?
十八、gRPC 和其它话题
在本章中,我们将介绍一些不适合本书前几章的主题。这是因为,尽管它们很重要,但在前面的章节中,它们没有理想的位置,或者它们需要自己的小章节。
其中一些主题非常重要,即谷歌远程过程调用(gRPC),这是一种跨平台、跨技术、强类型消息传递的新技术。gRPC 与新的 ASP.NET Core 端点路由系统集成良好,该系统允许 ASP.NET Core 服务于您能想到的几乎任何协议。我们还将介绍使用实体框架(EF核心)和 ASP.NET Core 的最佳实践。静态文件也很重要,因为我们离不开它们。
基本上,我们将在本章中介绍的主题包括:
- 地区
- 静态文件
- 应用生存期事件
- 习俗
- 嵌入式资源
- 主机扩展
- URL 重写
- 后台服务
- 使用 EF 核
- 理解 gRPC 框架
- 使用 HTTP 客户端工厂
让我们看看它们都是关于什么的。
技术要求
为了实现本章中介绍的示例,您需要.NET Core 3 SDK 和文本编辑器。当然,VisualStudio2019(任何版本)满足所有要求,但您也可以使用 VisualStudio 代码。
本章的源代码可从 GitHub 的检索 https://github.com/PacktPublishing/Modern-Web-Development-with-ASP.NET-Core-3-Second-Edition 。
使用区域组织代码
区域是以逻辑方式物理分隔应用内容的功能。例如,您可以有一个区域用于管理,另一个区域用于其他内容。这在大型项目中特别有用。每个区域都有自己的控制器和视图,这在第 3 章、路由中进行了讨论。
为了使用区域,我们需要在我们的应用中创建一个与Controllers和Views级别相同的Areas文件夹。在它下面,我们将创建一个特定的区域文件夹,例如,Admin——在它里面,我们需要一个类似于根目录中的结构,即,Controllers和Views文件夹:

代码中区域的使用
控制器的创建方式相同,但我们需要添加一个[Area]属性:
[Area("Admin")]
public class ManageController : Controller
{
}
It is OK to have multiple controllers with the same name, provided they are in different namespaces (of course) and are in different areas.
此控制器的视图将自动位于Areas/Admin/Views/Manage文件夹中;这是因为内置的视图位置扩展器(您可以在第 5 章、视图中阅读)已经查看了Areas下的文件夹。我们需要做的是在Configure方法中的默认路由(或任何定制路由)之前为该区域注册路由:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "areas",
pattern: "{area:exists}/{controller=Home}/{action=Index}");
endpoints.MapAreaControllerRoute(
name: "default",
areaName: "Personal",
pattern: "{area=Personal}/{controller=Home}/{action=Index}
/{id?}");
});
对MapControllerRoute的调用只是确保任何存在指定区域的路径都被接受,而对MapAreaControllerRoute的调用注册了一个名为Personal的显式区域,该区域实际上并不需要,因为第一次调用将覆盖该区域。
路由现在有一个额外的内置模板令牌-[area],您可以在路由中使用该令牌,使用方式与[controller]和[action]几乎相同:
[Route("[area]/[controller]/[action]")]
让我们看看如何在内置标记帮助器中引用区域。
标记和 HTML 帮助程序
包含的标记帮助程序(如<a>、<form>等)识别asp-area属性,可用于生成特定区域下控制器的正确 URL:
<a asp-controller="Manage" asp-action="Index" asp-area="Admin">Administration</a>
但是,HTML 帮助程序并非如此,您需要显式提供路由的area参数:
@Html.ActionLink(
linkText: "Administration",
actionName: "Index",
controllerName: "Manage",
routeValues: new { area = "Admin" } )
此代码生成一个超链接,该超链接除了引用控制器和操作外,还引用控制器所在的区域,例如/Admin/Manage/Index。
现在让我们从区域转到静态文件。我们不能没有他们,因为我们即将发现!
使用静态文件和文件夹
ASP.NET Core 可以提供静态文件、图像、样式表、JavaScript 脚本和文本,甚至支持文件系统文件夹。这非常有用,因为它们非常重要,因为并非所有内容都是动态生成的。让我们首先关注它们的配置。
配置
VisualStudio 中的默认模板包括Microsoft.AspNetCore.StaticFilesNuGet 包,该包也包含在Microsoft.AspNetCore.All元包中。此外,初始化主机 Kestrel 或 HTTP.sys 的代码将应用的根文件夹定义为用于提供静态文件(如 HTML、JavaScript、CSS 和图像)的根文件夹,如Directory返回的。
这是.GetCurrentDirectory(),但是您可以在Program类中使用UseContentRoot来更改,在Program类中主机被初始化:
public static IHostBuilder CreateHostBuilder(string[] args)
{
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.UseContentRoot("<some path>")
.UseStartup<Startup>();
});
}
请注意,这里实际上涉及两个文件夹:
- 承载 web 应用的根文件夹
- 提供文件的根文件夹
稍后我们将进一步探讨这一点。
允许目录浏览
可以为整个应用启用文件系统的目录浏览;只需在没有任何参数的情况下调用UseDirectoryBrowser(在Configure中):
app.UseDirectoryBrowser();
Beware—if you do this instead of running the default action of the default controller, you will end up with a file listing the files on the root folder!
但是,您可能希望在虚拟路径下公开根文件夹:
app.UseDirectoryBrowser("/files");
请注意前导正斜杠(**/**)字符,这是必需的。另外,请注意,如果您不包括对静态文件的支持(我们将在下面介绍),则您将无法下载任何静态文件,并将返回一条 HTTP404 Error消息。这不是一个物理位置,而是 ASP.NET Core 显示wwwroot文件夹中文件的路径。
再深入一点,可以指定要返回哪些文件以及如何呈现它们;这是通过采用DirectoryBrowserOptions参数的UseDirectoryBrowser重载完成的。此类具有以下属性:
RequestPath(string:虚拟路径。FileProvider(IFileProvider:获取目录内容的文件提供者。默认为null,此时将使用PhysicalFileProvider。
配置根目录的示例如下所示:
app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
RequestPath = "/resources",
FileProvider = new EmbeddedFileProvider(Assembly.GetEntryAssembly())
});
这个非常简单的示例公开了/resources虚拟路径下当前应用的所有程序集嵌入资源。
You can serve multiple directory browsers with different options.
您可以通过查看IWebHostEnvironment.ContentRootPath来检索安装 ASP.NET Core 应用的文件夹。默认根目录(wwwroot可用作IWebHostEnvironment.WebRootPath。只需使用Path.GetFullPath("wwwroot")即可,它将获得wwwroot文件夹的完整路径。
提供静态文件
为了服务(允许下载)静态文件,我们需要调用UseStaticFiles:
app.UseStaticFiles();
同样,可以将虚拟根目录设置为任意文件夹,如前一节所述:
app.UseStaticFiles("/files");
但是,您需要做更多的工作才能真正提供文件服务。
还有另一个过载UseStaticFiles采用StaticFileOptions参数。它具有以下属性:
DefaultContentType(string:未知文件的默认内容类型。默认值为null。ServeUnknownFileTypes(bool:是否提供 MIME 类型未知的文件。默认值为false。ContentTypeProvider(IContentTypeProvider:用于获取给定扩展的 MIME 类型。FileProvider(IFIleProvider:用于检索文件内容的文件提供者;默认为null,即使用PhysicalFileProvider。OnPrepareResponse(Action<StaticFileResponseContext>):可用于拦截响应的处理程序,例如设置默认头或明确拒绝响应。RequestPath(string:虚拟基路径。
如果要指定虚拟路径,请务必使用此重载:
app.UseStaticFiles(new StaticFileOptions
{
RequestPath = "/files",
FileProvider = new PhysicalFileProvider(Path.GetFullPath("wwwroot"))
});
对于嵌入文件,请使用以下命令:
app.UseStaticFiles(new StaticFileOptions
{
RequestPath = "/resources",
FileProvider = new EmbeddedFileProvider(Assembly.GetEntryAssembly())
});
You can have multiple calls to UseStaticFiles, as long as they have different RequestPath parameters.
对于文件,设置内容(MIME类型很重要;这是从文件扩展名推断出来的,我们可以选择是否允许下载未知文件类型(扩展名没有注册 MIME 类型的文件),如下所示:
app.UseStaticFiles(new StaticFileOptions
{
DefaultContentType = "text/plain",
ServeUnknownFileTypes = true
});
在这里,我们允许下载任何具有未知扩展名的文件,并使用text/plain内容类型为其提供服务。如果ServeUnknownFileTypes未设置为true并且您尝试下载这样的文件,您将收到一条 HTTP404 Error消息。
但是,有一个类知道公共文件扩展名-FileExtensionContentTypeProvider-并实现了IContentTypeProvider,这意味着我们可以将其分配给StaticFileOptions的ContentTypeProvider属性:
var provider = new FileExtensionContentTypeProvider();
provider.Mappings[".text"] = "text/plain";
app.UseStaticFiles(new StaticFileOptions
{
ContentTypeProvider = provider,
DefaultContentType = "text/plain",
ServeUnknownFileTypes = true
});
如您所见,我们正在向内置列表中添加一个新的扩展名(.text及其关联的 MIME 类型(text/plain。如果您感到好奇,您可以对它进行迭代以查看它包含的内容,或者通过调用Clear从头开始。扩展名需要有一个.字符,并且不区分大小写。
与目录浏览一样,我们可以通过设置FileProvider属性来指定IFileProvider,用于检索实际的文件内容。
如果我们想为所有文件设置一个自定义头或实现安全性(这在静态文件处理中明显不存在),我们可以使用以下方法:
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers.Add("X-SENDER", "ASP.NET Core");
};
});
当然,我们也可以返回 HTTP 错误代码、重定向或任何其他类型的操作。
提供默认文件
如果我们启用目录浏览,我们还可以提供一个默认文档(如果它存在的话)。为此,我们需要通过调用UseDefaultFiles(总是在UseStaticFiles之前)来添加一些中间件:
app.UseDefaultFiles();
app.UseStaticFiles();
不用说,可以通过传递一个DefaultFilesOptions实例来配置默认文档。此类包含以下内容:
DefaultFileNames(IList<string>:要服务的默认文件的有序列表RequestPath(string:虚拟路径FileProvider(IFileProvider:获取文件列表的文件提供者,默认为null
如果您感兴趣,默认文件名如下:
default.htmdefault.htmlindex.htmindex.html
配置单个默认文档的示例如下:
app.UseDefaultFiles(new DefaultFilesOptions
{
DefaultFileNames = new [] { "document.html" };
});
如果任何可浏览的文件夹中存在名为document.html的文件,则将提供该文件,并且不会列出该文件夹的内容。
应用安全性
正如我前面提到的,静态文件处理没有安全性,但是我们可以使用StaticFileOptions的OnPrepareResponse处理程序作为基础来实现我们自己的机制,如下所示:
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
//check if access should be granted for the current user and file
if (!AccessIsGranted(ctx.File, ctx.Context.User))
{
ctx.Context.Response.StatusCode = (int) HttpStatusCode.
Forbidden;
ctx.Context.Abort();
}
};
});
如果您想从wwwroot文件夹之外提供文件,请传递一个自定义IFileProvider属性,可能是PhysicalFileProvider的一个实例,该实例设置为使用不同的根目录。
文件提供者
ASP.NET Core 包括以下文件提供程序,它们是IFileProvider的实现:
PhysicalFileProvider:查找文件系统上的物理文件EmbeddedFileProvider:用于访问程序集中嵌入的文件,区分大小写ManifestEmbeddedFileProvider:当嵌入到程序集中时,使用程序集中编译的清单重建嵌入文件的原始路径CompositeFileProvider:组合多个文件提供程序NullFileProvider:始终返回null
文件提供程序(IFileProvider实现)负责以下工作:
- 返回给定文件夹的文件列表(
GetDirectoryContents - 返回文件夹中命名文件的信息(
GetFileInfo - 当文件掩码(文件夹中的文件)更改时获取通知(
Watch
具体如何实现这一点取决于提供者,甚至可能只在虚拟环境中发生。
如您所见,有两个提供程序可以处理嵌入式文件。两者的区别在于ManifestEmbeddedFileProvider在程序集开始构建时以完全逼真的方式尊重文件系统的结构,并允许我们正确地枚举目录。也优于EmbeddedFileProvider。
现在让我们从物理文件转到应用事件。
应用生存期事件
ASP.NET Core 公开整个应用生命周期的事件。您可以连接到这些事件,以便在它们即将发生时收到通知。这些事件由主机使用应用(IHostLifetime调用,第 1 章、ASP.NET Core 入门对此进行了解释。这个接口的入口点是IHostApplicationLifetime接口,您可以从依赖项注入框架获得该接口。它公开了以下属性:
ApplicationStarted(CancellationToken:主机完全启动并准备等待请求时引发。ApplicationStopping(CancellationToken):当应用即将在所谓的正常关闭中停止时引发。某些请求可能仍在处理中。ApplicationStopped(CancellationToken:应用完全停止时引发。
其中每一个都是一个CancellationToken属性,这意味着它可以传递给任何接受此类参数的方法,但更有趣的是,这意味着我们可以向其添加自己的处理程序:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime events)
{
events.ApplicationStopping.Register(
callback: state =>
{
//application is stopping
},
state: "some state");
events.ApplicationStarted.Register(state =>
{
//application started
var appParameter = state as IApplicationBuilder;
}, app);
}
state参数为可选参数;如果未提供,callback参数不接受任何参数。
有很多方法可以导致优雅的关机,一种是通过调用IHostApplicationLifetime接口的StopApplication方法,另一种是通过添加app_offline.htm文件。如果存在这种类型的文件,应用将停止响应,并在每个请求中返回其内容。
最后,您应该尽快连接到应用事件,无论是在Configure方法中(如图所示),还是在应用引导过程中:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host
.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(builder =>
{
builder.Configure(builder =>
{
var events = builder.ApplicationServices.
GetRequiredService<IHostApplicationLifetime>();
//hook to events here
});
builder.UseStartup<Startup>();
});
如果希望在请求的开始或结束时有事件,最好使用筛选器。您可以在第 10 章、了解过滤器中了解过滤器。
这就是应用事件的全部内容。现在,让我们转到汇编嵌入式资源。
使用嵌入式资源
从.NET 的原始版本开始,就可以在程序集中嵌入内容,包括二进制文件。这样做的原因很简单,因为它们包含在应用二进制文件中,所以可以最小化要分发的文件数量。为此,我们可以使用 Visual Studio 在 Visual Studio 的属性资源管理器中设置 Build Action 属性:

然后,要检索嵌入式资源,您需要一个EmbeddedFileProvider(前面讨论过)或ManifestEmbeddedFileProvider的实例。这些类属于Microsoft.Extensions.FileProviders.Embedded包并实现IFileProvider接口,这意味着它们可以在任何期望IFileProvider的 API 中使用。您可以通过向其传递程序集来初始化它们,如下所示:
var embeddedProvider = new ManifestEmbeddedFileProvider
(Assembly.GetEntryAssembly());
还可以传递可选的基本命名空间:
var embeddedProvider = new ManifestEmbeddedFileProvider
(Assembly.GetEntryAssembly(), "My.Assembly");
此基本命名空间是在项目属性中指定的命名空间:

EmbeddedFileProvider和ManifestEmbeddedFileProvider的区别在于后者需要将原始文件路径存储在程序集中,您需要将其(粗体)添加到.csproj文件中:
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>
只要坚持EmbeddedFileProvider就行了,不要太担心这个。
这取决于您是否了解嵌入式资源中的内容;例如,这可以是文本或二进制内容。您应该知道 ASP.NET Core 3 还允许包含 Razor 类库中的静态内容,这可能是一个更好的解决方案。这在第 9 章、可重用组件中进行了讨论。
下一节将讨论用于自动加载类和运行后台任务的内置基础结构机制。
主机扩展
我们现在将讨论一种自动从其他程序集中加载类的机制,以及另一种自动生成后台线程的机制。第一个用于.NET 自动注册某些扩展(即 Azure 和 Application Insights),第二个用于在后台执行工作,而不妨碍 web 应用。让我们从托管来自外部程序集的代码开始。
托管启动
有一个接口IHostingStartup,它公开了一个方法:
public class CustomHostingStartup : IHostingStartup
{
public void Configure(IWebHostBuilder builder)
{
}
}
这可以在主机启动时用于向主机中注入其他行为。IWebHostBuilder与Program.Main方法中使用的实例完全相同。那么,这个类是如何加载的呢?这可以通过以下两种方式之一实现:
- 通过在程序集级别添加一个
[HostingStartup]属性。我们可以指定一个或多个IHostingStartup实现的类,这些类应该从应用的程序集自动加载。 - 通过设置
ASPNETCORE_HOSTINGSTARTUPASSEMBLIES环境变量的值,该值可以是程序集名称和/或完全限定类型名称的分号分隔列表。对于每个程序集名称,宿主框架将检测任何[HostingStartup]属性,对于类型名称,如果它们实现IHostingStartup,将自动加载它们。
这是加载类(如插件)的一种很好的机制,事实上,一些 Microsoft 软件包(如 ApplicationInsights)就是这样工作的。
现在让我们看看如何在后台运行任务。
托管后台服务
IHostedService接口定义后台任务的合约。通过在一个具体的类中实现这个接口,我们可以在我们的应用的后台生成工作人员,并且我们不会干扰它。这些服务具有 ASP.NET 应用的生命周期。
Visual Studio 中有一个用于创建辅助服务的特殊模板:

这是一种特殊的项目,不能用于提供 web 内容。如果您感到好奇,它将使用以下声明:
<Project Sdk="Microsoft.NET.Sdk.Worker">
ASP.NET Core 提供了一个方便的类BackgroundService,您可以从中继承,而不是实现IHostedService:
public class BackgroundHostedService : BackgroundService
{
protected override Task ExecuteAsync(CancellationToken
cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
//do something
Task.Delay(1000, cancellationToken);
}
return Task.CompletedTask;
}
}
当然,托管服务启动时会自动调用ExecuteAsync。它接受一个CancellationToken参数,可以用来知道托管服务何时被取消。在它里面,我们通常执行一个永远运行的循环(直到cancellationToken被取消)。在这里,我们等待 1000 毫秒,但是您希望在循环之间延迟的时间取决于您和您的需求。
在引导的早期阶段,托管服务需要在依赖项注入框架中注册,即在工作者服务项目的Program类中:
public static IHostBuilder CreateHostBuilder(string [] args) =>
Host
.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHostedService<BackgroundHostedService>();
});
但是,它们也可以在 web 应用中使用;在本例中,只需在ConfigureServices中将我们的后台服务类注册为IHostedService的单例实例:
services.AddSingleton<IHostedService, BackgroundHostedService>();
托管服务可以通过其构造函数将服务注入其中,就像从依赖项注入容器构建的任何其他服务一样。它们与 ASP.NET Core 应用具有相同的生命周期,即使它们在依赖项注入容器中注册(作为临时实例),它们也不是真正需要手动检索的。如果您需要这样做,请考虑注册为单身,并有一些注册服务来传递数据。
下一个主题是关于 ASP.NET Core 模型约定的,它可以用于默认行为。
ASP.NET Core 模型约定
ASP.NET Core 支持约定,约定是实现已知接口的类,可以注册到应用以修改其某些方面。约定接口如下所示:
-
IApplicationModelConvention:这提供了对应用范围约定的访问,允许您迭代以下每个级别,即控制器模型、动作模型和参数模型。 -
IControllerModelConvention:这些是特定于控制器的约定,但也允许您评估较低级别(动作模型)。 -
IActionModelConvention:这允许您更改操作级别约定以及操作的任何参数(参数模型)。 -
IParameterModelConvention:仅针对参数。 -
IPageRouteModelConvention:这让我们可以自定义 Razor 页面的默认路由(ASP.NET Core 2.x)。 -
IPageApplicationModelConvention:允许定制剃须刀型号。
从最高到最低的范围,我们有应用,然后是控制器,然后是动作,最后是参数。
非剃须刀约定通过MvcOptions的Conventions集合进行注册:
services
.AddMvc(options =>
{
options.Conventions.Add(new CustomConvention());
});
因此,这适用于IApplicationModelConvention、IControllerModelConvention、IActionModelConvention和IParameterModelConvention。剃须刀约定在RazorPagesOptions中的类似集合上配置:
services
.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.Add(new CustomRazorConvention());
});
通过让自定义属性实现其中一个约定接口并在正确级别应用它,也可以应用自定义约定:
IControllerModelConvention:控制器类IActionModelConvention:动作方式IParameterModelConvention:动作方式参数
那么,我们可以用自定义约定做什么呢?一些例子如下:
-
注册新控制器并将属性动态添加到现有控制器
-
为所有或某些控制器动态设置路由前缀
-
动态定义操作方法的授权
-
动态设置动作方法中参数的默认位置
如果我们想在已注册的控制器列表中添加一个新控制器,我们将执行以下操作:
public class CustomApplicationModelConvention : IApplicationModelConvention
{
public void Apply(ApplicationModel application)
{
application.Controllers.Add(new ControllerModel
(typeof(MyController),
new List<object> { { new AuthorizeAttribute() } }));
}
}
要添加全局筛选器,我们可以执行以下操作:
application.Filters.Add(new CustomFilter());
如果我们希望所有控制器都有一个带有特定前缀(Prefix的[Route]属性,我们将执行以下操作:
foreach (var applicationController in application.Controllers)
{
foreach (var applicationControllerSelector in
applicationController.Selectors)
{
applicationControllerSelector.AttributeRouteModel =
new AttributeRouteModel(new RouteAttribute("Prefix"));
}
}
这也可以在IActionModelConvention实现中实现,但可以表明您可以在IApplicationModelConvention的所有级别应用约定。
现在,为了向某些动作方法添加一个以Auth结尾的[Authorize]属性,我们执行以下操作:
foreach (var controllerModel in application.Controllers)
{
foreach (var actionModel in controllerModel.Actions)
{
if (actionModel.ActionName.EndsWith("Auth"))
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
actionModel.Filters.Add(new AuthorizeFilter(policy));
}
}
}
最后,当参数的名称以Svc结尾时,我们可以将参数的源设置为服务提供者:
foreach (var controllerModel in application.Controllers)
{
foreach (var actionModel in controllerModel.Actions)
{
foreach (var parameterModel in actionModel.Parameters)
{
if (parameterModel.ParameterName.EndsWith("Svc"))
{
if (parameterModel.BindingInfo == null)
{
parameterModel.BindingInfo = new BindingInfo();
}
parameterModel.BindingInfo.BindingSource =
BindingSource.Services;
}
}
}
}
对于 Razor 页面,它有些不同,因为两个约定接口之间没有关系;也就是说,它们用于完全不同的目的。两个例子如下:
- 将所有页面模型属性设置为自动绑定到服务提供商
- 设置所有页面的根目录
对于第一个示例,我们需要一个IPageApplicationModelConvention实现:
public class CustomPageApplicationModelConvention : IPageApplicationModelConvention
{
public void Apply(PageApplicationModel model)
{
foreach (var property in model.HandlerProperties)
{
if (property.BindingInfo == null)
{
property.BindingInfo = new BindingInfo();
}
property.BindingInfo.BindingSource = BindingSource.Services;
}
}
}
它会自动将依赖项注入设置为页面模型类中任何属性的绑定源;这与在它们上设置[FromServices]属性相同。
设置自定义路由前缀时,使用IPageRouteModelConvention实现:
public class CustomPageRouteModelConvention : IPageRouteModelConvention
{
public void Apply(PageRouteModel model)
{
foreach (var selector in model.Selectors)
{
if (selector.AttributeRouteModel == null)
{
selector.AttributeRouteModel = new AttributeRouteModel(new
RouteAttribute("Foo"));
}
else
{
selector.AttributeRouteModel = AttributeRouteModel
.CombineAttributeRouteModel(selector.
AttributeRouteModel,
new AttributeRouteModel(new RouteAttribute
("Foo")));
}
}
}
}
在这里,我们要做的是将所有剃须刀页面的[Route]属性设置为以Foo开头。
接下来,我们将看到如何修改传入的请求。
应用 URL 重写
尽管 MVC 路由很方便,但有时我们需要向公众提供不同的 URL,反之亦然,以便能够接受公众知道的 URL。这就是 URL 重写的用武之地。
URL 重写不是新的;自从 ASP.NET Web 表单通过IIS URL 重写模块(参见以更先进的方式在本机上形成以来,它就一直存在 https://www.iis.net/downloads/microsoft/url-rewrite )。ASP.NET Core 通过Microsoft.AspNetCore.Rewrite包提供类似的功能。让我们看看它是如何工作的。
从本质上讲,URL 重写是一种功能,通过该功能,您可以根据一组预配置的规则将请求 URL 转换为不同的内容。Microsoft 建议在某些情况下使用此功能:
- 为需要临时或永久更改的资源提供不变的 URL
- 跨应用拆分请求
- 重新组织 URL 片段
- 为搜索引擎优化(SEO优化 URL)
- 创建用户友好的 URL
- 将不安全的请求重定向到安全端点
- 防止图像(或其他资产)热链接(有人从其他网站引用您的资产)
Microsoft.AspNetCore.Rewrite包可以通过代码进行配置,但也可以接受 IIS 重写模块配置文件(https://docs.microsoft.com/en-us/iis/extensions/url-rewrite-module/creating-rewrite-rules-for-the-url-rewrite-module )。毕竟,ASP.NET Core 是跨平台的,Apache 的mod_rewrite配置(http://httpd.apache.org/docs/current/mod/mod_rewrite.html 除 IIS 外,还支持。
URL 重写不同于 URL 重定向,后者中,服务器在收到应重定向的请求时发送一个3xx状态码,由客户端跟踪该请求。在 URL 重写中,服务器会立即处理请求,而无需再次往返,但根据重写规则,应用会以不同的方式看待请求。Microsoft.AspNetCore.Rewrite支持两种情况。
首先,有一个RewriteOptions类,用于定义所有规则。它有两种扩展方法:
AddRedirect:添加 URL 重定向规则,状态代码可选(3xx)AddRewrite:添加 URL 重写规则Add(Action<RewriteContext>):添加可用于动态生成重写或重定向规则的委托Add(IRule):添加IRule的实现,定义运行时规则,方式类似Action<RewriteContext>委托
然后,有两种特定于 Apache 和 IIS 的扩展方法:
AddApacheModRewrite:读取mod_rewrite配置文件AddIISUrlRewrite:读取 IIS URL 重写模块配置文件
这两种方法要么采用文件提供程序(IFileProvider)和路径,要么采用已经指向打开文件的TextReader实例。
最后,有两种强制 HTTPS 的方法:
AddRedirectToHttps:告知客户端请求相同的请求,但这次使用的是 HTTPS 协议而不是 HTTP。AddRedirectToHttpsPermanent:与前面的方法类似,只是发送301 Moved Permanently消息,而不是302 Found。
如果请求是针对服务器上的任何资源的 HTTP,这些方法将强制重定向到 HTTPS。接下来让我们看看 URL 重定向!
URL 重定向
首先,让我们看一个 URL 重定向的示例。本例使用RewriteOptions类:
services.Configure<RewriteOptions>(options =>
{
options.AddRedirect("redirect-rule/(.*)", "redirected/$1",
StatusCodes.Status307TemporaryRedirect);
});
第一个参数是一个正则表达式,它应该与请求匹配,我们可以在其中指定捕获(括号内)。第二个参数是重定向 URL;注意我们如何使用第一个参数中定义的捕获。第三个参数是可选的,如果不使用,则默认为302 Found。
Read about HTTP redirection in the HTTP specification at https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.
URL 重写
接下来,我们看看什么是内部 URL 重写。AddRewrite的一个例子可以是:
options.AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2", skipRemainingRules: true);
这里,我们指示Microsoft.AspNetCore.Rewrite将rewrite-rule之后由数字组成的任何路径组件(这也使用正则表达式)转换为查询字符串参数。如果找到第一个参数的匹配项,则第三个参数(skipRemainingRules指示重写中间件停止处理任何其他规则,只使用此规则。skipRemainingRules参数的默认值为false。
运行时评估
采用Action<RewriteContext>或IRule的扩展实际上做了相同的事情,第一个操作只是将传递的委托包装在DelegateRule中,这是IRule的一个具体实现。此接口仅定义一个方法:
void ApplyRule(RewriteContext context)
RewriteContext提供了几个属性,您可以从这些属性访问上下文并设置响应:
-
HttpContext(HttpContext:当前 HTTP 上下文。 -
StaticFileProvider(IFileProvider:用于检查是否存在静态文件和文件夹的当前文件提供程序。 -
Logger(ILogger:记录器。 -
Result(RuleResult:规则评估结果,必须设置。默认值为ContinueRules,指示中间件继续处理其他请求,其他可能的值为EndResponse(按照您的预期执行)和SkipRemainingRules(推迟处理其他规则,仅应用当前规则)。
要使用IRule或委托,我们使用以下选项之一:
.Add(new RedirectImageRule("jpg", "png"));
我们还可以使用以下工具:
.Add((ctx) =>
{
ctx.HttpContext.Response.Redirect("/temporary_offline",
permanent: true);
ctx.Result = RuleResult.EndResponse;
});
RedirectImageRule规则如下所示:
public sealed class RedirectImageRule : IRule
{
private readonly string _sourceExtension;
private readonly string _targetExtension;
public RedirectImageRule(string sourceExtension, string
targetExtension)
{
if (string.IsNullOrWhiteSpace(sourceExtension))
{
throw new ArgumentNullException(nameof(sourceExtension));
}
if (string.IsNullOrWhiteSpace(targetExtension))
{
throw new ArgumentNullException(nameof(targetExtension));
}
if (string.Equals(sourceExtension, targetExtension,
StringComparison.InvariantCultureIgnoreCase))
{
throw new ArgumentException("Invalid target extension.",
nameof(targetExtension));
}
this._sourceExtension = sourceExtension;
this._targetExtension = targetExtension;
}
public void ApplyRule(RewriteContext context)
{
var request = context.HttpContext.Request;
var response = context.HttpContext.Response;
if (request.Path.Value.EndsWith(this._sourceExtension,
StringComparison.OrdinalIgnoreCase))
{
var url = Regex.Replace(request.Path, $@"^(.*)\.
{this._sourceExtension}$",
$@"$1\.{this._targetExtension}");
response.StatusCode = StatusCodes.Status301MovedPermanently;
context.Result = RuleResult.EndResponse;
if (!request.QueryString.HasValue)
{
response.Headers[HeaderNames.Location] = url;
}
else
{
response.Headers[HeaderNames.Location] = url + "?" +
request.QueryString;
}
}
}
}
此类将对特定图像扩展的任何请求转换为另一个请求。委托故意非常简单,因为它只是重定向到本地端点,从而结束请求处理。
重定向到 HTTPS
如果当前请求是 HTTP,则重定向到 HTTPS 的扩展非常简单。唯一的选项是发送301 Moved Permanently消息而不是301 Found或指定自定义 HTTPS 端口:
.AddRedirectToHttps(sslPort: 4430);
接下来,我们继续讨论特定于平台的重写。
平台特定
AddIISUrlRewrite和AddApacheModRewrite具有相同的签名,它们都可以采用文件提供程序和现有文件的路径或流。以下是后者的一个例子:
using (var iisUrlRewriteStreamReader = File.OpenText("IISUrlRewrite.xml"))
{
var options = new RewriteOptions()
.AddIISUrlRewrite(iisUrlRewriteStreamReader)
}
I have not covered the format of the IIS Rewrite module or the mod_rewrite configuration files. Please refer to its documentation for more information.
接下来,我们将看到如何强制 URL 重写。
强制 URL 重写
在Configure方法中,我们必须添加对UseRewriter的调用。如果我们不传递参数,它将使用之前在依赖项注入中配置的RewriteOptions操作,但我们也可以在此处传递它的实例:
var options = new RewriteOptions()
.AddRedirectToHttps();
app.UseRewriter(options);
现在我们来看一些关于如何将 EF Core 与 ASP.NET Core 结合使用的实用建议。
使用 EF 核
EF Core是一款流行的对象关系映射器(ORM用于检索和更新数据。您可以想象,ASP.NET Core 对它有很好的支持,因为两者都是 Microsoft 工具。本节将介绍 EF Core 与 ASP.NET Core 的一些常见用法。
确保首先安装最新的dotnet-ef全局工具:
dotnet tool install --global dotnet-ef
接下来,让我们看看如何注册上下文。
注册 DbContext
首先,我们可以向依赖注入框架注册一个DbContext实例:
services.AddDbContext<MyDbContext>(options =>
{
options.UseSqlServer(this.Configuration.GetConnectionString
("<connection string name>"));
});
正如您在本例中看到的,我们必须设置提供者及其连接字符串;否则,上下文几乎毫无用处。默认情况下,上下文将注册为作用域实例,这通常是我们想要的,因为它将在请求结束时被销毁。注册后,它可以被注入到我们想要的任何地方,比如控制器中。如果您还记得第 2 章,配置,GetConnectionString是一种扩展方法,它从已知位置("ConnectionStrings:<named connection>")的配置中检索连接字符串。
您的DbContext-派生类必须有一个特殊的公共构造函数,采用DbContextOptions类型或DbContextOptions<T>类型的参数,其中T是上下文的类型(在本例中为MyDbContext:
public class OrdersContext : DbContext
{
public OrdersContext(DbContextOptions options) : base(options) { }
}
拥有此构造函数是必需的,但您可以拥有其他构造函数,甚至是无参数的构造函数。
使用异步方法
尽可能使用可用方法的异步版本(AddAsync、FindAsync、ToListAsync和SaveAsync)。这将提高应用的可伸缩性。此外,如果您希望将数据传递给一个视图,而该视图不可能被修改,请在结果查询中使用AsNoTracking扩展方法:
return this.View(await this.context.Products.AsNoTracking()
.ToListAsync());
这确保了实例化后从数据源返回的任何记录都不会添加到更改跟踪器中,这使得操作更快,占用的内存更少。
Do have a look at Microsoft's documentation for asynchronous programming at https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async.
急装
当返回具有惰性关联的实体时,如果您确定它们将被使用,请急切地获取它们。这将最小化数据库调用的数量:
var productsWithOrders = await this.context.Products.Include(x => x.Orders).ToListAsync();
如果您正在从 web API 返回数据,这一点尤其重要,因为一旦发送实体,它们将失去与数据库的连接,因此无法再从数据库加载惰性数据。
但请注意,快速加载通常会导致发出INNER JOINs或LEFT JOINs,这可能会增加返回结果的数量。
初始化数据库
这可能是应用启动后需要完成的任务之一。最好的方法是应用引导:
public static class HostExtensions
{
public static IHost CreateDbIfNotExists(IHost host)
{
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
var logger = services.GetRequiredService<ILogger<Program>>();
try
{
var context = services.GetRequiredService
<OrdersContext>();
var created = context.Database.EnsureCreated();
logger.LogInformation("DB created successfully:
{created}.", created);
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while
creating the DB.");
}
}
return host;
}
}
此代码创建一个作用域并从其内部请求DbContext;通过这种方式,我们确信它将在范围结束时得到适当处理,从而释放所有资源。如果数据库已经存在,EnsureCreated方法将返回false,如果数据库已经创建,true方法将返回。
构建通用主机时,只需将对此方法的调用排队:
public static void Main(string [] args)
{
CreateHostBuilder(args)
.Build()
.CreateDbIfNotExists()
.Run();
}
如果您需要将数据库迁移到最新的迁移版本,只需将对EnsureCreated的调用替换为Migrate:
context.Database.Migrate();
如果没有任何参数,它会将数据库迁移到最新的迁移版本。如果迁移程序集与应用程序程序集不同,则在注册上下文时必须执行以下操作:
services.AddDbContext<OrdersContext>(options =>
{
//set options, like, connection string and provider to use
options.MigrationsAssembly("MyMigrationAssembly");
});
本例声明MyMigrationAssembly为包含所有迁移代码的程序集。
显示迁移错误和运行迁移
在开发模式下运行时,通常会有一个开发人员异常页面,显示发生的错误及其堆栈跟踪和其他信息。虽然这很有用,但它不包括可能由数据库不匹配(例如缺少迁移)引起的错误。幸运的是,ASP.NET Core 包含一个中间件组件,它可以捕获这些错误,并提供一个友好的错误页面来突出显示这些错误。
为了使用这个数据库错误页面,我们需要添加对Microsoft.AspNetCore.Diagnostics. EntityFrameworkCoreNuGet 包的引用。然后,通过以下调用将中间件添加到Configure方法中的管道中:
app.UseDatabaseErrorPage();
这通常只在开发环境中启用,并且可以与UseDeveloperExceptionPage或您可能拥有的其他错误处理程序一起使用。
现在,您可能还想触发最新迁移的应用,可能是为了解决相关错误;这个中间件也允许您这样做。它使"/ApplyDatabaseMigrations"处的端点仅可用于此目的。您需要使用字段名context发布上下文的完全限定名称类型。以下是一个例子:
POST /ApplyDatabaseMigrations HTTP/1.1
context=Orders.OrderContext,Orders
本例在Orders程序集中使用了一个名为Orders.OrderContext的假设上下文。
如果出于任何原因,需要修改端点,可以按如下操作:
app.UseDatabaseErrorPage(new DatabaseErrorPageOptions { MigrationsEndPointPath = "/Migrate" });
这将使用"/Migrate"而不是默认路径。
将 EF 上下文与 HTTP 上下文集成
有时,在初始化 EF 上下文时,您可能需要从 HTTP 上下文获取一些信息。为什么?例如,这可能需要从请求或请求域获取用户特定的信息,在多租户场景中,这些信息可用于选择连接字符串。
在这种情况下,最好的方法是从 HTTP 上下文中获取应用服务提供者,唯一的方法是将IHttpContextAccessor服务注入DbContext的构造函数中:
public class OrdersContext : DbContext
{
public class OrdersContext(DbContextOptions options,
IHttpContextAccessor httpContextAccessor) :
base(options)
{
this.HttpContextAccessor = httpContextAccessor;
}
protected IHttpContextAccessor HttpContextAccessor { get; }
//rest goes here
}
别忘了我们需要通过调用AddDbContext来注册DbContext类。我们有了IHttpContextAccessor之后,我们可以在OnConfiguring中使用它来获取当前用户:
protected override void OnConfiguring(DbContextOptionsBuilder builder)
{
var httpContext = this.HttpContextAccessor.HttpContext;
var userService = httpContext.RequestServices.GetService
<IUserService>();
var user = httpContext.User.Identity.Name;
var host = httpContext.Request.Host.Host;
var connectionString = userService.GetConnectionStringForUser
(userService);
//var connectionString = userService
//.GetConnectionStringForDomain(host);
builder.UseSqlServer(connectionString);
base.OnConfiguring(builder);
}
在本例中,我们从当前请求中检索用户和主机。然后,我们使用一个虚构的服务-IUserService-检索该用户(GetConnectionStringForUser)或主机(GetConnectionStringForDomain)的连接字符串,然后使用该字符串。这是因为注入了IHttpContextAccessor类,它需要通过调用Startup.ConfigureServices中的AddHttpContextAccessor来注册。
Some people may object that this ties DbContext to ASP.NET Core. Although this is true, for the scope of this book, it makes total sense to do that.
现在让我们看一个更复杂的例子,说明如何使用 efcore 构建一个功能齐全的 REST 服务。
建立休息服务
下面是一个 REST 服务的完整示例,该服务使用 EF Core 作为数据访问的底层 API。它具有检索、创建、更新和删除实体的操作,适合用作 web API 或 AJAX 样式的 web 应用:
[ApiController]
[Route("api/[controller]")]
public class BlogController : ControllerBase
{
private readonly BlogContext _context;
public BlogController(BlogContext context)
{
this._context = context;
}
[HttpGet("{id?}")]
public async Task<ActionResult<Blog>> Get(int? id = null)
{
if (id == null)
{
return this.Ok(await this._context.Blogs.AsNoTracking()
.ToListAsync());
}
else
{
var blog = await this._context.Blogs.FindAsync(id);
if (blog == null)
{
return this.NotFound();
}
else
{
return this.Ok(blog);
}
}
}
[HttpPut("{id}")]
public async Task<ActionResult<Blog>> Put(int id, [FromBody]
Blog blog)
{
if (id != blog.Id)
{
return this.BadRequest();
}
if (this.ModelState.IsValid)
{
this._context.Entry(blog).State = EntityState.Modified;
try
{
await this._context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
return this.Conflict();
}
return this.Ok(blog);
}
else
{
return this.UnprocessableEntity();
}
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
var blog = await this._context.Blogs.FindAsync(id);
if (blog == null)
{
return this.NotFound();
}
this._context.Blogs.Remove(blog);
await this._context.SaveChangesAsync();
return this.Accepted();
}
[HttpPost]
public async Task<ActionResult<Blog>> Post([FromBody] Blog blog)
{
if (blog.Id != 0)
{
return this.BadRequest();
}
if (this.ModelState.IsValid)
{
this._context.Blogs.Add(blog);
await this._context.SaveChangesAsync();
return this.CreatedAtAction(nameof(Post), blog);
}
else
{
return this.UnprocessableEntity();
}
}
}
正如您所看到的,这个控制器有几个动作方法,每个 HTTP 动词(GET、POST、PUT和DELETE都有一个动作方法。以下是它的工作原理:
-
控制器在其构造函数中获取
DbContext派生类的一个实例,该类来自依赖项注入框架。 -
所有方法都是异步的。
-
所有方法都遵循约定,并且它们的名称与它们接受的 HTTP 谓词匹配。
-
所有方法都在签名中描述返回的内容。
-
对
DbContext派生类的所有调用都是异步的。 -
Get方法采用可选的id参数,如果提供该参数,将从该主键对单个实例发出查询。如果未找到,则返回404 Not Found结果;否则返回一个200 OK结果。如果没有传递 ID,则返回所有实体,但不会跟踪它们,因为它们不打算被修改。 -
Put方法获取从请求主体读取的 ID 和实体;如果 ID 与从请求中读取的 ID 不匹配,则返回一个501 Bad Request错误。然后验证实体,如果认为无效,则返回422 Unprocessable Entity结果。否则,将尝试将其标记为已修改并保存,但如果乐观并发检查失败,将返回一个409 Conflict结果。如果一切顺利,将返回一个200 OK结果 -
Post方法获取从请求主体读取的实体。如果该实体已经拥有 ID,则返回501 Bad Request结果;否则,它会尝试验证它。如果失败,422 Unprocessable Entity返回。否则,实体将添加到上下文并保存,并返回一个201 Created结果 -
最后,
Delete方法获取要删除的实体 ID 并尝试从中加载实体;如果找不到,则返回一个404 Not Found结果。否则,它会将实体标记为已删除并保存更改,返回一个202 Accepted结果。
就这样!这可以作为使用 efcore 构建 REST 服务的通用方法。
您可以在第 8 章、API 控制器中了解更多关于 API 控制器和 REST 的信息。
理解 gRPC 框架
gRPC 是一个相对较新的框架,用于绑定到.NET Core 的远程过程调用(RPC)。简单地说,它允许客户端和服务器之间的多种语言(包括 C++、java、JavaScript、C++、Python、DART 和 PHP)的结构化、高性能、类型安全的通信。ASP.NET Core 3 包括 gRPC 的一个实现。
gRPC 由谷歌创建,但现在是开源的,并使用现代标准,如 HTTP/2 进行数据传输和协议缓冲区进行内容序列化。
本节的目的不是深入介绍 gRPC,但应该足以让您开始学习!
首先,为了与 ASP.NET Core 一起使用,我们需要 NuGetGrpc.AspNetCore元包。Visual Studio 和dotnet工具都可以为 gRPC 创建模板项目:
dotnet new grpc
在我们开始之前,您可以先看看生成的代码。
接口定义
我们首先需要用 gRPC 自己的定义语言定义一个接口定义,从中可以用我们感兴趣的编程语言(在我们的例子中是 C#)生成存根。这包括将来回发送的方法和类型。以下是一个此类定义(Greet.proto的示例:
syntax = "proto3";
option csharp_namespace = "Greet";
package Greet;
enum Ok
{
No = 0;
Yes = 1;
}
service Greeter
{
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest
{
string name = 1;
}
message HelloReply
{
string message = 1;
Ok ok = 2;
}
我们可以在这里看到一些东西:
-
C#名称空间定义-
Greet,在本例中,该名称空间定义与包定义匹配,将用于生成的代码。稍后我们将进一步研究这个问题。 -
枚举-
Ok-具有两个可能的值。请注意,它们中的每一个都必须具有唯一的数值集。 -
服务或接口定义-
Greeter-具有 RPC 类型的单个方法-SayHello-将消息作为参数(HelloRequest)并返回消息作为响应(HelloReply)。 -
两个消息定义-
HelloRequest和HelloReply-每个消息定义都有一些字段,并分配了一个唯一的编号。
消息中的每个字段都将是以下类型之一(括号中为相应的.NET 类型):
- 消息类型
- 枚举类型
double:doublefloat:floatint32、sint32、sfixed32:intint64、sint64、sfixed64:longuint32和fixed32:uintuint64和fixed64:ulongbool:boolstring:string(最多 232UTF-8 个字符)bytes:byte[](最多 232字节)
也可以包含来自其他文件的定义:
import "Protos\common.proto";
在我们有了描述我们想要调用的服务的接口定义文件之后,我们必须编译它,以便为我们感兴趣的语言生成源代码。到目前为止,我们需要手动将该文件添加到我们的.csproj文件中,在服务器端使用如下条目:
<ItemGroup>
<Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
</ItemGroup>
客户端需要以下条目:
<ItemGroup>
<Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>
客户端生成的源代码和服务器端生成的源代码之间的区别在于我们想用它做什么。该代码是用于 ASP.NET Core web 应用来承载服务,还是用于希望连接服务的客户端应用?
消息类型
假设我们有一个乒乓服务,它发送乒乓并接收乒乓。gRPC 定义了四种类型的消息传递:
- 一元 RPC:客户端向服务器发送请求并返回响应:
rpc Ping(PingRequest) returns (PongResponse);
- 服务器流式 RPC:客户端向服务器发送一个请求,并获取一个可以读取消息的流,直到不再有消息为止:
rpc LotsOfPongs(PingRequest) returns (stream PongResponse);
- 客户端流式 RPC:客户端连续向服务器写入一系列消息:
rpc LotsOfPings(stream PingRequest) returns (PongResponse);
- 双向流式 RPC:双方使用独立的读写流发送一系列消息:
rpc BidiPingPong(stream PingRequest) returns (stream PongResponse);
Keep in mind that a full description of all of these messaging types is beyond the scope of this chapter, but you can find more information on the gRPC site at https://grpc.io/docs/guides/concepts.
其声明如下:
syntax = "proto3";
option csharp_namespace = "PingPong";
package PingPong;
message PingRequest
{
string name = 1;
};
message PongResponse
{
string message = 1;
Ok ok = 2;
};
enum Ok
{
No = 0;
Yes = 1;
};
service PingPongService
{
rpc Ping(PingRequest) returns (PongResponse);
rpc LotsOfPongs(PingRequest) returns (stream PongResponse);
rpc LotsOfPings(stream PingRequest) returns (PongResponse);
rpc BidiPingPong(stream PingRequest) returns (stream PongResponse);
};
接下来,我们将了解如何托管服务。
托管服务
在Startup类的ConfigureServices方法中,我们必须注册 gRPC 所需的服务:
services.AddGrpc();
可以为端点配置一些选项,但现在让我们暂且不谈。然后,我们需要为要公开的 gRPC 服务(或多个服务)创建端点:
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<PingPongService>();
});
PingPongService类是我们需要实现的;它是我们为其指定接口定义的服务的核心。我们可以按以下方式实施:
public class PingPongService : PingPong.PingPongService.PingPongServiceBase
{
private readonly ILogger<PingPongService> _logger;
public PingPongService(ILogger<PingPongService> logger)
{
this._logger = logger;
}
public async override Task<PongResponse> Ping(PingRequest request,
ServerCallContext context)
{
this._logger.LogInformation("Ping received");
return new PongResponse
{
Message = "Pong " + request.Name,
Ok = Ok.Yes
};
}
}
您可以看到,我们从PingPong.PingPongBase继承的基类将接口定义文件中定义的方法定义为抽象,因此我们需要实现它们,因此需要override关键字。
当您在 VisualStudio 中开始编写此代码时,您会注意到一些奇怪的事情,您对某些类型(即,PingPong名称空间和PingPong.PingPongBase类型)没有 IntelliSense。这是因为它们是在编译时根据PingPong.proto文件上的定义生成的,因此,.NET 等价物还不可用。
我们可以看到,using声明与.proto文件上的csharp_namespace直接匹配,PingPong静态类(就是这样!)来自该文件上的服务名称。PingPongBase是从编译器生成的,因为我们在.csproj文件中设置了Server选项。
我们可以看到依赖项注入的工作方式与我们以前的工作方式几乎相同。在本例中,我们通过构造函数注入一个记录器。实际上,默认情况下,依赖项注入框架将 gRPC 服务实例化为瞬态,但我们可以手动注册它,使其具有不同的生存期(通常,使用单例):
services.AddSingleton<PingPongService>();
现在让我们看看请求上下文中是什么。
请求上下文
在 gRPC 方法的实现中,最后一个参数始终为ServerCallContext。这使我们能够从运行的服务器和发出请求的客户端获得大量有用的信息。这一次,我们可以通过调用GetHttpContext扩展方法来获得当前请求的HttpContext,从那里,我们可以访问我们熟悉的所有属性和方法。我们还有以下几点:
Host(string:被调用主机的名称Method(string:被调用方法的名称(当前方法)Peer(string:客户端地址,URI 格式RequestHeaders(Metadata:客户端发送的所有报头ResponseTrailers(Metadata:将发送回客户端的所有头文件Status(Status:操作完成后发送给客户端的状态,通常自动设置UserState(IDictionary<object, object>):可用于在拦截器之间传递信息的数据(稍后讨论)WriteOptions(WriteOptions:一组标志,可用于调整响应的某些方面(如压缩和响应缓冲)
Metadata类只不过是一个键和值的字典,Status只包含一个状态码(整数)和一个细节(字符串)。
现在,我们怎样才能截获信息?
拦截器
拦截器可用于在 gRPC 方法之前、之后或代替 gRPC 方法执行操作。拦截器必须从基类继承,适当地称为Interceptor,该基类为每种消息传递类型提供虚拟方法。以下是一个简单的例子:
public class LogInterceptor : Interceptor
{
private readonly ILogger<LogInterceptor> _logger;
public LogInterceptor(ILogger<LogInterceptor> logger)
{
this._logger = logger;
}
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest,
TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
this._logger.LogInformation("AsyncUnaryCall called");
return base.AsyncUnaryCall(request, context, continuation);
}
public override TResponse BlockingUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
BlockingUnaryCallContinuation<TRequest, TResponse> continuation)
{
this._logger.LogInformation("BlockingUnaryCall called");
return base.BlockingUnaryCall(request, context, continuation);
}
public override Task<TResponse> UnaryServerHandler<TRequest,
TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
this._logger.LogInformation("UnaryServerHandler called");
return base.UnaryServerHandler(request, context, continuation);
}
}
当接收到一元 RPC 类型的请求以及将要发送响应时,这只会记录日志。我们不需要覆盖任何方法,只需要覆盖我们感兴趣的方法。根据发出请求的方式,每种消息类型都有一个阻塞版本和一个异步版本。如果我们希望返回自己的响应,我们只需从中返回我们想要的任何内容,而不是调用基本实现。因为所有这些方法都是通用的,所以我们需要使用反射来找出确切的参数(请求和响应)。另外,请注意所有方法中都存在ServerCallContext参数。
当 gRPC 服务从以下类型添加到依赖项注入框架时,拦截器按实例或类型注册:
services.AddGrpc(options =>
{
options.Interceptors.Add<LogInterceptor>();
});
通过一个实例,我们有以下几点:
services.AddGrpc(options =>
{
options.Interceptors.Add(new LogInterceptor());
});
之后,让我们检查听力选项。
听力选项
在开发模式下运行时,您可能需要侦听其他端口或禁用加密连接(TLS)。您可以在Program类中执行以下操作:
Host
.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(builder =>
{
builder.ConfigureKestrel(options =>
{
options.ListenLocalhost(5000, o => o.Protocols =
HttpProtocols.Http2);
});
builder.UseStartup<Startup>();
});
这将使红隼在端口5000上侦听,带有HTTP/2和不带 TLS。
使用 HTTP 客户端工厂
我们生活在一个微服务的世界中,在.NET 世界中,这些微服务经常使用 API 调用,比如HttpClient。HttpClient的问题在于,它经常被误用,因为即使它实现了IDisposable,但它实际上并不意味着每次使用后都会被处理掉,而是应该被重用。它是线程安全的,每个应用都应该有一个实例。
处理它会绕过类的原始目的,并且由于所包含的本机套接字不会立即被处理,如果您以这种方式实例化和处理许多HttpClientAPI,您可能会耗尽系统的资源。
.NET Core 2.1 引入了用于创建和维护预配置的HttpClientAPI 池的HttpClient工厂。这个想法很简单,用一个基本 URL 和一些可能的选项(比如头和超时)注册一个命名的客户机,并在需要时注入它们。当不再需要它时,它被返回到池中,但仍保持活着;然后,经过一段时间,它被回收。
让我们看一个例子。假设我们要注册一个对微服务的调用,该调用需要将特定授权作为头。我们将在ConfigureServices方法中添加如下内容:
services.AddHttpClient("service1", client =>
{
client.BaseAddress = new Uri("http://uri1");
client.DefaultRequestHeaders.Add("Authorization",
"Bearer <access token>");
client.Timeout = TimeSpan.FromSeconds(30);
});
唯一的强制设置是BaseAddress,但为了完整性,这里我还设置了Timeout和一个标题("Authorization**"**。显然,<access token>应该被实际的令牌所取代。
如果我们需要使用HttpClient的实例,我们使用依赖注入注入IHttpClientFactory并从中创建命名客户端:
public class HomeController : Controller
{
private readonly IHttpClientFactory _httpClientFactory;
public HomeController(IHttpClientFactory httpClientFactory)
{
this._httpClientFactory = httpClientFactory;
}
public async Task<IActionResult> Index()
{
var client = this._httpClientFactory.CreateClient("service1");
var result = await client.GetAsync("Process");
//URL relative to the base address
//do something with the result
return this.View();
}
}
传递给CreateClient的名称必须与在AddHttpClient注册的名称相同。注意,我们没有处理创建的客户端;这是没有必要的。
注册HttpClient时,有AddHttpClient的重载,同时接收IServiceProvider类型的参数,可用于从依赖注入框架获取服务:
services.AddHttpClient("service1", (serviceProvider, client) =>
{
var configuration = serviceProvider.GetRequiredService
<IConfiguration>();
var url = configuration["Services:Service1:Url"];
client.BaseAddress = new Uri(url);
});
在本例中,我从 DI 检索IConfiguration实例,并获取service1微服务的 URL,我使用该 URL 设置为HttpClient的基址。
另一个例子是,如果您需要将来自当前用户或当前上下文的信息传递给HttpClient该怎么办?在这种情况下,唯一的方法就是使用一个定制的DelegatingHandler实例并利用我们之前多次提到的IHttpContextAccessor服务。一些以DelegatingHandler开头的示例代码如下:
public class UserIdHandler : DelegatingHandler
{
public UserIdHandler(IHttpContextAccessor httpContextAccessor)
{
this.HttpContext = httpContextAccessor.HttpContext;
}
protected HttpContext HttpContext { get; }
protected override Task<HttpResponseMessage>
SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
request.Headers.Add("UserId", this.HttpContext.User.
Identity.Name);
return base.SendAsync(request, cancellationToken);
}
}
此类在其构造函数上接收一个IHttpContextAccessor实例,该实例是一个可用于获取当前HttpContext类的服务。在SendAsync覆盖上,它添加了一个包含当前用户名内容的标题。
在本例中,HttpClient实例的工厂注册如下:
services
.AddHttpClient("service1", client =>
{
client.BaseAddress = new Uri("http://uri1");
})
.AddHttpMessageHandler<UserIdHandler>();
唯一的区别是使用我们刚刚创建的类的泛型参数调用AddHttpMessageHandler。
正如我在开始时提到的,IHttpClientFactory返回的HttpClient实例被合并,经过一段时间后,它们被回收。这实际上意味着,保存本机套接字的内部HttpMessageHandler类保持活动状态,并且出于性能原因,套接字保持打开一段时间,然后将其处理并关闭套接字。当再次请求HttpClient时,插座再次打开。可以通过SetHandlerLifetime方法调整HttpClient保持活动的时间段:
services
.AddHttpClient("service1", client =>
{
client.BaseAddress = new Uri("http://uri1");
})
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
默认时间为 3 分钟。不建议将其打开太长时间,因为如果不需要发送请求,可能会浪费资源,并且可能检测不到 DNS 配置中的更改,例如目标主机地址的更改。这需要根据微服务的使用情况进行选择。如果在一段时间内频繁使用,您可能需要增加该值。如果您不确定,请保持原样。
我们还有另一个选项可以将HttpClient用于由强类型客户机组成的客户机工厂。本质上,这只是一个类,它在构造函数中接受HttpClient并公开一些方法来检索来自HttpClient的数据。让我们想象一下,我们想要从世界任何地方的城市检索天气信息。
我们可以使用现有的方法,而不是自己实现一个方法,例如OpenWeatherMap(https://openweathermap.org )。OpenWeatherMap 为开发人员提供了一个免费的 RESTAPI,可以实时返回大量有用的信息。为了使用此 API,我们需要编写一些类来对其发送的数据进行建模:
public class OpenWeatherCoordinates
{
public float lon { get; set; }
public float lat { get; set; }
}
public class OpenWeatherWeather
{
public int id { get; set; }
public string main { get; set; }
public string description { get; set; }
public string icon { get; set; }
}
public class OpenWeatherMain
{
public float temp { get; set; }
public int pressure { get; set; }
public int humidity { get; set; }
public float temp_min { get; set; }
public float temp_max { get; set; }
}
public class OpenWeatherData
{
public OpenWeatherCoordinates coord { get; set; }
public OpenWeatherWeather [] weather { get; set; }
public string @base { get; set; }
public OpenWeatherMain main { get; set; }
public int visibility { get; set; }
public OpenWeatherWind wind { get; set; }
public OpenWeatherClouds clouds { get; set; }
public OpenWeatherSys sys { get; set; }
public int dt { get; set; }
public int id { get; set; }
public string name { get; set; }
public int cod { get; set; }
}
public class OpenWeatherSys
{
public int type { get; set; }
public int id { get; set; }
public float message { get; set; }
public string country { get; set; }
public int sunrise { get; set; }
public int sunset { get; set; }
}
public class OpenWeatherClouds
{
public int all { get; set; }
}
public class OpenWeatherWind
{
public float wind { get; set; }
public int deg { get; set; }
}
Don't bother with all of these properties; they are required to fully map the information returned by OpenWeatherMap, but you can safely ignore most of it!
至于服务本身,我们可以表示为:
public interface IOpenWeatherMap
{
Task<OpenWeatherData> GetByCity(int id);
}
使用HttpClient的可能实现如下:
public class OpenWeatherMap : IOpenWeatherMap
{
//this is for sample data only
//get your own developer key from https://openweathermap.org/
private const string _key = "439d4b804bc8187953eb36d2a8c26a02";
private readonly HttpClient _client;
public OpenWeatherMap(HttpClient client)
{
this._client = client;
}
public async Task<OpenWeatherData> GetByCity(int id)
{
var response = await this._client.GetStringAsync($"/data/2.5
/weather?id=${id}&appid=${_key}");
var data = JsonSerializer.Deserialize<OpenWeatherData>(response);
return data;
}
}
这里唯一的问题是您必须更改密钥以供自己使用。此项仅用于抽样目的;您需要向 OpenWeatherMap 注册,并从获取您自己的密钥 https://openweathermap.org/appid 。
You can download the list of city codes from http://bulk.openweathermap.org/sample/city.list.json.gz.
现在,我们只需在ConfigureServices中登记本合同及其执行情况:
services.AddHttpClient<IOpenWeatherMap, OpenWeatherMap>("OpenWeatherMap", client =>
{
client.BaseAddress = new Uri("https://samples.openweathermap.org");
});
在此之后,您将能够使用依赖注入来注入IOpenWeatherMap。
在本节中,我们学习了如何使用 HTTP 客户端工厂预先准备HttpClient实例来调用微服务。与动态创建实例相比,这有几个优点,而且绝对值得使用。
总结
在本章中,我们看到,如果要在文件系统上提供某些文件,请始终为其指定一个虚拟路径,以免干扰控制器。
使用区域是组织内容的一种简便方法。它们在大型 ASP.NET Core 项目中特别有用。
我们还了解到,要结合使用静态文件、目录浏览和默认文档,您只需调用UseFileServer。你也应该提防不需要的文件下载,因为对它们应用安全性并不容易。
然后,我们看到资源文件非常有用,因为我们不需要将文件与程序集分开分发,并且可以使用与其余代码相同的版本控制。它们绝对值得考虑。
在下一节中,我们看到,如果我们不想公开站点的内部结构,不想遵守外部定义的 URL,不想使用托管服务自动生成后台服务并将其链接到应用的生命周期,那么应该使用 URL 重写。
EF Core 是一个有用的工具,但在 web 环境中使用时有一些缺陷。确保在从 web API 返回数据时避免延迟加载,使用其异步方法,并且不要跟踪对不打算修改的实体的更改。
然后,我们介绍了 gRPC,它为分布式编程提供了一个高性能的框架,它与语言无关,因此对于将不同的系统用于通信非常有用。
最后,我们看到HTTPClient工厂可以通过在中心位置注册每个微服务所需的配置,并允许通过依赖项注入注入客户端,从而提高需要调用多个微服务的代码的可读性和可伸缩性。
本章结束了我们的 ASP.NET Core API 之旅。我们查看了一些使用较少的功能,这些功能在 ASP.NET Core 中发挥着重要作用。在下一章中,我们将介绍有关应用部署的不同选项。
问题
您现在应该能够回答以下问题:
- 什么是 gRPC?
- URL 重写的目的是什么?
- 后台服务对什么有用?
- 区域的目的是什么?
- 公约对什么有用?
- 我们如何从引用的程序集中自动执行代码?
- 我们可以从程序集中加载文件吗?
十九、应用部署
在阅读了前面的章节之后,一旦您实现了应用并对其进行了测试,并且您对它感到满意,就应该部署它了。这将使它对外开放,或至少部分开放!
在本章中,我们将了解如何做到这一点,并通过涵盖以下主题探讨一些可用选项:
- 手动部署和编译实时更改
- 使用 VisualStudio 部署
- 部署到 IIS
- 部署到 NGINX
- 部署到 Azure
- 部署到亚马逊网络服务(AWS)
- 部署到 Docker
- 作为 Windows 服务部署
技术要求
要实现本章中介绍的示例,您需要.NET Core 3 SDK 和文本编辑器。当然,VisualStudio2019(任何版本)满足所有要求,但您也可以使用 VisualStudio 代码。
如果您将部署到云(Azure 或 AWS),您将需要选择的提供商提供一个可用的帐户。
手动部署应用
要手动部署应用,dotnet命令行工具提供publish命令。简而言之,它所做的是将所有内容打包在一起,从项目文件中获取所有必需的依赖项,构建应用和任何依赖项目,然后将所有输出复制到目标文件夹。它提供了很多选择,但最常见的可能是:
他们正在用户界面上运行。让我们看看如何:
-c | --configuration:定义构建配置。默认值是 Debug,另一个常用选项是 Release,但当然,您可以创建其他 VisualStudio 配置文件。-r | --runtime:在自包含部署的情况下,发布给定运行时的应用;默认情况下,使用目标计算机上可用的任何运行时。请参阅自包含部署和运行时部分中的说明。-f | --framework:设置目标框架。参见设置目标框架一节中的以下列表。-o | --output:设置目标输出文件夹的路径。-h | --help:显示使用信息。-v | --verbosity:将构建详细级别从q[uiet](无输出)的第一级设置为m[inimal]、n[ormal]、d[etailed]和diag[nostic](最高级别)。默认值为n[ormal]。--force:即使最后一次恢复成功,也会强制解决所有依赖项;它会有效地删除所有输出文件并再次尝试检索它们。--self-contained:这会将.NET Core 运行时与应用一起发布,这样就不需要在目标计算机上安装。
For more information, please use the help command.
以下是dotnet publish命令的示例:
dotnet publish MyApplication -c Release -o /Output
值得一提的是,您还可以通过使用p标志将参数传递给 MSBuild:
dotnet publish /p:Foo=Bar
Do not forget that the target environment is defined by the ASPNETCORE_ENVIRONMENT environment variable, so you may want to set it before calling dotnet publish.
有关受支持的目标框架的列表,请参见下一节。
设定目标框架
当您以应用或库中的框架为目标时,您指定了希望应用或库可用的 API 集。如果您的目标是.NET 标准之一,那么您就可以在更广泛的平台上使用它,例如,Linux 将不会有完整的.NET 框架,但它将有.NET 标准。此框架在项目文件中指定,但您可以为特定发布覆盖它。
用于 framework 命令的名字对象如下所示:
| 目标框架 | 名称 |
| .NET 标准 | netstandard1.0``netstandard1.1``netstandard1.2``netstandard1.3``netstandard1.4``netstandard1.5``netstandard1.6``netstandard2.0``netstandard2.1 |
| .NET Core | netcoreapp1.0``netcoreapp1.1``netcoreapp2.0``netcoreapp3.0``netcoreapp3.1 LTS |
设置框架的示例如下:
dotnet publish MyApplication -c Release -o /Output -f netcoreapp3.0
For your information, I have only listed the most useful ones; you can find the full (and updated) list at: https://docs.microsoft.com/en-us/dotnet/standard/frameworks.
现在让我们看看支持的运行时。
自包含部署和运行时
如果您为您的应用指定目标运行时,那么您也将默认的自包含设置为真。这意味着发布包将包含运行所需的所有内容。这有一些优点也有一些缺点。
其优点如下:
- 您可以完全控制应用将运行的.NET 版本。
- 您可以放心,目标服务器将能够运行您的应用,因为您正在提供运行时。
以下是缺点:
- 部署包的大小将更大,因为它包括运行时;如果您部署了许多具有自己运行时的不同应用,这可能会占用大量磁盘空间。
- 您需要事先指定目标平台。
与运行时命令一起使用的名称由以下内容组成:
- 目标操作系统名字对象
- 版本
- 建筑
这些示例包括ubuntu.14.04-x64、win7-x64和osx.10.12-x64。有关完整列表和一般规格,请参考https://docs.microsoft.com/en-us/dotnet/core/rid-catalog 。
You may want to have a look at https://docs.microsoft.com/en-us/aspnet/core/publishing and https://docs.microsoft.com/en-us/dotnet/core/deploying for a more in-depth introduction to deploying ASP.NET Core applications.
最后,下一个主题是监视应用的更改并实时重建它们。
实时重建
一个dotnet命令,它提供了一种功能,通过该功能,它可以实时监控代码的任何更改,并在代码更改为dotnet watch时自动构建代码。您可以在:上阅读相关信息 https://docs.microsoft.com/en-us/aspnet/core/tutorials/dotnet-watch 。
简而言之,要使用它,您需要在.csproj文件中将Microsoft.DotNet.Watcher.Tools包添加到您的项目中:
<ItemGroup>
<PackageReference Include="Microsoft.DotNet.Watcher.Tools" Version="2.0.2" />
</ItemGroup>
在此之后,您将运行dotnet watch,而不是发出dotnet run命令:
dotnet watch run
命令行就足够了;让我们转到 Visual Studio。
使用 VisualStudio 部署
大多数时候(至少对我来说),我们使用 VisualStudio 进行所有的开发和发布工作。Visual Studio 中也提供了dotnet publish的所有选项。我们需要创建发布配置文件,这可以通过在 Visual Studio 中右键单击项目并单击发布来实现,如以下屏幕截图所示:

在此之后,我们需要从文件系统、FTP、Web 部署或 Web 部署包中选择发布方法(稍后将详细介绍这两种方法)。
无论publish方法如何,我们都可以通过点击设置来配置常用发布选项,如下所示:

For a more in-depth guide, please refer to https://docs.microsoft.com/en-us/aspnet/core/publishing/web-publishing-vs.
Visual Studio 发布配置文件存储在Properties\PublishProfiles文件夹中:

本节是关于使用 VisualStudio 进行部署的,但是如果应用部署到 IIS,我们也可以使用它进行部署。让我们看看如何。
通过 IIS 部署
应用部署最常见的服务器可能是互联网信息服务器(IIS)。实际上,IIS 只是充当反向代理,将 HTTP/HTTPS 流量定向到.NET Core 主机。IIS 主机支持 Windows 7 及以上版本。它需要ASP.NET Core 模块,默认情况下与 Visual Studio 2019 和.NET Core SDK 一起安装。
为什么要使用 IIS 而不仅仅是 Kestrel 或 HTTP.sys?IIS 为您提供了更多选项,例如:
- 身份验证:例如,您可以轻松设置 Windows 身份验证。
- 日志记录:您可以配置 IIS 为所有访问生成日志。
- 自定义响应:IIS 可以为每个 HTTP 响应代码提供不同的页面。
- 安全:您可以为您的站点设置 HTTPS,配置 SSL 证书非常容易,IIS 管理器甚至可以生成虚拟证书。
- 管理:使用IIS 管理器工具,即使从远程服务器,也可以轻松管理。
您应该在目标计算机上安装 IIS/IIS 管理器,并在主机创建代码中添加对 IIS 宿主的支持。默认情况下,ASP.NET Core 3.x 已经做到了这一点,因此无需进行任何更改。
您可以使用 Visual Studio 在运行时或发布时自动为您创建网站,也可以自己创建网站;您只需记住以下两件事:
- 应用池不应使用.NET CLR 版本(无托管代码。
- 应启用
AspNetCoreModule模块。
如果您还记得的话,有两种发布方法是 WebDeploy 和 WebDeployPackage。Web 部署使用可通过 Web 部署工具(安装的Web 部署代理服务****MsDepSvcWindows 服务 https://www.iis.net/downloads/microsoft/web-deploy )。如果它正在运行,您可以让 Visual Studio 直接连接到远程(或本地)站点,并在那里安装 web 项目(如果选择 web 部署方法),如下所示:

另一方面,Web 部署包生成一个包含可通过 IIS 管理器控制台部署的包的.zip文件;只需右键单击任何站点并选择部署|导入服务器或站点包…:

您可能已经注意到由dotnet publish(或 Visual Studio 发布向导)生成的Web.config文件。它不是由 ASP.NET Core 使用,而是由AspNetCoreModule模块使用。仅当您希望在 IIS 之后托管应用时才需要此选项。您可以调整一些设置,例如启用文件的输出日志记录:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<aspNetCore
processPath="dotnet"
arguments=".\MyApplication.dll"
stdoutLogEnabled="true"
stdoutLogFile=".\logs\stdout.log" />
</system.webServer>
</configuration>
在这里,我更改了stdoutLogEnabled和stdoutLogFile属性;这应该很容易理解。
Once again, for the full documentation, please refer to https://docs.microsoft.com/en-us/aspnet/core/publishing/iis.
现在让我们来看看如何使用 NGINX 将请求代理到 ASP.NET Core。
使用 NGINX 部署
NGINX是 Unix 和 Linux 系列操作系统中非常流行的反向代理服务器。与 IIS 一样,它提供了 ASP.NET Core 主机无法提供的有趣功能,如缓存请求、直接从文件系统提供文件服务、SSL 终止等。您可以将其配置为将请求转发到独立运行的 ASP.NET Core 应用。需要修改此应用以确认转发的标头,如下所示:
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders =
ForwardedHeaders.XForwardedFor |
ForwardedHeaders.XForwardedProto |
ForwardedHeaders.XForwardedHost
});
此代码所做的是从头文件X-Forwarded-For(请求客户端 IP 和可能的端口)、X-Forwarded-Proto(请求协议)和X-Forwarded-Host(请求主机)中提取信息,并将其设置在HttpContext.Connection属性的适当属性中。这是因为 NGINX 剥离了这个请求信息并将其存储在这些头中,所以 ASP.NET Core 需要它,并且您可以在您通常期望的地方找到它。
我们还需要配置 NGINX 将请求转发到 ASP.NET Core(/etc/nginx/sites-available/default:
server
{
listen 80;
server_name server.com *.server.com;
location /
{
proxy_pass http://localhost:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
此代码设置 NGINX 侦听端口80并将请求转发到端口5000上的localhost。HTTP 版本被设置为1.1和一些附加的头(参见前面的.NET 代码)。服务器名称设置为server.com,但也接受低于server.com的任何内容。
阅读有关 NGINX 的所有信息,网址为:https://docs.microsoft.com/en-us/aspnet/core/publishing/linuxproduction 。
部署到 Azure
Microsoft Azure 也是托管您的应用的有力候选。要发布到 Azure,请右键单击项目并选择发布:

创建发布配置文件时,选择 Azure App Service 作为发布目标:

您需要选择所有适当的设置:订阅、资源组、应用服务计划等。
Of course, you need to have a working Azure subscription. There is no need for resource groups or app service plans—these can be created from inside the Visual Studio publish wizard.
如果您需要更多信息,请导航至https://docs.microsoft.com/en-us/aspnet/core/tutorials/publish-to-azure-webapp-using-vs 。
部署到 AWS
AWS 是 Microsoft Azure 的亚马逊竞争对手。它是一个云提供商,提供与 Azure 非常相似的功能。VisualStudio 可以通过AWS Visual Studio 工具包与之交互,可从此处免费获得:https://marketplace.visualstudio.com/items?itemName=AmazonWebServices.AWSToolkitforVisualStudio2017 。当然,您需要在 AWS 拥有一个工作帐户。
我们将了解如何将 ASP.NET Core 应用部署到 AWS Elastic Beanstalk,这是亚马逊为 web 应用提供的易于使用的托管和扩展服务:
- 要部署到 Elastic Beanstalk,我们必须首先使用 AWS Elastic Beanstalk 控制台(创建一个环境 https://console.aws.amazon.com/elasticbeanstalk )。
- 然后,我们需要压缩所有应用的内容,减去任何第三方 NuGet 软件包或二进制输出,然后将它们上传到 AWS。幸运的是,用于 VisualStudio 的 AWS 工具包为我们完成了所有这一切!只需在解决方案资源管理器中的项目上单击鼠标右键,然后选择“发布到 AWS Elastic Beanstalk”:

- 然后,您可以指定部署的所有方面,或者只使用默认值,如下所示:

这是对 AWS 部署的一个非常基本的介绍。接下来是 Docker。
与 Docker 一起部署
Docker在快速创建和销毁容器时,提供了一个非常好的选择,具有相同的精确内容。通过这种方式,你可以非常肯定事情会像你期望的那样工作!
Docker 支持内置于 ASP.NET Core 项目中。当您创建一个时,您将在项目中获得一个Dockerfile。它将包含如下内容:
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
ARG source
WORKDIR /app
EXPOSE 80
COPY ${source:-obj/Docker/publish} .
ENTRYPOINT ["dotnet", "MyApplication.dll"]
请注意一个名为docker-compose的额外解决方案文件夹。Docker Compose 是一个用于定义和运行多容器 Docker 应用的工具,您可以在以下网址阅读:https://docs.docker.com/compose 。在此文件夹中,您将找到三个文件:
docker-compose.ci.build.yml:用于持续集成(CI的 Docker 合成文件docker-compose.yml:用于定义要构建和运行的图像集合的基本 Docker Compose 文件docker-compose.override.yml:开发环境对docker-compose.yml的覆盖
您可以为其他环境创建类似于docker-compose.override.yml的文件。
工具栏将为 Docker 添加额外选项:

你可以使用 Docker 运行甚至调试你的应用;它是透明的。VisualStudio2019 引入了对容器的出色支持。在以下屏幕截图中,您可以看到定义的环境:

以下是文件系统:

以下屏幕截图显示了执行日志:

即使您的解决方案中有多个 Docker 项目,您也可以在调试时无缝地从一个项目跳到另一个项目,这非常酷!在构建项目时,您只需确保 Docker 已提前运行,并且 Docker 映像(mcr.microsoft.com/dotnet/core/aspnet:3.1在本地可用。
Docker images for ASP.NET Core are listed at https://hub.docker.com/_/microsoft-dotnet-core-aspnet.
As of now, running under Docker requires Docker For Windows (https://www.docker.com/docker-windows).
有关更多信息,请跳至https://docs.microsoft.com/en-us/aspnet/core/publishing/docker 和https://docs.microsoft.com/en-us/aspnet/core/publishing/visual-studio-tools-for-docker 。
Docker 是现代开发的无价工具,也是云开发的根源,所以我强烈建议您看看。但由于我们必须使用其他设置,现在让我们看看如何将我们的应用部署为 Windows 服务。
作为 Windows 服务部署
部署应用的另一种选择是将 ASP.NET Core 应用作为 Windows 服务托管。当然,这本质上是不可移植的,因为 Windows 服务只在 Windows 上可用(当然,Windows Docker 容器确实存在)。无论如何,有时,特别是对于简单的应用/API,这是最好的选择,因为您可以根据自己的喜好轻松启动和停止服务,并且可以轻松查看它们是否从用户界面运行。让我们看看如何:
- 首先添加
Microsoft.AspNetCore.Hosting.WindowsServices和Microsoft.Extensions.Hosting.WindowsServicesNuGet 包。 - 然后,修改你的
Program类如下:
Host
.CreateDefaultBuilder(args)
.UseWindowsService()
.ConfigureWebHostDefaults(builder =>
{
builder.UseStartup<Startup>();
});
- 然后,使用
dotnet publish将您的应用部署到您机器上的文件夹中,然后在 Windows 中注册并启动服务:
sc create MyService binPath="C:\Svc\AspNetCoreService.exe"
sc start MyService
总之,这将实现以下功能:
- 将
WindowsServiceLifetime设置为应用的IHostLifetime。 - 将当前目录设置为
AppContext.BaseDirectory。 - 启用以应用名称作为事件源名称的事件日志记录。
页面位于https://docs.microsoft.com/en-us/aspnet/core/hosting/windows-service 包含所有这些信息和更多信息;一定要看。
总结
本章将帮助您更好地了解可用的不同托管选项以及如何使用它们。特别是 Azure、AWS 和 Docker 可能非常有用;Azure 和 AWS 都完全支持 DOCKER,所以确保你把它们当作部署策略的一部分!
即使使用 VisualStudio 部署应用很方便,但知道如何使用命令行也是很有用的,这基本上就是 VisualStudio 所做的。
大多数时候,我们 Windows 开发人员将部署到 IIS;因此,您应该学习如何使用 Web 部署工具服务和用户界面。您可以很容易地将 web 部署包作为.zip文件分发。对于其他操作系统的用户来说,NGINX 是一个受欢迎的选择,拥有大量的用户社区。
Docker 是街区里新来的(很酷的)孩子;它在创建容器方面提供了前所未有的便利性,您可以选择容器并将其部署到 Azure、AWS 或其他云提供商,或者只在本地基础设施上运行。
Windows 服务有时对简单的事情很有用;你可以随时开始和停止它,你不需要太在意它。我不希望你能充分利用它们,但很高兴知道这个选项是可用的。
本书到此结束。我希望你喜欢它——我当然喜欢写它!请让我和 Packt 知道您对它的想法,以及在未来的版本中可以改进的地方!非常感谢你的陪伴!
问题
因此,在本章结束时,您应该了解以下内容:
- 使用 IIS 向外界公开应用有什么好处?
- 为什么要将 web 应用作为 Windows 服务托管?
- 使用 Docker 的优势是什么?
- 什么是自包含部署?
- 是否可以在运行时检测更改并自动重新编译代码?
- 我们可以使用命令行部署还是需要 VisualStudio?
- 您可以从 VisualStudio 内部部署到哪些云提供商?
二十、附录 A:.NET 工具
网络工具
dotnet工具是.NET Core 开发的瑞士军刀。它可以用于很多事情,从运行和创建新项目,到构建它们,添加 NuGet 引用,运行您命名的单元测试。安装了.NET Core软件开发工具包(SDK)。在这里,我们将看到它提供的一些最有用的命令。
默认情况下,dotnet始终使用它能找到的最新.NET Core 版本运行。您可以通过--info参数列出所有已安装的版本,如下所示:
dotnet --info
建筑
此工具可用于构建项目或整个解决方案。用于构建的命令为build,最典型的参数如下:
-
<solution | project>:构建特定的项目或解决方案;如果未指定,它将尝试在当前文件夹中查找一个 -
-c <configuration>:配置值之一,如调试、发布等 -
-r <runtime>:支持的运行时配置之一,如果您希望针对特定的运行时配置;有关可能值的列表,请参见第 19 章、应用部署 -
-f <framework>:支持的框架之一;有关可能值的列表,请参见第 19 章、应用部署 -
-o <folder>:目的地输出目录;如果未指定,则默认为当前文件夹
此外,clean命令会清除所有以前的构建,并且除了解决方案或项目之外,它采用相同的参数。
从模板创建项目
从模板创建项目有时很有用,即使不使用 VisualStudio。此命令为new,典型参数如下:
-l:列出可用的模板<template> -name <name>:根据给定名称的模板创建项目;如果未指定名称,则默认为当前文件夹名称<template> -lang <lang>:根据给定语言的模板创建项目;支持的语言有C、F和Visual Basic(VB),如果未指定,则默认为C-o <folder>:目的地输出目录;如果未指定,则默认为当前文件夹--update-apply:更新本地模板
单元测试
dotnet可以使用test命令列出并运行单元测试项目。最常见的参数如下:
-
-t:列出测试 -
--filter <expression>:仅执行与给定表达式匹配的测试;参见https://aka.ms/vstest-filtering 获取支持的语法 -
-d <logfile>:允许记录到文件 -
-r <folder>:放置结果的文件夹 -
--blame:生成一个描述运行顺序的文件(Sequence.xml),以隔离导致测试主机崩溃的问题 -
<solution | project>:构建特定的项目或解决方案;如果未指定,它将尝试在当前文件夹中查找一个 -
-c <configuration>:配置值之一,如调试、发布等 -
-r <runtime>:支持的运行时配置之一,如果您希望针对特定的运行时配置;有关可能值的列表,请参见第 19 章、应用部署 -
-f <framework>:支持的框架之一;有关可能值的列表,请参见第 19 章、应用部署 -
-o <folder>:目的地输出目录;如果未指定,则默认为当前文件夹
管理包引用
dotnet工具可用于在项目文件中添加或删除对 NuGet 软件包或其他项目的引用。命令为add或remove,一些常见参数如下:
<project> package <nugetref>:添加或删除对项目的 NuGet 引用;不能与reference组合<project> reference <projectref>:添加或删除对项目的项目引用;项目参考必须是绝对的或相对于要添加的项目的;不能与package组合
跑
该工具可以运行项目,可以选择先恢复依赖项并构建它。默认情况下,它使用默认设置(概要文件、框架和运行时)构建项目并恢复所有依赖项。命令为run,典型参数(可组合)如下:
-
-p <project>:项目运行的路径;如果未提供,它将尝试在当前文件夹中查找单个项目或解决方案 -
--launch-profile <profile>:从launchSettings.json运行配置文件 -
--interactive:允许交互,如请求验证或用户输入 -
--no-restore:在生成之前不还原依赖项(NuGet 软件包) -
--no-dependencies:不恢复依赖的项目依赖关系,只恢复目标项目 -
--force:强制解决依赖关系,即使上次还原失败 -
--no-build:未在项目运行前进行项目建设;假设已经生成了一个版本 -
-v <verbosity>:设置输出的日志详细级别(安静、最小、正常、详细、诊断) -
-c <configuration>:配置值之一,如调试、发布等 -
-r <runtime>:支持的运行时配置之一,如果您希望针对特定的运行时配置;有关可能值的列表,请参见第 19 章、应用部署 -
-f <framework>:支持的框架之一;有关可能值的列表,请参见第 19 章、应用部署
出版
发布时,命令为publish,典型参数如下:
-
--manifest <manifest>:包含要从publish命令中排除的包列表的清单文件的路径 -
--self-contained:将应用发布为独立的,即不需要在目标机器上安装.NET Core 运行时;不能与--no-self-contained组合 -
--no-self-contained:将发布应用,使其不需要在目标机器上安装.NET Core 运行时;不能与--self-contained组合;这是默认设置 -
--interactive:允许交互,如请求验证或用户输入 -
--no-restore:在生成之前不还原依赖项(NuGet 软件包) -
--no-dependencies:不恢复依赖的项目依赖关系,只恢复目标项目 -
--force:强制解决依赖关系,即使上次还原失败 -
--no-build:未在项目运行前进行项目建设;假设已经生成了一个版本 -
-v <verbosity>:设置输出的日志详细级别(安静、最小、正常、详细、诊断) -
-o <folder>:目的地输出目录;如果未指定,则默认为当前文件夹 -
-c <configuration>:配置值之一,如调试、发布等 -
-r <runtime>:支持的运行时配置之一,如果您希望针对特定的运行时配置;有关可能值的列表,请参见第 19 章、应用部署 -
-f <framework>:支持的框架之一;有关可能值的列表,请参见第 19 章、应用部署
努吉
有几个命令可用于生成和发布 NuGet 包。首先,我们可以将项目打包为 NuGet 包。这是通过pack命令实现的,典型参数如下:
-
<project | solution>:要打包的项目或解决方案 -
--include-symbols:包括带有调试符号的包 -
--include-source:包括.pdb和源文件 -
--no-build:未在项目运行前进行项目建设;假设已经生成了一个版本 -
-v <verbosity>:设置输出的日志详细级别(安静、最小、正常、详细、诊断) -
--interactive:允许交互,如请求验证或用户输入 -
--no-restore:在生成之前不还原依赖项(NuGet 软件包) -
--no-dependencies:不恢复依赖的项目依赖关系,只恢复目标项目 -
--force:强制解决依赖关系,即使上次还原失败 -
-o <folder>:目的地输出目录;如果未指定,则默认为当前文件夹 -
-c <configuration>:配置值之一,如调试、发布等 -
-r <runtime>:支持的运行时配置之一,如果您希望针对特定的运行时配置;有关可能值的列表,请参见第 19 章、应用部署 -
-f <framework>:支持的框架之一;有关可能值的列表,请参见第 19 章、应用部署
拥有 NuGet 文件(.nupkg后,可以将其发布到 NuGet 存储库或删除现有版本。这是通过nuget命令实现的,以下是最常见的参数:
push <package> -s <url> -k <apikey>:使用 NuGet应用编程接口(API键)将特定版本的包推送到源服务器--skip-duplicate:忽略服务器上是否已经存在包和版本delete <package> -s <url> -k <apikey>:从 NuGet 存储库中删除具有特定版本的包locals -l:列出本地 NuGet 缓存位置locals -c <all | http-cache | global-packages | temp>:清除其中一个指定的 NuGet 缓存;all清除所有这些内容
全球工具
.NET Core 具有全局工具,可以用作dotnet工具的扩展;例如,用户机密和实体框架(EF核心是通过dotnet本身访问和安装的全局工具。命令为tool,最常见的参数如下:
-
install <tool>:安装工具;明显与uninstall或update不兼容 -
uninstall <tool>:卸载工具;不能与install或update一起使用 -
update <tool>:将工具更新至最新版本;无法与install一起使用或卸载 -
-g:全局安装/卸载/更新工具;如果未指定,它将在本地文件夹上运行 -
--version <version>:安装特定版本;仅适用于install -
list:列出已安装的工具 -
restore:恢复清单文件中指定的工具;不能与-g一起使用 -
--tool-manifest <manifest>:清单文件的路径 -
--configfile <file>:指定 NuGet 配置文件的路径 -
--add-source <location>:增加一个额外的包源;<location>可以是 NuGet 存储库统一资源定位器(URL)或本地文件夹
工具可以全局安装在添加到路径的默认操作系统特定位置,也可以本地安装在bin文件夹中。工具是从 NuGet 存储库安装的。
用户机密
dotnet也可用于管理用户机密。命令为user-secrets,最常用的参数如下:
-p <project>:项目运行的路径;如果未提供,它将尝试在当前文件夹中查找单个项目或解决方案-c <configuration>:配置值之一,如调试、发布等clear:清除用户机密init:初始化用户机密数据库list:列出当前键remove <key>:删除密钥set <key> <value>:设置键值--id <id>:要使用的用户密码 ID;通常在.csproj文件中指定
文件监视程序
dotnet工具通过watch扩展名,可以用来运行命令,同时监视文件夹下的文件。它有助于检测文件的更改并立即对其作出反应。任何命令都可以通过watch运行,Microsoft Build(MSBuild).csproj语法支持指定不需要监视的扩展名或文件的排除。该命令为watch,您将传递给dotnet的任何其他命令都可以在它之后传递,例如,以下命令:
dotnet watch test
默认情况下,watch跟踪所有文件扩展名;如果您希望排除并仅包括某些,您可以使用全局模式在.csproj文件中指定它们,如下所示:
<ItemGroup>
<Watch Include="**\*.js" Exclude="node_modules\**\*;**\*.js.map;obj\**\*;bin\**\*" />
</ItemGroup>
这将监视所有的.js文件,除了node_modules、obj或bin下的任何文件,以及以.js.map结尾的任何文件。还可以逐个文件指定项目文件中不应监视的文件。事情是这样的:
<ItemGroup>
<Compile Include="File.cs" Watch="false" />
<EmbeddedResource Include="Resource.resx" Watch="false" />
<ProjectReference Include="..\ClassLibrary\ClassLibrary.csproj" Watch="false" />
</ItemGroup>
在本例中,我们可以看到,我们关闭了对单个文件、嵌入式资源和整个项目引用的监视。
EF 核心
让 EF Core 与数据库交互的方法也是通过dotnet——具体来说就是ef工具。以下是一些有用的命令:
-
database drop:删除目标数据库 -
database drop -f:在没有确认的情况下强制执行下降操作 -
database drop --dry-run:显示要删除的内容,但实际上并没有这样做 -
database update <0 | migration>:将目标数据库更新为特定迁移;如果未指定(或0),则默认为最新版本 -
-c <context>:上下文类型名称或完全限定类型名称;如果未指定,则它必须是目标项目上的唯一上下文 -
-p <project>:用于上下文的项目 -
-s <startupproject>:要使用的启动项目 -
--no-build:未在项目运行前进行项目建设;假设已经生成了一个版本 -
--configuration <configuration>:配置值之一,如调试、发布等 -
-r <runtime>:支持的运行时配置之一,如果您希望针对特定的运行时配置;有关可能值的列表,请参见第 19 章、应用部署 -
-f <framework>:支持的框架之一;有关可能值的列表,请参见第 19 章、应用部署 -
migrations add <migration>:使用给定名称创建新迁移 -
migrations add <migration> -o <folder>:目的地输出目录;如果未指定,则默认为迁移 -
migrations list:列出现有迁移 -
migrations remove:删除最新的迁移 -
migrations script <0 | from> <to>:生成一个结构化查询语言(SQL)脚本,将源迁移(如果未指定,默认为初始迁移)更改为目标迁移(如果未指定,则为最后一个) -
dbcontext list:列出目标项目的可用上下文 -
dbcontext info:显示特定上下文的信息;需要-c参数 -
dbcontext scaffold <connection> <provider>:为特定连接字符串和提供程序生成普通旧公共语言运行时(CLR)对象(POCO类) -
--schema <schema>:要为其生成 POCO 类的架构名称;可提供多个;如果没有,则包括所有模式 -
--table <table>:生成 POCO 类的表名;可提供多个;如果没有,则选择所有表 -
--use-database-names:不要使用类似 C 的名称,而是使用数据库中的名称 -
-d:使用属性配置scaffold生成的模型 -
--context-dir <path>:放置生成的上下文的路径
二十一、答案
第一章
- 依赖注入有什么好处?
它允许更好地分离接口和实际实现,并允许我们随时更改实现。它还递归地注入所有必需的依赖项。
- 什么是环境?
一组命名的启动值。
- MVC 是什么意思?
Easy peasy:模型视图控制器!
- 内置依赖注入容器中支持的生存期是多少?
Transient(每次创建一个新实例)、Scoped(每次 HTTP 请求都创建一个新实例并始终返回)、Singleton(创建一个实例)。
- .NET Core 与.NET 标准有什么区别?
.NET 标准只是由.NET 和.NET Core 等实现的一组标准 API。
- 什么是元包?
由 Microsoft 定义的一组软件包。它包含简单 ASP.NET Core 项目通常需要的所有包。
- OWIN 是什么?
OWIN 代表为.NET开放的 Web 界面,您可以在上阅读 http://owin.org 。本质上,它是一种将 web 应用与特定服务器(如 IIS)分离的规范。
第二章
- 检索配置值的根接口是什么?
是IConfiguration。这是IConfigurationRoot和IConfigurationSection继承的地方。
- 什么是.NET Core 中内置的基于文件的配置提供程序?
JSON、XML 和 INI。
- 是否可以将配置绑定到开箱即用的 POCO 类?
是的,但是我们需要添加Microsoft.Extensions.Configuration.BinderNuGet 包。
- 在
IOptions<T>和IOptionsSnapshot<T>接口之间有什么区别?
IOptionsSnapshot<T>在基础配置更改时更新(如果我们为此进行了配置),但IOptions<T>始终保留原始配置值。
- 我们是否需要在依赖项注入容器中显式注册配置对象?
否。从 ASP.NET Core 2.0 开始,IConfiguration自动注入。
- 我们如何拥有可选的配置文件?
注册基于文件的配置文件时,将optional参数设置为true。
- 当配置发生变化时,是否可以获得通知?
对我们需要获得一个重新加载令牌,然后在其中注册一个更改回调委托。
第三章
- 什么是特殊路线代币?
[controller]、[action]和[area]。
- 我们如何防止根据请求的 HTTP 谓词选择路由?
将[HttpPost]、[HttpGet]、[HttpPut]或其他 HTTP 属性之一应用于其操作方法。
- 除非请求使用 HTTPS,否则如何防止路由被选择?
将[RequireHttps]属性应用于路由的操作方法。
- 我们如何根据发生的 HTTP 错误代码提供不同的视图?
一种方法是在Startup类的Configure方法中使用UseStatusCodePagesWithRedirects或UseStatusCodePagesWithReExecute,通过向控制器添加[HttpGet("error/401")]属性,在控制器上创建响应特定错误代码(例如/error/401的操作方法。
- 如何防止调用控制器中的方法?
最好的方法是对我们想要隐藏的方法应用[NonAction]属性。
- 我们如何强制路由值为特定类型(例如,数字)?
添加number验证令牌,例如[HttpGet("{id:number]")]。
- 什么是路线处理程序?
实际处理请求的是IRouter接口的实现。对于 MVC,它通常是MvcRouteHandler类,通过UseMvc扩展方法添加到管道中。
第四章
- 模型状态的默认验证提供程序是什么?
它是数据注释 API。
- 什么是动作?
可以在控制器上调用以响应 HTTP 请求的方法。
- 什么是全球化?全球化与本土化有何不同?
全球化意味着您的应用将支持多种区域性。本地化是使应用正常工作并对特定文化做出相应反应的过程。
- 临时数据的用途是什么?
临时数据用于在同一客户端的两个后续请求之间持久化数据,如微会话。
- 缓存有什么用?
缓存对于需要很长时间才能生成或检索的数据非常有用。我们将这些数据存储在缓存中,并可以快速访问它。
- 什么是会话?
会话是在给定时间范围内与网站进行的一组用户交互。每个交互都通过会话 ID 与会话相关联。
- 从
Controller基类继承控制器有什么好处?
Controller类提供了ControllerBase中没有的一些有用方法,例如返回视图的方法(View。
第五章
- 视图的基类是什么?
为RazorPage<T>,其中T泛型类型默认为dynamic。
- 如何向视图中注入服务?
在.cshtml文件上使用@inject声明,或者从RazorPage<T>类继承并使用构造函数注入。
- 什么是视图位置扩展器?
它是一个组件,可用于告诉 ASP.NET Core 在何处查找物理.cshtml文件。
- 什么是视图布局?
它类似于 ASP.NET Classic 中的母版页。本质上,它定义了不同页面可以使用的布局或结构。
- 什么是局部视图?
部分视图类似于 ASP.NET Classic 中的 web 用户控件(.ascx文件)。它们是包含可重用标记的文件,可能还包含要跨不同视图重用的代码。
- 什么功能可以替代局部视图?
查看组件。
- 特殊文件
_ViewStart.cshtml的作用是什么?
无论您在其中输入什么代码、@using或@inject声明,都会在实际视图之前执行。
第六章
- 默认的验证提供程序是什么?
它是数据注释验证程序。
- 用于呈现 HTML 字段的方法叫什么?
HTML 助手。
- 什么是模型元数据?
它是描述模型属性的代码,例如它们的显示名称、是否需要它们、它们应该使用什么验证等等。
- ASP.NET Core 是否支持客户端验证?
是的。
- 什么是可以绑定到上传文件的基本接口?
IFormFile。
- 什么是不引人注目的验证?
这是一个过程,通过添加几个 JavaScript 库,可以根据一些约定自动设置验证。
- 我们如何进行服务器端验证?
通过利用[Remote]属性并在控制器上实现验证操作方法。
第七章
- 我们可以使用什么属性来标记方法或控制器,以便只能通过 HTTPS 调用它?
[RequireHttps]。
- 基于角色的授权和基于策略的授权有什么区别?
基于策略的授权更强大;它可以同时使用这两个角色,也可以使用您可以想到的任何其他自定义需求。
- CORS 的目的是什么?
CORS 是一种机制,通过该机制,服务器可以告诉浏览器绕过其正常的安全限制,并允许从不同的源(服务器)加载静态资源(通常是脚本)。
- HSTS 的目的是什么?
这是一种告诉浏览器只能通过 HTTPS 与服务器交互的 web 策略。RFC 6797 中对此进行了规定。
- 认证过程的挑战阶段是什么?
挑战是当服务器要求客户端提供有效凭据时。
- 将请求绑定到模型类时,我们为什么要小心?
我们不希望客户不应提供的敏感信息绑定到模型。
- 饼干的滑动过期时间是多少?
这意味着在每次请求时,cookie 的更新时间与最初设置的时间相同。
第八章
- 什么是小田?
OData 是一个开放协议,用于公开和使用可查询的 RESTful 数据模型。
- 什么是内容协商?
这是一个客户机和服务器就要返回的内容类型达成一致的过程。
- 为什么不适合在 web API 中使用 cookie 进行身份验证?
因为这些 API 的客户端通常不是 web 浏览器,因此可能没有存储 cookie 的能力。
- 我们可以通过哪些不同的方式请求特定版本的 API?
查询字符串或 HTTP 标头。
- 关于行动方法的公约的目的是什么?
约定允许我们定义从每个操作方法返回的返回类型和 HTTP 状态代码,以及可能出现的任何异常。
- 问题详情是什么?
以标准方式返回错误信息的一种方法。RFC 7807 中定义了问题详细信息。
- 什么是休息?
一种用于定义 web 服务的体系结构样式,设计用于互操作性,它依赖于 HTTP 谓词、URL 和标头。
第九章
- 如何从不同的部件加载局部视图?
通过使用嵌入式资源或更好的 Razor 类库。
- 呈现局部视图的两种方式是什么?
一个是使用Html.PartialAsync方法,另一个是使用<partial>标记助手。
- 标记帮助器和标记帮助器组件之间有什么区别?
标记帮助器呈现组件而不是自定义或 HTML 标记,标记帮助器组件允许我们在呈现所有 HTML 标记之前拦截并可能修改它们。
- 我们如何根据环境限制视图上显示的内容?
通过使用<environment>标记帮助器。
- Razor 类库和类库有什么区别?
Razor 类库允许我们非常容易地将静态资源提供给 web 项目,而类库只涉及代码。
- 什么是嵌入式资源?
包含在程序集中并可以从中检索的静态文件(图像、文本和其他文件)。
- 执行视图组件的两个语法是什么?
一个是代码语法-Component.InvokeAsync("mycomponent", new { arg1 = "...", arg2 = 123 })-另一个是标记-<mycomponent arg1="..." arg2="123"/>。
第十章
- 用于控制资源授权的两个接口是什么?
IAuthorizationFilter和IAsyncAuthorizationFilter。
- 为什么每种过滤器都有两种版本?
总是有一个异步版本,这可能是首选。
- 如何通过在动作方法上指定过滤器类型来应用过滤器?
通过ServiceFilterAttribute或TypeFilterAttribute进行。
- 我们如何将订单应用于过滤器的应用?
当使用属性应用过滤器时,我们可以使用Order属性。
- 我们可以应用过滤器的不同级别是什么?
全局、控制器和操作方法。
- 我们如何将上下文从一个过滤器传递给其他过滤器?
使用HttpContext.Items集合。
- 过滤器如何利用依赖注入?
[ServiceFilter]可根据其类型从 DI 获得过滤器。
第十一章
- Razor 页面是否使用代码隐藏?
是的,他们可以,但这不是强制性的。代码隐藏类必须继承自PageModel并与.cshtml文件一起定位。
- 页面模型的用途是什么?
它是实现不同 HTTP 谓词(GET、POST等)的处理程序的地方。
- 什么是页面处理程序?
它们是在PageModel类中处理请求的代码。
- 如何限制匿名用户调用 Razor 页面?
在使用AddRazorPagesOptions方法配置 Razor 页面选项时,我们使用AllowAnonymousToPage扩展方法添加了一个约定,在AddMvc之后。
- 我们可以通过哪两种方式将服务注入剃须刀页面?
一种方法是在PageModel类上使用构造函数注入,另一种方法是在.cshtml文件上使用@inject指令。
- 剃须刀页面是否使用页面布局?
是的,请确保它们与其他视图布局分开。
- 剃须刀页面默认在哪里提供?
Pages文件夹。
第十二章
- 什么是事件计数器?
由应用发出并由操作系统拾取的轻量级代码。
- 遥测的好处是什么?
从单个仪表板集中存储日志和事件并监视应用。
- 我们如何过滤日志记录?
按类别名称或日志级别。
- 什么是健康检查?
它们指示我们的应用或依赖项(例如,数据库、外部服务等)的性能。
- 中间件在日志记录中有何用处?
在处理请求之前和之后,它可以坐在请求和日志的中间。
- 什么是榆树?
错误日志中间件它用于查看处理请求期间引发的事件。
- 与普通日志相比,诊断有什么好处?
Diagnostics 提供强类型事件,并与 ELM 集成,以方便查看事件。
第十三章
- 什么是比较流行的单元测试框架?
NUnit、xUnit 和 MSTest。
- 嘲笑有什么好处?
替换依赖项。
- 单元测试和集成测试有什么区别?
单元测试应该是快速的,没有任何副作用,应该只测试一个特定的操作,而集成测试的规模要大得多。
- 什么是 TDD?
测试驱动开发:一种提倡从单元测试开始的方法。
- 单元测试的局限性是什么?
它们通常不测试用户界面或数据库操作等方面。
- 如何将数据自动传递给单元测试?
所有研究的单元测试框架都允许将数据从属性传递到测试方法。
- 红绿重构是什么意思?
这是 TDD 的一种实践,我们从编写测试开始,测试最初失败(红色),然后让测试通过(绿色),只有这样,我们才应该担心重构代码以提高效率。
第十四章
- 打字脚本有什么好处?
它有很强的类型,是一个完全面向对象的编程模型。
- JavaScript 是否只在浏览器上运行?
不,它也可以在服务器端运行。
- 什么是温泉?
单页应用,基于 JavaScript 的应用,通过 AJAX 风格的调用调用服务器端功能。
- 图书馆经理的目的是什么?
在本地项目中安装客户端库。
- dotnet SPA 框架的模板是否已硬编码?
不,它们作为 NuGet 软件包提供,可以安装和更新。
- 我们如何从.NET Core 运行 JavaScript 代码?
节点服务提供了这一功能。
- 列举一些具有 dotnet 模板的 SPA 框架。
Vue、React、Angular、Aurelia 和 Knockout。
第 15 章
- ASP.NET Core 3 可用的两台主机是什么?
红隼和 HTTP.sys。
- 可用的两种缓存是什么?
在内存和分布式中。
- 压缩响应有什么好处?
最小化响应的延迟。
- 缓存响应的目的是什么?
避免了请求和发送响应的需要。
- 异步操作是否提高了性能?
没有,但它们提高了可伸缩性。
- 什么是捆绑?
捆绑是在单个响应中组合多个文件。
- 剖析器有什么用?
它们可以向我们展示代码中执行时间最长的部分。
第十六章
- Signal 支持哪两种序列化格式化程序?
JSON 和消息包。
- 信号员支持什么交通工具?
AJAX 长轮询、WebSocket 和服务器发送事件。
- 消息包协议的好处是什么?
消息包更紧凑,因此延迟更低。
- 我们可以向哪些目标发送消息?
单个收件人、所有用户或用户组。
- 我们为什么要限制信号员使用交通工具?
某些传输可能不受支持或受到限制,例如 WebSocket。
- 我们能否从承载 Signal 的 web 应用外部向其发送消息?
对
- 我们可以用信号机进行身份验证吗?
对
第 17 章
- 页面和组件之间有什么区别?
两者之间的唯一区别是可以直接从浏览器访问页面。
- 服务器托管模式和 WebAssembly 托管模式有什么区别?
服务器模型依赖信号器与服务器进行通信,而 WebAssembly 仅存在于正在编译为webassembly的客户机上,因此得名。WebAssembly 在断开连接时可以工作。
- 我们可以在 Blazor 页面中使用标签助手吗?
不,我们不能(在.razor文件中)。
- 是否可以从 Blazor 内部访问包含的网页?
是的,它是,使用 JavaScript 互操作性。
- Blazor 是否支持依赖注入?
是的,但不是构造函数注入。
- Blazor 页面布局是否支持区域?
是的,但只有一个区域体。
- 组件的不同渲染模式有什么区别?
不同之处在于性能和容量:Static最快,但不支持服务器事件;Server最慢,无法获取参数;ServerPrerendered是两者之间的折衷——组件的 a 部分已预呈现,但实际交互仅在页面完全加载时开始。
第 18 章
- 什么是 gRPC?
gRPC 是 Google Remote Procedure Call,这是一种跨平台的技术,用于实现与该技术无关的 web 服务。
- URL 重写的目的是什么?
显示用户友好的 URL,这些 URL 将被转换为对应用有意义的内容。
- 后台服务的用途是什么?
在后台运行任务。
- 区域的用途是什么?
从物理上和概念上分离应用的各个部分。
- 习俗有什么好处?
自动强制默认设置。
- 我们如何自动执行引用程序集的代码?
通过向库添加一个[HostingStartup]属性并将其包含在ASPNETCORE_HOSTINGSTARTUPASSEMBLIES环境变量中。
- 我们可以从组件内部加载文件吗?
是,作为嵌入式资源或来自 Razor 类库。
第 19 章
- 使用 IIS 向外界公开应用有什么好处?
例如,您可以添加日志记录和 Windows 身份验证。
- 您为什么要将 web 应用作为 Windows 服务托管?
它使启动和停止以及查看服务是否正在运行变得更加容易。
- 使用 Docker 有什么好处?
您可以准确地控制依赖项和运行环境。
- 什么是自包含部署?
它是一种不需要在目标计算机上安装.NET Core 的部署。
- 是否可以在运行时检测更改并自动重新编译代码?
是的,我们只需要运行dotnet watch命令。
- 我们可以使用命令行部署还是需要 Visual Studio?
我们可以使用命令行(dotnet)或 Visual Studio。
- 您可以从 VisualStudio 内部部署到哪些云提供商?
Azure 和 AWS 就是两个例子。
第一部分:ASP.NET Core 3 基础
第一部分将介绍 ASP.NET Core 和模型视图控制器(MVC)模式的基本原理,以及两者如何结合,并将探讨.NET Core 及其概念。
本节包括以下章节:
第二部分:提升生产力
本节将向我们展示如何通过强制重用、流程表单和有效的安全措施来提高生产效率。
本节包括以下章节:
第三部分:高级话题
本节将介绍高级主题,介绍经验证的提高代码性能的技术。
本节包括以下章节:


浙公网安备 33010602011771号