💡

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 にします。

NativeMethods.json
{
	"$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)

参考リンク

主な改訂履歴

  • 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