JSON Patch

1.前言

能夠這麼說的是,任何一種非強制性約束同時也沒有「標杆」工具支持的開發風格或協議(僅靠文檔是遠遠不夠的),最終的實現上都會被程序員冠上「務實」的名頭,而無論成型了多少個版本,與最初的設計有什麼區別。DDD 是如此,微服務是如此,REST 也是如此。html

雖然這也不難理解,風格從一開始被創造出來後,便再也不屬於做者了。因此仍然把你的符合如下標準git

  • 知足以資源形式定義定義 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」直覺設計。github

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 謂詞,一般狀況下,更新部分資源的部分數據時,有如下四種作法:web

  1. 使用 PUT 謂詞, 儘量使用完整對象來更新資源(即根本不使用 PATCH )。
  2. 使用 JSON Merge Patch 更新部分資源的部分數據(須要使用指定 MIME application/merge-patch+json 來表示)。
  3. 使用 PATCH 謂詞和 JSON Patch(須要使用指定 MIME application/json-patch+json 來表示)
  4. 若是請求不以 MIME 的語義定義的方式修改資源,使用具備合理描述的 POST 謂詞。

我相信大部分系統中,採起的都是第1種和第4種作法,而本文的主題則是第3種作法。mongodb

RFC 5789(PATCH method for HTTP) 中,有一個關於 PATCH 請求的小例子:docker

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表明操做類型,frompath表明目標 json 的層級路徑,value表明操做值。相關語義想必你們都能直接讀出來,更多的信息請參考What is JSON Patch?RFC JSON Patchjson

示例應用

示例程序引入了swaggerMongoDBdocker-compose等功能,關於 JsonPatch 的部分則使用微軟官方的 JsonPatch 編寫,該庫支持addremovereplacemovecopy方法,實現並不困難。實際使用時,直接以JsonPatchDocument<T>做爲包裝便可。api

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

參考文獻

  1. JSON Patch
  2. Github v3 API
相關文章
相關標籤/搜索