mcpField
N
native-aot

native-aot

github.com/codewithmukesh开发807153 次安装2 天前更新
代码仓库

关于 native-aot

面向 .NET 开发者的 Native AOT、裁剪与单文件发布指南,介绍兼容性权衡、源生成替代反射的做法,以及如何在 CI 中验证发布产物。

主要功能

  1. 01配置 Native AOT 与单文件发布
  2. 02使用源生成 JSON 和正则
  3. 03识别并处理裁剪与 AOT 警告
  4. 04在 CI 中测试发布后的二进制文件
  5. 05按应用类型评估 AOT 适用性

适用场景

  1. 01为 CLI 工具构建独立发布产物
  2. 02降低无服务器应用冷启动开销
  3. 03检查依赖项的裁剪兼容性
  4. 04为 AOT 兼容库设置质量门槛

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill native-aot

安装后在 Claude Code 中输入/native-aot确认已加载。

详细介绍

原生 AOT、裁剪与单文件(.NET 10)

核心原则

  1. 尽早决定 — AOT 会限制库的选择(不能使用运行时反射或代码生成)。将 AOT 加入一个成熟且大量使用反射的应用,等同于重写。从第一天起就使用 AOT,几乎不需要付出额外成本。
  2. 裁剪/AOT 警告预示着未来的运行时崩溃 — IL2xxx(裁剪)和 IL3xxx(AOT/单文件)警告表示代码可能会抛出 MissingMethodException,或在发布的二进制文件中悄然出现异常行为。应将它们视为错误,并确保数量为零。
  3. 源生成器取代反射 — JSON、正则表达式、日志记录、配置绑定、选项验证和 MVVM 都有对应的生成器。它们能让 AOT 路径保持高效且可分析。
  4. 测试发布产物,而不是 dotnet run — dotnet run 使用 JIT 和完整元数据,因此会掩盖所有裁剪问题。应在 CI 中对 dotnet publish 生成的二进制文件进行冒烟测试。
  5. 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.EmitAOT 不支持;请使用 JIT
单个可执行文件下载,依赖项与 AOT 不兼容使用 PublishSingleFile + SelfContained(JIT)
面向 AOT 使用者的库使用 IsAotCompatible + 零警告,并以 net8.0+ 为目标进行多目标编译
依赖项会产生修剪警告替换它,或通过 [RequiresUnreferencedCode] 隔离;不要一概抑制警告

评论

支持 Markdown