1.前言 可以這麼說的是,任何一種非強制性約束同時也沒有“標桿”工具支持的開發風格或協議(僅靠文檔是遠遠不夠的),最終的實現上都會被程式員冠上“務實”的名頭,而不管成型了多少個版本,與最初的設計有什麼區別。DDD 是如此,微服務是如此,REST 也是如此。 雖然這也不難理解,風格從一開始被創造出來後 ...
1.前言
可以這麼說的是,任何一種非強制性約束同時也沒有“標桿”工具支持的開發風格或協議(僅靠文檔是遠遠不夠的),最終的實現上都會被程式員冠上“務實”的名頭,而不管成型了多少個版本,與最初的設計有什麼區別。DDD 是如此,微服務是如此,REST 也是如此。
雖然這也不難理解,風格從一開始被創造出來後,便不再屬於作者了。所以仍然把你的符合以下標準
- 滿足以資源形式定義定義 Uri
- 滿足以 HTTP 謂詞語義增刪改查資源
- 符合命名要求
- ……
的“不標準” Web API 看作是 RESTful 的,也未嘗不可。畢竟,誰在乎呢?
更深層次的討論參見Why Some Web APIs Are Not RESTful and What Can Be Done About It。什麼才是真正的 REST Api 並不是本文的重點(Github Rest API v3),筆者在後文討論的具體實現,也只是符合目前流行的“RESTful”直覺設計。
2. HTTP 謂詞
謂詞 | 釋義 | 冪等性 | 安全性 |
---|---|---|---|
HEAD | 用於獲取資源的 HTTP Header 信息 | 是 | 是 |
GET | 用於檢索信息 | 是 | 是 |
POST | 用於創建資源 | 否 | 否 |
PUT | 用於更新或替換完整資源或批量更新集合。對於沒有 Body 的 PUT 動作,請將 Content-Length 設置為 0 |
是 | 否 |
DELETE | 用於刪除資源 | 是 | 否 |
PATCH | 用於使用部分 JSON 數據更新資源信息(在一個請求里可搭載多個動作)。PATCH 是一個相對較新的 HTTP 謂詞,在客戶端或伺服器不支持 PATCH 動作時,也可以使用 Post/Put 更新資源 | 否 | 否 |
3. PATCH & JSON Patch
結合上述 HTTP 謂詞,通常情況下,更新部分資源的部分數據時,有以下四種做法:
- 使用 PUT 謂詞, 儘可能使用完整對象來更新資源(即根本不使用 PATCH )。
- 使用 JSON Merge Patch 更新部分資源的部分數據(需要使用指定 MIME
application/merge-patch+json
來表示)。 - 使用 PATCH 謂詞和 JSON Patch(需要使用指定 MIME
application/json-patch+json
來表示) - 如果請求不以 MIME 的語義定義的方式修改資源,使用具有合理描述的 POST 謂詞。
我相信大部分系統中,採取的都是第1種和第4種做法,而本文的主題則是第3種做法。
在 RFC 5789(PATCH method for HTTP) 中,有一個關於 PATCH 請求的小例子:
PATCH /file.txt HTTP/1.1
Host: www.example.com
Content-Type: application/example
If-Match: "e0023aa4e"
Content-Length: 100
[description of changes]
[description of changes]
代表對目標資源的一系列操作,而JSON Patch
則是描述操作的文檔格式。
// 示例 json 文檔
{
"a":{
"b":{
"c":"foo"
}
}
}
// JSON Patch 操作
[
{ "op": "test", "path": "/a/b/c", "value": "foo" },
{ "op": "remove", "path": "/a/b/c" },
{ "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] },
{ "op": "replace", "path": "/a/b/c", "value": 42 },
{ "op": "move", "from": "/a/b/c", "path": "/a/b/d" },
{ "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }
]
在這個JSON Patch
的例子中,op
代表操作類型,from
和path
代表目標 json 的層級路徑,value
代表操作值。相關語義想必大家都能直接讀出來,更多的信息請參考What is JSON Patch?和 RFC JSON Patch。
示例應用
示常式序引入了swagger
,MongoDB
,docker-compose
等功能,關於 JsonPatch 的部分則使用微軟官方的 JsonPatch 編寫,該庫支持add
,remove
,replace
,move
,copy
方法,實現並不困難。實際使用時,直接以JsonPatchDocument<T>
作為包裝即可。
MongoDB 客戶端推薦註冊為單例。
public interface IMongoDatabaseProvider
{
IMongoDatabase Database { get; }
}
public class MongoDatabaseProvider : IMongoDatabaseProvider
{
private readonly IOptions<Settings> _settings;
public MongoDatabaseProvider(IOptions<Settings> settings)
{
_settings = settings;
}
public IMongoDatabase Database
{
get
{
var client = new MongoClient(_settings.Value.ConnectionString);
return client.GetDatabase(_settings.Value.Database);
}
}
}
/* Startup/ConfigureServices.cs */
public void ConfigureServices(IServiceCollection services)
{
…
services.AddSingleton<IMongoDatabaseProvider, MongoDatabaseProvider>();
…
}
appsettings.json
文件中的資料庫配置部分則為:
{
"ConnectionString": "mongodb://mongodb",
"Database": "ExampleDb"
}
docker-compose.yml
對 web 應用和 MongoDB 的配置如下:
version: '3.4'
services:
aspnetcorejsonpatch:
image: aspnetcorejsonpatch
build:
context: .
dockerfile: AspNetCoreJsonPatch/Dockerfile
depends_on:
- mongodb
ports:
- "8080:80"
mongodb:
image: mongo
ports:
- "27017:27017"
啟動時,定位到docker-compose.yml
所在文件夾,運行docker-compose up
,然後在瀏覽器訪問localhost:8080/swagger
,應用在啟動後會自動創建ExampleDb
資料庫並插入一條數據。筆者也寫了一個獲取信息的介面/api/Persons
,返回值如下:
[
{
"name": "LeBron James",
"oId": "5af995a5b8ea8500018d54b7"
}
]
然後再使用返回的oId
請求/api/Persons/{id}
(UpdateThenAddThenRemoveAsync
)介面,body
的 JsonPatch 描述則用:
/* body */
[
{
"value": "Daby",
"path": "FirstName",
"op": "replace"
},
{
"value": "Example Address",
"path": "Address",
"op": "add"
},
{
"path": "Mail",
"op": "remove"
}
]
/* PersonsController.cs */
[HttpPatch("{id}")]
public async Task<PersonDto> UpdateThenAddThenRemoveAsync(string id,
[FromBody] JsonPatchDocument<Person> personPatch)
{
var objectId = new ObjectId(id);
var person = await _personRepository.GetAsync(objectId);
personPatch.ApplyTo(person);
await _personRepository.UpdateAsync(person);
return new PersonDto
{
OId = person.Id.ToString(),
Name = $"{person.FirstName} {person.LastName}"
};
}
其他相關代碼另請查閱。不過需要再提一點的是,Visual Studio 15.7 版本對docker-compose.yml
的文本語法解析有些問題,詳見MSBuild failing to parse a valid compose file,比如以下代碼將無法編譯:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://0.0.0.0:80
- ConnectionString=${MONGODB:-mongodb://mongodb}
- Database=ExampleDb