在ASP.NET 5和MVC6中,Routing功能被所有重写了,虽然用法有些相似,但和以前的Routing原理彻底不太同样了,该Routing框架不只能够支持MVC和Web API,还支持通常的ASP.NET5程序。新版的改变有以下几个部分。html
首先,Routing系统是基于ASP.NET 5的,是一个独立于MVC的路由框架,而不是基于MVC的。MVC只是在上面扩展了一个快捷方式而已。git
其次,在ASP.NET 5中,MVC和Web API控制器没有区别了,即合二为一了。二者派生于同一个Controller基类。也就是说该Routing框架是适用于二者的,适用于MVC则意味着也适用于Web API。github
最后,无论在基于约定的Route声明仍是基于Attribute的Route声明,均可以使用内联约束和参数选项。例如,你能够约定路由中某个参数的数据类型,也可让一个参数标记为可选类型,再或者给其提供一个默认值。web
基本的Routing框架是基于Middleware来实现的,这样就能够将其添加到HTTP的请求Pipeline中了,它能够喝其它任意Middleware一块儿进行组合使用,如静态文件处理程序、错误页、或者SignalR服务器。正则表达式
在使用Routing框架以前,首要要了解Routing的做用,做用很简单:json
路由系统的执行流程以下:api
和以前的Routing系统有点不一样的是,老版的Routing系统一旦成功匹配一个路由,就将其交由其对应的Handler,无论对应的Handler能不能处理该请求,因此就会出现route匹配成功了,可是找不到对应的action,此时就会出现404错误,而新版对此做出了上述第4步骤的改进(从新将控制权交回给Routing系统,进行从新匹配),看起来仍是很是不错的。数组
在以前的route设置中,要约束一个参数的数据类型的话,咱们须要使用类型以下代码:服务器
routes.MapRoute( "Product", "Product/{productId}", defaults: new { controller = "Product", action = "Details" }, constraints: new { productId = @"\d+" });
而在新版route中,就能够直接设置Product/{productId:int}
了,约束条件遵照以下约定:mvc
{parameter:constraint}
目前支持的约束以下:
约束 | 示例 | 说明 |
---|---|---|
required | "Product/{ProductName:required}" | 参数必选 |
alpha | "Product/{ProductName:alpha}" | 匹配字母,大小写不限 |
int | "Product/{ProductId:int}" | 匹配int类型 |
long | "Product/{ProductId:long}" | 匹配long类型 |
bool | "Product/{ProductId:bool}" | 匹配bool类型 |
double | "Product/{ProductId:double}" | 匹配double类型 |
float | "Product/{ProductId:float}" | 匹配float类型 |
guid | "Product/{ProductId:guid}" | 匹配guid类型 |
decimal | "Product/{ProductId:decimal}" | 匹配decimal类型 |
datetime | "Search/{datetime:datetime}" | 匹配datetime类型 |
composite | "Product/{ProductId:composite}" | 匹配composite类型 |
length | "Product/{ProductName:length(5)}" | 长度必须是5个字符 |
length | "Product/{ProductName:length(5, 10)}" | 长度在5-10个之间 |
maxlength | "Product/{productId:maxlength(10)}" | 最大长度为10 |
minlength | "Product/{productId:minlength(3)}" | 最小长度为3 |
min | "Product/{ProductID:min(3)}" | 大于等于3 |
max | "Product/{ProductID:max(10)}" | 小于等于10 |
range | "Product/{ProductID:range(5, 10)}" | 对应的数组在5-10之间 |
Regex | "Product/{productId:regex(^\d{4}$)}" | 符合指定的正则表达式 |
而对于可选参数,则值须要在约束类型后面加一个问号便可,示例以下:
routes.MapRoute( "Product", "Product/{productId:long?}", new { controller = "Product", action = "Details" });
若是参数是必填的,须要保留一个默认值的话,则能够按照以下示例进行设置:
routes.MapRoute( "Product", "Product/{productId:long=1000}", new { controller = "Product", action = "Details" });
关于示例使用,咱们先不从MVC开始,而是先从普通的Routing使用方式开始,新版route添加的时候默认添加的是TemplateRoute
实例,而且在该实例实例化的时候要设置一个Handler
。
举例来讲,咱们先建立一个空的ASP.NET 5项目,并在project.json文件的dependencies节点中添加程序集"Microsoft.AspNet.Routing": "1.0.0-beta3"
,,在Startup.cs
的Configure
方法里添加以下代码:
public void Configure(IApplicationBuilder app) { RouteCollection routes = new RouteCollection(); routes.Add(new TemplateRoute(new DebuggerRouteHandler("RouteHandlerA"), "", null)); routes.Add(new TemplateRoute(new DebuggerRouteHandler("RouteHandlerB"), "test/{a}/{b:int}", null)); routes.Add(new TemplateRoute(new DebuggerRouteHandler("RouteHandlerC"), "test2", null)); app.UseRouter(routes); // 开启Routing功能 }
在这里,咱们设置HTTP请求处理的的Handler为DebuggerRouteHandler
,该类继承于IRouter
,实例代码以下:
public class DebuggerRouteHandler : IRouter { private string _name; public DebuggerRouteHandler(string name) { _name = name; } public string GetVirtualPath(VirtualPathContext context) { throw new NotImplementedException(); } public async Task RouteAsync(RouteContext context) { var routeValues = string.Join("", context.RouteData.Values); var message = String.Format("{0} Values={1} ", _name, routeValues); await context.HttpContext.Response.WriteAsync(message); context.IsHandled = true; } }
上述类,继承IRouter
之后,必须实现一个RouteAsync
的方法,而且若是处理成功,则将IsHandled
设置为true
。
访问以下网址便可查看相应的结果:
正常:`http://localhost:5000/` 正常:`http://localhost:5000/test/yyy/12` 404 :`http://localhost:5000/test/yyy/s` 正常:`http://localhost:5000/test2` 404 :`http://localhost:5000/test3`
注意:
TemplateRoute
和DebuggerRouteHandler
都继承于IRouter
,是实现前面所述的不出现404错误(继续匹配下一个路由)的核心。
在MVC示例程序中,咱们只须要配置在调用app.UseMVC
方法的时候,使用委托中的MapRoute
方法来定义各类route就能够了。在这里咱们以空白项目为例,来看看MVC的route如何使用。
第一步:在project.json文件的dependencies节点中引用程序集"Microsoft.AspNet.Mvc": "6.0.0-beta3"
,
第二部:添加MVC的Middleware,并使用MVC,而后添加一条默认的路由,代码以下:
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); } public void Configure(IApplicationBuilder app) { app.UseMvc(routeBuilder => { routeBuilder.MapRoute( name: "default", template: "{controller}/{action}/{id?}", defaults: new { controller = "Home", action = "Index" }); }); }
第三步:分别建立以下以下三种Controller,其中ProductsController
继承于Microsoft.AspNet.Mvc
下的Controller
。
public class ProductsController : Controller { public IActionResult Index() { return Content("It Works with Controller Base Class!"); } } public class DemoController { public IActionResult Index() { return new ObjectResult("It Works without Controller Base Class!"); } } public class APIController { public object Index() { return new { Code = 100000, Data = "OK" }; } }
访问http://localhost:5000/products
和http://localhost:5000/demo
,均能显示正常的输出结果;而访问http://localhost:5000/api
的时候返回的则是json数据。
这就是咱们在前面ASP.NET5新特性中所讲的MVC和API合二为一了,而且也能够不继承于Controller基类(但类名要以Controller结尾)。这种技术的核心是Controller的查找机制,关于如何在一个项目中查找合适的程序集,请参考《Controller与Action》章节。
新版MVC在断定Controller的时候,有2个条件:要么继承于Controller,要么是引用MVC程序集而且类名以Controller结尾。
因此,在建立MVC Controller和Web API Controller的时候,若是你不须要相关的上下文(如HTTPContext、ActionContext等)的话,则能够没必要继承于Controller基类;但推荐都继承于Controller,由于能够多多利用基类的方法和属性,由于无论继承不继承,你定义的全部Controller类都要走MVC的各个生命周期,咱们经过ActionFilter来验证一下:
第一步:在project.json文件的dependencies节点中引用程序集"Microsoft.AspNet.Server.WebListener": "1.0.0-beta3"
。
第二步:建立一个Aciton Filter,分别在Action执行前和执行后输出一行文字,代码以下:
public class ActionFilterTest : IActionFilter { public void OnActionExecuting(ActionExecutingContext context) { var typeName = context.Controller.GetType().FullName; Console.WriteLine(typeName + "." + context.ActionDescriptor.Name + ":Start"); } public void OnActionExecuted(ActionExecutedContext context) { var typeName = context.Controller.GetType().FullName; Console.WriteLine(typeName + "." + context.ActionDescriptor.Name + ":END"); } }
第三步:在ConfigureServices方法里注册该Action Filter。
services.Configure<MvcOptions>(options => { options.Filters.Add(typeof(ActionFilterTest)); });
运行程序,并访问响应的路径,三种类型的代码均会按计划输出内容,输出内容以下:
RouterTest.ProductsController.Index:Start RouterTest.ProductsController.Index:End RouterTest.DemoController.Index:Start RouterTest.DemoController.Index:End RouterTest.APIController.Index:Start RouterTest.APIController.Index:End
普通的ASP.NET5程序和MVC程序是能够在一块儿混合使用Routing功能的。
ASP.NET 5和MVC6都提供了丰富的Route自定义功能,关于普通Route的自定义,能够参考前面小节的DebuggerRouteHandler,这种方式须要实现本身的HTTP输出,至关于原来轻量级的IHttpHandler同样。本节,咱们将这种在基于MVC的Route自定义功能,即定义的Route的Handler处理程序都是MvcRouteHandler。
在以前版本的MVC中,要自定义Route,通常都是继承于RouteBase基类或Route类;而在新版的MVC6中,要实现自定义Route,有三种方式,分别以下:
本例中,咱们以继承继承于TemplateRoute为例,首先建立一个继承于该类的子类PromoTemplateRoute
,该类只匹配/promo
目录下的路径。
public class PromoTemplateRoute : TemplateRoute { public PromoTemplateRoute(IRouter target, string routeTemplate, IInlineConstraintResolver inlineConstraintResolver) : base(target, routeTemplate, inlineConstraintResolver: inlineConstraintResolver) { } public PromoTemplateRoute(IRouter target, string routeTemplate, IDictionary<string, object> defaults, IDictionary<string, object> constraints, IDictionary<string, object> dataTokens, IInlineConstraintResolver inlineConstraintResolver) : base(target, routeTemplate, defaults, constraints, dataTokens, inlineConstraintResolver) { } public PromoTemplateRoute(IRouter target, string routeName, string routeTemplate, IDictionary<string, object> defaults, IDictionary<string, object> constraints, IDictionary<string, object> dataTokens, IInlineConstraintResolver inlineConstraintResolver) : base(target, routeName, routeTemplate, defaults, constraints, dataTokens, inlineConstraintResolver) { } public async override Task RouteAsync(RouteContext context) { var requestPath = context.HttpContext.Request.Path.Value ?? string.Empty; if (!requestPath.StartsWith("/promo", StringComparison.OrdinalIgnoreCase)) { return; } await base.RouteAsync(context); } }
为了方便使用,咱们也比葫芦画瓢,建立一些扩展方法,示例以下:
public static class RouteBuilderExtensions { public static IRouteBuilder MapPromoRoute(this IRouteBuilder routeCollectionBuilder, string name, string template) { MapPromoRoute(routeCollectionBuilder, name, template, defaults: null); return routeCollectionBuilder; } public static IRouteBuilder MapPromoRoute(this IRouteBuilder routeCollectionBuilder, string name, string template, object defaults) { return MapPromoRoute(routeCollectionBuilder, name, template, defaults, constraints: null, dataTokens: null); } public static IRouteBuilder MapPromoRoute(this IRouteBuilder routeCollectionBuilder, string name, string template, object defaults, object constraints, object dataTokens) { var inlineConstraintResolver = routeCollectionBuilder.ServiceProvider.GetService<IInlineConstraintResolver>(); routeCollectionBuilder.Routes.Add( new PromoTemplateRoute( routeCollectionBuilder.DefaultHandler, name, template, ObjectToDictionary(defaults), ObjectToDictionary(constraints), ObjectToDictionary(dataTokens), inlineConstraintResolver)); return routeCollectionBuilder; } private static IDictionary<string, object> ObjectToDictionary(object value) { var dictionary = value as IDictionary<string, object>; if (dictionary != null) { return dictionary; } return new RouteValueDictionary(value); } }
使用的时候,则很简单,和以前的方式很是相似,示例以下:
routes.MapPromoRoute( name: "default2", template: "promo/{controller}/{action}/{id?}", defaults: new { controller = "Home", action = "Index" });
经过这种方式,咱们能够在符合路由匹配条件的时候,使用PromoTemplateRoute
类来处理一些自定义逻辑,好比添加一些额外的文件头信息等等。
基于Attribute的Routing功能一直是MVC所期待的功能,在Web API已经经过RoutePrefix
(Controller上使用)和Route
(Action上使用)来实现了。该特性在MVC 6中进行了重写和加强,而且因为MVC和Web API合二而一了,因此在这两种Controller上均可以使用该特性。
举例来讲:
[Route("bookhome")] public class HomeController : Controller { public IActionResult Index() { return View(); } [Route("about")] public IActionResult About() { ViewBag.Message = "Your application description page."; return View(); } [Route("contactus")] public IActionResult Contact() { ViewBag.Message = "Your contact page."; return View(); } }
在上述Controller上定义一个bookhome前缀,而且在About和Contact上又分别定义了action名称,因此上述3个Action的访问地址则是以下这种形式:
/bookhome /bookhome/about /bookhome/contactus
在这里,咱们须要注意,Controller和Action使用的Attribute
都是Route
,同时,在这些路由模板字符串中,依然可使用内联参数,好比,咱们能够定义相似这样的路由:
[Route("products/{productId:int}")]
另外,针对Route的模板字符串,不只支持内联参数,还支持Controller和Action的标记位,即不用写死该Controller或Action的名称,使用一个[controller]
或[action]
的字符便可表示该Controller或Action的名称。好比,咱们能够在Controller上定义这样的一个路由(Action上什么都不定义):
[Route("book/[controller]/[action]")]
这样访问首页的地址就变成了:/book/Home/Index
。
在Web API中,咱们通常还要定义GET、POST这样的请求方式,为了方便,新版的HTTPGET等一系列方法都集成了Route功能,直接在构造函数传入Route模板便可,示例以下:
[HttpGet("products/{productId:int}")]
上述Route的定义,即代表,既要符合products/{productId:int}
的路由规则,又要是GET请求。
- 其实HTTPGET这一系列Attribute也能够在普通的MVC Controller上使用,由于在MVC6中,MVC Controller和Web API Controller自己就是同一个东西,只不过MVC的返回类型都是IActionResult而已。
- Route定义,不只仅支持GET请求,还支持POST等其它类型的请求,即不限制请求方式。
- 在HttpXXX系列特性中,也是支持内联参数和[controller]、[action]标记位的,大可放心使用。
- 目前可用的特性类有:HttpGet、HttpPost、HttpPut、HttpDelete、HttpPatch。
基于Attribute的Route定义很方便,但也很危险,具体规则和危险性以下。
规则1:Controller上定义了Route特性很危险
一旦在Controller上定义了Route特性,该Controller下的全部路由规则都不受其它规则控制了,好比,若是你定义了相似这样的
[Route("book")] public class HomeController : Controller { public IActionResult Index() { return View(); } public IActionResult About() { ViewBag.Message = "Your application description page."; return View(); } }
那么,上述2个Action你都再也没办法访问了,由于默认的action的名称根本就不会起做用,即/book/index
和/book/about
这两个路径没法路由到对应的Action方法上。并且/book
也访问不了,由于有两个以上的Action,系统没法定位到其中一个Action上。
因此要让上述Action能访问,必需要在其中一个Action上定义再Route,例如:
[Route("book")] public class HomeController : Controller { public IActionResult Index() { return View(); } [Route("about")] public IActionResult About() { ViewBag.Message = "Your application description page."; return View(); } }
这样,就能够经过/book/about
来访问About方法了,而访问/book
则能够访问默认的index方法了,由于该index方法是默认惟一一个没有定义路由的方法,因此他就是/book路由规则的默认Action。若是,有3个Action的话,则必需要至少给两个Action定义Route,示例以下:
[Route("book")] public class HomeController : Controller { [Route("index")] public IActionResult Index() { return View(); } [Route("about")] public IActionResult About() { ViewBag.Message = "Your application description page."; return View(); } public IActionResult Contact() { ViewBag.Message = "Your contact page."; return View(); } }
此时,Contact
方法就是默认/book
路由的Action了,访问/book
路径的话,就会显示Contact对应的页面。
规则2:Route和HttpGet能够一块儿使用,但也很危险
咱们前面提到,在Action上便可以使用Route特性,也可使用HttpGet特性,二者之间的不一样,就是多了一个Http Method。不少同窗能够要问两个特性在一块儿使用的时候会有问题么?
其实,这两个特性是能够在一块儿使用的,示例以下:
[Route("book")] public class HomeController : Controller { [Route("Contact")] [HttpGet("home/Contact2")] public IActionResult Contact() { ViewBag.Message = "Your contact page."; return View(); } }
这样/book/contact
和/book/home/contact2
这两个网址,均可以访问了。但若是这里定义HttpGet,状况就不同了,示例以下:
[Route("Contact")] [HttpPost("home/Contact2")]
此时,访问该Action的方式,要么是以GET的方式访问/book/contact
地址,要么是以POST的方式访问/book/home/contact2
。因此为了不出错,建议使用的时候不要讲二者混用,即使是要同时支持GET和POST,那也是建议用同类型的HttpXXX来定义这些路由,例如:
[HttpGet("Contact")] [HttpPost("home/Contact2")]
这样,看起来就清晰多了。
规则3:多个Route和多个HttpXXX也能够一块儿使用,但也很危险
在以下示例中,咱们为HomeController定义了2个Route特性,而Contact定义了2个Route特性和1个HttpPost特性。
[Route("book")] [Route("tom")] public class HomeController : Controller { [Route("Contact")] [Route("ContactUS")] [HttpPost("home/Contact2")] public IActionResult Contact() { ViewBag.Message = "Your contact page."; return View(); } }
那么,在上述代码生效后,咱们将有六种访问来访问该Action,这六种方式分布以下:
GET:/book/contact GET:/book/contactus GET:/tom/contact GET:/tom/contactus POST:/book/home/contact2 POST:/tom/home/contact2
可是,在视图文件中,经过@Html.ActionLink("Contact", "Contact", "Home")
生成连接地址的话,则默认会使用第一个定义的Route,若是要强制指定顺序,则可使用Order属性来定义排序值,默认会优先使用最小的值。示例以下:
[Route("book", Order = 1)] [Route("tom", Order = 0)] public class HomeController : Controller { [Route("Contact", Order = 1)] [Route("ContactUS", Order = 0)] [HttpPost("home/Contact2", Order = 2)] public IActionResult Contact() { ViewBag.Message = "Your contact page."; return View(); } }
在前面的介绍中,咱们知道任意类型的路由在定义的时候都支持不一样的内联参数约束,由于这些约束是基于ASP.NET 5的,而不是基于MVC6的,而且这些约束仍是能够扩展的,本节咱们就来看看如何自定义一些扩展。
首先,咱们来看一个比较简单的约束,即无参数约束,相似于{productId:int}
这样的类型约束,假设咱们要实现一个AABBCC字符串限定的约束,示例以下:
[Route("index/{productId:aabbcc}")]
为了确保/index/112233和/index/aabbcc是符合约束的,而/index/aabbccdd是不符合约束的,咱们首先要自定义一个约束类AABBCCRouteConstraint
,并实现IRouteConstraint
接口,示例以下:
public class AABBCCRouteConstraint : IRouteConstraint { public bool Match(HttpContext httpContext, IRouter route, string routeKey, IDictionary<string, object> values, RouteDirection routeDirection) { bool b = false; object value; if (values.TryGetValue(routeKey, out value) && value != null) { if (value is string) // 获取传入的值,好比aabbcc或112233 { string aabbcc = value.ToString(); b = !string.IsNullOrWhiteSpace(aabbcc) && aabbcc.Length == 6 && aabbcc[0] == aabbcc[1] && aabbcc[2] == aabbcc[3] && aabbcc[4] == aabbcc[5]; } } return b; } }
在该实现类中,要实现Match方法,根据传入的各类参数,判断是否符合定义的约束,并返回true或false,Match方法的参数中,其中routeKey
是约束{productId:aabbcc}
对应的参数名称(本例中是productId),values集合中会有该productId所对应的数字(如112233),在该方法经过响应的判断返回true和false。
下一步,就是要将该约束类注册到Routing系统的约束集合中,在Startup.cs
的ConfigureServices
方法中,执行以下语句:
services.Configure<RouteOptions>(opt => { opt.ConstraintMap.Add("aabbcc", typeof(AABBCCRouteConstraint)); });
注意,这里注册的aabbcc
就是前面咱们所指定约束名称,完成上述步骤之后,便可实现相似{productId:int}
的功能了。
通常状况下,有些时候可能须要定义一些约束的值,好比Length(1,10)
来表示1-10之间的字符串长度,举例来讲,加入咱们要定义一个4个参数的约束规则,如abcd(1,10,20,30)
来表示一个特殊的验证项,则须要声明有4个参数的构造函数,示例以下:
public class ABCDRouteConstraint : IRouteConstraint { public int A { get; private set; } public int B { get; private set; } public int C { get; private set; } public int D { get; private set; } public ABCDRouteConstraint(int a, int b, int c, int d) { A = a;B = b;C = c;D = d; } public bool Match(HttpContext httpContext, IRouter route, string routeKey, IDictionary<string, object> values, RouteDirection routeDirection) { bool b = false; object value; if (values.TryGetValue(routeKey, out value) && value != null) { var valueString = value.ToString();//这里须要进行进一步的验证工做 return true; } return b; } }
假如你在Action上了定义了以下约束:
[Route("index/{productId:abcd(1,20,30,40)}")]
那么,在注册该约束类型之后,系统启动厚扫描全部的Route进行注册的时候,会分析你定义的这4个值,而后会将这4个值赋值给该路由对应的约束实例上的A、B、C、D四个属性上,以便在HTTP请求过来的时候,分析URL上的值,看是否符合Match里定义的规则(在验证的时候就可使用这4个属性值)。
默认约束的全部代码能够参考: https://github.com/aspnet/Routing/tree/dev/src/Microsoft.AspNet.Routing/Constraints
另外,若是定义了4个参数的约束,那么在action上定义路由的时候则必须符合参数的数据类型,若是不符合,系统启动的时候就会出错,示例错误以下:
[Route("index/{productId:abcd}")] //没有为该对象定义无参数的构造函数 [Route("index/{productId:abcd(a)}")] [Route("index/{productId:abcd('a')}")] //输入字符串的格式不正确 [Route("index/{productId:abcd(1,2,3)}")] //构造函数的参数个数和定义的参数个数不一致。
若是你定义的参数类型是字符串类型,则下面2种形式的定义都是合法的:
[Route("index/{productId:abcd(a,b,c,d)}")] [Route("index/{productId:abcd('a','b','c','d')}")]
虽然ASP.NET 5 和MVC6的路由使用方式很简单,可是相关的使用规则却很复杂,你们使用的时候须要多加注意。
本文已同步至目录索引:解读ASP.NET 5 & MVC6系列