Native ahead-of-time compilation: startup and memory gains, required trimming, reflection and dynamic code limits, ASP.NET Core support and analysers.
NativeAOT (<PublishAot>true</PublishAot>) compiles the whole app, with the parts of the runtime it needs, into one native executable with no JIT and no IL at run time. You get very fast startup, lower memory and a smaller deployment with no .NET runtime to install. The cost is a closed world: trimming is mandatory, Reflection.Emit and loading new assemblies at run time are not available, reflection works only on what the compiler can see, and libraries must be AOT-compatible. ASP.NET Core supports it for minimal APIs and gRPC but not MVC or Razor Pages. Fix the IL2xxx and IL3xxx warnings before shipping.
NativeAOT compiles a .NET application ahead of time into a single native executable for a specific OS/CPU, including only the code that's reachable (trimmed) and a small runtime (GC, type system, exception handling, threading) — but no JIT. You get very fast start-up, lower memory and self-contained deployment; in exchange, anything that needs runtime code generation or unbounded reflection is restricted.
<PropertyGroup>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization> <!-- optional: smaller, if culture data isn't needed -->
</PropertyGroup>
dotnet publish -c Release -r linux-x64
Setting PublishAot in the project also enables AOT/trim analysers during build, so incompatibilities show up as warnings (IL2xxx, IL3xxx) early. ASP.NET Core has an AOT-friendly template (dotnet new webapiaot) using CreateSlimBuilder and minimal APIs.
| Benefit | Detail |
|---|---|
| Start-up | Typically tens of milliseconds (no JIT, no assembly loading/IL parsing) |
| Memory | Lower working set (no JIT, less metadata, trimmed code) |
| Deployment | Single self-contained native file; no .NET runtime install needed |
| Size | Small for trimmed apps (a minimal API can be ~10–20 MB) |
| Predictable performance | No warm-up/tiering; good for short-lived processes |
| Platforms | Native binaries for Linux, Windows, macOS (x64, Arm64), plus native libraries callable from C/C++ |
Great for: serverless/scale-to-zero workloads, CLI tools, sidecars, containers with tight memory, microservices with simple dependencies.
System.Reflection.Emit, DynamicMethod, runtime proxies (Castle/Moq-style), Expression.Compile() falls back to a slow interpreter.Assembly.LoadFrom, plugins, AssemblyLoadContext loading new code — not supported.[DynamicallyAccessedMembers], [DynamicDependency], rd.xml-style descriptors).MakeGenericType/MakeGenericMethod with value types not seen at compile time fail.System.Text.Json reflection mode, Newtonsoft.Json, AutoMapper, many DI scanning patterns, older ORMs — need source-generated alternatives.dotnet-* tools have limited support; native debuggers work.PlatformNotSupportedException (e.g. Assembly.Location returns empty, COM limitations).PublishAot (or IsAotCompatible for libraries) and fix all warnings.JsonSerializerContext, [LoggerMessage], [GeneratedRegex], configuration binding generator, Request Delegate Generator (minimal APIs), [LibraryImport].[DynamicallyAccessedMembers].dotnet run).A webhook relay running on Azure Container Apps with scale-to-zero had ~2 s cold starts, causing provider retries. Its dependencies were minimal (minimal APIs, HttpClient, Service Bus SDK), so we moved it to NativeAOT: replaced reflection JSON with a JsonSerializerContext, enabled RDG, fixed a handful of trim warnings in the Service Bus configuration code. Cold start dropped to ~150 ms, memory per replica from ~120 MB to ~35 MB, and the image shrank substantially. A heavier API with EF Core reporting stayed on JIT + R2R because its AOT warnings were not practical to resolve.
The interviewer checks you understand both the benefits and the closed-world constraints.
Green flags: no JIT, mandatory trimming, reflection and dynamic code limits, source-generated JSON, IL2026 and IL3050, ASP.NET Core support boundaries, compare with R2R.
Red flags: "just set PublishAot and it works"; ignores warnings; recommends it for plugin hosts.
Runtime.lib/libRuntime.a) to produce an executable.Includes the same GC as CoreCLR, a minimal type system, exception handling, threading, and a reflection implementation over precomputed metadata (only for kept members). No JIT, no IL interpreter for normal methods.
Reflection works for members that the compiler kept. The trimming annotations (DynamicallyAccessedMembers, RequiresUnreferencedCode, RequiresDynamicCode) propagate requirements through the call graph so warnings point at problematic call sites. Libraries mark themselves IsAotCompatible to run the analysers.
InvariantGlobalization=true (no ICU data).OptimizationPreference=Size or Speed.StackTraceSupport=false, UseSystemResourceKeys=true (smaller, less descriptive errors).IlcGenerateMstatFile + tools like sizoscope to analyse what contributes to size.dotnet new webapiaot, publish, and measure start-up and size.JsonSerializerContext.<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
using System.Text.Json.Serialization;
var builder = WebApplication.CreateSlimBuilder(args);
// Source-generated JSON: no reflection at run time
builder.Services.ConfigureHttpJsonOptions(o =>
o.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default));
var app = builder.Build();
app.MapGet("/orders/{id:int}", (int id) => new Order(id, "Pending"));
app.MapPost("/orders", (Order order) => Results.Created($"/orders/{order.Id}", order));
app.Run();
public record Order(int Id, string Status);
[JsonSerializable(typeof(Order))]
internal partial class AppJsonContext : JsonSerializerContext;
What happens: publishing with trim/AOT warnings unresolved. Why it is wrong: runtime failures in production. Do instead: treat AOT warnings as errors.
dotnet runWhat happens: behaviour verified in JIT mode only. Why it is wrong: AOT binaries can behave differently (trimmed members, reflection). Do instead: test the published native binary.
What happens: AutoMapper, Newtonsoft.Json or assembly scanning in AOT apps. Why it is wrong: incompatible or slow fallbacks. Do instead: source-generated alternatives.
What happens: loading assemblies at run time. Why it is wrong: not supported in NativeAOT. Do instead: compile features in, or use out-of-process plugins.
What happens: switching for throughput gains. Why it is wrong: no dynamic PGO; steady state may be slower. Do instead: benchmark steady-state workloads.
What happens: expecting one binary for all platforms. Why it is wrong: AOT output is per RID. Do instead: build per target in CI.
What happens: InvariantGlobalization breaks culture-specific formatting.
Why it is wrong: wrong dates/currency for users.
Do instead: enable only if culture data isn't needed.
What happens: suppressing warnings with blanket UnconditionalSuppressMessage.
Why it is wrong: hides real runtime failures.
Do instead: fix root causes; suppress only with proof.
NativeAOT is an architectural decision because it restricts which libraries and patterns you can use.
| Good fit | Poor fit |
|---|---|
| CLI tools and sidecars | Apps with plugin models |
| Scale-to-zero APIs and functions | MVC or Razor Pages apps |
| Small minimal-API microservices | Heavy use of reflection-based libraries |
| Memory-dense container hosting | Teams unable to keep warnings at zero |
A senior plan starts with the analysers on in a branch, counts warnings by dependency, and decides from that evidence.
Trimming (<PublishTrimmed>true</PublishTrimmed>, self-contained only, and always on for NativeAOT) runs the ILLink trimmer at publish to remove types and members that static analysis proves are unreachable, shrinking the app. Reflection is its enemy: code that finds types or members by string at run time may lose them. The trimmer reports this with warnings such as IL2026 (RequiresUnreferencedCode) and IL2070-series (missing DynamicallyAccessedMembers). You make code trim-safe by using source generators instead of reflection, annotating Type parameters with [DynamicallyAccessedMembers], and marking unavoidable reflection with [RequiresUnreferencedCode]. Libraries declare <IsTrimmable>true</IsTrimmable>.
ReadyToRun is ahead-of-time compilation that keeps the IL and adds precompiled native code to each assembly. Publish with <PublishReadyToRun>true</PublishReadyToRun> and a runtime identifier. At startup the runtime uses the native code instead of JIT-compiling, which cuts startup time; hot methods are still rejitted at Tier 1 so steady-state performance is kept. Costs: assemblies are larger (often two to three times) and R2R code is a little less optimised because it must stay valid across versions of dependencies outside its version bubble. Use it for services where cold start matters; it is still a JIT-based app, unlike NativeAOT.
A source generator is a compiler plug-in that reads your code (usually a marker attribute on a partial declaration) and adds new C# files to the compilation; it can never change or delete your code. .NET ships several: System.Text.Json ([JsonSerializable] on a JsonSerializerContext) for reflection-free serialisation, [GeneratedRegex] for regex compiled to C# at build time, [LoggerMessage] for allocation-free structured logging, [LibraryImport] for P/Invoke marshalling, and in ASP.NET Core the configuration binding and Request Delegate generators that replace reflection in Bind/Get<T> and minimal APIs. They improve startup, cut allocations and make code trim and Native AOT safe. Inspect output under Dependencies, Analyzers in the IDE, or set EmitCompilerGeneratedFiles to write the files to obj.
How to tell it (STAR)
Our [webhook receiver] cold-started slowly and partners retried. I trialled NativeAOT in a branch, fixed [two] reflection warnings and switched to source-generated JSON. Cold start went from [about 1 s] to [under 100 ms] and memory roughly halved.
Last updated 31 Oct 2026.