AOTの使用経験のまとめ

AOTの使用経験のまとめ

プロジェクト作成当初から、新機能を追加したり新しい構文を使用したりした場合には、すぐにAOT公開テストを実施するという良い習慣を身につけるべきです。

最終更新 2026/02/07 14:26
沙漠尽头的狼
読了目安 7 分
カテゴリ
.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?のようなnull許容型が存在する場合:

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. Winforms / XP互換性

8番の手順後も実行できない場合は、前回の記事「.NET 9 AOTのブレークスルー - 旧来のWin7とXP環境をサポート - 碼坊 (dotnet9.com)」を参照してVC-LTLパッケージを追加してください。ここでは詳しく説明しません。

10. その他

他にも注意すべき点が多数あります。後日思い出したらこの記事を徐々に改善していきます。

三、まとめ

AOT公開テストは途中で多くの問題に遭遇する可能性がありますが、タイムリーなテストと正しい設定調整により、最終的にプロジェクトのスムーズな公開を実現できます。上記の経験が皆さんのAOT使用時に役立ち、開発プロセスでの無駄を減らし、プロジェクトの開発効率と品質を向上させることを願っています。同時に、皆さんが実践の中で探求とまとめを続け、共に技術の進歩と発展を推進することを期待しています。

AOT参考プロジェクト:

さらに探索

関連読書

その他の記事
同じカテゴリ / 同じテーマ 2023/08/29

.NET 8.0 AOT DebugView

DebugView は、ローカルシステムまたはTCP/IP経由でアクセス可能なネットワーク上の任意のコンピュータのデバッグ出力を監視できるアプリケーションです。

続きを読む