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

什麼是REST

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

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

最先是由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 最關心的有這幾方面: 性能, 可擴展性, 簡潔性, 互操做性, 通信可見性, 組件便攜性和可靠性.app

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

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

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裏面添加:

 

今天先寫到這,尚未切入正題。

相關文章
相關標籤/搜索