---
name: native-aot
description: >
Native AOT, trimming, and single-file publishing for .NET 10: PublishAot,
IsAotCompatible analyzers, source-generated JSON/regex/logging/config binding,
fixing IL2xxx/IL3xxx trim and AOT warnings, per-RID publishing, and when NOT
to use AOT. Load when the user mentions "Native AOT", "PublishAot", "trimming",
"PublishTrimmed", "single-file", "IL2026", "IL3050", "trim warning",
"startup time", "self-contained", or "JsonSerializerContext".
---
# Native AOT, Trimming & Single-File (.NET 10)
## Core Principles
1. **Decide early** — AOT constrains library choice (no runtime reflection or code generation). Adding it to a mature reflection-heavy app is a rewrite. Adding it on day one costs almost nothing.
2. **A trim/AOT warning is a future runtime crash** — IL2xxx (trimming) and IL3xxx (AOT/single-file) warnings mark code that may throw `MissingMethodException` or silently misbehave in the published binary. Treat them as errors and keep the count at zero.
3. **Source generators replace reflection** — JSON, regex, logging, configuration binding, options validation, and MVVM all have generators. They keep the AOT path fast and analyzable.
4. **Test the published artifact, not `dotnet run`** — `dotnet run` uses the JIT and full metadata, so it hides every trimming bug. Smoke-test the binary that `dotnet publish` produces, in CI.
5. **AOT is a trade, not a free upgrade** — You get fast startup, small memory, and no runtime install. You lose dynamic loading, `Reflection.Emit`, and some steady-state throughput from Dynamic PGO. Pick it where those trade-offs pay.
## Patterns
### Project Setup
```xml
net10.0
true
true
Size
true
true
```
```bash
dotnet publish -c Release -r linux-x64 # one RID per publish
```
**Why:** putting `PublishAot` in the project file (not only on the command line) runs the analyzers on every build, so warnings appear while you code. `IsAotCompatible` on libraries surfaces problems at the source instead of in the app. Native AOT needs the platform toolchain (VS "Desktop development with C++" on Windows, `clang` + `zlib` dev packages on Linux, Xcode CLI tools on macOS). Build each OS on that OS in a CI matrix. A Linux binary runs only on the same or newer distro version it was built on.
### Source-Generated JSON
```csharp
public sealed record OrderDto(Guid Id, string Customer, decimal Total, DateTimeOffset CreatedAt);
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(OrderDto))]
[JsonSerializable(typeof(List))]
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);
```
```csharp
// 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));
```
**Why:** reflection-based `System.Text.Json` is disabled in trimmed/AOT apps. The generated metadata is also faster on the JIT. Register every type that crosses the wire, including collection shapes (`List`). `CreateSlimBuilder` leaves out non-AOT-friendly defaults. The minimal API Request Delegate Generator turns on automatically with `PublishAot`.
### Generated Regex, Logging, Config Binding, Options Validation
```csharp
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;
public sealed class ImportOptions
{
[Range(1, 10_000)] public int BatchSize { get; set; } = 500;
}
```
```xml
true
```
**Why:** `[GeneratedRegex]` emits matching code at compile time (no `RegexOptions.Compiled`, which needs runtime IL emit). `[LoggerMessage]` avoids boxing and parsing the template on every call. The configuration binding generator replaces the reflection inside `Bind`/`Get`/`Configure`. `[OptionsValidator]` replaces reflection-based data-annotation validation.
### Fixing Trim Warnings Honestly
```csharp
// 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)!;
```
**Why:** `[RequiresUnreferencedCode]`/`[RequiresDynamicCode]` move the warning to the call site, where a caller can choose a safe path. `[DynamicallyAccessedMembers]` lets the trimmer keep exactly the members reflection needs. Use `[UnconditionalSuppressMessage]` only when you can explain in the justification why the code is provably safe.
### Single-File Without AOT
```xml
true
true
true
true
```
```csharp
// BAD in single-file: Assembly.Location is empty (IL3000)
var dir = Path.GetDirectoryName(typeof(Program).Assembly.Location);
// GOOD
var dir = AppContext.BaseDirectory;
```
**Why:** single-file gives one-exe distribution while keeping the JIT and full reflection. Use it when a dependency isn't AOT-compatible but you still want an easy download. File-path APIs are the usual breakage.
### Verifying in CI
```yaml
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
```
**Why:** only the published binary proves trimming kept what you need. A smoke command on each OS catches missing metadata, native library, and globalization problems before release.
## Anti-patterns
### Suppressing Warnings to Get a Green Build
```xml
$(NoWarn);IL2026;IL3050
partial
```
```csharp
// GOOD — fix the cause: generated serializer instead of reflection
JsonSerializer.Serialize(order, AppJsonContext.Default.OrderDto);
```
### Reflection-Based Serialization Overloads
```csharp
// BAD — IL2026/IL3050: needs runtime reflection over OrderDto
var json = JsonSerializer.Serialize(order);
var dto = await response.Content.ReadFromJsonAsync(ct);
// GOOD — JsonTypeInfo overloads
var json = JsonSerializer.Serialize(order, AppJsonContext.Default.OrderDto);
var dto = await response.Content.ReadFromJsonAsync(AppJsonContext.Default.OrderDto, ct);
```
### Convention-Based Assembly Scanning
```csharp
// 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();
services.AddTransient();
```
### `RegexOptions.Compiled` and Runtime Expression Compilation
```csharp
// 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; }
```
## Decision Guide
| Scenario | Recommendation |
|----------|---------------|
| CLI tool shipped to non-.NET users | Native AOT per RID |
| `dotnet run app.cs` script published | Native AOT is already the default for file-based apps |
| Serverless / scale-to-zero / many small containers | Native AOT (fast cold start, low memory); see `container-publish` for `-aot` base images |
| Avalonia desktop app | Native AOT viable with compiled bindings + trim-safe view mapping |
| WPF or WinForms | No trimming/AOT support; use ReadyToRun + single-file for startup |
| Long-running, high-throughput API | Usually JIT (Dynamic PGO wins at steady state); measure before switching |
| EF Core data access | EF Core AOT (precompiled queries) is still experimental; stay JIT for EF-heavy apps |
| MVC controllers, SignalR-heavy, Blazor Server | Not AOT targets; use minimal APIs if AOT is required |
| Plugins / `Assembly.LoadFrom` / `Reflection.Emit` | Not possible under AOT; use JIT |
| One-exe download, AOT-incompatible deps | `PublishSingleFile` + `SelfContained` (JIT) |
| Library meant for AOT consumers | `IsAotCompatible` + zero warnings, multi-target `net8.0`+ |
| Dependency emits trim warnings | Replace it, or isolate behind `[RequiresUnreferencedCode]`; don't blanket-suppress |