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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏黑泽君的专栏

网络通信的三要素

5104
来自专栏一场梦

ping命令的高级用法

1605
来自专栏coding for love

在线商城项目03-启用mock服务

对于前后端分离的开发,在后台接口还未就绪时,前端需要使用mock数据进行开发。最容易想到的办法,当然是把mock数据写在页面里,但是这会让我们的页面代码很臃肿,...

970
来自专栏程序员与猫

JSON Patch

1326
来自专栏Seebug漏洞平台

披着狼皮的羊——寻找惠普多款打印机中的RCE漏洞

原文:《A Sheep in Wolf’s Clothing – Finding RCE in HP’s Printer Fleet》

3678
来自专栏腾讯Bugly的专栏

Bugly即将支持的ANR,精神哥告诉你是个什么鬼?

上帝说要有ANR,于是Bugly就有了ANR上报,那么ANR到底是什么? 最近很多童鞋问起精神哥ANR的问题,那么这次就来聊一下,鸡爪怎么泡才好吃...

4244
来自专栏Seebug漏洞平台

披着狼皮的羊——寻找惠普多款打印机中的RCE漏洞

原文:https://foxglovesecurity.com/2017/11/20/a-sheep-in-wolfs-clothing-finding-rce...

973
来自专栏安富莱嵌入式技术分享

【安富莱原创开源应用第2期】基于RL-USB和RL-FlashFS的完整NAND解决方案,稳定好用,可放心用于产品批量

说明: 0. NAND Flash这块经常有人咨询,这里发布一个完整的解决方案,支持擦写均衡,坏块管理,ECC和掉电保护。         早期的时候我们是用的...

4404
来自专栏数据之美

迷之 crontab 异常:不运行、不报错、无日志

1、背景 前几天新同学入职,一不小心将跳板机上的 crontab 清空了,导致凌晨一大批任务异常,同事问了运维同学也没有备份,这一百多个任务要是恢复起来可不是件...

4716
来自专栏代码GG之家

ANR 原理与实战技巧

? 00 手机用用,就卡卡卡。莫名其妙的出现一堆程序无响应,欲哭无泪。这是为什么呢?因为你用的android手机。 android手机,为了...

4867

扫码关注云+社区