Native AOT moved from an experimental curiosity to a mainstream deployment option, and interviewers now expect senior .NET engineers to know not just how to flip PublishAot to true, but what it actually costs in flexibility and where those costs show up. This is a topic that rewards hands-on experience: candidates who've only read the marketing bullet points ("faster startup, smaller footprint") struggle the moment an interviewer asks why a library that works fine under JIT throws at runtime once trimmed, or what a RequiresDynamicCode warning actually means for their code. For engineers with a decade or more of experience, this is also a judgment question — knowing when not to reach for Native AOT is as important as knowing how to use it. The ten questions below cover the compilation model, trimming mechanics, library authoring, and the operational trade-offs of shipping ahead-of-time-compiled .NET.

Q1 How does Native AOT compilation actually work?#

Short answer: At publish time, the IL compiler (ILC) performs whole-program static analysis starting from your app's entry point, determines everything that's actually reachable, and compiles that IL directly to native machine code for the target platform, producing a single self-contained native executable with no JIT and no separate runtime installation required at run time.

XML
<PropertyGroup>
  <PublishAot>true</PublishAot>
</PropertyGroup>
Bash
dotnet publish -r linux-x64 -c Release

This is fundamentally different from both ordinary JIT compilation and ReadyToRun. Ordinary JIT compiles each method the first time it's called, at run time, using whatever the CLR discovers about the program as it executes — which means the compiler can be conservative and still correct, because it always has the full, dynamic picture available. ReadyToRun precompiles IL to native code ahead of time too, but as an optimization layered on top of a still-fully-present JIT and runtime: if a ReadyToRun-compiled method turns out to need something the ahead-of-time pass couldn't resolve, the JIT is still there as a fallback. Native AOT removes that fallback entirely — there is no JIT in the published app, so ILC has to prove, at publish time, that everything the program could possibly need is either compiled in or explicitly and correctly excluded. That's why Native AOT requires full trimming: the compiler needs a closed, statically known universe of reachable code, and anything it can't prove is reachable through static analysis (a type loaded by name from a string, a method invoked purely through late-bound reflection) is either flagged as a warning or simply isn't there at run time.

What interviewers look for: a clear distinction between Native AOT, JIT and ReadyToRun specifically — many candidates conflate AOT with ReadyToRun, and interviewers use this question to filter for that mistake.

Q2 What are the hard limitations of Native AOT compared to JIT-compiled .NET?#

Short answer: Native AOT prohibits anything that depends on generating code at run time or loading unknown assemblies dynamically — no System.Reflection.Emit, no Assembly.LoadFile/late-bound plugin loading, the dynamic keyword doesn't work because its call-site binder relies on runtime code generation, LINQ Expression.Compile() falls back to a slower interpreter instead of emitting IL, and C++/CLI and COM interop are unsupported on Windows.

LimitationWhyPractical impact
No Reflection.EmitNo JIT present to run emitted ILDynamic proxy libraries, some serializers and ORMs that emit code at run time won't work as-is
No dynamic assembly loadingILC must know the whole reachable graph at publish timeTrue plugin architectures (load an arbitrary DLL at run time) aren't supported
dynamic keyword unsupportedThe DLR binder generates call sites via Reflection.EmitCode using dynamic for COM or loosely-typed interop needs rewriting
Expression.Compile() interpreted onlyNo IL emission availableExpression-tree-heavy code (some LINQ providers, mapping libraries) runs slower, not faster
Every generic instantiation is separately compiledNo shared generic code generation at run timeHeavy generic use across many value-type arguments increases binary size

The common thread is that Native AOT trades runtime flexibility for compile-time certainty. Anything that says "figure this out while the program is running" — load this DLL I don't know about yet, generate this proxy type, interpret this expression tree as IL — is exactly what a fully ahead-of-time model can't support, because by definition nothing else can be compiled once the app is running. Most modern BCL and first-party libraries have been updated to avoid these patterns internally or provide source-generator-based alternatives, but third-party and internal libraries written before AOT was a priority often haven't been, which is the single biggest practical blocker teams hit.

What interviewers look for: the ability to explain why each limitation exists (not generatable at run time, full closed-world analysis required) rather than a memorized list, and awareness that dynamic specifically is affected, which surprises many candidates.

Q3 What is trim analysis, and what do warnings like the IL2xxx and IL3xxx families mean?#

Short answer: Trim analysis is the static analysis pass that determines which code is reachable, so unreferenced code can be removed; warnings in the IL2xxx range flag places where the analyzer can't prove a reflection-based pattern is safe once unused code is trimmed away, while warnings in the IL3xxx range specifically flag code that requires runtime code generation and therefore breaks under Native AOT even if it's otherwise trim-safe.

C#
[RequiresUnreferencedCode("Uses reflection to enumerate members; incompatible with trimming.")]
public static object CreateInstance(string typeName) => Activator.CreateInstance(Type.GetType(typeName)!)!;

public static T Create<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>()
    where T : new()
    => new T();

The distinction between the two attributes above maps directly onto the two warning families. Code that genuinely can't be analyzed — loading a type by an arbitrary runtime string, walking members discovered purely at run time — gets marked [RequiresUnreferencedCode], which propagates the warning to every caller so they know they're calling into code the trimmer can't verify. Code that uses reflection against a type known at compile time — a generic method whose type parameter's constructors need to survive trimming — gets [DynamicallyAccessedMembers] instead, telling the trimmer exactly which member kinds to preserve for that type, which turns an unverifiable pattern into a verifiable one as long as every caller supplies a type satisfying the annotation. RequiresDynamicCode is the AOT-specific cousin: it flags code that would trim fine but still needs runtime code generation, such as constructing a generic type or reflecting over types only known at run time. Fixing a warning means either restructuring the code to be statically analyzable, usually via a source generator, or explicitly annotating genuinely dynamic code so the warning propagates honestly instead of failing silently at run time. Suppressing with [UnconditionalSuppressMessage] should be reserved for cases you've manually verified are safe through some mechanism the analyzer can't see.

What interviewers look for: correct understanding that trimming warnings (IL2xxx) and AOT warnings (IL3xxx, RequiresDynamicCode) are related but distinct — code can be fully trim-safe and still break under Native AOT specifically because it needs runtime codegen.

Q4 What do you need to do to make a widely used NuGet library AOT- and trim-compatible?#

Short answer: Enable trim and AOT analysis for the project (IsAotCompatible or IsTrimmable), fix every warning the analyzer surfaces rather than suppressing them, replace reflection-based serialization or configuration binding with source generators, explicitly annotate any genuinely unavoidable reflection-based API with RequiresUnreferencedCode/RequiresDynamicCode so it fails loudly for AOT consumers instead of silently misbehaving, and add a trimmed, AOT-published smoke test to CI so regressions are caught automatically rather than reported by users.

XML
<PropertyGroup>
  <IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>

Setting IsAotCompatible turns on both trim and AOT analyzers for that project, which is the fastest way to get a real inventory of problems — expect the first run on an older library to surface dozens of warnings, many clustered around a handful of root causes (a generic reflection helper used everywhere, a JSON serializer relying on runtime type discovery). Work through them by category rather than one by one: switching to System.Text.Json source generation typically clears a large batch of serialization-related warnings at once, and auditing every Type.GetType(string) or Activator .CreateInstance call for whether the type is actually known at compile time clears another batch. For the warnings that remain because the pattern is genuinely dynamic (a true plugin system, for instance), annotate honestly rather than suppress — an honest RequiresUnreferencedCode warning that propagates to your library's own consumers is far more useful to them than a suppressed warning that turns into a runtime NotSupportedException in their production AOT build. Finally, publish a minimal AOT console app in CI that exercises the library's main code paths — trim warnings only tell you what might break; an actual AOT-published smoke test tells you what does break, and catches regressions future contributors would otherwise introduce unknowingly.

What interviewers look for: a systematic process (enable analysis, fix by root cause, annotate honestly, verify with a real AOT smoke test) rather than "add the trimming attributes," and awareness that suppressing warnings without verification just moves the failure from build time to a customer's production incident.

Q5 How do source generators let code avoid reflection, and why does that matter for AOT?#

Short answer: A source generator runs at compile time and emits ordinary C# code — a JsonSerializerContext with generated serialization methods, a regex's matching state machine, strongly typed configuration binding code — so the same work that reflection would otherwise do by inspecting types at run time is done once by the compiler instead, producing code the AOT compiler can see, analyze and compile ahead of time just like any other method you wrote by hand.

C#
[JsonSerializable(typeof(Order))]
internal partial class AppJsonContext : JsonSerializerContext { }

var order = JsonSerializer.Deserialize(json, AppJsonContext.Default.Order);
C#
[GeneratedRegex(@"^\d{3}-\d{2}-\d{4}$")]
private static partial Regex SsnPattern();

This matters for AOT specifically because reflection-based serialization has to discover a type's properties, constructors and attributes at run time using System.Reflection, and building an efficient serializer for that type traditionally meant emitting IL on the fly with Reflection.Emit — exactly the capability Native AOT doesn't have. A source generator sidesteps the problem entirely: instead of discovering "what does this type look like" at run time, the generator already knows at compile time, because it's running against your actual source code, and it emits plain, ahead-of-time-compilable C# that reads and writes Order directly with no reflection involved at all. The same pattern applies to the configuration binding source generator (turns IConfiguration.Bind<T>() into generated binding code) and to regular expressions ([GeneratedRegex] emits a hand-optimized-equivalent state machine instead of building one at run time via RegexOptions.Compiled). The broader lesson for AOT-readiness: wherever a library's hot path relies on "inspect the type, then act," the AOT-friendly fix is usually "generate the code that acts, at compile time," not "make reflection faster."

What interviewers look for: understanding the mechanism (compile-time code emission replacing run-time type discovery), not just "source generators are faster," plus a concrete example beyond JSON serialization.

Q6 When is Native AOT the wrong choice for a project?#

Short answer: Avoid Native AOT when the app genuinely needs a plugin architecture that loads unknown assemblies at run time, when its dependency graph includes libraries that are heavily reflection-based and haven't been updated for trimming or AOT, when the team can't absorb the added build complexity of publishing per-platform native binaries instead of one portable framework-dependent build, or when startup latency and memory footprint simply aren't problems worth the trade-off for that workload.

Concrete scenarios where sticking with JIT or ReadyToRun is the right call: a modular application whose entire value proposition is loading third-party or user-provided plugins at run time — that's precisely the dynamic-loading capability AOT removes; an app built on a mature framework or ORM whose reflection- heavy internals haven't been fully ported to AOT-compatible patterns, where fighting the trimmer would cost more engineering time than the startup win is worth; a long-running server process where the JIT's warm-up cost is a one-time, amortized expense against days or weeks of uptime, making startup latency largely irrelevant compared to steady-state throughput, where tiered compilation and profile-guided optimization can actually out-perform AOT's static compilation over time; and any team early in migrating a large codebase, where the fastest path to running on modern .NET at all is a framework-dependent or ReadyToRun deployment, with Native AOT treated as a later optimization once the trim/AOT warning backlog is small enough to be worth clearing. The build and CI cost is also a real, often underestimated factor: Native AOT requires publishing a separate native binary per target OS/CPU combination instead of one portable build, which multiplies CI matrix time and artifact count for teams supporting many platforms.

What interviewers look for: balanced judgment rather than treating AOT as strictly superior — naming the plugin-loading and reflection-heavy-dependency cases specifically is a strong signal of real experience trying to adopt it.

Q7 What levers do you have to reduce Native AOT binary size and cut cold-start latency further?#

Short answer: Beyond the trimming AOT already requires, the biggest additional levers are disabling globalization support you don't need (InvariantGlobalization), turning off diagnostic features you don't use in production (event source, stack trace metadata, HTTP activity propagation), choosing full trim mode over partial, and avoiding generic instantiation explosion by limiting how many distinct value- type arguments a hot generic type or method is used with.

XML
<PropertyGroup>
  <PublishAot>true</PublishAot>
  <InvariantGlobalization>true</InvariantGlobalization>
  <UseSystemResourceKeys>true</UseSystemResourceKeys>
  <EventSourceSupport>false</EventSourceSupport>
  <HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
  <StackTraceSupport>false</StackTraceSupport>
  <TrimMode>full</TrimMode>
</PropertyGroup>

InvariantGlobalization is usually the single largest win: it drops the ICU globalization data the runtime otherwise carries for culture-aware string comparison and formatting, at the cost of losing culture-specific behavior — fine for services that only ever need ordinal comparisons and invariant formatting, wrong for anything that must sort or format text correctly for end users in multiple locales. UseSystemResourceKeys replaces full exception message strings with short resource keys, trading debuggability for size — worth it for a tiny CLI tool, usually not worth it for a service where you need readable exception messages in logs. The diagnostic feature switches (EventSourceSupport, HttpActivityPropagationSupport, StackTraceSupport) each remove a slice of the runtime's built-in observability machinery, so they're a real trade-off against production debuggability, not a free size win — disable them deliberately, having confirmed you have equivalent observability another way (your own structured logging and tracing), not by default. On the generics side, a type like Dictionary<TKey, TValue> instantiated with many different value-type key/value combinations across a codebase generates a separate native code path per combination under AOT, so consolidating on a smaller set of concrete types in hot paths measurably shrinks output size.

What interviewers look for: knowledge of specific, real MSBuild switches and their actual trade-offs (not just "trimming makes it smaller"), and the judgment to flag that several of these switches cost observability, which shouldn't be disabled blindly.

Q8 What ASP.NET Core features work with Native AOT today, and what doesn't?#

Short answer: Minimal APIs are the fully supported, recommended model for Native AOT ASP.NET Core apps, along with gRPC, JWT authentication, CORS, health checks, output caching, rate limiting, response compression and WebSockets; MVC controllers, Razor Pages and Blazor Server are not supported, and JSON handling must go through source-generated JsonSerializerContext types rather than the default reflection-based serializer.

C#
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.ConfigureHttpJsonOptions(o => o.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default));

var app = builder.Build();
app.MapGet("/orders/{id:int}", (int id, IOrderStore store) => store.Find(id) is { } o ? Results.Ok(o) : Results.NotFound());
app.Run();

The reason MVC and Razor Pages aren't supported comes down to how deeply those frameworks rely on runtime reflection and dynamic code generation internally — controller and view discovery, model binding, Razor's runtime compilation of views all lean on patterns Native AOT explicitly removes, and porting them would mean re-architecting large parts of those frameworks rather than annotating a few call sites. Minimal APIs were designed with this constraint in mind from early on, which is why they're the supported path: route handlers are ordinary delegates the AOT compiler can see statically, and request/response bodies go through the JsonSerializerContext you register explicitly instead of a runtime-reflected serializer. This has a real design consequence for AOT-first services: your API surface ends up looking like a set of small, explicit endpoint delegates with explicitly registered JSON contracts, rather than convention-based controllers with implicit model binding — more upfront declaration, but every piece of it is something the AOT compiler (and a code reviewer) can see directly instead of inferring from convention and reflection at run time.

What interviewers look for: the specific supported/unsupported feature split, the why behind MVC's exclusion (reflection-heavy internals, not an arbitrary restriction), and the API-design implication for teams building AOT-first services.

Q9 How does Native AOT change the calculus for serverless or Lambda-style .NET functions?#

Short answer: Serverless platforms bill and measure success largely by cold-start latency and memory footprint — exactly the two dimensions Native AOT improves most, since there's no JIT warm-up and no framework assemblies to load beyond what's actually reachable — which makes AOT one of the highest- leverage deployment choices available for .NET functions that scale from zero and need to respond quickly on a cold invocation.

A JIT-compiled function on a serverless platform pays a JIT-warm-up tax on every cold start: the host process starts, the runtime initializes, and the methods on the critical path get JIT-compiled the first time they're called, all before the function can serve its first invocation. A Native AOT-published function skips essentially all of that — the executable already contains natively compiled code, so startup is much closer to a native binary launching than a managed runtime spinning up, which directly reduces the latency a caller sees on a cold invocation and the compute time a serverless platform bills for. This benefit compounds with a platform's scale-to-zero behavior: a function that scales down to no running instances between bursts of traffic pays the cold-start cost on every burst's first request, so shrinking that cost has an outsized effect on tail latency for spiky, infrequent workloads compared to a steadily warm, long-running service where JIT warm-up is a one-time cost amortized over a long uptime. The trade-off is the same AOT restrictions discussed earlier — no dynamic plugin loading, reflection- heavy dependencies need auditing — which is usually an easier constraint to accept for a small, focused function than for a large monolith.

What interviewers look for: the connection between AOT's specific benefits (no JIT warm-up, smaller footprint) and serverless's specific cost model (cold start latency, scale-to-zero), not just "AOT is good for serverless" as an assertion.

Q10 A Native AOT-published app throws NotSupportedException that never happened under JIT. Diagnose it.#

Short answer: This is almost always a reflection-, dynamic-code-, or dynamic-loading-dependent code path that the JIT tolerated at run time but that either wasn't exercised during development (so no trim warning ever fired) or whose warning was suppressed or missed — the fix is to reproduce the failure with a full, unsuppressed trim/AOT analysis build, find the exact call site from the exception's stack trace, and either replace it with a statically analyzable alternative or, if unavoidable, isolate it outside the AOT-published component.

The diagnostic sequence: first, get the actual stack trace from the AOT-published failure — unlike a generic "it doesn't work," NotSupportedException from Native AOT almost always names the exact runtime capability that's missing, which usually points directly at the offending call. Second, rebuild the same project with full trim and AOT analysis enabled (IsAotCompatible=true) rather than relying on prior JIT-based testing, since a rarely exercised path — an error-handling branch, a feature flag, a type only built via specific configuration — can go untested until it hits production under AOT; this is exactly why an AOT-published smoke test in CI matters, because manual testing under dotnet run will never catch it. Third, once you've found the pattern (commonly Activator.CreateInstance with a configured type name, a reflection-based mapper, or a DI container doing runtime proxy generation), decide whether to replace it with a source-generator-based or statically-typed alternative, or — if the dependency genuinely can't be made AOT-safe in time — fall that one component back to a framework- dependent deployment while the rest of the system stays AOT, rather than blocking the whole migration on one stubborn dependency.

What interviewers look for: a methodical diagnosis grounded in the exception's specifics plus a proper AOT-analysis rebuild, rather than guessing, and a pragmatic fallback (partial AOT adoption) as a legitimate option under time pressure.

Quick-Fire Round#

QuestionAnswer
Is there a JIT present in a Native AOT-published app?No — everything is compiled to native code ahead of time.
Does the dynamic keyword work under Native AOT?No — its binder relies on runtime code generation.
Which warning family flags trimming issues versus AOT-specific runtime-codegen issues?IL2xxx for trimming, IL3xxx / RequiresDynamicCode for AOT.
What MSBuild property enables both trim and AOT analysis at once?IsAotCompatible.
What typically replaces reflection-based JSON serialization for AOT?A generated JsonSerializerContext.
Which single switch usually shrinks AOT output the most?InvariantGlobalization.
Are MVC controllers supported under ASP.NET Core Native AOT?No — only Minimal APIs are fully supported today.
Why does AOT help serverless cold starts so much?No JIT warm-up cost on a cold invocation.

How to Prepare#

  • Take a real library you use and turn on IsAotCompatible; work through the actual warnings instead of reading about them, since the warning messages themselves are excellent interview material.
  • Publish the same small app three ways — framework-dependent, self-contained and Native AOT — and compare startup time and output size directly.
  • Deliberately trigger a RequiresDynamicCode warning and a RequiresUnreferencedCode warning so you can explain the difference from firsthand experience, not just definitions.
  • Build a minimal API with System.Text.Json source generation end to end, including the JsonSerializerContext registration, so you can write it from memory.
  • Be ready to argue both sides: a strong case for Native AOT and a strong case against it for a given scenario, since interviewers often ask you to argue the position you didn't pick first.