在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系列