原始碼產生(Source Generation)是 .NET 5 加入的新功能,能在編譯過程對 C# 原始碼進行增補,動態加入額外原始碼一起編譯進結果,在某些情境可展現神奇效果。(延伸閱讀:新手上路 C# 原始碼產生器 (Source Generators) by 保哥)

JSON 序列化傳統會使用 Reflection 技術列舉序列化對象的屬性,逐一對映成 JSON 結構,反序列化則需要取得建構式、屬性及欄位資訊建立物件填入屬性值,除此外有些屬性會標註 JsonIgnore 等 Attribute 加入客製化行為。JSON 程式庫通常會在第一次序列化或反序列化時透過 Reflection 蒐集前述 Metadata 資訊並快取到記憶體以重複使用。

Reflection 效能較差眾所皆知,且快取 Metadata 會耗用記憶體,又容易導致組件修剪(Trim)失敗,而最重要的是 Native AOT 不支援 Refletion,會導致 JSON 轉換無法執行。 (延伸閱讀:用 .NET 開發程式庫供 Python 呼叫 - Native AOT 應用)

不信,可在 Native AOT 專案跑看看 System.Text.Json.JsonSerializer.Serialize(someString),編譯過程會有以下警示:(原因是 Serialize(object) 這個 Overloading 被標註了 RequiresUnreferencedCodeAttribute,標示會導致 Trim 失敗)

Trim analysis warning IL2026: Program.<Main>$(String[]): Using member 'System.Text.Json.JsonSerializer.Serialize<String>(String,JsonSerializerOptions )' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.
AOT analysis warning IL3050: Program.<Main>$(String[]): Using member 'System.Text.Json.JsonSerializer.Serialize(String,JsonSerializerOptions)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.

執行則會發生以下錯誤 Unhandled Exception: System.InvalidOperationException: Reflection-based serialization has been disabled for this application. Either use the source generator APIs or explicitly configure the 'JsonSerializerOptions.TypeInfoResolver' property.

System.Text.Json 自 .NET 6 開始支援應用 Source Generation 技術取代 Reflection,它有兩種模式:Metabase-Based 及 Serialization-Optimization (Fast Path)。

啟用 Metabase-Based 模式後,System.Text.Json 會在編譯期間蒐集序列化所需 Metadata 轉成相關程式碼 (依據效能測試可加快 40%,並大幅減少記憶體用量)。Serialization-Optimization 模式則可進一步產生用 Utf8JsonWriter 直接輸出 JSON 的程式碼,有效提升效能。

要用 Source Generation 輔助 System.Text.Json,做法很簡單,繼承 JsonSerializerContext 實作自訂型別(要用 partial class),型別不必實作方法,用 [JsonSerializable(typeof(T))] 標註序列化會涉及的型別即可,預設會同時啟用 Metabase-Based 及 Serialization-Optimization 模式。用描述的很難說明白,直接看 Code 就清楚了,以下是個簡單練習:(官方文件有更詳細說明)

using System.Text.Json;
using System.Text.Json.Serialization;

var s = "Hello World!";
// JsonTypeInfo<T>
Console.WriteLine(JsonSerializer.Serialize(s, MyJsonSerializerContext.Default.String));
// JsonSerializerContext
Console.WriteLine(JsonSerializer.Serialize(s, typeof(string), MyJsonSerializerContext.Default));
// JsonSerializerOptions
var jsonOptions = new JsonSerializerOptions {
    TypeInfoResolver  = MyJsonSerializerContext.Default
};
// 以下兩種寫法可成功,但會有 RequiresUnreferencedCodeAttribute 警告,不推
//Console.WriteLine(JsonSerializer.Serialize(s, typeof(string), jsonOptions));
//Console.WriteLine(JsonSerializer.Serialize(s, jsonOptions));

var p = new Player { 
    Name = "Jeffrey", 
    RegDate = DateTime.Today, 
    Score = 32767
};
var json = JsonSerializer.Serialize(p, MyJsonSerializerContext.Default.Player);
Console.WriteLine(json);
// 反序列化
var restored = JsonSerializer.Deserialize<Player>(json, MyJsonSerializerContext.Default.Player);
Console.WriteLine($"{restored.Name} == {p.Name}");

[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(Player))]
internal partial class MyJsonSerializerContext : JsonSerializerContext 
{
}

public class Player {
    public string Name { get; set; }
    public DateTime RegDate { get; set;}
    public int Score { get; set; }
}

最後,試著反組譯,看看 MyJsonSerializerContext 產生出來的原始碼長什麼樣子:

前面說到用 Utf8JsonWriter 直接輸出 JSON,程式碼如下,要輸出哪些屬性寫死成固定程式碼,難怪速度完全碾壓 Reflection。

private void PlayerSerializeHandler(global::System.Text.Json.Utf8JsonWriter writer, global::Player? value)
{
    if (value == null)
    {
        writer.WriteNullValue();
        return;
    }
    
    writer.WriteStartObject();

    writer.WriteString(PropName_Name, ((global::Player)value).Name);
    writer.WriteString(PropName_RegDate, ((global::Player)value).RegDate);
    writer.WriteNumber(PropName_Score, ((global::Player)value).Score);

    writer.WriteEndObject();
}

就醬,又學到新東西了。而編譯時自動產生程式碼這招很威,我已想到一些有趣的應用,有機會再來試玩看看。

Introduce to using source generation to improve the performance of System.Text.Json serialization and deserialization and make it support AOT.


Comments

# by 古蹟維護人員

適合用在: 1.產生 FOR 專案客戶的專用KEY、簽章、浮水印。 2.因應主機環境不同,產生最佳化編譯 3.按照客戶端設備的差異產生專屬程式碼,EX IPHONE、ANDROID、手機、平板、電視牆

Post a comment