AOT使用經驗總結

AOT使用經驗總結

從專案建立伊始,就應養成良好的習慣,即只要添加了新功能或使用了較新的語法,就及時進行 AOT 發布測試。

最後更新 2026/2/7 下午2:26
沙漠尽头的狼
預計閱讀 9 分鐘
分類
.NET
專題
C# AOT
標籤
.NET C# Dapper Prism AOT

一、引言

站長接觸 AOT 已有 3 個月之久,此前在《好消息:NET 9 X86 AOT 的突破 - 支援老舊 Win7 與 XP 環境》一文中就有所提及。在這段時間裡,站長使用 Avalonia 開發的專案也成功完成了 AOT 發佈測試。然而,這一過程並非一帆風順。站長在專案功能完成大半部分才開始進行 AOT 測試,期間遭遇了不少問題,可謂是「踩坑無數」。為了方便日後回顧,也為了給廣大讀者提供參考,在此將這段經歷進行總結。

.NET AOT 是將 .NET 程式碼提前編譯為本機程式碼的技術。其優勢眾多,啟動速度快,減少執行時期資源占用,還提高安全性。AOT 發佈後無需再安裝 .NET 執行時期等相依元件。.NET 8、9 AOT 發佈後,可在 XP、Win7 非 SP1 作業系統下執行。這使得應用部署更便捷,能適應更多老舊系統環境,為開發者拓展了應用場景,在效能提升的同時,也增加了系統相容性,讓 .NET 應用的開發和部署更具靈活性和廣泛性,給使用者帶來更好的體驗。

二、經驗之談

(一)測試策略的重要性

從專案建立伊始,就應養成良好的習慣,即只要新增了功能或使用了較新的語法,就及時進行 AOT 發佈測試。否則,問題累積到後期,解決起來會異常艱難,站長就因前期忽視了這一點,付出了慘痛的代價。無奈的解決方法是重新建立專案,然後逐個還原功能並進行 AOT 測試。經過了一週的加班 AOT 測試,每個 AOT 發佈過程大致如下:

  1. 內網 AOT 發佈一次需 2、3 分鐘,這段時間只能看看需求文件、技術文章、需求文件、技術文章。。。
  2. 發佈完成,執行無效果,體現在雙擊未出現介面,處理程序列表沒有它,說明程式崩潰了,查看系統應用事件記錄,記錄中通常會包含例外警告資訊。
  3. 依據記錄資訊檢查程式碼,修改相關 API。
  4. 再次進行 AOT 發佈,重複上述 1 - 3 步驟。

經過一週的努力,專案 AOT 後功能測試終於正常,至此完工。

(二)AOT 需要注意的點及解決方法

1. 新增 rd.xml

在主專案建立一個 XML 檔案,例如 Roots.xml,內容大致如下:

<linker>
	<assembly fullname="CodeWF.Toolbox.Desktop" preserve="All" />
</linker>

需要支援 AOT 的專案,在該 XML 中新增一個 assembly 節點,fullname 是組件名稱,CodeWF.Toolbox.Desktop 是站長小工具的主專案名稱,點擊查看原始碼。

在主專案新增 ItemGroup 節點關聯該 XML 檔案:

<ItemGroup>
    <TrimmerRootDescriptor Include="Roots.xml" />
</ItemGroup>

2. Prism 支援

站長使用了 Prism 框架及 DryIOC 容器,若要支援 AOT,需要新增以下 NuGet 套件:

<PackageReference Include="Prism.Avalonia" Version="8.1.97.11073" />
<PackageReference Include="Prism.DryIoc.Avalonia" Version="8.1.97.11073" />

rd.xml 需要新增

<assembly fullname="Prism" preserve="All" />
<assembly fullname="DryIoc" preserve="All" />
<assembly fullname="Prism.Avalonia" preserve="All" />
<assembly fullname="Prism.DryIoc.Avalonia" preserve="All" />

8.1.97.11073 版本為最後一個開源版本,9.X 及後面的為付費版本

3. App.config 讀寫

在 .NET Core 中使用 System.Configuration.ConfigurationManager 套件操作 App.config 檔案,rd.xml 需新增如下內容:

<assembly fullname="System.Configuration.ConfigurationManager" preserve="All" />

使用 Assembly.GetEntryAssembly().location 失敗,目前使用 ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None) 取得的應用程式組態設定,指定路徑的方式後續再研究。

4. HttpClient 使用

rd.xml 新增如下內容:

<assembly fullname="System.Net.Http" preserve="All" />

一般不直接使用 HttpClient,可嘗試 Refit 之類的三方庫用作 Http 請求更便捷:

<assembly fullname="Refit" preserve="All" />

5. Dapper 支援

Dapper 的 AOT 支援需要安裝 Dapper.AOT 套件,rd.xml 新增如下內容:

<assembly fullname="Dapper" preserve="All" />
<assembly fullname="Dapper.AOT" preserve="All" />

資料庫操作的方法需要新增 DapperAOT 特性,舉例如下:

[DapperAot]
public static bool EnsureTableIsCreated()
{
    try
    {
        using var connection = new SqliteConnection(DBConst.DBConnectionString);
        connection.Open();

        const string sql = $@"
            CREATE TABLE IF NOT EXISTS {nameof(JsonPrettifyEntity)}(
                {nameof(JsonPrettifyEntity.IsSortKey)} Bool,
                {nameof(JsonPrettifyEntity.IndentSize)} INTEGER
        )";

        using var command = new SqliteCommand(sql, connection);
        return command.ExecuteNonQuery() > 0;
    }
    catch (Exception ex)
    {
        return false;
    }
}

6. System.Text.Json

參考 JsonExtensions.cs

序列化

public static bool ToJson<T>(this T obj, JsonSerializerOptions? options,  out string? json, out string? errorMsg)
{
    if (obj == null)
    {
        json = default;
        errorMsg = "Please provide object";
        return false;
    }

    if (options == null)
    {
        options = new JsonSerializerOptions()
        {
            WriteIndented = true,
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
            TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
        };
    }

    try
    {
        json = JsonSerializer.Serialize(obj, options);
        errorMsg = default;
        return true;
    }
    catch (Exception ex)
    {
        json = default;
        errorMsg = ex.Message;
        return false;
    }
}

反序列化

public static bool FromJson<T>(this string? json, JsonSerializerOptions? options, out T? obj, out string? errorMsg)
{
    if (string.IsNullOrWhiteSpace(json))
    {
        obj = default;
        errorMsg = "Please provide json string";
        return false;
    }

    try
    {
        if (options == null)
        {
            options = new JsonSerializerOptions()
            {
                Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
                TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
                Converters = { new NullableDateTimeConverter() }
            };
        }

        obj = JsonSerializer.Deserialize<T>(json!, options);
        errorMsg = default;
        return true;
    }
    catch (Exception ex)
    {
        obj = default;
        errorMsg = ex.Message;
        return false;
    }
}

上面設定簡單類別序列化與反序列化正常用,如果類別中存在 int?double? 之類的可空型別:

public class Project
{
    public int? Id { get; set; }

    public string Name { get; set; }

    public int Record { get; set; }

    [XmlArray(ElementName = "Members")]
    [XmlArrayItem(typeof(Member))]
    public List<Member> Members { get; set; }
}

則建立一個 JsonSerializerContext 的子類別即可解決:

[JsonSerializable(typeof(Project))]
[JsonSerializable(typeof(List<Member>))]
internal partial class ProjectJsonSerializerContext : JsonSerializerContext
{
}

注意:建立好即可,不需要主動呼叫

傳統的 JsonSerializer.Serialize(project) 底層依賴執行時期反射來解析 Project 類別的屬性、特性等資訊,但 AOT 編譯會在編譯期裁剪掉未被明確引用的反射元資料,導致序列化失敗(擲回 NotSupportedException 或無法序列化 / 反序列化某些屬性)。

而 ProjectJsonSerializerContext 是透過編譯期產生靜態元資料的方式工作。

7. 反射問題

參考專案 CodeWF.NetWeaver

  1. 建立指定型別的 List<T>Dictionary<T> 實例:
public static object CreateInstance(Type type)
{
    var itemTypes = type.GetGenericArguments();
    if (typeof(IList).IsAssignableFrom(type))
    {
        var lstType = typeof(List<>);
        var genericType = lstType.MakeGenericType(itemTypes.First());
        return Activator.CreateInstance(genericType)!;
    }
    else
    {
        var dictType = typeof(Dictionary<,>);
        var genericType = dictType.MakeGenericType(itemTypes.First(), itemTypes[1]);
        return Activator.CreateInstance(genericType)!;
    }
}
  1. 反射呼叫 List<T>Dictionary<T>Add 方法新增元素失敗,下面是虛擬碼:
// List<T>
var addMethod = type.GetMethod("Add");
addMethod.Invoke(obj, new[]{ child })

// Dictionary<Key, Value>
var addMethod = type.GetMethod("Add");
addMethod.Invoke(obj, new[]{ key, value })

解決辦法,轉換為實作的介面呼叫:

// List<T>
(obj as IList).Add(child);

// Dictionary<Key, Value>
(obj as IDictionary)[key] = value;
  1. 取得陣列、List<T>Dictionary<key, value> 的元素個數

同上面 Add 方法反射取得 Length 或 Count 屬性皆回傳 0,value.Property("Length", 0),封裝的 Property 非 AOT 執行正確:

public static T Property<T>(this object obj, string propertyName, T defaultValue = default)
{
    if (obj == null) throw new ArgumentNullException(nameof(obj));
    if (string.IsNullOrEmpty(propertyName)) throw new ArgumentNullException(nameof(propertyName));

    var propertyInfo = obj.GetType().GetProperty(propertyName);
    if (propertyInfo == null)
    {
        return defaultValue;
    }

    var value = propertyInfo.GetValue(obj);

    try
    {
        return (T)Convert.ChangeType(value, typeof(T));
    }
    catch (InvalidCastException)
    {
        return defaultValue;
    }
}

AOT 成功:直接透過轉換為基底型別或實作的介面呼叫屬性即可:

// 陣列
var length = ((Array)value).Length;

// List<T>
 if (value is IList list)
{
    var count = list.Count;
}

// Dictionary<key, value>
if (value is IDictionary dictionary)
{
    var count = dictionary.Count;
}

8. Windows 7 支援

如遇 AOT 後無法在 Windows 7 執行,請新增 YY-Thunks 套件:

<PackageReference Include="YY-Thunks" Version="1.1.4-Beta3" />

並指定目標架構為 net9.0-windows

9. Winform / 相容 XP

如果第 8 條後還執行不了,請參考上一篇文章《.NET 9 AOT 的突破 - 支援老舊 Win7 與 XP 環境 - 碼坊 (dotnet9.com)》新增 VC-LTL 套件,這裡不贅述。

10. 其他

還有許多其他需要注意的地方,後續想起來逐漸完善本文。

三、總結

AOT 發佈測試雖然過程中可能會遇到諸多問題,但透過及時的測試和正確的組態調整,最終能夠實現專案的順利發佈。希望以上總結的經驗能對大家在 AOT 使用過程中有所幫助,讓大家在開發過程中少走彎路,提高專案的開發效率和品質。同時,也期待大家在實踐中不斷探索和總結,共同推動技術的進步和發展。

AOT 可參考專案:

繼續探索

延伸閱讀

更多文章
同分類 / 同專題 2023/8/29

.NET 8.0 AOT DebugView

Debugview 是一個應用程式,支援你監視本機系統上或可透過 TCP/IP 存取的網路上任何電腦的偵錯輸出。

繼續閱讀