Asp.NET Web API 2系列(二):靈活多樣的路由配置

愛在846發表於2019-06-19

1. 導言

路由系統是請求訊息進入ASP.NET Web API訊息處理管道的第一道屏障,其根本目的在於利用註冊的路由對請求的URL進行解析以確定目標HTTPController和Action的名稱,以及與目標Action方法某個引數進行繫結的路由變數。

WebService和WCF的協議都是soap協議,資料的序列化和反序列化都是soap的格式。而WebAPI是基於Http協議,請求和返回格式結果預設是 json格式,因此,比WCF更簡單、更通用,比 WebService 更節省流量、更簡潔。 Web API是在.NET Framework上構建RESTful應用程式的理想平臺,為了更清楚弄懂WebAPI的路由配置,我們首先要了解HTTP協議和RESTful架構風格。

2. HTTP協議

HTTP協議(HyperText Transfer Protocol,超文字傳輸協議)是因特網上應用最為廣泛的一種網路傳輸協議,所有的WWW檔案都必須遵守這個標準。我們在這裡緊列舉和本文關係密切的HTTP請求方法和HTTP狀態碼。

2.1 HTTP請求方法

根據HTTP標準,HTTP請求可以使用多種請求方法。

HTTP1.0定義了三種請求方法: GET, POST 和 HEAD方法,HTTP1.1新增了五種請求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法,如下表所示:

序號方法描述
1 GET 請求指定的頁面資訊,並返回實體主體。
2 HEAD 類似於get請求,只不過返回的響應中沒有具體的內容,用於獲取報頭
3 POST 向指定資源提交資料進行處理請求(例如提交表單或者上傳檔案)。資料被包含在請求體中。POST請求可能會導致新的資源的建立和/或已有資源的修改。
4 PUT 從客戶端向伺服器傳送的資料取代指定的文件的內容。
5 DELETE 請求伺服器刪除指定的頁面。
6 CONNECT HTTP/1.1協議中預留給能夠將連線改為管道方式的代理伺服器。
7 OPTIONS 允許客戶端檢視伺服器的效能。
8 TRACE 回顯伺服器收到的請求,主要用於測試或診斷。

2.2 HTTP狀態碼

當瀏覽者訪問一個網頁時,瀏覽者的瀏覽器會向網頁所在伺服器發出請求。當瀏覽器接收並顯示網頁前,此網頁所在的伺服器會返回一個包含HTTP狀態碼(HTTP Status Code)的資訊頭(server header)用以響應瀏覽器的請求。

常見的HTTP狀態碼如下表:

狀態碼狀態碼英文名稱中文描述
200 OK 請求成功。一般用於GET與POST請求
301 Moved Permanently 永久移動。請求的資源已被永久的移動到新URI,返回資訊會包括新的URI,瀏覽器會自動定向到新URI。今後任何新的請求都應使用新的URI代替
404 Not Found 伺服器無法根據客戶端的請求找到資源(網頁)。通過此程式碼,網站設計人員可設定"您所請求的資源無法找到"的個性頁面
500 Internal Server Error 伺服器內部錯誤,無法完成請求

3. RESTful介紹

在介紹RESTful之前,我們需瞭解什麼REST,他有那些特徵,以及REST成熟度模型。

3.1 REST介紹

REST是Representational State Transfer的縮寫,翻譯為表象化狀態轉變或表述性狀態轉變,是Roy Fielding博士在2000年他的博士論文中提出來的一種軟體架構風格,它包含了一個分散式超文字系統中對於元件、聯結器和資料的約束。REST 是作為網際網路自身架構的抽象而出現的,其關鍵在於所定義的架構上的各種約束。只有滿足這些約束,才能稱之為符合 REST 架構風格。

3.2 REST系統的特徵

RESR系統包括6個特徵,如下所示:

(1)客戶端-伺服器結構(Client-Server)

通過一個統一的介面來分開客戶端和伺服器,使得兩者可以獨立開發和演化。客戶端的實現可以簡化,而伺服器可以更容易的滿足可伸縮性的要求;

(2)無狀態(Stateless)

在不同的客戶端請求之間,伺服器並不儲存客戶端相關的上下文狀態資訊。任何客戶端發出的每個請求都包含了伺服器處理該請求所需的全部資訊;

(3)可快取(Cachable)

客戶端可以快取伺服器返回的響應結果。伺服器可以定義響應結果的快取設定。

(4)分層的系統(Layered System)

在分層的系統中,可能有中間伺服器來處理安全策略和快取等相關問題,以提高系統的可伸縮性。客戶端並不需要了解中間的這些層次的細節。

(5)按需程式碼(Code-On-Demand,可選)

伺服器可以通過傳輸可執行程式碼的方式來擴充套件或自定義客戶端的行為。這是一個可選的約束。

(6)統一介面(Uniform Interface)

該約束是 REST 服務的基礎,是客戶端和伺服器之間的橋樑。該約束又包含下面 4 個子約束。

  • 資源識別符號:每個資源都有各自的識別符號。客戶端在請求時需要指定該識別符號。在 REST 服務中,該識別符號通常是 URI。客戶端所獲取的是資源的表達(representation),通常使用 XML 或 JSON 格式。
  • 通過資源的表達來操縱資源:客戶端根據所得到的資源的表達中包含的資訊來了解如何操縱資源,比如對資源進行修改或刪除。
  • 自描述的訊息:每條訊息都包含足夠的資訊來描述如何處理該訊息。
  • 超媒體作為應用狀態的引擎(HATEOAS):客戶端通過伺服器提供的超媒體內容中動態提供的動作來進行狀態轉換。

3.3 REST成熟度模型

Richardson 提出的 REST 成熟度模型。該模型把 REST 服務按照成熟度劃分成 4 個層次,我們常用到的就是Level1和Level2,如下圖所:

具體說明如下: 

Level 0:Web 服務只是使用 HTTP 作為傳輸方式,實際上只是遠端方法呼叫(RPC)的一種具體形式。SOAP 和 XML-RPC 都屬於此類。

Level 1:Web 服務引入了資源的概念。每個資源有對應的識別符號和表達。

Level 2:Web 服務使用不同的 HTTP 方法來進行不同的操作,並且使用 HTTP 狀態碼來表示不同的結果。如 HTTP GET 方法來獲取資源,HTTP DELETE 方法來刪除資源。

Level 3:Web 服務使用 HATEOAS。在資源的表達中包含了連結資訊。客戶端可以根據連結來發現可以執行的動作。例如,客戶端通過訂單資源中包含的連結取消某一訂單,GET 請求被髮送去獲取該訂單。HATEOAS 的優點包括無需在客戶端程式碼中寫入硬連結的 URL。此外,由於資源資訊中包含可允許操作的連結,客戶端無需猜測在資源的當前狀態下執行何種操作。

 3.4 RESTful

RESTful是一種常見的REST應用,是遵循REST風格的web服務,REST式的web服務是一種ROA(面向資源的架構)。

RESTful資源操作如下表:

http方法資源操作冪等安全
GET SELECT
POST INSERT
PUT UPDATE
DELETE DELETE

注:冪等性:對同一REST介面的多次訪問,得到的資源狀態是相同的;安全性:對該REST介面訪問,不會使伺服器端資源的狀態發生改變。

RESTful URL請求格式與傳統請求格式比較如下表所示:

傳統URL請求格式RESTFul請求格式描述
http:/localhost/user/query/1  GET  http:/localhost/user/1  GET 根據使用者id查詢使用者資料
http:/localhost/user/save      POST http:/localhost/user     POST 新增使用者
http:/localhost/user/update   POST  http:/localhost/user     PUT 修改使用者資訊
http:/localhost/user/delete    GET/POST http:/localhost/user     DELETE 刪除使用者資訊

4.Web API路由

路由的目的是用於解析請求的URL來確定Controller和Action。Web API預設路由是通過http的方法(get/post/put/delete)去匹配對應的action,也就是說webapi的預設路由並不需要指定action的名稱,當然,WebApi也支援MVC裡面的路由機制,但RestFul風格的服務要求請求的url裡面不能包含action,所以,在WebApi裡面是並不提倡使用MVC路由機制的。下邊通過例子介紹Web API路由原理以及使用。

4.1新建空的Web API應用程式

上篇部落格介紹了手動搭建基本框架,這次我們通過VS2017提供的模板,新建空的WebAPI應用程式MyWebAPI2,構建過程中僅勾選Web API,如下圖所示。

建立完成,應用程式的目錄結構如下圖所示。

  4.2預設路由

App_Start資料夾下WebApiConfig.cs類用於註冊Web API路由,預設路由程式碼如下:

public static void Register(HttpConfiguration config)
        {
           //預設路由
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }

在Models資料夾增加一個Student類,程式碼如下:

public class Student
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public string Sex { get; set; }
        public int Age { get; set; }
        public string Dept { get; set; }
    }

在Controllers資料夾中新增一個Web API控制類(v2.1),命名為StudentController,並修改相關程式碼,如下所示:

 public class StudentController : ApiController
    {
        private static List<Student> studentList = new List<Student>()
        {
            new Student() {Id = "001", Name = "張三", Sex = "", Age = 19, Dept = "軟體學院"},
            new Student() {Id = "002", Name = "李麗", Sex = "", Age = 19, Dept = "資環學院"}
        };
        
        [HttpGet]
        public IEnumerable<Student> Get()
        {
            return studentList;
        }

        [HttpGet]
        public Student Get(string id)
        {
            List<Student> tempList = studentList.Where(p => p.Id == id).ToList();
            return tempList.Count==1?tempList.First():null ;
        }

        [HttpPost]
        public bool Post([FromBody]Student student)
        {
            if (student == null) return false;
            if (studentList.Where(p=>p.Id==student.Id).ToList().Count>0) return false;
            studentList.Add(student);
            return true;
        }

        [HttpPut]
        public bool Put(string id, [FromBody]Student student)
        {
            if (student == null) return false;
            List<Student> tempList = studentList.Where(p => p.Id == id).ToList();
            if (tempList.Count == 0) return false;
            Student originStudent = tempList[0];
            originStudent.Name = student.Name;
            originStudent.Sex = student.Sex;
            originStudent.Age = student.Age;
            originStudent.Dept = student.Dept;
            return true;
        }
[HttpDelete]
public bool Delete(string id) { List<Student> tempList = studentList.Where(p => p.Id == id).ToList(); if (tempList.Count == 0) return false; studentList.Remove(tempList[0]); return true; } }

在實際專案中,增刪改查這些操作都是和資料庫打交道的,這裡為了演示具體實現,用一個靜態陣列儲存資料。執行程式,我們採用Fiddler工具進行測試呼叫。

測試一:呼叫Get()方法

功能說明:通過路由解析StudentController中的Get()Action,獲取所有學生列表。

URL:http://localhost:52317/api/student;HTTP方法:GET。如下圖所示:

測試結果:HTTP狀態碼為200,獲取的JSON資料如下所示:

測試二:呼叫Get(string id)方法

功能說明:通過路由解析StudentController中的Get(string id)Action,根據學號獲取某個學生資訊。

URL:http://localhost:52317/api/student/002;HTTP方法:GET

測試結果:HTTP狀態碼為200,獲取的JSON資料如下所示:

測試三:呼叫Post([FromBody]Student student)方法

功能說明:通過路由解析StudentController中的Post([FromBody]Student student)Action,插入一條資料,其中引數前新增[FromBody]是指該引數不是從URL中獲取,而是在RequestBody中獲取。

URL:http://localhost:52317/api/student;HTTP方法:POST。在RequestBody輸入Json格式的資料,如下圖所示:

 測試結果:HTTP狀態碼為200,返回的資料TRUE。重複測試一,獲取所有學生列表如下圖所示。

測試四:呼叫Put(string id, [FromBody]Student student)方法

功能說明:通過路由解析StudentController中的Put(string id, [FromBody]Student student)Action,實現通過學號更新資訊,其中學號id是在Url中獲取。

URL:http://localhost:52317/api/student/002;HTTP方法:PUT

 測試結果:HTTP狀態碼為200,返回的資料TRUE。

測試五:呼叫Delete(string id)方法

功能說明:通過路由解析StudentController中的Delete(string id)Action,刪除某學生資訊,其中學號id是在Url中獲取。

URL:http://localhost:52317/api/student/002;HTTP方法:DELETE

 測試結果:HTTP狀態碼為200,返回的資料TRUE。

 總結:通過上邊測試,我們可以到URL中沒有用到Action,預設路由就是通過引數和HTTP方法匹配Controller中的Action。

 4.3自定義路由

在實際專案中,如果http請求的型別相同,且請求引數相同(如Get),這個時候按照預設路由肯定會出問題,具體出現什麼問題,我們在這做個測試:

在StudentController中新增一個Action,用於查詢某個學院的所有同學,程式碼如下:

        [HttpGet]
        public IEnumerable<Student> GetByDept(string id)
        {
            List<Student> tempList = studentList.Where(p => p.Dept == id).ToList();
            return tempList;
        }

這時,執行程式,輸入URL:http://localhost:52317/api/student/資環學院;HTTP方法:GET。測試結果發現返回的結果null,跟蹤測試程式碼發現該URL呼叫的Get(string id)這個Action。

那麼問題就來了,怎麼才能呼叫這個Action呢?其中有兩種方法一種是自定義路由,就像MVC那樣在模板中增加Action,另一種為通過Web API路由特性實現。

在App_Start資料夾下WebApiConfig.cs類中新增ActionAPI路由模板,程式碼如下:

public static void Register(HttpConfiguration config)
        {
           //預設路由
            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 }
            );
        }

執行程式,輸入URL:http://localhost:52317/api/student/GetByDept/資環學院;HTTP方法:GET。執行結果如下:

注:若出現中文亂碼,解決途徑請參考Fiddler抓包中文亂碼問題

由此可知通過自定義路由可以解決該問題,但是不符合RESTful風格,所有不提倡使用該方法。

4.4特性路由

預設路由解決不了的訪問,可以通過Web API的路由特性解決。

啟動路由特性:在App_Start資料夾下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 }
            );
            //自定義路由
            config.Routes.MapHttpRoute(
                name: "ActionApi",
                routeTemplate: "api/{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }

修改StudentController中GetByDept程式碼如下:

        [Route("student/GetByDept/{dept}")]
        [HttpGet]
        public IEnumerable<Student> GetByDept(string dept)
        {
            List<Student> tempList = studentList.Where(p => p.Dept == dept).ToList();
            return tempList;
        }

執行程式,輸入URL:http://localhost:52317/student/GetByDept/軟體學院;HTTP方法:GET。執行結果如下:

通過此例,可以看到Web API特性路由非常靈活方便,具體使用要結合實際專案靈活運用。

5. 總結

本文在開始部分介紹了HTTP協議及RESTful架構風格,讓我們更深入的瞭解Web API的設計初衷,又通過實際的例子(文中的程式碼及結果均通過了測試),讓我們掌握了路由配置的各種方法。文中若有不足之處,還望海涵,博文寫作不易希望多多支援,後續會更新更多內容,感興趣的朋友可以加關注,歡迎留言交流!

 

相關文章