前言
剛開始表面上感覺Web API內容似乎沒什么,也就是返回JSON數據,事實上遠非我所想,不去研究不知道,其中的水還是比較深,那又如何,一步一個腳印來學習都將迎刃而解。
Self-Host
我們知道Web API它可以快速為HTTP客戶端提供API來創建Web服務,為何如此這樣說呢?因為我們可以將其作為主機也就是一個服務器來用完全不需要IIS,這就是我們下面要講的第一個內容Self-Host,實現對Web API寄宿的方式有多種並且都是獨立於ASP.NET框架之外,如下Self-Host寄宿是存在於Web API 1中的,而在Web API 2中實現寄宿是采用Web-Host來進行寄宿(通過程序包packages中的Web-Host以及Web Client可得知),因為Web API本身是無法提供【請求-響應】的機制,所以需要寄宿來實現,即通過具體的應用程序來為Web API運行提供一個環境。下面且聽我娓娓道來。既然是進行交互必然有服務器和客戶端,下面我們將從建立控制台應用程序開始來進行了解。
Web API服務器
第一步
建立一個SelfHost的控制台應用程序,添加【Microsoft.AspNet.WebApi.SelfHost】程序包,搜索時會出現多個包注意不是【AspNetWebApi.SelfHost.】如下:
第二步
添加類,如下:
public class Product { public int Id { get; set; } public string Name { get; set; } public string Category { get; set; } public decimal Price { get; set; } }
第三步
添加派生自APiController控制的類以及要演示的數據,如下:
public class ProductsController : ApiController { Product[] products = new Product[] { new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 }, new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M }, new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M } }; public IEnumerable<Product> GetAllProducts() { return products; } public Product GetProductById(int id) { var product = products.FirstOrDefault((p) => p.Id == id); if (product == null) { throw new HttpResponseException(HttpStatusCode.NotFound); //如果未找到數據並返回狀態碼404 } return product; } public IEnumerable<Product> GetProductsByCategory(string category) { return products.Where(p => string.Equals(p.Category, category, StringComparison.OrdinalIgnoreCase)); } }
第四步
在控制台主程序中配置服務器以及添加路由
var config = new HttpSelfHostConfiguration("http://localhost:8080"); //配置主機 config.Routes.MapHttpRoute( //配置路由 "API Default", "api/{controller}/{id}", new { id = RouteParameter.Optional }); using (HttpSelfHostServer server = new HttpSelfHostServer(config)) //監聽HTTP { server.OpenAsync().Wait(); //開啟來自客戶端的請求 Console.WriteLine("Press Enter to quit."); Console.ReadLine(); }
以上就是關於Web API關於主機的設置,接下來就是建立一個客戶端來訪問此服務器上的資源。
Web API客戶端
第一步
同理建立一個ClientApp的控制台應用程序,同時添加Web API客戶端程序包【Microsoft.AspNet.WebApi.Client】如下:
第二步
既然是要訪問服務器上的資源,自然要添加對Web API服務器(SelfHost)的引用了。
第三步
接下來就是建立客戶端並讀取服務器上的資源
static HttpClient client = new HttpClient(); //利用此對象進行對Web API的調用 static void ListAllProducts() { HttpResponseMessage resp = client.GetAsync("api/products").Result; resp.EnsureSuccessStatusCode(); var products = resp.Content.ReadAsAsync<IEnumerable<SelfHost.Product>>().Result; foreach (var p in products) { Console.WriteLine("{0} {1} {2} ({3})", p.Id, p.Name, p.Price, p.Category); } } static void ListProduct(int id) { var resp = client.GetAsync(string.Format("api/products/{0}", id)).Result; resp.EnsureSuccessStatusCode(); var product = resp.Content.ReadAsAsync<SelfHost.Product>().Result; Console.WriteLine("ID {0}: {1}", id, product.Name); } static void ListProducts(string category) { Console.WriteLine("Products in '{0}':", category); string query = string.Format("api/products?category={0}", category); var resp = client.GetAsync(query).Result; resp.EnsureSuccessStatusCode(); var products = resp.Content.ReadAsAsync<IEnumerable<SelfHost.Product>>().Result; foreach (var product in products) { Console.WriteLine(product.Name); } }
-
通過調用HttpClient.GetAsync來發出一個Get請求來請求服務器上的Uri資源。
-
通過調用HttpResponseMessage.EnsureSuccessStatusCode方法來確定請求是否成功,若失敗即返回錯誤狀態碼則拋出一個異常。
-
通過調用ReadAsAsync<T>將HTTP中響應的數據類型進行反序列化,該方法為一個擴展方法。
GetAsync和ReadAsync為異步方法,直到獲得結果值即Result屬性的值操作完成,否則將一直阻塞線程。
第四步
最后在客戶端控制台主程序中建立客戶端與服務器端的通信服務即可
client.BaseAddress = new Uri("http://localhost:8080"); ListAllProducts(); ListProduct(1); ListProducts("toys"); Console.WriteLine("Press Enter to quit."); Console.ReadLine();
接下來就是啟動Web API服務器程序,通過Web API客戶端來訪問服務器並獲得其請求的資源。【注意】windows 8系統啟動服務器必須以管理員身份運行,否則報錯。
訪問資源成功,如下:
路由原理
如果對MVC框架中路由熟悉的話,Web API的路由原理和其相似,但是不同的是Web API是使用的HTTP方法來選擇Action方法而不是通過URI路徑來選擇Action方法。
路由表
在Web API中處理HTTP請求的是一個控制器類,控制器中的公有方法叫做Action方法,當Web API框架獲得一個請求時,會根據請求路由到一個Action方法上。為了決定調用哪個Action方法,Web API框架利用路由表來決定,當創建項目模板時將在App_Start文件下的 WebApiConfig 創建一個默認的路由,如下:
config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } );
當我們需要將Web API作為服務器時此時必須直接在HttpSelfHostConfiguration上設置路由表【上述關於SelfHost已經演示】
注冊路由
在Web API路由配置文件中有一個 Register 方法,該方法中有一個 HttpConfiguration 實例參數,該HttpConfiguration是消息管道中的全局對象,我們可以通過其對管道某一個行為作出相關操作,從而達到我們所需。通過HttpConfiguration上的屬性_routes即 HttpRouteCollection 對象中的 CreateRoute 方法再依據默認的路由模板、默認值以及相關約束來創建實現了 IHttpRoute 接口的路由對象即 HttpRoute ,並通過HttpConfiguration中的Routes對象中的擴展方法MapHttpRoute方法來達到注冊路由映射的目的,當然我們也可以直接通過調用路由集合HttpRouteCollection中的 Add 方法來注冊路由到路由表中。
路由表中每一條都包含一個對應的路由模板,Web API默認的路由模板是api/{controller}/{id} ,在此模板下,api是一個路徑段,而{controller}和{id}是占位符。當Web API收到HTTP請求時,它嘗試去匹配路由表中的路由模板之一的URI,若沒匹配上,則返回404。
例如,用下面的URI來匹配默認的路由
/api/contacts /api/contacts/1 /api/products/gizmo1
【注意】為何在Web API的控制器中以API開頭的原因是避免和MVC框架中的路由沖突,如/contacts會進入到MVC路由中,而/api/contacts會進入到Web API框架中,當然我們可以通過改變默認的路由表來改變約定,但是不建議這么做。
一旦一個匹配的路由被找到,Web API根據默認約定來選擇Controller和Action:
-
Web API會用Controller替代占位符{controller}的值中來找到控制器。
-
Web API着眼根據HTTP方法來找到Action方法,然后找到一個以一個HTTP方法的名稱開始的Action方法,例如,對於一個Get請求,Web API會找到一以Get開頭的Action方法,如:GetContact或者GetAllContact的Action方法。這種約定同樣也適用於GET、POST、PUT以及DELETE方法。我們通過在控制器上使用特性來啟用其他的方法。
-
在路由模板中的其他的占位符,如{id}被映射到action方法參數上。
查找控制器
當一個URI資源請求過來時Web API框架會查找已經注冊的路由表中的路由並進行匹配,如果匹配通過,此時將創建一個包含每個占位符的值的字典,字典的鍵為占位符的名稱,當然不包括大括號,字典的值為請求過來的URI中的值或者是路由模板中的默認值,然后這個字典將會保存在 IHttpRouteData 對象中的字典中。該對象還存在一個路由對象IHttpRoute。當請求的URI過來時,此時會通過請求信息即 HttpRequestMessage 對象中的 RquestUri 屬性來獲得其URI,接下來HttpRoute會接受到HttpRequestMessage中的URI並進行解析,然后將通過調用 GetRouteData 等方法來封裝路由數據給實現了IHttpRouteData接口的 HttpRouteData ,通過HttpRouteData中的構造函數中的路由對象來獲取傳遞過來的路由對象HttpRoute(該路由對象也根據請求過來的路由變量來綁定到路由模板中最終生成一個完整的URL),同時因為實現了IHttpRouteData接口則此時該接口的字典將傳遞給HttpRouteData構造函數中的路由字典即 HttpRouteValueDictionary 。【注意】GetRouteData方法中的參數有一個為virtualPathRoot即虛擬根路徑,當執行此方法時得到的是相對路徑,也就是說通過路由模板進行匹配是根據相對路徑來進行匹配的。
查找控制器是通過 IHttpControllerSelector 接口上的 SelectController ,此方法參數為HttpRequestMessage的實例並返回一個 HttpControllerDescriptor ,此接口的默認實現是通過 DefaultHttpControllerSelector 類實現,此類實現的算法有如下三點:
-
在上述路由字典即HttpRouteValueDictionary中查找controller的鍵。
-
獲取鍵的值並將字符串Controller作為控制器的類型名。
-
用此類型名來查找一個Web API控制器。
查找Action
在查找到控制器之后,框架將會通過 IHttpActionSelector 接口中的 SelectAction 方法來查找Action方法,該方法參數要獲取一個控制器上下文即 HttpControllerContext 返回一個 HttpActionDescriptor 對象實例。其默認實現是通過 APiControllerActionSelector 來提供的,要查找到Action要經過如下三點:
-
請求的HTTP方法。
-
在路由模板中占位符{action}對應的值。
-
在控制器上Action方法的參數。
【注意】如何確定在控制上的方法是Action方法呢?當查找Action方法時,僅僅只着眼於控制上的公有的實例方法(不包括從APiController上繼承的特殊名稱的方法,如:重載、事件、構造等等)並且是從APiController類上繼承的方法。
總結
綜上所述,對於Web API上的一個路由系統總共有三個階段
-
匹配一個URI到一個路由模板。
-
選擇一個控制器。
-
選擇一個Action方法。
當然你可以對路由模板進行自定義以及相關參數利用正則表達式進行約束等,這就不再詳細描述。
接下來我們就上述敘述來進行相關例子
public class ProductsController : ApiController { public void GetAllProducts() { } public IEnumerable<Product> GetProductById(int id) { } public HttpResponseMessage DeleteProduct(int id){ } }
對於以下可能的HTTP請求,會對於每個請求對應應該被調用的Action方法。
HTTP Method | URI PATH | Action | Parameter |
GET | api/products | GetAllProducts | (none) |
GET | api/products/4 | GetProductById | 4 |
DELETE | api/products/4 | DeleteProduct | 4 |
POST | api/products | (no match) |
【注意】在上述中的URI的{id},如果存在則會被映射到Action方法中的id參數中,如上述中兩種Get方法,一個有id參數一個沒有沒有id參數。同時也應注意POST請求將會失敗,因為在控制器中沒有定義一個POST....方法。
根據約定來進行映射Action方法注意
假設Web API控制器名稱ProductController並繼承APiController,在該控制器下有如下兩個方法:
public Product GetProductById(int id) { var product = products.FirstOrDefault((p) => p.Id == id); if (product == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } return product; } public string Get(int id) { return "value"; }
當發出Get(如:/api/product/1)請求時將會出錯,出錯原因為匹配到多個路由,默認能夠匹配到GetProductById方法就不用說了,此時同樣能匹配到Get方法,所以此時需要將Get標記為【NonAction】或者采用其他方法來區別這兩個路由。
HTTP方法
除了使用約定的HTTP方法之外,我們也可以使用特性HttpGet、HttpPost、HttpPut以及HttpDelete來修飾Action方法來顯式指定一個Action的HTTP方法。
在下面的例子中,FindProduct方法會被映射到GET請求。
public class ProductsController : ApiController { [HttpGet] public Product FindProduct(id) {} }
我們可以使用 AcceptVerbs 特性對一個Action使用多個HTTP方法或者說是使用HTTP方法而不是POST、DELETE、GET以及PUT,如下:
多個請求到同一Action
[AcceptVerbs("GET", "Post")] public Product FindProduct(int id) { } 或者 [AcceptVerbs(HttpVerbs.Get | HttpVerbs.Post)] public Product FindProduct(int id) { }
根據請求不同來響應相同Action,如下:
public Product FindProduct(int id) { } [AcceptVerbs(HttpVerbs.Post] public Product FindProduct(string guid) { }
通過Action名稱配置路由
由於默認的路由模板,Web API使用HTTP方法來選擇Action,但是我們可以創建一個包括在URI中的Action名稱的路由。如下:
routes.MapHttpRoute( name: "ActionApi", routeTemplate: "api/{controller}/{action}/{id}", defaults: new { id = RouteParameter.Optional } );
在上述路由模板中,{action} 參數名稱命名了在控制器上的Action方法,在此種路由下,可以使用特性來指定HTTP方法,例如,允許控制器有如下方法:
public class ProductsController : ApiController { [HttpGet] public string Details(int id); }
在此種情況下,對於api/products/details的GET請求會映射到Details方法,這種路由風格和MVC相似。
通過使用ActionName特性來覆蓋Action的名稱,例如下列例子,有兩種映射到如api/products/thumbnail/id的方法,一種支持GET請求,一種支持POST請求
public class ProductsController : ApiController { [HttpGet] [ActionName("Thumbnail")] public HttpResponseMessage GetThumbnailImage(int id); [HttpPost] [ActionName("Thumbnail")] public void AddThumbnailImage(int id); }
Non-Actions
為了阻止一個方法作為Action方法被調用,通過使用Non-Actions特性來將其標記為不是一個Action方法,即使這個方法匹配到了路由規則
// Not an action method. [NonAction] public string GetPrivateData() { ... }