native-aot について
.NET 開発者向けに、Native AOT、トリミング、単一ファイル発行を解説するガイドです。反射に代わるソース生成や制約を説明し、CI で発行済み成果物を検証する方法も扱います。
主な機能
- 01Native AOT と単一ファイル発行の設定
- 02ソース生成 JSON と正規表現の活用
- 03トリミングと AOT の警告への対処
- 04CI での発行済みバイナリのテスト
- 05アプリ種別ごとの AOT 適性評価
活用シーン
- 01CLI ツール向けのスタンドアロン配布物の作成
- 02サーバーレスアプリのコールドスタート低減
- 03依存関係のトリミング互換性の確認
- 04AOT 対応ライブラリの品質基準の設定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ codewithmukesh/ dotnet-claude-kit --skill native-aotインストール後、Claude Code で/native-aotを入力して読み込まれたか確認します。
詳細
Native AOT、トリミング、シングルファイル(.NET 10)
基本原則
- 早い段階で決める — AOT ではライブラリの選択が制限されます(実行時リフレクションやコード生成は使えません)。リフレクションを多用する成熟したアプリに後から導入する場合、書き直しが必要になります。初日から導入すれば、コストはほとんどかかりません。
- トリミング/AOT の警告は、将来の実行時クラッシュにつながります — IL2xxx(トリミング)と IL3xxx(AOT/シングルファイル)の警告は、公開バイナリで
MissingMethodExceptionがスローされたり、気付かないうちに誤動作したりする可能性のあるコードを示しています。警告をエラーとして扱い、件数をゼロに保ってください。 - ソースジェネレーターでリフレクションを置き換える — JSON、正規表現、ロギング、構成バインディング、オプション検証、MVVM には、いずれもジェネレーターがあります。AOT の経路を高速かつ解析可能に保てます。
dotnet runではなく、公開した成果物をテストする —dotnet runは JIT と完全なメタデータを使用するため、トリミングのバグをすべて見えなくします。CI でdotnet publishが生成するバイナリをスモークテストしてください。- AOT は無償のアップグレードではなく、トレードオフです — 起動の高速化、メモリ使用量の削減、実行時のインストール不要という利点が得られます。一方で、動的読み込み、
Reflection.Emit、Dynamic PGO による一部の定常状態のスループットを失います。トレードオフに見合う場合に導入してください。
パターン
プロジェクトの設定
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
<OptimizationPreference>Size</OptimizationPreference>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
dotnet publish -c Release -r linux-x64 # one RID per publish
理由: PublishAot をコマンドラインだけでなくプロジェクトファイルに設定すると、すべてのビルドでアナライザーが実行されるため、コーディング中に警告が表示されます。ライブラリに IsAotCompatible を設定すると、アプリではなくソースの段階で問題を明らかにできます。Native AOT にはプラットフォームのツールチェーンが必要です(Windows では VS の「C++ によるデスクトップ開発」、Linux では clang と zlib の開発パッケージ、macOS では Xcode CLI ツール)。CI マトリックスでは、各 OS 上でその OS 向けにビルドしてください。Linux バイナリは、ビルドに使ったものと同じか、それより新しいディストリビューションのバージョンでのみ実行できます。
ソース生成 JSON
public sealed record OrderDto(Guid Id, string Customer, decimal Total, DateTimeOffset CreatedAt);
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(OrderDto))]
[JsonSerializable(typeof(List<OrderDto>))]
internal sealed partial class AppJsonContext : JsonSerializerContext;
// Usage — pass the generated JsonTypeInfo, never a bare Type
var json = JsonSerializer.Serialize(order, AppJsonContext.Default.OrderDto);
var orders = await JsonSerializer.DeserializeAsync(stream, AppJsonContext.Default.ListOrderDto, ct);
// ASP.NET Core minimal APIs: register the context once
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.ConfigureHttpJsonOptions(o =>
o.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default));
理由: リフレクションベースの System.Text.Json は、トリミング/AOT アプリでは無効になります。生成されたメタデータは JIT でも高速です。コレクションの形状(List<OrderDto>)を含め、シリアライズ対象となるすべての型を登録してください。CreateSlimBuilder は、AOT と互換性のない既定値を除外します。Minimal API の Request Delegate Generator は、PublishAot を設定すると自動的に有効になります。
生成される正規表現、ロギング、構成バインディング、オプション検証
internal static partial class Patterns
{
[GeneratedRegex(@"^ORD-\d{6}$", RegexOptions.CultureInvariant)]
internal static partial Regex OrderNumber { get; }
}
internal static partial class Log
{
[LoggerMessage(Level = LogLevel.Information, Message = "Imported {Count} orders in {ElapsedMs} ms")]
internal static partial void Imported(ILogger logger, int count, long elapsedMs);
}
[OptionsValidator]
internal sealed partial class ImportOptionsValidator : IValidateOptions<ImportOptions>;
public sealed class ImportOptions
{
[Range(1, 10_000)] public int BatchSize { get; set; } = 500;
}
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
理由: [GeneratedRegex] はコンパイル時にマッチングコードを生成します(実行時の IL 出力が必要な RegexOptions.Compiled は使いません)。[LoggerMessage] を使うと、呼び出しのたびにボックス化したりテンプレートを解析したりする必要がなくなります。構成バインディングジェネレーターは、Bind/Get<T>/Configure<T> 内のリフレクションを置き換えます。[OptionsValidator] は、リフレクションベースのデータ注釈検証を置き換えます。
トリミング警告を適切に修正する
// Propagate the requirement to callers instead of hiding it
[RequiresUnreferencedCode("Loads plugin types by name; not trim-safe.")]
[RequiresDynamicCode("Creates generic types at runtime.")]
public static object CreatePlugin(string typeName) => Activator.CreateInstance(Type.GetType(typeName)!)!;
// Or tell the trimmer exactly what to keep
public static T Create<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>()
where T : new() => new();
public static object CreateFrom(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type) =>
Activator.CreateInstance(type)!;
理由: [RequiresUnreferencedCode]/[RequiresDynamicCode] を使うと、警告が呼び出し元に移るため、安全な経路を選択できます。[DynamicallyAccessedMembers] を使うと、リフレクションに必要なメンバーだけをトリマーに保持させられます。[UnconditionalSuppressMessage] を使うのは、コードが確実に安全である理由を正当化の説明で示せる場合に限ってください。
AOT を使わないシングルファイル
<PropertyGroup>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>
// BAD in single-file: Assembly.Location is empty (IL3000)
var dir = Path.GetDirectoryName(typeof(Program).Assembly.Location);
// GOOD
var dir = AppContext.BaseDirectory;
理由: シングルファイルでは、JIT と完全なリフレクションを維持したまま、単一の実行ファイルとして配布できます。依存関係が AOT と互換性はないものの、ダウンロードを簡単にしたい場合に使ってください。一般的な問題はファイルパス API です。
CI での検証
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- run: dotnet publish src/Contoso.Cli -c Release -o out
- run: ./out/contoso --version && ./out/contoso import --input samples/orders.csv --dry-run
理由: 必要なものがトリミング後も保持されていることを証明できるのは、公開バイナリだけです。各 OS でコマンドをスモークテストすれば、リリース前にメタデータ、ネイティブライブラリ、グローバリゼーションに関する問題を検出できます。
アンチパターン
ビルドを成功させるために警告を抑制する
<NoWarn>$(NoWarn);IL2026;IL3050</NoWarn>
<TrimMode>partial</TrimMode>
// GOOD — fix the cause: generated serializer instead of reflection
JsonSerializer.Serialize(order, AppJsonContext.Default.OrderDto);
リフレクションベースのシリアライズオーバーロード
// BAD — IL2026/IL3050: needs runtime reflection over OrderDto
var json = JsonSerializer.Serialize(order);
var dto = await response.Content.ReadFromJsonAsync<OrderDto>(ct);
// GOOD — JsonTypeInfo overloads
var json = JsonSerializer.Serialize(order, AppJsonContext.Default.OrderDto);
var dto = await response.Content.ReadFromJsonAsync(AppJsonContext.Default.OrderDto, ct);
規約ベースのアセンブリスキャン
// BAD — trimmer removes "unused" types; GetTypes() finds nothing at runtime
foreach (var t in typeof(Program).Assembly.GetTypes().Where(t => t.IsAssignableTo(typeof(ICommandHandler))))
services.AddTransient(typeof(ICommandHandler), t);
// GOOD — explicit registration (or a source generator that emits it)
services.AddTransient<ICommandHandler, ImportHandler>();
services.AddTransient<ICommandHandler, ExportHandler>();
RegexOptions.Compiled と実行時の式コンパイル
// BAD — needs Reflection.Emit; under AOT it falls back to slower interpretation
private static readonly Regex Sku = new(@"^[A-Z]{3}-\d+$", RegexOptions.Compiled);
// GOOD
[GeneratedRegex(@"^[A-Z]{3}-\d+$")]
private static partial Regex Sku { get; }
判断ガイド
| シナリオ | 推奨事項 |
|---|---|
| .NET 以外のユーザー向けに配布する CLI ツール | RID ごとの Native AOT |
dotnet run app.cs スクリプトの発行 | ファイルベースのアプリでは、Native AOT がすでにデフォルトです |
| サーバーレス/ゼロスケール/小規模コンテナーを多数使用 | Native AOT(コールドスタートが速く、メモリ使用量が少ない)。-aot ベースイメージについては container-publish を参照してください |
| Avalonia デスクトップアプリ | コンパイル済みバインディングとトリミングセーフなビューのマッピングを使用すれば、Native AOT を利用できます |
| WPF または WinForms | トリミング/AOT はサポートされていません。起動時間を短縮するには ReadyToRun と単一ファイルを使用してください |
| 長時間稼働する高スループット API | 通常は JIT(定常状態では Dynamic PGO が優位です)。切り替える前に測定してください |
| EF Core のデータアクセス | EF Core AOT(プリコンパイル済みクエリ)はまだ実験段階です。EF を多用するアプリでは JIT を使用してください |
| MVC コントローラー、SignalR を多用するアプリ、Blazor Server | AOT の対象ではありません。AOT が必要な場合は最小限の API を使用してください |
プラグイン/Assembly.LoadFrom/Reflection.Emit | AOT では利用できません。JIT を使用してください |
| 1 つの実行ファイルでダウンロードできる、AOT 非対応の依存関係がある場合 | PublishSingleFile + SelfContained(JIT) |
| AOT 利用者向けライブラリ | IsAotCompatible + 警告ゼロ、net8.0 以降をマルチターゲットにする |
| 依存関係がトリミング警告を出す | 置き換えるか、[RequiresUnreferencedCode] の背後に隔離してください。一律に警告を抑制しないでください |