Native AOT トラブル対策 Tips 集
はじめに
C# アプリを Native AOT(以降「AOT」)でビルドする際のあれやこれやです。基本的にはリフレクションを避けてソース生成(ソースジェネレーター)を活用する方向になります。
AOT についてはまだ情報が少なく、知見をお持ちの方はコメントいただけると幸いです。
AOT ビルドの流れについてはこちらの記事をどうぞ。
サンプルプログラム
サンプルプログラムは GitHub に上げてあります。
全般 Tips
AOT でよく使う設定
プロジェクトファイル(.csproj)直接編集により AOT の設定を変更することができます。
<PropertyGroup>
<PublishAot>true</PublishAot>
<IsAotCompatible>true</IsAotCompatible>
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
<TrimmerSingleWarn>false</TrimmerSingleWarn>
<OptimizationPreference>speed</OptimizationPreference>
<TrimMode>partial</TrimMode>
</PropertyGroup>
PublishAot
<PublishAot>true</PublishAot>
で AOT を有効にします。プロジェクトプロパティの「ネイティブ AOT の公開」と同じです。
IsAotCompatible
<IsAotCompatible>true</IsAotCompatible>
でプロジェクトを AOT 互換として構成します。
これにより IsTrimmable も true になり、ライブラリのトリミング警告が表示されるようになります。
互換構成せずにトリミング警告だけ表示するには、代わりに <EnableTrimAnalyzer>true</EnableTrimAnalyzer>
を使用します。
SuppressTrimAnalysisWarnings
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
でトリミングによって破損する可能性があるパターンで警告を表示します。
OptimizationPreference
AOT デプロイの最適化方法を指定します。
<OptimizationPreference>speed</OptimizationPreference>
で速度最適化、<OptimizationPreference>size</OptimizationPreference>
でサイズ最適化になります。
未指定の場合は混合アプローチで、速度とサイズの間くらいです。
TrimMode
<TrimMode>full</TrimMode>
で最大限トリミングし、ファイルサイズが小さくなります。注意しないと事故が起こりやすくなると言えるかもしれません。
<TrimMode>partial</TrimMode>
はトリミングをオプトインしたアセンブリのみをトリミングするので、ファイルサイズが少し大きくなります。
COM を使う
COM を AOT で使用するには、こちらの記事をどうぞ。
CsWin32 を使う
Windows API を P/Invoke するための CsWin32 を AOT で使用するには、allowMarshaling を false にします。
{
"$schema": "https://aka.ms/CsWin32.schema.json",
"public": true,
"allowMarshaling": false
}
CsWin32 を分離プロジェクトで使う方法についてはこちらの記事をどうぞ。
DllImport
DllImport
属性は AOT では使えないので、代わりに LibraryImport
属性を使います。アンセーフコードの使用許可が必要です。
public static partial class extLib
{
[LibraryImport("myDll")]
public static partial Int32 SomeFunc(Int32 arg);
}
Int32 result = extLib.SomeFunc(99);
DllImport
より LibraryImport
のほうがマーシャリングが強化されているようです。
EF Core を使う
AOT において Entity Framework Core でデータベース操作をすると、実行時に以下のような例外になります。
Model building is not supported when publishing with NativeAOT. Use a compiled model.
コンパイル済みモデルおよびコンパイル済みクエリにより EF Core が使用可能になります。
要点は以下になります。
プロジェクトに
- Microsoft.EntityFrameworkCore.Design
- Microsoft.EntityFrameworkCore.Tasks
パッケージをインストールします。
プロジェクトファイルに
<PropertyGroup>
<InterceptorsNamespaces>$(InterceptorsNamespaces);Microsoft.EntityFrameworkCore.GeneratedInterceptors</InterceptorsNamespaces>
</PropertyGroup>
を追加します。
開発者コマンドプロンプトで、.csproj のあるフォルダーで、
dotnet tool install --global dotnet-ef (初回のみ。更新は dotnet tool update --global dotnet-ef)
dotnet ef dbcontext optimize --precompile-queries --nativeaot -o OutputDir -v
としてモデルを生成します。
生成されたファイル群をプロジェクトに追加し、DbContext の OnConfiguring() に UseModel() を追加します。
optionsBuilder
.UseModel(HogeContextModel.Instance)
.UseSqlite("Data Source=Hoge.sqlite");
ただし、それでも EnsureCreated() は使えないようです。
JSON の読み書き
AOT で JSON (System.Text.Json) を使おうとすると以下のような警告が出ます。
IL2026 Using member 'System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute'~
IL3050 Using member 'System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)' which has 'RequiresDynamicCodeAttribute'~
ソース生成により JSON を使えるようになります。
JsonSerializerContext の派生クラスを作り、JsonSerializable
属性で JSON 化したいクラスを指定します。
[JsonSerializable(typeof(AddressBook))]
internal partial class MyJsonSerializerContext : JsonSerializerContext
{
}
シリアライズ・デシリアライズ時に MyJsonSerializerContext を指定します。
String json = JsonSerializer.Serialize(addressBook, MyJsonSerializerContext.Default.AddressBook);
AddressBook addressBook2 = JsonSerializer.Deserialize(json, MyJsonSerializerContext.Default.AddressBook) ?? throw new Exception("デシリアライズ失敗");
JSON の読み書き(カスタムコンバーター)
JSON シリアライズ・デシリアライズ時にカスタムコンバーターを使いたい場合、やり方が 2 つあります。
解決策① JsonConverter 属性
カスタムコンバーターを適用してシリアライズ等したいクラス(データ側のクラス)に JsonConverter
属性を付けます。
[JsonConverter(typeof(RgbaConverter))]
internal class Rgba
{
// クラス内容
}
シンプルな方法で、対象クラスのシリアライズ・デシリアライズすべてにカスタムコンバーターが適用されます。
解決策② JsonSourceGenerationOptions 属性の Converters オプション
JsonSerializerContext の派生クラスに JsonSourceGenerationOptions
属性を付け、コンバーターを指定します。
[JsonSourceGenerationOptions(Converters = [typeof(RgbaConverter)])]
[JsonSerializable(typeof(Rgba))]
internal partial class MyJsonSerializerContext2 : JsonSerializerContext
{
}
JsonSerializerContext の派生クラスを複数作り分けることで、同じデータクラスに対しても細やかな制御が可能になります。
- カスタムコンバーター A とカスタムコンバーター B を使い分ける
- シリアライズではカスタムコンバーターを使用せず(デフォルト動作)、デシリアライズのみカスタムコンバーターを使用する
サンプルプログラムは解決策②で動作しています。
カスタムウィンドウプロシージャー
Windows API の SetWindowSubclass() によってウィンドウプロシージャーをカスタム化するのを AOT でやるには、こちらの記事の「AOT の場合」章をどうぞ。
別バイナリへの分離
AOT 化が困難な機能がある場合、その部分をサブアプリとして別 EXE に切り出し、そちらは AOT ではない通常のビルドにします。
残りの本体部分を AOT にして、サブアプリとはアプリケーション間通信などをする、という手法も考えられます。
構造が複雑になるうえアプリケーション間通信のオーバーヘッドも生じるため、注意が必要です。
WinUI 3 Tips
ComboBox を使うと落ちる
こちらをご覧ください。
ComboBox の DisplayMemberPath が使えない
ComboBox で DisplayMemberPath を設定しても該当のプロパティーは表示されません。
GeneratedBindableCustomProperty を設定することで表示されるようになります。
<ComboBox ItemsSource="{x:Bind ViewModel.TestList}" DisplayMemberPath="Name" />
[GeneratedBindableCustomProperty([nameof(Name)], [typeof(String)])]
internal partial record Person(String Name, Int32 Age);
public List<Person> TestList
{
get;
} =
[
new ("太郎", 20),
];
FileSavePicker.FileTypeChoices で例外
fileSavePicker.FileTypeChoices.Add(string1, [string2]);
のように FileTypeChoices にコレクション式で追加しようとすると、AOT では例外が発生します。
コレクション式を使わなければ大丈夫です。
fileSavePicker.FileTypeChoices.Add(string1, new[] { string2 });
ItemsSource をバインドすると落ちる
ComboBox や ItemsView などの ItemsSource を AOT で使用すると実行時に落ちたりエラーになったりします。
CsWinRT パッケージをプロジェクトにインストールすることで使用できるようになります。
AOT を使うなら CsWinRT は入れておく方が良いように思います(本来は依存で入っているような気もするのですが、Windows App SDK 1.7 現在はうまくいっていないようです)。
MVVM バインド
動的バインディングの Binding
はそのままでは動作しません。以下のような警告が出ます。
WMC1510 Ensure the property path is trimming and AOT compatible by making use of 'Compiled Bindings (x:bind)'~
解決策① x:Bind を使う
Binding
の代わりに静的バインディングの x:Bind
を使います。
<TextBlock Text="{x:Bind ViewModel.TestInt32, Mode=OneWay}" />
解決策② GeneratedBindableCustomProperty 属性を使う
何らかの事情で x:Bind
が使えない場合、頑張れば Binding
も使えます。
XAML 側に x:DataType
を使います。
<Page
x:Class="TestAotTips.Views.MainWindows.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:v="using:TestAotTips.ViewModels.MainWindows"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:DataType="v:MainPageViewModel"
mc:Ignorable="d">
<TextBlock Text="{Binding TestInt32}" />
</Page>
バインドされる ViewModel のクラスに GeneratedBindableCustomProperty
属性を付けます。
[GeneratedBindableCustomProperty]
public partial class MainPageViewModel : ObservableRecipient
{
}
確認環境
項目 | 環境 |
---|---|
OS | Windows 11 Pro 23H2 |
Visual Studio | 2022 17.13.4 |
.NET | 9.0 |
Template Studio for WinUI | 5.5 |
WinUIEx | 2.5.1 |
Windows App SDK | 1.7.250310001 (1.7.0) |
参考リンク
- AOT Support?
- AOT デプロイを最適化する
- CsWin32
- CsWin32 を別プロジェクトに分離する
- CsWinRT
- Native AOT で COM を使ってみた
- NativeAOT のサポートとプリコンパイル済みクエリ (試験段階)
- .NET 9 の
LibraryImport
自動マーシャリング機能を使いこなすうえでの注意点 - .NET trimming and AOT support in C#/WinRT
- .NET で JSON シリアル化 (マーシャリング) のためのカスタム コンバーターを作成する方法
- SetWindowSubclass によるウィンドウプロシージャーのカスタム
- System.Text.Json でソース生成を使用する方法
- Windows.Storage.Pickers.FileSavePicker.FileTypeChoices.Add throw System.InvalidCastException: 'Specified cast is not valid.' with PublishAOT=true
- WinUI 3 で Native AOT が簡単になっていた
- 高度なパフォーマンスのトピック
- サンプルプログラム
- デザイン時 DbContext 作成
- トリミングのオプション
- トリミング用に .NET ライブラリを準備する
- プラットフォーム呼び出し用のソース生成
主な改訂履歴
- 2025/03/22 初版。
- 2025/03/27 「AOT でよく使う設定」を新規作成。
- 2025/05/15 「EF Core を使う」を新規作成。
- 2025/05/16 「EF Core を使う」を更新。
- 2025/07/21 「JSON の読み書き(カスタムコンバーター)」を新規作成。
- 2025/08/08 「別バイナリへの分離」を新規作成。
Discussion