1.前言
我们知道在ASP.NET Web Forms中,一个URL请求往往对应一个aspx页面,一个aspx页面就是一个物理文件,它包含对请求的处理。
而在ASP.NET MVC中,一个URL请求是由对应的一个Controller中的Action来处理的,由URL Routing来告诉MVC如何定位到正确的Controller和Action。
总的来说,URL Routing包含两个主要功能:解析URL 和 生成URL,本文将围绕这两个大点进行讲解。
2.URL ROUTING的定义方式
在域名的后面,默认使用“/”来对URL进行分段。路由系统通过类似于 {controller}/{action} 格式的字符串可以知道这个URL的 Admin 和 Index 两个片段分别对应Controller和Action的名称。
默认情况下,路由格式中用“/”分隔的段数是和URL域名的后面的段数是一致的,比如,对于{controller}/{action} 格式只会匹配两个片段。如下表所示:
URL路由是在MVC工程中的App_Start文件夹下的RouteConfig.cs文件中的RegisterRoutes方法中定义的,下面是创建一个空MVC项目时系统生成的一个简单URL路由定义:
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 } ); }
静态方法RegisterRoutes是在Global.asax.cs文件中的Application_Start方法中被调用的,除了URL路由的定义外,还包含其他的一些MVC核心特性的定义:
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); }
RouteConfig.RegisterRoutes方法中传递的是 RouteTable 类的静态 Routes 属性,返回一个RouteCollection的实例。其实,“原始”的定义路由的方法可以这样写:
public static void RegisterRoutes(RouteCollection routes) { Route myRoute = new Route("{controller}/{action}", new MvcRouteHandler()); routes.Add("MyRoute", myRoute); }
创建Route对象时用了一个URL格式字符串和一个MvcRouteHandler对象作为构造函数的参数。不同的ASP.NET技术有不同的RouteHandler,MVC用的是MvcRouteHandler。
这种写法有点繁琐,一种更简单的定义方法是:
public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute("MyRoute", "{controller}/{action}"); }
这种方法简洁易读,一般我们都会用这种方法定义路由。
3.示例准备
public class HomeController : Controller { public ActionResult Index() { ViewBag.Controller = "Home"; ViewBag.Action = "Index"; return View("ActionName"); } } public class CustomerController : Controller { public ActionResult Index() { ViewBag.Controller = "Customer"; ViewBag.Action = "Index"; return View("ActionName"); } public ActionResult List() { ViewBag.Controller = "Customer"; ViewBag.Action = "List"; return View("ActionName"); } } public class AdminController : Controller { public ActionResult Index() { ViewBag.Controller = "Admin"; ViewBag.Action = "Index"; return View("ActionName"); } }
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>ActionName</title> </head> <body> <div>The controller is: @ViewBag.Controller</div> <div>The action is: @ViewBag.Action</div> </body> </html>
public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute("MyRoute", "{controller}/{action}"); }
4.给片段变量定义默认值
routes.MapRoute("MyRoute", "{controller}/{action}", new { controller = "Home", action = "Index" });
各种匹配情况:
5.定义静态片段
并不是所有的片段都是用来作为匹配变量的,比如,我们想要URL加上一个名为Public的固定前缀,那么我们可以这样定义:
routes.MapRoute("", "Public/{controller}/{action}", new { controller = "Home", action = "Index" });
这样,请求的URL也需要一个Public前缀与之匹配。我们也可以把静态的字符串放在大括号以外的任何位置,如:
routes.MapRoute("", "X{controller}/{action}", new { controller = "Home", action = "Index" });
在一些情况下这种定义非常有用。比如当你的网站某个链接已经被用户普遍记住了,但这一块功能已经有了一个新的版本,但调用的是不同名称的controller,那么你把原来的controller名称作为现在controller的别名。这样,用户依然使用他们记住的URL,而导向的却是新的controller。如下使用Shop作为Home的一个别名:
routes.MapRoute("ShopSchema", "Shop/{action}", new { controller = "Home" });
6.自定义片段变量
自定义片段变量的定义和取值
contrlloer和action片段变量对MVC来说有着特殊的意义,在定义一个路由时,我们必须有这样一个概念:contrlloer和action的变量值要么能从URL中匹配得到,要么由默认值提供,总之一个URL请求经过路由系统交给MVC处理时必须保证contrlloer和action两个变量的值都有。当然,除了这两个重要的片段变量,我们也可从通过自定义片段变量来从URL中得到我们想要的其它信息。如下自定义了一个名为Id的片段变量,而且给它定义了默认值:
routes.MapRoute("MyRoute", "{controller}/{action}/{id}", new{ controller = "Home", action = "Index", id = "DefaultId"}); });
我们在HomeController中增加一个名为CustomVariable的ACtion来演示一下如何取自定义的片段变量:
publicActionResult CustomVariable() { ViewBag.Controller = "Home"; ViewBag.Action = "CustomVariable"; ViewBag.CustomVariable = RouteData.Values["id"]; return View("ActionName"); }
可以通过 RouteData.Values[segment] 来取得任意一个片段的变量值。
7.将自定义片段变量作为Action方法的参数
我们可以将自定义的片段变量当作参数传递给Action方法,如下所示:
public ActionResult CustomVariable(string id) { ViewBag.Controller = "Home"; ViewBag.Action = "CustomVariable"; ViewBag.CustomVariable =id; return View("ActionName"); }
效果和上面是一样的,只不过这样省去了用 RouteData.Values[segment] 的方式取自定义片段变量的麻烦。这个操作背后是由模型绑定来做的。
8.指定自定义片段变量为可选
指定自定片段变量为可选,即在URL中可以不用指定片段的值。如下面的定义将Id定义为可选:
routes.MapRoute("MyRoute", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id=UrlParameter.Optional });
定义为可选以后,需要对URL中没有Id这个片段值的情况进行处理,如下:
public ActionResult CustomVariable(string id) { ViewBag.Controller = "Home"; ViewBag.Action = "CustomVariable"; ViewBag.CustomVariable = id == null ? "<no value>" : id; return View("ActionName"); }
当Id是整型的时候,参数的类型需要改成可空的整型(即int? id)。
为了省去判断参数是否为空,我们也可以把Action方法的id参数也定义为可选,当没有提供Id参数时,Id使用默认值,如下所示:
public ActionResult CustomVariable(string id = "DefaultId") { ViewBag.Controller = "Home"; ViewBag.Action = "CustomVariable"; ViewBag.CustomVariable = id; return View("ActionName"); }
routes.MapRoute("MyRoute", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "DefaultId" });
9. 定义可变数量的自定义片段变量
我们可以通过 catchall 片段变量加 * 号前缀来定义匹配任意数量片段的路由。如下所示:
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional });
这个路由定义的匹配情况如下所示:
使用*catchall,将匹配的任意数量的片段,但我们需要自己通过“/”分隔catchall变量的值来取得独立的片段值。
10. 路由约束
正则表达式约束
通过正则表达式,我们可以制定限制URL的路由规则,下面的路由定义限制了controller片段的变量值必须以 H 打头:
我们可以用正则表达式约束来定义只有指定的几个特定的片段值才能进行匹配,如下所示:
routes.MapRoute("MyRoute", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new { controller = "^H.*", action = "^Index$|^About$" } );
这个定义,限制了只能访问controller以H打着的,action片段值只能是Index或About,不区分大小写。
Http请求方式约束
我们还可以限制路由只有当以某个特定的Http请求方式才能匹配。如下限制了只能是Get请求才能进行匹配:
routes.MapRoute("MyRoute", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new { controller = "^H.*", httpMethod = new HttpMethodConstraint("GET") } );
通过创建一个 HttpMethodConstraint 类的实例来定义一个Http请求方式约束,构造函数传递是允许匹配的Http方法名。这里的httpMethod属性名不是规定的,只是为了区分。
这种约束也可以通过HttpGet或HttpPost过滤器来实现。
自定义路由约束
如果标准的路由约束满足不了你的需求,那么可以通过实现 IRouteConstraint 接口来定义自己的路由约束规则。
我们来做一个限制浏览器版本访问的路由约束。在MVC工程中添加一个文件夹,取名Infrastructure,然后添加一个 UserAgentConstraint 类文件,代码如下:
public class UserAgentConstraint : IRouteConstraint { private string requiredUserAgent; public UserAgentConstraint(string agentParam) { requiredUserAgent = agentParam; } public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { return httpContext.Request.UserAgent != null && httpContext.Request.UserAgent.Contains(requiredUserAgent); } }
这里实现IRouteConstraint的Match方法,返回的bool值告诉路由系统请求是否满足自定义的约束规则。我们的UserAgentConstraint类的构造函数接收一个浏览器名称的关键字作为参数,如果用户的浏览器包含注册的关键字才可以访问。接一来,我们需要注册自定的路由约束:
public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute("ChromeRoute", "{*catchall}", new { controller = "Home", action = "Index"}, new { customConstraint = new UserAgentConstraint("Chrome") } ); }
11. 定义请求磁盘文件路由
并不是所有的URL都是请求controller和action的。有时我们还需要请求一些资源文件,如图片、html文件和JS库等。
我们先来看看能不能直接请求一个静态Html文件。在项目的Content文件夹下,添加一个html文件,内容随意。然后把URL定位到该文件,如下图:
我们看到,是可以直接访问一静态资源文件的。
默认情况下,路由系统先检查URL是不是请求静态文件的,如果是,服务器直接返回文件内容并结束对URL的路由解析。我们可以通过设置 RouteCollection的 RouteExistingFiles 属性值为true 让路由系统对静态文件也进行路由匹配,如下所示:
public static void RegisterRoutes(RouteCollection routes) { routes.RouteExistingFiles = true;// routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }); }
设置了routes.RouteExistingFiles = true后,还需要对IIS进行设置,这里我们以IIS Express为例,右键IIS Express小图标,选择“显示所有应用程序”,弹出如下窗口:
点击并打开配置文件,Control+F找到UrlRoutingModule-4.0,将这个节点的preCondition属性改为空,如下所示:
<add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" preCondition=""/>
然后我们运行程序,再把URL定位到之前的静态文件:
这样,路由系统通过定义的路由去匹配RUL,如果路由中没有定义该静态文件的匹配,则会报上面的错误。
一旦定义了routes.RouteExistingFiles = true,我们就要为静态文件定义路由,如下所示:
public static void RegisterRoutes(RouteCollection routes) { routes.RouteExistingFiles = true; routes.MapRoute("DiskFile", "Content/StaticContent.html", new { controller = "Customer", action = "List", }); routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }); }
这个路由匹配Content/StaticContent.html的URL请求为controller = Customer, action = List。我们来看看运行结果:
这样做的目的是为了可以在Controller的Action中控制对静态资源的请求,并且可以阻止对一些特殊资源文件的访问。
设置了RouteExistingFiles属性为true后,我们要为允许用户请求的资源文件进行路由定义,如果每种资源文件都去定义相应的路由,就会显得很繁琐。
我们可以通过RouteCollection类的IgnoreRoute方法绕过路由定义,使得某些特定的静态文件可以由服务器直接返回给给浏览器,如下所示:
public static void RegisterRoutes(RouteCollection routes) { routes.RouteExistingFiles = true; routes.IgnoreRoute("Content/{filename}.html"); routes.MapRoute("DiskFile", "Content/StaticContent.html", new { controller = "Customer", action = "List", }); routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }); }
这样,只要是请求Content目录下的任何html文件都能被直接返回。这里的IgnoreRoute方法将创建一个RouteCollection的实例,这个实例的Route Handler 为 StopRoutingHandler,而不是 MvcRouteHandler。运行程序定位到Content/StaticContent.html,我们又看到了之前的静态面面了。