Loading

ASP.NET Core 实战-5.使用路由将 URL 映射到 Razor 页面

什么是路由?

路由是将传入请求映射到处理它的方法的过程。 您可以使用路由来控制您在应用程序中公开的 URL。 您还可以使用路由来启用强大的功能,例如将多个 URL 映射到同一个 Razor 页面并自动从请求的 URL 中提取数据。

ASP.NET Core 应用程序包含一个中间件管道,它定义了您的应用程序的行为。 中间件非常适合处理横切关注点,例如日志记录和错误处理,以及关注范围狭窄的请求,例如对图像和 CSS 文件的请求。

要处理更复杂的应用程序逻辑,您通常会在中间件管道的末端使用 EndpointMiddleware。该中间件可以通过调用称为 Razor 页面上的页面处理程序的方法来处理适当的请求 或 MVC 控制器上的操作方法,并使用结果生成响应。

如何在收到请求时选择要执行的 Razor 页面或操作方法。 是什么让请求“适合”给定的 Razor 页面处理程序? 将请求映射到处理程序的过程称为路由

定义 ASP.NET Core 中的路由是将传入的 HTTP 请求映射到特定处理程序的过程。 在 Razor Pages 中,处理程序是 Razor Page 中的页面处理程序方法。在 MVC 中,处理程序是控制器中的操作方法。

至此,您已经在前面的章节中看到了几个使用 Razor Pages 构建的简单应用程序,因此您已经看到了路由的实际应用,即使您当时没有意识到这一点。 即使是简单的 URL 路径(例如 /Index)也使用路由来确定应该执行 Index.cshtml Razor页面,如图 5.1 所示。

图 5.1 路由器将请求 URL 与配置的路由模板列表进行比较,以确定要执行的操作方法。
image

从表面上看,这似乎很简单。 你可能想知道为什么我需要一整章来解释这个明显的映射。 在这种情况下,映射的简单性掩盖了路由的强大功能。 如果这种基于文件布局的方法是唯一可用的方法,那么您可以构建的应用程序将受到严重限制。

例如,考虑一个销售多种产品的电子商务应用程序。每个产品都需要有自己的 URL,所以如果您使用的是纯粹的基于文件布局的路由系统,那么您只有两种选择:

  • 为您的产品系列中的每个产品使用不同的 Razor 页面。 对于几乎任何实际尺寸的产品系列来说,这都是完全不可行的。
  • 使用单个 Razor 页面并使用查询字符串来区分产品。 这更加实用,但您最终会得到一些难看的 URL,例如“/product?name=big-widget”或“/product?id=12”。

定义 查询字符串是 URL 的一部分,其中包含不适合路径的附加数据。 路由基础架构不使用它来识别要执行的操作,但它可以用于模型绑定。

使用路由,您可以拥有一个可以处理多个 URL 的 Razor 页面,而不必求助于丑陋的查询字符串。 从 Razor 页面的角度来看,查询字符串和路由方法非常相似——Razor 页面会根据需要动态显示正确产品的结果。 不同的是,通过路由,您可以完全自定义 URL,如图 5.2 所示。 由于搜索引擎优化 (SEO) 的原因,这为您提供了更大的灵活性,并且在现实生活中的应用程序中可能很重要。

图 5.2 如果您使用基于文件布局的映射,您的产品范围内的每个产品都需要一个不同的 Razor 页面。通过路由,多个 URL 映射到单个 Razor 页面,并且动态参数捕获 URL 中的差异。
image

除了启用动态 URL 之外,路由从根本上将应用程序中的 URL 与 Razor 页面的文件名分离。 例如,假设您的项目中有一个带有 Razor 页面的货币转换器应用程序,该页面位于路径 Pages/Rates/View.cshtml,用于查看货币的汇率,例如美元。 默认情况下,这可能对应于用户的 URL /rates/view/1。 这可以正常工作,但它并不能告诉用户太多——这将显示哪种货币? 是历史观点还是当前汇率?

幸运的是,通过路由可以轻松修改暴露的 URL,而无需更改 Razor 页面文件名或位置。 根据您的路由配置,您可以将指向 View.cshtml Razor 页面的 URL 设置为以下任何一项:

/rates/view/1
/rates/view/USD
/rates/current-exchange-rate/USD
/current-exchange-rate-for-USD

我知道我最想在浏览器的 URL 栏中看到哪些,最有可能点击哪些! 这种级别的自定义通常不是必需的,从长远来看,默认 URL 通常是最佳选择,但在需要时能够自定义 URL 非常有用。

ASP.NET Core 中的路由

路由从一开始就是 ASP.NET Core 的一部分,但在 ASP.NET Core 3.0 中它经历了一些大的变化。 在 ASP.NET Core 2.0 和 2.1 中,路由仅限于 Razor Pages 和 ASP.NET Core MVC 框架。 中间件管道中没有专用的路由中间件——路由只发生在 Razor Pages 或 MVC 组件中。

鉴于您的应用程序的大部分逻辑都是在 Razor Pages 中实现的,因此在大多数情况下,仅对 Razor Pages 使用路由就可以了。 不幸的是,将路由限制到 MVC 基础架构使一些事情变得有些混乱。这意味着一些横切关注点,例如授权,仅限于 MVC 基础架构,并且很难从应用程序中的其他中间件中使用。 这种限制导致不可避免的重复,这并不理想。

在 ASP.NET Core 3.0 中,引入了一个新的路由系统,端点路由。 端点路由使路由系统成为 ASP.NET Core 的一个更基本的功能,并且不再将其绑定到 MVC 基础结构。 Razor Pages 和 MVC 仍然依赖端点路由,但现在其他中间件也可以使用它。 ASP.NET Core 5.0 使用与 ASP.NET Core 3.0 相同的端点路由系统。

在 ASP.NET Core 中使用端点路由

端点路由是除最简单的 ASP.NET Core 应用程序之外的所有应用程序的基础。 它是使用两个中间件实现的,您之前已经看到过:

  • EndpointMiddleware - 当您启动应用程序时,您可以使用此中间件在路由系统中注册端点。 中间件在运行时执行端点之一。
  • EndpointRoutingMiddleware - 此中间件选择 EndpointMiddleware 注册的哪些端点应在运行时为给定请求执行。 为了更容易区分这两种类型的中间件,我将在本书中将这种中间件称为 RoutingMiddleware

EndpointMiddleware 是您配置系统中所有端点的地方。 这是您注册 Razor Pages 和 MVC 控制器的地方,但您也可以注册 MVC 框架之外的其他处理程序,例如确认您的应用程序仍在运行的运行状况检查端点。

定义 ASP.NET Core 中的端点是一些返回响应的处理程序。 每个端点都与一个 URL 模式相关联。 Razor 页面处理程序和 MVC 控制器操作方法通常构成应用程序中的大部分端点,但您也可以使用简单的中间件作为端点或运行状况检查端点。

要在应用程序中注册终结点,请在 Startup.csConfigure 方法中调用 UseEndpoints。 此方法采用配置 lambda 操作来定义应用程序中的端点,如下面的清单所示。 您可以使用 MapRazorPages 等扩展程序在应用程序中自动注册所有 Razor 页面。 此外,您可以使用 MapGet 等方法显式注册其他端点。

清单 5.1 在 Startup.Configure 中注册多个端点

public void Configure(IApplicationBuilder app)
{
    app.UseRouting(); //将 EndpointRoutingMiddleware 添加到中间件管道。
    app.UseEndpoints(endpoints => //将 EndpointMiddleware 添加到管道并提供配置 lambda。
                     {
                         //将应用程序中的所有 Razor 页面注册为端点。
                         endpoints.MapRazorPages();
                         endpoints.MapHealthChecks("/healthz"); //在路由 /healthz 注册一个健康检查端点。
                         
                         //内联注册一个返回“Hello World!”的端点 在路线/测试。
                         endpoints.MapGet("/test", async context =>
                                          {
                                              await context.Response.WriteAsync("Hello World!");
                                          });
                     });
}

每个端点都与一个路由模板相关联,该模板定义了端点应该匹配的 URL。您可以在前面的清单中看到两个路由模板,“/healthz”和“/test”。

定义 路由模板是用于匹配请求 URL 的 URL 模式。它们是固定值的字符串,如前面清单中的“/test”。它们还可以包含变量的占位符,如您将在 5.3 节中看到的。

EndpointMiddleware 将注册的路由和端点存储在字典中,它与 RoutingMiddleware 共享。 在运行时,RoutingMiddleware 将传入请求与字典中注册的路由进行比较。 如果 RoutingMiddleware 找到匹配的端点,它会记下选择了哪个端点并将其附加到请求的 HttpContext 对象中。 然后它调用管道中的下一个中间件。 当请求到达 EndpointMiddleware 时,中间件会检查选择了哪个端点并执行它,如图 5.3 所示。

图 5.3 端点路由使用两步过程。RoutingMiddleware 选择执行哪个端点,EndpointMiddleware 执行它。如果请求 URL 与路由模板不匹配,端点中间件将不会生成响应。
image

如果请求 URL 与路由模板不匹配,则 RoutingMiddleware 不会选择端点,但请求仍会沿中间件管道继续。 由于没有选择端点,EndpointMiddleware 会默默地忽略该请求并将其传递给管道中的下一个中间件。 EndpointMiddleware 通常是管道中的最后一个中间件,因此“下一个”中间件通常是总是返回 404 Not Found 响应的虚拟中间件。

如果请求 URL 与路由模板不匹配,则不会选择或执行任何端点。 整个中间件管道仍在执行,但通常在请求到达虚拟 404 中间件时返回 404 响应。

乍一看,使用两个独立的中间件来处理这个过程的优势可能并不明显。 图 5.3 暗示了主要的好处——放置在 RoutingMiddleware 之后的所有中间件都可以看到哪个端点将在它之前执行。

只有放置在 RoutingMiddleware 之后的中间件才能检测到哪个端点将被执行。

图 5.4 显示了一个更真实的中间件管道,其中中间件放置在 RoutingMiddleware 之前以及 RoutingMiddlewareEndpointMiddleware 之间。

图 5.4 放置在路由中间件之前的中间件不知道路由中间件会选择哪个端点。放置在路由中间件和端点中间件之间的中间件可以看到选中的端点。
image

图 5.4 中的 StaticFileMiddleware 放置在 RoutingMiddleware 之前,因此它在选择端点之前执行。 反之,AuthorizationMiddleware 放在 RoutingMiddleware 之后,所以可以看出 Index.cshtml Razor Page 端点最终将被执行。 此外,它可以访问有关端点的某些元数据,例如其名称以及访问 Razor 页面所需的权限。

AuthorizationMiddleware 需要知道哪个端点将是 执行,所以它必须放在中间件管道中的 RoutingMiddleware 之后和 EndpointMiddleware 之前。

在构建应用程序时,记住两种类型的路由中间件的不同角色很重要。 如果您有一个中间件需要知道给定请求将执行哪个端点(如果有),那么您需要确保将它放在 RoutingMiddleware 之后和 EndpointMiddleware 之前。

基于约定的路由与属性路由

路由是 ASP.NET Core 的关键部分,因为它将传入请求的 URL 映射到要执行的特定端点。 您有两种不同的方式在应用程序中定义这些 URL 端点映射:

  • 使用基于约定的全局路由
  • 使用属性路由

您使用哪种方法通常取决于您使用的是 Razor 页面还是 MVC 控制器,以及您是在构建 API 还是网站(使用 HTML)。

基于约定的路由是为您的应用程序全局定义的。您可以使用基于约定的路由将应用程序中的端点(MVC 控制器操作)映射到 URL,但您的 MVC 控制器必须严格遵守您定义的约定。 传统上,使用 MVC 控制器生成 HTML 的应用程序倾向于使用这种路由方法。 这种方法的缺点是它使得为控制器和操作的子集定制 URL 变得更加困难。

或者,您可以使用基于属性的路由将给定的 URL 绑定到特定的端点。对于 MVC 控制器,这涉及将 [Route] 属性放置在操作方法本身上,因此称为属性路由。 这提供了更多的灵活性,因为您可以明确定义每个操作方法的 URL 应该是什么。 这种方法通常比基于约定的方法更冗长,因为它需要将属性应用于应用程序中的每个操作方法。 尽管如此,它提供的额外灵活性可能非常有用,尤其是在构建 Web API 时。

有点令人困惑的是,Razor Pages 使用约定来生成属性路由! 在许多方面,这种组合提供了两全其美的优势——您可以获得基于约定的路由的可预测性和简洁性,并且可以轻松自定义属性路由。 每种方法都有权衡,如表 5.1 所示。

表 5.1 ASP.NET Core 中可用的路由样式的优缺点

路由类型 典型用途 好处 缺点
基于约定的路线 生成 HTML 的 MVC 控制器 在您的应用程序的一个位置进行非常简洁的定义
强制 MVC 控制器的布局一致
路由定义在与控制器不同的位置。
覆盖路由约定可能很棘手且容易出错。
在路由请求时添加额外的间接层
属性路由 Web API MVC 控制器 为每个操作提供对路线模板的完全控制。
路由是在它们执行的操作旁边定义的。
与基于约定的路由相比,冗长可以很容易地过度自定义路由模板。
路由模板分散在整个应用程序中,而不是在一个位置。
基于约定的属性路由生成 Razor Pages 鼓励一致的公开 URL 集 遵守约定时简洁 轻松覆盖单个页面的路由模板。
全局自定义约定以更改公开的 URL。
可能过度自定义路由模板 您必须计算页面的路由模板是什么,而不是在您的应用程序中明确定义。

那么你应该使用哪种方法呢? 我的观点是,在 99% 的情况下,基于约定的路由不值得付出努力,您应该坚持使用属性路由。 如果您遵循我使用 Razor Pages 的建议,那么您已经在使用属性路由。 此外,如果您使用 MVC 控制器创建 API,属性路由是最佳选择,也是推荐的方法。

传统上使用基于约定的路由的唯一场景是使用 MVC 控制器生成 HTML。但是,如果您遵循我在第 4 章中的建议,您将使用 Razor Pages 来生成 HTML 应用程序,并且只有在完全必要时才回退到 MVC 控制器。为了保持一致性,在那种情况下我仍然会坚持使用属性路由。

无论您使用哪种技术,您都将使用路由模板定义应用程序的预期 URL。这些定义了您期望的 URL 的模式,并为可能不同的部分提供了占位符。

定义 路由模板定义应用程序中已知 URL 的结构。它们是带有占位符的字符串,用于可以包含可选值的变量。

单个路由模板可以匹配许多不同的 URL。 例如,/customer/1/customer/2 URL 都将由“customer/{id}”路由模板匹配。路由模板语法功能强大,包含许多不同的功能,这些功能通过将 URL 拆分为多个来控制 段。

段是 URL 的一个小的连续部分。 它通过至少一个字符与其他 URL 段分隔,通常由 / 字符分隔。路由涉及将 URL 的段与路由模板匹配。

对于每个路由模板,您可以定义

  • 特定的、预期的字符串
  • URL 的可变段
  • URL 的可选段
  • 未提供可选段时的默认值
  • 对 URL 段的约束,例如确保它是数字的

大多数应用程序将使用各种这些功能,但您通常只会在这里和那里使用一两个功能。 在大多数情况下,Razor Pages 生成的基于约定的默认属性路由模板将是您所需要的。 在下一节中,我们将详细了解这些约定以及路由如何将请求的 URL 映射到 Razor 页面。

路由到 Razor 页面

正如我在 5.2.2 节中提到的,Razor Pages 通过创建基于约定的路由模板来使用属性路由。 当您在 Startup.csConfigure 方法中调用 MapRazorPages 时,ASP.NET Core 在应用启动期间为应用中的每个 Razor Page 创建一个路由模板:

对于应用程序中的每个 Razor 页面,框架使用 Razor 页面文件相对于 Razor 页面根目录 (Pages/) 的路径,不包括文件扩展名 (.cshtml)。 例如,如果您有一个位于 Pages/Products/View.cshtml 路径的 Razor 页面,则框架会创建一个值为“Products/View”的路由模板,如图 5.5 所示。

默认情况下,基于文件相对于根目录 Pages 的路径为 Razor Pages 生成路由模板。
image

对 URL /products/view 的请求与路由模板“Products/View”匹配,后者又对应于 View.cshtml Razor 页面。 RoutingMiddleware 选择 View.cshtml Razor Page 作为请求的端点,一旦请求在中间件管道中到达,EndpointMiddleware 就会执行页面的处理程序。

路由不区分大小写,因此请求 URL 不需要与要匹配的路由模板具有相同的 URL 大小写。

当我们说“执行 Razor 页面”时,我们真正的意思是“创建了 Razor 页面的 PageModel 实例,并调用模型上的页面处理程序”。 Razor 页面可以有多个页面处理程序,因此一旦 RoutingMiddleware 选择了一个 Razor 页面,EndpointMiddleware 仍然需要选择要执行的处理程序。您将在 5.6 节中了解框架如何选择要调用的页面处理程序。

默认情况下,每个 Razor 页面会根据其文件路径创建单个路由模板。此规则的例外是名为 Index.cshtml 的 Razor 页面。 Index.cshtml 页面创建两个路由模板,一个以“Index”结尾,另一个没有结尾。 例如,如果您在 Pages/ToDo/Index.cshtml 路径中有一个 Razor 页面,则会生成两个路由模板:

"ToDo"
"ToDo/Index"

作为最后一个示例,考虑使用 Visual Studio 创建 Razor Pages 应用程序或使用 .NET CLI 运行 dotnet new web 时默认创建的 Razor Pages。标准模板包括三个 Razor Pages 页面目录:

Pages/Error.cshtml
Pages/Index.cshtml
Pages/Privacy.cshtml

这将为应用程序创建四个路由的集合,由以下模板定义:

"" maps to Index.cshtml
"Index" maps to Index.cshtml
"Error" maps to Error.cshtml
"Privacy" maps to Privacy.cshtml

在这一点上,路由可能感觉微不足道,但这只是您使用默认 Razor Pages 约定免费获得的基础知识,这对于任何网站的大部分内容通常都足够了。 但是,在某些时候,您会发现您需要更动态的东西,例如您希望每个产品都有自己的 URL,但映射到单个 Razor 页面的电子商务网站。 这就是路由模板和路由数据发挥作用的地方,展示了路由的真正威力。

自定义 Razor 页面路由模板

Razor 页面的路由模板默认基于文件路径,但您也可以为每个页面自定义最终模板,甚至完全替换它。 在本节中,我将展示如何为单个页面自定义路由模板,以便您可以自定义应用程序的 URL 并将多个 URL 映射到单个 Razor 页面。

路由模板具有丰富、灵活的语法,但一个简单的示例如图 5.6 所示。

图 5.6 显示文字段和两个必需的路由参数的简单路由模板
image

路由中间件通过将路由模板拆分为多个段来解析路由模板。段通常由 / 字符分隔,但它可以是任何有效字符。每个段是

  • 文字值——例如,图 5.6 中的 product
  • 一个路由参数——例如,图 5.6 中的 {category}{name}

文字值必须与请求 URL 完全匹配(忽略大小写)。 如果您需要精确匹配特定的 URL,您可以使用仅包含文字的模板。这是 Razor Pages 的默认情况,如您在第 5.2.3 节中所见; 每个 Razor 页面由一系列文字段组成,例如“ToDo/Index”。

假设您的应用程序中有一个联系人页面,路径为 Pages/About/Contact.cshtml。 此页面的路线模板是“About/Contact”。 此路由模板仅包含文字值,因此仅匹配确切的 URL。 以下 URL 均不匹配此路由模板:

/about
/about-us/contact
/about/contact/email
/about/contact-us

路由参数是 URL 的一部分,可能会有所不同,但仍将与模板匹配。 它们是通过给它们命名并将它们放在大括号中来定义的,例如 {category} 或 {name}。 这种方式使用时,参数是必填项,所以在请求的URL中必须有一个段对应,但值可以变化。

有些词不能用作路由参数的名称:area、action、controller、handler 和 page。

使用路由参数的能力为您提供了极大的灵活性。 例如,简单的路由模板“{category}/{name}”可用于匹配电子商务应用程序中的所有产品页面 URL,例如

/bags/rucksack-a—其中 category=bags 和 name=rucksack-a
/shoes/black-size 9 - 其中 category=shoes 和 name=black-size 9

但请注意,此模板不会映射以下 URL:

/socks/ - 未指定名称参数
/trousers/mens/formal - 额外的 URL 段,正式的,在路由模板中找不到

当路由模板定义路由参数并且路由匹配 URL 时,将捕获与参数关联的值并将其存储在与请求关联的值的字典中。 这些路由值通常会驱动 Razor 页面中的其他行为,例如模型绑定。

定义 路由值是基于给定路由模板从 URL 中提取的值。 模板中的每个路由参数都有一个 associateroute 值,它们以字符串对的形式存储在字典中。

文字段和路由参数是 ASP.NET Core 路由模板的两个基石。 有了这两个概念,就可以为您的应用程序构建各种形式的 URL。 但是如何自定义 Razor 页面以使用其中一种模式?

将分段添加到 Razor 页面路由模板

要自定义 Razor 页面路由模板,请更新 Razor 页面的 .cshtml 文件顶部的 @page 指令。 该指令必须是 Razor 页面文件中的第一件事,才能正确注册页面。

您必须在 Razor 页面的 .cshtml 文件顶部包含 @page 指令。 没有它,ASP.NET Core 将不会将该文件视为 Razor 页面,并且您将无法查看该页面。

要将附加段添加到 Razor 页面的路由模板,请在 @page 语句之后添加一个空格,后跟所需的路由模板。 例如,要将“Extra”添加到 Razor 页面的路由模板,请使用

@page "Extra"

这会将提供的路由模板附加到为 Razor 页面生成的默认模板。 例如,Pages/Privacy.html 中 Razor 页面的默认路由模板是“隐私”。 使用前面的指令,页面的新路由模板将是“Privacy/Extra”。

注意 @page 指令中提供的路由模板附加到 Razor 页面的默认模板的末尾。

像这样自定义 Razor 页面的路由模板的最常见原因是添加路由参数。 例如,您可以有一个 Razor 页面,用于在路径 Pages/Products.cshtml 处显示电子商务站点中的产品,并在 @page 指令中使用路由参数

@page "{category}/{name}"

这将给出 Products/{category}/{name} 的最终路由模板,它将匹配以下所有 URL:

/Products/phones/iPhoneX
/products/shoes/black-size9
/products/bags/white-rucksack

像这样将路线段添加到 Razor 页面模板是很常见的,但如果这还不够呢? 也许您不想在前面的 URL 的开头有 /products 段,或者您想为页面使用完全自定义的 URL。 幸运的是,这同样容易实现。

完全替换 Razor 页面路由模板

如果您可以尽可能坚持默认路由约定,并在必要时为路由参数添加额外的段,您将最高效地使用 Razor Pages。 但有时你只需要更多的控制权。 正如您在上一节中看到的,您的应用程序中的重要页面通常是这种情况,例如电子商务应用程序的结帐页面,甚至是产品页面。

要为 Razor 页面指定自定义路由,请在 @page 指令中为路由添加 / 前缀。 例如,要从上一节中的路由模板中删除“product/”前缀,请使用以下指令:

@page "/{category}/{name}"

请注意,该指令在路由开头包含“/”,表示这是自定义路由模板,而不是添加。 此页面的路由模板将是“{category}/{name}”,无论它应用于哪个 Razor 页面。

同样,您可以通过以“/”开始模板并仅使用文字段来为页面创建静态自定义模板。 例如,

@page "/checkout"

无论您将结帐 Razor 页面放在 Pages 文件夹中的哪个位置,使用此指令可确保它始终具有路由模板“checkout”,因此它将始终与请求 URL /checkout 匹配。

您还可以将以“/”开头的自定义路由模板视为绝对路由模板,而其他路由模板则相对于它们在文件层次结构中的位置。

请务必注意,当您为 Razor 页面自定义路由模板时,无论是附加到默认路由还是将其替换为自定义路由时,默认模板都不再有效。 例如,如果您在位于 Pages/Payment.cshtml 的 Razor 页面上使用上述“结帐”路由模板,则只能使用 URL /checkout 访问它; URL /Payment 不再有效,不会执行 Razor 页面。

使用 @page 指令自定义 Razor 页面的路由模板会替换页面的默认路由模板。 在 5.7 节中,我展示了如何在保留默认路由模板的同时添加其他路由。

探索路由模板语法

使用可选值和默认值

在上一节中,您看到了一个简单的路由模板,其中包含一个文字段和两个必需的路由参数。 在图 5.7 中,您可以看到使用许多附加功能的更复杂的路线。

图 5.7 显示文字段、命名路由参数、可选参数和默认值的更复杂的路由模板
image

文字产品段和所需的 {category} 参数与您在图 5.6 中看到的相同。 {name} 参数看起来很相似,但它使用 =all 为其指定了一个默认值。 如果 URL 不包含对应于 {name} 参数的段,路由器将使用 all 值代替。

图 5.7 的最后一段,{id?},定义了一个可选的路由参数,称为 id。 URL 的这一部分是可选的——如果存在,路由器将捕获 {id} 参数的值; 如果不存在,则不会为 id 创建路由值。

您可以在模板中指定任意数量的路由参数,当涉及到模型绑定时,您可以使用这些值。图 5.7 的复杂路由模板允许您通过使 {name}{id} 可选,并为 {name} 提供默认值来匹配更多种类的 URL。表 5.2 显示了该模板将匹配的一些可能的 URL 以及路由器将设置的相应路由值。

表 5.2 匹配图 5.7 模板的 URL 及其对应的路由值

URL 路由值
/product/shoes/formal/3 category=shoes, name=formal, id=3
/product/shoes/formal category=shoes, name=formal
/product/shoes category=shoes, name=all
/product/bags/satchels category=bags, name=satchels
/product/phones category=phones, name=all
/product/computers/laptops/ABC-123 category=computes, name=laptops,id=ABC-123

请注意,如果不指定 {category}{name} 参数,就无法为可选的 {id} 参数指定值。 您只能在路由模板的末尾放置一个可选参数(没有默认值)。 例如,假设您的路由模板有一个可选的 {category} 参数:

{category?}/{name}

现在试着想一个可以指定 {name} 参数但不指定 {category} 的 URL。 做不到! 像这样的模式本身不会导致错误; category 参数本质上是必需的,即使您已将其标记为可选。

使用默认值允许您有多种方式来调用同一个 URL,这在某些情况下可能是可取的。 例如,给定图 5.7 中的路由模板,以下两个 URL 是等价的:

/product/shoes
/product/shoes/all

这两个 URL 将执行相同的 Razor 页面,具有相同的 category=shoes 和 name=all 路由值。 使用默认值允许您在应用程序中使用更短、更容易记住的 URL 来处理常见 URL,但仍然可以灵活地匹配单个模板中的各种路由。

向路由参数添加额外的约束

通过定义路由参数是必需的还是可选的,以及它是否具有默认值,您可以使用非常简洁的模板语法匹配广泛的 URL。不幸的是,在某些情况下,这最终可能会有点过于宽泛。 路由仅将 URL 段与路由参数匹配; 它对您期望这些路由参数包含的数据一无所知。 如果您考虑类似于图 5.7 中的模板,“{category}/{name=all}/{id?}”,则以下 URL 将全部匹配:

/shoes/sneakers/test
/shoes/sneakers/123
/Account/ChangePassword
/ShoppingCart/Checkout/Start
/1/2/3

考虑到模板的语法,这些 URL 都是完全有效的,但有些可能会给您的应用程序带来问题。 这些 URL 都有两个或三个段,因此路由器很乐意分配路由值并在您可能不希望时匹配模板! 这些是分配的路线值:

/shoes/sneakers/test—category=shoes, name=sneakers, id=test
/shoes/sneakers/123—category=shoes, name=sneakers, id=123
/Account/ChangePassword—category=Account, name=ChangePassword
/Cart/Checkout/Start—category=Cart, name=Checkout, id=Start
/1/2/3—category=1, name=2, id=3

通常,路由器通过称为模型绑定的过程将路由值传递给 Razor Pages, 例如,带有处理程序 public void OnGet(int id) 的 Razor 页面将从 id 路由值中获取 id 参数。 如果 id 路由参数最终从 URL 中分配了一个非整数值,那么当它绑定到整数 id 参数时,你会得到一个异常。

为了避免这个问题,可以向路由模板添加额外的约束,必须满足这些约束才能使 URL 被视为匹配。 可以使用 :(冒号)在给定路由参数的路由模板中定义约束。 例如,{id:int} 会将 IntRouteConstraint 添加到 id 参数。 对于要被视为匹配的给定 URL,分配给 id 路由值的值必须可转换为整数。

您可以将大量路由约束应用于路由模板,以确保路由值可转换为适当的类型。 您还可以检查更高级的约束,例如,整数值是否具有特定的最小值,或者字符串值是否具有最大长度。

表 5.3 一些路由约束及其应用时的行为

约束 样例 匹配示例 描述
int 123, -123, 0 匹配任何整数
Guid d071b70c-a812-4b54-87d2-7769528e2814 匹配任何Guid
decimal 29.99, 52, -1.01 匹配任何十进制值
min(value) 18, 20 匹配 18 或更大的整数值
length(value) andrew,123456 匹配长度为 6 的字符串值
optional int 123, -123, 0, null 可选匹配任何整数
optional int max(value) 3, -123, 0, null 可选匹配任何小于等于 10 的整数

使用约束允许您缩小给定路由模板将匹配的 URL。 当路由中间件将 URL 匹配到路由模板时,它会询问约束以检查它们是否都有效。 如果它们无效,则路由模板不会被视为匹配,并且不会执行 Razor 页面。

约束最好谨慎使用,但当您对应用程序使用的 URL 有严格要求时,它们会很有用,因为它们可以让您解决一些其他棘手的组合。

使用 catch-all 参数匹配任意 URL

您已经看到路由模板如何获取 URL 段并尝试将它们与参数或文字字符串匹配。 这些段通常围绕斜杠字符 / 分开,因此路由参数本身不会包含斜杠。 如果你需要它们包含一个斜线,或者你不知道你将有多少段,你会怎么做?

假设您正在构建一个货币转换器应用程序,该应用程序显示从一种货币到一种或多种其他货币的汇率。 您被告知此页面的 URL 应包含所有货币作为单独的段。 这里有些例子:

/USD/convert/GBP—显示美元与英镑的汇率
/USD/convert/GBP/EUR - 显示美元与英镑和欧元的汇率
/USD/convert/GBP/EUR/CAD - 显示美元以及英镑、欧元和加元的汇率

如果您想像上面的 URL 那样支持显示任意数量的货币,您需要一种在转换段之后捕获所有内容的方法。 您可以通过在 @page 指令中使用 catch-all 参数来为 Pages/Convert.cshtml Razor页面实现此目的,如图 5.8 所示。

图 5.8 您可以使用 catch-all 参数来匹配 URL 的其余部分。 包罗万象的参数可能包括“/”字符或者可能是一个空字符串。
image

可以在参数定义中使用一个或两个星号来声明任意的参数,例如 {*others}{**others}。 这些将匹配 URL 中剩余的不匹配部分,包括任何斜杠或其他不属于早期参数的字符。 它们也可以匹配一个空字符串。 对于 USD/convert/GBP/EUR URL,其他路由值的值将是单个字符串“GBP/EUR”。

任意参数是贪婪的,将捕获 URL 的整个不匹配部分。 在可能的情况下,为避免混淆,请避免使用与其他路径模板重叠的包罗万象的参数定义路径模板。

将传入请求路由到 Razor 页面时,catch-all 参数的一个星号和两个星号版本的行为相同。 仅当您生成 URL(我们将在下一节中介绍)时才会出现差异:单星号版本的 URL 编码正斜杠,而双星号版本的 URL 不编码。 两个星号版本的往返行为通常是您想要的。

您没看错——将 URL 映射到 Razor 页面只是 ASP.NET Core 中路由系统的一半职责。 它还用于生成 URL,以便您可以轻松地从应用程序的其他部分引用您的 Razor 页面。

从路由参数生成 URL

在本节中,我们将了解路由的另一半——生成 URL。 您将学习如何将 URL 生成为可在代码中使用的字符串,以及如何自动发送重定向 URL 作为 Razor 页面的响应。

在 ASP.NET Core 中使用路由基础结构的副产品之一是您的 URL 可能有些流动。 如果重命名 Razor 页面,与该页面关联的 URL 也会更改。 例如,将 Pages/Cart.cshtml 页面重命名为 Pages/Basket/View.cshtml 会导致您用于访问该页面的 URL 从 /Cart 更改为 /Basket/View

尝试在您的应用程序中手动管理这些链接将导致心痛、链接断开和 404 错误。如果你的 URL 是硬编码的,你必须记住每次重命名都要进行查找和替换!

幸运的是,您可以使用路由基础架构在运行时动态生成适当的 URL,从而将您从负担中解放出来。 从概念上讲,这几乎与将 URL 映射到 Razor 页面的过程完全相反,如图 5.9 所示。 在“路由”的情况下,路由中间件获取一个 URL,将其与路由模板匹配,并将其拆分为路由值。 在“URL 生成”的情况下,生成器接收路由值并将它们与路由模板组合以构建 URL。

图 5.9 路由和 URL 生成的比较。路由接受 URL 并生成路由值,但 URL 生成使用路由值来生成 URL。
image

为 Razor 页面生成 URL

您将需要在应用程序的不同位置生成 URL,一个常见位置是在 Razor 页面和 MVC 控制器中。 以下清单显示了如何使用来自 PageModel 基类的 Url 帮助器生成指向 Pages/Currency/View.cshtml Razor 页面的链接。

清单 5.2 使用 IUrlHelper 和 Razor 页面名称生成 URL

public class IndexModel : PageModel //从 PageModel 派生可以访问 Url 属性。
{
    public void OnGet()
    {
        //您提供 Razor 页面的相对路径以及任何其他路由值。
        var url = Url.Page("Currency/View", new { code = "USD" });
    }
}

Url 属性是 IUrlHelper 的一个实例,它允许您通过按文件路径引用其他 Razor 页面来轻松地为您的应用程序生成 URL。 它公开了一个 Page 方法,您可以将 Razor Page 的名称和任何其他路由数据传递给该方法。 路由数据作为键值对打包到单个 C# 匿名对象中。如果需要传递多个路由值,可以向匿名对象添加其他属性。 然后,助手将根据引用页面的路由模板生成一个 URL。

IUrlHelper 有几个不同的 Page 方法重载。 其中一些方法允许您指定特定的页面处理程序,另一些允许您生成绝对 URL 而不是相对 URL,还有一些允许您传递其他路由值。

在清单 5.2 中,除了提供文件路径外,我还传入了一个匿名对象 new { code = "USD" }。 此对象在生成 URL 时提供额外的路由值,在本例中将代码参数设置为“USD”。

如果选定的路由在其定义中明确包含定义的路由值,例如在“Currency/View/{code}”路由模板中,则路由值将用于 URL 路径,给出 /Currency/View/GBP

如果路由没有明确包含路由值,如在“货币/视图”模板中,路由值将作为附加数据作为查询字符串的一部分附加,例如 /Currency/View?code=GBP

根据您要执行的页面生成 URL 很方便,也是大多数情况下采用的常用方法。 如果您为 API 使用 MVC 控制器,则该过程与 Razor Pages 的过程大致相同,尽管方法略有不同。

为 MVC 控制器生成 URL

为 MVC 控制器生成 URL 与 Razor 页面非常相似。 主要区别在于您在 IUrlHelper 上使用 Action 方法,并且您提供 MVC 控制器名称和操作名称而不是页面路径。下面的清单显示了一个 MVC 控制器,它使用来自 Controller 基类的 Url 帮助器生成从一个操作方法到另一个操作方法的链接。

清单 5.3 使用 IUrlHelper 和动作名称生成 URL

public class CurrencyController : Controller //从 Controller 派生可以访问 Url 属性。
{
    [HttpGet("currency/index")]
    public IActionResult Index()
    {
        //您提供操作和控制器名称,以及任何其他路由值。
        var url = Url.Action("View", "Currency",new { code = "USD" });
        //这将返回“URL 是 /Currency/View/USD”。
        return Content($"The URL is {url}");
    }
    
    //生成的 URL 将路由到 View 操作方法。
    [HttpGet("currency/view/{code}")]
    public IActionResult View(string code)
    {
        /* method implementation*/
    }
}

您可以从 Razor Pages 和 MVC 控制器调用 IUrlHelper 上的 ActionPage 方法,因此您可以在需要时在它们之间来回生成链接。 重要的问题是,URL 的目的地是什么? 如果您需要的 URL 引用 Razor 页面,请使用 Page 方法。 如果目标是 MVC 操作,请使用 Action 方法。

如果您要路由到同一控制器中的操作,则可以使用不同的操作重载,在生成 URL 时省略控制器名称。 IUrlHelper 使用当前请求中的环境值,并使用您提供的任何特定值覆盖这些值。

环境值是当前请求的路由值。 它们包括从 MVC 控制器调用时的控制器和操作,但也可以包括在最初使用路由定位操作或 Razor 页面时设置的附加路由值。

使用 ActionResults 生成 URL

您已经了解了如何为 Razor 页面和 MVC 操作生成包含 URL 的字符串。 例如,如果您需要向用户显示 URL,或者将 URL 包含在 API 响应中,这将非常有用。 但是,您不需要经常显示 URL。更常见的是,您希望自动将用户重定向到 URL。 对于这种情况,您可以使用 ActionResult 来处理 URL 生成。

以下清单显示了如何生成一个使用 ActionResult 自动将用户重定向到不同 Razor 页面的 URL。 RedirectToPage 方法采用 Razor 页面的路径和任何所需的路由参数,并以与 Url.Page 方法相同的方式生成 URL。 该框架会自动将生成的 URL 作为响应发送,因此您永远不会在代码中看到该 URL。 然后用户的浏览器从响应中读取 URL 并自动重定向到新页面。

清单 5.4 从 ActionResult 生成重定向 URL

public class CurrencyModel : PageModel
{
    public IActionResult OnGetRedirectToPage()
    {
        //RedirectToPage 方法使用生成的 URL 生成 RedirectToPageResult。
        return RedirectToPage("Currency/View", new { id = 5 });
    }
}

您可以使用类似的方法 RedirectToAction 来自动重定向到 MVC 操作。 与 Page 和 Action 方法一样,它是控制您是否需要使用 RedirectToPageRedirectToAction 的目标。 仅当您使用 MVC 控制器生成 HTML 而不是 Razor 页面时,才需要 RedirectToAction

除了从 Razor 页面和 MVC 控制器生成 URL 之外,您经常会发现在视图中构建 HTML 时需要生成 URL。 这是在您的 Web 应用程序中提供导航链接所必需的。

如果您需要从 Razor 页面或 MVC 基础架构之外的应用程序部分生成 URL,您将无法使用 IUrlHelper 帮助程序或 ActionResult。 相反,您可以使用 LinkGenerator 类。

从应用程序的其他部分生成 URL

编写 Razor Pages 和 MVC 控制器,那么您应该尽量保持 Razor Pages 相对简单。 这要求您在单独的类和服务中执行应用程序的业务和域逻辑。

在大多数情况下,您的应用程序使用的 URL 不应该是您的域逻辑的一部分。 这使您的应用程序更容易随着时间的推移而发展,甚至完全改变。 例如,您可能希望创建一个重用 ASP.NET Core 应用程序的业务逻辑的移动应用程序。 在这种情况下,在业务逻辑中使用 URL 是没有意义的,因为当从移动应用程序调用逻辑时它们是不正确的!

在可能的情况下,尽量将前端应用程序设计的知识排除在业务逻辑之外。 这种模式通常被称为依赖倒置原则。

不幸的是,有时这种分离是不可能的,或者它使事情变得更加复杂。 一个例子可能是当您在后台服务中创建电子邮件时——您可能需要在电子邮件中包含指向您的应用程序的链接。 LinkGenerator 类允许您生成该 URL,以便在应用程序中的路由发生更改时自动更新。

LinkGenerator 类可用于应用程序的任何部分,因此您可以在中间件和任何其他服务中使用它。 如果您愿意,您也可以从 Razor Pages 和 MVC 中使用它,但 IUrlHelper 通常更容易,并且隐藏了使用 LinkGenerator 的一些细节。

LinkGenerator 具有多种生成 URL 的方法,例如 GetPathByPageGetPathByActionGetUriByPage,如下面的清单所示。 使用这些方法有一些微妙之处,特别是在复杂的中间件管道中,所以尽可能坚持使用 IUrlHelper,如果遇到问题,请务必查阅文档。

清单 5.5 使用 LinkGeneratorClass 生成 URL

public class CurrencyModel : PageModel
{
    //LinkGenerator 可以使用依赖注入来访问。
    private readonly LinkGenerator _link;
    public CurrencyModel(LinkGenerator linkGenerator)
    {
        _link = linkGenerator;
    }
    public void OnGet ()
    {
        //Url 可以使用 Url.Page 生成相对路径。您可以使用相对或绝对 Page 路径。
        var url1 = Url.Page("Currency/View", new { id = 5 });
        //GetPathByPage 相当于传入 HttpContext 时的 Url.Page。不能使用相对路径。
        var url3 = _link.GetPathByPage(
            HttpContext,
            "/Currency/View",
            values: new { id = 5 });
        
        // 其他重载不需要 HttpContext。
        var url2 = _link.GetPathByPage( 
            "/Currency/View",
            values: new { id = 5 });
        //GetUriByPage 生成绝对 URL 而不是相对 URL。
        var url4 = _link.GetUriByPage(
            page: "/Currency/View",
            handler: null,
            values: new { id = 5 },
            scheme: "https",
            host: new HostString("example.com"));
    }
}

无论您是使用 IUrlHelper 还是 LinkGenerator 生成 URL,在使用路由生成方法时都需要小心。 确保提供正确的 Razor 页面路径和任何必要的路由参数。 如果出现错误——例如,路径中有错字或忘记包含必需的路由参数——生成的 URL 将为空。 值得明确检查生成的 URL 是否为 null,以确保没有问题。

选择要调用的页面处理程序

在本章开始时,我说过路由是关于将 URL 映射到处理程序。 对于 Razor Pages,这意味着页面处理程序,但到目前为止,我们只讨论了基于 Razor Page 的路由模板的路由。 在本节中,您将了解 EndpointMiddleware 在执行 Razor 页面时如何选择要调用的页面处理程序。

以及它们在 Razor 页面中的作用,但我们还没有讨论如何为给定请求选择页面处理程序。 Razor Pages 可以有多个处理程序,因此如果 RoutingMiddleware 选择了一个 Razor Page,EndpointMiddleware 仍然需要知道如何选择执行哪个处理程序。

考虑以下清单中显示的 Razor Page SearchModel。 此 Razor 页面具有三个处理程序:OnGetOnPostAsyncOnPostCustomSearch。 处理程序方法的主体没有显示,因为此时,我们只对 RoutingMiddleware 如何选择调用哪个处理程序感兴趣。

清单 5.6 带有多个页面处理程序的 Razor 页面

public class SearchModel : PageModel
{
    //处理 GET 请求
    public void OnGet()
    {
        // Handler implementation
    }
    //处理 POST 请求。 async 后缀是可选的,出于路由目的而被忽略。
    public Task OnPostAsync()
    {
        // Handler implementation
    }
    //处理处理程序路由值具有值 CustomSearch 的 POST 请求
    public void OnPostCustomSearch()
    {
        // Handler implementation
    }
}

Razor 页面可以包含任意数量的页面处理程序,但只有一个响应给定请求而运行。 当 EndpointMiddleware 执行选定的 Razor 页面时,它会根据两个变量选择要调用的页面处理程序:

  • 请求中使用的 HTTP 动词(例如 GET、POST 或 DELETE)
  • 处理程序路由值的值

处理程序路由值通常来自请求 URL 中的查询字符串值,例如 /Search?handler=CustomSearch。 如果您不喜欢查询字符串的外观,您可以在 Razor 页面的路由模板中包含 {handler} 路由参数。 例如,对于清单 5.6 中的搜索页面,您可以更新 页面的指令

@page "{handler}"

这将提供一个完整的路由模板,例如“Search/{handler}”,它将匹配诸如 /Search/CustomSearch 之类的 URL。

EndpointMiddleware 使用处理程序路由值和 HTTP 动词以及标准命名约定来标识要执行的页面处理程序,如图 5.10 所示。 handler 参数是可选的,通常作为请求的查询字符串的一部分或作为路由参数提供,如上所述。async 后缀也是可选的,通常在处理程序使用异步编程结构(如 Task 或 async/await)时使用 .

图 5.10 Razor 页面处理程序根据 HTTP 动词和可选的处理程序参数与请求匹配。
image

根据这个约定,我们现在可以识别清单 5.6 中每个页面处理程序对应的请求类型:

  • OnGet - 为未指定处理程序值的 GET 请求调用。
  • OnPostAsync - 为未指定处理程序值的 POST 请求调用。返回一个 Task,因此它使用 Async 后缀,出于路由目的而忽略该后缀。
  • OnPostCustomSearch - 为指定处理程序值“CustomSearch”的 POST 请求调用。

清单 5.6 中的 Razor 页面指定了三个处理程序,因此它只能处理三个动词处理程序对。 但是,如果您收到与这些不匹配的请求,例如使用 DELETE 动词的请求、具有非空处理程序值的 GET 请求或具有无法识别的处理程序值的 POST 请求,会发生什么?

对于所有这些情况,EndpointMiddleware 会改为执行隐式页面处理程序。 隐式页面处理程序不包含任何逻辑; 他们只是渲染 Razor 视图。 例如,如果您在清单 5.6 中向 Razor 页面发送了一个 DELETE 请求,则会执行一个隐式处理程序。 隐式页面处理程序等效于以下处理程序代码:

public void OnDelete() { }

定义 如果页面处理程序与请求的 HTTP 谓词和处理程序值不匹配,则会执行隐式页面处理程序以呈现关联的 Razor 视图。 隐式页面处理程序参与模型绑定并使用页面过滤器,但不执行任何逻辑。

隐式页面处理程序规则有一个例外:如果请求使用 HEAD 动词,并且没有相应的 OnHead 处理程序,则 Razor Pages 将改为执行 OnGet 处理程序(如果存在)。

至此,我们已经介绍了将请求 URL 映射到 Razor 页面并使用路由基础结构生成 URL,但是我们一直使用的大多数 URL 都有些丑陋。 如果在您的 URL 中看到大写字母让您感到困扰,那么下一部分适合您。 在下一节中,我们将自定义您的应用程序用于生成路由模板的约定。

使用 Razor 页面自定义约定

Razor Pages 建立在一系列旨在减少您需要编写的样板代码量的约定之上。 在本节中,您将看到一些自定义这些约定的方法。 通过自定义 Razor Pages 在应用程序中使用的约定,您可以完全控制应用程序的 URL,而无需手动自定义每个 Razor Page 的路由模板。

默认情况下,ASP.NET Core 生成的 URL 与 Razor 页面的文件名非常匹配。 例如,位于路径 Pages/Products/ProductDetails.cshtml 的 Razor 页面将对应于路由模板 Products/ProductDetails

如今,在 URL 中看到大写字母并不常见。 类似地,URL 中的单词通常使用“kebab-case”而不是“PascalCase”来分隔,例如,使用 product-details 而不是 ProductDetails。 最后,确保您的 URL 始终以斜杠结尾也很常见,例如,/product-details/ 而不是 /product-details。 Razor Pages 使您可以完全控制应用程序用于生成路由模板的约定,但这是我进行的两个常见更改。

以下清单显示了如何确保 URL 始终为小写并且始终带有斜杠。 您可以通过在 Startup.cs 中配置 RouteOptions 对象来更改这些约定。 此对象包含整个 ASP.NET Core 路由基础结构的配置,因此您所做的任何更改都将应用于 Razor Pages 和 MVC。

清单 5.7 在 Startup.cs 中使用 RouteOptions 配置路由约定

public void ConfigureServices(IServiceCollection services)
{
    //添加标准的 Razor Pages 服务。
    services.AddRazorPages();
    //通过提供配置方法来配置 RouteOptions 对象。
    services.Configure<RouteOptions>(options =>
                                     {
                                         //您可以更改用于生成 URL 的约定。 默认情况下,这些属性为 false。
                                         options.AppendTrailingSlash = true;
                                         options.LowercaseUrls = true;
                                         options.LowercaseQueryStrings = true;
                                     });
}

要在您的应用程序中使用 kebab-case,您必须创建一个自定义参数转换器。 这是一个有点高级的话题,但在这种情况下实现起来相对简单。 以下清单显示了如何创建一个参数转换器,该转换器使用正则表达式将生成的 URL 中的 PascalCase 值替换为 kebab-case。

清单 5.8 创建一个 kebab-case 参数转换器

//创建一个实现参数转换器接口的类。
public class KebabCaseParameterTransformer
    : IOutboundParameterTransformer
    {
        public string TransformOutbound(object value)
        {
            if (value == null) return null; //防范空值以避免运行时异常。
            //正则表达式用 kebab-case 替换 PascalCase 模式。
            return Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2").ToLower();
        }
    }

您可以使用 Startup.cs 中的 AddRazorPagesOptions 扩展方法在应用程序中注册参数转换器。 此方法链接在 AddRazorPages 方法之后,可用于完全自定义 Razor Pages 使用的约定。 以下清单显示了如何注册 kebab-case 转换器。它还显示了如何为给定的 Razor 页面添加额外的页面路由约定。

清单 5.9 使用 RazorPagesOptions 注册参数转换器

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages()
        //AddRazorPagesOptions 可用于自定义 Razor 页面使用的约定。
        .AddRazorPagesOptions(opts =>
                              {
                                  //将参数转换器注册为所有 Razor 页面使用的约定
                                  opts.Conventions.Add(
                                      new PageRouteTransformerConvention(
                                          new KebabCaseParameterTransformer()));
                    //AddPageRoute 向 Pages/Search/Products/StartSearch.cshtml 添加了一个额外的路由模板。
                                  opts.Conventions.AddPageRoute(
                                      "/Search/Products/StartSearch", "/search-products");
                              });
}

AddPageRoute 约定添加了执行单个 Razor 页面的替代方法。与使用 @page 指令自定义 Razor 页面的路由模板不同,使用 AddPageRoute 会向页面添加额外的路由模板,而不是替换默认值。 这意味着有两个路由模板可以访问该页面。

还有许多其他方法可以自定义 Razor Pages 应用程序的约定,但大多数情况下这不是必需的。 如果您确实发现需要以某种方式自定义应用程序中的所有页面,例如为每个页面的响应添加额外的标题,则可以使用自定义约定。

约定是 Razor Pages 的一个关键特性,你应该尽可能地依赖它们。 虽然您可以手动覆盖单个 Razor 页面的路由模板,但正如您在前面部分中看到的那样,我建议尽可能不要这样做。 尤其,

  • 避免在页面的 @page 指令中用绝对路径替换路由模板。
  • 避免将文字段添加到 @page 指令。 而是依赖文件层次结构。
  • 避免使用 AddPageRoute 约定向 Razor 页面添加其他路由模板。 拥有多种访问页面的方式有时会让人感到困惑。
  • 将路由参数添加到@page 指令以使您的路由动态化,例如@page {name}
  • 当您想要更改所有 Razor 页面的路由模板时,请考虑使用全局约定,例如使用 kebab-case,如您在上一节中所见。

简而言之,这些规则相当于“遵守约定”。 如果不这样做,危险在于您可能会意外创建两个具有重叠路线模板的 Razor 页面。 不幸的是,如果您最终处于这种情况,您将不会在编译时收到错误消息。 相反,当您的应用程序接收到与多个路由模板匹配的请求时,您将在运行时收到异常,如图 5.11 所示。

图 5.11 如果多个 Razor 页面注册了重叠的路由模板,当路由器无法确定选择哪一个时,您将在运行时收到异常。
image

总结

  • 路由是将传入请求 URL 映射到将执行以生成响应的 Razor 页面的过程。 您可以使用路由将 URL 与项目中的文件分离,并将多个 URL 映射到同一个 Razor 页面。
  • ASP.NET Core 使用两个中间件进行路由。 通过调用 UseRouting()Startup.cs 中添加 EndpointRoutingMiddleware,通过调用 UseEndpoints() 添加 EndpointMiddleware
  • EndpointRoutingMiddleware 通过使用路由匹配请求 URL 来选择应该执行的端点。 EndpointMiddleware 执行端点。
  • 在对 UseRouting()UseEndpoints() 的调用之间放置的任何中间件都可以判断哪个端点将为请求执行。
  • 路由模板定义应用程序中已知 URL 的结构。它们是带有变量占位符的字符串,变量可以包含可选值并映射到 Razor 页面或 MVC 控制器操作。
  • 路由参数是从请求的 URL 中提取的变量值。
  • 路由参数可以是可选的,并且可以在缺少值时使用默认值。
  • 路由参数可以具有限制允许的可能值的约束。如果路由参数与其约束不匹配,则路由不被视为匹配项。
  • 不要将路由约束用作通用输入验证器。 使用它们来消除两条相似路线之间的歧义。
  • 使用 catch-all 参数将 URL 的其余部分捕获到路由值中。
  • 您可以使用路由基础结构为您的应用程序生成内部 URL。
  • IUrlHelper 可用于根据操作名称或 Razor 页面将 URL 生成为字符串。
  • 您可以使用 RedirectToAction 和 RedirectToPage 方法生成 URL,同时生成重定向响应。
  • LinkGenerator 可用于从应用程序中的其他服务生成 URL,您无法访问 HttpContext 对象。
  • 执行 Razor 页面时,会根据请求的 HTTP 动词和处理程序路由值的值调用单个页面处理程序。
  • 如果请求没有页面处理程序,则使用隐式页面处理程序来呈现 Razor 视图。
  • 您可以通过配置 RouteOptions 对象来控制 ASP.NET Core 使用的路由约定,例如强制所有 URL 为小写,或始终附加尾部斜杠。
  • 您可以通过在 Startup.cs 中的 AddRazorPages() 之后调用 AddRazorPagesOptions() 来为 Razor Pages 添加其他路由约定。 这些约定可以控制路由参数的显示方式,或者可以为特定的 Razor 页面添加额外的路由模板。
  • 在可能的情况下,避免为 Razor 页面自定义路由模板,而是依赖约定。
posted @ 2022-09-03 17:35  F(x)_King  阅读(795)  评论(0编辑  收藏  举报