📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is NativeAOT in .NET, what does it give you and what are its restrictions?

Native ahead-of-time compilation: startup and memory gains, required trimming, reflection and dynamic code limits, ASP.NET Core support and analysers.

Senior Theory Advanced aot clr nativeaot startup trimming

🎯 Key points

  • NativeAOT produces a single native executable with no JIT and no IL at run time
  • Benefits: fast startup, lower memory, no runtime installation
  • Trimming is mandatory and reflection is limited to what the compiler can see
  • Reflection.Emit and loading new assemblies at run time are not supported
  • Use source generators such as JsonSerializerContext; fix IL2026 and IL3050 warnings
  • ASP.NET Core supports minimal APIs and gRPC with AOT, not MVC or Razor Pages
  • No Dynamic PGO; benchmark steady state as well as startup

⏱ 30-second answer

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.

🗣 Full answer (senior)

How I would answer this in an interview

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.

1. How to use it

<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.

2. What it gives you

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.

3. Restrictions

  • No dynamic code generation: System.Reflection.Emit, DynamicMethod, runtime proxies (Castle/Moq-style), Expression.Compile() falls back to a slow interpreter.
  • No dynamic assembly loading: Assembly.LoadFrom, plugins, AssemblyLoadContext loading new code — not supported.
  • Trimming is mandatory: reflection over members not statically referenced may find nothing unless annotated ([DynamicallyAccessedMembers], [DynamicDependency], rd.xml-style descriptors).
  • Generic instantiations must be known: MakeGenericType/MakeGenericMethod with value types not seen at compile time fail.
  • Reflection-based libraries: classic System.Text.Json reflection mode, Newtonsoft.Json, AutoMapper, many DI scanning patterns, older ORMs — need source-generated alternatives.
  • EF Core: AOT support is improving (precompiled queries/compiled models in EF Core 9/10) but has limitations.
  • Platform-specific builds; cross-compilation has constraints (e.g. building Linux from Windows needs containers/toolchains).
  • No dynamic PGO, so steady-state throughput can be lower than a JIT app for some workloads.
  • Debugging/diagnostics: different tooling; some dotnet-* tools have limited support; native debuggers work.
  • Some APIs throw PlatformNotSupportedException (e.g. Assembly.Location returns empty, COM limitations).

4. Making an app AOT-compatible

  1. Enable PublishAot (or IsAotCompatible for libraries) and fix all warnings.
  2. Replace reflection-based components with source generators: JsonSerializerContext, [LoggerMessage], [GeneratedRegex], configuration binding generator, Request Delegate Generator (minimal APIs), [LibraryImport].
  3. Avoid assembly scanning; register services explicitly.
  4. Annotate necessary reflection with [DynamicallyAccessedMembers].
  5. Test the published binary (behaviour can differ from dotnet run).
  6. Measure start-up, memory, size and throughput vs JIT/R2R.

5. Choosing

  • NativeAOT when start-up/memory/size are critical and dependencies are AOT-friendly.
  • ReadyToRun when you need faster start-up but full compatibility.
  • JIT when steady-state throughput and flexibility matter most.

6. A production example

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.

🔍 What the interviewer is checking

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.

📚 Learn in depth

1. How NativeAOT builds

  1. Roslyn compiles C# to IL as usual.
  2. ILLink-style trimming analysis and the ILCompiler (based on the crossgen2/RyuJIT backend) perform whole-program analysis starting from roots (entry point, annotated members), determining reachable types/methods/generic instantiations.
  3. The compiler generates native code for everything reachable, plus runtime data structures (MethodTables, GC info, reflection metadata for kept members).
  4. The platform linker links with the NativeAOT runtime (Runtime.lib/libRuntime.a) to produce an executable.

2. The runtime in NativeAOT

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.

3. Reflection in AOT

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.

4. Size optimisation options

  • 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.

5. Performance characteristics

  • Start-up: excellent.
  • Steady state: often comparable to JIT; sometimes slower due to no dynamic PGO and conservative devirtualisation, sometimes faster due to whole-program optimisations.
  • Memory: lower baseline.
  • GC behaviour: same GC options apply (Server/Workstation, DATAS).

Try it yourself

  1. Create dotnet new webapiaot, publish, and measure start-up and size.
  2. Add Newtonsoft.Json usage and read the warnings.
  3. Replace reflection JSON with a JsonSerializerContext.
  4. Compare steady-state throughput (k6/NBomber) between JIT and AOT builds.

💻 Code

<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;

⚠ Common mistakes

1. Ignoring AOT warnings

What happens: publishing with trim/AOT warnings unresolved. Why it is wrong: runtime failures in production. Do instead: treat AOT warnings as errors.

2. Testing only with dotnet run

What 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.

3. Using reflection-heavy libraries

What happens: AutoMapper, Newtonsoft.Json or assembly scanning in AOT apps. Why it is wrong: incompatible or slow fallbacks. Do instead: source-generated alternatives.

4. Expecting plugins to work

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.

5. Assuming AOT is always faster

What happens: switching for throughput gains. Why it is wrong: no dynamic PGO; steady state may be slower. Do instead: benchmark steady-state workloads.

6. Forgetting platform-specific builds

What happens: expecting one binary for all platforms. Why it is wrong: AOT output is per RID. Do instead: build per target in CI.

7. Dropping globalisation unintentionally

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.

8. Over-annotating to silence warnings

What happens: suppressing warnings with blanket UnconditionalSuppressMessage. Why it is wrong: hides real runtime failures. Do instead: fix root causes; suppress only with proof.

🏛 Senior insight: trade-offs

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.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate says trimming is mandatory for AOT Why: Checks how to make code trim-safe
    How to answer

    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>.

  2. When: Candidate mentions apps that cannot use AOT Why: Checks the ReadyToRun alternative
    How to answer

    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.

  3. When: Candidate recommends source-generated JSON Why: Probes how source generators replace reflection
    How to answer

    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.

🛠 Real-world problems: how you handle them

Scale-to-zero webhook receiver moved to NativeAOT
Situation
A webhook receiver on [Azure Container Apps] scaled to zero overnight and cold-started on each partner burst.
Signals
Partners retried because first responses took [about 1 s]; memory per replica was [about 120 MB].
Investigation
Enabled `PublishAot` in a branch, listed the warnings and found two from a reflection-based mapper.
Resolution
Replaced the mapper with hand-written mapping, added a source-generated JSON context, and published with NativeAOT.
Prevention
CI builds with `IsAotCompatible` analysers and fails on any AOT warning.

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.

↩ Usually asked after

More in CLR Fundamentals

Last updated 31 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me