关于 native-aot
面向 .NET 开发者的 Native AOT、裁剪与单文件发布指南,介绍兼容性权衡、源生成替代反射的做法,以及如何在 CI 中验证发布产物。
主要功能
- 01配置 Native AOT 与单文件发布
- 02使用源生成 JSON 和正则
- 03识别并处理裁剪与 AOT 警告
- 04在 CI 中测试发布后的二进制文件
- 05按应用类型评估 AOT 适用性
适用场景
- 01为 CLI 工具构建独立发布产物
- 02降低无服务器应用冷启动开销
- 03检查依赖项的裁剪兼容性
- 04为 AOT 兼容库设置质量门槛
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ codewithmukesh/ dotnet-claude-kit --skill native-aot安装后在 Claude Code 中输入/native-aot确认已加载。
详细介绍
原生 AOT、裁剪与单文件(.NET 10)
核心原则
- 尽早决定 — AOT 会限制库的选择(不能使用运行时反射或代码生成)。将 AOT 加入一个成熟且大量使用反射的应用,等同于重写。从第一天起就使用 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,可以在问题源头而非应用中发现问题。原生 AOT 需要平台工具链(Windows 上的 VS“使用 C++ 的桌面开发”,Linux 上的 clang + zlib 开发包,macOS 上的 Xcode 命令行工具)。应在 CI 矩阵中让每个操作系统都在其自身环境下构建。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));
**原因:**在经过裁剪或启用 AOT 的应用中,基于反射的 System.Text.Json 已禁用。生成的元数据在 JIT 下也更快。应注册所有在线路上传递的类型,包括集合类型(List<OrderDto>)。CreateSlimBuilder 会省略不兼容 AOT 的默认项。启用 PublishAot 后,最小 API 请求委托生成器会自动启用。
生成的正则表达式、日志记录、配置绑定与选项验证
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
**原因:**只有已发布的二进制文件才能证明裁剪器保留了所需内容。在每个操作系统上运行冒烟命令,可在发布前发现缺失的元数据、原生库和全球化问题。
反模式
为了构建成功而抑制警告
<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 |
| 单个可执行文件下载,依赖项与 AOT 不兼容 | 使用 PublishSingleFile + SelfContained(JIT) |
| 面向 AOT 使用者的库 | 使用 IsAotCompatible + 零警告,并以 net8.0+ 为目标进行多目标编译 |
| 依赖项会产生修剪警告 | 替换它,或通过 [RequiresUnreferencedCode] 隔离;不要一概抑制警告 |