ASP.NET Core Razor Pages in Action 4 使用路由将 URL 匹配到 Razor 页面

ASP.NET Core Razor Pages in Action 4 使用路由将 URL 匹配到 Razor 页面

本章涵盖

• 评估 URL 与 Razor 页面的匹配方式
• 检查使用路由模板来控制匹配过程
• 使用在 URL 中传递的数据
• 覆盖传统路由
• 生成出站 URL

在上一章中,我们研究了如何通过将 HTTP 方法名称合并到处理程序方法的名称中来影响为特定请求调用的处理程序方法,例如 OnGet、OnPost 等。在处理程序选择过程开始之前,必须选择正确的页面。本章重点介绍路由,路由是将传入请求映射到特定页面或终端节点(传入 URL)并生成映射到这些终端节点的 URL(传出 URL)的过程。

许多 Web 应用程序框架根据 URL 段与文件夹结构的匹配,将 URL 映射到网页在磁盘上的文件路径。延续上一章中提供度假套餐的 Web 应用程序的主题,表 4.1 提供了一些示例,说明一些想象中的分段 URL 与其在 Web 应用程序中的文件路径之间的一对一匹配。

表 4.1 在传入 URL 和页面文件路径之间找到一对一的映射是很常见的。

Incoming URL

Maps to

https://domain.com/city/london

c:\website\city\london.cshtml

https://domain.com/booking/checkout

c:\website\booking\checkout.cshtml

https://domain.com/login

c:\website\login.cshtml

本章将探讨 Razor Pages 如何使用基于页面的路由(URL 和文件路径之间的映射)作为应用程序内路由的基础。它还将探讨当 URL 与磁盘上位置之间的直接关系不足以满足您的需求时,如何使用配置来自定义 URL 映射到端点的方式。

URL 和文件路径之间的一一对应关系很容易推理,但其本身就非常有限。表 4.1 中的第一个示例暗示了一个页面,该页面提供有关伦敦作为度假胜地的信息。如果还想提供有关 Paris (https://domain.com/city/paris) 的信息,则必须将另一个页面添加到名为 paris.cshtml 的 city 文件夹。正如前几章所讨论的,这不是构建应用程序的可持续方式。在上一章中,我们研究了如何将 URL 中的数据作为查询字符串值传递。本章将探讨将数据作为 URL 本身的一部分传递,以及如何让单个页面根据该数据做出不同的响应。

URL 被视为 Web 应用程序 UI 的一部分。因此,您需要能够对它们进行与在应用程序中生成 HTML 一样多的控制。路由系统控制框架如何生成指向端点的 URL,但也有可用的自定义选项。您将了解如何使用它们来确保您的 URL 具有描述性和人类可读性。

在上一章中,我们查看了一些使用 Razor 语法迭代城市列表的示例。为了演示本章和后续章节中的概念,我们将继续此方案,并开始构建一个在全球城市提供度假套餐的应用程序(在英国,我们称之为 City Breaks)。如果您想尝试本章和以下章节中的代码示例,您应该根据第 2 章中的说明创建一个新的 Razor Pages 应用程序,并将其命名为 CityBreaks。

4.1 路由基础知识

路由不是 Razor Pages 框架的一部分。它作为单独的组件提供,因此作为中间件插入到您的应用程序中。在 Razor Pages 应用程序中控制路由的两个关键组件是 EndpointRouting 中间件和 Endpoint 中间件,如图 4.1 所示。它们分别通过 UseRouting 和 MapRazorPages 方法添加到应用程序管道中。清单 4.1 显示了 Program 类中的相关代码行作为提醒。

图 4.1 路由中间件在静态文件中间件之后注册。Endpoint middleware 在管道的末尾注册。

列表 4.1 在 UseEndpoints 之前调用 UseRouting

app.UseStaticFiles();      ❶
app.UseRouting();          ❷
app.UseAuthorization();
app.MapRazorPages();       ❸

❶ 静态文件中间件在路由之前注册。
❷ EndpointRouting 中间件在 UseRouting 方法中注册。
❸ 终结点中间件在管道末尾注册,并根据 Razor Pages 约定注册终结点。

EndpointRouting 中间件的作用是将传入的 URL 与端点匹配。对于 Razor Pages 应用程序,终结点通常是 Razor 页面。如果进行了匹配,则有关匹配端点的信息将添加到 HttpContext 中,该信息将沿管道传递。图 4.2 说明了此过程。将端点添加到 HttpContext 后,可以通过 HttpContext 的 GetEndpoint 方法访问它。

图 4.2 传入 URL 的路径与路由集合匹配。与匹配的路由相关的端点被添加到 HttpContext 中。

Endpoint 中间件负责执行选定的端点。如果未找到匹配的终端节点,则框架在管道中注册的最后一个中间件将返回 404 Not Found HTTP 状态代码。

不依赖于路由的中间件(如静态文件中间件)应放置在调用 UseRouting 之前。任何需要了解所选终结点的中间件都放置在 UseRouting 和 UseEndpoints 之间。授权中间件需要了解所选的端点,例如,确定当前用户是否有权访问它。

4.1.1 路由模板

EndpointRouting 中间件尝试通过将 URL 的路径(域后面的部分)与路由模板进行比较来将 URL 与终端节点匹配。路由模板是路由模式的字符串表示形式。该调用在 Endpoint 中间件注册中创建到 MapRazorPages 的路由模板,该注册指示框架根据 Razor Pages 约定创建终结点。具体而言,将创建 RouteEndpoint 对象的集合。RouteEndpoint 是一种可用于 URL 匹配和生成的终端节点。

Razor 页面的路由模板是根据默认根目录(Pages 文件夹)中找到的 Razor 文件的文件路径和名称创建的。

注意 : 在启动阶段,可以通过 Razor-PagesOptions 对象为 Razor 页面配置备用根。以下示例将根目录更改为 Content 而不是 Pages:

builder.Services.AddRazorPages().AddRazorPagesOptions(options => {
    options.RootDirectory = "/Content";
});

或者,可以使用 WithRazorPagesRoot 扩展方法:

builder.Services.AddRazorPages().WithRazorPagesRoot("/Content");

要使 Razor 文件被视为可导航页面,它的顶部需要有一个 @page 指令,并且其名称中不能有前导下划线。表 4.2 显示了为默认项目模板页面生成的路由模板以及与生成的模板匹配的 URL。

表 4.2 路由模板由文件名和位置构成

Page

Route template

Matches

\Pages\Index.cshtml

"Index"

https://domain.com/index

\Pages\Index.cshtml

""

https://domain.com/

\Pages\Privacy.cshtml

"Privacy"

https://domain.com/privacy

\Pages\Error.cshtml

"Error"

https://domain.com/error

Index.cshtml 文件是一种特殊情况。为此生成了两个路由模板 — 一个包含文件名,另一个由空字符串而不是 Index 组成。发生这种情况是因为 Index 被视为默认文档。因此,只需匹配 URL 中的文件夹名称即可访问它。如果您不小心,这可能会导致路由不明确导致问题。例如,如果添加名为 Privacy 的文件夹,其中包含 Index 文件,则会生成表 4.3 中的路由模板。

表 4.3 可能会生成不明确的路由。

Page

Route template

\Pages\Privacy\Index.cshtml

"Privacy/Index"

\Pages\Privacy\Index.cshtml

"Privacy"

\Pages\Privacy.cshtml

"Privacy"

表 4.3 中的最后两个路由模板相同。终端节点选择过程无法区分哪个终端节点(页面)应该匹配,因此当您尝试导航到 http://domain.com/privacy 时,框架会引发 AmbiguousMatchException。有时这种类型的文件配置是不可避免的,因此您需要知道如何解决歧义路由,这将在本章中介绍。

4.2 自定义路由模板

到目前为止,您看到的路由模板模式由文本组成。如果要成功进行终端节点匹配,它们需要 URL 和路由模板中的字符完全匹配。如果路由系统仅限于生成与现有文件路径完全匹配的模板,那将毫无意义。正如您对任何实际 Web 应用程序框架所期望的那样,Razor Pages 提供了足够的范围来自定义路由模板生成过程。

路由模板自定义的主要入口点是 Razor 页面本身中的 @page 指令。在 Razor 页面文件的第一行 @page 指令之后,唯一允许的其他内容是用于自定义页面的路由模板的字符串:

@page "route-template"

除了文字文本,路由模板还可以包含另外两种类型的元素:parameters 和 separators,如图 4.3 所示。参数是动态值的占位符,就像 C# 方法的参数一样,分隔符表示 URL 中段之间的边界。文本和参数之间的区别在于后者括在大括号中。参数是 URL 到终端节点匹配方面非常强大的工具。您很快就会学到更多关于在路由中使用参数的信息,但在此之前,我们将了解如何使用文字文本来覆盖页面的默认路由模板。

图 4.3 演示了路由模板中的文本、分隔符和大括号内参数值的占位符

4.2.1 覆盖路由

假设您发现自己处于表 4.3 中所示的那种 pickle 中,其中两个页面生成相同的路由模板。在这种情况下,您可能希望覆盖其中一个 S S 生成的模板,以防止在运行时引发异常。为此,可以向以分隔符 (/) 开头的 @page 指令提供替代文本值。例如,您可以在 \Pages\Privacy.cshtml 文件中的 @page 指令后添加“/privacy-policy”,如下所示:

@page "/privacy-policy"

这将替换默认路由模板。表4.4显示了由于此更改而生成的路由。

表 4.4 在路由模板中使用文本覆盖默认路由生成

Page

Route template

\Pages\Privacy\Index.cshtml

"Privacy\Index"

\Pages\Privacy\Index.cshtml

"Privacy"

\Pages\Privacy.cshtml

"privacy-policy"

您可以将路由模板的行为方式与 URL 相同,因为那些不以路径分隔符开头的模板是相对于当前页面的,而那些以路径分隔符开头的模板是绝对的,不会附加到当前页面的路由中。

4.2.2 路由参数

我们了解了 URL https://domain.com/city/london 如何映射到名为 City 的文件夹中名为 london.cshtml 的单个文件。london 可以表示数据,而不是文件名。在 Razor Pages 中,在 URL 中传递的数据称为路由数据,在路由模板中由路由参数表示。城市不再是一个文件夹;它是一个处理传入数据的页面。通过在大括号 { } 中指定参数的名称,将参数添加到路由模板中。名称本身或多或少可以是任何内容,但以下保留字除外:

• Action
• Area
• Controller
• Handler
• Page

如果要继续作,请将新的 Razor 页面添加到名为 City 的 Pages 文件夹。以下代码行显示了正在添加到 City 页面的路由模板中的名为 cityName 的参数:

@page "{cityName}"

为页面创建的生成路由模板将为

City/{cityName}

传递给 name 参数的任何值都将添加到 RouteValueDictionary 对象(实际上是路由值的字典)中,该对象将添加到 RouteData 对象中。然后,在页面中,可以使用 RouteData.Values 检索该值。清单 4.2 显示了如何定义 product route 参数,以及如何在 City 页面的代码中检索参数的值。

清单 4.2 向页面添加 route 参数并检索其值以进行显示

@page "{cityName}"                                ❶
@model CityBreaks.Pages.CityModel
 <h2>@RouteData.Values["cityName"] Details</h2>    ❷

❶ 路由模板在大括号中添加一个名为 “name” 的参数。
❷ name 参数的值从 RouteData.Values 获取并显示在页面中。

现在,如果您导航到 /City,您注意到的第一件事是收到 404 Not Found 错误。

图 4.4 如果所需的路由参数没有提供值,则应用程序会返回 404 HTTP 错误码。

这样做的原因是,默认情况下,name 参数的值是必需的。URL /City 本身与任何现有路由模板都不匹配(图 4.4)。导航到 /City/London,然后检索并显示参数值(图 4.5)。

图 4.5 获取并显示参数值。

通过在参数名称后放置问号,可以将参数设置为可选参数,例如 {cityName?}。将参数设为可选后,您可以导航到该页面,而无需提供值。可选参数只能用作路由模板中的最后一个参数。所有其他参数必须是必需的,或者必须为其分配默认值。

将默认值分配给路由参数的方式与分配给普通 C# 方法参数的方式相同,尽管字符串值周围没有引号:

"{cityName=Paris}"

现在,如果您导航到 /City,则会显示默认值(图 4.6)。

图 4.6 路由参数可以分配默认值。

4.2.3 将路由数据绑定到处理程序参数

您将记得上一章中,如果 URL 中的查询字符串值名称匹配,则它们会自动绑定到 PageModel 处理程序方法中的参数。路由数据也是如此。如果路由参数名称匹配,则传入值将自动分配给处理程序方法参数。然后,您可以将参数值分配给 PageModel 属性,这是使用路由数据的推荐方法,而不是访问路由值字典。清单 4.3 显示了修改了 CityModel OnGet 方法,该方法采用一个名为 cityName 的参数,并将传入的值分配给公共属性,然后在 City Razor 页面中进行更改以使用它。

列表 4.3 将路由数据绑定到 PageModel 处理程序参数

[City.cshtml.cs]
public class CityModel : PageModel
{
    public string CityName { get; set; }    ❶
    public void OnGet(string cityName)      ❷
    {
        CityName = cityName;                ❸
    }
}

[City.cshtml]
@page "{cityName=Paris}"
@model CityBreaks.Pages.CityModel
@{
}
<h3>@Model.CityName Details</h3>            ❹

❶ 将公共属性添加到 PageModel。
❷ OnGet 中添加了一个参数,该参数的名称与 route 参数相同,数据类型相同。
❸ 参数值分配给 PageModel 属性。
❹ 在页面中引用 PageModel 属性,而不是 RouteData.Values。

您可以在路由模板中指定多个参数。通常,每个参数都会在 URL 中占据自己的段。清单 4.4 显示了 City 页面及其 PageModel 的更新,以处理新 URL 段中的附加可选参数,该参数可能使用户能够指定指定城市内潜在场所的最低可接受评级。

列表 4.4 单独 URL 段中的多个参数

[City.cshtml.cs]
public class CityModel : PageModel
{
    public string CityName { get; set; }
    public int? Rating { get; set; }
    public void OnGet(string cityName, int? rating)
    {
        CityName = cityName;
        Rating = rating;
    }
}

[City.cshtml]
@page "{cityName}/{rating?}"
@model CityBreaks.Pages.CityModel
@{
}
<h3>@Model.CityName Details</h3>
<p>Minimum Rating: @Model.Rating.GetValueOrDefault()</p>

还允许在同一区段中使用多个参数。每个参数必须由参数值中未包含的文本分隔。假设您的 City 页面接受表示所需到达日期的值,而不是传入的评级值。由于连字符未出现在日期部分中,因此可以按如下方式构建路由模板:

 "{cityName}/{arrivalYear}-{arrivalMonth}-{arrivalDay}"

例如,这将匹配 /City/London/2022-4-18,但不匹配 /City/London/2022/ 4/18。

4.2.4 Catchall 参数

到目前为止,您看到的路由参数用于匹配 URL 中的特定段。有时,您可能不知道 URL 将包含多少个区段。例如,您可以构建一个内容管理系统,允许用户构建自己的任意 URL,这些 URL 不会映射到文件路径。它们会映射到数据库条目。您将需要一个匹配任何内容的路由模板,以便所选的终端节点可以负责在数据库中查找 URL 并显示相关内容。路由系统提供了一个 catchall 参数来满足这种情况。

catchall 参数的声明方式是在名称前加上一个或两个星号,例如 {*cityName} 或 {**cityName}。Catchall 参数会匹配从 URL 中的参数位置到末尾的所有内容,因此在 catchall 参数之后包含其他参数是没有意义的。无论您使用一个星号还是两个星号,匹配过程都是相同的。当您使用路由模板生成 URL 时,使用一个或两个星号之间的区别很明显(您将在本章后面看到)。如果使用一个星号,则生成的 URL 中的路径分隔符将进行 URL 编码,即,它们将呈现为 %2F。例如,URL /City/London/2022/4/18 将呈现为 /City%2FLondon%2F2022%2F4%2F18。当您使用两个星号时,编码将被解码或往返,并且生成的 URL 将包含文本路径分隔符:/City/London/2022/4/18。

4.2.5 路由约束

正如您现在应该已经了解的那样,路由参数类似于您在大多数编程语言中遇到的方法参数。它们是在运行时提供给应用程序各部分的变量数据的占位符。因此,您不可避免地希望能够更频繁地对这些 Importing 执行某种处理。您可能希望使用传递到到达日期示例的路由值构造一个 .NET DateTime 对象,以便您可以在 .NET 代码中使用它:

public DateTime Date { get; set; }
public void OnGet(string cityName, int arrivalYear, int arrivalMonth, int arrivalDay)
{
    Date = new DateTime(arrivalYear, arrivalMonth, arrivalDay);
}

出现潜在问题,因为没有对传入值执行任何类型的检查。就路由而言,所有 URL 数据都是字符串类型。因此,对 /City/London/foo-bar-baz 的请求将匹配路由,并导致在 .NET 尝试在 foo 年中构造有效日期时引发异常。Route constraints 为此问题提供了解决方案。它们使您能够指定路由数据项必须符合的数据类型和可接受值范围,才能将其视为与路由模板匹配。

路由约束是通过用冒号将它们与参数名称分隔来应用的。以下示例演示如何将每个日期部分限制为整数类型:

"{cityName}/{arrivalYear:int}-{arrivalMonth:int}-{arrivalDay:int}"

URL City/London/foo-bar-baz 不再匹配此模板,但 /City/ London/2022-4-18 匹配。但是,用户仍然可以调整 URL 并提供导致创建无效日期的值,例如 /City/London/2022-4004-18。为了防止这种情况,您可以使用 range 约束来限制有效值的范围。范围约束接受最小和最大可接受值,并且可以应用于 month 参数,如下所示:

"{cityName}/{arrivalYear:int}-{arrivalMonth:range(1-12)}-{arrivalDay:int}"

但是,更有可能的是,您会完全更改模板并将传入值限制为 datetime 类型:

"{cityName}/{arrivalDate:datetime}"

可以使用多种约束来限制对特定数据类型和范围的匹配。表 4.5 中列出了最常用的 constraints。https://github.com/dotnet/aspnetcore/tree/main/src/Http/Routing/src/Constraints 中提供了所有的约束。

表 4.5 路由模板中可用的约束

Constraint

Description

Example

alpha

Matches uppercase or lowercase Latin alphabet characters (a-z or A-Z)

{title:alpha}

bool

Matches a Boolean value

{isActive:bool}

int

Matches a 32-bit integer value

{id:int}

datetime

Matches a DateTime value

{startdate:datetime}

decimal

Matches a decimal value

{cost:decimal}

double

Matches a 64-bit floating-point value

{latitude:double}

float

Matches a 32-bit floating-point value

{x:float}

long

Matches a 64-bit integer value

{x:long}

guid

Matches a GUID value

{id:guid}

length

Matches a string with the specified length or within a specified range of lengths

{key:length(8)} {postcode:length(6,8)}

min

Matches an integer with a minimum value

{age:min(18)}

max

Matches an integer with a maximum value

{height:max(10)}

minlength

Matches a string with a minimum length

{title:minlength(2)}

maxlength

Matches a string with a maximum length

{postcode:maxlength(8)}

range

Matches an integer within a range of values

{month:range(1,12)}

regex

Matches a regular expression

{postcode:regex(^[A-Z]{2}\d\s?\d[A-Z]{2}$)}

在本章的前面部分,您了解了将 Index 文件添加到 Privacy 文件夹时如何创建不明确的路由。如果您添加 City 文件夹并在其中放置 Index 文件,则会出现相同的情况。上次,我为其中一个页面提供了一个全新的路由模板,以防止生成重复的路由模板。约束还可用于确保每个页面都被视为不同。“{cityName:alpha}”的模板要求参数仅由字母(a-z 或 A-Z)组成,而模板“{id:int}”将仅匹配数字。

可以将多个约束应用于一个参数。例如,您可能希望指定一个值必须仅由字母表的字母组成,并且长度不得超过 9 个字符。这是通过使用冒号分隔符附加其他约束来实现的:

"{cityName:alpha:maxlength(9)}"
自定义路由约束

如表 4.5 所示,您可以使用广泛的路由约束来满足您的大多数需求。还可以创建自己的自定义 route constraint 并将其注册到 routing 服务以满足应用程序特定的需要。

最终,您的 City 页面可能会采用传递给 cityName 参数的值,并使用该值对数据库执行查找,以获取指定位置的更多详细信息。这工作正常,但您很快就会开始意识到有时数据库查询不会返回结果。当您查看日志时,您会发现传递给数据库查询的值并不是您希望在 URL 中看到的值。它可能是城市名称的一部分,可能添加了一些额外的字符,或者实际上,它可能与数据库中的任何内容完全没有相似之处。当被编写不佳的机器人共享或存储时,指向您网站的链接可能会以无数种方式断开。或者精明的用户可能会破解您的 URL 并添加数据库中不存在的城市名称。

防止这些数据库查找不存在的值所产生的浪费处理,并通知请求者他们正在寻找的页面不存在,这可能是明智的。在本例中,您希望返回 404 Not Found 状态代码。

以下步骤向您展示如何创建自己的路由约束,以根据预先确定的城市集合检查 cityName 参数的值。如果路由数据项不在列表中,则匹配失败,并且应用程序将返回 404 Not Found 状态代码。此示例中的集合是一个简单的数组。在实际应用程序中,数据将驻留在数据库中,但您将对城市的缓存版本执行查找,以避免需要进行数据库调用。在第 14 章中,您将探索如何使用 ASP.NET Core 提供的缓存功能来查询存储在 Web 服务器内存中的集合。

路由约束是实现 IRouteConstraint 接口的类。因此,第一步是创建一个名为 CityRouteConstraint 的 C# 类。这应该放在名为 RouteConstraints 的文件夹中,这样您就不会有杂乱的代码文件弄乱项目的根文件夹。下面的清单中详细介绍了 CityRouteConstraint 类的代码。

清单 4.5 自定义 CityRouteConstraint 类

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;    
using System;
using System.Linq;

namespace CityBreaks.RouteContraints
{
    public class CityRouteConstraint : IRouteConstraint                    ❶
    {
        public bool Match(HttpContext httpContext, IRouter route,          ❷
         string routeKey, RouteValueDictionary values, RouteDirection    ❷
         routeDirection)                                                 ❷
        {
            var cities = new[] { "amsterdam", "barcelona", "berlin",       ❸
                                  ”copenhagen”, “dubrovnik”, “edinburgh”,❸
                                  "london", "madrid", "paris", "rome",   ❸
                                  "venice" };                            ❸
            return cities.Contains(values[routeKey]?.ToString()
.ToLowerInvariant());                                       ❹
        }
    }
}

❶ 约束必须实现 IRouteConstraint。
❷ IRouteConstraint 指定返回布尔值的 Match 方法。
❸ 有效值的数组
❹ 确定匹配是否有效的代码。

IRouteConstraint 接口有一个成员,即一个名为 Match 的方法,该方法返回一个布尔值。它需要许多项目作为参数,并非所有项目都是在每种情况下执行匹配所必需的。在此示例中,只需要 RouteValuesDictionary 和 routeKey。routeKey 值是需要检查的参数的名称。如果传入的参数值与 cities 数组中的项之间存在匹配项,则 Match 方法将返回 true。

注意IRouteConstraint 中的 Match 方法是同步方法,这使得 IRouteConstraint 不适用于应涉及异步处理的任何要求。如果你想异步地约束实际应用程序中的传入路由,支持此功能的替代机制包括 middleware (请参阅 第 2 章) 和 IEndPointSelectorPolicy (https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.routing.matching.iendpointselectorpolicy?view=aspnetcore-7.0),本书未对此进行介绍

必须注册自定义路由约束。这是在 Program 类中完成的。如下面的清单所示,通过传入约束的名称和应该使用的匹配类型,将约束添加到 ConstraintMap 集合中。

列表 4.6 使用 RouteOptions 注册的自定义路由约束

builder.Services.AddRazorPages();
builder.Services.Configure<RouteOptions>(options =>
{
    options.ConstraintMap.Add("city", typeof(CityRouteConstraint));   ❶
});

❶ 约束以名称 “city” 注册。

一旦 constraint 被注册,就可以像任何其他 constraint 一样使用它,只需将 constraint 的名称放在参数 name 之后即可:

@page "{cityName:city}"

4.2.6 创建其他路由

当您通过 @page 指令自定义路由模板时,会影响路由系统的结果模板。除了替换默认路由模板之外,您还可以指定页面可以匹配的其他模板。执行此作的最直接方法是在配置 RazorPagesOptions 时使用 AddPageRoute 方法(类似于为页面指定备用根文件夹),如清单 4.7 所示,其中 Index 页面应用了额外的路由,因此可以在 URL /FindMe 以及系统生成的路由中找到它。

列表 4.7 通过配置添加额外的路由模板

builder.Services
    .AddRazorPages()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions.AddPageRoute("/Index", "FindMe");
    });

使用此方法可以为特定页面定义的其他路由数量没有限制,该方法采用要映射的页面的名称和与该页面匹配的路由模板。此特定示例显示了文本值为“FindMe”的路由模板,但应用于 AddPageRoute 方法的模板可以包含参数和约束,就像您到目前为止一直在使用的模板一样。

4.2.7 直接使用 PageRouteModel 约定

在上一节中,您通过 AddPageRoute 方法为页面声明了其他路由模板。此方法提供了一种向应用程序添加新路由约定的便捷方法,尽管一次只能添加到一个页面。但是,如果要将新的路由约定应用于多个页面,甚至应用程序中的所有页面,该怎么办?AddPageRoute 方法的扩展性不是很好。在 ConfigureServices 方法中,您最终可能会有数百行代码,并且必须记住为每个新页面添加新的注册。

使用 AddPageRoute 方法时,框架会创建一个新的 PageRouteModelConvention,并将其添加到由 RazorPagesOptions Conventions 属性表示的 PageConventionCollection 中。该约定由 IPageRouteModelConvention 接口表示,您可以直接使用该接口将新约定应用于多个页面。为了说明其工作原理,假设您的新兴应用程序需要支持以多种语言显示内容。

注意为要以多种语言显示的内容提供支持的过程称为本地化,这是一个专门的主题,除了此示例之外,本书不会详细介绍。如果您想了解有关 Razor Pages 中本地化的更多信息,请在此处查看我的系列博客文章:https://www.mikesdotnetting.com/category/41/localization

要呈现内容的本地化(翻译)版本,应用程序需要知道用户更喜欢哪种区域性或语言。通常,这是通过要求用户从多个选项中进行选择,然后以某种方式跟踪该选择来确定的。存储所选内容并将其从一个页面传递到另一个页面的常用方法之一是在 URL 中作为数据。包含首选区域性的 URL 的典型示例可能是 https://domain.com/en/Welcome,其中 /en/ 区段指定英语作为区域性,或者 https://domain.com/fr/Welcome 表示法语版本。

如前所述,您将 URL 中的数据作为参数值传递,并且需要显式声明包含参数的路由模板。如果要支持将路由模板添加到多个页面,则可以创建可应用于任意数量的页面的 PageRoute- ModelConvention。

PageRouteModelConventions 实现 IPageRouteModelConvention 接口,该接口指定必须实现的单个方法,即 Apply 方法,该方法将 PageRouteModel 作为参数:

public interface IPageRouteModelConvention : IPageConvention
{
    void Apply(PageRouteModel model);
}

请注意,IPageRouteModelConvention 接口反过来又实现 IPageConvention 接口。这是一个标记接口。它没有方法,用作路由发现过程的一部分,用于将实现类表示为包含应应用于应用程序的路由模型约定的类。

PageRouteModel 参数提供了一个网关,用于应用新约定来为 Razor 页面生成路由。正是通过此对象,您可以应用自定义约定。PageRouteModel 具有 Selectors 属性,该属性表示 SelectorModel 对象的集合。每个选项都有一个 AttributeRouteModel 属性,而该属性又具有一个 Template 属性,该属性表示一个路由模板,该模板允许将 URL 映射到此特定页面。以下是此结构在应用程序中查找当前 Index 页的方式:

PageRouteModel
    RelativePath: "/Pages/Index.cshtml"
    Selectors: [Count = 3]
        SelectorModel[0]:
            AttributeRouteModel:
                Template: "Index"
        SelectorModel[1]:
            AttributeRouteModel:
                Template: ""
        SelectorModel[2]:
            AttributeRouteModel:
                Template: "FindMe"

这是一个大规模简化的表示形式,只关注那些直接感兴趣的属性。PageRouteModel 类和组成其属性的类比这复杂得多。请注意,在 Index 页的此表示形式中有三个 SelectorModel 对象。最终的 SelectorModel 包含 FindMe 路由模板,该模板是在上一节中通过 AddPageRoute 方法添加的。

在 Apply 方法中,您可以访问现有的 SelectorModel 对象并修改 Template 属性的值以更改现有模板,也可以将 SelectorModel 对象添加到 Selectors 集合以添加其他路由模板。下面的清单显示了一个 PageRouteModelConvention,它复制现有的路由模板,插入一个名为 culture 的可选路由参数作为第一个段,然后将该副本添加到应用程序中发现的每个页面的新 SelectorModel 中。

列表 4.8 创建 PageRouteModelConvention 以添加自定义路由约定

using Microsoft.AspNetCore.Mvc.ApplicationModels;                            ❶

namespace CityBreaks.PageRouteModelConventions
{
    public class CultureTemplatePageRouteModelConvention :
     IPageRouteModelConvention                                             ❷
    {
        public void Apply(PageRouteModel model)                              ❸
        {
            var selectorCount = model.Selectors.Count;

            for (var i = 0; i < selectorCount; i++)                          ❹
            {
                var selector = model.Selectors[i];

                model.Selectors.Add(new SelectorModel                        ❺
                {
                    AttributeRouteModel = new AttributeRouteModel
                    {
                        Order = 100,                                         ❻
                        Template =                                           ❻
                         AttributeRouteModel.CombineTemplates("{culture?}",❻
                         selector.AttributeRouteModel.Template)            ❻
                    }
                });
            }
        }
    }
}

❶ Microsoft.AspNetCore.Mvc.ApplicationModels 命名空间需要 using 指令。
❷ 您正在调用此类 CultureTemplatePage-RouteModelConvention 并实现 IPageRouteModelConvention 接口。
❸ 该类实现 Apply 方法。
❹ 迭代 PageRouteModel 的 SelectorModels 集合以获取有关其路由模板的信息。
❺ 通过将名为 culture 的新参数与现有选择器的副本相结合,将新的 SelectorModel 添加到集合中。
❻ 新模板的 order 设置为较高的数字,因此它不会干扰现有路线。

通过将 CultureTemplatePageRouteModelConvention 添加到 RazorPagesOptions Conventions 集合来注册该 CultureTemplatePageRouteModelConvention,其方式与之前调用 AddPageRoute 方法的方式类似:

builder.Services.AddRazorPages().AddRazorPagesOptions(options => {
    options.Conventions.AddPageRoute("/Index", "FindMe");
    options.Conventions.Add(new CultureTemplatePageRouteModelConvention());
});

注册约定后,将调用该约定,并在启动时为应用程序中的每个 Razor 页面执行其 Apply 方法。最终结果是应用程序中的路由模板总数增加了一倍。例如,索引页面的 SelectorModels 总数现在为 6 个,具有以下路由模板:

"Index"
""
"FindMe"
"{culture?}/Index"
"{culture?}"
"{culture?}/FindMe"

现在,您可以访问 URL 中包含或不包含区域性的主页。如果您想将区域性设为强制性的,则可以更新现有模板:

foreach (var selector in model.Selectors)
{
    selector.AttributeRouteModel.Template = AttributeRouteModel.CombineTemplates("{culture}",
                selector.AttributeRouteModel.Template);
}

这样,我们就完成了对如何将 URL 映射到各个 Razor 页面以及如何在必要时使用各种方式自定义过程的探索。下一节将介绍可用于根据应用程序的路由信息生成 URL 的工具。

4.3 生成 URL

任何对 HTML 有一点了解的人都知道,只需将锚元素添加到页面中,并将目标 URL 作为硬编码字符串应用于 href 属性,那么为什么本书专门用一节来讨论生成 URL 的主题呢?如果你要创建链接的 URL 位于你的网站外部,那么你将使用普通的锚元素和硬编码的 href 值。你也可以将硬编码的 URL 用于内部链接,但正如你刚刚阅读的那样,你可以非常轻松地更改 URL 映射到页面的方式。如果需要调整特定页面或页面组的查找方式,则需要在应用程序内的所有位置更新链接到这些页面的 URL。现在,这可能会带来维护方面的麻烦,而 Razor Pages 框架的开发人员不喜欢维护方面的麻烦,因此他们提供了此问题的解决方案。

Razor Pages 包含一些工具,这些工具可根据应用程序的路由配置生成 URL。因此,如果您更改路由,它们生成的链接将自动更新。主要工具是锚点标记帮助程序(我们之前已经简要介绍过)和 LinkGenerator 服务。这些是我们接下来要看的内容。

4.3.1 锚点标签助手

如上一章所述,标记帮助程序是自动生成 HTML 的组件。锚点标签帮助程序负责生成链接到应用程序内页面的锚点元素。href 属性的值是 anchor 元素的基本部分。锚点标记帮助程序负责根据应用程序的路由配置生成此消息。如果通过调整页面的路由模板或添加新约定来更改路由系统配置,则标记帮助程序的输出会自动相应地调整。您无需浏览 Razor 页面即可更新整个应用程序的链接。

提醒一下,以下是您在第 2 章中添加到导航的 Welcome 页面的锚点标签帮助程序:

<a class="nav-link text-dark" asp-area="" asp-page="/Welcome">Welcome</a>

这个特定的锚点标签帮助程序包括两个自定义属性,这两个属性都以 asp- 为前缀。请注意,没有 href 属性。不能在锚点标记帮助程序中包含 href 属性。如果尝试这样做,应用程序将在运行时引发 InvalidOperationException。Razor Pages 应用程序最感兴趣的 anchor 标记支持的自定义属性如下:

• page
• page-handler
• route-*
• all-route-data
• host
• protocol
• area

page、page-handler 和 route-* 属性是您最常使用的。page 属性是最重要的;它采用要生成链接的页面名称(没有扩展名的路径,根位于 Pages 文件夹中),前面有一个正斜杠(例如,/Welcome)。您必须传入有效的页面名称,如果页面位于子文件夹中,则包括路径分隔符。因此,如果要生成链接的页面位于 \Pages\Admin\Index.cshtml,则传递给 page 属性的值将为 /Admin/Index。如果路由系统找不到匹配的页面,它将生成一个带有空字符串的 href 属性。因此,如果您发现定位标记帮助程序的呈现输出意外地将您带到应用程序的主页,则应仔细检查分配给 page 属性的值。

某些页面具有多个路由模板。在这种情况下,最后一个模板将用于生成 href 值(参见图 4.7)。名为 Index.cshtml 的文件的默认约定是生成两个路由模板;第一个是 Index,最后一个是空字符串,如前所述。将 /Index 或 /Admin/Index 传递给 asp-page 属性时,生成的输出为 href=“/” 或 href=“/Admin/”。您之前通过 Startup 中的 AddPageRoute 方法为 Index 页面添加了第三个路由模板 — “FindMe”,该方法将生成 href 值。因此,<a asp-page=“/Index”>Home</a> 的呈现 HTML 将为 <a href=“/FindMe”>Home</a>

图 4.7 属于最后一个 SelectorModel 的模板用于生成 URL。

page-handler 属性用于指定应执行的命名处理程序方法的名称。其工作方式与上一章中讨论使用 Search 处理程序和 Register 处理程序的命名处理程序时看到的表单标记帮助程序的工作方式相同。默认情况下,传递给 page-handler 属性的值将应用于具有名为 “handler” 的键的查询字符串:

?handler=Search

您可以通过更改页面的路由模板以包含名为 “handler” 的参数来更改此行为,以便处理程序的名称成为 URL 的一部分。这通常作为可选参数添加,因此也可以访问常规的 OnGet 和 OnPost 处理程序:

@page "{handler?}"

route- 属性适用于路由参数值,其中 表示参数的名称。下面是一个标签帮助程序,它使用您在路由模板中指定的 cityName 参数生成指向罗马城市页面的链接:

<a asp-page="/City" asp-route-cityName="Rome">Rome</a>

如果参数不是路由模板的一部分,则传递给 route-* 属性的名称和值将应用于 URL 查询字符串。因此,如果将上一个示例中的 route 属性替换为 asp-route-foo=“bah”,则生成的 URL 将为 /City?foo=bah。

all-route-data 参数采用 Dictionary<string, string> 作为值,用于包装多个路由参数值。它是为了方便您,让您不必添加多个 route-* 属性:

var d = new Dictionary<string, string> { { "cityName", "Madrid" },{ "rating", 
 "5" } };
<a asp-page="/City" asp-all-route-data="d">Click</a>

锚点标签帮助程序的默认行为是根据目标页面的位置生成相对 URL。假设您选择使用 protocol 属性指定协议(例如 HTTPS)或使用 host 属性指定主机名(域)。在这种情况下,锚点标记帮助程序将使用指定的值生成绝对 URL。

列表中的最后一个属性是 area 属性,用于指定目标页面所在的区域。区域的名称将作为生成的 URL 的第一个段包含在内。

Razor Pages 中的区域

Razor Pages 中的区域功能很奇怪,因为了解它最重要的事情是您通常应该避免使用它。在本书中,我根本不会费心提及 areas,除了默认模板包含具有 area 属性的锚点标签帮助程序(尽管它们分配了一个空字符串),并且 areas 在 Identity 框架中使用,您将在第 9 章中介绍。
区域源自 MVC 框架。MVC 框架的一个问题是,将控制器类放在名为 Controllers 的文件夹中,将视图文件放在名为 Views 的文件夹中,这是由自动代码基架系统强制执行的约定。在大型应用程序中,此文件夹中可能会有大量的控制器类文件。区域功能旨在帮助将大型应用程序分解为单独的子应用程序。MVC 项目中的每个区域都有其自己的 Controllers、Views 和 Models 文件夹。通过将区域名称作为 URL 中的第一个段来访问区域的内容,因此该区域还实施一个层次结构以进行路由。您可以通过向 Pages 文件夹添加新的子文件夹,在 Razor Pages 中实现完全相同的分层路由效果,这通常是不建议在 Razor Pages 中使用区域的原因。它们大大增加了更容易解决的问题的复杂性。
在 Razor Pages 中启用区域的主要原因是它们促进了 Razor 类库,这是本书不会详细介绍的另一个功能。如果您有兴趣了解有关 Razor 类库的更多信息,可以参考官方文档 (http://mng.bz/o2zd)。Identity 框架包括一个示例 UI,该 UI 作为 Razor 类库实现。因此,虽然您不会添加任何自己的区域,但如果您想自定义 Identity UI,您很可能会发现自己必须使用区域。

4.3.2 使用 IUrlHelper 生成 URL

锚标签助手用于为内部链接生成 URL,但有时,您需要生成将用于其他目的的 URL。您可能需要生成一个 URL,该 URL 将包含在电子邮件中,例如,您经常需要单击以验证您在网站上的注册的 URL。或者,您可能需要在 Razor 页面上的 img 标记中生成 URL。IUrlHelper 服务可用于此目的。它通过 PageModel 类的 Url 属性和 Razor 页面本身在 Razor Pages 中提供给您。

Url 属性具有许多方法,其中两种方法在 Razor Pages 应用程序中特别有趣:Page 方法和 PageLink 方法。Page 方法提供了许多版本或重载,这些版本或重载从传递给它的页面名称以及页面处理程序方法和路由值生成相对 URL 作为字符串。其他重载可用于生成绝对 URL。PageLink 方法根据当前请求生成绝对 URL。下面的示例演示如何使用 Page 方法生成包含路由数据的相对 URL,该 URL 作为匿名类型传递到 Page 方法,其中属性名称与路由参数名称匹配。

清单 4.9 使用 IUrlHelper 生成相对 URL

public class IndexModel : PageModel
{
    public void OnGet()
    {
        var target = Url.Page("City", new { cityName = "Berlin", rating = 4 });
    }
}

根据清单 4.4 中的 “{cityName}/{rating?}” 模板,目标变量的值为 “/City/Berlin/4”。匿名类型中名称与参数名称不匹配的任何属性都将添加到 URL 查询字符串中。

下一个清单演示了如何使用 PageLink 方法生成绝对 URL。基于同一路由模板的输出为“https://localhost :5001/City/Berlin/4”。

清单 4.10 使用 IUrlHelper 生成绝对 URL

public class IndexModel : PageModel
{
    public void OnGet()
    {
        var target = Url.PageLink("City", values: new { cityName = "Berlin", 
         rating = 4 });
    }
}

4.3.3 从 ActionResults 生成重定向 URL

在上一章的结尾,我们研究了不同的 ActionResult 类型,这些类型可以用作处理程序方法的返回类型,以及用于创建 ActionResult 类实例的帮助程序方法。其中两个帮助程序方法生成作为响应的一部分包含在位置标头中的 URL:RedirectToPage 和 RedirectToPagePermanent。这两种方法都用于指示浏览器重定向到生成的 URL。该方法 RedirectToPage 还会生成 HTTP 302 状态代码,该代码指示位置更改是暂时的。相比之下,该方法 RedirectToPagePermanent 会生成 301 HTTP 状态代码,指示应将重定向视为表示永久更改。当您想要通知搜索引擎资源已移动到新 URL 时,通常会使用后一种方法。下面的清单显示了如何使用 RedirectToPage 方法生成将用户发送到特定城市页面的 RedirectToPageResult。

列表 4.11 使用 IUrlHelper 生成绝对 URL

public class IndexModel : PageModel
{
    public RedirectToPageResult OnGet()                                        ❶
    {
        return RedirectToPage("City", new { cityName = "Berlin", rating = 4 });❷
    }
}

❶ 最好尽可能具体地设置处理程序返回类型,在本例中为 RedirectToPageResult。
❷ 用户将被定向到 /City/Berlin/4。

如果要将用户重定向到某个区域内的页面,则必须将区域名称作为路由参数传递:

return RedirectToPage("/Account/Login", new { area = "Identity" });

4.3.4 自定义 URL 生成

在本章的开头,我指出 URL 应被视为 Web 应用程序 UI 的一部分。在此基础上,您需要能够尽可能多地控制他们的生成。理想情况下,您希望您的 URL 可读且令人难忘。用于生成 URL 的约定可能不适合您的目的。例如,通常将 Pascal 大小写(复合词中每个单词的首字母大写)用于 Razor 页面名称。默认情况下,页面路由使用与页面名称相同的大小写。如果您像我一样,喜欢在应用程序中使用小写 URL,则可以使用 RouteOptions 对象在 Program 类中对其进行配置。它与之前用于添加自定义约束的 RouteOptions 对象相同。它有一个名为 LowercaseUrls 的 Boolean 属性,当设置为 true 时,将导致以小写形式生成出站 URL 的 path 部分。

清单 4.12 配置出站 URL 路径以使用小写

builder.Services.Configure<RouteOptions>(options =>
{
    options.LowercaseUrls = true;                               ❶
    options.ConstraintMap.Add("city", typeof(CityConstraint));   
});

❶ 将小写设置为生成的 URL 的 path 部分的默认值。

使用 RouteOptions 对象,您可以通过其 LowercaseQueryStrings 属性(也是一个布尔值)对查询字符串应用相同的大小写首选项:

options.LowercaseQueryStrings = true;

今后,您在阅读本书时将构建的应用程序将把小写 URL 设置为 true。您不会对查询字符串值应用小写,因为这会破坏安全令牌,您将在第 9 章中作为用户管理的一部分使用。

有些人喜欢他们的 URL 以正斜杠 (/) 结尾。从技术上讲,这样做没有任何好处,但如果您选择实现此模式,则必须保持一致,因为搜索引擎将 City/London 和 City/London/ 视为两个不同的 URL。RouteOptions 对象包含名为 AppendTrailingSlash 的属性,该属性在设置为 true 时始终会导致附加斜杠:

options.AppendTrailingSlash = true;

4.3.5 使用参数变换器自定义路由和参数值生成

现在,您已以小写形式生成 URL,但您可能仍需要自定义 URL 生成的某些方面。假设你有一个名为 CityReview.cshtml 的页面。为此页面生成的 URL 将为 /cityreview,而您可能希望复合词 CityReview 中的每个单词都用连字符分隔:city-review。这被称为烤肉串案例(想想烤串)。您可以通过将页面命名为 City-Review.cshtml 来实现此目的,这将生成名为 City_ReviewModel 的 PageModel。如果您不喜欢 PageModel 类的名称中包含下划线,则可以更改该类的名称,但您可能还受到某些全局命名约束的约束,这些约束会阻止您首先在页面名称中包含连字符。另一种可能的解决方案是使用 AddPageRoute 方法应用其他路由模板,该模板将用于 URL 生成,但您需要记住,对于名称中可能包含复合词的所有其他页面,您都要这样做。因此,理想情况下,您需要一个影响应用程序中所有页面的全局解决方案。参数变压器提供全局解决方案。

参数转换器是实现 IOutboundParameterTransformer 接口的类,该接口指定一种方法:TransformOutbound。该方法将对象作为参数并返回字符串。尽管名称如此,但参数转换器可用于转换生成的页面路由以及参数值,具体取决于它在应用程序中的注册方式。转换逻辑放置在 TransformOutbound 方法中。

下面的清单显示了一个参数转换器,该参数转换器在 Pascal 大小写复合词中第二个和后续单词的第一个字母之前插入连字符,因此 CityReview 变为 City-Review。

Listing 4.13 作用于页面路由的参数转换器

using Microsoft.AspNetCore.Routing;                                        ❶
using System.Text.RegularExpressions;                                      ❷

public class KebabPageRouteParameterTransformer :                          ❸
 IOutboundParameterTransformer                                           ❸
{                                                                          ❸
    public string TransformOutbound(object value)                          ❸
    {
        if (value == null)                                                 ❹
        {
            return null;
        }
        return Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2"); ❺
    }
}

❶ IOutboundParameterTransformer 位于 Microsoft.AspNetCore.Routing 命名空间中。
❷ 你在方法体中使用了一个正则表达式,所以你需要为适当的命名空间添加一个 using 指令。
❸ 该类实现 IOutboundParameterTransformer 接口及其 TransformOutbound 方法。
❹ 需要进行 null 检查,以防传入的页面名称不正确。
❺ 一个简单的(如果有的话)正则表达式,用于标识字符串中大写字母跟在小写字母后面的位置,并在找到它们的位置插入连字符

此特定转换器旨在处理页面的路由,而不是参数值,因此必须将其注册为 PageRouteTransformerConvention。注册在 Program 类中进行,就像本章前面的 CultureTemplatePageRouteModelConvention 一样:

builder.Services.AddRazorPages().AddRazorPagesOptions(options => {
    options.Conventions.AddPageRoute("/Index", "FindMe");
    options.Conventions.Add(new CultureTemplatePageRouteModelConvention());
    options.Conventions.Add(new PageRouteTransformerConvention(
        new KebabPageRouteParameterTransformer())); 
});

PageRouteTransformerConvention 类型实现 IPageRouteModelConvention 接口。您之前已作为 CultureTemplatePageRouteModelConvention 的一部分介绍了该接口及其 Apply 方法。当应用程序启动时,您的新参数 transformer 将分配给 PageRouteTransformerConvention 的 Apply 方法中的 PageRouteModel 的 RouteParameterTransformer 属性。从那里,每当需要出站路由时,都会应用其逻辑。

变换路径参数值

我们了解如何管理 URL 生成的最后一部分侧重于自定义路由参数值。假设您有一个城市列表,并且您希望为每个城市生成链接作为 City 页面的 route 参数值。将您在前几章中学到的知识付诸实践,您可能会想出一些代码,这些代码类似于 PageModel 类中的以下清单。

清单 4.14 在 PageModel 中生成城市数组

public class IndexModel : PageModel
{ 
    public string[] Cities { get; set; }                           ❶
    public void OnGet()
    {
        Cities = new[] { "London", "Berlin", "Paris", "Rome" };    ❷
    }
}

❶ 字符串数组在 PageModel 上声明为公共属性。
❷ 它被分配了许多城市的值。

您将循环访问 Razor 页面中的数组,并使用 anchor 标记帮助程序呈现每个城市的链接。

清单 4.15 生成每个城市的链接列表

<ul>
@foreach (var city in Model.Cities)
{
    <li><a asp-page="/City" asp-route-cityName="@city">@city</a></li>
}
</ul>

请记住,你已将 LowercaseUrls 设置为 true,因此生成的 HTML 如图 4.8 所示。

图 4.8 anchor 标签从数据中呈现的链接

这正是您想要的,但是如果您将 New York 包含在集合中会发生什么?嗯,当呈现锚标签时,两个单词之间的空格被 URL 编码为 %20(图 4.9)。

图 4.9 默认情况下,URL 中的空格编码为 %20。

这不是一个好看的外观,特别是如果你的链接开始出现在搜索引擎结果中,其中有很多东西可以最好地被普通访问者描述为其中的 gobbledygook。你真的想使用与 routes 相同的 kebab 大小写来渲染这些 route 值,以使其更具可读性,这次将参数值中的空格替换为连字符。当 URL 包含博客文章标题或书名等内容时,您经常会看到此类内容,例如,这本书的 URL 为:www.manning.com/books/ asp-net-core-razor-pages-in-action。URL 的粗体部分通常称为 slug。

现在,您将使用另一个参数转换器转换参数值。此代码如下面的清单所示。

Listing 4.16 在路由参数值中将空格转换为连字符

public class SlugParameterTransformer :                ❶
 IOutboundParameterTransformer                       ❶
{                                                      ❶
    public string TransformOutbound(object value)      ❶
    {
        return value?.ToString().Replace(" ", "-");    ❷
    }
}

❶ 该类实现 IOutboundParameterTransformer 及其 TransformOutbound 方法。
❷ 处理逻辑只是用连字符替换空格。

这种特殊的实现非常幼稚。例如,它不考虑正在转换的值中的任何现有连字符或任何双空格,但作为示例就足够了。作用于路由参数值的参数转换器的注册过程与之前用于注册自定义路由约束的过程非常相似。参数 transformer 被添加到 RouteOptions 对象的 ConstraintMap 属性中,并且它被分配给目标路由参数,其方式与路由约束相同。下一个清单显示了如何将 SlugParameterTransformer 注册为 RouteOptions 的一部分。

Listing 4.17 像 constraint 一样注册一个参数转换器

builder.Services.Configure<RouteOptions>(options =>
{
    options.LowercaseUrls = true;
    options.ConstraintMap.Add("city", typeof(CityConstraint));
    options.ConstraintMap.Add("slug", typeof(SlugParameterTransformer));
});

注册参数 transformer 后,您可以将其应用于选定的路由参数,其方式与具有注册时指定的名称的路由约束相同:

@page "{name:slug}/{rating?}"

注意尽管您以类似于 route constraint 的方式注册和应用了参数 transformer,但参数 transformer 的作用与 constraint 不同。它在将 URL 映射到路由模板方面不起任何作用。

现在,当您重新运行应用程序时,指向 New York 页面的链接看起来要好得多(图 4.10)。

图 4.10 参数 transformer 适用于呼出路由。

我们对 Razor Pages 中的路由系统的探索到此结束。你已了解将 URL 映射到路由模板的默认行为,路由模板是从应用程序中的 Razor 页面位置生成的。您还学习了如何基于单个页面和全局自定义此行为,以及如何使用路由参数在 URL 中传递数据。

你已了解如何使用定位标记帮助程序生成链接的 URL,以及如何通过 Razor 页面和 PageModel 的 Url 属性使用 IUrlHelper 生成 URL,以便可能用于其他目的。最后,您研究了如何使用 RouteOptions 和参数转换器自定义生成的 URL。下一章重点介绍如何使用 Razor Pages 应用程序中的表单来捕获和处理数据。

总结

路由是将传入 URL 映射到终端节点并生成传出 URL 的过程。
路由是使用 UseRouting 和 UseEndpoints 方法作为中间件添加的。
路由模板表示要匹配的终端节点。它们由文本、分隔符和参数组成,这些文本、分隔符和参数表示 URL 中的数据。
路由是从磁盘上 Razor 页面的位置生成的。
您可以通过向 @page 指令提供新模板来自定义各个页面路由。
您可以使用路由约束来消除类似路由模式之间的歧义。
您可以使用 AddPageRoute 向页面添加其他路由。
或者,您可以创建自己的 PageRouteModelConvention 来自定义多个页面的路由。
定位点标记帮助程序根据路由系统生成传出 URL,IUrlHelper 也是如此。
RouteOptions 对象提供了一些自定义 URL 生成过程的方法。
您可以使用参数转换器来完全控制 URL 生成过程。

Leave a Reply

Your email address will not be published. Required fields are marked *