JSON Patch


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 謂詞,通常情況下,更新部分資源的部分數據時,有以下四種做法:

  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種做法。

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

示例應用

示例程序引入了swaggerMongoDBdocker-compose等功能,關於 JsonPatch 的部分則使用微軟官方的 JsonPatch 編寫,該庫支持addremovereplacemovecopy方法,實現並不困難。實際使用時,直接以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

參考文獻

  1. JSON Patch
  2. Github v3 API


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM