ASP.NET Core Razor Pages in Action 2 构建您的第一个应用程序
本章涵盖
• 创建 Razor Pages 应用程序
• 添加您的第一个页面
• 探索项目文件及其所扮演的角色
• 使用中间件配置应用程序管道
在上一章中,你了解了 Razor Pages Web 开发框架(作为 ASP.NET Core 的一部分)如何适应整个 .NET Framework。您已经发现了可以使用 Razor Pages 构建的应用程序类型,而且重要的是,当它不是最佳解决方案时。您已经了解了使用 Razor Pages 高效工作所需的工具,并希望下载并安装了 Visual Studio 或 VS Code 以及最新版本的 .NET SDK。现在您已经设置了开发环境,是时候开始使用代码了。
在本章中,您将使用 Visual Studio 和 CLI 创建您的第一个 Razor Pages 应用程序,以便您可以在所选的作系统上进行作。大多数 Web 开发框架都提供初学者工具包或项目 — 一个简单的应用程序,构成您自己的应用程序的起点。Razor Pages 也不例外。构成初学者工具包的应用程序只有三个页面,但它包括一个基本配置,您可以在此基础上构建以创建自己的更复杂的应用程序。
创建应用程序并设法在浏览器中启动它后,您将向应用程序添加新页面并包含一些动态内容,以便您可以开始了解 Razor 页面的实际含义。测试页面以确保其正常工作后,您将使用网站的主模板文件将页面添加到网站导航中。
然后,我将讨论该工具生成的应用程序文件,以了解每个生成的文件在 Razor Pages 应用程序中所扮演的角色。本演练将帮助您了解所有 ASP.NET Core 应用程序背后的基础知识。
在本演练的最后,我们将仔细研究主要应用程序配置:请求管道。这是应用程序的核心。它定义应用程序如何处理请求以及向客户端提供响应。您将了解如何从中间件组件构建它,以及如何通过添加自己的中间件来扩展它。
在本章结束时,您应该对 Razor Pages 应用程序的工作原理有一个很好的高级了解,从接收请求到最终将 HTML 发送回客户端。然后,您将准备好在第 3 章中深入探讨如何使用 Razor 页面及其配套 PageModel 类。
2.1 创建您的第一个网站
本部分将介绍如何使用可用工具快速生成功能齐全的 Razor Pages 应用程序。您将在 Windows 10 上使用 Visual Studio 2022 Community Edition,并为非 Windows 读者使用 CLI。我将讨论在 Visual Studio Code 中使用 CLI,尽管您可以使用任何终端应用程序来执行 CLI 命令。因此,以下部分假定您已安装并运行环境,以及支持 .NET 6 开发的 SDK 版本。您可以通过打开命令 shell 并执行以下命令来测试您的机器上是否安装了合适的 SDK 版本:
dotnet --list-sdks
您应该会看到列出了一个或多个版本,每个版本都有自己的安装路径。至少有一个版本应以 6 开头。在此阶段,如果您是第一次使用的用户,您还需要信任自签名证书,该证书是在本地系统上通过 HTTPS 轻松浏览站点所需的(第 14 章中有更详细的介绍)。为此,请执行以下命令:
dotnet dev-certs https --trust
证书本身作为 SDK 安装的一部分进行安装。
2.1.1 使用 Visual Studio 创建网站
如第 1 章所述,Visual Studio 是在 Windows 上工作的 .NET 开发人员的主要 IDE。它包括用于执行最常见任务的简单菜单驱动工作流。Razor Pages 应用程序是在 Visual Studio 中创建为项目,因此打开 Visual Studio 后,您的起点是创建新项目。您可以通过单击启动启动画面上的 Create a New Project 按钮或转到 File > New Project...在主菜单栏中。
在下一个屏幕上,您可以从模板列表中选择要创建的项目类型。在此之前,我建议从右侧窗格顶部的语言选择器中选择 C# 以过滤掉一些干扰。选择 ASP.NET Core Web App 模板 — 名称中没有 (Model-View-Controller) 的模板,还要注意避免选择名称非常相似的 ASP.NET Core Web API 模板。正确的模板带有以下说明:“用于创建 ASP.NET Core 应用程序的项目模板,其中包含 ASP.NET Razor Pages 内容。
为应用程序文件选择合适的位置并移动到下一个屏幕后,请确保您的 Target Framework 选择是 .NET 6,将所有其他选项保留为默认值。Authentication Type 应该设置为 None,应该选中 Configure for HTTPS,并且你应该取消选中 Enable Docker 选项(图 2.1)。对选择感到满意后,单击 Create 按钮。此时,Visual Studio 应该会打开,并在 Solution Explorer 中显示您的新应用程序(图 2.2)。
图 2.1 在点击 Create 按钮之前检查您是否已应用这些设置。
图 2.2 新应用程序将在 Visual Studio 中打开,其中有一个概述页,右侧打开“解决方案资源管理器”窗口,其中显示了 WebApplication1 解决方案及其单个项目(也称为 WebApplication1)的结构和内容。
尽管 Solution Explorer 的内容看起来像文件结构,但并非您看到的所有项实际上都是文件。我们将在本章后面仔细研究这些项目。
2.1.2 使用命令行界面创建网站
如果您已经使用 Visual Studio 构建了应用程序,则可能需要跳过此步骤。但是,我建议您也尝试这种方法来创建应用程序,因为该过程会揭示 Visual Studio 中的新项目创建向导隐藏的一两个令人兴奋的事情。
CLI 是一种基于文本的工具,用于对 dotnet.exe 工具执行命令,这两者都是作为 SDK 的一部分安装的。CLI 的入口点是 dotnet 命令,用于执行 .NET SDK 命令和运行 .NET 应用程序。在接下来的部分中,您将将其用于第一个目的。SDK 的默认安装会将 dotnet 工具添加到您的 PATH 变量中,因此您可以从系统上的任何位置对它执行命令。
可以使用您喜欢的任何命令 shell 调用 CLI 工具,包括 Windows 命令提示符、Bash、终端或 PowerShell(有跨平台版本)。从现在开始,我将 shell 称为终端,主要是因为它在 VS Code 中命名。以下步骤并不假定您使用 VS Code 执行命令,但您可以使用 VS Code 提供的集成终端来执行命令。
首先,在系统上的适当位置创建一个名为 WebApplication1 的文件夹,然后使用终端导航到该文件夹,或在 VS Code 中打开该文件夹。如果您选择使用 VS Code,则可以通过按 Ctrl-' 访问终端。在命令提示符下,键入以下命令,并在每个命令后按 Enter 键。
列表 2.1 使用 CLI 创建 Razor Pages 应用程序
dotnet new sln ❶
dotnet new webapp -o WebApplication1 ❷
dotnet sln add WebApplication1\WebApplication1.csproj ❸
❶ 创建解决方案文件
❷ 搭建新的 Razor Pages 应用程序基架,并将输出放入名为 WebApplication1 的子文件夹中
❸ 将 Razor Pages 应用程序添加到解决方案
执行最后一个命令后,所有应用程序文件都应该成功创建。您还应该从终端获得一些与某些 “post-creation actions” 相关的反馈。您到 WebApplication1 的路径可能与我的路径大不相同,如下面的清单所示,但其余的反馈应该相似。
列表 2.2 CLI 执行的创建后作的通知
Processing post-creation actions...
Running 'dotnet restore' on WebApplication1\WebApplication1.csproj...
Determining projects to restore...
Restored D:\MyApps\WebApplication1\WebApplication1\WebApplication1.csproj
(in 80 ms).
Restore succeeded.
CLI 在您的应用程序上执行 dotnet restore 命令,确保您的应用程序所依赖的所有软件包都已获取且是最新的。如果使用 Visual Studio 创建应用程序,将执行相同的命令,但指示它已发生并不那么明显。它显示在 IDE 底部的状态栏中(图 2.3)。
图 2.3 Visual Studio 底部的状态栏显示项目已恢复。
2.1.3 运行应用程序
现在,应用程序已使用您选择的任何方式创建,您可以在浏览器中运行和查看它。要从 Visual Studio 运行应用程序,您只需按 Ctrl-F5 或单击顶部菜单栏中轮廓的绿色三角形(不是实心三角形)。这将负责构建和启动应用程序,以及在浏览器中启动它。如果您使用的是 CLI,请执行以下命令:
dotnet run --project WebApplication1\WebApplication1.csproj
此命令包括 --project 开关,用于指定项目文件的位置。如果从包含 csproj 文件的文件夹中执行命令,则省略 --project 开关。如果您更喜欢在 Visual Studio 中使用 CLI,请按 Ctrl-' 打开集成终端,然后从内部执行命令。
您应该在终端中收到正在构建应用程序的反馈,然后再确认它正在侦听两个 localhost 端口,其中一个使用 HTTP,另一个使用 HTTPS。实际端口号因项目而异:
info: Microsoft.Hosting.Lifetime[0]
Now listening on: https://localhost:7235
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://localhost:5235
打开浏览器,然后导航到使用 HTTPS 的 URL。在此示例随附的下载中,即 https://localhost:7235。如果您的浏览器警告您该站点不安全,您可能忽略了信任自签名证书所需的命令:dotnet dev-certs https --trust。如果一切顺利,您应该会看到类似于图 2.4 的内容。
图 2.4 首页
该应用程序是初级的。主页包含最少的样式和内容。使用页面顶部的导航或页脚中的链接导航到 Privacy (隐私) 页面。请注意,相同的最小样式也被应用于 Privacy 页面(图 2.5),并且存在导航。
图 2.5 隐私页面包含与主页相同的页眉、页脚和样式。
目前,您可以使用此应用程序执行的其他作不多。目前还没有任何有趣的方式来与它交互,因此是时候向应用程序添加一个页面了。
2.1.4 添加新页面
在本节中,您将向应用程序添加新页面。您还将探索添加到 .NET 6 中的新功能,称为热重载。此功能会导致对代码所做的更改反映在正在运行的应用程序中,而无需重新启动它。这是为 Visual Studio 用户自动激活的。VS Code 用户需要使用略有不同的命令来启用热重载。此功能适用于对现有文件的更改。由于您要添加新文件,因此需要先停止应用程序。Visual Studio 用户只需关闭浏览器即可停止应用程序。如果您使用 CLI 命令启动了应用程序,则应在终端窗口中按 Ctrl-C 以关闭应用程序。
Visual Studio 用户应右键单击 Solution Explorer 中的 Pages 文件夹,然后从可用选项中选择 Add > Razor Page(添加 Razor 页面)(图 2.6)。将文件命名为 Welcome .cshtml。
图 2.6 要在 Visual Studio 中添加新页面,请右键单击 Pages 文件夹,然后选择 Add,然后选择 Razor Page。
VS Code 用户应确保其终端位于项目文件夹(包含 csproj 文件的文件夹)中,然后执行以下命令:
dotnet new page -n Welcome -o Pages -na WebApplication1.Pages
new page 命令将 Razor 页面添加到应用程序。-n(或 --name)选项指定创建页面时应使用的名称。-o(或 --output)选项指定将放置页面的输出位置。-na(或 --namespace)选项指定应应用于生成的 C# 代码文件的命名空间。或者,您可以导航到 Pages 文件夹以创建页面并省略 -o 选项。如果这样做,则必须记住导航回包含 csproj 文件的文件夹,以便在没有其他参数的情况下执行 run 命令。
Visual Studio 用户不需要指定命名空间。应用于使用 Visual Studio 向导创建的代码文件的默认命名空间是通过将项目名称与其在项目中的位置连接起来自动生成的。
现在运行应用程序。请记住,在 Visual Studio 中是 Ctrl-F5,而 CLI 用户(VS Code 或 Visual Studio)这次应该在终端中执行 dotnet watch run(而不是 dotnet run),然后打开浏览器并导航到记录到终端的第一个 URL。导航到 /welcome。页面应该除了页眉和页脚之外没有任何内容(图 2.7)。
图 2.7 新页面除了页眉和页脚之外是空的。
这里有三个有趣的点需要注意。第一个原因是您导航到 /welcome,并且找到并呈现了您刚刚添加到应用程序的 Welcome 页面。您无需执行任何配置即可实现此目的。ASP.NET Core 框架中负责此作的部分称为路由。它会根据 Razor 页面在项目中的位置自动查找 Razor 页面。第 4 章详细介绍了 routing。
需要注意的第二点是,新页面包括您在主页和隐私页面中看到的导航、页脚和样式。您的页面从布局文件(一种主模板)继承了这些内容。同样,这种情况的发生无需您采取任何具体步骤即可实现。您将在下一章中了解 layout 文件以及如何配置它们。
最后要注意的是页面的标题,如浏览器选项卡中所示:WebApplication1。布局页面也提供此值。
现在,可以向页面添加一些代码。更新 Welcome .cshtml 的内容,使其如下所示。
清单 2.3 向 Welcome 页面添加内容
@page
@model WebApplication1.Pages.WelcomeModel
@{
ViewData["Title"] = "Welcome";
}
<h1>Welcome!</h1>
您甚至不需要刷新浏览器,您应用的更改就会在保存后立即显示。这是热重载功能的工作原理。您应该会看到一个一级标题,并且浏览器选项卡中的标题已更改为包含您应用于 ViewData[“Title”] 的值(图 2.8)。ViewData 是一种将数据从 Razor 页面传递到其布局的机制。您将在下一章中看到 ViewData 的工作原理。
图 2.8 对 Razor 页面所做的更改可见,无需刷新浏览器。
2.1.5 修改以包含动态内容
到目前为止,您添加的是静态内容。每次运行此页面时,它看起来都一样。使用 Razor Pages 的全部意义在于显示动态内容,因此现在是时候添加一些内容了。假设您需要在输出中包含当天部分的名称(例如,上午、下午或晚上),也许作为送达确认说明的一部分(例如,“您的包裹将在早上送到您身边”)。首先,您需要根据时间计算一天的一部分,然后您需要渲染它。下面的清单显示了如何从当前时间获取一天中的部分并将其呈现给浏览器。
列表 2.4 向 Razor 页面添加动态内容
@page
@model WebApplication1.Pages.WelcomeModel
@{
ViewData["Title"] = "Welcome!";
var partOfDay = "morning"; ❶
if(DateTime.Now.Hour > 12){
partOfDay= "afternoon"; ❷
}
if(DateTime.Now.Hour > 18){
partOfDay= "evening"; ❸
}
}
<h1>Welcome</h1>
<p>It is @partOfDay on @DateTime.Now.ToString(“dddd, dd MMMM”)</p> ❹
❶ partOfDay 变量被声明并初始化为值 “morning”。
❷ 如果是在中午之后,则使用值 “afternoon” 重新分配变量。
❸ 如果是在下午 6:00 之后,该值将更新为“晚上”。
❹ 变量与当前时间一起呈现到浏览器。
这些更改涉及声明一个名为 partOfDay 的变量,该变量实例化为值 “morning”。两个 if 语句随后会根据一天中的时间更改值。如果是在中午之后,则 partOfDay 将更改为 “afternoon”。下午 6:00 后再次更改为“晚上”。所有这些都是纯 C# 代码,并放置在代码块中,该代码块以 @{ 开头,以结束 } 结尾。然后,您在 Welcome 标题下添加了一个 HTML 段落元素,包括带有两个 C# 表达式的文本,这两个表达式都以 @ 符号为前缀。您刚刚编写了第一段 Razor 模板语法。@ 前缀指示 Razor 呈现 C# 表达式的值。这一次,根据一天中的时间,您应该会在标题下看到呈现给浏览器的新段落,如图 2.9 所示。
图 2.9 浏览器中修改后的 Welcome 页面
2.1.6 将页面添加到导航
接下来,您将新页面添加到站点导航中,因此您不必在浏览器中键入地址即可找到它。在 Pages/Shared 文件夹中找到 _Layout.cshtml 文件并打开它。使用 navbar-nav flex-grow-1 的 CSS 类标识 ul 元素,并在下面的清单中添加粗体代码行。
清单 2.5 将 Welcome 页面添加到主导航中
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Welcome">Welcome</a>
</li>
</ul>
再次刷新浏览器;现在,每个页面顶部的导航菜单将包含指向 Welcome 页面的链接。您刚才所做的更改已应用于应用程序中的每个页面。这是因为您更改了布局文件,该文件由应用程序中的所有页面使用。Razor 页面的内容与布局页面中的内容合并,以生成最终输出。
您可能想知道为什么您添加到布局页面以创建链接的锚元素上没有 href 属性。此元素称为锚点标记帮助程序。标记帮助程序是针对常规 HTML 元素的组件,它使服务器端代码能够通过通常以 asp- 开头的特殊属性来影响它们呈现到浏览器的方式。例如,asp-page 属性采用一个值,该值表示要生成链接的页面的名称。标签帮助程序将在下一章中更详细地介绍。
因此,您已经了解了 C# 和 HTML 在 Razor 页面中协同工作以生成 HTML 的一些方法。通常,最好的建议是将 Razor 页面中包含的 C# 代码量限制为仅影响演示文稿所需的代码量。应用程序逻辑(包括确定时间的算法)应保留在 Razor 页面文件中。Razor 页面文件和应用程序逻辑之间的第一级分离是 PageModel 类,该类构成了下一章的重点,以及我已经介绍的其他与视图相关的技术,包括布局、部件和标记帮助程序。
2.2 浏览工程文件
现在,您已经创建了第一个 Razor Pages 应用程序并尝试了一些 Razor 语法,现在是时候更详细地探索构成您刚刚创建的 Web 应用程序的每个文件夹和文件的内容,以了解每个文件夹和文件在应用程序中所扮演的角色。在此过程中,您将更清楚地了解 ASP.NET Core 应用程序的工作原理。您还将了解磁盘上的物理文件与您在 Visual Studio 的“解决方案资源管理器”窗口中看到的内容之间的区别。
2.2.1 WebApplication1.sln 文件
SLN 文件称为解决方案文件。在 Visual Studio 中,解决方案充当管理相关项目的容器,解决方案文件包含每个项目的详细信息,包括项目文件的路径。Visual Studio 在打开解决方案时使用此信息加载所有相关项目。
较大的 Web 应用程序通常由多个项目组成:一个负责 UI 的 Web 应用程序项目和多个类库项目,每个项目负责应用程序中的一个逻辑层,例如数据访问层或业务逻辑层。也可能有一些单元测试项目。然后,您可能会看到其他项目添加了表示其用途的后缀:WebApplication1.Tests、WebApplication1.Data 等。
此应用程序由单个项目组成。因此,它实际上根本不需要放在解决方案中,但 Visual Studio 仍然会创建解决方案文件。如果使用 CLI 创建应用程序,则通过 dotnet new sln 命令创建了解决方案文件。然后,通过 dotnet sln add 命令将 WebApplication1 项目显式添加到解决方案中。您可以跳过这些步骤,仅在需要向应用程序添加其他项目时才创建解决方案文件。
2.2.2 WebApplication1.csproj 文件
CSPROJ 文件是一个基于 XML 的文件,其中包含有关生成系统(称为 MSBuild)的项目的信息,它负责将源代码文件转换为可针对 .NET 运行时执行的格式。首先,项目文件包含与项目目标的 .NET Framework 版本和您正在使用的 SDK 相关的信息。Microsoft.NET.Sdk 是基本 SDK,用于构建控制台和类库项目等。Web 应用程序是针对 Microsoft.NET.Sdk.Web SDK 构建的。
项目文件包括两个附加属性:Nullable 和 ImplicitUsings。这些功能使您能够切换新的 C# 功能。第一个选项为项目设置可为 null 的注释和警告上下文。简单来说,这控制了您从代码分析器获得的反馈级别,这些代码分析器在代码中查找 NullReferenceException 的潜在来源。此异常是整个 中更多混淆和问题的原因。比其他任何社区都专注于 NET。该功能称为可为 null 的引用类型,默认处于启用状态。您可以通过将值更改为 disable 来关闭它。
ImplicitUsings 属性用于启用或禁用 C# 10 功能,该功能可减少代码文件中所需的显式 using 指令的数量。相反,它们是在 SDK 中全局设置的。已全局启用的 using 指令的选择包括以下常用 API:
• System
• System.Collections.Generic
• System.Linq
• System.Threading.Tasks
此外,该列表还包括一系列特定于 ASP.NET Core 的 API。默认情况下,此功能也处于启用状态。您可以通过将值设置为 disable 或删除该属性来禁用它。
随着时间的推移,项目文件将包含有关项目所依赖的包或外部库的信息。您可以手动将包添加到此文件中,或者更常见的是使用工具添加包(包管理器),该工具将为您更新工程文件的内容。您可以编辑文件的内容以自定义构建的元素。
项目文件在 Visual Studio 中的“解决方案资源管理器”中不可见。您可以通过右键单击 Solution Explorer 中的项目并选择 Edit Project File(编辑项目文件)来访问它。如果您使用的是 VS Code,则该文件在文件资源管理器中可见,您可以像访问任何其他文件一样访问和编辑它。
2.2.3 bin 和 obj 文件夹
bin 和 obj 文件夹在构建过程中使用。这两个文件夹又细分为两个文件夹(Debug 和 Release),它们对应于构建项目时使用的构建配置。最初,bin 和 obj 文件夹仅包含 Debug 文件夹。只有在 Release 模式下构建后,才会创建 Release 文件夹。除非您在上一节中按 Ctrl-F5 时更改了任何配置设置,否则您的应用程序目前仅在 Debug 模式下构建。
obj 文件夹包含构建过程中使用的工件,bin 文件夹包含构建的最终输出。在第 14 章中发布应用程序时,您将更详细地了解此输出。如果删除 bin 或 obj 文件夹,则会在下次生成项目时重新创建它们。
默认情况下,这两个文件夹在解决方案资源管理器中都不可见。但是,如果单击“显示所有文件”选项,则可以看到它们以虚线轮廓表示。此指示符表示文件夹不被视为项目本身的一部分。同样,它们并没有对 VS Code 用户隐藏。
2.2.4 Properties 文件夹
Properties 文件夹包含特定于项目的资源和设置。当前文件夹中的唯一项目是 launchSettings.json 文件,其中包含运行应用程序时要使用的设置的详细信息。
第一组设置与用于在本地运行应用程序的 IIS Express Web 服务器配置相关。IIS Express 是完整 IIS Web 服务器的轻量级版本,与 Visual Studio 一起安装。
第二组设置表示不同的启动配置文件。IIS Express 配置文件指定应用程序应在 IIS Express 上运行。请注意,applicationUrl 包含一个端口号。为 SSL 端口提供了不同的端口号。这些是按项目生成的。如果您愿意,您可以自由更改端口号。
第二个配置文件使用项目名称来标识自身。如果选择此配置文件来启动应用程序,它将完全在其内部或进程内 Web 服务器上运行。默认服务器实现称为 Kestrel。您将在本章后面了解更多信息。最终配置文件 (WSL 2) 与在适用于 Linux 的 Windows 子系统中运行应用程序有关。本书不涉及 WSL,但如果您想了解更多信息,Microsoft 文档提供了一个很好的起点:https://docs.microsoft.com/en-us/windows/wsl/。
2.2.5 wwwroot 文件夹
wwwroot 文件夹是 Web 应用程序中的一个特殊文件夹。它在 Solution Explorer 中有一个地球图标。它是 Web 根目录,包含静态文件。由于是 Web 根目录,wwwroot 被配置为允许直接浏览其内容。它是样式表、JavaScript 文件、图像和其他内容的正确位置,这些内容在下载到浏览器之前不需要任何处理。因此,您不应将任何不希望用户能够访问的文件放在 wwwroot 文件夹中。可以将备用位置配置为 Web 根目录,但新位置不会在“解决方案资源管理器”中获得特殊图标。
项目基架在 wwwroot 文件夹中创建了三个文件夹:css、js 和 lib。css 文件夹包含一个 site.css 文件,其中包含模板站点的一些基本样式声明。js 文件夹包含一个名为 site.js 的文件,除了一些注释外,它什么都没有。一般的想法是,您将自己的 JavaScript 文件放在此文件夹中。lib 文件夹包含外部样式和脚本库。模板提供的库是 Bootstrap,一种流行的 CSS 框架;jQuery,一个跨浏览器的 JavaScript 实用程序库;以及两个基于 jQuery 的验证库。它们用于验证表单提交。
wwwroot 中的文件夹结构不是一成不变的。你可以随心所欲地移动东西。
2.2.6 Pages 文件夹
按照约定,Pages 文件夹配置为 Razor 页面文件的主页。这是框架希望找到 Razor 页面的位置。
项目模板从三个页面开始。您已经看到了其中两个 - 索引(或主页)和隐私页面。当然,您的示例包括您创建的 Welcome 页面。项目模板提供的第三个页面是 Error。查看磁盘上的实际文件夹,您会注意到每个页面都包含两个文件:一个扩展名为 .cshtml 的文件(Razor 文件),另一个以 .cshtml.cs 结尾的文件(C# 代码文件)。当您查看 Solution Explorer 时,这可能不是立即显而易见的。默认情况下,文件是嵌套的(图 2.10)。您可以通过在解决方案资源管理器顶部的工具栏中禁用文件嵌套或单击页面旁边的展开器图标来查看它们,这不仅会显示嵌套文件,还会显示一个显示 C# 类大纲(包括属性、字段和方法)的树。
图 2.10 解决方案资源管理器自动嵌套相关文件。您可以使用 menu 命令切换文件嵌套。
顶级文件 (.cshtml 文件) 是 Razor 页面文件。它也称为内容页面文件或视图文件。为了保持一致性,我将其称为 Razor 页面(单数,带有小写 p 以区别于 Razor Pages 框架)。如上一节所示,此文件充当视图模板,包含 Razor 语法,该语法是 C# 和 HTML 的混合体,因此,文件扩展名是 cs 和 html。第二个文件是一个 C# 代码文件,其中包含一个派生自 PageModel 的类。此类充当 Razor 页面的组合控制器和视图模型。您将在下一章中详细介绍这些文件。
Pages 文件夹中还有两个文件 — 一个名为 _ViewStart.cshtml,另一个名为 _ViewImports.cshtml。以前导下划线命名的 Razor 文件不应直接呈现。这两个文件在应用程序中起着重要作用,不应重命名它们。这些文件的用途将在下一章中解释。
Pages 文件夹还包含一个 Shared 文件夹。其中还有另外两个 Razor 文件,名称中都有前导下划线。_Layout.cshtml 文件充当其他文件的主模板,其中包含常见内容,包括您在上一节中更改的导航。另一个 Razor 文件 _ValidationScriptsPartial .cshtml) 是部分文件。部分文件通常用于包含可插入页面或布局的 UI 代码片段。它们支持 HTML 和 Razor 语法。此特定部分文件包含对客户端验证库的一些脚本引用。您将在第 5 章中介绍这些内容。最后一个文件是一个 CSS 样式表,它有一个奇怪的名字:_Layout .cshtml.css。它包含应用于 _Layout.cshtml 文件的样式声明。命名约定由 .NET 6 中的一项新功能使用,称为 CSS 隔离。您将在第 11 章中了解这是什么以及它是如何工作的。
2.2.7 应用设置文件
应用程序设置文件用作存储应用程序范围的配置设置信息的地方。项目模板由两个应用程序设置文件组成:appSettings.json 和 appSettings.Development.json。第一个 appSettings.json 是将与已发布应用程序一起部署的生产版本。另一个版本是开发应用程序时使用的版本。文件内容的结构为 JSON。
这两个版本都包含用于日志记录的基本配置。开发版本还包含一个名为 DetailErrors 的配置条目,该条目设置为 true。这样就可以将应用程序中发生的任何错误的完整详细信息呈现到浏览器。主机筛选是在生产版本中配置的。您几乎可以在 app-settings 文件中存储任何应用程序配置信息。稍后,您将使用它们来存储数据库连接字符串和电子邮件设置。
应用程序设置文件并不是您可以存储配置信息的唯一位置。许多其他位置(包括环境变量)都是开箱即用的,您可以配置自己的位置。您将在第 14 章中了解有关配置的更多信息。
2.2.8 Program.cs
熟悉 C# 编程的读者都知道,Program.cs 提供了控制台应用程序的入口点。按照约定,它包含一个静态 Main 方法,其中包含用于执行应用程序的逻辑。此文件没有什么不同,只是没有可见的 Main 方法。项目模板利用了一些较新的 C# 语言功能,这些功能在 C# 10 中引入,其中之一是顶级语句。此功能允许您省略 Program.cs 中的类声明和 Main 方法,并开始编写可执行代码。编译器将生成 class 和 Main 方法,并在该方法中调用您的可执行代码。
Program.cs 文件中的代码负责配置或引导 Web 应用程序并启动它。在 .NET 5 及更早版本中,此代码被拆分为两个单独的文件。大部分应用程序配置被委托给一个名为 Startup 的单独类。随着 .NET 6 的发布,ASP.NET 背后的开发人员试图降低过去存在于基本应用程序配置中的复杂性。他们没有将代码跨两个文件,而是将其合并到一个文件中,利用一些新的 C# 功能来进一步减少样板,然后引入了他们所说的最小托管 API,以获取启动和运行 Razor Pages 应用程序所需的最少代码,代码最少为 15 行。在以前的版本中,它接近 80 行代码,分布在两个文件中。
第一行代码创建一个 WebApplicationBuilder:
var builder = WebApplication.CreateBuilder(args);
请记住,此代码将在编译器生成的 Main 方法中执行,因此传递给 CreateBuilder 方法的 args 是由调用应用程序的任何进程传递到任何 C# 控制台应用程序的 Main 方法的标准 args。
WebApplicationBuilder 是 .NET 6 中的新增功能,与另一种新类型(WebApplication)一起构成了最小托管 API 的一部分,您稍后将介绍它。WebApplicationBuilder 具有多个属性,每个属性都支持对应用程序的各个方面进行配置:
• Environment - 提供有关应用程序运行的 Web 托管环境的信息
• Services — 表示应用程序的服务容器(请参阅 第 7 章)
• Configuration - 启用配置提供程序的组合(请参阅 14)
• Logging — 通过 ILoggingBuilder 启用日志记录配置
• Host — 支持配置特定于应用程序主机的服务,包括第三方 DI 容器
• WebHost — 启用 Web 服务器配置
应用程序主机负责引导应用程序、启动和关闭应用程序。术语 bootstrapping 是指应用程序本身的初始配置。此配置包括以下内容:
• 设置内容根路径,这是包含应用程序内容文件的目录的绝对路径
• 从传入 args 参数、app-settings 文件和环境变量的任何值加载配置信息
• 配置日志记录提供程序
所有 .NET 应用程序都以这种方式进行配置,无论它们是 Web 应用程序、服务还是控制台应用程序。最重要的是,为 Web 应用程序配置了 Web 服务器。Web 服务器通过 WebHost 属性进行配置,该属性表示 IWebHostBuilder 类型的实现。默认 Web 服务器是名为 Kestrel 的轻量级且速度极快的 Web 服务器。Kestrel 服务器已合并到您的应用程序中。IWebHostBuilder 还配置主机筛选以及与 Internet Information Services (IIS)(即 Windows Web 服务器)的集成。
IWebHostBuilder 对象公开了多个扩展方法,这些方法支持进一步配置应用程序。例如,前面我讨论了将 wwwroot 文件夹的替代路径配置为 Web 根路径。WebHost 属性使您能够在有充分理由的情况下执行此作。在下面的清单中,Content 文件夹被配置为 wwwroot 的替代品。
列表 2.6 配置静态文件位置
builder.WebHost.UseWebRoot("content");
Services 属性提供依赖项注入容器的入口点,该容器是应用程序服务的集中位置。您将在第 7 章中更详细地探讨依赖关系注入,但与此同时,只需知道容器负责管理应用程序服务的生命周期并根据需要为应用程序的任何部分提供实例就足够了。默认模板包括以下代码行,这些代码行使 Razor Pages 基础结构所依赖的基本服务可供应用程序使用:
builder.Services.AddRazorPages();
这些服务包括 Razor 视图引擎、模型绑定、请求验证、标记帮助程序、内存缓存和 ViewData。如果这些术语看起来不熟悉,请不要担心。在阅读本书时,您将更详细地了解它们。需要注意的重要一点是,Services 属性为您提供了一个位置,可以根据需要注册和配置其他服务。
有时,这些服务是你选择启用的框架的一部分(如 Razor Pages 示例),有时它们表示你作为单独包安装的服务。通常,它们将是您自己编写的包含应用程序逻辑的服务,例如获取和保存数据。
Build 方法将配置的应用程序作为 WebApplication 类型的实例返回。此类型表示其他三种类型的合并:
• IApplicationBuilder — 允许配置应用程序的请求或中间件管道
• IEndpointRouteBuilder - 启用将传入请求映射到特定页面的配置
• IHost - 提供启动和停止应用程序的方法
WebApplication 允许您注册中间件组件来构建和配置应用程序的请求管道。现在,让我们从高级角度看一下以下清单中的默认配置。您将在本书的后面详细了解 pipeline 中更有趣的部分。
列表 2.7 默认请求管道
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
每个中间件都通过 IApplicationBuilder 类型的扩展方法添加到管道中,该方法由 WebApplication 实现。IWebHost- Environment 可通过 Environment 属性访问,该属性包含有关当前环境的信息。您将在第 14 章中了解有关环境的更多信息,但目前,只需说此属性用于确定应用程序当前是否在 Development 模式下运行就足够了,如果是,则调用 UseException- Handler 方法,该方法添加中间件以捕获错误并在浏览器中显示其详细信息。否则,您在 Pages 文件夹中看到的错误页面将用于显示一条平淡无奇的消息,该消息向用户隐藏了有关错误细节的任何敏感信息,例如包含用户凭据的数据库连接字符串或有关服务器上文件路径的信息。添加 HTTP 严格传输安全标头的中间件也已注册 (app.UseHsts()),但前提是应用程序未在开发模式下运行。此标头告诉浏览器在访问网站时仅使用 HTTPS。我在第 13 章中更详细地介绍了这一点。
UseHttpsRedirection 方法添加了中间件,以确保任何 HTTP 请求都重定向到 HTTPS。然后,在此之后,注册静态文件中间件。默认情况下,ASP.NET Core 应用程序不支持提供静态文件,例如图像、样式表和脚本文件。您必须选择使用此功能,并且可以通过添加静态文件中间件来实现。此中间件将 wwwroot 文件夹配置为允许直接请求静态文件,并将其提供给客户端。
路由中间件负责根据请求中包含的信息选择应执行的端点。我在第 4 章中讨论了路由在 Razor Pages 中的工作原理。然后,注册授权中间件,它负责确定当前用户是否有权访问所请求的资源。授权在第 10 章中介绍。
最后,MapRazorPages 方法将中间件添加到最初将 Razor Pages 配置为终结点的管道。此后,此中间件还负责执行请求。
2.3 理解 middleware
哇。那是很多抽象的术语!端点、中间件、管道 ...但它们实际上意味着什么呢?他们代表什么?在下一节中,我们将更详细地探讨它们。
注意 ASP.NET Core 中间件是一个相当大的话题。我将只介绍可能在大多数 Razor Pages 应用程序中使用的区域。如果您想探索更高级的中间件概念,例如分支管道,我推荐 Andrew Lock 的 ASP.NET Core in Action, Second Edition(Manning,2021 年)。
首先,鉴于 Razor Pages 应用程序的目的是提供对 HTTP 请求的响应,因此查看和了解 HTTP 请求的性质以及它在 Razor Pages 应用程序中的表示方式是合适的。这将构成您了解管道和终端节点的基础。
2.3.1 HTTP 刷新器
超文本传输协议 (HTTP) 是万维网的基础。它是在客户端-服务器模型中的系统之间传输信息的协议。HTTP 事务可以看作由两个基本元素组成:请求和响应。请求是输入,响应是输出。客户端发起请求,服务器提供响应,如图 2.11 所示。
图 2.11 客户端(浏览器)发起 HTTP 请求,该请求被发送到服务器。服务器负责将请求路由到已配置的应用程序并返回 HTTP 响应。
HTTP 请求包含许多数据。请求消息的第一行 (起始行) 包括以下内容:
• HTTP 方法
• 资源的标识符
• 协议版本(例如 HTTP/1.1)
该方法由动词(例如 GET、POST、PUT、DELETE、TRACE 或 CONNECT)或名词(例如 HEAD 或 OPTIONS)表示。向网站请求最常用的方法是 GET 和 POST,其中 GET 主要用于从服务器请求数据,POST 主要用于将数据传输到服务器,尽管 POST 方法也可能导致数据被发送回客户端。这是本书中将介绍的仅有的两种方法。
该标识符由统一资源标识符 (URI) 表示。此特定数据通常也称为统一资源定位符 (URL),就好像它们表示同一事物一样。从技术上讲,它们有所不同。就本书而言,知道所有 URL 都是 URI,但并非所有 URI 都是 URL 就足够了。RFC3986 的 1.1.3 节详细解释了差异: https://www.ietf.org/rfc/rfc3986.txt.在示例中,我将使用的 URI 类型在所有情况下都是 URL。
该请求还包括一组标头 — 名称-值对,可用于向服务器提供可能影响其响应的其他信息。例如,If-Modified-Since 标头指定日期时间值。如果请求的资源在指定时间后未被修改,则服务器应返回 304 Not Modified 状态码;否则,它应该发送修改后的资源。其他标头可能会通知服务器响应的首选语言或请求者可以处理的内容类型。
该请求还可以包括 cookie,即浏览器存储的信息片段,这些信息片段可能特定于网站用户,也可能不特定于网站用户。Cookie 的最常见用途包括:在用户登录到网站后存储用户的身份验证状态,或存储令牌,用于唯一标识访客以进行 Analytics 跟踪。
请求还可以包括 body。通常,这适用于 POST 请求,其中正文包含提交给服务器的表单值。
服务器返回的响应的结构与此类似。它有一个状态行,该行指定正在使用的协议版本、HTTP 状态代码和一些用于描述结果的文本 - 正式名称为原因短语。状态行示例可能如下所示:
HTTP/1.1 200 OK
响应还可以包含标头,这些标头可以指定所发送数据的内容类型、大小以及用于对响应进行编码的方法(如果已编码),例如 gzip。响应通常包括一个包含已请求数据的正文。
2.3.2 HttpContext
HTTP 事务中的所有信息都需要可供 Razor Pages 应用程序使用。用于封装当前 HTTP 事务(请求和响应)的详细信息的对象是 HttpContext 类。处理请求的进程内 Web 服务器负责使用实际 HTTP 请求中的详细信息创建 HttpContext 的实例。它为您(开发人员)提供了通过正式 API 访问请求数据的权限,而不是强迫您自己解析 HTTP 请求以获取此信息。HttpContext 还封装了此特定请求的响应。Web 服务器创建 HttpContext 后,它就可供请求管道使用。HttpContext 以各种形式在整个应用程序中显示,因此您可以根据需要使用其属性。表 2.1 详细介绍了 HttpContext 的主要属性以及它们所代表的内容。
表 2.1 HttpContext 属性
Property | Description |
---|---|
Request | Represents the current HTTP request (see table 2.2) |
Response | Represents the current HTTP response (see table 2.3) |
Connection | Contains information about the underlying connection for the request, including the port number and the IP address information of the client |
Session | Provides a mechanism for storing data scoped to a user, while they browse the website |
User | Represents the current user (see chapters 9 and 10) |
Request 属性由 HttpRequest 类表示。表 2.2 详细介绍了此类的主要属性及其用途。
表 2.2 主要 HttpRequest 属性
The Response property is represented by the HttpResponse class. Table 2.3 details the main members of this class and their purpose.
Table 2.3 Primary HttpResponse members
Response 属性由 HttpResponse 类表示。表 2.3 详细说明了该类的主要成员及其用途。
表 2.3 主要 HttpResponse 成员
上表中详述的方法和属性在直接处理请求和响应时非常有用,例如,在创建自己的中间件时将执行此作。
2.3.3 应用程序请求管道
当 Web 服务器将请求路由到您的应用程序时,应用程序必须决定如何处理它。需要考虑许多因素。请求应定向或路由到何处?是否应记录请求的详细信息?应用程序是否应该只返回文件的内容?它应该压缩响应吗?如果在处理请求时遇到异常,会发生什么情况?发出请求的人是否真的被允许访问他们请求的资源?应如何处理 Cookie 或其他与请求相关的数据?
此决策过程称为请求管道。在 ASP.NET Core 应用程序中,请求管道由一系列软件组件组成,每个组件都有自己的单独责任。其中一些组件在请求进入应用程序的途中作用于请求,而其他组件则对应用程序返回的响应进行作。有些人可能会两者兼而有之。执行这些功能的各个组件称为中间件。
图 2.12 说明了这个概念,显示了一个来自 Web 服务器的请求,然后通过多个中间件组件的管道传递,然后到达标记为 Razor Pages 的实际应用程序本身。
图 2.12 请求进入顶部的管道,流经所有中间件,直到到达 Razor Pages,在那里进行处理并作为响应返回。
这就是对示例应用程序主页的请求的流动方式。每个中间件都会检查请求,并确定在将请求传递到管道中的下一个中间件之前是否需要执行任何作。请求到达 Razor Pages 并得到处理后,响应将流回服务器,因为管道继续沿相反方向进行。管道本身在 Web 服务器上开始和结束。在图 2.13 中,静态文件中间件做出决策,并将控制权传递给下一个中间件,或者使进程短路并返回响应。
图 2.13 中间件处理请求,并在请求针对已知文件时返回响应。
静态文件中间件会检查到达它的每个请求,以确定该请求是否针对已知文件,即驻留在 wwwroot 文件夹中的文件。如果是这样,静态文件中间件只会返回文件,从而使管道的其余部分短路。否则,请求将传递到管道中的下一个中间件。
2.3.4 创建 middleware
现在,您已经更好地了解了中间件所扮演的角色,您应该了解它是如何实现的,以便您可以为请求管道提供自己的自定义功能。本节将介绍如何创建您自己的中间件组件并将其注册到管道中。
中间件组件作为 RequestDelegate实现,即,将 HttpContext 作为参数并返回 Task 的 .NET 委托,或者换句话说,表示 HttpContext 上的异步作的方法:
public delegate Task RequestDelegate(HttpContext context)
代表 101:快速复习
.NET 中的委托是表示方法签名和返回类型的类型。下面的示例声明一个名为 MyDelegate 的委托,该委托将 DateTime 作为参数并返回一个整数:
delegate int MyDelegate(DateTime dt);
任何具有相同签名和返回类型的方法都可以分配给 MyDelegate 的实例并调用,包括下面显示的两个方法。
根据匹配的签名和返回类型为委托分配方法
int GetMonth(DateTime dt) ❶
{
return dt.Month;
}
int PointlessAddition(DateTime dt) ❶
{
return dt.Year + dt.Month + dt.Day;
}
MyDelegate example1 = GetMonth; ❷
MyDelegate example2 = PointlessAddition; ❷
Console.WriteLine(example1(DateTime.Now)); ❸
Console.WriteLine(example2(DateTime.Now)); ❸
❶ 两种方法都采用 DateTime 参数并返回一个整数。
❷ 将两种方法都分配给委托实例。
❸ 通过委托实例调用方法。
你可以将内联匿名方法分配给委托:
MyDelegate example3 = delegate(DateTime dt) {
return dt.Now.AddYears(-100).Year; };
Console.WriteLine(example3(DateTime.Now));
更常见的是,您将看到以 lambda 表达式形式编写的匿名内联方法,其中推断了方法参数的数据类型:
MyDelegate example4 = (dt) => { return dt.Now.AddYears(-100).Year; };
Console.WriteLine(example4(DateTime.Now));
因此,任何将 HttpContext 作为参数并返回任务的方法都可以用作中间件。
如前所述,中间件是通过 WebApplication 添加到管道中的。通常,中间件创建为通过扩展方法注册的单独类,但也可以将 RequestDelegate直接添加到管道。清单 2.8 展示了一个简单的方法,该方法将 HttpContext 作为参数并返回一个 Task,这意味着它满足 RequestDelegate 类型规范。如果您想尝试此示例,可以将方法添加到 Program.cs。您还需要向 Startup 类添加 using 指令,以将 Microsoft.AspNetCore.Http 引入范围。
示例 2.8 RequestDelegate 将 HttpContext 作为参数并返回 Task
async Task TerminalMiddleware(HttpContext context)
{
await context.Response.WriteAsync("That’s all, folks!");
}
此特定中间件将消息写入响应。控制权不会传递给任何其他中间件组件,因此这种类型的中间件称为终端中间件。它会终止管道中的进一步处理。终端中间件通过 WebApplication 对象的 Run 方法注册:
app.Run(TerminalMiddleware);
RequestDelegate 是标准的 .NET 委托,因此也可以使用 lambda 表达式将其内联编写为匿名函数,而不是命名方法。
列表 2.9 使用 lambda 表达式内联指定主体的委托
app.Run(async context =>
await context.Response.WriteAsync("That’s all, folks!")
);
尝试使用任一方法通过放置应用程序来注册此中间件。在管道的开头运行 call — 在检查当前环境是否为 Development 的条件之前。
列表 2.10 将中间件添加到管道的开头
app.Run(async context =>
await context.Response.WriteAsync("That’s all, folks!")
);
if (app.Environment.IsDevelopment())
{
...
然后运行应用程序。您应该看到如图 2.14 所示的输出。
图 2.14 中间件的输出
下一个清单说明了一个中间件,它有条件地将处理传递给管道中的下一个中间件。
列表 2.11 有条件地将控制权传递给下一个中间件的中间件
async Task PassThroughMiddleware(HttpContext context, Func<Task> next)
{
if (context.Request.Query.ContainsKey("stop"))
{
await context.Response.WriteAsync("Stop the world");
}
else
{
await next();
}
}
此示例将 HttpContext 作为参数,但它也采用返回 Task 的 Func,表示管道中的下一个中间件。如果请求包含名为 stop 的查询字符串参数,则中间件会将管道短路,并将 Stop the world! 写入响应。不会调用其他中间件。否则,它将调用传入的 Func<Task>
,将控制权传递给下一个中间件。将控制权传递给管道中下一个组件的中间件使用 Use 方法注册:
app.Use(PassThroughMiddleware);
同样,此中间件可以编写为内联 lambda。
清单 2.12 使用 Use 方法内联注册中间件
app.Use(async (context, next) =>
{
if (context.Request.Query.ContainsKey("stop"))
{
await context.Response.WriteAsync("Stop the world");
}
await next();
});
你可以通过将代码放在 await next() 之后,将代码添加到控制权传递给下一个中间件后运行。假设没有其他中间件使管道短路,则您放置在其中的任何 logic 都将在管道反转其方向返回 Web 服务器时执行。例如,您可能希望执行此作以包括 logging。
Listing 2.13 在调用其他中间件后执行函数
app.Use(async (context, next) =>
{
if (context.Request.Query.ContainsKey("stop"))
{
await context.Response.WriteAsync("Stop the world");
}
else
{
await next();
logger.LogInformation("The world keeps turning");
}
});
注册中间件时,位置很关键。如果要将此中间件放在管道的开头,它将针对每个请求执行并记录信息消息,除非找到指定的查询字符串项。假设你要在 static files middleware 之后注册此中间件。在这种情况下,它只会执行和记录对非静态文件资源的请求,因为静态文件中间件在返回静态文件时会使管道短路。
2.3.5 中间件类
到目前为止,您看到的所有示例中间件都已添加为内联 lambda。这种方法适用于你目前看到的简单中间件,但如果你的中间件涉及任何复杂程度,则很快就会达不到要求,可重用性和可测试性都会受到不利影响。此时,您可能会在中间件自己的类中编写中间件。
有两种方法可以实现中间件类。第一种选择是使用基于约定的方法,该方法从一开始就是 ASP.NET Core 的一部分。第二个选项涉及实现 IMiddleware 接口,该接口与 Razor Pages 同时引入 ASP.NET Core 2.0。
基于约定的方法
约定是必须应用于某些组件设计的规则,这些组件旨在与框架一起使用,以便它们按预期方式运行。可能必须以特定方式命名类,以便框架可以识别它的意图。例如,MVC 中的 controller 类就是这种情况,其名称必须包括 Controller 作为后缀。或者,可能适用一个约定,指定为特定用例设计的类必须包含以某种方式命名并带有预定义签名的方法。
必须应用于基于约定的中间件类的两个约定是:(1) 声明一个构造函数,该构造函数将 RequestDelegate 作为参数,表示管道中的下一个中间件,以及 (2) 一个名为 Invoke 或 InvokeAsync 的方法,该方法返回一个 Task 并至少具有一个参数,第一个参数是 HttpContext。
要尝试此作,请将名为 IpAddressMiddleware 的新类添加到应用程序中。为简单起见,以下示例直接添加到项目的根目录中。将代码替换为下一个列表,该列表说明了一个中间件类,该类实现这些约定并记录访客 IP 地址的值。
列表 2.14 基于约定的方法的中间件类
namespace WebApplication1
{
public class IpAddressMiddleware
{
private readonly RequestDelegate _next;
public IpAddressMiddleware(RequestDelegate next) => _next =
next; ❶
public async Task InvokeAsync(HttpContext context,
ILogger<IpAddressMiddleware> logger) ❷
{
var ipAddress = context.Connection.RemoteIpAddress;
logger.LogInformation($"Visitor is from {ipAddress}"); ❸
await _next(context); ❹
}
}
}
❶ 构造函数将 RequestDelegate 作为参数。
❷ InvokeAsync 方法返回一个任务,并将 HttpContext 作为第一个参数。任何其他服务都将注入到 Invoke/InvokeAsync 方法中。
❸ 在 InvokeAsync 方法中执行处理
❹ 将控制权传递给管道中的下一个中间件
接下来,将 using 指令添加到 Program.cs 文件的顶部,以将 WebApplication1 命名空间引入范围:
using WebApplication1;
var builder = WebApplication.CreateBuilder(args);
中间件类通过 WebApplication 上的 UseMiddleware 方法添加到管道中。此方法有两个版本。第一个选项将类型作为参数:
app.UseMiddleware(typeof(IpAddressMiddleware));
第二个版本采用一个泛型参数,表示中间件类。这个版本是你更有可能遇到的版本:
app.UseMiddleware<IpAddressMiddleware>();
或者,建议您在 IApplicationBuilder 上创建自己的扩展方法来注册中间件。以下示例(如下面的清单所示)放置在名为 Extensions 的类中,该类也已添加到项目的根目录中。
清单 2.15 使用扩展方法注册中间件
namespace WebApplication1
{
public static class Extensions
{
public static IApplicationBuilder UseIpAddressMiddleware(this IApplicationBuilder app)
{
return app.UseMiddleware<IpAddressMiddleware>();
}
}
}
然后,扩展方法的使用方式与注册框架中间件时遇到的所有其他扩展方法相同:
app.UseIpAddressMiddleware();
在这种情况下,您可能希望在 static files 中间件之后注册此中间件,这样它就不会为每个请求的文件记录同一访问者的 IP 地址。
遵循基于约定的方法的中间件在应用程序首次启动时创建为单一实例,这意味着在应用程序的生命周期内只创建一个实例。此实例将重复用于到达它的每个请求。
实现中间件
编写新中间件类的推荐方法涉及实现 IMiddleware 接口,该接口公开一种方法:
Task InvokeAsync(HttpContext context, RequestDelegate next)
下一个清单显示了您使用基于约定的方法创建的相同 IpAddressMiddleware,并进行了重构以实现 IMiddleware。
列表 2.16 重构 IpAddressMiddleware 以实现 IMiddleware
public class IpAddressMiddleware : IMiddleware ❶
{
private ILogger<IpAddressMiddleware> _logger;
public IpAddressMiddleware(ILogger<IpAddressMiddleware> logger)
=> _logger = logger; ❷
public async Task InvokeAsync(HttpContext context, RequestDelegate next)❸
{
var ipAddress = context.Connection.RemoteIpAddress;
_logger.LogInformation($"Visitor is from {ipAddress}");
await next(context);
}
}
❶ 中间件类实现 IMiddleware 接口。
❷ 依赖项被注入到构造函数中。
❸ InvokeAsync 将 HttpContext 和 RequestDelegate 作为参数。
InvokeAsync 与使用基于约定的方法编写的 InvokeAsync 非常相似,不同之处在于这次的参数是 HttpContext 和 RequestDelegate。该类所依赖的任何服务都是通过中间件类的构造函数注入的,因此需要字段来保存注入的服务的实例。
此中间件的注册方式与基于约定的示例完全相同:通过 UseMiddleware 方法或扩展方法。但是,基于 IMiddle ware 的组件还需要执行一个额外的步骤:它们还必须注册到应用程序的服务容器中。在第 7 章中,您将了解有关服务和依赖关系注入的更多信息,但目前,只需知道您需要将下一个清单中的粗体代码行添加到 Program 类就足够了。
清单 2.17 将 IMiddleware 注册为服务
builder.Services.AddRazorPages();
builder.Services.AddScoped<IpAddressMiddleware>();
那么,为什么有两种不同的方法可以创建中间件类,您应该使用哪一种呢?嗯,基于约定的方法要求您学习特定的约定并记住它们。没有编译时检查来确保你的 middleware 正确实现约定。这种方法称为弱类型。通常,当您第一次发现忘记将方法命名为 Invoke 或 InvokeAsync 或第一个参数应该是 HttpContext 时,它会崩溃。如果你和我一样,你经常会发现你得回头查阅文档,以提醒自己约定的细节。
第二种方法会产生强类型中间件,因为您必须实现 IMiddleware 接口的成员;否则,编译器会抱怨,您的应用程序甚至不会构建。因此,IMiddleware 方法不太容易出错,并且实现起来可能更快,尽管您必须采取额外的步骤来向服务容器注册中间件。
这两种方法之间还有另一个区别。我之前提到过,在首次构建管道时,基于约定的中间件被实例化为单例。IMiddleware 组件由实现 IMiddlewareFactory 接口的组件针对每个请求进行实例化,并且这种差异会根据中间件的生存期对中间件所依赖的服务产生影响。我在第 7 章中更详细地解释了服务生命周期。现在,请理解 lifetime 不是 singleton 的服务不应该被注入到 singleton 的构造函数中。这意味着非单例服务不应该被注入到基于约定的中间件的构造函数中。但是,它们可以注入到 IMiddleware 组件的构造函数中。请注意,可以将非单例服务安全地注入到基于约定的中间件的 Invoke/InvokeAsync 方法中。
需要注意的是,大多数框架中间件都是使用基于约定的方法编写的。这主要是因为它大部分是在引入 IMiddleware 之前编写的。虽然没有迹象表明框架设计人员认为有必要将现有组件迁移到 IMiddleware,但他们建议您将 IMiddleware 用于您自己创建的任何中间件。
我们已经详细研究了如何使用中间件来构建请求管道,但尚未真正详细地介绍已添加到默认项目模板中的中间件。这将在接下来的章节中更深入地介绍。具体来说,我们将在第 4 章中介绍路由和端点中间件如何组合,在第 10 章中介绍授权的工作原理,在第 12 章中介绍如何管理自定义错误页面。
总结
Razor Pages 应用程序的起点基于模板。
Razor Pages 应用程序创建为项目。
解决方案是用于管理项目的容器。
Razor 语法可用于向页面添加动态内容。
Razor 语法支持将 C# 代码嵌入到 HTML 中。
Razor 运行时编译通过刷新浏览器使对 Razor 文件的更改可见。
布局页面充当整个网站的主模板。
Razor Pages 应用程序是以 Main 方法作为入口点的控制台应用程序。Main 方法作为 C# 10 中顶级语句功能的一部分隐藏在视图中。
WebApplicationBuilder 用于配置应用程序的服务和请求管道。
请求管道确定应用程序的行为。
请求管道由中间件组件组成。
中间件作为 RequestDelegate 实现,RequestDelegate 是一个将 HttpContext 作为参数并返回 Task 的函数。
中间件通过 WebApplication 对象添加到管道中。中间件可以终止管道或将控制权传递给下一个中间件。
Middleware 将按照其注册顺序进行调用。
可以使用内联 lambda 表达式添加简单的中间件。
复杂中间件可以创建为单独的类,并使用 IApplicationBuilder 类型的扩展方法进行注册。
中间件类应使用约定或实现 IMiddleware 接口。
基于约定的中间件实例化为单一实例,并且应该通过 Invoke/InvokeAsync 方法获取依赖项。
IMiddleware 按请求实例化,并且可以通过其构造函数获取依赖项。