使用ASP.net Core的JSON补丁
JSONPatch是一种以非常明确的方式更新API上的文档的方法。它本质上是一个合同,用于准确描述您希望如何修改文档(例如,将字段中的值替换为另一个值),而不必同时发送其余未更改的值。
JSON补丁请求是什么样的?
JSON Patch的官方文档存在于这里:
http
: //jsonpatch.com/,但我们将进行一些挖掘,以了解它在ASP / C#中的工作原理,因为并非所有应用程序都能正常工作。事实上,一项操作还没有成为ASP.net Core的正式版本,但无论如何我们都会快速谈谈它。
对于所有示例,我将针对在C#中看起来像这样的对象编写JSON补丁请求:
1
2
3
4
5
6
|
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public List<string> Friends { get; set; }
}
|
补丁请求都遵循类似的结构类型。它是数组中“操作”的列表。该操作本身有3个属性。
“op” - 定义您要执行的操作的“类型”。例如,添加,替换,测试等
“path” - 要编辑的对象上属性的“路径”。在上面的示例中,如果我们想要编辑“FirstName”,则“path”属性看起来像“/ firstname”
“value” - 在大多数情况下,定义我们想要在操作中使用的值。
现在让我们来看看每个单独的操作。
加
添加操作通常意味着您要向对象添加属性或将项“添加”到数组中。对于前者,这在C#中不起作用。因为C#是强类型语言,所以不能将属性“添加”到编译时尚未定义的对象上。
要将项添加到数组中,请求将如下所示:
1
|
{ "op": "add", "path": "/Friends/1", "value": "Mike" }
|
这将在Friends数组中的索引1处“插入”“Mike”的值。或者,您可以使用“ - ”字符在数组末尾插入记录。
1
|
{ "op": "add", "path": "/Friends/-", "value": "Mike" }
|
去掉
与上面概述的“添加”操作类似,“删除操作”通常表示您要么从对象中删除属性,要么从数组中删除项目。但是因为你实际上无法从C#中的对象“移除”属性,实际发生的是它会将值设置为默认值(T)。在某些情况下,如果对象可以为空(或引用类型),则将其设置为NULL。但要小心,因为当在值类型上使用时,例如int,那么该值实际上会重置为“0”。
要在对象属性上运行Remove以“重置”它,您将运行以下命令:
1
|
{ "op": "remove", "path": "/FirstName"}
|
您还可以运行“删除”操作以删除阵列中的特定项目
1
|
{ "op": "remove", "path": "/Friends/1" }
|
这将从数组索引1中删除该项。没有这样的“where”子句用于删除或删除所以有时这对于从这样的数组索引中删除项目似乎非常危险,因为如果数组自我们获取后发生了更改它来自服务器?实际上有一个JSON Patch操作可以帮助解决这个问题,但稍后会有更多内容。
更换
替换完全符合它在锡上的说法。它取代了另一个的任何价值。这可以用于对象的简单属性:
1
|
{ "op": "replace", "path": "/FirstName", "value": "Jim" }
|
它还可以替换数组项中的特定属性:
1
|
{ "op": "replace", "path": "/Friends/1", "value": "Bob" }
|
它也可以替换整个对象/数组,如下所示:
1
|
{ "op": "replace", "path": "/Friends", "value": ["Bob", "Bill"] }
|
复制
复制会将值从一个路径移动到另一个路径。这可以是属性,对象,数组等。在下面的示例中,我们将Firstname的值移动到LastName。实际上,这并不是很多,因为你通常会看到两个属性的简单替换而不是副本,但它是可能的!如果您真正深入了解JSON Patch的ASP.net核心实现的源代码,您将看到复制操作简单在路径的后台执行“添加”操作。
1
|
{ "op": "copy", "from": "/FirstName", "path" : "/LastName" }
|
移动
Move操作与Copy非常相似,但正如它在tin上所说,该值将不再位于“from”字段。这是另一个,如果你看看ASP.net Core的底层,它实际上在from字段上删除,并在Path字段上添加。
1
|
{ "op": "move", "from": "/FirstName", "path" : "/LastName" }
|
测试
测试操作目前不存在于ASP.net Core的公开发行版中,但是如果你在Github上检查源代码,你可以看到它已经被微软处理,应该进入下一个版本。测试几乎是一种执行乐观锁定的方法,或者更简单地说是检查自检索数据以来服务器上的对象是否已更改。
考虑以下完整补丁:
1
2
3
4
|
[
{ "op": "test", "path": "/FirstName", "value": "Bob" }
{ "op": "replace", "path": "/FirstName", "value": "Jim" }
]
|
这就是说,首先检查“/ FirstName”路径上的值是Bob,如果是,则将其更改为Jim。如果该值不是Bob,则不会发生任何事情。需要注意的一点是,您可以在单个修补程序有效内容中执行多个测试操作,但如果其中任何一个测试失败,则不会应用整个修补程序。
但是为什么JSON补丁呢?
显然,JSON Patch的一大优势在于它的有效载荷非常轻巧,只能准确发送对象上发生的变化。但是在ASP.net Core中还有另一个很大的好处,它实际上有一些很好的用途,唯一的原因是C#是一种打字语言。没有好的例子,很难解释。所以想象一下我从API请求一个“Person”对象。在C#中,模型可能如下所示:
1
2
3
4
5
|
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
|
当从API作为JSON对象返回时,它看起来像这样:
1
2
3
4
|
{
"firstName" : "James",
"lastName" : "Smith"
}
|
现在从前端开始,不使用JSON补丁,我决定只需要更新名字,所以我发回以下有效负载:
1
2
3
|
{
"firstName" : "Jim"
}
|
现在,当我在C#中反序列化此模型时,问题就出现了。没有看下面。我们的模型的价值是什么?
1
2
3
4
5
|
public class Person
{
public string FirstName { get; set; } //Jim
public string LastName { get; set; } //<Null>
}
|
嗯。因为我们没有通过LastName发送它,它被序列化为Null。但这很简单,我们可以忽略null的值,并在我们的数据层上做一些挑剔的事情,只更新我们实际传递的字段。但这不一定是真的,如果该字段实际上可以为空呢?如果我们通过以下有效负载发送了怎么办
1
2
3
4
|
{
"firstName" : "Jim",
"lastName" : null
}
|
所以现在我们实际上已经指定我们想要取消该字段。但是因为C#是强类型的,所以我们无法在服务器端确定有效负载中缺少的值与使用标准模型绑定实际设置为null的值相比。
这似乎是一个奇怪的场景,因为前端可以始终发送完整的模型,永远不会省略字段。在大多数情况下,前端Web库的模型将始终与API的模型匹配。但有一种情况并非总是如此,那就是移动应用程序。通常在提交移动应用程序来说Apple App Store时,可能需要数周时间才能获得批准。在这个时候,您可能还有需要推出的Web或Android应用程序才能使用新模型。在不同平台之间实现同步非常困难,而且往往是不可能 虽然API版本确实可以解决这个问题,但我仍然认为JSON Patch在以稍微不同的方式解决这个问题方面具有很大的实用性。
最后,它就到了这一点!考虑Person对象的以下JSON Patch有效负载:
1
2
3
4
5
6
7
|
[
{
"op": "replace",
"path": "/firstName",
"value": "Jim"
}
]
|
这明确表示我们想要更改名字而不是其他任何名称。从来没有模棱两可的“我们只是忘记发送该财产,还是我们真的希望它'被淘汰'”。它是精确的,并准确地告诉我们将要发生什么。
将JSON补丁添加到您的ASP.net核心项目中
在Visual Studio内部,从程序包管理器控制台运行以下命令以安装官方JSON补丁程序库(在新项目中没有开启ASP.net Core)。
1
|
Install-Package Microsoft.AspNetCore.JsonPatch
|
对于此示例,我将使用以下完整控制器。要注意的要点是我们使用的HTTP Verb是“Patch”,我们接受一种类型的“JsonPatchDocument <T>”并且“应用”我们在补丁上简单地称为“ApplyTo”的更改并传入对象我们想要更新。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
[Route("api/[controller]")]
public class PersonController : Controller
{
private readonly Person _defaultPerson = new Person
{
FirstName = "Jim",
LastName = "Smith"
};
[HttpPatch("update")]
public Person Patch([FromBody]JsonPatchDocument<Person> personPatch)
{
personPatch.ApplyTo(_defaultPerson);
return _defaultPerson;
}
}
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
|
在我们的示例中,我们只是使用存储在控制器上的简单对象并对其进行更新,但在实际的API中,我们将从数据源中提取数据,应用补丁,然后将其保存回来。
当我们使用以下有效负载调用此端点时:
1
2
3
|
[
{"op" : "replace", "path" : "FirstName", "value" : "Bob"}
]
|
我们得到的回应是:
1
2
3
4
|
{
"firstName": "Bob",
"lastName": "Smith"
}
|
真棒!我们的名字改为鲍勃!使用JSON Patch启动和运行真的很简单。
生成补丁
许多人问的第一个问题是他们应该如何制作他们的JSON Patch有效载荷。您不必手动执行此操作!有许多javascript库可以“比较”两个对象以生成补丁。更简单的是,有许多可以“观察”对象并按需生成补丁。JSON Patch网站有一个很好的入门库列表:http:
//jsonpatch.com/
使用带有JSON补丁的Automapper
我经常在JSON Patch周围看到的一个重要问题是,您经常从API返回View Models / DTO,并从那里生成补丁。但是,那么如何将这些补丁应用回数据库对象?人们倾向于过度思考并创建疯狂的“变换”以将补丁从一个对象转换到另一个对象,但是使用Automapper的一种更简单的方法是非常简单。代码的工作原理如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
[HttpPatch("update/{id}")]
public Person Patch(int id, [FromBody]JsonPatchDocument<PersonDTO> personPatch)
{
PersonDatabase personDatabase = _personRepository.GetById(id); // Get our original person object from the database.
PersonDTO personDTO = _mapper.Map<PersonDTO>(personDatabase); //Use Automapper to map that to our DTO object.
personPatch.ApplyTo(personDTO); //Apply the patch to that DTO.
_mapper.Map(personDTO, personDatabase); //Use automapper to map the DTO back ontop of the database object.
_personRepository.Update(personDatabase); //Update our person in the database.
return personDTO;
}
|
需要注意的一点是,在第二次Map调用中将DTO映射回Database对象时,Automapper有一个很好的功能,它可以映射到现有对象,只映射它关心的字段。数据库对象上的任何其他字段将保持不变。
如果您需要设置Automapper的帮助,请查看我们关于
在ASP.net Core中使用Automapper的
精彩教程 。