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
相关文章
相关标签/搜索