

使用 Json.NET 定義一個混合強型別與弱型別的 JSON 資料回應
source link: https://blog.miniasp.com/post/2021/06/24/Using-JsonExtensionData-with-JSONNET
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

我們最近有個專案需求特別複雜,由於是個已經持續維護 10 年的系統要改版,很多商業邏輯已經不可考,只能從程式碼中找尋蛛絲馬跡。不過,有些資料的欄位來自於程式碼,但更多來自於一組極其複雜的動態資料表設計。我們除了從現有的頁面上進行新系統設計外,有時候還會意外的多出幾個莫名的欄位,因此對於資料模型類別的規劃變的異常困難。本篇文章我將分享一種罕見的 Json.NET 資料序列化技巧,幫助你可以做到動態的 JSON 資料回應格式,同時又能保有強型別的設計。
建立 Console 應用程式
我打算用一個簡單的例子來說明這個過程,但事實上你在撰寫 Web API 的時候,只要是使用 Json.NET (Newtonsoft.Json) 來進行序列化/反序列化,其過程都是完全相同的!
-
我們先建立一個 .NET 5 的 Console 應用程式專案
mkdir c1 && cd c1 dotnet new globaljson --sdk-version 5.0.301 dotnet new console dotnet add package Newtonsoft.Json
-
在專案中建立一個
sample.json
檔案,其內容如下[ { "_id": 959, "date": "2021/1/1", "name": "中華民國開國紀念日", "description": "全國各機關學校放假一日", "holidayCategory": "放假之紀念日及節日", "isHoliday": "是" }, { "_id": 960, "date": "2021/1/2", "holidayCategory": "星期六、星期日", "isHoliday": "是" }, { "_id": 961, "date": "2021/1/3", "holidayCategory": "星期六、星期日", "isHoliday": "是" } ]
你可以從上述資料看出,其實 JSON 資料中的
name
與description
並不是每一筆資料都有! -
建立一個名為
Holiday
的資料模型類別using Newtonsoft.Json; namespace c1 { public partial class Holiday { [JsonProperty("_id")] public long Id { get; set; } [JsonProperty("date")] public string Date { get; set; } [JsonProperty("name")] public string Name { get; set; } [JsonProperty("description")] public string Description { get; set; } [JsonProperty("holidayCategory")] public string HolidayCategory { get; set; } [JsonProperty("isHoliday")] public string IsHoliday { get; set; } } }
-
建立主程式
using System; using System.IO; using Newtonsoft.Json; namespace c1 { class Program { static void Main(string[] args) { var data = JsonConvert.DeserializeObject<Holiday[]>(File.ReadAllText("./sample.json")); foreach (var item in data) { Console.WriteLine(item.Id + "\t" + item.Date + "\t" + item.IsHoliday); Console.WriteLine("假期名稱: " + item.Name); Console.WriteLine("假期說明: " + item.Description); } } } }
-
輸出結果如下
959 2021/1/1 是 假期名稱: 中華民國開國紀念日 假期說明: 全國各機關學校放假一日 960 2021/1/2 是 假期名稱: 假期說明: 961 2021/1/3 是 假期名稱: 假期說明:
重新調整資料模型類別
我們想把這兩個欄位定義成「非必要」的欄位,一般的時候不需要回應給用戶端知道,當前端需要的時候才需要序列化給用戶端。
事實上我們專案中的欄位有數十到數百個動態欄位,設計之初可以從需求訪談得知一些「必要」的欄位,並設計到強型別的模型類別中,但其他的擴充欄位,我們打算當成「額外的」附加資料,有資料的時候就回傳,沒資料的時候就完全看不到。
這時我打算調整一下我們的模型類別,將 Name
與 Description
欄位給刪除,並加入另一個特殊的 AdditionalData
屬性。
-
加入
AdditionalData
屬性並標示[JsonExtensionData]
屬性(Attribute)using System.Collections.Generic; using Newtonsoft.Json; namespace c1 { public partial class Holiday { [JsonProperty("_id")] public long Id { get; set; } [JsonProperty("date")] public string Date { get; set; } // [JsonProperty("name")] // public string Name { get; set; } // [JsonProperty("description")] // public string Description { get; set; } [JsonProperty("holidayCategory")] public string HolidayCategory { get; set; } [JsonProperty("isHoliday")] public string IsHoliday { get; set; } [JsonExtensionData] public IDictionary<string, object> AdditionalData { get; set; } = new Dictionary<string, object>(); } }
這裡的
AdditionalData
屬性要標示[JsonExtensionData]
屬性(Attribute),就必須要宣告型別為IDictionary<string, object>
或IDictionary<string, JToken>
才行! -
然後將主程式修改成如下
using System; using System.IO; using Newtonsoft.Json; namespace c1 { class Program { static void Main(string[] args) { var data = JsonConvert.DeserializeObject<Holiday[]>(File.ReadAllText("./sample.json")); foreach (var item in data) { Console.WriteLine(item.Id + "\t" + item.Date + "\t" + item.IsHoliday); if (item.AdditionalData.Keys.Contains("name")) { Console.WriteLine("假期名稱: " + item.AdditionalData["name"]); } if (item.AdditionalData.Keys.Contains("description")) { Console.WriteLine("假期說明: " + item.AdditionalData["description"]); } } } } }
簡單來說,這些所謂的「擴充欄位」全部都會自動放在
AdditionalData
這個IDictionary<string, object>
字典型別的屬性下! -
輸出結果如下
959 2021/1/1 是 假期名稱: 中華民國開國紀念日 假期說明: 全國各機關學校放假一日 960 2021/1/2 是 961 2021/1/3 是
注意:如果試圖存取一個不存在的 Key 將會導致
Unhandled exception. System.Collections.Generic.KeyNotFoundException: The given key 'description1' was not present in the dictionary.
例外發生!
序列化強型別物件到可彈性擴充屬性的 JSON 輸出
假設我們想將 Holiday
物件外加一些額外的屬性到序列化後的 JSON 資料中,就可以參考以下寫法:
-
主程式修改如下
using System; using Newtonsoft.Json; namespace c1 { class Program { static void Main(string[] args) { var data = new Holiday() { Id = 959, Date = "2021/1/1", IsHoliday = "是" }; Console.WriteLine(JsonConvert.SerializeObject(data)); data.AdditionalData.Add("name", "中華民國開國紀念日"); data.AdditionalData.Add("description", "全國各機關學校放假一日"); Console.WriteLine(JsonConvert.SerializeObject(data)); } } }
-
輸出結果如下
{"_id":959,"date":"2021/1/1","holidayCategory":null,"isHoliday":"是"} {"_id":959,"date":"2021/1/1","holidayCategory":null,"isHoliday":"是","name":"中華民國開國紀念日","description":"全國各機關學校放假一日"}
合併多個欄位到另一個強型別的屬性中
本篇文章講解的「擴充屬性」技巧,也可以套用在 反序列化 (Deserialize) 的過程中,將兩個或多個屬性合併成一個或多個屬性,相當實用!
如下程式碼範例,你只要先定義出以下 DTO 模型物件,就可以做到更多反序列化過程的客製化行為:
- 定義一個
private void OnDeserialized(StreamingContext context)
方法,並套用[OnDeserialized]
屬性(Attribute) - 你可以從私有的
_additionalData
屬性取出 JSON 的鍵值,然後拆解到其他的強型別屬性中
public class DirectoryAccount
{
// normal deserialization
public string DisplayName { get; set; }
// these properties are set in OnDeserialized
public string UserName { get; set; }
public string Domain { get; set; }
[JsonExtensionData]
private IDictionary<string, JToken> _additionalData;
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
// SAMAccountName is not deserialized to any property
// and so it is added to the extension data dictionary
string samAccountName = (string)_additionalData["SAMAccountName"];
Domain = samAccountName.Split('\\')[0];
UserName = samAccountName.Split('\\')[1];
}
public DirectoryAccount()
{
_additionalData = new Dictionary<string, JToken>();
}
}
當然,如果你的擴充屬性是
type1
,type2
,type3
, ... 這類的資料,更可以利用這個技巧,將這些資料轉成強型別的陣列型別,讓你更有效率的操作資料!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK