用ASP.NET Core 2.0 建立規範的 REST API -- 預備知識

solenovex發表於2018-05-09

什麼是REST

REST 是 Representational State Transfer 的縮寫. 它是一種架構的風格, 這種風格基於一套預定義的規則, 這些規則描述了網路資源是如何定義和定址的.

一個實現了REST這些規則的服務就叫做RESTful的服務.

最早是由Roy Fielding提出的.

RPC 風格

/getUsers
/getUser?id=1
/createUser
/deleteUser?id=4
/updateUser?name=dave

 

上面這些節點是針對User的CRUD操作. 

這種樣式風格的web服務更傾向於叫做RPC風格的服務.

在RPC的世界裡, 節點僅僅就是可以在遠端被觸發的函式, 而在REST的世界裡, 節點就是實體, 也叫做資源.

REST的原則/約束

REST有6大原則/約束, 每一個原則都是對API有正面或負面影響的設計決定.

RESTful API 最關心的有這幾方面: 效能, 可擴充套件性, 簡潔性, 互操作性, 通訊可見性, 元件便攜性和可靠性.

這些方面被封裝在REST的6個原則裡, 它們是: 

1. 客服端-服務端約束: 客戶端和服務端是分離的, 它們可以獨自的進化.

2. 無狀態: 客戶端和服務段的通訊必須是無狀態的, 狀態應包含在請求裡的. 也就是說請求裡要包含服務端需要的所有的資訊, 以便服務端可以理解請求並可以創造上下文.

3. 分層系統: 就像其它的軟體架構一樣, REST也需要分層結構, 但是不允許某層直接訪問不相鄰的層. 

4. 統一介面: 這裡分為4點, 他們是: 資源識別符號(URI), 資源的操作(也就是方法Method, HTTP動詞), 自描述的響應(可以認為是媒體型別Media-Type), 以及狀態管理(超媒體作為應用狀態的引擎 HATEOAS, Hypermedia as the Engine of Application State).

5. 快取: 快取約束派生於無狀態約束, 它要求從服務端返回的響應必須明確表明是可快取的還是不可快取的.

6. 按需編碼: 這允許客戶端可以從服務端訪問特定的資源而無須知曉如何處理它們. 服務端可以擴充套件或自定義客戶端的功能.

只有滿足了這6個原則的系統才可以真正稱得上是RESTful的, 其實大部分系統的RESTful API並不是RESTful的, 但這樣並不代表這些API就不好, 利弊需要開發人員去衡量.

Richardson 成熟度模型

Richardson 成熟度模型代表著你的API是否足夠成熟, 分為4個級別, 0代表最差, 3代表最好.

0級, Plain Old XML沼澤:

這裡HTTP協議只是被用來進行遠端互動, 協議的其餘部分都用錯了, 都是RPC風格的實現(例如SOAP, 尤其是使用WCF的時候).

例如:

POST (查詢資料資訊)
http://host/myapi

POST (建立資料)
http://host/myapi

 

1級, 資源:

這級裡, 每個資源都對映到一個URI上了, 但是HTTP方法並沒有正確的使用, 結果的複雜度不算太高.

例如這兩個查詢:

POST
http://host/api/authors
POST
http://host/api/authors/{id}

 

2級, 動詞:

正確使用了HTTP動詞, 狀態碼也正確的使用了, 同時也去掉了不必要的變種.

例如:

GET
http://host/api/authors
200 Ok (authors)
POST (author representation)
http://host/api/authors
201 Created (author)

 

3級, 超媒體:

API支援超媒體作為應用狀態的引擎 HATEOAS, Hypermedia as the Engine of Application State, 引入了可發現性.

例如:

GET
http://host/api/authors
200 Ok (返回了authors 和 驅動應用程式的超連結)

 

介紹ASP.NET Core

略.

但是, 你需要知道以下概念: .NET Core, .NET Standard.

還需要會使用下列工具: .NET Core CLI, Visual Studio 2017/Visual Studio Code/Visual Studio for Mac

ASP.NET Core 支援建立Web API, 但並不是直接支援RESTful的 Web API.

 

ASP.NET Core的基本知識

這部分還是需要簡單的介紹下, 如果已經會了, 請略過本文其餘部分.

建立ASP.NET Core專案

開啟VS2017, 選擇ASP.NET Core Web Application專案模板, 寫好名字, OK.

 

選擇空模板, OK:

 

專案建立好了, 結果如下:

然後我們看一下專案檔案, 右鍵編輯MyRestful.Api:

這裡, SDK屬性表示了我們使用的是哪個SDK, 而目標框架是.NET Core 2.0.

(提示: 如果需要指向多個目標框架的話可以使用TargetFrameworks元素, 注意多了個s)

 

看一下Program.cs:

Main方法是程式的入口. 而Web的宿主是通過BuildWebHost函式來例項化的, 它呼叫了WebHost.CreateDefaultBuilder方法, 很明顯這是一個建造者模式, 它最終會構建出一個web宿主.

呼叫WebHost.CreateDefaultBuilder會返回一個IWebHostBuilder, 它允許我們進行一些配置動作.

程式啟動

UseStartup方法會註冊一個類, 這個類負責配置整個程式的啟動過程. 這裡預設用的是Startup類.

Startup類有兩個方法 ConfigureServices (這個可以沒有) 和 Configure (這個必須有):

在Configure方法裡, 配置應該遵循Add/Use的風格樣式, 首先定義需要什麼, 然後定義如何使用它.

而在ConfigureServices方法裡, 所有程式級的依賴項都可以在這裡註冊到預設的IoC容器裡, 把它們新增到IServiceCollection即可.

Configure方法才是真正負責配置HTTP請求管道的方法, 並且執行時也需要它.

IApplicationBuilder的擴充套件方法Run會傳遞一個RequestDelegate, 其內部功能就是回寫Hello World.

 

ASP.NET Core還允許我們按約定為指定環境建立單獨的啟動配置. 啟動類可以通過這個函式定義UseStartup(startupAssemblyName: xxx); 執行時會在這個指定的元件查詢叫做Startup, Startup[環境名]的類, 其中[環境名]就是ASPNETCORE_ENVIRONMENT這個環境變數的值. 如果能找到指定環境的類, 那麼它將覆蓋預設的啟動類. 

例如 環境變數值如果是Developmen的話, 那麼執行時就會嘗試尋找Startup和StartupDevelopment類, 該約定在啟動類裡面的方法名上也有效, 環境特定的啟動類裡的兩個方法分別是 Configure[環境名]和Configure[環境名]Services.

 

除了之前講的Run方法外, IApplicationBuilder還有一個Use擴充套件方法.

Use擴充套件方法接受RequestDelegate作為引數來提供HttpContext, 同時接受也為下一層準備的RequestDelegate引數.

需要注意的是, Run方法和Use方法定義的順序非常重要, 執行時將會精確的按照建立的順序來執行.

 

伺服器

ASP.NET Core 伺服器的作用是響應客戶端發過來的請求, 這些請求會作為HttpContext傳遞進來. ASP.NET Core 內建兩種伺服器:

Kestrel, 它是跨平臺的伺服器, 基於Libuv.

HTTP.sys, 它是僅限Windows系統的伺服器, 基於HTTP.sys核心驅動.

下面就是從客戶端發請求到應用程式的流圖:

其中Kestrel可以作為一個獨立程式自行託管, 也可以在IIS裡. 但是還是建議使用IIS或Nginx等作為反向代理伺服器. 在構建API或微服務時, 這些伺服器可以作為閘道器使用, 因為它們會限制對外暴露的東西也可以更好的與現有系統整合, 所以它們會提供額外的防禦層, 

使用反向代理伺服器(IIS)之後的流圖如下:

讓web宿主工作於IIS之後需要使用IWebHostBuilder的UseIISIntegration這個擴充套件方法.

除了內建的兩種伺服器, 您還可以使用自定義的伺服器, 使用IWebHostBuilder的UserServer擴充套件方法, 它接受一個實現了IServer介面的例項, 您的自定義伺服器需要實現該介面. 這裡就不講了.

 

中介軟體

在應用程式請求管道內裝配的元件就是中介軟體, 它們負責處理通過管道的請求和響應.

在HTTP請求管道的上下文裡, 中介軟體可以叫做請求委託, 它們是由Run, Map 和 Use 擴充套件方法共同組建而成的.

每個中介軟體可以在它被呼叫之前和之後執行可選的邏輯, 同時也可以決定該請求是否可以被送到管道的下一個中介軟體那裡.

請求在中介軟體裡的流圖如下:

看一下這個例子:

如果我在瀏覽器地址輸入 http://localhost:5000/return, 那麼結果就是Returned!

如果輸入 http://localhost:5000/end, 那麼是The End.

如果輸入 http://localhost:5000/xxx?value=1234, 結果是 the number is 1234

如果輸入 http://localhost:5000/xxx?value=abcde, 結果是 Hello, the value is abcde!

 

注意: 應用程式管道里的請求委託(中介軟體)定義的順序是非常重要的, 請求的時候按定義的順序執行, 而響應的順序正好相反.

 

中介軟體最好不要像上面一樣寫在Startup類裡, 每個中介軟體應該放在單獨的類裡. 

我把上例中檢查是否為數字的中介軟體寫在一個單獨的類裡:

這種中介軟體沒有實現特定的介面或者繼承特定類, 它更像是Duck Typing (你走起路來像個鴨子, 叫起來像個鴨子, 那麼你就是個鴨子).

然後在Startup的Configure方法裡呼叫app.UseMiddleware<NumberMiddleware>()即可:

 

路由

在ASP.NET Core裡,使用路由中介軟體RouterMiddleware來處理路由.

想要使用路由, 同樣也是遵循 Add/Use 這個模式. 

首先在ConfigureServices方法裡新增(Add):

然後在Configure方法裡使用(Use):

UseRouter這個擴充套件方法可以接受IRouter或者Action<IRouterBuilder>作為引數.

例如:

當傳送 http://localhost:5000/ GET請求的時候, 返回 Default route.

當 GET http://localhost:5000/user/dave的時候, 返回 Hi dave

當 POST http://localhost:5000/user/dave的時候, 返回 Hi, posted name is dave

其中{name}, 是名為name的引數.

如果寫成"user/{name}/{age:number}", 那麼age這個引數的必須可以被解析為數值型.

而"user/{name}/{gender?}", 這裡的gender引數可以沒有.

 

Controller

HTTP請求通過管道最終到達Action並返回的流圖如下:

預設情況下Controller放在ASP.NET Core專案的Controllers目錄下。

在ASP.NET Core專案裡可以通過多種方式來建立Controller,當然最建議的方式還是通過繼承AspNetCore.Mvc.Controller這個抽象類來建立Controller。

例如:

上例中類名可以不是以Controller結尾。

 

還有其它的方式建立Controller,按約定類名以Controller結尾的POCO類也會被認為是Controller,例如:

 

針對POCO類, 即使名稱不是以Controller結尾,仍然可以把它作為Controller,這就需要在類上面新增 [Controller] 這個屬性:

 

如果某個類的名字以Controller結尾, 但是你不想把它當作Controller,那麼就應該為該類標註 [NonController] 這個屬性:

 

實際上, 看原始碼就可以知道 Controller 繼承於 ControllerBase:

 

 而ControllerBase上面標註著 [Controller] 屬性。

 

Action

在Controller裡面,可以使用public修飾符來定義Action,通常會帶有引數,可以返回任何型別,但是大多數情況下應該返回IActionResultAction的方法名要麼是以HTTP的動詞開頭,要麼是使用HTTP動詞屬性標籤,包括:[HttpGet], [HttpPut], [HttpPost], [HttpDelete], [HttpHead], [HttpOptions], [HttpPatch].

例如:

其中某個方法名如果恰好是以HTTP的動詞開頭,那麼可以通過標註 [NonAction] 屬性來表示這個方法不是Action。

通過繼承Controller基類的方法來建立Controller還是有很多好處的,因為它提供了很多幫助方法,例如:Ok, NotFound, BadRequest等,它們分別對應HTTP的狀態碼 200, 404, 400;此外還有Redirect,LocalRedirect,RedirectToRoute,Json,File,Content等方法。

 

為MVC定義路由有兩種方式:使用IRouteBuilder或者使用基於屬性標籤的路由。針對Rest,最好還是使用基於屬性標籤的方式。

路由屬性標籤可以標註在Controller或者Action方法上,例如:

Controller類上標註的路由“api/[controller]”,其中[controller] 就代表該類的名字去掉結尾Controller的部分,也就是“api/person”。

在Controller上使用[Route]屬性就定義了該Controller下所有Action的路由基地址,每個Action可以包含一個或者多個相對的路由模板(地址),這些路由模板可以在[Http...]中定義。但是如果使用 ~ 這個符號的話,該Action的地址將會是絕對路由地址,也就是覆蓋了Controller定義的基路由。

 

實體繫結

傳入的請求會對映到Action方法的引數,可以實原始資料型別也可以是複雜的型別例如Dto(data transfer object)或ViewModel。這個把Http請求繫結到引數的過程叫做實體繫結。

例如:

 

其中id引數是定義在路由裡的,而name引數在路由裡沒有,但是仍然可以從查詢引數中把name引數對映出來。

注意路由引數和查詢引數的區別,下面這個URL裡val1和val2是查詢引數,它們是在url的後邊使用?和&分隔:

/product?val1=2&val2=10

 

而針對上面的Action,下面這個URL的路由引數id就是123:

/api/first/123

 

 

針對下面這個POST Action:

我們可以通過幾種方式為其傳遞型別為Person的引數。

可以使用查詢引數:/api/people?id=1&name=Dave

如果POST Json資料:

那麼在Action裡面得到的引數person的屬性值都是null。這是因為這樣的原始資料是包含在請求的Body裡面,為了解決這個問題,你需要告訴Action從哪裡獲取引數,針對這個例子就應該使用 [FromBody] 屬性標籤:

如果提交的是表單資料,那麼就應該使用[FromForm]:

其它的出處還有 [FromHeader], [FromRoute], [FromServices]等。

再看一個FromHeader的例子:

 

如果使用複雜型別Person來獲取person引數好像不行,只能使用原始型別的吧?

 

實體驗證

ASP.NET Core內建的實體驗證是通過驗證屬性標籤來實現的,大多數情況下這樣會很方便。

例如:

其中Display不是驗證標籤,但是通過它可以自定義屬性的顯式名稱,在其它錯誤資訊裡可以使用{0}來引用該名稱。

 

判斷實體引數是否符合要求,可以檢查ModelState.IsValid屬性,這個屬性也是由ControllerBase提供的,例如:

傳送一個請求:

這是個不合理的引數,返回的是400 BadRequest,帶著驗證結果:

 

儘管大多數情況西,驗證屬性標籤都滿足要求,但是有時候還是需要進行一些靈活的驗證,你可以使用像FluentValidation這樣的第三方庫,也可以使用內建的方式來實現自定義驗證。

ASP.NET Core內建支援兩種方式來進行自定義驗證:通過繼承ValidationAttribute來建立自定義驗證屬性標籤,或者讓實體實現IValidatebleObject介面。

使用自定義驗證屬性標籤:

把該標籤放到name屬性上

使用剛才的請求,其結果是:

 

另一種方式,在Person類實現IValidatableObject介面

但是我使用這種方法並不好用,不知道我哪裡用錯了!

 

過濾器

和中介軟體一樣,ASP.NET Core MVC的過濾器也可以在請求管道的特定階段的之前或之後執行某些程式碼。過濾器還可以有子管道,子管道里麵包含著其它過濾器。

過濾器和中介軟體的區別:中介軟體是應用程式級別的,它可以處理每個傳送過來的請求;而過濾器是針對MVC的,它只會處理髮往MVC的請求。

ASP.NET Core MVC的過濾器分為5類:

  • 授權過濾器,它是第一個執行的,它的作用就是判斷HTTP Context中的使用者是否擁有當前請求的許可權,如果使用者沒有許可權,那麼它就會“短路”管道。
  • 資源過濾器,在授權過濾器後執行,在管道其它動作之前,和管道動作都結束後執行。它可以實現快取或由於效能原因執行短路操作。它在實體繫結之前執行,所以它也可以對影響實體繫結。
  • Action過濾器,它在Action方法呼叫之前和之後立即執行,它可以操作傳進Action的引數和返回的結果。
  • 異常過濾器,針對在寫入響應Body之前發生的未處理的異常,它可以應用全域性的策略,
  • 結果過濾器,它可以在每個Action結果執行之前和之後執行程式碼,但也只是在Action方法無錯誤的成功完成後才可以執行。

下圖示明瞭這些過濾器在管道中是如何互動的:

過濾器可以作為屬性標籤使用,或者也可以在Startup類裡面進行全域性註冊。

例子:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;

namespace MyRestful.Api.Filters
{
    public class DefaultNameFilter: IActionFilter, IAsyncActionFilter
    {
        public void OnActionExecuting(ActionExecutingContext context)
        {
            context.ActionDescriptor.RouteValues["name"] = "Anonymous";
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
            context.HttpContext.Response.Headers["X-Name"] = context.ActionDescriptor.RouteValues["name"];
        }

        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            OnActionExecuting(context);
            var result = await next();
            OnActionExecuted(result);
        }
    }
}

全域性註冊,在Startup裡:

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc(options =>
            {
                options.Filters.Add<DefaultNameFilter>();
            });
        }

或者自定義一個屬性標籤,內部的程式碼是一樣的:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;

namespace MyRestful.Api.Filters
{
    public class DefaultUserNameFilterAttribute: Attribute, IActionFilter, IAsyncActionFilter
    {
        public void OnActionExecuting(ActionExecutingContext context)
        {
            context.ActionDescriptor.RouteValues["name"] = "Anonymous";
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
            context.HttpContext.Response.Headers["X-Name"] = context.ActionDescriptor.RouteValues["name"];
        }

        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            OnActionExecuting(context);
            var result = await next();
            OnActionExecuted(result);
        }
    }
}

 

然後把該標籤用在Action方法上即可:

        [DefaultUserNameFilter]
        [HttpGet("first/{id}")]
        public IActionResult FindFirstPerson(int id, string name)
        {
            return null;
        }

 

格式化響應結果

Action的結果最好使用IActionResult, 但也可以使用其他型別,例如IEnumerable<T>等。強制結果輸出為特定的型別可以通過呼叫特定的方法來實現,例如JsonResponse就是輸出JSON,ContentResponse就是輸出文字。另外也可以使用[Produces(xxx)] 這個過濾器,它可以應用於全域性,controller或者Action。

在REST服務裡,有個詞叫內容協商,它表示客戶端通過Accept Header裡的media-type來指定所需的結果格式。

ASP.NET Core MVC 預設實現並使用JSON格式化,但也支援其它格式,這需要在startup裡面註冊。

客戶端瀏覽器可能在請求的Accept Headers裡提供了多種的格式,但是ASP.NET Core MVC 預設是忽略瀏覽器的Accept Header的,並使用標準的輸出格式。但是修改MvcOptions的RespectBrowserAcceptHeader值為true,可以改變這個行為:

ASP.NET Core還提供了 XML 格式,可以在MvcOptions裡面新增:

 

今天先寫到這,還沒有切入正題。

相關文章