Json Patch介紹 根據RFC 690 標準定義的。JSON Patch 允許您指定如何修改 JSON 文檔,而無需傳送整個文檔,特別是在 RESTful API 中,用於局部更新資源而不是使用 PUT 或 POST 請求進行整個替換。這有助於減少傳輸量並提高效率,尤其在資源很大或頻繁更新時。
範例程式 ASP.NET Core Web API 中的 JsonPatch
Github範例程式https://github.com/DeliaHung/dotnet-Json-Patch
請求格式 { "op" : "replace" , "path" : "/FirstName" , "value" : "hong" }
op 描述所需的更改。這些操作包括:
add:在 JSON 文檔中添加新的項目。
remove:從 JSON 文檔中刪除現有的項目。
replace:替換 JSON 文檔中現有項目的值。
move:將一個項目移動到另一個位置。
copy:從一個位置複製一個項目到另一個位置。
test:測試 JSON 文檔的某個值是否等於指定的值。
這些操作是按照順序應用的,如果有任何一個操作失敗, 整個patch都會被終止。
套件安裝 PM> Install-Package Microsoft.AspNetCore.JsonPatch
Program.cs註冊
System.Text.Json型輸入格式器不支援轉換,需要使用 NewtonsoftJson
builder.Services.AddControllers() .AddNewtonsoftJson();
若要保持其他請求使用System.Text.Json,範例如下。
builder.Services.AddControllers(options => { options.InputFormatters.Insert(0 , MyJPIF.GetJsonPatchInputFormatter()); });
public static class MyJPIF { public static NewtonsoftJsonPatchInputFormatter GetJsonPatchInputFormatter () { var builder = new ServiceCollection() .AddLogging() .AddMvc() .AddNewtonsoftJson() .Services.BuildServiceProvider(); return builder .GetRequiredService<IOptions<MvcOptions>>() .Value .InputFormatters .OfType<NewtonsoftJsonPatchInputFormatter>() .First(); } }
NewtonsoftJsonPatchInputFormatter 處理 JSON Patch 請求。
現有的 System.Text.Json 型輸入與格式器會處理所有其他 JSON 請求和回應。
範例物件
用使用者及送貨地址為例,一個User包含多個送貨地址、多個Email。
public class User { public int Id { get ; set ; } public string ? FirstName { get ; set ; } public string ? LastName { get ; set ; } public DateTime? Birthday { get ; set ; } public int Age { get ; internal set ; } public bool IsNewsletterSubscribed { get ; internal set ; } public List<string > EmailList { get ; set ; } = new List<string >(); public List<ShippingAddress> ShippingAddresses { get ; set ; } = new List<ShippingAddress>(); } public class ShippingAddress { public string ? ZipCode { get ; set ; } public string ? Address { get ; set ; } }
測試程式 [ApiController ] [Route("[controller]" ) ] public class UserController : ControllerBase { [HttpGet("{id:int}" ) ] public IActionResult GetUserById ([FromRoute] int id ) { var user = FakeUserRepository.GetUserById(id); return Ok(user); } [HttpPatch("{id:int}" ) ] public IActionResult JsonPatch ([FromRoute] int id, [FromBody] JsonPatchDocument<User> command ) { var user = FakeUserRepository.GetUserById(id); if (user == null ) return BadRequest(); command.ApplyTo(user); return Ok(user); } } public static class FakeUserRepository { static readonly List<User> users = new () { new User { Id = 1 , Email = "pingchun.hung@gmail.com" , Account = "delia" , Password = "123" , Birthday = new DateTime(1996 , 03 , 31 ), FirstName = "hung" , LastName = "pung chun" , Age = 27 , IsNewsletterSubscribed = true , EmailList = new List<string >() { "pingchun.hung@gmail.com" , "jyun87123@gmail.com" }, ShippingAddresses = new List<ShippingAddress> { new ShippingAddress { ZipCode = "001" , Address = "台北市內湖區文德路87號" }, new ShippingAddress { ZipCode = "002" , Address = "台北市板橋區四維路123號" } } } }; public static User? GetUserById(int id) { return users.FirstOrDefault(f => f.Id == id); } }
原始資料 { "id" : 1 , "account" : "delia" , "password" : "123" , "firstName" : "hung" , "lastName" : "pung chun" , "email" : "pingchun.hung@gmail.com" , "birthday" : "1996-03-31T00:00:00" , "age" : 27 , "isNewsletterSubscribed" : true , "emailList" : [ "pingchun.hung@gmail.com" , "jyun87123@gmail.com" ] , "shippingAddresses" : [ { "zipCode" : "001" , "address" : "台北市內湖區文德路87號" } , { "zipCode" : "002" , "address" : "台北市板橋區四維路123號" } ] }
新增
path 指向屬性:設定屬性值。
path 指向陣列元素:將新元素插入至 path 所指定的元素之前。
path 指定陣列位置 : /shippingAddresses/[2]
path 加到陣列結尾 : /shippingAddresses/-
如果 path 指向不存在的位置:
修改動態物件:加入屬性。
修改靜態物件:要求失敗。
Request
[ { "op" : "add" , "path" : "/firstName" , "value" : "hong" } , { "op" : "add" , "path" : "/emailList/1" , "value" : "delia85@gmail.com" } , { "op" : "add" , "path" : "/shippingAddresses/-" , "value" : { "zipCode" : "003" , "Address" : "高雄市苓雅區四維路871號" } } ]
Response
{ "id" : 1 , "firstName" : "hong" , "lastName" : "pung chun" , "birthday" : "1996-03-31T00:00:00" , "age" : 27 , "isNewsletterSubscribed" : true , "emailList" : [ "pingchun.hung@gmail.com" , "delia85@gmail.com" , "jyun87123@gmail.com" ] , "shippingAddresses" : [ { "zipCode" : "001" , "address" : "台北市內湖區文德路87號" } , { "zipCode" : "002" , "address" : "台北市板橋區四維路123號" } , { "zipCode" : "003" , "address" : "高雄市苓雅區四維路871號" } ] }
移除
Request
path 指向陣列元素:移除該元素。
path 指向屬性: 修改動態物件:移除屬性。 修改靜態物件:
屬性可為 Null:將它設定為 Null。
屬性不可為 Null,則將它設定為 default。
[ { "op" : "remove" , "path" : "/firstName" } , { "op" : "remove" , "path" : "/emailList/1" } , { "op" : "remove" , "path" : "/shippingAddresses" } ]
Response
{ "id" : 1 , "firstName" : null , "lastName" : "pung chun" , "birthday" : "1996-03-31T00:00:00" , "age" : 27 , "isNewsletterSubscribed" : true , "emailList" : [ "pingchun.hung@gmail.com" ] , "shippingAddresses" : null }
更新
此作業在功能上與 remove 之後接著 add 相同。
Request
[ { "op" : "replace" , "path" : "/FirstName" , "value" : "hong" } , { "op" : "replace" , "path" : "/emailList" , "value" : null } , { "op" : "replace" , "path" : "/shippingAddresses/1" , "value" : { "ZipCode" : "087" , "Address" : "更新地址" } } ]
Response
{ "id" : 1 , "firstName" : "hong" , "lastName" : "pung chun" , "birthday" : "1996-03-31T00:00:00" , "age" : 27 , "isNewsletterSubscribed" : true , "emailList" : null , "shippingAddresses" : [ { "zipCode" : "001" , "address" : "台北市內湖區文德路87號" } , { "zipCode" : "087" , "address" : "更新地址" } ] }
移動
path 指向陣列元素:將 from 元素複製到 path 元素的位置,然後在 from 元素上執行 remove 作業。
path 指向屬性:將 from 屬性的值複製到 path 屬性,然後在 from 屬性上執行 remove 作業。
如果 path 指向不存在的屬性:
更新靜態物件:要求失敗。
更新動態物件:將 from 屬性複製到 path 所指出的位置,然後在 from 屬性上執行 remove 作業。
Request
[ { "op" : "move" , "from" : "/shippingAddresses/0/Address" , "path" : "/LastName" } , { "op" : "move" , "from" : "/ShippingAddresses/1" , "path" : "/ShippingAddresses/0" } ]
Response
{ "id" : 1 , "firstName" : "hung" , "lastName" : "台北市內湖區文德路87號" , "birthday" : "1996-03-31T00:00:00" , "age" : 27 , "isNewsletterSubscribed" : true , "emailList" : [ "pingchun.hung@gmail.com" , "jyun87123@gmail.com" ] , "shippingAddresses" : [ { "zipCode" : "002" , "address" : "台北市板橋區四維路123號" } , { "zipCode" : "001" , "address" : null } ] }
複製
此作業在功能上與不含最後 remove 步驟的 move 作業相同。
Request
[ { "op" : "move" , "from" : "/shippingAddresses/0/Address" , "path" : "/LastName" } , { "op" : "copy" , "from" : "/shippingAddresses/1" , "path" : "/shippingAddresses/0" } ]
Response
{ "id" : 1 , "firstName" : "hung" , "lastName" : "台北市內湖區文德路87號" , "birthday" : "1996-03-31T00:00:00" , "age" : 27 , "isNewsletterSubscribed" : true , "emailList" : [ "pingchun.hung@gmail.com" , "jyun87123@gmail.com" ] , "shippingAddresses" : [ { "zipCode" : "002" , "address" : "台北市板橋區四維路123號" } , { "zipCode" : "001" , "address" : null } , { "zipCode" : "002" , "address" : "台北市板橋區四維路123號" } ] }
測試 如果 path 所指出位置上的值與 value 中所提供的值不同,則要求會失敗。 一個Test操作的請求內可以包含多個Test操作,其中任何一個Test操作驗證失敗,整個 PATCH 要求會失敗,
test 作業通常會用來防止在發生並行衝突時進行更新。
Request
[ { "op" : "test" , "path" : "/lastName" , "value" : "Nancy" } , { "op" : "replace" , "path" : "/firstName" , "value" : "Hong" } ]
Response
Microsoft.AspNetCore.JsonPatch.Exceptions.JsonPatchException: 'The current value 'pung chun' at path 'lastName' is not equal to the test value 'Nancy'.'
使用AutoMapper轉換dto builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
public class ProfileMapping : Profile { public ProfileMapping () { CreateMap<User, UpdateUserCommand>().ReverseMap(); CreateMap<ShippingAddress, UpdateUserShippingAddress>().ReverseMap(); } }
[HttpPatch("{id:int}" ) ] public IActionResult JsonPatch ([FromRoute] int id, [FromBody] JsonPatchDocument<UpdateUserCommand> command ){ var user = FakeUserRepository.GetUserById(id); if (user == null ) return BadRequest(); UpdateUserCommand dto = _mapper.Map<UpdateUserCommand>(user); command.ApplyTo(dto); _mapper.Map(dto, user); return Ok(user); }
結論 如果沒有使用json patch,如上述範例中如果要更新FirstName的值,就要開一個更新FirstName的API,如果只想開一個API參數有User內所有屬性,就要有大量if判斷,而使用JSON Patch可以解決這個問題。
if (keys.Contains("FirstName" )){ entity.FirstName = command.FirstName; }
但由於json patch操作較為複雜,也會有非冪等性問題,可以使用較簡單的json merge patch來實現Patch。
參考文件 https://learn.microsoft.com/zh-tw/aspnet/core/web-api/jsonpatch?view=aspnetcore-7.0
https://inwedo.com/blog/when-not-use-json-patch-in-asp-net-core/
https://ithelp.ithome.com.tw/m/articles/10261369
https://www.ipshop.xyz/8591.html
https://www.cnblogs.com/lwqlun/p/10433615.html