隨着前后端分離的大熱,WebApi在項目中的作用也是越來越重要,由於公司的原因我之前一直沒有機會參與前后端分離的項目,但WebApi還是要學的呀,因為這東西確實很有用,可單獨部署、與前端和App交互都很方便,既然有良好的發展趨勢,我們當然應該順勢而為——搞懂WebApi!
從MVC到WebApi,路由機制一直都在其中扮演着重要的角色。
它可以很簡單:如果你只需要會用一些簡單的路由,如/Home/Index那么你只需要配置一個默認路由就能搞定。
它可以很神秘:你的url可以千變萬化,看到一些“無厘頭”的url,很難理解它是如何找到匹配的Action,例如/api/Pleasure/1/detail,這樣的url可以讓你糾結半天。
它可以很深奧:當面試官提問“請簡單分析下MVC路由機制的原理”,你可能事先就准備好了答案,然后劈里啪啦一頓(型如:UrlRoutingMoudle—>Routes—>RouteData—>RequestContext—>Controller),你可能回答的很流利,但並不一定理解這些個對象到底是啥意思。):目前為止我還沒能理解透,以后會繼續努力的直到弄清楚。
一、MVC和WebApi路由機制比較
1、MVC使用的路由
在MVC中,默認路由機制是通過解析url路徑來匹配Action。比如:/User/GetList,這個url就表示匹配User控制器下的GetList方法,這是MVC路由的默認解析方式。為什么默認的解析方式是這樣子的呢?因為MVC定義了一個默認路由,路由代碼放在App_Start文件夾下的RouteConfig.cs中,今后我們如果想要自定義路由規則,那自定義路由的代碼也要寫在RouteConfig.cs中。
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
url:"{controller}/{action}/{id}"定義了路由解析規則,{controller}是必填參數默認值是Home,{action}是必填參數默認值是Index,{id}表示匹配名稱為id的形參,而且是可選參數(方法的參數列表中可以有名為id的形參,也可以沒有)。
2、WebApi使用的路由
在WebApi中,默認路由機制是通過解析http請求的類型來匹配Action,也就是說WebApi的默認路由機制不需要指定Action的名稱。比如:/api/Pleasure,這個url就表示匹配Pleasure控制器下的[HttpGet]方法,/api是固定必填值,這是WebApi路由的默認解析方式。WebApi的默認解析方式之所以如此,同樣也是因為定義了默認路由,路由代碼放在App_Start文件夾下的WebApiConfig.cs中,今后我們如果想要自定義路由規則,那自定義路由的代碼也要寫在WebApiConfig.cs中。
public static void Register(HttpConfiguration config)
{
// Web API 路由
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
routeTemplate:"api/{controller}/{id}"定義了路由解析規則,api是固定必填值,{controller}是必填參數無默認值,{id}表示匹配名稱為id的形參,而且是可選參數(方法的參數列表中可以有名為id的形參,也可以沒有)。
3、MVC和WebApi路由區別匯總
WebApi的默認路由機制通過http請求的類型匹配Action,MVC的默認路由機制通過url匹配Action
WebApi的路由配置文件是WebApiConfig.cs,MVC的路由配置文件是RouteConfig.cs
WebApi的Controller繼承自Web.Http.ApiController,MVC的Controller繼承自Web.Mvc.Controller
4、示例一
public class PleasureController : ApiController
{
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "value1", "value2" };
}
}
為什么http://localhost:7866/api/Pleasure能匹配到GetOne()方法呢?首先根據路由規則解析出控制器是Pleasure,其次通過瀏覽器地址欄直接發出的請求都是get請求而Pleasure中只有一個get類型的方法,因此就匹配到了GetOne()。
為什么http://localhost:7866/api/Pleasure/1也能匹配到GetOne()方法呢?因為id是可選形參,即使指定了id的值,也可以訪問不含形參id的方法。
5、示例二
public class PleasureController : ApiController
{
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "value1", "value2" };
}
[HttpGet]
public IEnumerable<string> GetOne(int id)
{
return new string[] { "含參-value1", "含參-value2" };
}
}
示例二中http://localhost:7866/api/Pleasure請求的結果與示例一中的結果是一樣的,在此不做過多的解釋。
示例二中http://localhost:7866/api/Pleasure/1請求到了含參方法,說明如果指定了形參id的值,而且Controller中存在指定請求類型的含參方法,會優先匹配含此形參的方法。若匹配不上含參方法,但參數類型為可選參數,那就再嘗試匹配不含此形參的方法。
6、示例三
public class PleasureController : ApiController
{
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "value1", "value2" };
}
[HttpGet]
public IEnumerable<string> GetOne(int id)
{
return new string[] { "含參-value1", "含參-value2" };
}
[HttpGet]
public string GetList()
{
return "value";
}
}
雖然WebApi的默認路由機制不需要指定Action,但是WebApi支持在url中指定Action。由於Restful風格的服務要求請求的url中不能包含Action,因此WebApi不提倡在url中指定Action。
7、示例四
public class PleasureController : ApiController
{
[HttpGet]
public string GetOne(int id,string name,int age)
{
return string.Format("id={0},name={1},age={2}", id, name, age);
}
}
二、WebApi基礎
1、默認路由
新建WebApi服務的時候,會自動在WebApiConfig.cs文件里面生成一個默認路由:
public static void Register(HttpConfiguration config)
{
// Web API 路由
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
將MapHttpRoute()方法轉到定義可以發現,它有四個重載方法:
下面來看看各個參數的作用:
name:"DefaultApi"表示此路由的名稱。注冊多個路由時必須保證名稱不重復。
routeTemplate:"api/{controller}/{id}"表示路由的匹配規則。api是固定必填值,這個值是可變的如果你把它改成“BalaApi”,那url就應該變成 “http://1.1.1.1:80/BalaApi/***”。{controller}是控制器的占位符,在真實的url中,該部分對應的是具體的控制器名稱,這個和MVC一致。{id}是形參id的占位符,id是形參的名字,一般這個參數都會在defaults中設置為可選。
defaults:new { id=RouteParameter.Optional }表示設置id為可選參數,routeTemplate中的{controller}和{id}都可以設置默認值。若defaults改成new { controller="Pleasure", id = RouteParameter.Optional },那么我們請求http://1.1.1.1:80/api這個url仍然能訪問到Pleasure控制器中的[HttpGet]方法。
constraints:new{ id = @"\d+" }表示為id添加約束。形參id不能為空而且必須是整數,優先匹配含參方法,也能匹配無參方法(詳情請回看上一部分的示例一)。
public class PleasureController : ApiController
{
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "value1", "value2" };
}
[HttpGet]
public IEnumerable<string> GetOne(int id)
{
return new string[] { "含參-value1", "含參-value2" };
}
}
我們如果想在不指定id值的時候仍然能夠正常請求到GetOne(),把id的約束改成“constraints: new { id = @"\d*" }”即可,這里就不放截圖了,大家可以自己去試試。我們只在路由中對id的值進行了約束,其實我們也可以約束Controller和Action,但一般不常用大家有興趣可以自己玩一下。
2、自定義路由
上面介紹了許多關於默認路由的內容,除此之外我們還可以自定義路由,比如將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 }
);
config.Routes.MapHttpRoute(
name: "ActionApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
2.1、自定義匹配到Action的路由
這個自定義路由很好理解,它和MVC中的默認路由大致相同,不同之處是此路由多了個api前綴。
添加了可匹配到Action的路由后,就能解決“第一部分->示例三”中遇到的問題了。
當同時存在多個路由時,一定要注意name不能重復。
public class PleasureController : ApiController
{
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}
[HttpGet]
public IEnumerable<string> GetOne(int id)
{
return new string[] { "GetOne(int id)->value1", "GetOne(int id)->value2" };
}
[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}
我艹,為什么http://localhost:7866/api/Pleasure/GetOne請求失敗了,為什么沒有匹配上GetOne()方法?
下面我來解釋下,DefaultApi定義在ActionApi之前,所以先檢查http://localhost:7866/api/Pleasure/GetOne能否與DefaultApi匹配成功,api是固定必填值,Pleasure是controller的值,GetOne是id的值,匹配成功!然后去PleasureController中找[HttpGet]類型而且含有string id形參的方法,當然是沒有啦。只找到一個[HttpGet]類型的GetOne(int id),所以錯誤信息中提示參數id的類型不匹配。
接下來說說解決方法,實際上id是int類型的參數,我們希望GetOne能匹配上ActionApi中的action,而不是DefaultApi中的id,這在默認情況下是做不到的,不過為DefaultApi中的id加上個約束后就能做到了,路由修改后如下:
public static void Register(HttpConfiguration config)
{
// Web API 路由
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: new { id = @"\d*" }
);
config.Routes.MapHttpRoute(
name: "ActionApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
2.2、修改方法的ActionName
WebApi默認GetOne()方法的ActionName就是GetOne,如果我們想要方法名和ActionName不一致,可以通過ActionName特性來修改。
public class PleasureController : ApiController
{
[ActionName("NewGetOne")]
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}
[HttpGet]
public IEnumerable<string> GetOne(int id)
{
return new string[] { "GetOne(int id)->value1", "GetOne(int id)->value2" };
}
[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}
三、WebApi路由執行過程
1、路由原理
有了上面的理論作為基礎,我們再來分析WebApi路由機制的原理以及路由匹配的過程。由於WebApi的路由機制和MVC有許多相似性,所以要想理解WebApi的路由機制,就需要搬出那些Asp.Net Routing里面的對象。這個過程有點復雜,我就根據搜羅的資料和自己的理解 ,提一些主要的過程:
a、WebApi服務啟動之后,會執行全局配置文件Global.asax.cs的 protected void Application_Start() 方法,其中的 GlobalConfiguration.Configure(WebApiConfig.Register); 會通過委托的方式執行WebApiConfig.cs中的 public static void Register(HttpConfiguration config),將配置的所有路由信息添加到HttpRouteCollection對象中保存起來。這里的HttpRouteCollection對象的實例名是Routes,這個很重要,后面會用到。
b、當我們發送請求到WebApi服務器的時候,比如當我們訪問http://localhost:7866/api/Pleasure時,首先請求會被UrlRoutingModule監聽組件截獲,然后按照定義順序檢查url能匹配上Routes集合中的哪個路由模板(如果都匹配不上,則返回404),最后返回對應的IHttpRoute對象。IHttpRoute對象是url匹配上的Routes集合里面的一個實體。
c、將IHttpRoute對象交給當前的請求上下文對象RequestContext處理,根據IHttpRoute對象中的url匹配到對應的controller,然后根據http請求的類型和參數找到對應的action。這樣就能找到請求對應的controller和action了。
這個過程是非常復雜的,為了簡化,我只選擇了最主要的幾個過程。更詳細的路由機制可以參考http://www.cnblogs.com/wangiqngpei557/p/3379095.html,這篇文章寫得有些深度,感興趣的朋友可以看下。
2、根據請求的url匹配路由模板
通過上文路由過程的分析,我們知道一個請求過來之后,路由主要經歷三個階段:
1、根據請求的url匹配路由模板
2、找到controller
3、找到action
第一步很簡單,主要就是將url與路由模板中配置的routeTemplate進行匹配校驗,在此不做過多的說明。
3、找到controller
你如果反編譯路由模塊的代碼,就會發現控制器的選擇主要在IHttpControllerSelector接口中的SelectController()方法里面處理。
該方法所需的HttpRequestMessag參數,是由當前請求封裝而來,返回HttpControllerDescriptor對象。這個接口默認由DefaultHttpControllerSelector類提供實現。
默認實現的方法里面大致的算法機制是:首先在路由字典中找到實際的控制器名稱(比如“Pleasure”),然后在此控制器名稱后面加上“Controller”字符串得到控制器的全稱“PleasureController”,最后找到WebApi對應的Controller再實例化就得到當前請求的控制器對象。
4、找到action
得到了控制器對象之后,Api引擎通過調用IHttpActionSelector接口中的SelectAction()方法去匹配action。這個過程主要包括:
解析當前http請求,得到請求類型(post、delete、put、get)
如果路由模板配置了{action},則直接去url中找action的名稱
解析請求的參數
如果路由模板配置了{action},那么直接去url中找對應的action即可。如果沒有配置action,則會首先匹配請求類型(post/delete/put/get),然后匹配請求參數,才能找到對應的action。
5、設置方法同時支持多種http請求類型
WebApi提供了AcceptVerbs,應用這一特性可以使方法同時支持多種請求類型。這一特性在實際中使用不是太多,大家了解即可。
[AcceptVerbs("get","post")]
public string GetList()
{
return "GetList()->value";
}
四、WebApi特性路由
如果http請求的類型相同(比如都是get請求),並且請求的參數也相同。這個時候似乎就有點不太好辦了,這種情況在實際項目中還是比較多的,比如:
public class PleasureController : ApiController
{
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}
[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}
當然這個問題可以通過添加匹配到Action的路由來解決,不過這就不符合Restful風格了,所有我們不提倡這種寫法。除此之外,利用特性路由也能解決上述問題,下面說說特性路由的東西。
1、啟動特性路由
public static void Register(HttpConfiguration config)
{
//啟動特性路由
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: new { id = @"\d*" }
);
config.Routes.MapHttpRoute(
name: "ActionApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
一般情況下通過新版本的VS2017創建WebApi項目時,這句話默認已經存在。
2、最簡單的無參特性路由
2.1、為GetOne()方法添加Route特性(特性路由沒參數,GetOne()方法也沒參數)
public class PleasureController : ApiController
{
[Route("Pleasure/GetOne")]
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}
[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}
結論:
為GetOne()添加了特性路由后,無法通過WebApi默認路由機制(不指定Action)訪問GetOne()
為GetOne()添加了特性路由后,無法通過指定ActionName的方式訪問GetOne()
為GetOne()添加了特性路由后,只能通過特性路由的路徑訪問GetOne()
2.2、為GetOne()方法添加Route特性(特性路由沒參數,GetOne()方法有參數)
public class PleasureController : ApiController
{
[Route("Pleasure/GetOne")]
[HttpGet]
public IEnumerable<string> GetOne(int age)
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}
[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}
public class PleasureController : ApiController
{
[Route("Pleasure/GetOne")]
[HttpGet]
public IEnumerable<string> GetOne(string name)
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}
[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}
特性路由不含參數時的結論:
方法沒參數時,只要url滿足特性路由的定義就能訪問到方法
方法有參數時,必須url滿足特性路由的規則而且指定了方法所需的參數值才能訪問到方法
3、含參特性路由
3.1、特性路由有參數,方法沒參數
public class PleasureController : ApiController
{
[Route("Pleasure/GetOne/{name}")]
[HttpGet]
public IEnumerable<string> GetOne()
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}
[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}
3.2、特性路由有參數,方法有參數
public class PleasureController : ApiController
{
[Route("Pleasure/GetOne/{name}")]
[HttpGet]
public IEnumerable<string> GetOne(string name)
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}
[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}
public class PleasureController : ApiController
{
[Route("Pleasure/GetOne/{name}")]
[HttpGet]
public IEnumerable<string> GetOne(string address)
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}
[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}
特性路由含參數時的結論:
方法沒參數時,只要url滿足特性路由的定義就能訪問到方法
方法有參數時,必須url滿足特性路由的規則而且指定了方法所需的參數值才能訪問到方法
3.3、特性路由有參數,參數在最后而且有默認值
public class PleasureController : ApiController
{
[Route("Pleasure/GetOne/{age:int=16}")]
[HttpGet]
public IEnumerable<string> GetOne(int age)
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}
[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}
3.4、特性路由有參數,參數在中間而且有默認值
public class PleasureController : ApiController
{
[Route("Pleasure/{age:int=16}/GetOne")]
[HttpGet]
public IEnumerable<string> GetOne(int age)
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}
[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}
4、路由前綴
在正式項目中,同一個控制器中所有的action的特性路由最好有一個相同的前綴,這並非是必須的,但能增加url的可讀性。一般的做法是在控制器上使用[RoutePrefix]特性來標識。當我們使用特性路由來訪問時,前邊必須加上定義的前綴。
[RoutePrefix("api/prefix")]
public class PleasureController : ApiController
{
[Route("Pleasure/GetOne/{age:int=16}")]
[HttpGet]
public IEnumerable<string> GetOne(int age)
{
return new string[] { "GetOne()->value1", "GetOne()->value2" };
}
[HttpGet]
public string GetList()
{
return "GetList()->value";
}
}
————————————————
版權聲明:本文為CSDN博主「changuncle」的原創文章,遵循CC 4.0 by-sa版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/xiaouncle/article/details/83869952