路由匹配主要有三個階段: git
1.將URI匹配到一個路由模版; web
2.選擇一個controller 正則表達式
3.選擇一個action; 算法
可使用系統提供的拓展點,修改默認的匹配與選擇邏輯規則。 api
路由模版: 框架
路由模版很像一個URI可是它能夠包含使用大括號包裹的佔位符。ui
"api/{controller}/public/{category}/{id}"
咱們能夠定義佔位符的默認值,defaults: new { category = "all" } spa
可使用正則表達式定義約束條件:constraints: new { id = @"\d+" } // Only matches if "id" is one or more digits. .net
模版中的非佔位符,文本部分必須嚴格匹配,佔位符能夠匹配任何值,除非你指定了約束條件。Web API 路由不會匹配URI的其餘部分,好比主機名,和查詢變量(一個uri : http://www.badiu.com/api/products/public/cat1/2?para1=a¶2=b,路由只關注紅色的部分。框架會使用第一個匹配的模版(也就是說,路由模版記錄是有順序的)。 code
有兩個特殊的佔位符那就是{controller} 和 {action},很容易理解:
1.{controller}提供controller的名稱;
2.{action}提供action的名稱,在Web API中一般的慣例是省略"{action}";
暫時能夠這樣理解,除了{controller} 和 {action}兩個佔位符,其餘的佔位符通常用在匹配傳遞的參數(給uri中的參數命名以匹配action方法的參數,用做模型綁定用)。
路由字典(Route Dictionary)
當框架找到一個可以匹配URI的路由模版,它會建立一個字典類型對象來存儲每個佔位符以及佔位符匹配到的值,key值對應不包含大括號的佔位符名稱,vaule則爲佔位符在uri匹配到的值或者提供默認值。這個字典存儲在一個IHttpRouteData類型的對象中。
在這個環節,特殊的佔位符{controller}和{action}被當作普通的佔位符對待,存儲在字典中。
在給佔位符指定默認值時,能夠給它指定 RouteParameter.Optional值,若是給一個佔位符指定了這個值,說明這個參數是可選的,若是不URI中沒有指定這個參數,那麼這個佔位符以及他的值將不會添加到路由字典中去。好比:
routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{category}/{id}", defaults: new { category = "all", id = RouteParameter.Optional } );
若是uri爲"api/products",那麼路由字典包含:
controller:"products"
category:"all"
若是uri爲"api/products/123",那麼路由指定包含:
controller: "products"
category: "toys"
id: "123"
也就是說當指定一個佔位符的默認值是:RouteParameter.Optional 時,uri沒有爲該佔位符提供值時,字典中不包含該鍵值對,uri爲該佔位符提供值的時候,字典中包含該鍵值對。
還能夠爲沒在路由模版中出現的佔位符指定默認值:
routes.MapHttpRoute( name: "Root", routeTemplate: "api/root/{id}", defaults: new { controller = "customers", id = RouteParameter.Optional } );
這樣匹配的controller老是爲customers,路由字典包含:
controller: "customers"
id: "8"
控制器的選擇(Selecting a Controller)
控制器的選擇由 IHttpControllerSelector.SelectController 方法處理,這個方法接收一個HttpRequestMessage對象返回一個HttpControllerDescriptor對象。SelectController方法默認的實現由DefaultHttpControllerSelector類提供,這個類使用簡單的控制器選擇算法:
1.在路由字典中查找是否有鍵名爲"controller"。
2.獲取這個controller健對應的值,與「Controller」字符串組合成一個新字符串,經過這個字符串得到控制器類名稱。
3.在Web API的控制器查找相同類型名稱的控制器。
例如:路由字典中包含鍵值對"controller"="products",那麼控制器類型即爲"ProductsController"。若是有沒有匹配的類型,獲取有多個匹配的類型,框架返回一個錯誤給客戶端。
在第三步中,DefaultHttpControllerSelector使用IHttpControllerTypeResolver接口去獲取一個當前項目Web API控制器類型的列表。IHttpControllerTypeResolver默認的實現方法返回:
--全部實現了IHttpController的公開的(public)控制器。
--且.非抽象類型(are not abstract);
--且.命名以"Controller"結尾。
Action的選擇(Action Selection)
在完成控制器的選擇後,框架經過調用IHttpActionSelector.SelectAction方法來選擇action,這個方法接收一個HttpControllerContext類型參數,返回一個HttpActionDescriptor類型對象,方法默認的實現由ApiControllerActionSelector類提供,選擇Action的依據:
--HTTP請求方法(GET POST PUT ....)
--路由模版中是否有{action}佔位符
--控制器中action方法的參數;
在瞭解action的選擇算法以前,咱們須要理解控制器 action的一些知識。
在控制器中什麼樣的方法能夠做爲action方法?
--控制器中的公共(public)方法,但不包括特殊名稱(constructors, events, operator overloads, and so forth)的方法,和從ApiController中繼承的方法。
HTTP請求方式:框架只選擇可以匹配HTTP請求方式的action,由如下幾個因素決定:
1.能夠指定action方法能夠匹配的HTTP請求方式,用 AcceptVerbs, HttpDelete, HttpGet, HttpHead, HttpOptions, HttpPatch, HttpPost, 或者 HttpPut這些標識屬性。
2.action方法名稱以"Get", "Post", "Put", "Delete", "Head", "Options", 或者 "Patch" 開頭。
3.若是action不知足上面兩個條件,action方法默認處理POST請求。
參數綁定:Web API如何給action方法選擇參數值,默認規則是:
--簡單類型的參數值從URI中獲取
--複雜類型的值從請求主體(request body)中獲取;
簡單類型包括全部.net 的基本類型( .NET Framework primitive types)以及 DateTime,Decimal,Guid,String和TimeSpan類型。對於一個action,最多隻有一個參數可以讀取請求主體(request body)???
修改默認的參數綁定規則,看WebAPI Parameter binding under the hood.
有了以上的知識,下面來看action的選擇算法:
1.建立一個匹配當前請求方式的控制器中的action列表集合;
2.若是路由字典中有"action"鍵,再去除名稱與字段中action鍵值不匹配的action;
3.嘗試將action方法的參數與URI中傳遞的參數匹配:
--循環一個action,獲取簡單類型的參數列表(不包括可選類型參數)
--嘗試將列表中的參數名與路由字典以及URI中查詢參數進行匹配,這個匹配忽略大小寫,以及參數順序。
--選擇一個:action方法的每個參數都在路由字典或者URI查詢參數中匹配到值的action。
--若是有多個action方法匹配成功,選擇參數(不包括可選參數)最多的那個。
4.忽略帶有[NonAction]標識屬性的方法。
第三步,基本的意思是,一個action方法的參數可以從URI,請求主體(request body),或者自定義綁定(custom binding)獲取它的參數值,對於來自URI中的參數,咱們但願肯定URI中是否包含一個當前action參數的值,不管是包含在路徑(路由字典)中仍是在查詢參數中(query string)。(也就是這裏的URI參數包括路由字典中的參數和查詢字符串中的參數)
好比如今有一個action
public void Get(int id)
參數id須要綁定URI傳遞的信息,所以這個action只能匹配一個包括"id"值的URI(id值能夠在路由路徑中或者查詢參數中)。
可選參數除外,若是URI中有爲可選參數提供值則綁定,若是沒有也沒有影響,可選參數使用默認值。
複雜類型參數是一個例外(action選擇時不會考慮複雜類型參數),複雜類型參數必須手工綁定,這個操做在action選擇以後,因此在選擇action時不會考慮複雜類型參數的匹配問題。
總結:
--action必須匹配HTTP請求方式
--若是指定了{action}佔位符,action的名稱必須與URI爲佔位符提供值相同;
--對於action的每個參數,若是參數值從URI中獲取,那麼參數名稱必須能夠在路由字典或者URI查詢條件(query string)中找獲得(可選參數和複雜類型參數除外),這裏有一個問題,請求主體(request body)中提供的鍵值對信息可不能夠匹配action的參數???
--嘗試匹配參數最多額action,最佳匹配方法可能沒有參數。
綜合實例:
路由模版定義以下:
routes.MapHttpRoute( name: "ApiRoot", routeTemplate: "api/root/{id}", defaults: new { controller = "products", id = RouteParameter.Optional } ); routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } );
控制器:
public class ProductsController : ApiController { public IEnumerable<Product> GetAll() {} public Product GetById(int id, double version = 1.0) {} [HttpGet] public void FindProductsByName(string name) {} public void Post(Product value) {} public void Put(int id, Product value) {} }
HTTP請求:
GET http://localhost:34701/api/products/1?version=1.5&details=1
這個URI匹配 "DefaultApi"路由模版,路由字典中包含:
controller:"products"
id:"1"
雖然路由字典不包含"version" 和 "details" 但在action選擇時仍是會考慮這兩個參數。
控制器選擇:ProductsController。
action選擇:首先看Http請求方式 ,這裏是GET方式,因此知足要求的只有GetAll ,GetById 和FindProductsByName 。再看路由模版中是否包含{action}佔位符,這裏不包含因此不用管action的名稱,最後咱們看知足要求的三個action的要求URI包括的參數,GetAll 沒有參數,GetById 要求URI中包含id參數,FindProductsByName要求URI中包含name參數值,由於這裏URI中不包含name參數值,因此排除FindProductsByName這個action,最後看匹配參數的個數,由於GetById匹配的參數個數1大於GetAll匹配參數個數0。因此最終選擇GetById。(這裏由於GetById的version參數是可選參數,選擇action根本不會考慮這個參數,但綁定參數時會使用URI傳遞的參數值)。
一個問題可選參數是否影響action的選擇,好比上面的例子中若是URI還包括一個name參數值。會選擇哪一個action 仍是會報錯??
結果:不影響。好比:同時包括id和name參數時報Multiple actions were found錯誤
拓展點:
Web API 爲路由過程控制提供拓展點:
接口 |
描述 |
IHttpControllerSelector |
選擇控制器 |
IHttpControllerTypeResolver |
獲取控制器類型列表,DefaultHttpControllerSelector從獲取的列表中選擇控制器類型 |
IAssembliesResolver |
獲取項目組件的列表,IHttpControllerTypeResolver接口使用這個列表去find控制器類型 |
IHttpControllerActivator |
建立一個新的控制器對象 |
IHttpActionSelector |
選擇action |
IHttpActionInvoker |
調用action |
你能夠在自定義的類中實現這些接口,而後使用HttpConfiguration對象的Services集合引用自定義的類來覆蓋默認的實現類
var config = GlobalConfiguration.Configuration; config.Services.Replace(typeof(IHttpControllerSelector), new MyControllerSelector(config));