RESTful 幾乎已算是 API 設計的標準,透過 HTTP Method 區分新增(Create)、查詢(Read)、修改(Update)跟刪除(Delete),簡稱 CRUD 四種資料存取方式,簡約又直覺的風格,讓人用的愛不釋手。
本篇將介紹如何透過 ASP.NET Core 實作 REST-Like API。
iT 邦幫忙 2018 鐵人賽 - Modern Web 組參賽文章:
[Day12] ASP.NET Core 2 系列 - REST-Like API
HTTP Method
REST-Like API 對資料的操作行為,透過 HTTP Method 分為以下四種方式:
- 新增(Create)
 用 HTTPPOST透過 Body 傳遞 JSON 或 XML 格式的資料給 Server。例如:| 12
 3
 4
 5
 
 | POST http:{
 "id": 1,
 "name": "John Wu"
 }
 
 |  
 
- 查詢(Read)
 用 HTTPGET透過 URL 帶查詢參數。通常查詢單一資源會用路由參數(Routing Parameter)帶上唯一值(Primary Key);多筆查詢會用複數,而查詢條件用 Query String。例如:| 12
 3
 4
 5
 6
 
 | GET http://localhost:5000/api/users/1
 
 GET http://localhost:5000/api/users
 
 GET http://localhost:5000/api/users?q=john
 
 |  
 
- 修改(Update)
 修改資料如同查詢跟新增的組合,用 HTTPPUT透過 URL 帶路由參數,作為找到要修改的目標;再透過 Body 傳遞 JSON 或 XML 格式的資料給 Server。例如:| 12
 3
 4
 
 | PUT http:{
 "name": "John"
 }
 
 |  
 
- 刪除(Delete)
 刪除資料同查詢,用 HTTPDELETE透過 URL 帶路由參數,作為找到要刪除的目標。例如:| 1
 | DELETE http://localhost:5000/api/users/1
 |  
 
HTTP Method Attribute
[鐵人賽 Day06] ASP.NET Core 2 系列 - MVC 有提到,過去 ASP.NET MVC 把 MVC 及 Web API 的套件分開,但在 ASP.NET Core 中 MVC 及 Web API 用的套件是相同的。所以只要裝 Microsoft.AspNetCore.Mvc 套件就可以用 Web API 了。路由方式也跟 [鐵人賽 Day07] ASP.NET Core 2 系列 - 路由 (Routing) 介紹的 RouteAttribute 差不多,只是改用 HTTP Method Attribute。
HTTP Method Attribute 符合 RESTful 原則的路由設定方式如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 
 | [Route("api/[controller]s")]public class UserController : Controller
 {
 [HttpGet]
 public List<UserModel> Get(string q)
 {
 
 }
 
 [HttpGet("{id}")]
 public UserModel Get(int id)
 {
 
 }
 
 [HttpPost]
 public int Post([FromBody]UserModel user)
 {
 
 }
 
 [HttpPut("{id}")]
 public void Put(int id, [FromBody]UserModel user)
 {
 
 }
 
 [HttpDelete("{id}")]
 public void Delete(int id)
 {
 
 }
 }
 
 | 
目前 ASP.NET Core 還沒有像 ASP.NET MVC 的 MapHttpAttributeRoutes 可以綁 Http Method 的全域路由,都要在 Action 加上 HTTP Method Attribute。
SerializerSettings
用以下程式碼,舉例 SerializerSettings:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 
 | public class UserModel{
 public int Id { get; set; }
 public string Name { get; set; }
 public string Email { get; set; }
 public string PhoneNumber { get; set; }
 public string Address { get; set; }
 }
 
 
 
 [Route("api/[controller]s")]
 public class UserController : Controller
 {
 [HttpGet("{id}")]
 public UserModel Get(int id)
 {
 return new UserModel {
 Id = 1,
 Name = "John Wu"
 };
 }
 }
 
 | 
camel Case
過去 ASP.NET Web API 2 預設是 Pascal Case;而 ASP.NET Core 預設是使用 camel Case。
若想要指定用 ContractResolver,可以在 Startup.cs 的 ConfigureServices 加入 MVC 服務時,使用 AddJsonOptions 設定如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | public class Startup
 {
 public void ConfigureServices(IServiceCollection services)
 {
 services.AddMvc()
 .AddJsonOptions(options =>
 {
 options.SerializerSettings.ContractResolver
 = new CamelCasePropertyNamesContractResolver();
 });
 
 
 }
 }
 
 | 
呼叫 http://localhost:5000/api/users/1 會回傳 JSON 如下:
| 12
 3
 4
 5
 6
 7
 
 | {"id": 1,
 "name": "John Wu",
 "email": null,
 "phoneNumber": null,
 "address": null
 }
 
 | 
Pascal Case
若想保持跟 ASP.NET Web API 2 一樣使用 Pascal Case,ContractResolver 則改用 DefaultContractResolver。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | public class Startup
 {
 public void ConfigureServices(IServiceCollection services)
 {
 services.AddMvc()
 .AddJsonOptions(options =>
 {
 options.SerializerSettings.ContractResolver
 = new DefaultContractResolver();
 });
 }
 }
 
 | 
DefaultContractResolver 名稱是延續 ASP.NET,雖然名稱叫 Default,但在 ASP.NET Core 它不是 Default。CamelCasePropertyNamesContractResolver 才是 ASP.NET Core 的 Default ContractResolver。
呼叫 http://localhost:5000/api/users/1 會回傳 JSON 如下:
| 12
 3
 4
 5
 6
 7
 
 | {"Id": 1,
 "Name": "John Wu",
 "Email": null,
 "PhoneNumber": null,
 "Address": null
 }
 
 | 
Ignore Null
上述兩個 JSON 回傳,都帶有 null 的欄位。在轉型的過程,找不到欄位會自動轉成 null,傳送的過程忽略掉也沒差,反而可以節省到一點流量。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | public class Startup
 {
 public void ConfigureServices(IServiceCollection services)
 {
 services.AddMvc()
 .AddJsonOptions(options =>
 {
 options.SerializerSettings.NullValueHandling
 = Newtonsoft.Json.NullValueHandling.Ignore;
 });
 }
 }
 
 | 
呼叫 http://localhost:5000/api/users/1 會回傳 JSON 如下:
| 12
 3
 4
 
 | {"id": 1,
 "name": "John Wu"
 }
 
 | 
範例程式
Startup.cs
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | public class Startup{
 public void ConfigureServices(IServiceCollection services)
 {
 services.AddMvc()
 .AddJsonOptions(options => {
 options.SerializerSettings.NullValueHandling
 = Newtonsoft.Json.NullValueHandling.Ignore;
 });
 }
 
 public void Configure(IApplicationBuilder app)
 {
 app.UseMvc();
 }
 }
 
 | 
Models\ResultModel.cs
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | namespace MyWebsite.Models{
 public class ResultModel
 {
 public bool IsSuccess { get; set; }
 public string Message { get; set; }
 public object Data { get; set; }
 }
 }
 
 | 
我習慣用一個 ResultModel 來包裝每個 API 回傳的內容,不論調用 Web API 成功失敗都用此物件包裝,避免直接 throw exception 到 Client,產生 HTTP Status 200 以外的狀態。
Controllers/UserController.cs
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 
 | using System.Collections.Generic;using System.Linq;
 using System.Text.RegularExpressions;
 using Microsoft.AspNetCore.Mvc;
 using MyWebsite.Models;
 
 namespace MyWebsite.Controllers
 {
 [Route("api/[controller]s")]
 public class UserController : Controller
 {
 private static List<UserModel> _users = new List<UserModel>();
 
 [HttpGet]
 public ResultModel Get(string q)
 {
 var result = new ResultModel();
 result.Data = _users.Where(c => string.IsNullOrEmpty(q)
 || Regex.IsMatch(c.Name, q, RegexOptions.IgnoreCase));
 result.IsSuccess = true;
 return result;
 }
 
 [HttpGet("{id}")]
 public ResultModel Get(int id)
 {
 var result = new ResultModel();
 result.Data = _users.SingleOrDefault(c => c.Id == id);
 result.IsSuccess = true;
 return result;
 }
 
 [HttpPost]
 public ResultModel Post([FromBody]UserModel user)
 {
 var result = new ResultModel();
 user.Id = _users.Count() == 0 ? 1 : _users.Max(c => c.Id) + 1;
 _users.Add(user);
 result.Data = user.Id;
 result.IsSuccess = true;
 return result;
 }
 
 [HttpPut("{id}")]
 public ResultModel Put(int id, [FromBody]UserModel user)
 {
 var result = new ResultModel();
 int index;
 if ((index = _users.FindIndex(c => c.Id == id)) != -1)
 {
 _users[index] = user;
 result.IsSuccess = true;
 }
 return result;
 }
 
 [HttpDelete("{id}")]
 public ResultModel Delete(int id)
 {
 var result = new ResultModel();
 int index;
 if ((index = _users.FindIndex(c => c.Id == id)) != -1)
 {
 _users.RemoveAt(index);
 result.IsSuccess = true;
 }
 return result;
 }
 }
 }
 
 | 
執行結果
透過 Postman 測試 API。
- 新增(Create)
 ![[鐵人賽 Day12] ASP.NET Core 2 系列 - REST-Like API - 新增(Create)](/images/ironman/i12-1.png) 
- 查詢(Read)
 ![[鐵人賽 Day12] ASP.NET Core 2 系列 - REST-Like API - 查詢(Read)](/images/ironman/i12-2.png) 
- 修改(Update)
 ![[鐵人賽 Day12] ASP.NET Core 2 系列 - REST-Like API - 修改(Update)](/images/ironman/i12-3.png) 
- 刪除(Delete)
 ![[鐵人賽 Day12] ASP.NET Core 2 系列 - REST-Like API - 刪除(Delete)](/images/ironman/i12-4.png) 
2018/01/02 補充
經大師指點,原標題為 ASP.NET Core 2 系列 - RESTful API,但範例未符合 HATEOAS(Hypermedia As The Engine Of Application State) 原則,所以不得稱為 RESTful API。
RESTful API 有四個重要的原則要遵守:
- Level 0
 使用 HTTP 做為資料傳輸的媒介。
- Level 1
 不要提供一個包山包海的 API,而是要區分資源,每個資源都該有對應的 API。
- Level 2
 透過 HTTP Method 區分新增(Create)、查詢(Read)、修改(Update)跟刪除(Delete)。
- Level 3
 對同資源可以用鏈結表達的方式,向下延伸查詢或修改。
 參考範例:HATEOAS
因本篇範例未符合 Level 3 HATEOAS 原則,所以把標題改為 ASP.NET Core 2 系列 - REST-Like API。
參考
Routing in ASP.NET Core
Attribute Routing in ASP.NET Core
Richardson Maturity Model
HATEOAS