原文: JSON Patch With ASP.NET Core
作者:.NET Core Tutorials
譯文:如何在ASP.NET Core中使用JSON Patch
地址:https://www.cnblogs.com/lwqlun/p/10433615.html
譯者:Lamond Lu
JSON Patch是一種使用API顯式更新文檔的方法。它本身是一種契約,用於描述如何修改文檔(例如:將字段的值替換成另外一個值),而不必同時發送其他未更改的屬性值。
一個JSON Patch請求是什么樣的?
你可以在以下鏈接(http://jsonpatch.com/)中找到JSON Patch的官方文檔,但是這里我們將進一步研究一下如何在ASP.NET Core中實現JSON Patch。
為了演示JSON Patch, 我創建了以下C#對象類, 后續我將使用JSON Patch請求來操作這個對象類的實例。
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public List<string> Friends { get; set; }
}
所有的JSON Patch請求都是遵循一個相似的結構。它有一個固定的“操作”列表。每個操作本身擁有3個屬性:
- "op" - 定義了你要執行何種操作,例如add, replace, test等。
- "path" - 定義了你要操作對象屬性路徑。用前面的
Person
類為例,如果你希望修改FirstName
屬性,那么你使用的操作路徑應該是"/FirstName"。 - "value" - 在大部分情況下,這個屬性表示你希望在操作中使用的值。
現在讓我們來看一下每一個的操作如何使用。
Add
Add操作通常意味着你要向對象中添加屬性,或者向數組中添加項目。對於前者,在C#中是沒有用的,因為C#是強類型語言,所以不能將屬性添加到編譯時尚未定義的對象上。
所以這里如果想往數組中添加項目,PATCH請求的內容應該如下所示。
{ "op": "add", "path": "/Friends/1", "value": "Mike" }
這將在Friends數組的索引1處插入一個"Mike"值。
或者你還可以使用"-"在數組尾部插入記錄。
{ "op": "add", "path": "/Friends/-", "value": "Mike" }
Remove
與Add操作類似,刪除操作意味着你希望刪除對象中屬性,或者從數據中刪除某一項。但是因為在C#中你無法移除屬性,實際操作時,它會將屬性的值變更為default(T)。在某些情況下,如果屬性是可空的,則會設置屬性值為NULL。但是需要小心,因為當在值類型上使用時,例如int, 則該值實際上會重置為"0"。
如果要在對象上刪除某一屬性以達到重置的效果,你可以使用一下命令。
{ "op": "remove", "path": "/FirstName"}
當然你也可以使用刪除命令刪除數組中的某一項。
{ "op": "remove", "path": "/Friends/1" }
這將刪除數組索引為1的項目。但是有時候使用索引從數組中刪除數據是非常危險的,因為這里沒有一個"where"條件來控制刪除, 有可能在刪除的時候,數據庫中對應數組已經發生變化了。實際上有一個JSON Patch操作可以幫助解決這個問題,后面我會描述它。
Replace
Replace操作和它的字面意思完全一樣,可以使用它來替換已有值。針對簡單屬性,你可以使用如下的命令。
{ "op": "replace", "path": "/FirstName", "value": "Jim" }
你同樣可以使用它來替換數組中的對象。
{ "op": "replace", "path": "/Friends/1", "value": "Bob" }
你甚至可以用它來替換整個數組。
{ "op": "replace", "path": "/Friends", "value": ["Bob", "Bill"] }
Copy
Copy操作可以將值從一個路徑復制到另一個路徑。這個值可以是屬性,對象,或者數據。在下面的例子中,我們將FirstName屬性的值復制到了LastName屬性上。這個命令的使用場景不是很多。
{ "op": "copy", "from": "/FirstName", "path" : "/LastName" }
Move
Move操作非常類似於Copy操作,但是正如它的字面意思,"from"字段的值將被移除。如果你看一下ASP.NET Core的JSON Patch的底層代碼,你會發現,它實際上它會在"from"路徑上執行Remove操作,在"path"路徑上執行Add操作。
{ "op": "move", "from": "/FirstName", "path" : "/LastName" }
Test
在當前的ASP.NET Core公開發行版中沒有Test操作,但是如果你在Github上查看源代碼,你會發現微軟已經處理了Test操作。Test操作是一種樂觀鎖定的方法,或者更簡單的說,它會檢測數據對象從服務器讀取之后,是否發生了更改。
我們以如下操作為例。
[
{ "op": "test", "path": "/FirstName", "value": "Bob" }
{ "op": "replace", "path": "/FirstName", "value": "Jim" }
]
這個操作首先會檢查"/FirstName"路徑的值是否"Bob", 如果是,就將它改為"Jim"。 如果不是,則什么事情都不會發生。這里你需要注意,在一個Test操作的請求體內可以包含多個Test操作,但是如果其中任何一個Test操作驗證失敗,所以的變更操作都不會被執行。
為什么要使用JSON Patch?
JSON Patch的一大優勢在於它的請求操作體很小,只發送對象的更改內容。 但是在ASP.NET Core中使用JSON Patch還有另一個很大的好處,就是C#是一種強類型語言,無法區分是要將模型的值設置為NULL,還是忽略該屬性, 而使用JSON Patch可以解決這個問題。
這里如果沒有好的例子,很難解釋。 所以想象一下我從API請求一個“Person”對象。 在C#中,模型可能如下所示:
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
當從API返回Json對象時,它看起來可能像這樣。
{
"firstName" : "James",
"lastName" : "Smith"
}
現在在前端,如果不使用JSON Patch, 如果我只想更新FirstName, 我可能在請求中附帶一下請求體。
{
"firstName" : "Jim"
}
現在當我在C#中反序列化這個模型時,問題就出現了。不要看下面的代碼,想一下此時我們的模型中的屬性值是什么?
public class Person
{
public string FirstName { get; set; } //Jim
public string LastName { get; set; } //<Null>
}
因為我們發送LastName屬性的值,所以它被反序列化為Null。 但這很簡單,我們可以忽略NULL的值,只更新我們實際傳遞的字段。 但這不一定是正確的,如果該字段實際上可以為空呢? 如果我們發送了以下請求體怎么辦?
{
"firstName" : "Jim",
"lastName" : null
}
所以現在我們實際上已經指定我們想要取消該字段。但是因為C#是強類型的,所以我們無法在服務器端進行模型綁定的時候,我們無法確定它是否要將該字段的值設置為NULL。
這似乎是一個奇怪的場景,因為前端可以始終發送完整的數據模型,永遠不會省略字段。並且在大多數情況下,前端Web庫的模型將始終與API的模型匹配。但有一種情況並非如此,那就是移動應用程序。通常向蘋果應用商店提交手機應用,可能需要數周時間才能獲得批准。在這個時候,你可能還需要在Web或Android應用程序中使用新模型。在不同平台之間實現同步非常困難,而且通常是不可能。雖然API版本確實對解決這個問題有很長的路要走,但我仍然認為JSON Patch在解決這個問題方面具有很大的實用性。
最后,讓我們使用JSON Patch!我們可以使用以下JSON Patch請求更新Person對象
[
{
"op": "replace",
"path": "/FirstName",
"value": "Jim"
}
]
這明確表示我們想要更改名字而不是其他內容。 它准確的告訴我們到底將要發生什么。
在ASP.NET Core項目中啟用JSON Patch
在Visual Studio中,我們可以在Package Manage Console中安裝官方的Json Patch庫(默認創建的ASP.NET Core項目中沒有該庫)。
Install-Package Microsoft.AspNetCore.JsonPatch
為了演示,我將添加如下的一個控制器類。這里需要注意我們使用的HTTP verb是HttpPatch, 請求參數的類型是JsonPatchDocument
。 為了更新對象,我們只需要簡單調用ApplyTo
方法,並傳入了需要更新的對象。
[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中,我們需要從數據庫中拉取數據對象,更新對象,並重新保存到數據庫。
當我們使用如下請求體發送JSON Patch請求時:
[
{"op" : "replace", "path" : "/FirstName", "value" : "Bob"}
]
我們可以得到如下響應內容:
{
"firstName": "Bob",
"lastName": "Smith"
}
真棒! 我們的名字改為Bob! 使用JSON Patch啟動和運行真的很簡單。
使用Automapper處理JSON Patch請求
針對JSON Patch的使用,最大的問題是,你經常需要從API返回View Model或者DTO, 並生成PATCH請求。但是如果將這些修改請求應用於數據庫對象上?大部分情況下,開發人員都掙扎在與此。這里我們可以使用Automapper來幫助完成這個轉換的工作。
例如如下代碼:
[HttpPatch("update/{id}")]
public Person Patch(int id, [FromBody]JsonPatchDocument<PersonDTO> personPatch)
{
//獲取原始Person對象實例
PersonDatabase personDatabase = _personRepository.GetById(id);
//將Person對象實例轉換為PersonDTO對象實例
PersonDTO personDTO = _mapper.Map<PersonDTO>(personDatabase);
//應用Patch修改
personPatch.ApplyTo(personDTO);
//將更新后的PersonDTO對象,重新映射到Person對象實例中
_mapper.Map(personDTO, personDatabase);
//將更新后的Person對象實例保存到數據庫
_personRepository.Update(personDatabase);
return personDTO;
}