1. 程式人生 > >C#進階系列——WebApi 路由機制剖析:你準備好了嗎?

C#進階系列——WebApi 路由機制剖析:你準備好了嗎?

事先 blank path can tex 全局配置 dex 找不到 save

前言:從MVC到WebApi,路由機制一直是伴隨著這些技術的一個重要組成部分。

它可以很簡單:如果你僅僅只需要會用一些簡單的路由,如/Home/Index,那麽你只需要配置一個默認路由就能簡單搞定;

它可以很神秘:你的url可以千變萬化,看到一些看似“無厘頭”的url,感覺很難理解它如何找到匹配的action,例如/api/user/1/detail,這樣一個url可以讓你糾結半天。

它可以很晦澀:當面試官提問“請簡單分析下MVC路由機制的原理”,你可能事先就準備好了答案,然後劈裏啪啦一頓(型如:UrlRoutingModule→Routes→RouteData→RequestContext→Controller),你可能回答很流利,但並不一定能理解這些個對象到底是啥意思。兩年前的面試,博主也這樣做過。

博主覺得,究竟路由機制在你的印象中處於哪一面,完全取決於你的求知欲。路由機制博大精深,博主並未完全理解,但博主是一個好奇心重的人,總覺得神秘的東西就得探索個究竟。今天,博主根據自己的理解,分享下WebApi裏面路由的原理以及使用,如有考慮不周,歡迎園友們指正。

WebApi系列文章

  • C#進階系列——WebApi接口測試工具:WebApiTestClient
  • C#進階系列——WebApi 跨域問題解決方案:CORS
  • C#進階系列——WebApi身份認證解決方案:Basic基礎認證
  • C#進階系列——WebApi接口傳參不再困惑:傳參詳解
  • C#進階系列——WebApi接口返回值不困惑:返回值類型詳解
  • C#進階系列——WebApi異常處理解決方案
  • C#進階系列——WebApi 路由機制剖析:你準備好了嗎?
  • C#進階系列——WebApi區域Area使用小結

一、MVC和WebApi路由機制比較

1、MVC裏面的路由

在MVC裏面,默認路由機制是通過url路徑去匹配對應的action方法,比如/Home/GetUser這個url,就表示匹配Home這個Controller下面的GetUser方法,這個很好理解,因為在MVC裏面定義了一個默認路由,在App_Start文件夾下面有一個RouteConfig.cs文件

技術分享

    public class RouteConfig
    {
        
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Department", action = "Index", id = UrlParameter.Optional } ); } }

url: "{controller}/{action}/{id}"這個定義了我們url的規則,{controller}/{action}定義了路由的必須參數,{id}是可選參數

2、WebApi裏面的路由

和MVC裏面的路由有點不同,WebApi的默認路由是通過http的方法(get/post/put/delete)去匹配對應的action,也就是說webapi的默認路由並不需要指定action的名稱。還是來看看它的默認路由配置,我們新建一個Webapi項目,在App_Start文件夾下面自動生成一個WebApiConfig.cs文件:

技術分享

    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API 路由
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }

和MVC類似,routeTemplate: "api/{controller}/{id}"這個定義了路由的模板,api/{controller}是必選參數,{id}是可選參數,那麽問題就來了,如果我們的url不包含action的名稱,那麽如何找到請求的方法呢?我們先來簡單看一個例子:

    public class OrderController : ApiController
    {
        [HttpGet]
        public object GetAll()
        {
            return "Success";
        }
    }

我們通過url來訪問

技術分享

說明請求能夠成功。

為什麽這個請求能夠成功呢?那是因為,當我們訪問http://localhost:21528/api/Order這個路徑的時候,webapi的路由引擎會自動去匹配"api/{controller}/{id}"這個模板,於是找到了控制器是Order這個,那麽問題來了?它是如何定位到GetAll()這個方法的呢?這裏就是和MVC不同的地方,前面說過,Webapi的路由規則是通過http方法去匹配對應的action,那麽,我們通過瀏覽器訪問http://localhost:21528/api/Order這個路徑的時候,瀏覽器默認通過url訪問的都是get請求,於是webapi的路由引擎就會去找Order這個控制器裏面的get請求的方法,由於沒有參數,所以自動匹配到了無參數的get請求→GetAll()方法,所以請求成功!

當然,WebApi也支持MVC裏面的路由機制,但RestFul風格的服務要求請求的url裏面不能包含action,所以,在WebApi裏面是並不提倡使用MVC路由機制的。

這是一個最簡單的例子,下面我們就來詳細看看WebApi裏面的路由原理以及使用。

二、WebApi路由基礎

1、默認路由

上面我們提到了,新建一個WebApi服務的時候,會自動在WebApiConfig.cs文件裏面生成一個默認路由:

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

將MapHttpRoute()方法轉到定義可以,它有四個重載方法:

技術分享

分別來看看各個參數的作用:

  • name:"DefaultApi"→表示此路由的名稱,這裏只需要保證路由名稱不重復就OK了。
  • routeTemplate: "api/{controller}/{id}"→表示路由的url規則,“api”是固定部分,主要用來標識當前請求的url是一個api服務的接口,區別MVC的路由,當然,這裏並不是一定要寫成“api”,如果你改成“apiserver”,那麽你請求的url裏面也需要寫成“apiserver”;“{controller}”是控制器的占位符部分,在真實的url裏面,該部分對應的是具體的控制器的名稱,這個和MVC裏面一致;“{id}”是參數的占位符部分,表示參數,一般這個參數都會在default裏面設置可選。有了這個路由模板約束請求的url,比如:我們請求的url寫成http://localhost:21528/Order,那麽肯定是找不到對應的路由的,因為“api”這個參數必選。如果請求的url匹配不到對應的路由,則會向客戶端返回一個404的狀態碼。
  • defaults: new { id = RouteParameter.Optional }→表示路由的默認值,比如上面的routeTemplate,{controller}和{id}部分都可以設置默認值,比如:defaults改成new { controller="Order", id = RouteParameter.Optional },那麽我們請求http://localhost:21528/api這個url仍然能訪問到GetAll()方法。
  • constraints→表示路由約束,一般是一個約束路由模板的正則表達式。比如:我們加入約束條件 constraints: new { id = @"\d+" } ,這就約束必須要匹配一到多個參數id,那麽,我們在OrderController裏面加入另一個方法
    public class OrderController : ApiController
    {

        [HttpGet]
        public object GetAll()
        {
            return "Success";
        }

        [HttpGet]
        public object GetById(int id)
        {
            return "Success" + id ;
        }
    }

我們通過http://localhost:21528/api/Order/2來訪問,得到結果:

技術分享

我們再通過http://localhost:21528/api/Order/a來訪問,得到結果:

技術分享

這個是很好理解的,id的值不匹配正則表達式。

而我們訪問http://localhost:21528/api/Order。結果:

技術分享

竟然連GetAll()方法都找不到了。這是為什麽呢?原來就是這個約束在作怪,正則\d+表示匹配一個或多個數字,所以如果請求的url裏面沒有傳數字,則自動匹配不到。所以,如果需要匹配無參的方法,我們把約束改成這樣: constraints: new { id = @"\d*" } ,這個表示匹配0個或多個數字,再來試試

技術分享

這樣就OK了。

上述說了那麽多都是約束id的,其實你也可以使用表達式去約束controller、action等等,但一般不常用,我們就不做過多講解。

2、自定義路由

上面介紹了這麽多,都是關於默認路由原理的介紹。除了默認路由,我們也可以自定義路由,我們將WebApiConfig.cs裏面改成這樣:

    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API 路由
            config.MapHttpAttributeRoutes();

            //1.默認路由
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            //2.自定義路由一:匹配到action
            config.Routes.MapHttpRoute(
                name: "ActionApi",
                routeTemplate: "actionapi/{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            //3.自定義路由二
            config.Routes.MapHttpRoute(
                name: "TestApi",
                routeTemplate: "testapi/{controller}/{ordertype}/{id}",
                defaults: new { ordertype="aa", id = RouteParameter.Optional }
            );
        }
    }

除了默認路由,我們再加入另外兩個自定義路由規則

2.1、自定義路由一:匹配到action

第一個自定義路由很好理解,和MVC裏面的路由機制保持一致,只不過為了區別默認路由,我們將路由模板的前綴改成了“actionapi”。我們通過這個自定義的路由也能找到匹配的方法。

比如我們訪問http://localhost:21528/actionapi/Order/GetAll,得到結果:

技術分享

通過action的名稱來匹配很好理解,上面的GetAll()是方法名,webApi會默認它就是action的名稱,如果你想要方法名和action的名稱不一致,你也可以自定義action的名稱,這個可以通過特性ActionName來實現,如下:

        [ActionName("TestActionName")]
        [HttpGet]
        public object GetById(int id)
        {
            return "Success" + id ;
        }

測試結果:

技術分享

之前博主演示參數和返回值的時候都是使用的匹配到action的路由。這種用法和MVC裏面保持一致,比較好理解,但是WebApi裏面並不提倡。

2.2、自定義路由二

第二個自定義路由第一眼看上去是不太好理解的,沒關系,我們先來按照它的路由模板規則使用試試。

技術分享

通過http://localhost:21528/testapi/Order/aa/匹配到GetAll()方法

技術分享

通過http://localhost:21528/testapi/Order/aa/2匹配到的是GetById()方法

技術分享

通過http://localhost:21528/testapi/Order/bb/2匹配到的也是GetById()方法。

什麽意思呢?也就是說,只要{ordertype}按照路由規則去配置,都能找到對應的方法。這裏的{ordertype}有什麽用呢?這個要留在下面介紹特性路由的時候來解釋。

3、路由原理

有了上面的這些理論作為基礎,我們再來分析下WebApi裏面路由機制的原理以及路由匹配的過程。由於WebApi的路由機制和MVC有許多的相似性,所以要想理解Webapi的路由機制,有需要搬出來那些asp.net Rounting裏面的對象。這個過程有點復雜,博主就根據自己的理解,提提一些主要的過程:

1、WebApi服務啟動之後,會執行全局配置文件Global.asax.cs的 protected void Application_Start(){GlobalConfiguration.Configure(WebApiConfig.Register);} 方法,通過參數委托執行WebApiConfig.cs裏面的 public static void Register(HttpConfiguration config) 這個方法,將所有配置的路由信息添加到 HttpRouteCollection 對象中(MVC裏面可能是RoutCollection對象)保存起來。這裏的HttpRoutCollection對象的實例名是Routes,這個很重要,後面要用到

2、當我們發送請求到WebApi服務器的時候,比如我們訪問http://localhost:21528/api/Order這個url的時候,請求首先還是會被UrlRoutingModule監聽組件截獲,然後,將截獲的請求在Routes路由集合中匹配到對應的路由模板(如果匹配不到對應的路由模板,則返回404),得到對應的IHttpRoute對象。IHttpRoute對象是Routes集合裏面匹配到的一個實體。

3、將IHttpRoute對象交給當前的請求的上下文對象RequestContext處理,根據IHttpRoute對象裏面的url匹配到對應的controller,然後再根據http請求的類型和參數找到對應的action。這樣一個請求就能找到對應的方法了。

這個過程本身是非常復雜的,為了簡化,博主只選擇了最主要的幾個過程。更詳細的路由機制可以參考:http://www.cnblogs.com/wangiqngpei557/p/3379095.html。這文章寫得有點深,有興趣的可以看看。

三、WebApi路由過程

通過上文路由的過程,我們知道,一個請求過來之後,路由主要需要經歷三個階段

  1. 根據請求的url匹配路由模板
  2. 找到控制器
  3. 找到action

1、根據請求的url匹配路由模板

這點上面已經說了很多了,主要就是路由模板的配置和url的匹配。在此不作過多說明。

2、找到控制器

如果你反編譯路由模塊的代碼,你會發現控制器的選擇主要在IHttpControllerSelector這個接口的SelectController()方法裏面處理。

技術分享

該方法將當前的請求以HttpRequestMessage對象作為參數傳入,返回HttpControllerDescriptor對象。這個接口默認由DefaultHttpControllerSelector這個類提供實現

技術分享

默認實現的方法裏面大致的算法機制是:首先在路由字典中找到實際的控制器的名稱(比如“Order”),然後在此控制器名稱上面加上字符串“Controller”的到請求控制器的全稱(比如“OrderController”),最後找到對應的WebApi的Controller,實例化就得到當前請求的控制器對象。

3、找到action

得到了控制器對象之後,Api引擎通過調用IHttpActionSelector這個接口的SelectAction()方法去匹配action。這個過程主要包括:

  • 解析當前的http請求,得到請求類型(是get、post、put還是delete)
  • 如果路由模板配置了{action},則直接取到url裏面的action名稱
  • 解析請求的參數

如果路由模板配置了{action},那麽找到對應的action就很簡單,如果沒有配置action,則會首先匹配請求類型(get/post/put/delete等),然後匹配請求參數,找到對應的action。我們看個例子,比如,我們的controller加如下一些方法。

    public class OrderController : ApiController
    {
        [HttpGet]
        public IHttpActionResult GetAll()
        {
            return Ok<string>("Success");
        }

        [HttpGet]
        public IHttpActionResult GetById(int id)
        {
            return Ok<string>("Success" + id );
        }

        [HttpPost]
        public HttpResponseMessage PostData(int id)
        {
            return Request.CreateResponse();
        }

        [HttpPost]
        public HttpResponseMessage SavaData(ORDER order)
        {
            return Request.CreateResponse();
        }

        [HttpPut]
        public IHttpActionResult Put(int id)
        {
            return Ok();
        }

        [HttpDelete]
        public IHttpActionResult DeleteById(int id)
        {
            return Ok();
        }
    }

匹配action的結果

urlhttp方法參數結果
http://localhost:21528/api/Order get none 匹配GetAll方法
http://localhost:21528/api/Order get id 匹配GetById方法
http://localhost:21528/api/Order post order 匹配SavaData方法
http://localhost:21528/api/Order put id 匹配Put方法
http://localhost:21528/api/Order delete id 匹配DeleteById方法

WebApi還提供了一個action同時支持多個http方法的請求,使用AcceptVerbs特性去標記。但博主覺得實際使用並不多,有興趣的可以了解下。

        [AcceptVerbs("GET", "POST")]
        public IHttpActionResult GetById(int id)
        {
            return Ok<string>("Success" + id );
        }

四、WebApi特性路由

上面說了這麽多都是路由的一些全局配置。並且存在問題:

如果http請求的方法相同(比如都是post請求),並且請求的參數也相同。這個時候似乎就有點不太好辦了,這種情況在實際項目中還是比較多的。比如

  public class OrderController : ApiController
    {
        //訂單排產
        [HttpPost]
        public void OrderProduct([FromBody]string strPostData)
        {

        }

        //訂單取消
        [HttpPost]
        public void OrderCancel([FromBody]string strPostData)
        {
            
        }

        //訂單刪除
        [HttpPost]
        public void OrderDelete([FromBody]string strPostData)
        {
           
        }
    }

這個時候如果使用我們上面講的Restful風格的路由是解決不了這個問題的。當然,有園友可能就說了,既然這樣,我們在路由模板裏面加上“{action}”不就搞定了麽!這樣確實可行。但還是那句話,不提倡。我們來看看如何使用特性路由解決這個問題。

1、啟動特性路由

如果要使用特性路由,首先在WebApiConfig.cs的Register方法裏面必須先啟用特性路由:

        public static void Register(HttpConfiguration config)
        {
            // 啟用Web API特性路由
            config.MapHttpAttributeRoutes();

            //1.默認路由
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }

一般情況下,當我們新建一個WebApi項目的時候,會自動在Register方法裏面加上這句話。

2、最簡單的特性路由

我們在OrderController這個控制器裏面加這個action

        [Route("Order/SaveData")]
        [HttpPost]
        public HttpResponseMessage SavaData(ORDER order)
        {
            return Request.CreateResponse();
        }

然後我們通過Web裏面的Ajax調用

$(function () {
    $.ajax({
        type: ‘post‘,
        url: ‘http://localhost:21528/Order/SaveData‘,
        data: { ID: 2, NO:"aaa"},
        success: function (data, status) {
            alert(data);
        }
    });
});

得到結果:

技術分享

當然,有人可能就有疑義了,這個特性路由的作用和“{action}”的作用一樣嘛,其實不然,如果這裏改成 [Route("Test/AttrRoute")] ,然後請求的url換成http://localhost:21528/Test/AttrRoute,一樣能找到對應的action。

技術分享

特性路由的目的是為了解決我們公共路由模板引擎解決不了的問題。一個action定義了特性路由之後,就能通過特性路由上面的路由規則找到。

3、帶參數的特性路由

特性路由的規則可以使用“{}”占位符動態傳遞參數,比如我們有這樣一個特性路由

        [Route("ordertype/{id}/order")]
        [HttpGet]
        public IHttpActionResult GetById(int id)
        {
            return Ok<string>("Success" + id );
        }

在瀏覽器裏面調用

技術分享

調用成功。到此,我們就能看懂本文最開始那個看似“怪異”的路由→/api/user/1/detail這個了。

4、參數的約束和默認值

        [Route("api/order/{id:int=3}/ordertype")]
        [HttpGet]
        public IHttpActionResult GetById(int id)
        {
            return Ok<string>("Success" + id );
        }

這裏約束可變部分{id}的取值必須是int類型。並且默認值是3.

看看效果

技術分享

技術分享

不滿足約束條件,則直接返回404。

5、路由前綴

在正式項目中,同一個控制器的所有的action的所有特性路由標識一個相同的前綴,這種做法並非必須,但這樣能夠增加url的可讀性。一般的做法是在控制器上面使用特性[RoutePrefix]來標識。

[RoutePrefix("api/order")]
    public class OrderController : ApiController
    {
        [Route("")]
        [HttpGet]
        public IHttpActionResult GetAll()
        {
            return Ok<string>("Success");
        }

        [Route("{id:int}")]
        [HttpGet]
        public IHttpActionResult GetById(int id)
        {
            return Ok<string>("Success" + id );
        }

        [Route("postdata")]
        [HttpPost]
        public HttpResponseMessage PostData(int id)
        {
            return Request.CreateResponse();
        }
    }

那麽這個這個控制器的action的時候,都需要/api/order開頭,後面接上action特性路由的規則。

五、第一個Restful風格的WebApi服務

通過以上,我們就可以構造一個Restful風格的WebApi服務。

  [RoutePrefix("api/AttrOrder")]
    public class OrderController : ApiController
    {
        [Route("")]
        [HttpGet]
        public IHttpActionResult GetAll()
        {
            return Ok<string>("Success");
        }

        [Route("{id:int=3}/OrderDetailById")]
        [HttpGet]
        public IHttpActionResult GetById(int id)
        {
            return Ok<string>("Success" + id );
        }

        [Route("{no}/OrderDetailByNo")]
        [HttpGet]
        public IHttpActionResult GetByNO(string no)
        {
            return Ok<string>("Success" + no);
        }

        [Route("{name}/OrderDetailByName")]
        [HttpGet]
        public IHttpActionResult GetByName(string name)
        {
            return Ok<string>("Success" + name);
        }

        [Route("postdata")]
        [HttpPost]
        public HttpResponseMessage PostData(int id)
        {
            return Request.CreateResponse();
        }

        [Route("Test/AttrRoute")]
        [HttpPost]
        public HttpResponseMessage SavaData(ORDER order)
        {
            return Request.CreateResponse();
        }
    }

得到結果

技術分享

六、總結

整了這麽久終於整完了。如果你覺得本文對你有幫助,請幫忙博主推薦,您的支持是博主最大的動力!

C#進階系列——WebApi 路由機制剖析:你準備好了嗎?