最近處理一個快取文案與顯示前台文案不一致的 Bug,尻了很久,找不到原因,最後才發現是因為某個 Class Property 被掛上了JsonIgnore,導致即使這個 Property 已經拿到了打 API 翻譯後的新文案,在將資料存進 RedisCache 的過程中,這個 property 的資料沒有送出去,而沒有正確的快取,因此重刷後又會拿到舊的文案(抓到舊 Cache)

當初其實有看到這個 Attribute 但沒有在意,而這就是所謂的 你不理他,他也不理你的境界

搜了一下專案發現,使用率其實挺高
Image

Jason Feels Ignored
Image

JsonIgnore

基本上作用用於在序列化和反序列化期間忽略特定的屬性,讓該屬性資料不要傳遞出去,.NET 中,我們熟知的System.Text.Json 以及 Json.NET都有支援 JsonIgnore Attribute,
作用上基本也相同。

舉個栗

假設有一間公司他的工作簡歷需要填上你是否是光頭的資訊,因為實在太不好意思,所以我只好偷偷加上 JsonIgnore 沙小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class JobApplicant
{
public string Name { get; set; }
public int Age { get; set; }
public string Email { get; set; }
public List<string> Skills { get; set; }
public List<WorkExperience> WorkHistory { get; set; }

[JsonIgnore]
public bool IsBald { get; set; }
}

public class WorkExperience
{
public string Company { get; set; }
public string Position { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public List<string> Responsibilities { get; set; }
}

今天有人對我發出 API Request 時,送出去呈現出來的 json 資料將會忽略它!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"Name": "Allen Lin",
"Age": 25,
"Email": "allenhandsome@gmail.com",
"Skills": [
"C#",
"ASP.NET",
"SQL"
],
"WorkHistory": [
{
"Company": "GG Inc.",
"Position": "Cold Working Machine",
"StartDate": "2020-01-01T00:00:00",
"EndDate": "2023-1-31T00:00:00",
"Responsibilities": [
"Mouth Fighter",
"Toilet Sleeper"
]
}
]
}

Ignore時機

來一點應用吧 甚麼光頭…

  1. 複雜的庫存管理資訊
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Product
{
public string Name { get; set; }

[JsonIgnore]
public string WarehouseLocation { get; set; }

[JsonIgnore]
public int ReorderPoint { get; set; }

public int StockQuantity { get; set; }
// 其他屬性...
}
  1. 計算屬性或冗餘資料
1
2
3
4
5
6
7
8
9
10
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
public decimal TaxRate { get; set; }

[JsonIgnore]
public decimal TaxAmount => Price * TaxRate;
// 其他屬性...
}

就是有先資料我們需要內部紀錄使用,但凡外人做 Request 有不會看到這些秘密

動態的決定 Ignore 屬性

有一點雞毛

某些時刻,我們因為不同的需求端,想忽略的屬性不同,寫死哪些屬性通通不給人看彈性有點低,我們是否能夠動態的決定哪些屬性要Ignore 呢?

參考 : https://learn.microsoft.com/zh-tw/dotnet/standard/serialization/system-text-json/migrate-from-newtonsoft?pivots=dotnet-9-0

Json.Net : DefaultContractResolver
System.Text.Json : DefaultJsonTypeInfoResolver

我們以 System.Text.Json 來做演練

我們有一個員工資料庫,一般員工只能查詢一些不敏感的資訊

先定義 人、員工

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

public class Person
{
public string Name { get; set; }

public int Age { get; set; }

public Person(string name, int age)
{
this.Name = name;
this.Age = age;
}
}

public class Employee : Person
{
public string Department { get; set; }

public decimal Salary { get; set; }

public Employee(string name,int age,string department,decimal salary) : base(name,age)
{
this.Department = department;
this.Salary = salary;
}
}

再來是我們的客製化 JsonResolver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

//// 自訂義序列化Resolver
public class MyJsonTypeInfoResolver : DefaultJsonTypeInfoResolver
{
private readonly HashSet<string> _ignoreProps;

public MyJsonTypeInfoResolver(IEnumerable<string> ignoreProps)
{
this._ignoreProps = new HashSet<string>(ignoreProps);
}

public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
{
JsonTypeInfo typeInfo = base.GetTypeInfo(type,options);

foreach(JsonPropertyInfo propertyInfo in typeInfo.Properties)
{
if(_ignoreProps.Contains(propertyInfo.Name))
{
//// 不序列化的部分
propertyInfo.ShouldSerialize = (_,_) => false;
}
}

return typeInfo;
}
}

開用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void Main()
{
var people = new List<Person>
{
new Person("Mr.1", 28),
new Employee("Mr.2", 35, "Engineering", 22000)
};

//// 忽略年齡、薪水
var ignoredProperties = new [] {"Age","Salary"};

var options = new JsonSerializerOptions
{
WriteIndented = true,
TypeInfoResolver = new MyJsonTypeInfoResolver(ignoredProperties)
};

string jsonResult = System.Text.Json.JsonSerializer.Serialize(people,options);

jsonResult.Dump();


/*
[
{
"Name": "Mr.1"
},
{
"Name": "Mr.2"
}
]
*/
}

藉由自訂義 Resolver,我們可以藉由傳入想要忽略的屬性,動態的移除要回傳的東西

實例探討

讓我們來聊聊這個 JsonIgnore 的小插曲

參考 : https://www.facebook.com/groups/DotNetUserGroupTaiwan/permalink/3545603032399456/?rdid=pgxeNGvkHSNjvUwc&share_url=https%3A%2F%2Fwww.facebook.com%2Fshare%2Fp%2F1JUiKaLxN6%2F

在實戰中,遇到這種情境絕對不是第一次。每個人都想要模型既優雅又功能強大,但在現實中,優雅和功能總是會互相搶位子。

問題是這樣

框架:ASP.NET Core 8 Web API
問題:[JsonIgnore] 屬性到底會不會影響模型繫結?

需求很簡單:一個 Model Class 裡有個 Password 欄位,希望序列化的時候別暴露(拜託,密碼流出可是大忌),但又需要能夠在 Model Binding 時正常吃到這個欄位的值。
結果:加了 [JsonIgnore],序列化的確藏起來了,但 Model Binding 也跟著罷工,欄位直接 GG。

現狀補充:沒有加 [BindNever],但只要拿掉 [JsonIgnore],Model Binding 就乖乖回來了。只是這樣一來,序列化的時候密碼還是乖乖露餡了。

解法討論

  1. 解法(一):分治之道,VM 與 DTO
    直接分開兩套模型,序列化用 ViewModel,繫結用 DTO。
    這是個保險牌,兩個模型分工明確,誰也不干擾誰。但對於那些追求乾淨模型結構的完美主義者來說,可能會覺得多此一舉,畢竟只是處理一個欄位,怎麼搞得像分家產一樣麻煩。

  2. 解法(二):投靠 Newton.Json 派系
    方案亮點:換用 Newtonsoft.Json 做序列化,把 [JsonIgnore] 換成它家專屬的屬性。
    在 ASP.NET Core 中,System.Text.Json 和 Newtonsoft.Json 完全不會互相打架。於是你就可以一邊使用 Newtonsoft.Json 的屬性來處理序列化,一邊讓 ASP.NET MVC Core 的模型繫結還是照舊,兩邊井水不犯河水。

想法

如果能忍,選方案一:一勞永逸,模型分層後面維護也比較方便。但可能會有點心煩。
如果偷懶是美德,選方案二:Newtonsoft.Json 就像老司機,雖然已經不是官方首推的框架,但有時候老工具就是香,解決起問題來輕鬆又穩定。

精神能量分析

精神能量 : 🪴

今天領養了一個孩只回家,要如何幫他取名字我想了一天,決定就叫做”正妹”,呵呵,請多指教阿正妹
Image