平時使用 API 更新資料時,通常會使用 PUT 這個 HTTP Method ,但是 PUT 通常代表的意思是更新整個資源。假設 product 有 名稱、價格... 等等 10 個屬性,在更新時需要把全部屬性都回傳一遍,如果沒有回傳很可能被當成 null 更新上去,如果有多個階層的資料會非常麻煩,並且占用的流量和資源也比較多。
而使用 PATCH 這個 HTTP Method 就可以避免這個問題,但是 PATCH 的更新就比較繁瑣一點,因為會需要指定要更新資源的哪個部分。接下來示範的是 JSON Patch 這種格式做示範。
假設 id 為 1 的 product 查詢結果如下:
常見的使用 PUT 更新的方式就是在 body 中帶入所有屬性(通常不會回傳 id,因為 id 無法更新):
而使用 JSON Patch 要更新這兩部分就需要在 Body 中指定該資源在 JSON 格式中的位置(path)和作業的類型(op):
在上面這個簡單的例子中使用使用 JSON Patch 看起來就是多此一舉,不過對於屬性越多的資源來說越方便,並且也可以減輕某種程度的更新衝突。
假設現在有「可樂」這個產品:
小明和小王同時開啟網頁,同時取得「可樂」這個產品的資訊,一個要改品名為「新版可樂」,另一個要把價格從 33 變成 40 ,猶豫半天後兩個人在網頁上儲存更新資訊,在網頁中使用 PUT 來送出更新請求,範例內容如下:
小明只要改名稱:
小王只要改價格:
如果小明先發送更新,小王比較後面才更新,但是因為是使用 PUT,所以會把小明已經更新過的名稱蓋掉,名稱還是維持當初小王取得的產品名稱:「可樂」,這時候如果使用 PATCH 來更新,只更新部分資訊,就可以減少這個問題。
當然依照不同的系統有不同的處理方式,例如使用最後更新時間或是雜湊來檢查,有衝突就強制重新整理不儲存資料等,不過本篇的重點是在 ASP.NET Core 中實作 JSON PATCH API,所以這裡就不展開討論了。
在 ASP.NET Core 中要實作 JsonPatch 的更新其實也滿簡單的,只是如果是使用原生的 System.Text.Json 來處理的話需要多一些設定的步驟,並且在微軟官方的教學中還是仰賴第三方套件 Newtonsoft.Json 來達成,所以就算使用官方方式還是需要安裝套件。
筆者在好幾個專案中都需要建立 JSON PATCH 這種格式的 API,但是已經不想要再每次都翻一次文件,不想要繼續每一次都複製貼上,出於懶惰(?),於是就寫了一個套件 JsonPatchSupport.AspNetCore ,加上 using 只要兩行程式碼就可以完成設定,實在是居家旅行、殺人滅口,必備良藥。
在 Program.cs 檔案中增加以下程式碼註冊服務:
安裝完畢。
如果之後出現類似下面的錯誤有一半的機率是忘記放上面的程式碼:
這裡就實作使用 JSON PATCH 更新 Product 的 API:
重點在於傳入的參數需要使用 JsonPatchDocument 包起來,然後取得原始的資料,原始資料的格式就是被 JsonPatchDocument 包起來的那個,兩個格式需要一樣。然後將原始資料使用 ApplyTo 來將更新的部分覆蓋上,如果使用 EF Core 則直接儲存就好了(因為會自動比對差異)。
附上使用 PATCH 更新的 cURL (請自行替換 port):
這裡也有重點,Content-Type 需要指定為 application/json-patch+json,如果出現下面的錯誤訊息,有 90% 的機率就是放錯 Content-Type ,因為內容是 JSON 預設都會被帶入 application/json ,只要將 Header 的 Content-Type 指定為 application/json-patch+json 即可,非常重要!
如果有照步驟做那可是非常簡單的,如果還是不會或是遇到問題,直接文章下面留言
參考資料:
GitHub - JsonPatchSupport.AspNetCore
NuGet.org - JsonPatchSupport.AspNetCore
Microsoft.Learn - JsonPatch in ASP.NET Core web API
mdn - PATCH
RFC 6902 - JavaScript Object Notation (JSON) Patch
而使用 PATCH 這個 HTTP Method 就可以避免這個問題,但是 PATCH 的更新就比較繁瑣一點,因為會需要指定要更新資源的哪個部分。接下來示範的是 JSON Patch 這種格式做示範。
假設 id 為 1 的 product 查詢結果如下:
{
"id": 1,
"name": "ProductName",
"price": 10
}
常見的使用 PUT 更新的方式就是在 body 中帶入所有屬性(通常不會回傳 id,因為 id 無法更新):
{
"name": "NewProductName",
"price": 100
}
而使用 JSON Patch 要更新這兩部分就需要在 Body 中指定該資源在 JSON 格式中的位置(path)和作業的類型(op):
[
{
"op": "replace",
"path": "/Name",
"value": "NewProductName"
},
{
"op": "replace",
"path": "/Price",
"value": "100"
}
]
在上面這個簡單的例子中使用使用 JSON Patch 看起來就是多此一舉,不過對於屬性越多的資源來說越方便,並且也可以減輕某種程度的更新衝突。
假設現在有「可樂」這個產品:
{
"name": "可樂",
"price": 33
}
小明和小王同時開啟網頁,同時取得「可樂」這個產品的資訊,一個要改品名為「新版可樂」,另一個要把價格從 33 變成 40 ,猶豫半天後兩個人在網頁上儲存更新資訊,在網頁中使用 PUT 來送出更新請求,範例內容如下:
小明只要改名稱:
{
"name": "新版可樂",
"price": 33
}
小王只要改價格:
{
"name": "可樂",
"price": 40
}
如果小明先發送更新,小王比較後面才更新,但是因為是使用 PUT,所以會把小明已經更新過的名稱蓋掉,名稱還是維持當初小王取得的產品名稱:「可樂」,這時候如果使用 PATCH 來更新,只更新部分資訊,就可以減少這個問題。
當然依照不同的系統有不同的處理方式,例如使用最後更新時間或是雜湊來檢查,有衝突就強制重新整理不儲存資料等,不過本篇的重點是在 ASP.NET Core 中實作 JSON PATCH API,所以這裡就不展開討論了。
在 ASP.NET Core 中要實作 JsonPatch 的更新其實也滿簡單的,只是如果是使用原生的 System.Text.Json 來處理的話需要多一些設定的步驟,並且在微軟官方的教學中還是仰賴第三方套件 Newtonsoft.Json 來達成,所以就算使用官方方式還是需要安裝套件。
筆者在好幾個專案中都需要建立 JSON PATCH 這種格式的 API,但是已經不想要再每次都翻一次文件,不想要繼續每一次都複製貼上,出於懶惰(?),於是就寫了一個套件 JsonPatchSupport.AspNetCore ,加上 using 只要兩行程式碼就可以完成設定,
安裝套件
先使用 NuGet 安裝 JsonPatchSupport.AspNetCore 套件,或是使用 .NET CLI 執行以下指令安裝
dotnet add package JsonPatchSupport.AspNetCore
在 Program.cs 檔案中增加以下程式碼註冊服務:
using JsonPatchSupport.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddJsonPatchSupport();
var app = builder.Build();
app.Run();
安裝完畢。
如果之後出現類似下面的錯誤有一半的機率是忘記放上面的程式碼:
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"$": [
"The JSON value could not be converted to Microsoft.AspNetCore.JsonPatch.JsonPatchDocument`1[Api.Dtos.ProductDto]. Path: $ | LineNumber: 0 | BytePositionInLine: 1."
],
"patchDocument": [
"The patchDocument field is required."
]
},
"traceId": ""
}
使用示範
假設我們有一個 API 用來更新 Product ,DTO 如下:
public class ProductDto
{
public string Name { get; set; }
public int Price { get; set; }
}
這裡就實作使用 JSON PATCH 更新 Product 的 API:
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{
[HttpPatch("{id}")]
public IActionResult Patch([FromRoute] int id, [FromBody] JsonPatchDocument<ProductDto> patchDocument)
{
// simulate getting the entity from a data source
var product = new ProductDto
{
Name = "product1",
Price = 10,
};
// Apply the patch to the product
patchDocument.ApplyTo(product, ModelState);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// Simulate saving the updated entity to a data source
Console.WriteLine($"Product.Name={product.Name}, Product.Price={product.Price}");
return Ok(product);
}
}
重點在於傳入的參數需要使用 JsonPatchDocument 包起來,然後取得原始的資料,原始資料的格式就是被 JsonPatchDocument 包起來的那個,兩個格式需要一樣。然後將原始資料使用 ApplyTo 來將更新的部分覆蓋上,如果使用 EF Core 則直接儲存就好了(因為會自動比對差異)。
附上使用 PATCH 更新的 cURL (請自行替換 port):
curl -X 'PATCH' \
'http://localhost:5183/api/Product/1' \
-H 'accept: */*' \
-H 'Content-Type: application/json-patch+json' \
-d '
[
{
"op": "replace",
"path": "/Name",
"value": "NewProductName"
},
{
"op": "replace",
"path": "/Price",
"value": "99"
}
]'
這裡也有重點,Content-Type 需要指定為 application/json-patch+json,如果出現下面的錯誤訊息,有 90% 的機率就是放錯 Content-Type ,因為內容是 JSON 預設都會被帶入 application/json ,只要將 Header 的 Content-Type 指定為 application/json-patch+json 即可,非常重要!
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.16",
"title": "Unsupported Media Type",
"status": 415,
"traceId": "00-a26073a738244ac49692bb56bf6d8a76-e6805d2b04a05b39-00"
}
如果有照步驟做那可是非常簡單的,如果還是不會或是遇到問題,直接文章下面留言
參考資料:
GitHub - JsonPatchSupport.AspNetCore
NuGet.org - JsonPatchSupport.AspNetCore
Microsoft.Learn - JsonPatch in ASP.NET Core web API
mdn - PATCH
RFC 6902 - JavaScript Object Notation (JSON) Patch
留言
張貼留言
如果有任何問題、建議、想說的話或文章題目推薦,都歡迎留言或來信: a@ruyut.com