Delia's Blog

菜鳥工程師小鈞的筆記

0%

如何在ASP.NET Core中使用Json Patch更新部分資料

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();//加入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 指向不存在的位置:
    1. 修改動態物件:加入屬性。
    2. 修改靜態物件:要求失敗。

Request

[
//更新值
{
"op":"add",
"path":"/firstName",
"value":"hong"
},
//加到陣列[1]位置
{
"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 指向屬性:
    修改動態物件:移除屬性。
    修改靜態物件:
    1. 屬性可為 Null:將它設定為 Null。
    2. 屬性不可為 Null,則將它設定為 default
[
//更新為null
{
"op":"remove",
"path":"/firstName"
},
//移除陣列[1]元素
{
"op":"remove",
"path":"/emailList/1"
},
//整個陣列更新為null
{
"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 指向不存在的屬性:
    1. 更新靜態物件:要求失敗。
    2. 更新動態物件:將 from 屬性複製到 path 所指出的位置,然後在 from 屬性上執行 remove 作業。

Request

[
//將shippingAddresses[0]的Address賦值給LastName,並將Address改為null
{
"op": "move",
"from": "/shippingAddresses/0/Address",
"path": "/LastName"
},
//將將shippingAddresses[1]與將shippingAddresses[0]交換
{
"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

[
//將shippingAddresses[0]的Address賦值給LastName
{
"op": "move",
"from": "/shippingAddresses/0/Address",
"path": "/LastName"
},
//在 shippingAddresses[0] 前面插入 shippingAddresses[1] 的複本。
{
"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

//in Program.cs
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);

//saveChange..

return Ok(user);
}

結論

如果沒有使用json patch,如上述範例中如果要更新FirstName的值,就要開一個更新FirstName的API,如果只想開一個API參數有User內所有屬性,就要有大量if判斷,而使用JSON Patch可以解決這個問題。

if (keys.Contains("FirstName"))
{
// 更新 NaFirstNameme
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