ASP.NET MVC 中的自定义路由
如何在ASP.NET MVC中自定义路由。
路由是MVC框架的主要方面之一,它使MVC成为今天的样子。虽然这可能过于简化,但在路由框架中,“约定优于配置”的MVC哲学显而易见。
路由也代表了MVC中一个更可能令人困惑的方面。一旦我们超越了基础知识,随着我们的应用程序变得更加复杂,通常需要自定义路由,而不仅仅是简单的灵活的默认MVC路由。
图片由 ~uminotorino 提供。 保留部分权利。
路由自定义是一个复杂的话题。在本文中,我们将探讨一些修改传统MVC路由机制以适应需要更灵活的应用程序需求的基本方法。如果您是MVC新手,您可能希望回顾一下 ASP.NET MVC中的路由基础知识。
2013年9月26日更新 - ASP.NET 5.0和WebApi 2.0引入了属性路由作为标准的“开箱即用”功能。属性路由仍然遵循此处讨论的大多数模式,但将路由定义移至其服务的控制器方法。属性路由不会取代此处讨论的正常集中式路由表,事实上,关于定义路由的“唯一正确方式”存在一些争议。我将在即将发布的文章中更仔细地研究ASP.NET的这一新功能。可以说,这两种方法都隐含着不同的架构和设计考虑。
注意:本文中的大多数示例都有点做作和随意。我的目标是演示一些基本的路由自定义选项,而不会被可能需要它们的业务案例特定细节所干扰。我承认在现实世界中,这些示例可能不代表自定义路由的合理案例。
在任何足够复杂的ASP.NET MVC项目中,您很可能需要添加一个或多个自定义路由,这些路由补充或替换了传统的MVC路由。正如我们在 ASP.NET MVC路由基础知识 中学到的那样,默认的MVC路由映射是
http://<domain>/{controller}/{action}/{id}
这个基本约定将处理令人惊讶数量的潜在路由。然而,通常在某些情况下需要更多的控制,以
- 在复杂场景中将传入请求路由到正确的应用程序逻辑。
- 允许结构良好的URL,反映我们站点的结构。
- 创建易于键入的URL结构,并且是“可破解”的,即用户在检查当前URL结构时,可能会根据该结构合理地进行一些导航猜测。
路由自定义可以通过多种方式实现,通常通过组合修改路由映射的基本模式、使用路由 **_默认选项_** 明确指定控制器和操作,以及较不常见地使用 _**路由约束**_ 来限制路由“匹配”特定URL段或参数的方式。
URL按顺序与路由匹配 – 第一个匹配获胜
传入的URL按照模式在 _**路由字典**_ 中出现的顺序(也就是我们在 _RouteConfig.cs_ 文件中添加路由映射的顺序)进行比较。第一个成功将控制器、操作和操作参数与URL中的参数或路由映射中定义的默认值匹配的路由将调用指定的控制器和操作。这很重要,需要我们仔细思考路由,以免无意中调用错误的处理器。
路由匹配的基本过程已在ASP.NET MVC路由基础知识中介绍。
修改URL模式
正如我们之前学到的,在MVC应用程序中,路由定义了传入URL与特定控制器以及这些控制器上的操作(方法)匹配的模式。路由映射将URL识别为由斜杠(/)字符分隔(或限定)的段模式。每个段可能包含各种 _**文字**_(文本值)和 _**参数占位符**_ 的组合。参数占位符在路由定义中通过大括号标识。
MVC 识别特殊的参数占位符 **`{controller}`** 和 **`{action}`**,并使用它们来定位适当的控制器和方法以响应传入的 URL。除了这两个特殊的占位符,我们几乎可以将任何内容作为参数占位符添加到路由中以适应我们的目的,只要我们遵守良好的 URL 设计原则,当然,合理地期望该参数有用。
我们熟悉上面显示的默认 MVC 模式。我们可以看到,该路由由三个段组成,没有文字,并且包含特殊参数 **`{controller}`** 和 **`{action}`** 的参数占位符,以及一个名为 **`{id}`** 的附加参数。
创建路由 URL 模式时,我们可以将文字与参数占位符以多种方式组合,只要参数占位符始终由分隔符或至少一个文字字符分隔。以下是有效路由模式的示例(它们是否合理是另一个问题)
ASP.NET MVC中有效路由模式示例
路由模式 | URL 示例 |
mysite/{username}/{action} |
~/mysite/jatten/login |
public/blog/{controller}-{action}/{postId} |
~/public/blog/posts-show/123 |
{country}-{lang}/{controller}/{action}/{id} |
~/us-en/products/show/123 |
products/buy/{productId}-{productName} |
~/products/but/2145-widgets |
以下模式无效,因为`{controller}`参数占位符和`{action}`参数占位符未用斜杠(/)或其他文字字符分隔。在这种情况下,MVC框架无法知道一个参数在哪里结束,下一个参数在哪里开始(假设控制器名为“`People`”,动作名为“`Show`”)
路由模式 | URL 示例 |
mysite/{controller}{action}/{id} |
~/mysite/peopleshow/5 |
并非所有上述有效路由示例都包含 **`{controller}`** 参数占位符或 **`{action}`** 参数占位符。最后一个示例两者都不包含。此外,大多数都包含用户定义的参数,例如 **`{username}`** 或 **`{country}`**。我们将在接下来的两节中查看这两者。
修改路由默认选项
结合修改路由 URL 模式,我们还可以利用路由默认值来创建更具体的路由,在某些情况下,这些路由只映射到特定的控制器和/或操作。
标准的MVC项目文件在名为 _RouteConfig.cs_ 的文件中包含路由映射配置
标准MVC RouteConfig.cs文件
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new {
controller = "Home",
action = "Index",
id = UrlParameter.Optional }
);
}
}
上述路由为控制器和操作都建立了默认值,以防传入URL中未提供其中一个或两个。
我们可以更进一步,添加一个新的、更具体的路由,当它匹配特定的 URL 模式时,会调用一个特定的控制器。在下面的内容中,我们在 **_RouteConfig.cs_** 文件中添加了一个新的路由映射
添加更严格的路由
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// ALL THE PROPERTIES:
// rentalProperties/
routes.MapRoute(
name: "Properties",
url: "RentalProperties/{action}/{id}",
defaults: new
{
controller = "RentalProperty",
action = "All",
id = UrlParameter.Optional
}
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new {
controller = "Home",
action = "Index",
id = UrlParameter.Optional }
);
}
首先,请注意,我们将新的、更具体的路由添加在 **_文件中的默认路由之前_** (这决定了它添加到路由字典中的顺序)。这是因为 **_路由按照顺序评估是否与传入 URL 匹配_**。默认的 MVC 路由非常通用,如果在向项目添加额外路由时未仔细考虑路由顺序,默认路由可能会无意中匹配到 intended for a different handler 的 URL。
除此之外,我们在上面添加的新路由中看到,“`Properties`”路由的 URL 模式中没有 **`{controller}`** 参数占位符。由于不能将控制器作为参数传递给此路由,因此它将始终映射到 **`RentalPropertiesController`**,以及作为参数提供的任何操作。由于定义了默认操作,如果传入的 URL 匹配路由但不包含 `Action` 参数,则将调用默认的 **`All()`** 方法。
一个做作的例子
到目前为止,在这个相当做作的例子中,我们还没有完成任何我们无法使用默认MVC路由模式完成的事情。然而,假设在我们的应用程序中,我们希望定义以下URL模式和相关的URL示例,用于访问物业管理应用程序中的视图
简单物业管理应用程序所需的URL模式
行为 | URL 示例 |
显示所有出租物业 | ~/rentalproperties/ |
显示特定出租物业 | ~/rentalproperties/propertyname/ |
显示物业中的特定单元 | ~/rentalproperties/propertyname/units/unitNo |
我们可以定义一个 **`RentalPropertyController`** 类如下
租赁物业控制器示例
public class RentalPropertiesController : Controller
{
private RentalPropertyTestData _data = new RentalPropertyTestData();
// List all the properties
public ActionResult All()
{
var allRentalProperties = _data.RentalProperties;
return View(allRentalProperties);
}
// get a specific property, display details and list all units:
public ActionResult RentalProperty(string rentalPropertyName)
{
var rentalProperty = _data.RentalProperties.Find(a => a.Name == rentalPropertyName);
return View(rentalProperty);
}
// get a specific unit at a specific property:
public ActionResult Unit(string rentalPropertyName, string unitNo)
{
var unit = _data.Units.Find(u => u.RentalProperty.Name == rentalPropertyName);
return View(unit);
}
}
鉴于上述控制器和上表中所需的 URL 模式,我们可以看到默认的 MVC 路由映射适用于部分 URL,但不适用于所有 URL。我们最好通过向应用程序添加以下特定于控制器和操作的路由来更好地服务,以便我们的 **_RouteConfig.cs_** 文件看起来像这样
物业管理示例应用程序的修改后的路由配置文件
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// RentalProperties/Boardwalk/Units/4A
routes.MapRoute(
name: "RentalPropertyUnit",
url: "RentalProperties/{rentalPropertyName}/Units/{unitNo}",
defaults: new
{
controller = "RentalProperties",
action = "Unit",
}
);
// RentalProperties/Boardwalk
routes.MapRoute(
name: "RentalProperty",
url: "RentalProperties/{rentalPropertyName}",
defaults: new
{
controller = "RentalProperties",
action = "RentalProperty",
}
);
// RentalProperties/
routes.MapRoute(
name: "RentalProperties",
url: "RentalProperties",
defaults: new
{
controller = "RentalProperties",
action = "All",
id = UrlParameter.Optional
}
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new
{
controller = "Home",
action = "Index",
id = UrlParameter.Optional
}
);
}
正如我们现在看到的,所有新添加的路由都明确指定了控制器和操作,并允许我们逐步增强的 URL 结构,其中包含作为 URL 一部分的属性名称。
当然,在现实世界中,这种 URL 方案可能无法按我们期望的方式工作,因为(取决于我们的数据库将如何使用)租赁物业的名称可能不唯一,即使在给定管理公司内也是如此。如果该公司在几个州甚至州内的县市经营租赁物业,则很有可能存在多个名为(例如)“Maple Glen”的物业。这是一个令人悲伤的事实,尽管我们在学习过程中可以找到所有奇妙的简单示例,但 **_现实世界往往无法按照我们想要的方式建模。_**
添加路由约束
路由约束可用于进一步限制可被视为与特定路由匹配的URL。虽然我们迄今为止已经根据URL结构和参数名称检查了路由是否匹配,但路由约束允许我们添加一些额外的特异性。
MVC 框架识别两种类型的约束,即 **`string`** 或实现接口 **`IRouteConstraint`** 的类。
路由约束中的正则表达式
当提供`string`作为约束时,MVC框架将该`string`解释为正则表达式,可用于限制URL匹配特定路由的方式。这方面的一个常见场景是限制路由参数的值为数字。
考虑以下路由映射
不带约束的博客应用程序示例路由映射
routes.MapRoute(
name: "BlogPost",
url: "blog/posts/{postId}",
defaults: new
{
controller = "Posts",
action = "GetPost",
},
);
此路由将调用假设博客应用程序的 **`PostsController`** 并根据唯一的 `postId` 检索特定的帖子。此路由将正确匹配以下 URL
不幸的是,它也会匹配这个
http://domain/blog/posts/gimme
基本的路由约束允许我们测试 **`{postId}`** 参数的值,以确定它是否为数字值。我们可以这样重写我们的路由映射
添加约束的博客应用程序示例路由映射
routes.MapRoute(
name: "BlogPost",
url: "blog/posts/{postId}",
defaults: new
{
controller = "Posts",
action = "GetPost",
},
new {postId = @"\d+" }
);
上面代码中的微小正则表达式 **`@"\d+`** 基本上将此路由的匹配限制为 `postId` 参数包含一个或多个数字的 URL。换句话说,请只允许整数。
我不会在这里深入研究正则表达式。 suffice it to say, 正则表达式可以非常有效地用于为您的应用程序开发复杂的路由方案。
正则表达式的注意事项和更多资源
正则表达式是一个强大的工具,也是一个巨大的麻烦。正如 Jamie Zawinskie 所说:**_“有些人,当遇到问题时,会想‘我知道了,我用正则表达式。’现在他们有两个问题了。”_** 然而,正则表达式有时是完成任务的最佳工具——在 MVC 应用程序中限制路由匹配就是其中一种情况。在编写更复杂的正则表达式时,我发现三个工具最有用。按重要性顺序排列:
- Google (废话!)
- Expresso 正则表达式开发工具 (老旧,但非常宝贵)
- 网站 Regular-Expressions.info (比你想象的还要多)
使用IRouteConstraint的自定义路由约束
使用路由约束的另一个选项是创建一个实现`IRouteConstraint`接口的类。
我们将快速查看一个自定义路由约束,该约束可用于确保特定控制器被排除为我们的默认 MVC 路由的匹配项。
_**注意**_: 以下代码改编自 Vijaya Anand(CodeProject 成员 After2050)的 CodeProject 文章,最初发布在他的个人博客 Proud Parrot。
在创建自定义约束时,我们首先创建一个实现 **`IRouteConstraint`** 的新类。 **`IRouteConstraint`** 定义了一个方法 **`Match`**,我们需要提供其实现。为了使约束起作用,我们还需要创建一个构造函数,通过它我们设置方法所需的参数
自定义约束类 ExcludeController
public class ExcludeController : IRouteConstraint
{
private readonly string _controller;
public ExcludeController(string controller)
{
_controller = controller;
}
public bool Match(HttpContextBase httpContext,
Route route, string parameterName,
RouteValueDictionary values,
RouteDirection routeDirection)
{
// Does the _controller argument match the controller value in the route
// dictionary for the current request?
return string.Equals(values["controller"].ToString(),
_controller, StringComparison.OrdinalIgnoreCase);
}
}
现在,假设我们有一个假想的控制器 **`ConfigurationController`**,我们为其定义了一个需要身份验证的特殊路由。我们不希望 MVC 默认路由映射无意中允许未经身份验证的用户访问 Configuration 控制器。我们可以通过修改我们的默认 MVC 路由来确保这一点,如下所示
为默认MVC路由添加自定义约束
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new
{
controller = "Home",
action = "Index",
id = UrlParameter.Optional
},
constraints: new { controller = new ExcludeController("Configuration")}
);
现在,如果一个尚未被任何先前路由映射匹配的 URL 被评估是否与我们的默认路由匹配,MVC 框架将调用我们的自定义约束 **`ExcludeController`** 的 `Match` 方法。在上述情况下,如果 URL 包含值为“`Configuration`”的 **`{controller}`** 参数,则 **`Match`** 方法将返回 `false`,导致路由拒绝该 URL 匹配。
Stephen Walther 在他的博客文章 ASP.NET MVC 技巧 #30 – 创建自定义路由约束 中提供了一篇关于自定义路由约束,特别是创建身份验证约束的精彩教程。
2013年11月26日 – 更新: 自本文撰写以来,ASP.NET MVC 5 已经发布。除了开箱即用地包含属性路由(如前所述),ASP.NET MVC 5 还带来了新的身份管理系统,以取代迄今为止使用的表单成员资格。请查看本激动人心的新版本中扩展身份账户和实现基于角色的身份管理,使用新的身份库。
额外资源
- ASP.NET MVC 中的路由基础
- ASP.NET Web API 中的路由基础知识
- 使用 VS 2012 和 ASP.NET MVC 4 创建一个干净、占用空间最小的 ASP.NET WebAPI 项目
- 使用最小Web API项目构建一个简洁的RESTful Web Api服务
- 从源代码管理部署Azure网站
- Webmatrix 3:集成Git和部署到Azure