一、路由介紹
ASP.NET Web API路由是整個API的入口。我們訪問某個資源就是通過路由映射找到對應資源的URL。通過URL來獲取資源的。
對於ASP.NET Web API內部實現來講,我們的請求最終將定位到一個具體的Action上。所以說,ASP.NET Web API路由就是把客戶端請求映射到對應的Action上的過程。
二、兩種路由模式
2.1 模板路由
模板路由是ASP.NET Web API默認提供的路由。下面我們就簡單講解此中路由的用法。
默認模板路由
模板路由使用前需要定義路由模板。如下面默認的路由模板:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http; namespace Supernova.Webapi { public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Web API 配置和服務 // Web API 路由 config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } } }
此模板路由是新建項目默認生成的,在App_Start文件夾下。
我們可以看到此模板的URL格式是api/{controller}/{id}。api代表在資源前面要帶上api目錄,controller代表請求資源的控制器名稱。id代表一條資源的id,id 是可選的。這種默認的模板是不帶action的,所以它是以請求方式來區分資源的,我們必須在action上添加請求方式特性加以區分。
1.我們添加一個測試控制器api。
public class TestController : ApiController { public object Get1() { return "d1"; } }
用fiddldr調試如下:
2.我們添加兩個方法如下:
public class TestController : ApiController { public object Get1() { return "d1"; } public object Get2() { return "d2"; } }
我們再用fiddler調試如下:
錯誤信息是:
{"Message":"出現錯誤。","ExceptionMessage":"找到了與該請求匹配的多個操作: \r\n類型 Supernova.Webapi.Controllers.TestController 的 Get1\r\n類型 Supernova.Webapi.Controllers.TestController 的 Get2","ExceptionType":"System.InvalidOperationException","StackTrace":" 在 System.Web.Http.Controllers.ApiControllerActionSelector.ActionSelectorCacheItem.SelectAction(HttpControllerContext controllerContext)\r\n 在 System.Web.Http.ApiController.ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken)\r\n 在 System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext()"}
我們將代碼改為如下:
public class TestController : ApiController { public object Get1() { return "d1"; } [HttpPost] public object Get2() { return "d2"; } }
調試返回Get1的信息。
從上面兩個測試我們可以得出如下結論:
- action的默認請求方式是HttpGet。
- 當多個action的 請求方式一樣的話,在默認路由模板下(沒有action),將會匹配多個操作。
- 基於上面兩點結論,默認路由模板無法滿足針對一種資源一種請求方式的多種操作(比如修改操作,可能針對不同的字段進行修改)。
定制模板路由
我們重新定制模板路由,如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http; namespace Supernova.Webapi { public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Web API 配置和服務 // Web API 路由 config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{action}/{id}", defaults: new { id = RouteParameter.Optional } ); } } }
從上面我們可以看出,在默認路由的基礎上,我們隊路由模板增加了一級action。
測試api如下:
public class TestController : ApiController { public object Get1() { return "d1"; } public object Get2() { return "d2"; } }
我們再通過http://192.168.0.230/api/test訪問,返回404,如圖:
我們通過http://192.168.0.230/api/test/Get1訪問,結果正確,如圖:
我們通過http://192.168.0.230/api/test/Get2訪問,結果正確,如圖:
通過定制路由模板我們可以得出如下結論:
- 通過在路由模板中增加action目錄,對資源的定位直接作用到action上。
- 多個HttpGet方法可以共存於一個controller中。
- 基於上面兩點結論,通過修改路由模板可以滿足針對一種資源一種請求方式的多種操作。
2.2 特性路由
特性路由是通過給action打attribute的方式定義路由規則。
有時候我們會有這樣的需求,我們請求的一個資源帶有子資源。比如文章評論這樣有關聯關系的資源。我們希望通過如下URL獲得某篇文章下的所有評論:api/book/id/comments。而僅僅憑借模板路由很難實現這種路由模式。這時候我們就需要特性路由來解決這個問題了。ASP.NET Web API為我們准備了Route特性,該特性可以直接打到Action上,使用非常靈活、直觀。
下面我將先簡單的介紹特性路由的使用方法。
我們重新定義api如下:
public class TestController : ApiController { [Route("demo")] [HttpGet] public object Get1() { return "d1"; } [Route("demo/get")] [HttpGet] public object Get2() { return "d2"; } }
我們可以看出,在action打上了標簽。
使用fiddler調試如下:
請求Get1的URL是http://192.168.0.230/demo
請求Get2的URL是http://192.168.0.230/demo/get
2.3 兩種路由的對比
- 模板路由針對於簡單的業務非常方便,但是對於復雜的資源操作略顯余力不足。
- 特性路由可以在不更改action名稱的情況下靈活修改。
- 特性路由對於關聯資源可以創建友好的URL。
- 特性路由可以對於路由參數的約束,起到精細化控制。
三、特性路由詳解
3.1 使用Route特性
使用特性路由很簡單,不需要做額外的配置,只需要在action上打上Route標簽就可以了。這樣模板路由就自動失效了。
如下:
public class TestController : ApiController { [Route("demo")] [HttpGet] public object Get1() { return "d1"; } [Route("demo/get")] [HttpGet] public object Get2() { return "d2"; } }
3.2 使用RoutePrefix特性
有時候我們想對某個資源的所有操作都加上一個統一的前綴。
第一種方式:
public class TestController : ApiController { [Route("api/demo")] [HttpGet] public object Get1() { return "d1"; } [Route("api/demo/get")] [HttpGet] public object Get2() { return "d2"; } }
這種方式看起來還可以哈,就是有點弱智。那么我們就可以使用RoutePrefix將特性加在controller上面,那么對資源的請求就要加上api目錄了。
第二種方法:
[RoutePrefix("api")] public class TestController : ApiController { [Route("demo")] [HttpGet] public object Get1() { return "d1"; } [Route("demo/get")] [HttpGet] public object Get2() { return "d2"; } }
3.3 重寫Action的前綴規則
3.2中的方法可以對某個資源的前面統一加上 前綴。那么問題來了,如果我們還會有這樣的需求,我的某個資源中的大部分請求都需要前綴,但是就是有那么一兩個資源不需要加前綴,腫么辦?其實微軟早就給我們想到了,人家說了,當然允許你重寫action前綴啊。
如下代碼,我們重新了Get1:
[RoutePrefix("api")] public class TestController : ApiController { [Route("~/demo")] [HttpGet] public object Get1() { return "d1"; } [Route("demo/get")] [HttpGet] public object Get2() { return "d2"; } }
用fiddler調試如下:
報錯了吧,正確的URL其實是:http://192.168.0.230/demo
3.4.1 路由參數約束
現在問題又來了,那么多的請求,特別是Get請求方式,都需要帶參數啊,怎么定義參數的類型,長度范圍等約束條件呢?
答案是可以通過"{參數變量名稱:約束}"來約束路由中的參數變量。
ASP.NET Web API內置約束包括:
{x:alpha} 約束大小寫英文字母
{x:bool}
{x:datetime}
{x:decimal}
{x:double}
{x:float}
{x:guid}
{x:int}
{x:length(6)}
{x:length(1,20)} 約束長度范圍
{x:long}
{x:maxlength(10)}
{x:min(10)}
{x:range(10,50)}
{x:regex(正則表達式)}
如下代碼:
[RoutePrefix("api")] public class TestController : ApiController { [Route("demo/{id:int}")] [HttpGet] public object Get1() { return "d1"; } [Route("demo/{name}")] [HttpGet] public object Get2() { return "d2"; } }
以上,如果片段變量id為int類型,就路由到第一個Action Get1,如果不是,路由到第二個Action Get2。
使用fiddler調試如下:
請求是Get1.
請求的是Get2
可以為一個參數變量同時設置多個約束:
如下代碼:
[RoutePrefix("api")] public class TestController : ApiController { [Route("demo/{id:int:min(5)}")] [HttpGet] public object Get1() { return "d1"; } [Route("demo/{name}")] [HttpGet] public object Get2() { return "d2"; } }
請求URL:http://192.168.0.230/api/demo/1 定位到Get2
3.4.2 擴展路由參數約束
實現IHttpRouteConstraint接口,可自定義約束規則。實現一個不能為0的約束。
代碼如下:
public class NonZeroConstraint : IHttpRouteConstraint { public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection) { object value; if (values.TryGetValue(parameterName, out value) && value != null) { long longValue; if (value is long) { longValue = (long)value; return longValue != 0; } string valueString = Convert.ToString(value, CultureInfo.InvariantCulture); if (Int64.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out longValue)) { return longValue != 0; } } return false; } }
在App_Start文件夾中的WebApiConfig中注冊自定義約束。必須要注釋原先的模板路由
public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Web API 配置和服務 // Web API 路由 //config.MapHttpAttributeRoutes(); //config.Routes.MapHttpRoute( // name: "DefaultApi", // routeTemplate: "api/{controller}/{action}/{id}", // defaults: new { id = RouteParameter.Optional } //); var constraintResolver = new DefaultInlineConstraintResolver(); constraintResolver.ConstraintMap.Add("nonzero", typeof(NonZeroConstraint)); config.MapHttpAttributeRoutes(constraintResolver); } }
測試代碼如下:
[RoutePrefix("api")] public class TestController : ApiController { [Route("demo/{id:nonzero}")] [HttpGet] public object Get1() { return "d1"; } [Route("demo/{name}")] [HttpGet] public object Get2() { return "d2"; } }
使用URL:http://192.168.0.230/api/demo/0 定位到Get2
3.5 可選參數及其默認值
有時候,我們請求的參數是可選的,怎么辦呢,我們就需要給參數設置默認值來處理了。
代碼如下:
[RoutePrefix("api")] public class TestController : ApiController { [Route("demo/{id:int?}")] [HttpGet] public object Get1(int id=1) { return "d1"+id; } [Route("demo/{name}")] [HttpGet] public object Get2() { return "d2"; } }
當參數不存在或者為int類型時定位的是Get1,當參數存在不為int時定位的是Get2.
URL:http://192.168.0.230/api/demo 定位 Get1
URL:http://192.168.0.230/api/demo/2 定位 Get1
URL:http://192.168.0.230/api/demo/abc 定位 Get2
3.6 給路由設置名稱
[RoutePrefix("api")] public class TestController : ApiController { [Route("demo/{id:int?}",Name="通過ID獲取內容")] [HttpGet] public object Get1(int id = 1) { return "d1" + id; } [Route("demo/{name}")] [HttpGet] public object Get2() { return "d2"; } }
3.7 路由優先順序
Route特性設置的路由優先順序是根據慣例和RouteOrder屬性來確定的。
慣例是:
1、靜態片段變量
2、帶約束的片段變量
3、不帶約束的片段變量
4、帶約束的通配符片段變量
5、不帶約束的通配符片段變量
RouteOrder屬性的默認值是0,屬性值越小,排在越前面。
測試代碼如下,按照優先級來的:
[RoutePrefix("api")] public class TestController : ApiController { [Route("orders/detail", Name = "靜態片段變量")] [HttpGet] public object Get1() { return "orders/detail"; } [Route("orders/{id:int}", Name = "帶約束的片段變量")] [HttpGet] public object Get2(int id) { return "orders/{id:int}"; } [Route("orders/{name}", Name = "不帶約束的片段變量")] [HttpGet] public object Get3(string name) { return "orders/{name}"; } [Route("orders/lily", Order = 1)] [HttpGet] public object Get4() { return "orders/lily"; } }
URL:http://192.168.0.230/api/orders/detail 定位 Get1 靜態片段變量 Order=0
URL:http://192.168.0.230/api/orders/lily定位 Get3 帶約束的片段變量 Order=0
URL:http://192.168.0.230/api/orders/1定位 Get2 不約束的片段變量 Order=0
Get3包含了Get4的定義,所以說永遠也無法定義到Get4。這也是在特性路由中需要特別注意的地方。
3.8 路由設計規范
1.URL中不能出現動詞。
參考:
http://www.eggtwo.com/news/detail/155
https://docs.microsoft.com/en-us/aspnet/web-api/overview/web-api-routing-and-actions/routing-in-aspnet-web-api
http://www.cnblogs.com/n-pei/archive/2012/07/17/2595352.html