Web API 通過 Morcatko.AspNetCore.JsonMergePatch 實現更新部分資源

我曾經在端點使用 Dictionary<string,object> 型別,當調用端傳入 {"name":null} 時,移除 name key;傳入 {"name":"123"} 時,name 得的值等於 "123",這樣便能夠做到類似 Json Path 的功能,參考上篇,在不改變合約的情況之下,這次我想要改用 Morcatko.AspNetCore.JsonMergePatch 來實現更新部分資源並且讓端點的合約變成強型別。

開始之前,先簡單科普一下

PATCH HTTP request method

PUT 和 PATCH HttpMethod 不一樣的地方在於 PUT 是替換所有的資源,而 PATCH 是替換指定的屬性

甚麼是 Json Patch

JSON Patch 是一種用於指定要應用於資源的更新的格式,JSON Patch文檔有一組操作,每個操作標識一種特定類型的更改,此類更改的示例包括添加列元素或替換屬性值。

例如,以下 JSON 文檔表示資源、資源的 JSON Patch 文檔以及應用 Patch 操作的結果。

原始

{
  "name": "yao",
  "foo": "bar"
}

Patch

[
  { "op": "replace", "path": "/name", "value": "boo" },
  { "op": "add", "path": "/hello", "value": ["world"] },
  { "op": "remove", "path": "/foo"}
]

結果

{
  "name": "boo",
  "hello": ["world"]
}

一個 JSON Patch 包含了一組 Patch 操作的 JSON 文件。Patch 操作包括 "add"、"remove"、"replace"、"move"、"copy" 和 "test",這些 Patch 操作是按照順序,如果有任何一個操作失敗,整個 Patch 都會被終止。更詳細的內容可以參考 RFC 6902 - JavaScript Object Notation (JSON) Patch (ietf.org)


JSON  Pointer IETF RFC 6901 定義了如何在 JSON 文檔中定位指定值的字串符號,用來指定 JSON Patch 要操作的位置,他是使用 / 符號來區分物件的 key 或是陣列的索引,以下為例

{
  "biscuits": [
    { "name": "Digestive" },
    { "name": "Choco Leibniz" }
  ]
}

/biscuits 指向陣列 biscuits,同時 /biscuits/1/name 指向 Choco Leibniz

開發環境

實作

安裝套件

新增一個 Web API 專案並安裝以下套件

dotnet add package Morcatko.AspNetCore.JsonMergePatch.SystemText --version 6.0.0
dotnet add package Swashbuckle.AspNetCore.Filters --version 7.0.6

這裡我還有使用到 Swagger Example,不知道  Example  的可以看這篇 [ASP.NET Core 6] 通過 Swashbuckle.AspNetCore 編寫 Web API 的 Swagger 文件 | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)

 

註冊 JsonMergePatch

builder.Services.AddControllers()
   .AddJsonOptions(opts => JsonSerializeFactory.Apply(opts.JsonSerializerOptions))
   .AddSystemTextJsonMergePatch(p => p.EnableDelete = true); 

 

註冊 Swagger Example 

builder.Services.AddSwaggerGen(p => p.ExampleFilters());
builder.Services.AddSwaggerExamplesFromAssemblies(Assembly.GetEntryAssembly());

 

從某個地方取得的 Employee 資料,內容長的像這樣,這是我原本的資料結構

Employee GetEmployee()
{
	var now = DateTimeOffset.Now;
	var userId = "Sys";
	return new Employee
	{
		Id = Guid.NewGuid(),
		Address = new Address
		{
			Address1 = "台北市",
			Address2 = "大安區",
			Street = "忠孝東路"
		},
		Birthday = new DateTime(2009, 12, 25),
		CreatedAt = now,
		CreatedBy = userId,
		ModifiedAt = now,
		ModifiedBy = userId
	};
}

 

Patch 請求的定義還有 Example

public class PatchEmployeeRequest
{
    public string? Name { get; set; }

    public Address? Address { get; set; }

    public DateTime? Birthday { get; set; }

    public class PatchEmployeeRequestExample : IExamplesProvider<PatchEmployeeRequest>
    {
        public PatchEmployeeRequest GetExamples()
        {
            return new PatchEmployeeRequest
            {
                Name = "小章",
                Address = new Address
                {
                    Address1 = "台北市",
                    Address2 = "大安區",
                    Street = "忠孝東路"
                },
                Birthday = null
            };
        }
    }
}

 

端點

[HttpPatch]
[SwaggerRequestExample(typeof(PatchEmployeeRequest), typeof(PatchEmployeeRequest.PatchEmployeeRequestExample))]
public async Task<ActionResult> Patch(JsonMergePatchDocument<PatchEmployeeRequest> request)
{
	var original = this.GetEmployee();
	var patchResult = request.ApplyToT(original);
	return this.Ok(patchResult);
}

 

我要新增 /name,異動 /birthday,移除了 /address/address2

調用端傳入,需要改變的才需要列出來,null 代表要移除

{
  "name": "小章",
  "Address": {
    "Street": null
  },
  "Birthday": "2009-12-29"
}

 

結果,如我所預期

{
    "id": "74dd6bcb-d3c8-4238-adda-da68c802fd4b",
    "name": "小章",
    "address": {
        "address1": "台北市",
        "address2": "大安區"
    },
    "birthday": "2009-12-29T00:00:00",
    "createdAt": "2023-05-08T09:21:40.188945+08:00",
    "createdBy": "Sys",
    "modifiedAt": "2023-05-08T09:21:40.188945+08:00",
    "modifiedBy": "Sys"
}

 

Postman 執行結果

注意:Content-Type 要選擇 application/merge-patch+json

 

接下來中斷觀察看看,Morcatko.AspNetCore.JsonMergePatch 可以得到甚麼內容

 

Json Patch Operation 長這樣,

[
  {
    "value": "小章",
    "OperationType": 2,
    "path": "/name",
    "op": "Replace"
  },
  {
    "value": {},
    "OperationType": 0,
    "path": "/Address",
    "op": "Add"
  },
  {
    "OperationType": 1,
    "path": "/Address/Street",
    "op": "Remove"
  },
  {
    "value": "2009-12-29",
    "OperationType": 2,
    "path": "/Birthday",
    "op": "Replace"
  }
]

心得

上篇  [.NET 6] 自訂 JsonConverter 反序列化 Dictionary<string, object> | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw) ,我使用反序列化Dictionary<string,obect> 得到哪一個屬性被設定成 null (代表 remove),透過今天分享這個技巧,就可以不需要自己實現反序列化Dictionary<string,obect>,對外的合約也變成是強型別,端點的操作也非常的簡單,不需要真的使用 Json Patch 的 Operations,就能完成部分資源更新。

經實驗,Morcatko.AspNetCore.JsonMergePatch 不支援集合

參考

範例位置

sample.dotblog/Json/Lab.JsonMergePatch at 257ff3b91651b01c9c33778b642c10b3318589a6 · yaochangyu/sample.dotblog · GitHub

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo