MVC也好,WebAPI也好,據我所知,有部分人是因為複雜的路由,而不想去學的。曾經見過一位程式猿,在他MVC程式中,一切皆路由,url中是完全拒絕"?"和“&”。對此,我也不好說什麼,搞不好是個人風格。路由雖然重要,但其實也只是實現MVC的一種手段,並非你用的路由越多,你的url完全不使用參數,你 ...
MVC也好,WebAPI也好,據我所知,有部分人是因為複雜的路由,而不想去學的。曾經見過一位程式猿,在他MVC程式中,一切皆路由,url中是完全拒絕"?"和“&”。對此,我也不好說什麼,搞不好是個人風格。路由雖然重要,但其實也只是實現MVC的一種手段,並非你用的路由越多,你的url完全不使用參數,你的MVC就越純正。說實話,筆者一開始對路由也感到恐懼,但是閱讀了官方文檔後,發現路由其實也可以很簡單,關鍵在於我們如何使用。由於筆者也是初學者,有什麼錯漏的地方,歡迎大家指正。
本系列文章使用的是vs2017,WebAPI版本是2。本系列大多數內容並非原創,而是來自官網的教程(https://docs.microsoft.com/en-us/aspnet/web-api/overview/getting-started-with-aspnet-web-api/),如果你英文不好,可以將鏈接中的en-us改成zh-cn。中文版地址:https://docs.microsoft.com/zh-cn/aspnet/web-api/overview/getting-started-with-aspnet-web-api/。不過建議你可以的話,還是看英文版本,因為有些翻譯是完全走樣。
廢話不多說,馬上來看看如何新建一個WebAPI項目。打開vs2017,文件-新建-項目
選擇空模板,勾選webapi
添加模型類,在右側資源管理器的Models文件夾上右鍵-添加-類
類的代碼如下:
1 namespace ProductsApp.Models 2 { 3 public class Product 4 { 5 public int Id { get; set; } 6 public string Name { get; set; } 7 public string Category { get; set; } 8 public decimal Price { get; set; } 9 } 10 }
添加空的控制器,在Controllers文件夾上右鍵-添加-控制器,選擇Web API2 控制器 - 空
控制器名稱為:ProductsController,代碼如下:
using ProductsApp.Models; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Web.Http; namespace ProductsApp.Controllers { 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 IHttpActionResult GetProduct(int id) { var product = products.FirstOrDefault((p) => p.Id == id); if (product == null) { return NotFound(); } return Ok(product); } } }
這樣,一個簡單的WebAPI就完成了。完成後,文件結構如下:
調用的話,我們直接使用IIS新建一個網站,埠為1111
打開edge瀏覽器,輸入地址:http://localhost:1111/api/Products,效果如下(註意,每次修改完代碼後,需要重新生成一下),
一切都很簡單,代碼也都不複雜,不過明顯有兩個問題,一個是,為什麼預設就調用了GetAllProducts()了,另一個是,我們明明返回一個列表的,怎麼到了客戶端就變成json了呢?
第一個問題,就是本文所要研究的問題。說到路由,筆者想起一樁往事。筆者自從接觸asp.net以來,一直都在使用webform,即使在MVC大行其道的時候。有一次,接到一個外包項目,利用某開源社區框架做業務的擴展,由於該開源框架用的是MVC,於是就問對方的技術負責人,業務擴展項目是否也必須用MVC,對方答道,用MVC幹嘛,繞來繞去不是更麻煩嗎?他這句話讓我深以為然,大有惺惺相惜之感。我這樣說,並非要貶低MVC,更無意挑起MVC與WebForm之爭,而是在遍地MVCer的情況下,還能找到WebFormer而高興。實際上,只要能滿足客戶要求,誰會在意你用MVC還是WebForm呢。
廢話不多說,回到我們的問題,為什麼我們輸入地址:http://localhost:1111/api/Products,就是在調用GetAllProducts()呢?首先,我們看看App_Start文件夾下的WebApiConfig.cs文件,這個文件是用來配置路由的,代碼如下:
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 } ); } }
上面的代碼,實質上就是定義了一個預設的路由規則。
我們再看看Global.asax
protected void Application_Start() { GlobalConfiguration.Configure(WebApiConfig.Register); }
很明顯,在程式第一次啟動的時候,我們的路由規則就被配置載入了。這個預設的規則就是 api/{controller}/{id},其中{controller}匹配一個控制類,例如ProductsController,{id}是可選的,匹配的是public方法(也就是action)的參數。那麼,Controller里的public方法,也就是action該如何匹配呢?
從官方的文檔可以查到這麼幾句話:If you are familiar with ASP.NET MVC, Web API routing is very similar to MVC routing. The main difference is that Web API uses the HTTP method, not the URI path, to select the action。意思就是,如果你熟悉MVC,那麼API的路由是跟MVC的路由非常相似的。兩者之間的不同,是Web API使用http方法,而非URI路徑去選擇action。這裡的action,就是我們Controller裡面的public 方法。
也就是說,預設路由api/{controller}/{id},首先匹配一個Controller類,然後用http請求方法匹配Action方法名,最後,用{id},匹配Action中的參數。
http請求方法是什麼東西?如果你是傳統的asp開發者,或是php開發者,相信都會非常熟悉。例如我們以前寫表單html,通常都會這樣寫:
<form action="form_action.asp" method="get"> .... </form>
裡面的method就是我們所說的http請求方法,最常見的就是get和post,get的話,就是將參數放到url上去提交,post的話,參數不會顯示在url中。更多的http方法,可以點擊這裡。
既然知道WebAPI的預設路由,是用http請求方法去匹配控制類中的action,那麼就好辦了,我們在地址欄輸入地址:http://localhost:1111/api/Products ,其實就是相當於在使用get方法與ProductsController中的Action進行匹配了。
然而,上面代碼中,兩個Action方法都沒明確表明是用什麼http請求方法,那怎麼確定調用哪一個方法呢?get跟GetAllProducts()到底有什麼關係呢,以至於GetAllProducts()可以被預設調用?或許有的人已經看出來了,沒錯,調用的方法GetAllProducts()那麼巧,也是以Get開頭的。這就是我們匹配的其中一個條件。如果Controller中,public方法的名字(也就是action的名字),是以"Get", "Post", "Put", "Delete", "Head", "Options", 或 "Patch"開頭,那麼按照約定,該方法(action)匹配對應的http請求方法的調用。如果開頭沒有上述的關鍵字,預設表示該方法只支持Post。
例如GetAllProducts()方法,就表示使用http的get方法調用。DeleteProduct(int id)就表示用http的deletel方法調用。由於我們調用的地址是:http://localhost:1111/api/Products,翻譯成匹配規則就是,匹配ProductsController中,一個使用get,同時沒有參數的Action(也就是public 方法),即GetAllProducts()。如果我們有另一個Get方法,同時也是沒有參數的話,就會報錯。例如,我們增加一個方法:
public string GetTest() { return "GetTest is called"; }
該方法明顯也是匹配Get方法,同時沒有參數。重新生成下項目,然後用PostMan調用一下,會發現匹配多個的錯誤。(PostMan的安裝就不說了,很簡單,不斷下一步。)
我們在原來的基礎上,修改一下ProductsController的代碼,增加一個方法(紅色字體部分),代碼如下:
using ProductsApp.Models; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Web.Http; namespace ProductsApp.Controllers { 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 IHttpActionResult GetProduct(int id) { var product = products.FirstOrDefault((p) => p.Id == id); if (product == null) { return NotFound(); } return Ok(product); } public string SayHello() { return "Hello,World"; } } }
我們在PostMan中輸入地址:http://localhost:1111/Products,http請求方法,選擇Post,按照我們的規則,應該會調用SayHello方法,實際效果如下:
如果我們將url改成:http://localhost:1111/api/Products/1,但方法依然是Post,那麼按照上面說的,先找Post的方法,而三個方法中,只有SayHello符合,雖然後面加了id,並且值為1,由於它是可選的,所以,在post下,調用的依然是SayHello,如下圖:
假如,我們將Post方法改為Get,那麼就會選擇調用我們的GetProduct方法,效果如下:
這個就是WebAPI預設的路由,主要使用Http請求方法來匹配Controller里的Action。而這個匹配的規則,就是使用首碼來決定哪一個最匹配,如果首碼都不是http方法,表示預設匹配Post。是不是感覺很簡單呢,如果這樣還覺得複雜,沒關係,下麵還有更簡單的方法,就是屬性路由。
上面的這種路由匹配規則,其實是屬於約定的路由。在調用的時候,你還多多少少需要想一下,究竟url是怎樣,會調用哪個方法,會不會有多個方法同時匹配等等。但是使用屬性路由,你就可以完全的“精准定位”。屬性路由,就是利用特性,重新定義路由。例如:
[HttpPost] [Route("aaa/bbb")] public IEnumerable<Product> GetAllProducts() { return products; }
HttpPost強制了這個方法是需要使用Post來調用,Route強制定義了這個方法的調用路徑。雖然這個方法是以Get開頭,但是[HttpPost]優先順序大於這個約定,我們用PostMan來測試下,我們依然先輸入之前的地址:http://localhost:1111/Products,方法為Get,可以看到拋出Not Found這個錯誤
直到我們將地址改為:http://localhost:1111/aaa/bbb,http方法改為Post的時候,調用才成功。是不是太厲害了,我們可以隨便定義訪問這個方法的路由,什麼約定的規則完全可以置之不理,我們可以完全實現“精准定位”,路由變得不再複雜了,一切都在我們的掌握之中。
那麼,我們怎樣才能使用這種屬性路由呢,首先,我們要打開App_Start文件夾中的WebApiConfig.cs文件,確保一下這句代碼存在:
public static void Register(HttpConfiguration config) { // 確保開啟了屬性路由 config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); }
其次,要引入命名空間:using System.Web.Http;就是這麼簡單,我們就可以使用屬性路由。看到WebApiConfig.cs的代碼,有人擔心,會不會是因為config.MapHttpAttributeRoutes()代碼在前,所以優先順序才大於約定的路由呢,我們可以換一下次序,代碼改成這樣:
public static void Register(HttpConfiguration config) { config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); //次序在後 config.MapHttpAttributeRoutes(); }
結果發現,屬性路由的優先順序依然大於約定的路由,如果你還擔心,大可直接刪除約定的路由,將上述的代碼改成這樣:
public static void Register(HttpConfiguration config) { config.MapHttpAttributeRoutes(); }
這樣,你就可以完全不用考慮約定的路由對屬性路由的影響,一切的映射都在你的牢牢掌握中。不過壞處就是,你的每個方法,都要顯式指定http方法和路由。但我覺得這個代價是值得的,因為我們再不用花時間去繞來繞去,再不用擔心增加了新方法會不會造成路由衝突等問題,我們只需定好命名規則,保證我們每個方法定義的路由不重覆即可。說著說著,我也覺得自己跟文章開頭說的那位程式猿一樣,在走向一個極端,他是在玩命的用路由,而我是在拼命的阻止路由的多匹配性,追求唯一確定,儘量不讓路由造成我的負擔,也許,這也是一種風格?
剩下的都是很簡單,例如,路由首碼,還是用官方的例子:
public class BooksController : ApiController { [Route("api/books")] public IEnumerable<Book> GetBooks() { ... } [Route("api/books/{id:int}")] public Book GetBook(int id) { ... } [Route("api/books")] [HttpPost] public HttpResponseMessage CreateBook(Book book) { ... } }
每個方法的路由首碼都是“api/books",是不是顯得很重覆,我們可以將首碼抽取,為整個控制器增加公共的首碼,代碼如下:
[RoutePrefix("api/books")] public class BooksController : ApiController { // GET api/books [Route("")] public IEnumerable<Book> Get() { ... } // GET api/books/5 [Route("{id:int}")] public Book Get(int id) { ... } // POST api/books [Route("")] public HttpResponseMessage Post(Book book) { ... } }
路由首碼的重寫,我們可以使用波浪符對首碼進行重寫,例如:
[RoutePrefix("api/books")] public class BooksController : ApiController { // GET /api/authors/1/books [Route("~/api/authors/{authorId:int}/books")] public IEnumerable<Book> GetByAuthor(int authorId) { ... } // ... }
除此之外,還有路由約束,例如Route("api/books/{id:int}"),表示id是一個32位整數,如果是可選的,可以在後面加"?",例如Route("api/books/{id:int?}")
更詳細的使用可以參考官網文檔:https://docs.microsoft.com/en-us/aspnet/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2
有了屬性路由,我們甚至可以極端的拋棄約定的路由,從而實現”精准定位“,一切定位都可以牢牢的掌握在自己手中。相信這樣,大家應該不會再害怕面對路由了吧。