第5章 使用路由将URL映射到Razor Pages(ASP.NET Core in Action, 2nd Edition)

发布时间 2023-04-10 11:58:15作者: 码农小白修炼记

本章包括(请点击这里阅读其他章节)

  • 将 URL 映射到 Razor 页面
  • 使用约束和默认值匹配 URL
  • 从路由参数生成 URL

在第 4 章中,您了解了 MVC 设计模式,以及 ASP.NET Core 如何使用它为使用 Razor Pages 的应用程序生成UI。Razor Pages 包含类似小型控制器的页面处理程序。页面处理程序调用应用程序模型以检索或保存数据。然后,处理程序将数据从应用程序模型传递到Razor 视图,后者生成HTML响应。

虽然不是 MVC 设计模式的一部分,但 Razor Pages 的一个关键部分是根据给定请求选择调用哪个 Razor Page。这个过程叫做路由,是本章的重点。

本章首先确定路由的需要以及它的有用之处。您将了解 ASP.NET Core 3.0 中引入的端点路由系统,查看路由技术的几个示例,并探索 Razor Page 文件布局与公开的 URL 之间的不同路由方式。

本章的主要内容是如何使用 Razor Pages 的路由来创建动态 URL,以便单个 Razor Page 可以处理对多个URL的请求。我将向您展示如何构建强大的路由模板,并让您体验其中的可用选项。

在第 5.5 节中,我描述了如何使用路由系统生成 URL,您可以使用它为应用程序创建链接和重定向请求。使用路由系统的好处之一是它将 Razor Pages 与用于执行它们的底层 URL 分离。您可以使用URL生成来避免将代码与诸如 /Product/View/3 之类的硬编码 URL 混在一起。相反,您可以根据路由系统在运行时生成 URL。这样做的好处是,它使更改 Razor Page 的 URL 配置更加容易。不必在使用 Razor Page 的URL时到处查找,URL 将自动为您更新,无需进行其他更改。

本章通过描述如何自定义 Razor Pages 使用的约定,从而让您完全控制应用程序使用的 URL。您将看到如何更改内置约定,例如对 URL 使用小写,以及如何编写自己的约定并将其全局应用于应用程序。

在本章结束时,您应该更加清楚地了解 ASP.NET Core 应用程序的工作原理。您可以将路由视为将中间件管道与 Razor Pages 和 MVC 框架联系起来的粘合剂。有了中间件、Razor Pages 和路由,您很快就能编写 web 应用程序!

5.1 什么是路由?

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

在第3章中,您看到 ASP.NET Core 应用程序包含一个中间件管道,它定义了应用程序的行为。中间件非常适合处理跨领域的问题,如日志记录和错误处理,以及关注范围较窄的请求,如对图像和 CSS 文件的请求。

为了处理更复杂的应用程序逻辑,您通常会在中间件管道的末尾使用 EndpointMiddleware (端点中间件),如第4章所示。这个中间件可以通过调用一个方法(Razor page 上的页面处理程序或 MVC 控制器上的操作方法)来处理适当的请求,并使用处理结果生成响应。

我在第4章中提到的一个方面是如何在收到请求时选择要执行的 Razor Page 或操作方法。是什么使请求“适合”给定的 Razor Page 处理程序?将请求映射到处理程序的过程称为路由。

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

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

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

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

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

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

定义:查询字符串是 URL 的一部分,该URL包含不完全是路径的其他数据。路由基础结构不使用它来标识要执行的操作,但它可以用于模型绑定,如第6章所示。

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

图5.2 如果您使用基于文件布局的映射,则需要为产品系列中的每个产品提供不同的Razor Page。通过路由,多个URL映射到一个Razor Page,并且一个动态参数捕获URL中的差异。

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

幸运的是,通过路由可以很容易地修改公开的URL,而无需更改Razor Page文件名或位置。根据您的路由配置,您可以将指向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中的路由在实践中是如何工作的。

5.2 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 Page的路由就可以了。不幸的是,将路由限制到MVC基础架构会使一些事情变得有些混乱。这意味着一些跨领域的关注点,如授权,仅限于MVC基础架构,很难从应用程序中的其他中间件中使用。这种限制导致了不可避免的重复,这并不理想。

在ASP.NET Core 3.0中,引入了一种新的路由系统,即端点路由(endpoint routing)。端点路由使路由系统成为ASP.NET Core的一个更基本的特性,不再将其与MVC基础架构联系在一起。Razor Pages和MVC仍然依赖端点路由,但现在其他中间件也可以使用它。ASP.NET Core 5.0使用与ASP.NET Core 3.0相同的端点路由系统。

在本节中,我将介绍

  • 端点路由在ASP.NET Core中的工作原理
  • 可用的两种路由类型:基于约定的路由和属性路由
  • Razor Pages的路由工作原理

在本节结束时,您应该对ASP.NET Core中的路由如何与Razor Pages一起工作有了很好的了解。

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

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

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

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

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

要在应用程序中注册端点,请在Startup.cs的Configure方法中调用UseEndpoints。此方法执行一个配置lambda操作,该操作定义应用程序中的端点,如下面的列表所示。您可以使用MapRazorPages等扩展名在应用程序中自动注册所有Razor Page。此外,可以使用MapGet等方法显式注册其他端点。

//将EndpointRoutingMiddleware添加到中间件管道中。 app.UseEndpoints(endpoints => //将EndpointMiddleware添加到管道中,并提供配置lambda。 { endpoints.MapRazorPages(); //将应用程序中的所有Razor Pages注册为端点。 endpoints.MapHealthChecks("/healthz"); //在route/healthz注册health-check端点。 //内联注册一个返回“Hello World!”的端点在路由/test中。 endpoints.MapGet("/test", async context => { await context.Response.WriteAsync("Hello World!"); }); }); }

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

定义:route template是用于匹配请求URL的URL模式。它们是固定值的字符串,如前一个列表中的“/test”。它们还可以包含变量的占位符,如第5.3节所示。

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

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

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

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

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

注:只有放置在RoutingMiddleware之后的中间件才能检测将要执行的端点。

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

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

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

提示:AuthorizationMiddleware需要知道将执行哪个端点,因此必须将其放置在中间件管道中的RoutingMiddleware之后和EndpointMiddleware之前。我将在第15章中更详细地讨论授权。

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

我们已经介绍了RoutingMiddleware和EndpointMiddleware如何交互以在ASP.NET Core中提供路由功能,但我们还没有研究RoutingMiddleware如何将请求URL与端点匹配。在下一节中,我们将研究ASP.NET Core中使用的两种不同方法。

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

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

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

您使用的方法通常取决于您使用的是Razor Pages还是MVC控制器,以及您是在构建API还是网站(使用HTML)。最近我非常倾向于属性路由,您很快就会看到。

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

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

有些令人困惑的是,Razor Pages使用约定来生成属性路由!在许多方面,这种组合提供了两全其美的效果。通过属性路由的简单定制,您可以获得基于约定的路由的可预测性和简洁性。如表5.1所示,每种方法都有各自的特点。

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

 

 

 

 

基于约定的路由

HTML生成MVC控制器

在您的应用程序中的一个位置有非常简洁的定义

强制MVC控制器的一致布局

强制MVC控制器的一致布局

路由在与控制器不同的位置定义。

在路由请求时添加额外的间接层

属性路由

Web API MVC控制器

为每个操作提供对路由模板的完全控制。

路由在它们执行的操作旁边定义。

与基于约定的路由相比更为详细

可以轻松过度自定义路由模板。

路由模板分散在整个应用程序中,而不是在一个位置。

基于约定的属性路由生成

Razor Pages

鼓励一致的公开URL集

当你坚持惯例的时候,你会感到恐惧。

轻松地覆盖单个页面的路线模板。

全局自定义约定以更改公开的URL。

可能过度自定义路由模板

您必须计算页面的路由模板,而不是在应用程序中明确定义。

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

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

注:出于上述原因,本书重点介绍属性路由。实际上,本节中描述的所有功能也适用于基于约定的路由。有关基于约定的路由的详细信息,请参阅Microsoft的“ASP.NET Core中的控制器操作路由”文档:http://mng.bz/ZP0O.

无论使用哪种技术,都将使用route templates定义应用程序的预期URL。它们定义了您期望的URL的模式,其中的占位符可能有所不同。

定义:route templates定义应用程序中已知URL的结构。它们是包含可选值的变量占位符的字符串。

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

定义:segment是URL的一个小的连续部分。它与其他URL段至少有一个字符分隔,通常用/字符分隔。路由包括将URL的段与路由模板匹配。

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

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

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

5.2.3 路由到Razor页面

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

app.UseEndpoints(endpoints =>
{
  endpoints.MapRazorPages();
});

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

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

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

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

在第4章中,您了解到Razor Page处理程序是在Razor页面上调用的方法。当我们说“执行Razor Page”时,我们实际上是指“创建Razor Page PageModel的实例,并调用该模型上的页面处理程序。”Razor Pages可以有多个页面处理程序,因此一旦RoutingMiddleware选择Razor Page,EndpointMiddleware仍需要选择要执行的处理程序。您将在第5.6节中了解框架如何选择要调用的页面处理程序。

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

  • "ToDo"
  • "ToDo/Index"

当这些路由中的任何一个匹配时,将选择相同的Index.cshtml Razor Page。例如,如果应用程序在URL处运行https://example.org,可以通过执行https://example.org/ToDo或https://example.org/ToDo/Index。

最后一个示例是,当您使用Visual Studio创建Razor Pages应用程序或使用.NET CLI运行dotnet新web时,请考虑默认创建的Razor Page,正如我们在第2章中所做的那样。标准模板在Pages目录中包含三个Razor页:

  • 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页面。这就是路由模板和路由数据的来源,显示了路由的真正威力。

5.3 自定义Razor页面路由模板

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

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

 

图5.6显示文本段和两个所需路由参数的简单路由模板

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

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

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

提示:ASP.NET Core中的文字段不区分大小写。

假设您的应用程序中有一个联系人页面,路径为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—Where category=bags and name=rucksack-a
  • /shoes/black-size9—Where category=shoes and name=black-size9

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

  • /socks/—No name parameter specified
  • /trousers/mens/formal—Extra URL segment, formal, not found in route template

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

定义:路由值是基于给定路由模板从URL中提取的值。模板中的每个路由参数都将有一个相关的路由值,它们作为字符串对存储在字典中。它们可以在模型绑定期间使用,如第6章所示。

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

5.3.1 向Razor Page路由模板添加段

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

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

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

@page "Extra"

这会将提供的路由模板附加到为Razor Page生成的默认模板。例如,Razor Page的默认路由模板位于Pages/Privacy.html是“Privacy”。根据前面的指令,页面的新路由模板将是“Privacy/Extra”。

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

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

@page "{category}/{name}"

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

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

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

5.3.2 完全更换Razor Page路线模板

如果您能够尽可能遵守默认的路由约定,并在必要时为路由参数添加额外的段,那么使用Razor Pages将是最有效的。但有时你只需要更多的控制。对于应用程序中的重要页面,例如电子商务应用程序的结账页面,甚至是产品页面,通常都是如此,正如您在上一节中所看到的。

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

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

请注意,此指令在路线开始处包含“/”,表示这是一个自定义路线模板,而不是添加。此页面的路由模板将是“{类别}/{名称}”,无论它应用于哪个Razor页面。

类似地,您可以通过以“/”开头并仅使用文字段来创建页面的静态自定义模板。例如

@page "/checkout"

无论您将签出Razor Page放置在Pages文件夹中的何处,使用此指令都可以确保它始终具有路由模板“checkout”,因此它将始终与请求URL /checkout相匹配。

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

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

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

在本节中,您学习了如何自定义Razor页面的路由模板。在下一节中,我们将更深入地研究路由模板语法和一些其他可用功能。

5.4 探索路由模板语法

除了文本和路由参数段的基本元素之外,路由模板还有额外的功能,可以让您更好地控制应用程序的URL。这些功能允许您具有可选的URL段,在未指定段时提供默认值,或对给定路由参数有效的值设置附加约束。本节将介绍这些功能以及应用它们的方法。

5.4.1 使用可选值和默认值

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

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

文字产品段和所需的{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

Route values

/product/shoes/formal/3

/product/shoes/formal

/product/shoes

/product/bags/satchels

/product/phones

/product/computers/laptops/ABC-123

category=shoes, name=formal, id=3 category=shoes, name=formal category=shoes, name=all category=bags, name=satchels category=phones, name=all

category=computes, name=laptops, id=ABC-123

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

{category?}/{name}

现在,尝试想想一个URL,它将指定{name}参数,但不指定{category}。这是不可能的!这样的模式不会导致错误;category参数基本上是必需的,尽管您已将其标记为可选的。

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

• /product/shoes
• /product/shoes/all

这两个URL都将执行相同的Razor Page,具有相同的路由值category=shoes和name=all。通过使用默认值,您可以在应用程序中使用较短且更容易记住的URL作为常用URL,但仍然可以灵活地在单个模板中匹配各种路由。

5.4.2 向路由参数添加附加约束

通过定义路由参数是必需的还是可选的,以及它是否具有默认值,您可以使用非常简洁的模板语法来匹配范围广泛的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,您在第4章中简要介绍了这个过程(我们将在下一章详细讨论)。例如,具有处理程序public void OnGet(int id) 的Razor Page将从id路由值获取id参数。如果id路由参数最终从URL中分配了一个非整数值,那么当它绑定到整数id参数时,您将得到一个异常。

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

可以将大量路由约束应用于路由模板,以确保路由值可以转换为适当的类型。还可以检查更高级的约束,例如,整数值具有特定的最小值,或者字符串值具有最大长度。表5.3描述了一些可能的限制条件,但您可以在Microsoft的“ASP.NET Core中的路由”文档中找到更完整的列表,网址为http://mng.bz/xmae.

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

约束

实例

匹配案例

描述

int

{qty:int}

123, -123, 0

匹配任意整数

Guid

{id:guid}

d071b70c-a812-4b54-87d2-7769528e2814

匹配任何Guid(全局唯一标识符)

decimal

{cost:decimal}

29.99, 52, -1.01

匹配任何小数

min(value)

{age:min(18)}

18, 20

匹配18或更大的整数值

length(value)

{name:length(6)}

andrew,123456

匹配长度为6的字符串值

optional int

{qty:int?}

123, -123, 0, null

可选匹配任何整数

optional int max(value)

{qty:int:max(10)?}

3, -123, 0, null

可选地匹配10或更小的任何整数

提示:如表5.3所示,还可以通过用冒号分隔约束来组合多个约束。

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

警告:不要使用路由约束来验证常规输入,例如检查电子邮件地址是否有效。这样做会导致404“找不到页面”错误,这会让用户感到困惑。

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

属性路由中的约束和排序
如果您的应用程序有一组设计良好的URL,您可能会发现实际上不需要使用路由约束。当您有“重叠”的路由模板时,路由约束确实很有用。
例如,假设您有一个Razor Page,其路由模板为“{number}/{name}”,另一个模板为“{product}/{id}”。当带有URL /shoes/123的请求到达时,选择哪个模板?它们都匹配,因此路由中间件会恐慌并抛出异常。不理想。
使用约定可以解决这个问题。例如,如果您将第一个模板更新为“{number:int}/{name}”,那么整数约束意味着URL不再匹配,路由中间件可以正确选择。但是,请注意,URL /123/Shoes仍然与两个路线模板匹配,因此您不会脱离困境。
通常,您应该避免像这样的重叠路线模板,因为它们通常会令人困惑,而且麻烦比它们的价值更大。
属性路由(Razor Pages和MVC控制器用于构建API)允许您显式控制路由中间件查看路由模板的顺序,也可用于解决上述问题。然而,如果您发现自己需要手动控制顺序,这是一个非常强烈的指示,表明您的URL令人困惑
如果您的路由模板定义良好,使得每个URL只映射到一个模板,ASP.NET Core路由将毫无困难地工作。尽可能坚持固有的惯例是保持幸福的最佳方式!

我们将结束对路由模板的研究,但在继续之前,还有一种类型的参数需要考虑:catch-all参数。

5.4.3 使用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 Page实现这一点,如图5.8所示。

图5.8 您可以使用catch all参数来匹配URL的其余部分。Catch all参数可以包含“/”字符,也可以是空字符串。

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

提示:捕获所有参数都将捕获URL的全部不匹配部分。在可能的情况下,为了避免混淆,请避免使用与其他路由模板重叠的“捕获所有”参数来定义路由模板。

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

您了解到,将URL正确映射到Razor Pages只是ASP.NET Core中路由系统的一半职责。它还用于生成URL,以便您可以从应用程序的其他部分轻松引用Razor Pages。

5.5 根据路由参数生成URL

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

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

尝试在你的应用程序中手动管理这些链接会导致管理复杂、链接断开和404。如果您的URL是硬编码的,那么您必须记住查找并替换每个重命名!

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

图5.9 路由和URL生成之间的比较。路由采用URL并生成路由值,但URL生成使用路由值来生成URL。

5.5.1 为Razor Page生成URL

您需要在应用程序的不同位置生成URL,一个常见的位置是Razor Pages和MVC控制器。下面的列表显示了如何使用PageModel基类的Url助手生成Pages/Currency/View.cshtml Razor Page的链接。

//您可以提供Razor页面的相对路径,以及任何其他路由值。 } }

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

如果需要传递多个路由值,可以向匿名对象添加其他属性。然后,助手将基于引用页面的路由模板生成URL。

提示:您可以提供Razor Page的相对文件路径,如清单5.2所示。或者,您可以通过以“/”开头的路径来提供绝对文件路径(相对于Pages文件夹),例如,“/Currency/View”。

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

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

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

如果一个路由没有像“Currency/View”模板中那样显式包含路由值,那么路由值将作为查询字符串的一部分作为附加数据附加,例如/Currency/View?code=GBP。

基于要执行的页面生成URL是很方便的,这是大多数情况下采用的常用方法。如果您正在为API使用MVC控制器,则过程与Razor Pages的过程大致相同,但方法略有不同。

5.5.2 为MVC控制器生成URL

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

//提供操作和控制器名称以及任何其他路由值。 return Content($"The URL is {url}"); //这将返回“URL为/Currency/View/USD”。 } [HttpGet("currency/view/{code}")] public IActionResult View(string code) //生成的URL将路由到View操作方法。 { /* 方法实现 */ } }

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

提示:不要使用字符串作为操作方法的名称,而是使用C#6 nameof运算符使值重构安全,例如nameof(View)。

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

定义:环境值是当前请求的路由值。当从MVC控制器调用时,它们包括控制器和动作,但也可以包括在最初使用路由定位动作或Razor Page时设置的其他路由值。有关详细信息,请参阅Microsoft的“ASP.NET Core中的路由”文档:http://mng.bz/xmae.

使用Url属性生成Url在实践中并不常见。相反,更常见的是使用ActionResult隐式生成URL。

5.5.3 使用ActionResults生成URL

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

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

//RedirectToPage方法使用生成的URL生成RedirectToPageResult } }

您可以使用类似的方法RedirectToAction来自动重定向到MVC操作。与Page和Action方法一样,它是控制是否需要使用RedirectToPage或RedirectToAction的目标。只有在使用MVC控制器生成HTML而不是Razor Pages时,RedirectToAction才是必要的。

提示:我建议您使用Razor Pages而不是MVC控制器来生成HTML。有关Razor Pages的好处的讨论,请参阅第4章。

除了从Razor Pages和MVC控制器生成URL之外,您通常会发现在视图中构建HTML时需要生成URL。这是在web应用程序中提供导航链接所必需的。当我们在第8章中查看Razor Tag Helpers时,您将看到如何实现这一点。

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

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

如果你按照第4章的建议编写Razor Pages和MVC控制器,你应该尽量保持Razor Page相对简单。这需要您在单独的类和服务中执行应用程序的业务和域逻辑。在大多数情况下,应用程序使用的URL不应该是域逻辑的一部分。这使您的应用程序更容易随时间演变,甚至完全改变。例如,您可能希望创建一个移动应用程序,以重用ASP.NET Core应用程序中的业务逻辑。在这种情况下,在业务逻辑中使用URL是没有意义的,因为当从移动应用程序调用该逻辑时,它们是不正确的!

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

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

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

LinkGenerator有各种生成URL的方法,如GetPathByPage、GetPathByAction和GetUriByPage,如下表所示。使用这些方法有一些微妙之处,特别是在复杂的中间件管道中,所以尽可能使用IUrlHelper,如果有问题,请务必查阅文档。

清单5.5 使用LinkGeneratorClass生成URL

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

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

到目前为止,在本章中,我们已经广泛地研究了传入请求如何路由到Razor Pages,但我们还没有真正看到页面处理程序是如何处理的。在下一节中,我们将讨论页面处理程序,以及如何在Razor页面上拥有多个处理程序。

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

在本章开始时,我说过路由是关于将URL映射到处理程序。对于Razor Pages,这意味着一个页面处理程序,但到目前为止,我们只讨论了基于Razor Page的路由模板的路由。在本节中,您将了解EndpointMiddleware在执行Razor Page时如何选择要调用的页面处理程序。您在第4章中了解了页面处理程序,以及它们在Razor Pages中的作用,但我们尚未讨论如何为给定请求选择页面处理程序。Razor Pages可以有多个处理程序,因此如果RoutingMiddleware选择Razor Page,EndpointMiddleware仍然需要知道如何选择要执行的处理程序。

考虑下表中显示的Razor PageSearchModel。此Razor Page有三个处理程序:OnGet、OnPostAsync和OnPostCustomSearch。处理程序方法的主体没有显示,因为此时我们只关心RoutingMiddleware如何选择要调用的处理程序。

图5.10 Razor Page处理程序与基于HTTP谓词和可选处理程序参数的请求匹配

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

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

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

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

public void OnDelete() { }

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

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

目前,我们已经介绍了将请求URL映射到Razor Pages并使用路由基础结构生成URL,但我们使用的大多数URL都有点难看。如果在URL中看到大写字母让您感到困扰,那么下一节将为您准备。在下一节中,我们将自定义应用程序用于生成路由模板的约定。

5.7 使用Razor Pages自定义约定

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

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

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

下面的列表显示了如何确保URL始终是小写的,并且始终有一个斜杠。您可以通过在Startup.cs中配置RouteOptions对象来更改这些约定。此对象包含整个ASP.NET Core路由基础结构的配置,因此所做的任何更改都将应用于Razor Pages和MVC。您将在第10章中了解有关配置选项的更多信息。

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

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

//防止空值以避免运行时异常。 return Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2").ToLower(); //正则表达式将PascalCase模式替换为kebab-case。 } }

您可以使用Startup.cs中的AddRazor PagesOptions扩展方法在应用程序中注册参数转换器。此方法链接在AddRazor Pages方法之后,可用于完全自定义Razor Page使用的约定。下面的列表显示了如何注册烤肉串案例转换器。它还显示了如何为给定的Razor页面添加额外的页面路由约定。

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

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

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

还有许多其他方法可以为Razor Pages应用程序自定义约定,但大多数情况下这不是必需的。如果您确实需要以某种方式自定义应用程序中的所有页面,例如在每个页面的响应中添加一个额外的标题,则可以使用自定义约定。Microsoft的“ASP.NET Core中的Razor Pages路由和应用程序约定”文档包含所有可用内容的详细信息:http://mng.bz/A0BK.

惯例是Razor Pages的一个关键特性,您应该尽可能依赖它们。虽然您可以手动覆盖各个Razor Pages的路由模板,正如您在前几节中所看到的,但我建议在可能的情况下不要这样做。特别地,

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

简而言之,这些规则相当于“遵守惯例”。如果你不遵守,危险是你可能会意外地创建两个Razor Pages,它们具有重叠的路由模板。不幸的是,如果您最终遇到这种情况,那么在编译时不会出现错误。相反,当应用程序收到与多个路由模板匹配的请求时,您将在运行时得到一个异常,如图5.11所示。

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

恭喜你,你已经完成了关于路由的详细讨论!路由是人们在构建应用程序时经常遇到的问题之一,这可能会令人沮丧。当我在第9章中描述如何创建WebAPI时,我们将再次讨论路由,但请放心,您已经在本章中介绍了所有棘手的细节!

在第6章中,我们将深入研究模型绑定。您将看到在路由过程中生成的路由值如何绑定到您的操作方法(MVC)或页面处理程序(Razor Pages)参数,也许更重要的是,如何验证您提供的值。

总结

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

(请点击这里阅读其他章节)