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 谓词,通常情况下,更新部分资源的部分数据时,有以下四种做法:
- 使用 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
参考文献
上一篇: Python数据结构与算法之图的最短路径(Dijkstra算法)完整实例
下一篇: PHP基础(2)
推荐阅读