dotnet-aot-compat について
.NET 8以降を対象とするプロジェクトの解析警告を解消し、Native AOTとトリミングに対応させるスキルです。ビルド結果を起点に調査し、警告の抑制ではなく注釈の伝播やリファクタリングを優先します。
主な機能
- 01AOT・トリミング解析の有効化
- 02警告コード別の問題分類
- 03リフレクション注釈の追加や呼び出しのリファクタリング
- 04ソース生成JSONシリアライズの利用
- 05複数TFMでのビルド検証
活用シーン
- 01ILトリミング・AOT警告の修正
- 02リフレクションコードへの注釈追加
- 03.NETプロジェクトのNative AOT対応
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dotnet/ skills --skill dotnet-aot-compatインストール後、Claude Code で/dotnet-aot-compatを入力して読み込まれたか確認します。
同梱ファイルreferences/polyfills.md
詳細
dotnet-aot-compat
すべての IL トリム/AOT アナライザーの警告を体系的に解決して、.NET プロジェクトを Native AOT とトリミングに対応させます。
このスキルを使用する場合
- 「このプロジェクトを AOT 対応にする」
- 「トリミングの警告を修正する」または「IL の警告を修正する」
- 「IL2070 / IL2067 / IL2072 / IL2026 / IL3050 の警告を解決する」
- 「DynamicallyAccessedMembers の注釈を追加する」
- 「.csproj で IsAotCompatible を有効にする」
- 「net8.0 にアップグレードしたら、プロジェクトでトリムアナライザーの警告が出るようになった」
- 「トリマー向けにリフレクションコードに注釈を付ける」
このスキルを使用しない場合
プロジェクトの対象が .NET Framework(net4x)のみの場合は、このスキルを使用しないでください。トリム/AOT アナライザーには対応していません。
前提条件
net8.0 以降を対象とする既存の .NET プロジェクト(または少なくとも 1 つの net8.0 以降の TFM を含むマルチターゲットプロジェクト)と、対応する .NET SDK がインストールされていること。
背景:AOT 対応の意味
Native AOT と IL トリマーは静的解析を行い、到達可能なコードを判断します。リフレクションを使用すると、実行時にアクセスされる型やメンバーをトリマーが把握できないため、この解析に支障が生じることがあります。IsAotCompatible プロパティを有効にすると、これらの問題をビルド警告(ILXXXX コード)として報告するアナライザーが有効になります。
重要なルール
❌ 警告を誤って抑制しないでください
- IL の警告に対して
#pragma warning disableを絶対に使用しないでください。ビルド時に Roslyn アナライザーから警告が隠れますが、IL リンカーと AOT コンパイラーには問題が引き続き検出されます。トリミング/発行時にコードが失敗します。 [UnconditionalSuppressMessage]を絶対に使用しないでください。アナライザーとリンカーの両方に警告を無視するよう指示するため、トリマーは安全性を検証できなくなります。問題を隠して実行時に気付かないまま破損させるより、ビルド時にエラーを発生させる方が常に望ましいです。
💡 推奨する方法
- 呼び出しチェーンを通じて型情報を伝えるには、
[DynamicallyAccessedMembers]の注釈を優先して使用してください。 - 注釈の伝達を妨げるパターン(例:
Typeをobject[]にボックス化すること)をなくすリファクタリングを優先してください。 - トリミングと根本的に互換性のないメソッドには、
[RequiresUnreferencedCode]/[RequiresDynamicCode]/[RequiresAssemblyFiles]を使用して、要件を呼び出し元へ伝播させてください。問題を隠すのではなく明確に示すことで、呼び出し元が互換性のなさを明示的に認識できるようにします。
注釈の伝達が重要です
トリマーは、代入、引数の受け渡し、戻り値を通じて [DynamicallyAccessedMembers] の注釈を追跡します。この伝達が途切れると(例:Type を object にボックス化する、型指定のないコレクションに格納する、インターフェイス経由でキャストする)、トリマーは追跡できなくなり、警告が発生します。解決策は警告を抑制することではなく、伝達を維持することです。
手順
事前にコードベースを調べないでください。 ビルド警告から、変更が必要なファイルと行が正確に分かります。ビルド → 警告を 1 つ選ぶ → 該当行のファイルを開く → 修正方法を適用する → 再ビルドという短いサイクルで進めてください。特定の警告で示された範囲を超えてソースファイルを読んだり解析したりするのは時間の無駄であり、タイムアウトの原因になります。コンパイラーの指示に従ってください。
❌ ビルド前にプロジェクト構造を把握するために
find、ls、grepを実行しないでください。README、ドキュメント、アーキテクチャ関連のファイルも読まないでください。最初に行うのはステップ 1(AOT 解析の有効化)です。その後でビルドしてください。
ステップ 1:.csproj で AOT 解析を有効にする
IsAotCompatible を追加します。プロジェクトの対象が net8.0 以降のみでない場合は、TFM 条件を追加します(AOT 解析には net8.0 以降が必要です)。
<PropertyGroup>
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsAotCompatible>
</PropertyGroup>
これにより、互換性のある TFM で EnableTrimAnalyzer=true と EnableAotAnalyzer=true が自動的に設定されます(例:netstandard2.0;net8.0 のような)マルチターゲットプロジェクトでは、条件によって古い TFM での NETSDK1210 警告を防げます。
ステップ 2:ビルドして警告を収集する
dotnet build <project.csproj> -f <net8.0-or-later-tfm> --no-incremental 2>&1 | grep 'IL[0-9]\{4\}'
並べ替えて重複を削除します。一般的な警告コード:
- IL2070:
[DynamicallyAccessedMembers]が付いていないTypeパラメーターに対するリフレクション呼び出し - IL2067:
[DynamicallyAccessedMembers]を必要とするメソッドに、注釈のないTypeを渡している - IL2072:戻り値または抽出された値に注釈がない(ボックス化解除による場合がよくあります)
- IL2057:定数でない引数を指定した
Type.GetType(string) - IL2026:
[RequiresUnreferencedCode]が付いたメソッドの呼び出し - IL2050:COM マーシャリングパラメーターを持つ P/invoke メソッド
- IL2075:戻り値が注釈なしでリフレクションに渡されている
- IL2091:制約で必要とされる
[DynamicallyAccessedMembers]がジェネリック引数にない - IL3000:シングルファイル/AOT アプリでは
Assembly.Locationが空の文字列を返す - IL3050:
[RequiresDynamicCode]が付いたメソッドの呼び出し
ステップ 3:コードごとに警告を分類する(すべてのファイルを読まないでください)
ステップ 2 の警告を警告コードごとにまとめ、件数を数えます。まだ個々のファイルは開かないでください。 件数の多い上位 1~2 個のパターンを特定します。これを基に修正方針を決めます。
| パターン | 一般的な修正方法 |
|---|---|
JsonSerializer に起因する IL2026 と IL3050 が多数 | すぐに Strategy C に進みます — JsonSerializerContext を作成し、すべての呼び出し箇所をまとめて更新します |
Type パラメーターに関する IL2070/IL2087 | 最も内側のメソッドに [DynamicallyAccessedMembers] を追加し、その後、呼び出し元へ順に適用します |
注釈のない Type を渡す IL2067 | 呼び出し元でパラメーターに注釈を付けます |
実際のプロジェクトの多くでは、JsonSerializer に起因する IL2026/IL3050 が大半を占めます。 警告の内訳に明らかな根拠がない限り、Strategy C から始めてください。JSON をまとめて修正した後、残りの警告を Strategies A~B で処理します。Strategy D は最後の手段としてのみ使用してください。
ステップ 4:警告を繰り返し修正する(最も内側から)
最も内側のリフレクション呼び出しから外側へ向かって作業します。修正のたびに、呼び出し元に新たな警告が波及することがあります。
警告を起点に進めてください。 警告ごとに、コンパイラーが報告したファイルと行だけを開き、パターンを特定して、以下の該当する修正手順を適用したら次に進みます。類似パターンを探してコードベースをスキャンしたり、アーキテクチャ全体を理解しようとしたりしないでください。コンパイラーの指示に従って修正し、再ビルドして、新たな警告を次の変更の指針にします。警告を少量(5~10 件)まとめて修正したら、すぐに再ビルドして進捗を確認します。
利用可能な場合はサブエージェントを使用してください。 サブエージェント(たとえば task ツール経由)を起動できる場合は、複数のサブエージェントを並行して起動し、それぞれ異なるファイルを同時に編集させます。メインのループではビルド、警告の解析、タスクの割り当てに集中し、実際のファイル編集はサブエージェントに委任します。JSON の一括更新では、各サブエージェントに 5~10 個のファイルを 1 つのプロンプトで割り当てます。ビルドと修正を 2 回繰り返したら、残りのファイル編集をすべてサブエージェントに並行して割り当ててください。ファイルを順番に修正し続けないでください。 例:
これらのファイルをソース生成 JSON を使用するように更新してください:
src/Models/Resource.Serialization.cs、src/Models/Identity.Serialization.cs、src/Models/Plan.Serialization.cs。各ファイルで、JsonSerializer.Serialize(writer, value)をJsonSerializer.Serialize(writer, value, MyProjectJsonContext.Default.TypeName)に、JsonSerializer.Deserialize<T>(ref reader)をJsonSerializer.Deserialize(ref reader, MyProjectJsonContext.Default.TypeName)に置き換えてください。JsonSerializer の呼び出し箇所のみを編集してください。
Strategy A:[DynamicallyAccessedMembers] を追加する(推奨)
メソッドが Type パラメーターに対してリフレクションを使用する場合、トリマーに必要なメンバーを伝えるため、パラメーターに注釈を付けます。
using System.Diagnostics.CodeAnalysis;
// Before (warns IL2070):
void Process(Type t) {
var method = t.GetMethod("Foo"); // trimmer can't verify
}
// After (clean):
void Process([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type t) {
var method = t.GetMethod("Foo"); // trimmer preserves public methods
}
パラメーターに注釈を付けると、すべての呼び出し元が適切に注釈された型を渡す必要があります。これにより変更が外側へ波及するため、各呼び出し元をたどり、必要に応じて注釈を付けるかリファクタリングします。呼び出し元の注釈には、呼び出し先と同じ、またはそれ以上のメンバー型を含める必要があります。 呼び出し先が PublicConstructors | NonPublicConstructors を必要とする場合、呼び出し元も同じ指定か、それ以上の指定が必要です。NonPublicConstructors のみを指定すると IL2091 が発生します。
戦略 B:アノテーションの伝播を維持するためにリファクタリングする
ボックス化(Type を object、object[]、または型指定のないコレクションに格納すること)によってアノテーションの伝播が妨げられている場合は、Type を直接渡すようにリファクタリングします。
// BROKEN: Type boxed into object[], annotation lost
void Process(object[] args) {
Type t = (Type)args[0]; // IL2072: annotation lost through boxing
Evaluate(t, ...);
}
// FIXED: Pass Type as a separate, annotated parameter
void Process(
object[] args,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type calleeType,
...) {
Evaluate(calleeType, ...); // annotation flows cleanly
}
伝播を妨げる一般的なパターンと修正方法:
object[]パラメーターのバッグ:Typeを専用のアノテーション付きパラメーターに取り出します- ディクショナリ/リストへの格納:代わりに、アノテーション付きの型指定フィールドを使用します
- インターフェイスを介した間接参照:インターフェイスメソッドのパラメーターにアノテーションを追加します
- ボックス化する getter を持つプロパティ:プロパティの戻り値の型にアノテーションを付けます
戦略 C:ソース生成 JSON シリアル化(一括修正)
警告の大半が JsonSerializer.Serialize/Deserialize による IL2026/IL3050 の場合、これは一括適用できる機械的な単一修正です。
-
影響を受ける型を収集する — すべての
JsonSerializer.SerializeおよびJsonSerializer.Deserializeの呼び出し箇所を grep します。シリアル化される型(Deserialize<T>の<T>、またはSerialize内のオブジェクトの実行時の型)を抽出します。 -
見つかったすべての型に
[JsonSerializable]を付けたJsonSerializerContextを1 つ作成します。外部パッケージの型は除外します(例:Azure.CoreのResponseError)。所有していない型はソース生成できません。外部の型は、以下の落とし穴 1 に従って別途処理します。
[JsonSerializerContext]
[JsonSerializable(typeof(ManagedServiceIdentity))]
[JsonSerializable(typeof(SystemData))]
// ... one attribute per type YOU OWN
// Do NOT add types from external packages (e.g., ResponseError)
internal partial class MyProjectJsonContext : JsonSerializerContext { }
-
すべての呼び出し箇所を一括更新する — ファイルを 1 つずつ読まないでください。次のパターンを機械的に適用します。
JsonSerializer.Serialize(obj)→JsonSerializer.Serialize(obj, MyProjectJsonContext.Default.TypeName)JsonSerializer.Deserialize<T>(json)→JsonSerializer.Deserialize(json, MyProjectJsonContext.Default.TypeName)
すべての呼び出し箇所を一度に見つけて更新します。
# Find all files with JsonSerializer calls grep -rl 'JsonSerializer\.\(Serialize\|Deserialize\)' src/ --include='*.cs'次に、
editの呼び出しを順番に使って、該当するすべてのファイルに同じ変換を適用します。C# コードにはsedを使用しないでください —Deserialize<T>()のようなジェネリックには山括弧や入れ子の丸括弧があり、sed で処理すると壊れてしまいます。 -
一度ビルドして確認します。残った警告はシリアル化以外の問題です。戦略 A~B または D で対処します。
戦略 D:[RequiresUnreferencedCode](最後の手段)
静的に記述できない任意のリフレクションがメソッドに本質的に必要な場合:
[RequiresUnreferencedCode("Loads plugins by name using Assembly.Load")]
public void LoadPlugin(string assemblyName) {
var asm = Assembly.Load(assemblyName);
// ...
}
これは呼び出し元にも伝播するため、呼び出し元にも [RequiresUnreferencedCode] のアノテーションが必要です。呼び出しチェーン全体がトリミング非対応になるため、使用は控えてください。
ステップ 5:再ビルドして繰り返す
少量の修正(警告 5~10 件)を適用するたびに、--no-incremental を付けて再ビルドし、新しい警告がないか確認します。再ビルドする前にすべての警告を修正しようとしないでください — 頻繁に再ビルドすることで、ミスを早期に発見し、連鎖する警告を明らかにできます。修正は連鎖します。内部メソッドにアノテーションを付けると、その呼び出し元で警告が発生することがあります。0 Warning(s) になるまで繰り返します。
ステップ 6:すべての TFM を検証する
すべてのターゲットフレームワークをビルドし、以下を確認します。
- net8.0 以降の TFM で IL 警告が 0 件
- NETSDK1210 警告がないこと(
IsAotCompatibleの条件で対処されます) - 古い TFM(netstandard2.0、net472 など)でクリーンビルドできること
dotnet build <project.csproj> # builds all TFMs
停止条件
- 警告パターンごとに、代表的なファイルを2~3個以上分析しないでください。 パターンに対する修正方法を特定したら、各ファイルを事前に確認せずに、該当するすべてのファイルに適用してください。
- 1回目のビルド後に修正を開始してください。 2回目の分析は行わず、手順3のトリアージ後、最も多い警告パターンの修正をすぐに実装してください。
- net8.0以降のTFMで IL警告が0件 になった時点で終了してください。すでに問題のない注釈を最適化したり、リファクタリングしたりしないでください。
- 注釈のフロー修正を超える アーキテクチャのリファクタリング が必要な警告(シリアル化レイヤー全体の置き換えなど)は、内容を記録して終了してください。大規模なサブシステムを書き換えないでください。
- 警告1件につき、ビルドと修正の反復は 最大3回 にしてください。3回試しても注釈のフローで解決しない場合は、
[RequiresUnreferencedCode]にエスカレーションしてください。 - 変更できない サードパーティの依存関係 の警告は追わないでください。記録して次に進んでください。
- ユーザーが対象を限定した質問(例:「このファイルの警告を修正してください」)をした場合は、プロジェクト全体に範囲を広げないでください。
古いTFM向けのポリフィル
netstandard2.0またはnet472を含むマルチターゲットプロジェクトでは、DynamicallyAccessedMembersAttribute と関連する型のポリフィルが必要です。references/polyfills.md を参照してください。
よくある落とし穴
- AOTセーフなシリアル化に対応していない外部型:変更できない依存関係の型(例:
Azure.CoreのResponseError)にソース生成シリアライザーがない場合、Options.GetConverter<T>()はリフレクションベースのため、IL警告が発生します。まず、その型がIJsonModel<T>を実装しているか確認してください(Azure SDKでは一般的です)。実装している場合は、JsonSerializerを完全に介さないようにします。
// Before (IL2026 — JsonSerializer uses reflection):
JsonSerializer.Serialize(writer, errorValue);
// After (AOT-safe — uses IJsonModel directly):
((IJsonModel<ResponseError>)errorValue).Write(writer, ModelReaderWriterOptions.Json);
// For deserialization:
var error = ((IJsonModel<ResponseError>)new ResponseError()).Create(ref reader, ModelReaderWriterOptions.Json);
外部型を JsonSerializerContext に追加しないでください。自分が所有していない型は、ソース生成の対象になりません。型が IJsonModel<T> を実装していない場合は、手動の Utf8JsonReader/Utf8JsonWriter ロジックを使ってカスタム JsonConverter<T> を作成し、コンテキストの [JsonSourceGenerationOptions] を介して登録してください。
-
シリアル化ライブラリ:リフレクションベースのシリアライザーの多く(例:
Newtonsoft.Json、XmlSerializer)はAOTに対応していません。JsonSerializerContextを使うSystem.Text.Jsonなど、ソース生成ベースのシリアライザーに移行してください。移行が難しい場合は、シリアル化の呼び出し箇所に[RequiresUnreferencedCode]を付けてください。 -
共有プロジェクト/projitems:
<Import>を介して複数のプロジェクトでソースを共有している場合、共有コードに追加した注釈は、使用するすべてのプロジェクトに影響します。すべての使用側プロジェクトが引き続き警告なくビルドできることを確認してください。
参考資料
制限事項 概念:トリミングを理解する ハウツー:トリミングとの互換性
チェックリスト
- TFM条件付きで
.csprojに<IsAotCompatible>を追加した - AOTアナライザーを有効にしてビルドした(net8.0以降のTFM)
- すべてのIL警告を注釈またはリファクタリングで修正した
- IL警告に対して
#pragma warning disableまたは[UnconditionalSuppressMessage]を使用していない - 必要に応じて、古いTFM向けのポリフィルを追加した
- すべてのターゲットフレームワークが警告0件でビルドできる
- 共有/リンクされたソースが関連プロジェクトに影響しないことを確認した