ASP.NET 路由

更新:2007 年 11 月

ASP.NET 路由使您可以使用不必映射到网站中特定文件的 URL。由于 URL 不必映射到文件,所以可以在 Web 应用程序中使用 URL,这些 URL 是描述性的用户操作,因此更易于被用户理解。

在一个不使用路由的 ASP.NET 应用程序中,对 URL 的传入请求通常映射到磁盘上的物理文件,如 .aspx 文件。例如,对 https://server/application/Products.aspx?id=4 的请求映射到名为 Products.aspx 的文件,该文件包含代码和标记用于呈现对浏览器的响应。网页使用 id=4 的查询字符串值来决定显示的内容类型,但是该值对用户可能意义不大。

在 ASP.NET 路由中,您可以定义 URL 模式,该模式包含在处理 URL 请求时使用的值的占位符。在运行时,应用程序名称后面的 URL 部分根据您所定义的 URL 模式分析为离散值。例如,在请求 https://server/application/Products/show/beverages 时,路由分析器可以将值 Products、show 和 beverages 传递给请求的处理程序。相反,在一个不由 URL 路由管理的请求中,/Products/show/beverages 片断将被解释为应用程序中一个文件的路径。

还可以使用 URL 模式通过编程方式来创建对应于路由的 URL。这使您能够集中逻辑用于创建 ASP.NET 应用程序中的超链接。

ASP.NET 路由与 URL 重写

ASP.NET 路由不同于其他 URL 重写方案。URL 重写通过在将请求发送到网页之前实际更改 URL 来处理传入请求。例如,一个使用 URL 重写的应用程序可能会将 URL 从 /Products/Widgets/ 更改为 /Products.aspx?id=4。此外,URL 重写通常没有相应的 API 来创建基于模式的 URL。在 URL 重写中,如果更改了 URL 模式,则必须手动更新包含原始 URL 的所有超链接。

由于 ASP.NET 路由可以从 URL 提取值,所以处理传入请求时不更改 URL。如果必须创建一个 URL,则将参数值传递到为您生成 URL 的方法中。若要更改 URL 模式,请在某位置更改该模式,您在应用程序中创建的基于该模式的所有链接将自动使用新模式。

定义 URL 路由

定义的 URL 模式称作“路由”。在路由中,您可以指定占位符,用于映射到从 URL 请求中分析的值。您还可以指定用于匹配 URL 请求的常量值。

在路由中,您可以通过用大括号( { 和 })括住占位符来定义占位符(称为“URL 参数”)。分析 URL 时将 / 字符解释为分隔符。将路由定义中不是分隔符和不在大括号中的信息视为一个常量值。将从两个分隔符之间提取的值分配给占位符。

您可以在分隔符之间定义多个占位符,但必须用一个常量值分隔开。例如,{language}-{country}/{action} 是有效的路由模式。但是,由于占位符之间没有常量或分隔符,所以 {language}{country}/{action} 不是有效的模式。因此,路由无法确定在哪里将 language 占位符的值与 country 占位符的值分隔开。

下表演示有效的路由模式和一些与模式匹配的 URL 请求的示例。

路由定义

匹配 URL 示例

{controller}/{action}/{id}

/Products/show/beverages

{table}/Details.aspx

/Products/Details.aspx

blog/{action}/{entry}

/blog/show/123

{reporttype}/{year}/{month}/{day}

/sales/2008/1/5

{locale}/{action}

/zh-cn/show

{language}-{country}/{action}

/zh-cn/show

通常情况下,您在 Global.asax 文件中 Application_Start 事件的处理程序调用的方法中添加路由。该方法可确保应用程序启动时路由可用。还使您能够在对应用程序进行单元测试时直接调用方法。如果您想在对应用程序进行单元测试时直接调用一个注册路由的方法,则该方法必须是静态的(Visual Basic 中的 Shared),并且必须具有一个 RouteCollection 参数。

您可以通过将路由添加到 RouteTable 类的静态 Routes 属性来添加路由。Routes 属性是一个 RouteCollection 对象,存储 ASP.NET 应用程序的所有路由。下面的示例演示从 Global.asax 文件中添加一个 Route 对象的代码,该对象定义了名为 action 和 categoryName 的两个 URL 参数。

Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
    RegisterRoutes(RouteTable.Routes)
End Sub

Shared Sub RegisterRoutes(routes As RouteCollection)
    Dim urlPattern As String
    Dim categoryRoute As Route

    urlPattern = "Category/{action}/{categoryName}"
    categoryRoute = New Route(urlPattern, New CategoryRouteHandler)

    routes.Add(categoryRoute)
End Sub
protected void Application_Start(object sender, EventArgs e)
{
    RegisterRoutes(RouteTable.Routes);
}

public static void RegisterRoutes(RouteCollection routes)
{
    routes.Add(new Route
    (
         "Category/{action}/{categoryName}"
         , new CategoryRouteHandler()
    ));
}

为路由参数设置默认值

定义路由时可以为参数分配一个默认值。如果 URL 没有包括该参数的值,则会使用默认值。通过将字典分配给 Route 类的 Defaults 属性,可以设置路由的默认值。下面的示例演示一个包含默认值的路由。

Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
    RegisterRoutes(RouteTable.Routes)
End Sub

Shared Sub RegisterRoutes(ByVal routes As RouteCollection)
    Dim urlPattern As String
    Dim categoryRoute As Route

    urlPattern = "Category/{action}/{categoryName}"
    categoryRoute = New Route(urlPattern, New CategoryRouteHandler)
    categoryRoute.Defaults = New RouteValueDictionary(New With _
        {.categoryName = "food", _
         .action = "show"} )

    routes.Add(categoryRoute)
End Sub
void Application_Start(object sender, EventArgs e) 
{
    RegisterRoutes(RouteTable.Routes);
}

public static void RegisterRoutes(RouteCollection routes)
{
  routes.Add(new Route
  (
     "Category/{action}/{categoryName}"
          new CategoryRouteHandler()
  )
    {
       Defaults = new RouteValueDictionary 
           {{"categoryName", "food"}, {"action", "show"}}
     }
  );
}

ASP.NET 路由处理 URL 请求时,在示例中演示的路由定义(使用针对 categoryName 的 food 的默认值和针对 action 的 show 的默认值)得到下表列出的结果。

URL

参数值

/Category

action = "show"(默认值)

categoryName = "food"(默认值)

/Category/add

action = "add"

categoryName = "food"(默认值)

/Category/add/beverages

action = "add"

categoryName= "beverages"

处理可变数量的段

有时您需要处理包含可变数量的 URL 段的 URL 请求。定义路由时,通过将参数标记星号 (*) 可以指定最后一个参数应与 URL 的其余部分匹配。因而该参数称为“全部捕捉”参数。具有全部捕捉参数的路由也将与那些不包含最后一个参数的任意值的 URL 相匹配。下面的示例演示一个与未知数量的段匹配的路由模式。

query/{queryname}/{*queryvalues}

ASP.NET 路由处理 URL 请求时,在示例中演示的路由定义得到下表列出的结果。

URL

参数值

/query/select/bikes/onsale

queryname = "select"

queryvalues = "bikes/onsale"

/query/select/bikes

queryname = "select"

queryvalues = "bikes"

/query/select

queryname = "select"

queryvalues = Empty string

向路由添加约束

除了按照 URL 中的参数数量将 URL 请求匹配到路由定义中,还可以指定参数中的值满足特定约束。如果一个 URL 包含路由的约束以外的值,则该路由不用于处理请求。添加约束以确保 URL 参数包含将在应用程序中起作用的值。

约束是通过使用正则表达式或使用实现 IRouteConstraint 接口的对象来定义的。将路由定义添加到 Routes 集合时,同时也通过创建一个包含验证测试的 RouteValueDictionary 对象添加了约束。然后将此对象分配给 Constraints 属性。字典中的关键字标识约束适用的参数。字典中的值可以是表示正则表达式的字符串,也可以是实现 IRouteConstraint 接口的对象。

提供字符串后,路由将视字符串为正则表达式,并通过调用 Regex 类的 IsMatch 方法检查参数值是否有效。总是将正则表达式视为不区分大小写。有关更多信息,请参见 .NET Framework 正则表达式

提供 IRouteConstraint 对象后,ASP.NET 路由将通过调用 IRouteConstraint 对象的 Match 方法检查参数值是否有效。Match 方法返回一个布尔值,该值指示参数值是否有效。

下面的示例演示限制在 locale 和 year 参数中包含的值的约束。

Shared Sub RegisterRoutes(ByVal routes As RouteCollection)
    Dim urlPattern As String
    Dim reportRoute As Route

    urlPattern = "{locale}/{year}"
    reportRoute = New Route(urlPattern, New ReportRouteHandler)
    reportRoute.Constraints = New RouteValueDictionary(New With _
        {.locale = "[a-z]{2}-[a-z]{2}", .year = "\d{4}"})

    routes.Add(reportRoute) 
End Sub
void Application_Start(object sender, EventArgs e) 
{
    RegisterRoutes(RouteTable.Routes);
}

public static void RegisterRoutes(RouteCollection routes)
{
    routes.Add(new Route
    (
      "{locale}/{year}"
         , new ReportRouteHandler()
    )
       {
          Constraints = new RouteValueDictionary 
          {{"locale", "[a-z]{2}-[a-z]{2}"},{year, @"\d{4}"}}
       });
}

路由处理 URL 请求时,在上一示例中演示的路由定义生成下表列出的结果。

URL

结果

/zh-CN

无匹配。locale 和 year 都是必需的。

/zh-CN/08

无匹配。对 year 的约束需要 4 个数字。

/zh-CN/2008

locale = "zh-CN"

year = "2008"

没有应用路由的方案

默认情况下,路由不处理映射到 Web 服务器上现有物理文件的请求。例如,如果 Products/Beverages/Coffee.aspx 上存在物理文件,则路由不处理对 https://server/application/Products/Beverages/Coffee.aspx 的请求。即使匹配一个定义的模式,例如 {controller}/{action}/{id},路由也不处理该请求。

如果希望路由处理所有请求(包括指向文件的请求),可以通过将 RouteCollection 对象的 RouteExistingFiles 属性设置为 true 来覆盖默认行为。将该值设置为 true 后,与定义的模式匹配的所有请求都将由路由处理。

还可以指定路由不应处理某些 URL 请求。通过定义路由并指定应使用 StopRoutingHandler 类来处理该模式,来阻止路由处理某些特定请求。当 StopRoutingHandler 对象处理请求时,StopRoutingHandler 对象会阻止以任何其他方式将该请求处理为路由。相反,会将该请求处理为 ASP.NET 页、Web 服务或其他 ASP.NET 终结点。例如,您可以添加以下路由定义来阻止路由处理 WebResource.axd 文件的请求。

Shared Sub RegisterRoutes(ByVal routes As RouteCollection)
    routes.Add(New Route("{resource}.axd/{*pathInfo}", New StopRouteHandler()))
End Sub
public static void RegisterRoutes(RouteCollection routes)
{
  routes.Add(new Route("{resource}.axd/{*pathInfo}", new StopRouteHandler()));
}

URL 如何与路由匹配

路由在处理 URL 请求时,还尝试将请求的 URL 与路由匹配。将 URL 请求与路由匹配取决于以下所有条件:

  • 包括在项目类型中的已经定义的路由模式或默认路由模式(如果有的话)。

  • 将路由添加到 Routes 集合中的顺序。

  • 已经提供给路由的所有默认值。

  • 已经提供给路由的任意约束。

  • 是否定义路由来处理匹配物理文件的请求。

为避免错误的处理程序处理请求,必须在定义路由时考虑以上所有条件。出现在 Routes 集合中的 Route 对象的顺序是很重要的。路由将在集合的整个路由过程中一直尝试匹配。当匹配发生时,无法计算更多的路由。通常,按从路由定义的具体性递减的顺序将路由添加到 Routes 属性。

例如,假定您使用以下模式添加路由:

  • 路由 1:{controller}/{action}/{id}

  • 路由 2:products/show/{id}

由于先计算路由 1,并将一直匹配也适用于路由 2 的请求,所以路由 2 将永远不处理请求。对 https://server/application/products/show/bikes 的请求看起来更与路由 2 匹配,却由路由 1 使用以下值处理:

  • controller = products

  • action = show

  • id = bikes

如果请求缺少参数,则会使用默认值。因此,可能导致路由匹配意外的请求。例如,假定您使用以下模式添加路由:

  • 路由 1:{report}/{year}/{month},对于 year 和 month 使用默认值。

  • 路由 2:{report}/{year},对于 year 使用默认值。

路由 2 将永远不处理请求。路由 1 可能用于月度报表,而路由 2 可能用于年度报表。但是,路由 1 中的默认值意味着将匹配同时适用于路由 2 的所有请求。

可以通过在模式中包括例如 annual/{report}/{year} 和 monthly/{report}/{year}/{month} 的常量来避免二义性。

如果 URL 与在 RouteTable 集合中定义的任何 Route 对象都不匹配,ASP.NET 路由将不处理请求。相反,会将处理传递给 ASP.NET 页、Web 服务或其他 ASP.NET 终结点。

从路由创建 URL

在希望集中逻辑用于构造 URL 时可以使用路由来生成 URL。通过将参数值作为字典传递给 RouteCollection 对象的 GetVirtualPath 方法来创建 URL。GetVirtualPath 方法在 RouteCollection 对象中查找与字典中的参数匹配第一个路由。匹配路由用于生成 URL。下面的示例演示路由定义。

Shared Sub RegisterRoutes(ByVal routes As RouteCollection)
    routes.Add(New Route( _
      "Category/{action}/{categoryName}", _
      New RouteValueDictionary(New With _
          {.categoryName = "food", _
           .action = "show"}), _
           New CategoryRouteHandler()) )
End Sub
public static void RegisterRoutes(RouteCollection routes)
{
  routes.Add(new Route
  (
     "Category/{action}/{categoryName}"
          new CategoryRouteHandler()
  )
    {
       Defaults = new RouteValueDictionary {{"categoryName", "food"}, 
           {"action", "show"}}
     }
  );
}

下面的示例演示一个基于路由创建 URL 的控件。

Dim urlParameters As RouteValueDictionary

urlParameters = New RouteValueDictionary(New With {.categoryName = "beverages", _
        .action = "summarize"})
HyperLink1.href = RouteTable.Routes.GetVirtualPath _
    (context, urlParameters).VirtualPath
HyperLink1.href = RouteTable.Routes.GetVirtualPath
  (context,
  new RouteValueDictionary { 
    { "categoryName", "beverages" }, 
    {"action", "summarize" }}
  ).VirtualPath;

运行此代码时,HyperLink1 控件将包含 href 属性中的值“Category/summarize/beverages”。

从路由创建 URL 时,可以通过包括路由的名称来指定使用哪个路由创建 URL。有关更多信息,请参见如何:通过路由构造 URL。

请参见

概念

理解 ASP.NET 基础结构