十一:URL路由
路由系统有两个功能
- 输入URL,推断想要请求哪个控制器和动作
- 输出URL,是视图渲染的HTML中出现的URL,以便用户点击这些链接时,调用一个特定的动作(又变成了输入URL)
与URL相关的概念有以下内容
- 映射URL到动作的方法:定义路由
- 允许省略的URL片段:定义默认值
- 匹配没有相应路由变量的URL片段:使用静态片段
- 给动作方法传递URL片段:定义自定义片段变量
- 允许URL片段省略默认值:定义可选片段
- 定义可以匹配任意数量URL片段的路由:使用全匹配片段
- 避免控制器名称混乱:在路由中指定命名空间优先级
- 限制路由可匹配URL:运用路由约束
- 启动属性(特效)路由:使用MapMvcAttributeRoutes方法
- 一个控制器定义路由:在动作方法中使用Route属性(特性)
- 约束属性(特性)路由:在路由模式中对片段变量运用约束
- 对控制器的多有属性路由定义一个普通前缀:对控制器类使用RoutePrefix属性(特性)
URL模式
1:新建示例Web项目。模板选择空,文件夹和引用选择MVC,项目名UrlsAndRoutes,并添加测试项目
2:安装Moq包。执行NuGet命名如下
Install-package Moq -version 4.1.1309.1617 -projectname UrlsAndRoutes.Tests
3:新建控制器HomeController并修改代码如下
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace UrlsAndRoutes.Controllers { public class HomeController : Controller { public ActionResult Index() { ViewBag.Controller = "Home"; ViewBag.Action = "Index"; return View("ActionName"); } } }
4:新建一个控制器CustomerController
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace UrlsAndRoutes.Controllers { 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"); } } }
5:新建一个控制器AdminController
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace UrlsAndRoutes.Controllers { public class AdminController : Controller { public ActionResult Index() { ViewBag.Controller = "Admin"; ViewBag.Action = "Index"; return View("ActionName"); } } }
6:Views文件夹下新建Shared文件夹,并新增View名为ActionName.cshtml。代码如下
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>ActionName</title> </head> <body> <div>控制器名:@ViewBag.Controller</div> <div>动作名:@ViewBag.Action</div> </body> </html>
7:运行程序,在地址后面加上/Home/Index得到完整地址如下
http://localhost:61509/Home/Index
其中第一个“/”后面的Home为URL地址的第一片段,第二个“/”后面的Index为第二片段
创建一个简单路由
8:查看项目中App_Start文件夹下的RouteConfig.cs文件
这个类的静态方法RegisterRoutes是有Global.asax.cs文件进行调用的。
修改RouteConfig.cs
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; namespace UrlsAndRoutes { public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); Route myRoute = new Route("{controller}/{action}",new MvcRouteHandler()); routes.Add("MyRoute",myRoute); } } }
9:测试路由
在测试项目中新增测试类
using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using System.Web; using System.Web.Routing; namespace UrlsAndRoutes.Tests { [TestClass] public class RouteTests { [TestMethod] public void Test() { } private HttpContextBase CreateHttpContext(string targetUrl=null,string HttpMedthod="GET") { //模仿请求 Mock<HttpRequestBase> mockReqest = new Mock<HttpRequestBase>(); mockReqest.Setup(m=>m.AppRelativeCurrentExecutionFilePath).Returns(targetUrl); mockReqest.Setup(m=>m.HttpMethod).Returns(HttpMedthod); //模仿相应 Mock<HttpResponseBase> mockResponse = new Mock<HttpResponseBase>(); mockResponse.Setup(m=>m.ApplyAppPathModifier(It.IsAny<string>())).Returns<string>(s=>s); //创建使用上述请求和相应的模仿上下文 Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>(); mockContext.Setup(m=>m.Request).Returns(mockReqest.Object); mockContext.Setup(m => m.Response).Returns(mockResponse.Object); //返回模仿上下文 return mockContext.Object; } private void TestRouteMatch(string url, string controller, string action, object routeProperties = null, string httpMethod = "GET") { RouteCollection routes = new RouteCollection(); RouteConfig.RegisterRoutes(routes); RouteData result = routes.GetRouteData(CreateHttpContext(url,httpMethod)); Assert.IsNotNull(result); Assert.IsTrue(TestIncomingRouteResult(result,controller,action,routeProperties)); } private bool TestIncomingRouteResult(RouteData routeResult, string controller, string action, object propertySet = null) { Func<object, object, bool> valCompare = (v1, v2) => { return StringComparer.InvariantCultureIgnoreCase.Compare(v1,v2) == 0; }; bool result = valCompare(routeResult.Values["controller"],controller)&& valCompare(routeResult.Values["action"],action); if(propertySet!=null) { PropertyInfo[] propInfo = propertySet.GetType().GetProperties(); foreach(var pi in propInfo) { if(!(routeResult.Values.ContainsKey(pi.Name))&&valCompare(routeResult.Values[pi.Name],pi.GetValue(propertySet,null))) { result = false; break; } } } return result; } private void TestRouteFail(string url) { RouteCollection routes = new RouteCollection(); RouteConfig.RegisterRoutes(routes); RouteData result = routes.GetRouteData(CreateHttpContext(url)); Assert.IsTrue(result==null||result.Route==null); } [TestMethod] public void TestIncomingRoutes() { TestRouteMatch("~/Admin/Index","Admin","Index"); TestRouteMatch("~/One/Two","One","Two"); TestRouteFail("~/Admin/Index/Segment"); TestRouteFail("~/Admin"); } } }
定义默认值
10:修改RouteConfig.cs的内容如下
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; namespace UrlsAndRoutes { public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute( name: null, url: "{controller}/{action}", defaults: new { action="Index"} ); } } }
这时候请求http://localhost:49198/Customer。可以看到运行的结果是可以请求到动作是Index
使用静态URL
11:修改RouteConfig.cs的内容如下
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; namespace UrlsAndRoutes { public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute( name: null, url: "Test/{controller}/{action}", defaults: new {controller="Home", action="Index"} ); } } }
这时候运行的地址中间必须有固定常量Test才可以请求
http://localhost:49198/Test/Customer/List
http://localhost:49198/Test/Customer
http://localhost:49198/Test
路由顺序:如果定义的路由有多条,只有当前面的路由不生效时才会匹配后面的路由
自定义片段变量
12:修改RouteConfig.cs的内容如下
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; namespace UrlsAndRoutes { public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute( name: null, url: "{controller}/{action}/{id}", defaults: new {controller="Home", action="Index",id="default"} ); } } }
13:修改HomeController
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace UrlsAndRoutes.Controllers { public class HomeController : Controller { // // GET: /Home/ public ActionResult Index() { ViewBag.Controller = "Home"; ViewBag.Action = "Index"; return View("ActionName"); } public ActionResult Test() { ViewBag.Controller = "Home"; ViewBag.Action = "Test"; ViewBag.MyId=RouteData.Values["id"]; return View("ActionName"); } } }
14: 修改ActionName.cshtml
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>ActionName</title> </head> <body> <div> 控制器名:@ViewBag.Controller </div> <div> 动作名:@ViewBag.Action </div> <div> ID:@ViewBag.MyId </div> </body> </html>
15:测试地址
http://localhost:49198/Home/Index显示的ID是空
http://localhost:49198/Home/Test显示的ID是default
http://localhost:49198/Home/Test/11显示的ID是11
使用自定义变量作为动作方法参数
16:修改HomeController中的方法Test()如下
public ActionResult Test(string id) { ViewBag.Controller = "Home"; ViewBag.Action = "Test"; ViewBag.MyId=id; return View("ActionName"); }
测试的结果和上面一致。
定义可选URL片段
17:修改RouteConfig.cs的RegisterRoutes方法
public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute( name: null, url: "{controller}/{action}/{id}", defaults: new {controller="Home", action="Index",id=UrlParameter.Optional} ); }
可以正确匹配的URL如下
http://localhost:49198/
http://localhost:49198/Home/
http://localhost:49198/Home/Index
http://localhost:49198/Home/Test
http://localhost:49198/Home/Test/MyIdVar
不能匹配的URL是 http://localhost:49198/Home/Test/MyIdVar/MyIdVar
18:如果不喜欢把该参数放在路由中,同样可以修改HomeController中的方法Test()把参数修改为
Test(string id="DefaultID")
定义可变长路由
19:修改路由方法如下
public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute( name: null, url: "{controller}/{action}/{id}/{*catchAll}", defaults: new {controller="Home", action="Index",id=UrlParameter.Optional} ); }
这时候再测试17步中不能匹配的路由地址http://localhost:49198/Home/Test/MyIdVar/MyIdVar。也可以正确匹配了。
结果是和http://localhost:49198/Home/Test/MyIdVar一致。
多命名空间控制器优先级
20:在项目下新建一个NewControllers的文件夹,并新建HomeController控制器代码如下
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace UrlsAndRoutes.NewControllers { public class HomeController : Controller { public ActionResult Index() { ViewBag.Controller = "New Controller - Home"; ViewBag.Action = "Index"; return View("ActionName"); } } }
再运行URL http://localhost:49198/ 会出现错误。因为路由规则中默认的匹配是Home/Index,但是现在有两个控制器都是Home
21:修改路由方法
public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute( name: null, url: "{controller}/{action}/{id}/{*catchAll}", defaults: new {controller="Home", action="Index",id=UrlParameter.Optional}, namespaces: new[]{"UrlsAndRoutes.NewControllers"} ); }
这时候再运行该项目就会发现默认匹配到NewControllers下的HomeController类的方法Index了。
22:如果禁用其他命名空间,只能使用某个命名空间下,需要修改路由方法如下
public static void RegisterRoutes(RouteCollection routes) { Route myRoute= routes.MapRoute( name: null, url: "{controller}/{action}/{id}/{*catchAll}", defaults: new {controller="Home", action="Index",id=UrlParameter.Optional}, namespaces: new[]{"UrlsAndRoutes.NewControllers"} ); myRoute.DataTokens["UseNamespaceFallBack"] = false; }
约束路由
URL模式在进行片段匹配时是保守的,如何进行片段匹配又是宽松的
23:正则表达式约束:修改路由方法如下
public static void RegisterRoutes(RouteCollection routes) { Route myRoute = routes.MapRoute( name: null, url: "{controller}/{action}/{id}/{*catchAll}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, namespaces: new[] { "UrlsAndRoutes.Controllers" }, constraints: new { controller="^H.*"} ); }
该规则约束Controller必须以H开头。
24:HTTP方法约束:修改路由方法如下
public static void RegisterRoutes(RouteCollection routes) { Route myRoute = routes.MapRoute( name: null, url: "{controller}/{action}/{id}/{*catchAll}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, namespaces: new[] { "UrlsAndRoutes.Controllers" }, constraints: new { controller="^H.*",HttpMethod=new HttpMethodConstraint("GET")} ); }
该约束指定请求的方式只能是GET
25:类型和值约束:修改路由方法如下
public static void RegisterRoutes(RouteCollection routes) { Route myRoute = routes.MapRoute( name: null, url: "{controller}/{action}/{id}/{*catchAll}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, namespaces: new[] { "UrlsAndRoutes.Controllers" }, constraints: new { controller="^H.*",HttpMethod=new HttpMethodConstraint("GET"),id=new RangeRouteConstraint(10,20)} ); }
地址http://localhost:49198/Home/Test/20是可以的,而http://localhost:49198/Home/Test/21就是一个非负URL
常用的约束类如下
- AlphaRouteConstraint:匹配字母字符(A~Z,a~z)
- BoolRouteConstraint:匹配bool
- DateTimeRouteConstraint:匹配可以解析成DateTime类型的值
- DecimalRouteConstraint:匹配可以解析成Decimal类型的值
- RangeRouteConstrain(min,max):匹配一个值在min和max之间
- 其他还有很多约束匹配
自定义约束
如果标准的约束不能满足要求,可以通过IRouteConstraint接口来自定义约束
26:新建一个类
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Routing; namespace UrlsAndRoutes { public class MyConstraint:IRouteConstraint { private string test; public MyConstraint(string Test) { test = Test; } public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { return test.Contains("Happy"); } } }
27:修改路由方法如下
public static void RegisterRoutes(RouteCollection routes) { Route myRoute = routes.MapRoute( name: null, url: "{controller}/{action}/{id}/{*catchAll}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, namespaces: new[] { "UrlsAndRoutes.Controllers" }, constraints: new { controller="^H.*", HttpMethod=new HttpMethodConstraint("GET"), id=new MyConstraint("NewYear")} ); }
发现该条路由匹配就是失效的,除非id=new MyConstraint中的参数含有要求的字符Happy
属性路由
MVC5新增了属性路由的技术
28:修改HomeController类,在Index()方法上新增属性路由[Route("Test")]